# Directory Structure ``` ├── .gitignore ├── dist │ ├── chrome-api.d.ts │ ├── chrome-api.js │ ├── index.d.ts │ ├── index.js │ ├── types.d.ts │ └── types.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── chrome-api.ts │ ├── image-utils.ts │ ├── index.ts │ └── types.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ package-lock.json # Build output dist/ build/ *.tsbuildinfo # IDE and editor files .vscode/ .idea/ *.swp *.swo *~ # Logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Environment files .env .env.local .env.*.local # Operating System .DS_Store Thumbs.db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Chrome Tools MCP Server An MCP server that provides tools for interacting with Chrome through its DevTools Protocol. This server enables remote control of Chrome tabs, including executing JavaScript, capturing screenshots, monitoring network traffic, and more. ## Why use an MCP server like this? This type of MCP Server is useful When you need to manually configure your browser to be in a certain state before you let an AI tool like Cline poke at it. You can also use this tool to listen to and pull network events into its context. ## Features - List Chrome tabs - Execute JavaScript in tabs - Capture screenshots - Monitor network traffic - Navigate tabs to URLs - Query DOM elements - Click elements with console output capture ## Installation ```bash npm install @nicholmikey/chrome-tools ``` ## Configuration The server can be configured through environment variables in your MCP settings: ```json { "chrome-tools": { "command": "node", "args": ["path/to/chrome-tools/dist/index.js"], "env": { "CHROME_DEBUG_URL": "http://localhost:9222", "CHROME_CONNECTION_TYPE": "direct", "CHROME_ERROR_HELP": "custom error message" } } } ``` ### Environment Variables - `CHROME_DEBUG_URL`: The URL where Chrome's remote debugging interface is available (default: http://localhost:9222) - `CHROME_CONNECTION_TYPE`: Connection type identifier for logging (e.g., "direct", "ssh-tunnel", "docker") - `CHROME_ERROR_HELP`: Custom error message shown when connection fails ## Setup Guide ### Native Setup (Windows/Mac/Linux) 1. Launch Chrome with remote debugging enabled: ```bash # Windows "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 # Mac /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 # Linux google-chrome --remote-debugging-port=9222 ``` 2. Configure MCP settings: ```json { "env": { "CHROME_DEBUG_URL": "http://localhost:9222", "CHROME_CONNECTION_TYPE": "direct" } } ``` ### WSL Setup When running in WSL, you'll need to set up an SSH tunnel to connect to Chrome running on Windows: 1. Launch Chrome on Windows with remote debugging enabled 2. Create an SSH tunnel: ```bash ssh -N -L 9222:localhost:9222 windowsuser@host ``` 3. Configure MCP settings: ```json { "env": { "CHROME_DEBUG_URL": "http://localhost:9222", "CHROME_CONNECTION_TYPE": "ssh-tunnel", "CHROME_ERROR_HELP": "Make sure the SSH tunnel is running: ssh -N -L 9222:localhost:9222 windowsuser@host" } } ``` ### Docker Setup When running Chrome in Docker: 1. Launch Chrome container: ```bash docker run -d --name chrome -p 9222:9222 chromedp/headless-shell ``` 2. Configure MCP settings: ```json { "env": { "CHROME_DEBUG_URL": "http://localhost:9222", "CHROME_CONNECTION_TYPE": "docker" } } ``` ## Tools ### list_tabs Lists all available Chrome tabs. ### execute_script Executes JavaScript code in a specified tab. Parameters: - `tabId`: ID of the Chrome tab - `script`: JavaScript code to execute ### capture_screenshot Captures a screenshot of a specified tab, automatically optimizing it for AI model consumption. Parameters: - `tabId`: ID of the Chrome tab - `format`: Image format (jpeg/png) - Note: This is only for initial capture. Final output uses WebP with PNG fallback - `quality`: JPEG quality (1-100) - Note: For initial capture only - `fullPage`: Capture full scrollable page Image Processing: 1. WebP Optimization (Primary Format): - First attempt: WebP with quality 80 and high compression effort - Second attempt: WebP with quality 60 and near-lossless compression if first attempt exceeds 1MB 2. PNG Fallback: - Only used if WebP processing fails - Includes maximum compression and color palette optimization 3. Size Constraints: - Maximum dimensions: 900x600 (maintains aspect ratio) - Maximum file size: 1MB - Progressive size reduction if needed ### capture_network_events Monitors and captures network events from a specified tab. Parameters: - `tabId`: ID of the Chrome tab - `duration`: Duration in seconds to capture - `filters`: Optional type and URL pattern filters ### load_url Navigates a tab to a specified URL. Parameters: - `tabId`: ID of the Chrome tab - `url`: URL to load ### query_dom_elements Queries and retrieves detailed information about DOM elements matching a CSS selector. Parameters: - `tabId`: ID of the Chrome tab - `selector`: CSS selector to find elements Returns: - Array of DOM elements with properties including: - `nodeId`: Unique identifier for the node - `tagName`: HTML tag name - `textContent`: Text content of the element - `attributes`: Object containing all element attributes - `boundingBox`: Position and dimensions of the element - `isVisible`: Whether the element is visible - `ariaAttributes`: ARIA attributes for accessibility ### click_element Clicks on a DOM element and captures any console output triggered by the click. Parameters: - `tabId`: ID of the Chrome tab - `selector`: CSS selector to find the element to click Returns: - Object containing: - `message`: Success/failure message - `consoleOutput`: Array of console messages triggered by the click ## License MIT ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, "outDir": "./dist", "rootDir": "./src", "declaration": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript // Re-export the ChromeTab type from chrome-remote-interface for compatibility export type { Target as ChromeTab } from 'chrome-remote-interface'; // Interface for DOM element information export interface DOMElement { nodeId: number; tagName: string; textContent: string | null; attributes: Record<string, string>; boundingBox: { x: number; y: number; width: number; height: number; } | null; isVisible: boolean; ariaAttributes: Record<string, string>; } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "@nicholmikey/chrome-tools", "version": "1.3.0", "description": "MCP server for Chrome DevTools Protocol integration - control Chrome tabs, execute JavaScript, capture screenshots, and monitor network traffic", "main": "dist/index.js", "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node --esm src/index.ts", "watch": "tsc --watch", "prepare": "npm run build", "test": "echo \"No tests specified\" && exit 0", "lint": "eslint src --ext .ts", "format": "prettier --write \"src/**/*.ts\"" }, "bin": { "mcp-chrome-tools": "./dist/index.js" }, "files": [ "dist", "README.md", "LICENSE" ], "keywords": [ "mcp", "chrome", "devtools", "debugging", "automation", "testing", "screenshots", "network-monitoring", "browser-automation", "chrome-devtools-protocol" ], "author": { "name": "nicholmikey", "url": "https://github.com/nicholmikey" }, "repository": { "type": "git", "url": "https://github.com/nicholmikey/chrome-tools-MCP.git" }, "bugs": { "url": "https://github.com/nicholmikey/chrome-tools-MCP/issues" }, "homepage": "https://github.com/nicholmikey/chrome-tools-MCP#readme", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.5.0", "@types/ws": "^8.5.14", "axios": "^1.7.9", "chrome-remote-interface": "^0.33.2", "sharp": "^0.32.6", "ts-node": "^10.9.2", "typescript": "^5.7.3", "ws": "^8.18.0", "zod": "^3.24.2" }, "devDependencies": { "@types/chrome-remote-interface": "^0.31.14" } } ``` -------------------------------------------------------------------------------- /src/image-utils.ts: -------------------------------------------------------------------------------- ```typescript import { createRequire } from 'module'; import path from 'path'; import fs from 'fs/promises'; const require = createRequire(import.meta.url); const sharp = require('sharp'); export const SCREENSHOT_DIR = path.join('/tmp', 'chrome-tools-screenshots'); export interface ProcessedImage { data: string; format: 'png'; size: number; } export async function saveImage(processedImage: ProcessedImage): Promise<string> { // Ensure screenshots directory exists await fs.mkdir(SCREENSHOT_DIR, { recursive: true }); const filename = `screenshot_${Date.now()}.webp`; const filepath = path.join(SCREENSHOT_DIR, filename); // Extract the base64 data after the "data:image/webp;base64," prefix const base64Data = processedImage.data.split(',')[1]; const imageBuffer = Buffer.from(base64Data, 'base64'); await fs.writeFile(filepath, imageBuffer); return filepath; } export async function processImage(base64Data: string): Promise<ProcessedImage> { try { // Convert base64 to buffer const buffer = Buffer.from(base64Data, 'base64'); // Create Sharp instance and resize maintaining aspect ratio const image = sharp(buffer).resize(900, 600, { fit: 'inside', withoutEnlargement: true }); // Try WebP first with good quality try { const webpBuffer = await image .webp({ quality: 80, effort: 6, // Higher compression effort lossless: false }) .toBuffer(); if (webpBuffer.length <= 1024 * 1024) { return { data: `data:image/webp;base64,${webpBuffer.toString('base64')}`, format: 'png', // Keep format as 'png' in interface for backward compatibility size: webpBuffer.length }; } // If still too large, try WebP with more aggressive compression const compressedWebpBuffer = await image .webp({ quality: 60, effort: 6, lossless: false, nearLossless: true }) .toBuffer(); if (compressedWebpBuffer.length <= 1024 * 1024) { return { data: `data:image/webp;base64,${compressedWebpBuffer.toString('base64')}`, format: 'png', // Keep format as 'png' in interface for backward compatibility size: compressedWebpBuffer.length }; } } catch (webpError) { console.error('WebP processing failed, falling back to PNG:', webpError); } // Fallback to PNG with compression if WebP fails or is too large const pngBuffer = await image .png({ compressionLevel: 9, palette: true }) .toBuffer(); if (pngBuffer.length > 1024 * 1024) { // If still too large, reduce dimensions further const scaleFactor = Math.sqrt(1024 * 1024 / pngBuffer.length); const resizedImage = sharp(buffer).resize( Math.floor(900 * scaleFactor), Math.floor(600 * scaleFactor), { fit: 'inside', withoutEnlargement: true } ); const compressedPngBuffer = await resizedImage .png({ compressionLevel: 9, palette: true, colors: 128 // Reduce color palette for smaller size }) .toBuffer(); if (compressedPngBuffer.length > 1024 * 1024) { throw new Error('Image is too large even after compression'); } return { data: `data:image/png;base64,${compressedPngBuffer.toString('base64')}`, format: 'png', size: compressedPngBuffer.length }; } return { data: `data:image/png;base64,${pngBuffer.toString('base64')}`, format: 'png', size: pngBuffer.length }; } catch (error) { throw new Error(`Failed to process image: ${error instanceof Error ? error.message : 'Unknown error'}`); } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ChromeAPI } from './chrome-api.js'; import { processImage, saveImage } from './image-utils.js'; import { z } from 'zod'; // Get Chrome debug URL from environment variable or use default const chromeDebugUrl = process.env.CHROME_DEBUG_URL || 'http://localhost:9222'; console.error(`Using Chrome debug URL: ${chromeDebugUrl}`); const chromeApi = new ChromeAPI({ baseUrl: chromeDebugUrl }); // Create the MCP server const server = new McpServer({ name: 'chrome-tools', version: '1.3.0' }); // Add the list_tabs tool server.tool( 'list_tabs', {}, // No input parameters needed async () => { try { console.error('Attempting to list Chrome tabs...'); const tabs = await chromeApi.listTabs(); console.error(`Successfully found ${tabs.length} tabs`); return { content: [{ type: 'text', text: JSON.stringify(tabs, null, 2) }] }; } catch (error) { console.error('Error in list_tabs tool:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true }; } } ); // Add the capture_screenshot tool server.tool( 'capture_screenshot', { tabId: z.string().describe('ID of the Chrome tab to capture. Only send this unless you are having issues with the result.'), format: z.enum(['jpeg', 'png']).optional() .describe('Initial capture format (jpeg/png). Note: Final output will be WebP with PNG fallback'), quality: z.number().min(1).max(100).optional() .describe('Initial capture quality (1-100). Note: Final output uses WebP quality settings'), fullPage: z.boolean().optional() .describe('Capture full scrollable page') }, async (params) => { try { console.error(`Attempting to capture screenshot of tab ${params.tabId}...`); const rawBase64Data = await chromeApi.captureScreenshot(params.tabId, { format: params.format, quality: params.quality, fullPage: params.fullPage }); console.error('Screenshot captured, optimizing with WebP...'); try { // Process image with the following strategy: // 1. Try WebP with quality 80 (best balance of quality/size) // 2. If >1MB, try WebP with quality 60 and near-lossless // 3. If WebP fails, fall back to PNG with maximum compression const processedImage = await processImage(rawBase64Data); console.error(`Image optimized successfully (${processedImage.data.startsWith('data:image/webp') ? 'WebP' : 'PNG'}, ${Math.round(processedImage.size / 1024)}KB)`); // Save the image and get the filepath const filepath = await saveImage(processedImage); console.error(`Screenshot saved to: ${filepath}`); return { content: [{ type: 'text', text: JSON.stringify({ status: 'Screenshot successful.', path: filepath }) }] }; } catch (error) { console.error('Image processing failed:', error); return { content: [{ type: 'text', text: `Error processing screenshot: ${error instanceof Error ? error.message : 'Unknown error'}` }], isError: true }; } } catch (error) { console.error('Error in capture_screenshot tool:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true }; } } ); // Add the execute_script tool server.tool( 'execute_script', { tabId: z.string().describe('ID of the Chrome tab to execute the script in'), script: z.string().describe('JavaScript code to execute in the tab') }, async (params) => { try { console.error(`Attempting to execute script in tab ${params.tabId}...`); const result = await chromeApi.executeScript(params.tabId, params.script); console.error('Script execution successful'); return { content: [{ type: 'text', text: result || 'undefined' }] }; } catch (error) { console.error('Error in execute_script tool:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true }; } } ); // Log when server starts console.error('Chrome Tools MCP Server starting...'); // Start the server const transport = new StdioServerTransport(); server.connect(transport).catch(console.error); // Add the load_url tool server.tool( 'load_url', { tabId: z.string().describe('ID of the Chrome tab to load the URL in'), url: z.string().url().describe('URL to load in the tab') }, async (params) => { try { console.error(`Attempting to load URL ${params.url} in tab ${params.tabId}...`); await chromeApi.loadUrl(params.tabId, params.url); console.error('URL loading successful'); return { content: [{ type: 'text', text: `Successfully loaded ${params.url} in tab ${params.tabId}` }] }; } catch (error) { console.error('Error in load_url tool:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true }; } } ); // Add the capture_network_events tool server.tool( 'capture_network_events', { tabId: z.string().describe('ID of the Chrome tab to monitor'), duration: z.number().min(1).max(60).optional() .describe('Duration in seconds to capture events (default: 10)'), filters: z.object({ types: z.array(z.enum(['fetch', 'xhr'])).optional() .describe('Types of requests to capture'), urlPattern: z.string().optional() .describe('Only capture URLs matching this pattern') }).optional() }, async (params) => { try { console.error(`Attempting to capture network events from tab ${params.tabId}...`); const events = await chromeApi.captureNetworkEvents(params.tabId, { duration: params.duration, filters: params.filters }); console.error(`Network event capture successful, captured ${events.length} events`); return { content: [{ type: 'text', text: JSON.stringify(events, null, 2) }] }; } catch (error) { console.error('Error in capture_network_events tool:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true }; } } ); // Add the query_dom_elements tool server.tool( 'query_dom_elements', { tabId: z.string().describe('ID of the Chrome tab to query'), selector: z.string().describe('CSS selector to find elements') }, async (params) => { try { console.error(`Attempting to query DOM elements in tab ${params.tabId}...`); const elements = await chromeApi.queryDOMElements(params.tabId, params.selector); console.error(`Successfully found ${elements.length} elements matching selector`); return { content: [{ type: 'text', text: JSON.stringify(elements, null, 2) }] }; } catch (error) { console.error('Error in query_dom_elements tool:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true }; } } ); // Add the click_element tool server.tool( 'click_element', { tabId: z.string().describe('ID of the Chrome tab containing the element'), selector: z.string().describe('CSS selector to find the element to click') }, async (params) => { try { console.error(`Attempting to click element in tab ${params.tabId}...`); const result = await chromeApi.clickElement(params.tabId, params.selector); console.error('Successfully clicked element'); return { content: [{ type: 'text', text: JSON.stringify({ message: 'Successfully clicked element', consoleOutput: result.consoleOutput }, null, 2) }] }; } catch (error) { console.error('Error in click_element tool:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true }; } } ); // Handle process termination process.on('SIGINT', () => { server.close().catch(console.error); process.exit(0); }); ``` -------------------------------------------------------------------------------- /src/chrome-api.ts: -------------------------------------------------------------------------------- ```typescript 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; } } ```