Chrome Tools MCP Server

  • src
import CDP from 'chrome-remote-interface'; import type { Client } from 'chrome-remote-interface'; import { ChromeTab, DOMElement } from './types.js'; type MouseButton = 'none' | 'left' | 'middle' | 'right' | 'back' | 'forward'; type MouseEventType = 'mousePressed' | 'mouseReleased'; export class ChromeAPI { private baseUrl: string; constructor(options: { port?: number; baseUrl?: string } = {}) { const { port = 9222, baseUrl } = options; this.baseUrl = baseUrl || `http://localhost:${port}`; const connectionType = process.env.CHROME_CONNECTION_TYPE || 'direct'; console.error(`ChromeAPI: Connecting to ${this.baseUrl} (${connectionType} connection)`); } /** * List all available Chrome tabs * @returns Promise<ChromeTab[]> * @throws Error if Chrome is not accessible or returns an error */ async listTabs(): Promise<ChromeTab[]> { try { console.error(`ChromeAPI: Attempting to list tabs on port ${this.port}`); const targets = await CDP.List({ port: this.port }); console.error(`ChromeAPI: Successfully found ${targets.length} tabs`); return targets; } catch (error) { console.error(`ChromeAPI: Failed to list tabs:`, error instanceof Error ? error.message : error); const errorHelp = process.env.CHROME_ERROR_HELP || 'Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222)'; throw new Error(`Failed to connect to Chrome DevTools. ${errorHelp}`); } } /** * Execute JavaScript in a specific Chrome tab * @param tabId The ID of the tab to execute the script in * @param script The JavaScript code to execute * @returns Promise with the result of the script execution * @throws Error if the tab is not found or script execution fails */ async executeScript(tabId: string, script: string): Promise<string> { console.error(`ChromeAPI: Attempting to execute script in tab ${tabId}`); let client: Client | undefined; try { // Connect to the specific tab client = await CDP({ target: tabId, port: this.port }); if (!client) { throw new Error('Failed to connect to Chrome DevTools'); } // Enable Runtime and set up console listener await client.Runtime.enable(); let consoleMessages: string[] = []; client.Runtime.consoleAPICalled(({ type, args }) => { const message = args.map(arg => arg.value || arg.description).join(' '); consoleMessages.push(`[${type}] ${message}`); console.error(`Chrome Console: ${type}:`, message); }); // Execute the script using Runtime.evaluate const result = await client.Runtime.evaluate({ expression: script, returnByValue: true, includeCommandLineAPI: true }); console.error('ChromeAPI: Script execution successful'); return JSON.stringify({ result: result.result, consoleOutput: consoleMessages }, null, 2); } catch (error) { console.error('ChromeAPI: Script execution failed:', error instanceof Error ? error.message : error); throw error; } finally { if (client) { await client.close(); } } } /** * Check if Chrome debugging port is accessible * @returns Promise<boolean> */ async isAvailable(): Promise<boolean> { try { await this.listTabs(); return true; } catch { return false; } } /** * Capture a screenshot of a specific Chrome tab * @param tabId The ID of the tab to capture * @param options Screenshot options (format, quality, fullPage) * @returns Promise with the base64-encoded screenshot data * @throws Error if the tab is not found or screenshot capture fails */ async captureScreenshot( tabId: string, options: { format?: 'jpeg' | 'png'; quality?: number; fullPage?: boolean; } = {} ): Promise<string> { console.error(`ChromeAPI: Attempting to capture screenshot of tab ${tabId}`); let client: Client | undefined; try { // Connect to the specific tab client = await CDP({ target: tabId, port: this.port }); if (!client) { throw new Error('Failed to connect to Chrome DevTools'); } // Enable Page domain for screenshot capabilities await client.Page.enable(); // If fullPage is requested, we need to get the full page dimensions if (options.fullPage) { // Get the full page dimensions const { root } = await client.DOM.getDocument(); const { model } = await client.DOM.getBoxModel({ nodeId: root.nodeId }); const height = model.height; // Set viewport to full page height await client.Emulation.setDeviceMetricsOverride({ width: 1920, // Standard width height: Math.ceil(height), deviceScaleFactor: 1, mobile: false }); } // Capture the screenshot const result = await client.Page.captureScreenshot({ format: options.format || 'png', quality: options.format === 'jpeg' ? options.quality || 80 : undefined, fromSurface: true, captureBeyondViewport: options.fullPage || false }); console.error('ChromeAPI: Screenshot capture successful'); return result.data; } catch (error) { console.error('ChromeAPI: Screenshot capture failed:', error instanceof Error ? error.message : error); throw error; } finally { if (client) { // Reset device metrics if we modified them if (options.fullPage) { await client.Emulation.clearDeviceMetricsOverride(); } await client.close(); } } } /** * Capture network events (XHR/Fetch) from a specific Chrome tab * @param tabId The ID of the tab to capture events from * @param options Capture options (duration, filters) * @returns Promise with the captured network events * @throws Error if the tab is not found or capture fails */ async captureNetworkEvents( tabId: string, options: { duration?: number; filters?: { types?: Array<'fetch' | 'xhr'>; urlPattern?: string; }; } = {} ): Promise<Array<{ type: 'fetch' | 'xhr'; method: string; url: string; status: number; statusText: string; requestHeaders: Record<string, string>; responseHeaders: Record<string, string>; timing: { requestTime: number; responseTime: number; }; }>> { console.error(`ChromeAPI: Attempting to capture network events from tab ${tabId}`); let client: Client | undefined; try { // Connect to the specific tab client = await CDP({ target: tabId, port: this.port }); if (!client) { throw new Error('Failed to connect to Chrome DevTools'); } // Enable Network domain await client.Network.enable(); const events: Array<any> = []; const requests = new Map(); // Set up event handlers const requestHandler = (params: any) => { const request = { type: (params.type?.toLowerCase() === 'xhr' ? 'xhr' : 'fetch') as 'xhr' | 'fetch', method: params.request.method, url: params.request.url, requestHeaders: params.request.headers, timing: { requestTime: params.timestamp } }; // Apply filters if specified if (options.filters) { if (options.filters.types && !options.filters.types.includes(request.type)) { return; } if (options.filters.urlPattern && !request.url.match(options.filters.urlPattern)) { return; } } requests.set(params.requestId, request); }; const responseHandler = (params: any) => { const request = requests.get(params.requestId); if (request) { request.status = params.response.status; request.statusText = params.response.statusText; request.responseHeaders = params.response.headers; request.timing.responseTime = params.timestamp; events.push(request); } }; // Register event handlers client.Network.requestWillBeSent(requestHandler); client.Network.responseReceived(responseHandler); // Wait for specified duration const duration = options.duration || 10; await new Promise(resolve => setTimeout(resolve, duration * 1000)); console.error('ChromeAPI: Network event capture successful'); return events; } catch (error) { console.error('ChromeAPI: Network event capture failed:', error instanceof Error ? error.message : error); throw error; } finally { if (client) { await client.close(); } } } /** * Navigate a Chrome tab to a specific URL * @param tabId The ID of the tab to load the URL in * @param url The URL to load * @returns Promise<void> * @throws Error if the tab is not found or navigation fails */ async loadUrl(tabId: string, url: string): Promise<void> { console.error(`ChromeAPI: Attempting to load URL ${url} in tab ${tabId}`); let client: Client | undefined; try { // Connect to the specific tab client = await CDP({ target: tabId, port: this.port }); if (!client) { throw new Error('Failed to connect to Chrome DevTools'); } // Enable Page domain for navigation await client.Page.enable(); // Navigate to the URL and wait for load await client.Page.navigate({ url }); await client.Page.loadEventFired(); console.error('ChromeAPI: URL loading successful'); } catch (error) { console.error('ChromeAPI: URL loading failed:', error instanceof Error ? error.message : error); throw error; } finally { if (client) { await client.close(); } } } /** * Query DOM elements using a CSS selector * @param tabId The ID of the tab to query * @param selector CSS selector to find elements * @returns Promise<DOMElement[]> Array of matching DOM elements with their properties * @throws Error if the tab is not found or query fails */ async queryDOMElements(tabId: string, selector: string): Promise<DOMElement[]> { console.error(`ChromeAPI: Attempting to query DOM elements in tab ${tabId} with selector "${selector}"`); let client: Client | undefined; try { // Connect to the specific tab client = await CDP({ target: tabId, port: this.port }); if (!client) { throw new Error('Failed to connect to Chrome DevTools'); } // Enable necessary domains await client.DOM.enable(); await client.Runtime.enable(); // Get the document root const { root } = await client.DOM.getDocument(); // Find elements matching the selector const { nodeIds } = await client.DOM.querySelectorAll({ nodeId: root.nodeId, selector: selector }); // Get detailed information for each element const elements: DOMElement[] = await Promise.all( nodeIds.map(async (nodeId) => { if (!client) { throw new Error('Client disconnected'); } // Get node details const { node } = await client.DOM.describeNode({ nodeId }); // Get node box model for position and dimensions const boxModel = await client.DOM.getBoxModel({ nodeId }) .catch(() => null); // Some elements might not have a box model // Check visibility using Runtime.evaluate const result = await client.Runtime.evaluate({ expression: ` (function(selector) { const element = document.querySelector(selector); if (!element) return false; const style = window.getComputedStyle(element); return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; })('${selector}') `, returnByValue: true }); // Extract ARIA attributes const ariaAttributes: Record<string, string> = {}; if (node.attributes) { for (let i = 0; i < node.attributes.length; i += 2) { const name = node.attributes[i]; if (name.startsWith('aria-')) { ariaAttributes[name] = node.attributes[i + 1]; } } } // Convert attributes array to object const attributes: Record<string, string> = {}; if (node.attributes) { for (let i = 0; i < node.attributes.length; i += 2) { attributes[node.attributes[i]] = node.attributes[i + 1]; } } return { nodeId, tagName: node.nodeName.toLowerCase(), textContent: node.nodeValue || null, attributes, boundingBox: boxModel ? { x: boxModel.model.content[0], y: boxModel.model.content[1], width: boxModel.model.width, height: boxModel.model.height } : null, isVisible: result.result.value as boolean, ariaAttributes }; }) ); console.error(`ChromeAPI: Successfully found ${elements.length} elements matching selector`); return elements; } catch (error) { console.error('ChromeAPI: DOM query failed:', error instanceof Error ? error.message : error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; throw new Error(`Failed to query DOM elements with selector "${selector}": ${errorMessage}. Note: :contains() is not a valid CSS selector. Use a valid CSS selector like tag names, classes, or IDs.`); } finally { if (client) { await client.close(); } } } /** * Click on a DOM element matching a CSS selector * @param tabId The ID of the tab containing the element * @param selector CSS selector to find the element to click * @returns Promise<void> * @throws Error if the tab is not found, element is not found, or click fails */ async clickElement(tabId: string, selector: string): Promise<{consoleOutput: string[]}> { console.error(`ChromeAPI: Attempting to click element in tab ${tabId} with selector "${selector}"`); let client: Client | undefined; try { // Connect to the specific tab client = await CDP({ target: tabId, port: this.port }); if (!client) { throw new Error('Failed to connect to Chrome DevTools'); } // Enable necessary domains await client.DOM.enable(); await client.Runtime.enable(); // Get the document root const { root } = await client.DOM.getDocument(); // Find the element const { nodeIds } = await client.DOM.querySelectorAll({ nodeId: root.nodeId, selector: selector }); if (nodeIds.length === 0) { throw new Error(`No element found matching selector: ${selector}`); } // Get element's box model for coordinates const { model } = await client.DOM.getBoxModel({ nodeId: nodeIds[0] }); // Calculate center point const centerX = model.content[0] + (model.width / 2); const centerY = model.content[1] + (model.height / 2); // Dispatch click event using Runtime.evaluate await client.Runtime.evaluate({ expression: ` (() => { const element = document.querySelector('${selector}'); if (!element) throw new Error('Element not found'); const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window, clientX: ${Math.round(centerX)}, clientY: ${Math.round(centerY)} }); element.dispatchEvent(clickEvent); })() `, awaitPromise: true }); // Set up console listener before the click let consoleMessages: string[] = []; const consolePromise = new Promise<void>((resolve) => { if (!client) return; client.Runtime.consoleAPICalled(({ type, args }) => { const message = args.map(arg => arg.value || arg.description).join(' '); consoleMessages.push(`[${type}] ${message}`); console.error(`Chrome Console: ${type}:`, message); resolve(); // Resolve when we get a console message }); }); // Set up a timeout promise const timeoutPromise = new Promise<void>((resolve) => { setTimeout(resolve, 1000); }); // Click the element await client.Runtime.evaluate({ expression: ` (() => { const element = document.querySelector('${selector}'); if (!element) throw new Error('Element not found'); const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window, clientX: ${Math.round(centerX)}, clientY: ${Math.round(centerY)} }); element.dispatchEvent(clickEvent); })() `, awaitPromise: true }); // Wait for either a console message or timeout await Promise.race([consolePromise, timeoutPromise]); console.error('ChromeAPI: Successfully clicked element'); return { consoleOutput: consoleMessages }; } catch (error) { console.error('ChromeAPI: Element click failed:', error instanceof Error ? error.message : error); throw error; } finally { if (client) { await client.close(); } } } private get port(): number { const portMatch = this.baseUrl.match(/:(\d+)$/); return portMatch ? parseInt(portMatch[1]) : 9222; } }