hangwin--mcp-chrome
270 行
8.2 KiB
TypeScript
270 行
8.2 KiB
TypeScript
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<ToolResult> {
|
|
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<ToolResult> {
|
|
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();
|