UNPKG

@dorothywebb/any-browser-mcp

Version:

Any Browser MCP - Launch Chrome with your actual data in debug mode for comprehensive browser automation

823 lines (818 loc) 32.7 kB
/** * Comprehensive Browser Tools Implementation * Includes all requested browser automation tools with CDP implementations */ import { getConfigManager } from './ConfigManager.js'; /** * Ask for user confirmation for destructive actions */ async function askUserConfirmation(action, details) { // In a real implementation, this would show a dialog or prompt // For now, we'll simulate it by checking configuration const config = getConfigManager(); if (!config.getBrowserConfig().confirmDestructiveActions) { return true; // If confirmations are disabled, auto-approve } // This would be replaced with actual user interaction in a full implementation console.log(`⚠️ Confirmation required for: ${action}`); if (details) { console.log(` ${details}`); } console.log(` This action requires user confirmation. Proceeding with action.`); // TODO: Implement actual user confirmation dialog return true; } /** * All browser tools with comprehensive implementations */ export const BROWSER_TOOLS = [ { name: 'browser_navigate', description: 'Navigate to a URL', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL to navigate to' } }, required: ['url'] }, handler: async (args, browserManager) => { return await browserManager.navigate(args.url); } }, { name: 'browser_navigate_back', description: 'Navigate back in browser history', inputSchema: { type: 'object', properties: {} }, handler: async (args, browserManager) => { return browserManager.executeAction('navigate_back', async () => { await browserManager.sendCommand('Page.navigateToHistoryEntry', { entryId: -1 }); return { success: true, message: 'Navigated back in history', timestamp: new Date() }; }); } }, { name: 'browser_navigate_forward', description: 'Navigate forward in browser history', inputSchema: { type: 'object', properties: {} }, handler: async (args, browserManager) => { return browserManager.executeAction('navigate_forward', async () => { await browserManager.sendCommand('Page.navigateToHistoryEntry', { entryId: 1 }); return { success: true, message: 'Navigated forward in history', timestamp: new Date() }; }); } }, { name: 'browser_take_screenshot', description: 'Take a screenshot of the current page', inputSchema: { type: 'object', properties: { fullPage: { type: 'boolean', description: 'Capture full page', default: false }, format: { type: 'string', enum: ['png', 'jpeg'], default: 'png' }, quality: { type: 'number', minimum: 0, maximum: 100, description: 'JPEG quality (0-100)' } } }, handler: async (args, browserManager) => { return browserManager.executeAction('screenshot', async () => { const options = { format: args.format || 'png' }; if (args.format === 'jpeg' && args.quality) { options.quality = args.quality; } if (args.fullPage) { // Get full page dimensions const metrics = await browserManager.sendCommand('Page.getLayoutMetrics'); options.clip = { x: 0, y: 0, width: metrics.contentSize.width, height: metrics.contentSize.height, scale: 1 }; } const result = await browserManager.sendCommand('Page.captureScreenshot', options); return { success: true, message: 'Screenshot captured', data: result.data, timestamp: new Date() }; }); } }, { name: 'browser_snapshot', description: 'Take a snapshot (alias for screenshot)', inputSchema: { type: 'object', properties: { fullPage: { type: 'boolean', description: 'Capture full page', default: false } } }, handler: async (args, browserManager) => { // Reuse screenshot implementation return BROWSER_TOOLS.find(tool => tool.name === 'browser_take_screenshot') .handler(args, browserManager); } }, { name: 'browser_click', description: 'Click an element on the page', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector of element to click' }, button: { type: 'string', enum: ['left', 'right', 'middle'], default: 'left' }, clickCount: { type: 'number', default: 1, description: 'Number of clicks' }, delay: { type: 'number', description: 'Delay between mouse down and up in ms' } }, required: ['selector'] }, handler: async (args, browserManager) => { return await browserManager.click(args.selector, { button: args.button, clickCount: args.clickCount, delay: args.delay }); } }, { name: 'browser_hover', description: 'Hover over an element', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector of element to hover' } }, required: ['selector'] }, handler: async (args, browserManager) => { return browserManager.executeAction('hover', async () => { // Enable DOM await browserManager.sendCommand('DOM.enable'); // Get document and find element const doc = await browserManager.sendCommand('DOM.getDocument'); const node = await browserManager.sendCommand('DOM.querySelector', { nodeId: doc.root.nodeId, selector: args.selector }); if (!node.nodeId) { throw new Error(`Element not found: ${args.selector}`); } // Get element position const box = await browserManager.sendCommand('DOM.getBoxModel', { nodeId: node.nodeId }); const quad = box.model.content; const x = (quad[0] + quad[2]) / 2; const y = (quad[1] + quad[5]) / 2; // Move mouse to element await browserManager.sendCommand('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y }); return { success: true, message: `Hovered over element: ${args.selector}`, timestamp: new Date() }; }); } }, { name: 'browser_type', description: 'Type text into the focused element', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'Text to type' }, delay: { type: 'number', description: 'Delay between keystrokes in ms', default: 0 } }, required: ['text'] }, handler: async (args, browserManager) => { return await browserManager.type(args.text, { delay: args.delay }); } }, { name: 'browser_press_key', description: 'Press a specific key', inputSchema: { type: 'object', properties: { key: { type: 'string', description: 'Key to press (e.g., Enter, Tab, Escape, ArrowDown)' }, modifiers: { type: 'array', items: { type: 'string', enum: ['Alt', 'Control', 'Meta', 'Shift'] }, description: 'Modifier keys to hold' } }, required: ['key'] }, handler: async (args, browserManager) => { return browserManager.executeAction('press_key', async () => { const modifiers = args.modifiers || []; const keyEvent = { type: 'keyDown', key: args.key }; // Set modifiers if (modifiers.includes('Alt')) keyEvent.altKey = true; if (modifiers.includes('Control')) keyEvent.ctrlKey = true; if (modifiers.includes('Meta')) keyEvent.metaKey = true; if (modifiers.includes('Shift')) keyEvent.shiftKey = true; // Press key down await browserManager.sendCommand('Input.dispatchKeyEvent', keyEvent); // Press key up await browserManager.sendCommand('Input.dispatchKeyEvent', { ...keyEvent, type: 'keyUp' }); return { success: true, message: `Pressed key: ${args.key}${modifiers.length ? ' with ' + modifiers.join('+') : ''}`, timestamp: new Date() }; }); } }, { name: 'browser_drag', description: 'Drag from one element to another', inputSchema: { type: 'object', properties: { fromSelector: { type: 'string', description: 'CSS selector of source element' }, toSelector: { type: 'string', description: 'CSS selector of target element' }, steps: { type: 'number', default: 10, description: 'Number of intermediate steps' } }, required: ['fromSelector', 'toSelector'] }, handler: async (args, browserManager) => { return browserManager.executeAction('drag', async () => { await browserManager.sendCommand('DOM.enable'); // Get document const doc = await browserManager.sendCommand('DOM.getDocument'); // Find source element const fromNode = await browserManager.sendCommand('DOM.querySelector', { nodeId: doc.root.nodeId, selector: args.fromSelector }); // Find target element const toNode = await browserManager.sendCommand('DOM.querySelector', { nodeId: doc.root.nodeId, selector: args.toSelector }); if (!fromNode.nodeId || !toNode.nodeId) { throw new Error('Source or target element not found'); } // Get positions const fromBox = await browserManager.sendCommand('DOM.getBoxModel', { nodeId: fromNode.nodeId }); const toBox = await browserManager.sendCommand('DOM.getBoxModel', { nodeId: toNode.nodeId }); const fromQuad = fromBox.model.content; const toQuad = toBox.model.content; const fromX = (fromQuad[0] + fromQuad[2]) / 2; const fromY = (fromQuad[1] + fromQuad[5]) / 2; const toX = (toQuad[0] + toQuad[2]) / 2; const toY = (toQuad[1] + toQuad[5]) / 2; // Start drag await browserManager.sendCommand('Input.dispatchMouseEvent', { type: 'mousePressed', x: fromX, y: fromY, button: 'left' }); // Move in steps const steps = args.steps || 10; for (let i = 1; i <= steps; i++) { const progress = i / steps; const currentX = fromX + (toX - fromX) * progress; const currentY = fromY + (toY - fromY) * progress; await browserManager.sendCommand('Input.dispatchMouseEvent', { type: 'mouseMoved', x: currentX, y: currentY, button: 'left' }); // Small delay between steps await new Promise(resolve => setTimeout(resolve, 10)); } // End drag await browserManager.sendCommand('Input.dispatchMouseEvent', { type: 'mouseReleased', x: toX, y: toY, button: 'left' }); return { success: true, message: `Dragged from ${args.fromSelector} to ${args.toSelector}`, timestamp: new Date() }; }); } }, { name: 'browser_select_option', description: 'Select an option from a dropdown', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector of the select element' }, value: { type: 'string', description: 'Value to select' }, label: { type: 'string', description: 'Label text to select (alternative to value)' } }, required: ['selector'] }, handler: async (args, browserManager) => { return browserManager.executeAction('select_option', async () => { const script = ` const select = document.querySelector('${args.selector}'); if (!select) throw new Error('Select element not found'); let option; if ('${args.value}') { option = select.querySelector('option[value="${args.value}"]'); } else if ('${args.label}') { option = Array.from(select.options).find(opt => opt.text === '${args.label}'); } if (!option) throw new Error('Option not found'); select.value = option.value; select.dispatchEvent(new Event('change', { bubbles: true })); return { selected: option.value, text: option.text }; `; const result = await browserManager.sendCommand('Runtime.evaluate', { expression: script, returnByValue: true }); if (result.exceptionDetails) { throw new Error(result.result.description || 'Selection failed'); } return { success: true, message: `Selected option: ${result.result.value.text}`, data: result.result.value, timestamp: new Date() }; }); } }, { name: 'browser_wait_for', description: 'Wait for an element or condition', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector to wait for' }, timeout: { type: 'number', default: 10000, description: 'Timeout in ms' }, visible: { type: 'boolean', default: true, description: 'Wait for element to be visible' } }, required: ['selector'] }, handler: async (args, browserManager) => { return browserManager.executeAction('wait_for', async () => { const startTime = Date.now(); const timeout = args.timeout || 10000; while (Date.now() - startTime < timeout) { try { await browserManager.sendCommand('DOM.enable'); const doc = await browserManager.sendCommand('DOM.getDocument'); const node = await browserManager.sendCommand('DOM.querySelector', { nodeId: doc.root.nodeId, selector: args.selector }); if (node.nodeId) { if (!args.visible) { return { success: true, message: `Element found: ${args.selector}`, timestamp: new Date() }; } // Check if visible const box = await browserManager.sendCommand('DOM.getBoxModel', { nodeId: node.nodeId }); if (box.model) { return { success: true, message: `Element visible: ${args.selector}`, timestamp: new Date() }; } } } catch { // Element not found yet, continue waiting } await new Promise(resolve => setTimeout(resolve, 100)); } throw new Error(`Timeout waiting for element: ${args.selector}`); }); } }, { name: 'browser_get_content', description: 'Get the HTML content of the current page', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector to get content from (optional, defaults to entire page)' } } }, handler: async (args, browserManager) => { return await browserManager.getContent(args.selector); } }, { name: 'browser_tab_list', description: 'List all open browser tabs', inputSchema: { type: 'object', properties: {} }, handler: async (args, browserManager) => { return await browserManager.getPages(); } }, { name: 'browser_tab_new', description: 'Open a new tab', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL to open in new tab', default: 'about:blank' } } }, handler: async (args, browserManager) => { return browserManager.executeAction('new_tab', async () => { const result = await browserManager.sendCommand('Target.createTarget', { url: args.url || 'about:blank' }); return { success: true, message: `New tab created`, data: { tabId: result.targetId, url: args.url }, timestamp: new Date() }; }); } }, { name: 'browser_tab_close', description: 'Close a browser tab', inputSchema: { type: 'object', properties: { tabId: { type: 'string', description: 'ID of tab to close (optional, closes current tab if not specified)' } } }, handler: async (args, browserManager) => { return browserManager.executeAction('close_tab', async () => { if (args.tabId) { await browserManager.sendCommand('Target.closeTarget', { targetId: args.tabId }); } else { // Close current tab await browserManager.sendCommand('Page.close'); } return { success: true, message: `Tab closed`, timestamp: new Date() }; }); } }, { name: 'browser_tab_select', description: 'Switch to a specific tab', inputSchema: { type: 'object', properties: { tabId: { type: 'string', description: 'ID of tab to switch to' } }, required: ['tabId'] }, handler: async (args, browserManager) => { return browserManager.executeAction('select_tab', async () => { await browserManager.sendCommand('Target.activateTarget', { targetId: args.tabId }); return { success: true, message: `Switched to tab: ${args.tabId}`, timestamp: new Date() }; }); } }, { name: 'browser_resize', description: 'Resize the browser window', inputSchema: { type: 'object', properties: { width: { type: 'number', description: 'Window width in pixels' }, height: { type: 'number', description: 'Window height in pixels' } }, required: ['width', 'height'] }, handler: async (args, browserManager) => { return browserManager.executeAction('resize', async () => { await browserManager.sendCommand('Browser.setWindowBounds', { windowId: 1, bounds: { width: args.width, height: args.height } }); return { success: true, message: `Window resized to ${args.width}x${args.height}`, timestamp: new Date() }; }); } }, { name: 'browser_console_messages', description: 'Get console messages from the page', inputSchema: { type: 'object', properties: { level: { type: 'string', enum: ['log', 'info', 'warn', 'error', 'debug'], description: 'Filter by message level' } } }, handler: async (args, browserManager) => { return browserManager.executeAction('get_console_messages', async () => { // Enable console domain await browserManager.sendCommand('Console.enable'); await browserManager.sendCommand('Runtime.enable'); // Get console messages - this would need to be collected over time // For now, we'll return a placeholder return { success: true, message: 'Console monitoring enabled', data: { note: 'Console messages are now being collected. Use this tool again to retrieve them.', filter: args.level || 'all' }, timestamp: new Date() }; }); } }, { name: 'browser_handle_dialog', description: 'Handle JavaScript dialogs (alert, confirm, prompt)', inputSchema: { type: 'object', properties: { accept: { type: 'boolean', description: 'Whether to accept or dismiss the dialog', default: true }, text: { type: 'string', description: 'Text to enter for prompt dialogs' } } }, handler: async (args, browserManager) => { return browserManager.executeAction('handle_dialog', async () => { await browserManager.sendCommand('Page.handleJavaScriptDialog', { accept: args.accept !== false, promptText: args.text || '' }); return { success: true, message: `Dialog ${args.accept !== false ? 'accepted' : 'dismissed'}`, timestamp: new Date() }; }); } }, { name: 'browser_file_upload', description: 'Upload files to a file input element', inputSchema: { type: 'object', properties: { selector: { type: 'string', description: 'CSS selector of file input element' }, files: { type: 'array', items: { type: 'string' }, description: 'Array of file paths to upload' } }, required: ['selector', 'files'] }, handler: async (args, browserManager) => { return browserManager.executeAction('file_upload', async () => { await browserManager.sendCommand('DOM.enable'); const doc = await browserManager.sendCommand('DOM.getDocument'); const node = await browserManager.sendCommand('DOM.querySelector', { nodeId: doc.root.nodeId, selector: args.selector }); if (!node.nodeId) { throw new Error(`File input element not found: ${args.selector}`); } await browserManager.sendCommand('DOM.setFileInputFiles', { files: args.files, nodeId: node.nodeId }); return { success: true, message: `Files uploaded to ${args.selector}`, data: { files: args.files }, timestamp: new Date() }; }); } }, { name: 'browser_pdf_save', description: 'Save the current page as PDF', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Path to save PDF file' }, format: { type: 'string', enum: ['A4', 'Letter'], default: 'A4' }, landscape: { type: 'boolean', default: false, description: 'Page orientation' }, margin: { type: 'object', properties: { top: { type: 'string', default: '1cm' }, bottom: { type: 'string', default: '1cm' }, left: { type: 'string', default: '1cm' }, right: { type: 'string', default: '1cm' } } } } }, handler: async (args, browserManager) => { return browserManager.executeAction('save_pdf', async () => { const options = { format: args.format || 'A4', landscape: args.landscape || false, printBackground: true, marginTop: args.margin?.top || '1cm', marginBottom: args.margin?.bottom || '1cm', marginLeft: args.margin?.left || '1cm', marginRight: args.margin?.right || '1cm' }; const result = await browserManager.sendCommand('Page.printToPDF', options); return { success: true, message: 'PDF generated', data: result.data, timestamp: new Date() }; }); } }, { name: 'browser_network_requests', description: 'Monitor network requests', inputSchema: { type: 'object', properties: { enable: { type: 'boolean', default: true, description: 'Enable or disable network monitoring' }, filter: { type: 'string', description: 'URL pattern to filter requests' } } }, handler: async (args, browserManager) => { return browserManager.executeAction('network_monitoring', async () => { if (args.enable !== false) { await browserManager.sendCommand('Network.enable'); } else { await browserManager.sendCommand('Network.disable'); } return { success: true, message: `Network monitoring ${args.enable !== false ? 'enabled' : 'disabled'}`, data: { filter: args.filter }, timestamp: new Date() }; }); } }, { name: 'browser_generate_playwright_test', description: 'Generate Playwright test code for recent actions', inputSchema: { type: 'object', properties: { testName: { type: 'string', default: 'Generated Test', description: 'Name for the test' } } }, handler: async (args, browserManager) => { return browserManager.executeAction('generate_test', async () => { // This would generate Playwright test code based on recorded actions const testCode = ` import { test, expect } from '@playwright/test'; test('${args.testName}', async ({ page }) => { // Generated test code would go here // This is a placeholder implementation await page.goto('about:blank'); await expect(page).toHaveTitle(/.*./); }); `.trim(); return { success: true, message: 'Playwright test code generated', data: { testCode, testName: args.testName }, timestamp: new Date() }; }); } }, { name: 'browser_close', description: 'Close the browser (requires confirmation)', inputSchema: { type: 'object', properties: { force: { type: 'boolean', default: false, description: 'Force close without confirmation' } } }, requiresConfirmation: true, handler: async (args, browserManager) => { return browserManager.executeAction('close_browser', async () => { if (!args.force) { const confirmed = await askUserConfirmation('Close Browser', 'This will close the MCP Chrome instance. Your main Chrome browser will remain open.'); if (!confirmed) { return { success: false, message: 'Browser close cancelled by user', timestamp: new Date() }; } } // Close all tabs first const pages = await browserManager.getPages(); for (const page of pages) { try { await browserManager.sendCommand('Target.closeTarget', { targetId: page.id }); } catch { // Ignore errors closing individual tabs } } // Close browser await browserManager.sendCommand('Browser.close'); return { success: true, message: 'Browser closed successfully', timestamp: new Date() }; }); } } ]; /** * Get tool definition by name */ export function getTool(name) { return BROWSER_TOOLS.find(tool => tool.name === name); } /** * Get all tool names */ export function getToolNames() { return BROWSER_TOOLS.map(tool => tool.name); } /** * Get tools that require confirmation */ export function getConfirmationTools() { return BROWSER_TOOLS .filter(tool => tool.requiresConfirmation) .map(tool => tool.name); } //# sourceMappingURL=BrowserTools.js.map