/** * UID-based input interaction tools * Require valid UIDs from take_snapshot */ import { successResponse, errorResponse } from '../utils/response-helpers.js'; import { handleUidError } from '../utils/uid-helpers.js'; import { onTabOrFocused } from '../utils/tab-routing.js'; import type { McpToolResponse } from '../types/common.js'; // Tool definitions export const clickByUidTool = { name: 'click_by_uid', description: 'Click element by UID. Runs on your own tab in the background, so it does not disturb what is on screen.', inputSchema: { type: 'object', properties: { uid: { type: 'string', description: 'Element UID from snapshot', }, dblClick: { type: 'boolean', description: 'Double-click (default: false)', }, }, required: ['uid'], }, }; export const hoverByUidTool = { name: 'hover_by_uid', description: 'Hover over element by UID. Runs in the background on your own tab.', inputSchema: { type: 'object', properties: { uid: { type: 'string', description: 'Element UID from snapshot', }, }, required: ['uid'], }, }; export const fillByUidTool = { name: 'fill_by_uid', description: 'Fill text input/textarea by UID, typing as a person would. Runs in the background on your own tab.', inputSchema: { type: 'object', properties: { uid: { type: 'string', description: 'Input element UID from snapshot', }, value: { type: 'string', description: 'Text to fill', }, }, required: ['uid', 'value'], }, }; export const dragByUidToUidTool = { name: 'drag_by_uid_to_uid', description: 'Drag element to another (HTML5 drag events). Runs in the background on your own tab.', inputSchema: { type: 'object', properties: { fromUid: { type: 'string', description: 'Source element UID', }, toUid: { type: 'string', description: 'Target element UID', }, }, required: ['fromUid', 'toUid'], }, }; export const fillFormByUidTool = { name: 'fill_form_by_uid', description: 'Fill multiple form fields at once. Runs in the background on your own tab.', inputSchema: { type: 'object', properties: { elements: { type: 'array', description: 'Array of {uid, value} pairs', items: { type: 'object', properties: { uid: { type: 'string', description: 'Field UID', }, value: { type: 'string', description: 'Field value', }, }, required: ['uid', 'value'], }, }, }, required: ['elements'], }, }; export const uploadFileByUidTool = { name: 'upload_file_by_uid', description: 'Upload file to file input by UID. The path is read by the browser, so it must exist inside the browser container. Runs in the background on your own tab.', inputSchema: { type: 'object', properties: { uid: { type: 'string', description: 'File input UID from snapshot', }, filePath: { type: 'string', description: 'Local file path', }, }, required: ['uid', 'filePath'], }, }; // Handlers export async function handleClickByUid(args: unknown): Promise { try { const { uid, dblClick, tab } = args as { uid: string; dblClick?: boolean; tab?: string }; if (!uid || typeof uid !== 'string') { throw new Error('uid parameter is required and must be a string'); } const { getFirefox } = await import('../index.js'); const firefox = await getFirefox(); try { await onTabOrFocused( firefox, tab, (tabId) => firefox.clickUidInTab(tabId, uid, dblClick), () => firefox.clickByUid(uid, dblClick) ); return successResponse(`✅ ${dblClick ? 'dblclick' : 'click'} ${uid}`); } catch (error) { throw handleUidError(error as Error, uid); } } catch (error) { return errorResponse(error as Error); } } export async function handleHoverByUid(args: unknown): Promise { try { const { uid, tab } = args as { uid: string; tab?: string }; if (!uid || typeof uid !== 'string') { throw new Error('uid parameter is required and must be a string'); } const { getFirefox } = await import('../index.js'); const firefox = await getFirefox(); try { await onTabOrFocused( firefox, tab, (tabId) => firefox.hoverUidInTab(tabId, uid), () => firefox.hoverByUid(uid) ); return successResponse(`✅ hover ${uid}`); } catch (error) { throw handleUidError(error as Error, uid); } } catch (error) { return errorResponse(error as Error); } } export async function handleFillByUid(args: unknown): Promise { try { const { uid, value, tab } = args as { uid: string; value: string; tab?: string }; if (!uid || typeof uid !== 'string') { throw new Error('uid parameter is required and must be a string'); } if (value === undefined || typeof value !== 'string') { throw new Error('value parameter is required and must be a string'); } const { getFirefox } = await import('../index.js'); const firefox = await getFirefox(); try { await onTabOrFocused( firefox, tab, (tabId) => firefox.fillUidInTab(tabId, uid, value), () => firefox.fillByUid(uid, value) ); return successResponse(`✅ fill ${uid}`); } catch (error) { throw handleUidError(error as Error, uid); } } catch (error) { return errorResponse(error as Error); } } export async function handleDragByUidToUid(args: unknown): Promise { try { const { fromUid, toUid, tab } = args as { fromUid: string; toUid: string; tab?: string }; if (!fromUid || typeof fromUid !== 'string') { throw new Error('fromUid parameter is required and must be a string'); } if (!toUid || typeof toUid !== 'string') { throw new Error('toUid parameter is required and must be a string'); } const { getFirefox } = await import('../index.js'); const firefox = await getFirefox(); try { await onTabOrFocused( firefox, tab, (tabId) => firefox.dragUidToUidInTab(tabId, fromUid, toUid), () => firefox.dragByUidToUid(fromUid, toUid) ); return successResponse(`✅ drag ${fromUid}→${toUid}`); } catch (error) { // Check both UIDs for staleness const errorMsg = (error as Error).message; if (errorMsg.includes('stale') || errorMsg.includes('Snapshot') || errorMsg.includes('UID')) { throw new Error(`UIDs stale/invalid. Call take_snapshot first.`); } throw error; } } catch (error) { return errorResponse(error as Error); } } export async function handleFillFormByUid(args: unknown): Promise { try { const { elements, tab } = args as { elements: Array<{ uid: string; value: string }>; tab?: string; }; if (!elements || !Array.isArray(elements) || elements.length === 0) { throw new Error('elements parameter is required and must be a non-empty array'); } // Validate all elements for (const el of elements) { if (!el.uid || typeof el.uid !== 'string') { throw new Error(`Invalid element: uid is required and must be a string`); } if (el.value === undefined || typeof el.value !== 'string') { throw new Error(`Invalid element for uid "${el.uid}": value must be a string`); } } const { getFirefox } = await import('../index.js'); const firefox = await getFirefox(); try { await onTabOrFocused( firefox, tab, (tabId) => firefox.fillFormUidInTab(tabId, elements), () => firefox.fillFormByUid(elements) ); return successResponse(`✅ filled ${elements.length} fields`); } catch (error) { const errorMsg = (error as Error).message; if (errorMsg.includes('stale') || errorMsg.includes('Snapshot') || errorMsg.includes('UID')) { throw new Error(`UIDs stale/invalid. Call take_snapshot first.`); } throw error; } } catch (error) { return errorResponse(error as Error); } } export async function handleUploadFileByUid(args: unknown): Promise { try { const { uid, filePath, tab } = args as { uid: string; filePath: string; tab?: string }; if (!uid || typeof uid !== 'string') { throw new Error('uid parameter is required and must be a string'); } if (!filePath || typeof filePath !== 'string') { throw new Error('filePath parameter is required and must be a string'); } const { getFirefox } = await import('../index.js'); const firefox = await getFirefox(); try { await onTabOrFocused( firefox, tab, (tabId) => firefox.uploadFileUidInTab(tabId, uid, filePath), () => firefox.uploadFileByUid(uid, filePath) ); return successResponse(`✅ upload ${uid}`); } catch (error) { const errorMsg = (error as Error).message; // Check for UID staleness if (errorMsg.includes('stale') || errorMsg.includes('Snapshot') || errorMsg.includes('UID')) { throw handleUidError(error as Error, uid); } // Check for file input specific errors if (errorMsg.includes('not a file input') || errorMsg.includes('type="file"')) { throw new Error(`${uid} is not a file input`); } if (errorMsg.includes('hidden') || errorMsg.includes('not visible')) { throw new Error(`${uid} is hidden/not interactable`); } throw error; } } catch (error) { return errorResponse(error as Error); } }