import { createErrorResponse, ToolResult } from '@/common/tool-handler'; import { BaseBrowserToolExecutor } from '../base-browser'; import { TOOL_NAMES } from 'chrome-mcp-shared'; import { TOOL_MESSAGE_TYPES } from '@/common/message-types'; import { TIMEOUTS, ERROR_MESSAGES } from '@/common/constants'; interface Coordinates { x: number; y: number; } interface ClickToolParams { selector?: string; // CSS selector or XPath for the element to click selectorType?: 'css' | 'xpath'; // Type of selector (default: 'css') ref?: string; // Element ref from accessibility tree (window.__claudeElementMap) coordinates?: Coordinates; // Coordinates to click at (x, y relative to viewport) waitForNavigation?: boolean; // Whether to wait for navigation to complete after click timeout?: number; // Timeout in milliseconds for waiting for the element or navigation frameId?: number; // Target frame for ref/selector resolution double?: boolean; // Perform double click when true button?: 'left' | 'right' | 'middle'; bubbles?: boolean; cancelable?: boolean; modifiers?: { altKey?: boolean; ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean }; tabId?: number; // target existing tab id windowId?: number; // when no tabId, pick active tab from this window } /** * Tool for clicking elements on web pages */ class ClickTool extends BaseBrowserToolExecutor { name = TOOL_NAMES.BROWSER.CLICK; /** * Execute click operation */ async execute(args: ClickToolParams): Promise { const { selector, selectorType = 'css', coordinates, waitForNavigation = false, timeout = TIMEOUTS.DEFAULT_WAIT * 5, frameId, button, bubbles, cancelable, modifiers, } = args; console.log(`Starting click operation with options:`, args); if (!selector && !coordinates && !args.ref) { return createErrorResponse( ERROR_MESSAGES.INVALID_PARAMETERS + ': Provide ref or selector or coordinates', ); } try { // Resolve tab const explicit = await this.tryGetTab(args.tabId); const tab = explicit || (await this.getActiveTabOrThrowInWindow(args.windowId)); if (!tab.id) { return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID'); } let finalRef = args.ref; let finalSelector = selector; // If selector is XPath, convert to ref first if (selector && selectorType === 'xpath') { await this.injectContentScript(tab.id, ['inject-scripts/accessibility-tree-helper.js']); try { const resolved = await this.sendMessageToTab( tab.id, { action: TOOL_MESSAGE_TYPES.ENSURE_REF_FOR_SELECTOR, selector, isXPath: true, }, frameId, ); if (resolved && resolved.success && resolved.ref) { finalRef = resolved.ref; finalSelector = undefined; // Use ref instead of selector } else { return createErrorResponse( `Failed to resolve XPath selector: ${resolved?.error || 'unknown error'}`, ); } } catch (error) { return createErrorResponse( `Error resolving XPath: ${error instanceof Error ? error.message : String(error)}`, ); } } await this.injectContentScript(tab.id, ['inject-scripts/click-helper.js']); // Send click message to content script const result = await this.sendMessageToTab( tab.id, { action: TOOL_MESSAGE_TYPES.CLICK_ELEMENT, selector: finalSelector, coordinates, ref: finalRef, waitForNavigation, timeout, double: args.double === true, button, bubbles, cancelable, modifiers, }, frameId, ); // Determine actual click method used let clickMethod: string; if (coordinates) { clickMethod = 'coordinates'; } else if (finalRef) { clickMethod = 'ref'; } else if (finalSelector) { clickMethod = 'selector'; } else { clickMethod = 'unknown'; } return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: result.message || 'Click operation successful', elementInfo: result.elementInfo, navigationOccurred: result.navigationOccurred, clickMethod, }), }, ], isError: false, }; } catch (error) { console.error('Error in click operation:', error); return createErrorResponse( `Error performing click: ${error instanceof Error ? error.message : String(error)}`, ); } } } export const clickTool = new ClickTool(); interface FillToolParams { selector?: string; selectorType?: 'css' | 'xpath'; // Type of selector (default: 'css') ref?: string; // Element ref from accessibility tree // Accept string | number | boolean for broader form input coverage value: string | number | boolean; frameId?: number; tabId?: number; // target existing tab id windowId?: number; // when no tabId, pick active tab from this window } /** * Tool for filling form elements on web pages */ class FillTool extends BaseBrowserToolExecutor { name = TOOL_NAMES.BROWSER.FILL; /** * Execute fill operation */ async execute(args: FillToolParams): Promise { const { selector, selectorType = 'css', ref, value, frameId } = args; console.log(`Starting fill operation with options:`, args); if (!selector && !ref) { return createErrorResponse(ERROR_MESSAGES.INVALID_PARAMETERS + ': Provide ref or selector'); } if (value === undefined || value === null) { return createErrorResponse(ERROR_MESSAGES.INVALID_PARAMETERS + ': Value must be provided'); } try { const explicit = await this.tryGetTab(args.tabId); const tab = explicit || (await this.getActiveTabOrThrowInWindow(args.windowId)); if (!tab.id) { return createErrorResponse(ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID'); } let finalRef = ref; let finalSelector = selector; // If selector is XPath, convert to ref first if (selector && selectorType === 'xpath') { await this.injectContentScript(tab.id, ['inject-scripts/accessibility-tree-helper.js']); try { const resolved = await this.sendMessageToTab( tab.id, { action: TOOL_MESSAGE_TYPES.ENSURE_REF_FOR_SELECTOR, selector, isXPath: true, }, frameId, ); if (resolved && resolved.success && resolved.ref) { finalRef = resolved.ref; finalSelector = undefined; // Use ref instead of selector } else { return createErrorResponse( `Failed to resolve XPath selector: ${resolved?.error || 'unknown error'}`, ); } } catch (error) { return createErrorResponse( `Error resolving XPath: ${error instanceof Error ? error.message : String(error)}`, ); } } await this.injectContentScript(tab.id, ['inject-scripts/fill-helper.js']); // Send fill message to content script const result = await this.sendMessageToTab( tab.id, { action: TOOL_MESSAGE_TYPES.FILL_ELEMENT, selector: finalSelector, ref: finalRef, value, }, frameId, ); if (result && result.error) { return createErrorResponse(result.error); } return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: result.message || 'Fill operation successful', elementInfo: result.elementInfo, }), }, ], isError: false, }; } catch (error) { console.error('Error in fill operation:', error); return createErrorResponse( `Error filling element: ${error instanceof Error ? error.message : String(error)}`, ); } } } export const fillTool = new FillTool();