UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

1,257 lines (1,217 loc) 41.5 kB
/** * @claude-flow/browser - MCP Tools * 50+ browser automation tools for claude-flow MCP server */ import { AgentBrowserAdapter } from '../infrastructure/agent-browser-adapter.js'; import type { ActionResult, Snapshot } from '../domain/types.js'; // Session registry for multi-agent coordination const sessions = new Map<string, AgentBrowserAdapter>(); function getAdapter(sessionId?: string): AgentBrowserAdapter { const id = sessionId || 'default'; if (!sessions.has(id)) { sessions.set(id, new AgentBrowserAdapter({ session: id })); } return sessions.get(id)!; } export interface MCPTool { name: string; description: string; category: string; inputSchema: { type: 'object'; properties: Record<string, unknown>; required?: string[]; }; handler: (input: Record<string, unknown>) => Promise<unknown>; } // ============================================================================ // Navigation Tools // ============================================================================ const navigationTools: MCPTool[] = [ { name: 'browser/open', description: 'Navigate to a URL. Returns page title and final URL after redirects.', category: 'browser-navigation', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL to navigate to' }, session: { type: 'string', description: 'Session ID for isolated browser instance' }, waitUntil: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'], description: 'When to consider navigation complete', }, headers: { type: 'object', description: 'HTTP headers to set (scoped to URL origin)', additionalProperties: { type: 'string' }, }, }, required: ['url'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.open({ url: input.url as string, waitUntil: input.waitUntil as 'load' | 'domcontentloaded' | 'networkidle', headers: input.headers as Record<string, string>, }); }, }, { name: 'browser/back', description: 'Navigate back in browser history', category: 'browser-navigation', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.back(); }, }, { name: 'browser/forward', description: 'Navigate forward in browser history', category: 'browser-navigation', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.forward(); }, }, { name: 'browser/reload', description: 'Reload the current page', category: 'browser-navigation', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.reload(); }, }, { name: 'browser/close', description: 'Close the browser session', category: 'browser-navigation', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); const result = await adapter.close(); sessions.delete(input.session as string || 'default'); return result; }, }, ]; // ============================================================================ // Snapshot Tools (AI-Optimized) // ============================================================================ const snapshotTools: MCPTool[] = [ { name: 'browser/snapshot', description: 'Get accessibility tree with element refs (@e1, @e2). Best for AI - use refs to interact with elements. Returns structured tree with interactive elements highlighted.', category: 'browser-snapshot', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, interactive: { type: 'boolean', description: 'Only show interactive elements (buttons, links, inputs)', default: true }, compact: { type: 'boolean', description: 'Remove empty structural elements', default: true }, depth: { type: 'number', description: 'Limit tree depth (e.g., 3 levels)' }, selector: { type: 'string', description: 'Scope snapshot to CSS selector' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.snapshot({ interactive: input.interactive !== false, compact: input.compact !== false, depth: input.depth as number, selector: input.selector as string, }); }, }, { name: 'browser/screenshot', description: 'Capture screenshot. Returns base64 PNG if no path specified.', category: 'browser-snapshot', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, path: { type: 'string', description: 'File path to save (optional, returns base64 if omitted)' }, fullPage: { type: 'boolean', description: 'Capture full scrollable page', default: false }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.screenshot({ path: input.path as string, fullPage: input.fullPage as boolean, }); }, }, { name: 'browser/pdf', description: 'Save page as PDF', category: 'browser-snapshot', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, path: { type: 'string', description: 'File path to save PDF' }, }, required: ['path'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.pdf(input.path as string); }, }, ]; // ============================================================================ // Interaction Tools // ============================================================================ const interactionTools: MCPTool[] = [ { name: 'browser/click', description: 'Click an element. Use @e1 refs from snapshot or CSS selectors.', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, button: { type: 'string', enum: ['left', 'right', 'middle'], default: 'left' }, clickCount: { type: 'number', description: 'Number of clicks (2 for double-click)' }, force: { type: 'boolean', description: 'Force click even if element is not visible' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.click({ target: input.target as string, button: input.button as 'left' | 'right' | 'middle', clickCount: input.clickCount as number, force: input.force as boolean, }); }, }, { name: 'browser/fill', description: 'Clear and fill an input field. Use @e1 refs from snapshot.', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, value: { type: 'string', description: 'Text to fill' }, force: { type: 'boolean', description: 'Force fill even if element is not visible' }, }, required: ['target', 'value'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.fill({ target: input.target as string, value: input.value as string, force: input.force as boolean, }); }, }, { name: 'browser/type', description: 'Type text character by character (with key events). Slower than fill but simulates real typing.', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, text: { type: 'string', description: 'Text to type' }, delay: { type: 'number', description: 'Delay between keystrokes in ms' }, }, required: ['target', 'text'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.type({ target: input.target as string, text: input.text as string, delay: input.delay as number, }); }, }, { name: 'browser/press', description: 'Press a keyboard key (Enter, Tab, Escape, Control+a, etc.)', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, key: { type: 'string', description: 'Key to press (Enter, Tab, Control+a, etc.)' }, delay: { type: 'number', description: 'Key hold duration in ms' }, }, required: ['key'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.press(input.key as string, input.delay as number); }, }, { name: 'browser/hover', description: 'Hover over an element', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.hover(input.target as string); }, }, { name: 'browser/select', description: 'Select dropdown option by value', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, value: { type: 'string', description: 'Option value to select' }, }, required: ['target', 'value'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.select(input.target as string, input.value as string); }, }, { name: 'browser/check', description: 'Check a checkbox', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.check(input.target as string); }, }, { name: 'browser/uncheck', description: 'Uncheck a checkbox', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.uncheck(input.target as string); }, }, { name: 'browser/scroll', description: 'Scroll the page or element', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, direction: { type: 'string', enum: ['up', 'down', 'left', 'right'], description: 'Scroll direction' }, pixels: { type: 'number', description: 'Pixels to scroll (default: viewport height)' }, }, required: ['direction'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.scroll( input.direction as 'up' | 'down' | 'left' | 'right', input.pixels as number ); }, }, { name: 'browser/upload', description: 'Upload files to a file input', category: 'browser-interaction', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, files: { type: 'array', items: { type: 'string' }, description: 'File paths to upload' }, }, required: ['target', 'files'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.upload(input.target as string, input.files as string[]); }, }, ]; // ============================================================================ // Get Info Tools // ============================================================================ const getInfoTools: MCPTool[] = [ { name: 'browser/get-text', description: 'Get text content of an element', category: 'browser-info', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.getText(input.target as string); }, }, { name: 'browser/get-html', description: 'Get innerHTML of an element', category: 'browser-info', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.getHtml(input.target as string); }, }, { name: 'browser/get-value', description: 'Get value of an input element', category: 'browser-info', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.getValue(input.target as string); }, }, { name: 'browser/get-attr', description: 'Get an attribute value from an element', category: 'browser-info', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, attribute: { type: 'string', description: 'Attribute name (href, src, data-*, etc.)' }, }, required: ['target', 'attribute'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.getAttr(input.target as string, input.attribute as string); }, }, { name: 'browser/get-title', description: 'Get the page title', category: 'browser-info', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.getTitle(); }, }, { name: 'browser/get-url', description: 'Get the current page URL', category: 'browser-info', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.getUrl(); }, }, { name: 'browser/get-count', description: 'Count elements matching a selector', category: 'browser-info', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, selector: { type: 'string', description: 'CSS selector to count' }, }, required: ['selector'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.getCount(input.selector as string); }, }, ]; // ============================================================================ // State Check Tools // ============================================================================ const stateTools: MCPTool[] = [ { name: 'browser/is-visible', description: 'Check if an element is visible', category: 'browser-state', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.isVisible(input.target as string); }, }, { name: 'browser/is-enabled', description: 'Check if an element is enabled', category: 'browser-state', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.isEnabled(input.target as string); }, }, { name: 'browser/is-checked', description: 'Check if a checkbox is checked', category: 'browser-state', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.isChecked(input.target as string); }, }, ]; // ============================================================================ // Wait Tools // ============================================================================ const waitTools: MCPTool[] = [ { name: 'browser/wait', description: 'Wait for element, time, text, URL, or load state', category: 'browser-wait', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, selector: { type: 'string', description: 'Wait for element to be visible' }, timeout: { type: 'number', description: 'Wait for milliseconds' }, text: { type: 'string', description: 'Wait for text to appear on page' }, url: { type: 'string', description: 'Wait for URL pattern (glob)' }, load: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'], description: 'Wait for load state' }, fn: { type: 'string', description: 'Wait for JavaScript condition to be true' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.wait({ selector: input.selector as string, timeout: input.timeout as number, text: input.text as string, url: input.url as string, load: input.load as 'load' | 'domcontentloaded' | 'networkidle', fn: input.fn as string, }); }, }, ]; // ============================================================================ // JavaScript Execution // ============================================================================ // Defense-in-depth: pattern blocklist for eval scripts (CRIT-03) // NOTE: This is a best-effort defense layer, not a sandbox. Determined attackers can bypass // pattern matching via encoding/obfuscation. The primary defense is the browser sandbox itself. // This blocklist catches accidental misuse and unsophisticated injection attempts. const DANGEROUS_EVAL_PATTERNS = [ /\bprocess\b/, // Node.js process access /\brequire\b/, // CommonJS require /\b__dirname\b/, // Node path leaking /\b__filename\b/, // Node path leaking /\bchild_process\b/, // Command execution /\bglobal\b\s*\./, // Global object mutation /\bglobalThis\b/, // globalThis access (bypasses global. check) /\bFunction\s*\(/, // Function constructor (eval-equivalent) /\.constructor\b/, // Constructor access (e.g., "".constructor) /\bReflect\b/, // Reflect API (can invoke constructors) /\bimport\s*\(/, // Dynamic import /\beval\s*\(/, // Direct eval calls ]; const DEFAULT_MAX_EVAL_SCRIPT_LENGTH = 20_000; const MAX_EVAL_SCRIPT_LENGTH = parseInt(process.env.CLAUDE_FLOW_MAX_EVAL_SCRIPT_LENGTH || '', 10) || DEFAULT_MAX_EVAL_SCRIPT_LENGTH; const evalTools: MCPTool[] = [ { name: 'browser/eval', description: 'Execute JavaScript in the page context (validated, length-limited)', category: 'browser-eval', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, script: { type: 'string', description: `JavaScript code to execute (max ${MAX_EVAL_SCRIPT_LENGTH} chars)`, maxLength: MAX_EVAL_SCRIPT_LENGTH, }, }, required: ['script'], }, handler: async (input) => { const script = input.script as string; // Validate script length if (!script || script.length === 0) { throw new Error('browser/eval: script must not be empty'); } if (script.length > MAX_EVAL_SCRIPT_LENGTH) { throw new Error(`browser/eval: script exceeds maximum length of ${MAX_EVAL_SCRIPT_LENGTH} characters`); } // Check for dangerous patterns for (const pattern of DANGEROUS_EVAL_PATTERNS) { if (pattern.test(script)) { throw new Error(`browser/eval: script contains disallowed pattern: ${pattern.source}`); } } // Audit log console.info(`[browser/eval] Executing script (${script.length} chars) in session ${input.session || 'default'}`); const adapter = getAdapter(input.session as string); return adapter.eval({ script }); }, }, ]; // ============================================================================ // Storage Tools // ============================================================================ const storageTools: MCPTool[] = [ { name: 'browser/cookies-get', description: 'Get all cookies for the current page', category: 'browser-storage', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.getCookies(); }, }, { name: 'browser/cookies-set', description: 'Set a cookie', category: 'browser-storage', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, name: { type: 'string', description: 'Cookie name' }, value: { type: 'string', description: 'Cookie value' }, }, required: ['name', 'value'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.setCookie(input.name as string, input.value as string); }, }, { name: 'browser/cookies-clear', description: 'Clear all cookies', category: 'browser-storage', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.clearCookies(); }, }, { name: 'browser/localstorage-get', description: 'Get localStorage value (or all if no key)', category: 'browser-storage', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, key: { type: 'string', description: 'Key to get (omit for all)' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.getLocalStorage(input.key as string); }, }, { name: 'browser/localstorage-set', description: 'Set localStorage value', category: 'browser-storage', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, key: { type: 'string', description: 'Key to set' }, value: { type: 'string', description: 'Value to set' }, }, required: ['key', 'value'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.setLocalStorage(input.key as string, input.value as string); }, }, ]; // ============================================================================ // Network Tools // ============================================================================ const networkTools: MCPTool[] = [ { name: 'browser/network-route', description: 'Intercept, block, or mock network requests', category: 'browser-network', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, urlPattern: { type: 'string', description: 'URL pattern to match (glob)' }, abort: { type: 'boolean', description: 'Block matching requests' }, body: { type: 'string', description: 'Mock response body (JSON string)' }, status: { type: 'number', description: 'Mock response status code' }, }, required: ['urlPattern'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.networkRoute({ urlPattern: input.urlPattern as string, abort: input.abort as boolean, body: input.body as string, status: input.status as number, }); }, }, { name: 'browser/network-unroute', description: 'Remove network route', category: 'browser-network', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, urlPattern: { type: 'string', description: 'URL pattern to remove (omit for all)' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.networkUnroute(input.urlPattern as string); }, }, { name: 'browser/network-requests', description: 'Get tracked network requests', category: 'browser-network', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, filter: { type: 'string', description: 'Filter by URL substring' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.networkRequests(input.filter as string); }, }, ]; // ============================================================================ // Tab & Session Tools // ============================================================================ const tabTools: MCPTool[] = [ { name: 'browser/tab-list', description: 'List all open tabs', category: 'browser-tabs', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.listTabs(); }, }, { name: 'browser/tab-new', description: 'Open a new tab', category: 'browser-tabs', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, url: { type: 'string', description: 'URL to open in new tab' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.newTab(input.url as string); }, }, { name: 'browser/tab-switch', description: 'Switch to a specific tab', category: 'browser-tabs', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, index: { type: 'number', description: 'Tab index (0-based)' }, }, required: ['index'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.switchTab(input.index as number); }, }, { name: 'browser/tab-close', description: 'Close a tab', category: 'browser-tabs', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, index: { type: 'number', description: 'Tab index to close (current if omitted)' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.closeTab(input.index as number); }, }, { name: 'browser/session-list', description: 'List all active browser sessions', category: 'browser-session', inputSchema: { type: 'object', properties: {}, }, handler: async () => { const adapter = getAdapter(); return adapter.listSessions(); }, }, ]; // ============================================================================ // Settings Tools // ============================================================================ const settingsTools: MCPTool[] = [ { name: 'browser/set-viewport', description: 'Set browser viewport size', category: 'browser-settings', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, width: { type: 'number', description: 'Viewport width' }, height: { type: 'number', description: 'Viewport height' }, }, required: ['width', 'height'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.setViewport(input.width as number, input.height as number); }, }, { name: 'browser/set-device', description: 'Emulate a device (iPhone 14, Pixel 5, etc.)', category: 'browser-settings', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, device: { type: 'string', description: 'Device name (e.g., "iPhone 14", "Pixel 5")' }, }, required: ['device'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.setDevice(input.device as string); }, }, { name: 'browser/set-geolocation', description: 'Set geolocation', category: 'browser-settings', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, latitude: { type: 'number', description: 'Latitude' }, longitude: { type: 'number', description: 'Longitude' }, }, required: ['latitude', 'longitude'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.setGeolocation(input.latitude as number, input.longitude as number); }, }, { name: 'browser/set-offline', description: 'Toggle offline mode', category: 'browser-settings', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, enabled: { type: 'boolean', description: 'Enable offline mode' }, }, required: ['enabled'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.setOffline(input.enabled as boolean); }, }, { name: 'browser/set-media', description: 'Emulate color scheme (dark/light mode)', category: 'browser-settings', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, scheme: { type: 'string', enum: ['dark', 'light'], description: 'Color scheme' }, }, required: ['scheme'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.setMedia(input.scheme as 'dark' | 'light'); }, }, ]; // ============================================================================ // Debug Tools // ============================================================================ const debugTools: MCPTool[] = [ { name: 'browser/trace-start', description: 'Start recording a trace for debugging', category: 'browser-debug', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, path: { type: 'string', description: 'Path to save trace' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.traceStart(input.path as string); }, }, { name: 'browser/trace-stop', description: 'Stop recording trace and save', category: 'browser-debug', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, path: { type: 'string', description: 'Path to save trace' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.traceStop(input.path as string); }, }, { name: 'browser/console', description: 'Get console messages', category: 'browser-debug', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, clear: { type: 'boolean', description: 'Clear console after getting messages' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); if (input.clear) { return adapter.clearConsole(); } return adapter.getConsole(); }, }, { name: 'browser/errors', description: 'Get page errors', category: 'browser-debug', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, clear: { type: 'boolean', description: 'Clear errors after getting' }, }, }, handler: async (input) => { const adapter = getAdapter(input.session as string); if (input.clear) { return adapter.clearErrors(); } return adapter.getErrors(); }, }, { name: 'browser/highlight', description: 'Highlight an element on the page (for visual debugging)', category: 'browser-debug', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, target: { type: 'string', description: 'Element ref (@e1) or CSS selector' }, }, required: ['target'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.highlight(input.target as string); }, }, { name: 'browser/state-save', description: 'Save authentication state (cookies, localStorage) to file', category: 'browser-debug', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, path: { type: 'string', description: 'Path to save state file' }, }, required: ['path'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.saveState(input.path as string); }, }, { name: 'browser/state-load', description: 'Load authentication state from file', category: 'browser-debug', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, path: { type: 'string', description: 'Path to state file' }, }, required: ['path'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.loadState(input.path as string); }, }, ]; // ============================================================================ // Semantic Locator Tools (Find Commands) // ============================================================================ const findTools: MCPTool[] = [ { name: 'browser/find-role', description: 'Find element by ARIA role and perform action', category: 'browser-find', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, role: { type: 'string', description: 'ARIA role (button, link, textbox, etc.)' }, action: { type: 'string', enum: ['click', 'fill', 'check', 'hover', 'text'], description: 'Action to perform' }, name: { type: 'string', description: 'Accessible name to match' }, value: { type: 'string', description: 'Value for fill action' }, exact: { type: 'boolean', description: 'Exact text match' }, }, required: ['role', 'action'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.findByRole(input.role as string, input.action as string, { name: input.name as string, exact: input.exact as boolean, }); }, }, { name: 'browser/find-text', description: 'Find element by text content and perform action', category: 'browser-find', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, text: { type: 'string', description: 'Text to find' }, action: { type: 'string', enum: ['click', 'hover', 'text'], description: 'Action to perform' }, }, required: ['text', 'action'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.findByText(input.text as string, input.action as string); }, }, { name: 'browser/find-label', description: 'Find input by label and perform action', category: 'browser-find', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, label: { type: 'string', description: 'Label text' }, action: { type: 'string', enum: ['click', 'fill', 'check', 'hover', 'text'], description: 'Action to perform' }, value: { type: 'string', description: 'Value for fill action' }, }, required: ['label', 'action'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.findByLabel(input.label as string, input.action as string, input.value as string); }, }, { name: 'browser/find-testid', description: 'Find element by data-testid and perform action', category: 'browser-find', inputSchema: { type: 'object', properties: { session: { type: 'string', description: 'Session ID' }, testId: { type: 'string', description: 'data-testid value' }, action: { type: 'string', enum: ['click', 'fill', 'check', 'hover', 'text'], description: 'Action to perform' }, value: { type: 'string', description: 'Value for fill action' }, }, required: ['testId', 'action'], }, handler: async (input) => { const adapter = getAdapter(input.session as string); return adapter.findByTestId(input.testId as string, input.action as string, input.value as string); }, }, ]; // ============================================================================ // Export All Tools // ============================================================================ export const browserTools: MCPTool[] = [ ...navigationTools, ...snapshotTools, ...interactionTools, ...getInfoTools, ...stateTools, ...waitTools, ...evalTools, ...storageTools, ...networkTools, ...tabTools, ...settingsTools, ...debugTools, ...findTools, ]; export default browserTools;