├── .gitignore ├── LICENSE ├── README.md ├── dist ├── chrome-api.d.ts ├── chrome-api.js ├── index.d.ts ├── index.js ├── types.d.ts └── types.js ├── package-lock.json ├── package.json ├── src ├── chrome-api.ts ├── image-utils.ts ├── index.ts └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | package-lock.json 4 | 5 | # Build output 6 | dist/ 7 | build/ 8 | *.tsbuildinfo 9 | 10 | # IDE and editor files 11 | .vscode/ 12 | .idea/ 13 | *.swp 14 | *.swo 15 | *~ 16 | 17 | # Logs 18 | *.log 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # Environment files 24 | .env 25 | .env.local 26 | .env.*.local 27 | 28 | # Operating System 29 | .DS_Store 30 | Thumbs.db 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 nicholmikey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chrome Tools MCP Server 2 | 3 | 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. 4 | 5 | ## Why use an MCP server like this? 6 | 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. 7 | 8 | ## Features 9 | 10 | - List Chrome tabs 11 | - Execute JavaScript in tabs 12 | - Capture screenshots 13 | - Monitor network traffic 14 | - Navigate tabs to URLs 15 | - Query DOM elements 16 | - Click elements with console output capture 17 | 18 | ## Installation 19 | 20 | ```bash 21 | npm install @nicholmikey/chrome-tools 22 | ``` 23 | 24 | ## Configuration 25 | 26 | The server can be configured through environment variables in your MCP settings: 27 | 28 | ```json 29 | { 30 | "chrome-tools": { 31 | "command": "node", 32 | "args": ["path/to/chrome-tools/dist/index.js"], 33 | "env": { 34 | "CHROME_DEBUG_URL": "http://localhost:9222", 35 | "CHROME_CONNECTION_TYPE": "direct", 36 | "CHROME_ERROR_HELP": "custom error message" 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | ### Environment Variables 43 | 44 | - `CHROME_DEBUG_URL`: The URL where Chrome's remote debugging interface is available (default: http://localhost:9222) 45 | - `CHROME_CONNECTION_TYPE`: Connection type identifier for logging (e.g., "direct", "ssh-tunnel", "docker") 46 | - `CHROME_ERROR_HELP`: Custom error message shown when connection fails 47 | 48 | ## Setup Guide 49 | 50 | ### Native Setup (Windows/Mac/Linux) 51 | 52 | 1. Launch Chrome with remote debugging enabled: 53 | ```bash 54 | # Windows 55 | "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 56 | 57 | # Mac 58 | /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 59 | 60 | # Linux 61 | google-chrome --remote-debugging-port=9222 62 | ``` 63 | 64 | 2. Configure MCP settings: 65 | ```json 66 | { 67 | "env": { 68 | "CHROME_DEBUG_URL": "http://localhost:9222", 69 | "CHROME_CONNECTION_TYPE": "direct" 70 | } 71 | } 72 | ``` 73 | 74 | ### WSL Setup 75 | 76 | When running in WSL, you'll need to set up an SSH tunnel to connect to Chrome running on Windows: 77 | 78 | 1. Launch Chrome on Windows with remote debugging enabled 79 | 2. Create an SSH tunnel: 80 | ```bash 81 | ssh -N -L 9222:localhost:9222 windowsuser@host 82 | ``` 83 | 3. Configure MCP settings: 84 | ```json 85 | { 86 | "env": { 87 | "CHROME_DEBUG_URL": "http://localhost:9222", 88 | "CHROME_CONNECTION_TYPE": "ssh-tunnel", 89 | "CHROME_ERROR_HELP": "Make sure the SSH tunnel is running: ssh -N -L 9222:localhost:9222 windowsuser@host" 90 | } 91 | } 92 | ``` 93 | 94 | ### Docker Setup 95 | 96 | When running Chrome in Docker: 97 | 98 | 1. Launch Chrome container: 99 | ```bash 100 | docker run -d --name chrome -p 9222:9222 chromedp/headless-shell 101 | ``` 102 | 103 | 2. Configure MCP settings: 104 | ```json 105 | { 106 | "env": { 107 | "CHROME_DEBUG_URL": "http://localhost:9222", 108 | "CHROME_CONNECTION_TYPE": "docker" 109 | } 110 | } 111 | ``` 112 | 113 | ## Tools 114 | 115 | ### list_tabs 116 | Lists all available Chrome tabs. 117 | 118 | ### execute_script 119 | Executes JavaScript code in a specified tab. 120 | Parameters: 121 | - `tabId`: ID of the Chrome tab 122 | - `script`: JavaScript code to execute 123 | 124 | ### capture_screenshot 125 | Captures a screenshot of a specified tab, automatically optimizing it for AI model consumption. 126 | Parameters: 127 | - `tabId`: ID of the Chrome tab 128 | - `format`: Image format (jpeg/png) - Note: This is only for initial capture. Final output uses WebP with PNG fallback 129 | - `quality`: JPEG quality (1-100) - Note: For initial capture only 130 | - `fullPage`: Capture full scrollable page 131 | 132 | Image Processing: 133 | 1. WebP Optimization (Primary Format): 134 | - First attempt: WebP with quality 80 and high compression effort 135 | - Second attempt: WebP with quality 60 and near-lossless compression if first attempt exceeds 1MB 136 | 2. PNG Fallback: 137 | - Only used if WebP processing fails 138 | - Includes maximum compression and color palette optimization 139 | 3. Size Constraints: 140 | - Maximum dimensions: 900x600 (maintains aspect ratio) 141 | - Maximum file size: 1MB 142 | - Progressive size reduction if needed 143 | 144 | ### capture_network_events 145 | Monitors and captures network events from a specified tab. 146 | Parameters: 147 | - `tabId`: ID of the Chrome tab 148 | - `duration`: Duration in seconds to capture 149 | - `filters`: Optional type and URL pattern filters 150 | 151 | ### load_url 152 | Navigates a tab to a specified URL. 153 | Parameters: 154 | - `tabId`: ID of the Chrome tab 155 | - `url`: URL to load 156 | 157 | ### query_dom_elements 158 | Queries and retrieves detailed information about DOM elements matching a CSS selector. 159 | Parameters: 160 | - `tabId`: ID of the Chrome tab 161 | - `selector`: CSS selector to find elements 162 | Returns: 163 | - Array of DOM elements with properties including: 164 | - `nodeId`: Unique identifier for the node 165 | - `tagName`: HTML tag name 166 | - `textContent`: Text content of the element 167 | - `attributes`: Object containing all element attributes 168 | - `boundingBox`: Position and dimensions of the element 169 | - `isVisible`: Whether the element is visible 170 | - `ariaAttributes`: ARIA attributes for accessibility 171 | 172 | ### click_element 173 | Clicks on a DOM element and captures any console output triggered by the click. 174 | Parameters: 175 | - `tabId`: ID of the Chrome tab 176 | - `selector`: CSS selector to find the element to click 177 | Returns: 178 | - Object containing: 179 | - `message`: Success/failure message 180 | - `consoleOutput`: Array of console messages triggered by the click 181 | 182 | ## License 183 | 184 | MIT 185 | -------------------------------------------------------------------------------- /dist/chrome-api.d.ts: -------------------------------------------------------------------------------- 1 | import { ChromeTab, DOMElement } from './types.js'; 2 | export declare class ChromeAPI { 3 | private baseUrl; 4 | constructor(options?: { 5 | port?: number; 6 | baseUrl?: string; 7 | }); 8 | /** 9 | * List all available Chrome tabs 10 | * @returns Promise 11 | * @throws Error if Chrome is not accessible or returns an error 12 | */ 13 | listTabs(): Promise; 14 | /** 15 | * Execute JavaScript in a specific Chrome tab 16 | * @param tabId The ID of the tab to execute the script in 17 | * @param script The JavaScript code to execute 18 | * @returns Promise with the result of the script execution 19 | * @throws Error if the tab is not found or script execution fails 20 | */ 21 | executeScript(tabId: string, script: string): Promise; 22 | /** 23 | * Check if Chrome debugging port is accessible 24 | * @returns Promise 25 | */ 26 | isAvailable(): Promise; 27 | /** 28 | * Capture a screenshot of a specific Chrome tab 29 | * @param tabId The ID of the tab to capture 30 | * @param options Screenshot options (format, quality, fullPage) 31 | * @returns Promise with the base64-encoded screenshot data 32 | * @throws Error if the tab is not found or screenshot capture fails 33 | */ 34 | captureScreenshot(tabId: string, options?: { 35 | format?: 'jpeg' | 'png'; 36 | quality?: number; 37 | fullPage?: boolean; 38 | }): Promise; 39 | /** 40 | * Capture network events (XHR/Fetch) from a specific Chrome tab 41 | * @param tabId The ID of the tab to capture events from 42 | * @param options Capture options (duration, filters) 43 | * @returns Promise with the captured network events 44 | * @throws Error if the tab is not found or capture fails 45 | */ 46 | captureNetworkEvents(tabId: string, options?: { 47 | duration?: number; 48 | filters?: { 49 | types?: Array<'fetch' | 'xhr'>; 50 | urlPattern?: string; 51 | }; 52 | }): Promise; 59 | responseHeaders: Record; 60 | timing: { 61 | requestTime: number; 62 | responseTime: number; 63 | }; 64 | }>>; 65 | /** 66 | * Navigate a Chrome tab to a specific URL 67 | * @param tabId The ID of the tab to load the URL in 68 | * @param url The URL to load 69 | * @returns Promise 70 | * @throws Error if the tab is not found or navigation fails 71 | */ 72 | loadUrl(tabId: string, url: string): Promise; 73 | /** 74 | * Query DOM elements using a CSS selector 75 | * @param tabId The ID of the tab to query 76 | * @param selector CSS selector to find elements 77 | * @returns Promise Array of matching DOM elements with their properties 78 | * @throws Error if the tab is not found or query fails 79 | */ 80 | queryDOMElements(tabId: string, selector: string): Promise; 81 | /** 82 | * Click on a DOM element matching a CSS selector 83 | * @param tabId The ID of the tab containing the element 84 | * @param selector CSS selector to find the element to click 85 | * @returns Promise 86 | * @throws Error if the tab is not found, element is not found, or click fails 87 | */ 88 | clickElement(tabId: string, selector: string): Promise<{ 89 | consoleOutput: string[]; 90 | }>; 91 | private get port(); 92 | } 93 | -------------------------------------------------------------------------------- /dist/chrome-api.js: -------------------------------------------------------------------------------- 1 | import CDP from 'chrome-remote-interface'; 2 | export class ChromeAPI { 3 | constructor(options = {}) { 4 | const { port = 9222, baseUrl } = options; 5 | this.baseUrl = baseUrl || `http://localhost:${port}`; 6 | const connectionType = process.env.CHROME_CONNECTION_TYPE || 'direct'; 7 | console.error(`ChromeAPI: Connecting to ${this.baseUrl} (${connectionType} connection)`); 8 | } 9 | /** 10 | * List all available Chrome tabs 11 | * @returns Promise 12 | * @throws Error if Chrome is not accessible or returns an error 13 | */ 14 | async listTabs() { 15 | try { 16 | console.error(`ChromeAPI: Attempting to list tabs on port ${this.port}`); 17 | const targets = await CDP.List({ port: this.port }); 18 | console.error(`ChromeAPI: Successfully found ${targets.length} tabs`); 19 | return targets; 20 | } 21 | catch (error) { 22 | console.error(`ChromeAPI: Failed to list tabs:`, error instanceof Error ? error.message : error); 23 | const errorHelp = process.env.CHROME_ERROR_HELP || 'Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222)'; 24 | throw new Error(`Failed to connect to Chrome DevTools. ${errorHelp}`); 25 | } 26 | } 27 | /** 28 | * Execute JavaScript in a specific Chrome tab 29 | * @param tabId The ID of the tab to execute the script in 30 | * @param script The JavaScript code to execute 31 | * @returns Promise with the result of the script execution 32 | * @throws Error if the tab is not found or script execution fails 33 | */ 34 | async executeScript(tabId, script) { 35 | console.error(`ChromeAPI: Attempting to execute script in tab ${tabId}`); 36 | let client; 37 | try { 38 | // Connect to the specific tab 39 | client = await CDP({ target: tabId, port: this.port }); 40 | if (!client) { 41 | throw new Error('Failed to connect to Chrome DevTools'); 42 | } 43 | // Enable Runtime and set up console listener 44 | await client.Runtime.enable(); 45 | let consoleMessages = []; 46 | client.Runtime.consoleAPICalled(({ type, args }) => { 47 | const message = args.map(arg => arg.value || arg.description).join(' '); 48 | consoleMessages.push(`[${type}] ${message}`); 49 | console.error(`Chrome Console: ${type}:`, message); 50 | }); 51 | // Execute the script using Runtime.evaluate 52 | const result = await client.Runtime.evaluate({ 53 | expression: script, 54 | returnByValue: true, 55 | includeCommandLineAPI: true 56 | }); 57 | console.error('ChromeAPI: Script execution successful'); 58 | return JSON.stringify({ 59 | result: result.result, 60 | consoleOutput: consoleMessages 61 | }, null, 2); 62 | } 63 | catch (error) { 64 | console.error('ChromeAPI: Script execution failed:', error instanceof Error ? error.message : error); 65 | throw error; 66 | } 67 | finally { 68 | if (client) { 69 | await client.close(); 70 | } 71 | } 72 | } 73 | /** 74 | * Check if Chrome debugging port is accessible 75 | * @returns Promise 76 | */ 77 | async isAvailable() { 78 | try { 79 | await this.listTabs(); 80 | return true; 81 | } 82 | catch { 83 | return false; 84 | } 85 | } 86 | /** 87 | * Capture a screenshot of a specific Chrome tab 88 | * @param tabId The ID of the tab to capture 89 | * @param options Screenshot options (format, quality, fullPage) 90 | * @returns Promise with the base64-encoded screenshot data 91 | * @throws Error if the tab is not found or screenshot capture fails 92 | */ 93 | async captureScreenshot(tabId, options = {}) { 94 | console.error(`ChromeAPI: Attempting to capture screenshot of tab ${tabId}`); 95 | let client; 96 | try { 97 | // Connect to the specific tab 98 | client = await CDP({ target: tabId, port: this.port }); 99 | if (!client) { 100 | throw new Error('Failed to connect to Chrome DevTools'); 101 | } 102 | // Enable Page domain for screenshot capabilities 103 | await client.Page.enable(); 104 | // If fullPage is requested, we need to get the full page dimensions 105 | if (options.fullPage) { 106 | // Get the full page dimensions 107 | const { root } = await client.DOM.getDocument(); 108 | const { model } = await client.DOM.getBoxModel({ nodeId: root.nodeId }); 109 | const height = model.height; 110 | // Set viewport to full page height 111 | await client.Emulation.setDeviceMetricsOverride({ 112 | width: 1920, // Standard width 113 | height: Math.ceil(height), 114 | deviceScaleFactor: 1, 115 | mobile: false 116 | }); 117 | } 118 | // Capture the screenshot 119 | const result = await client.Page.captureScreenshot({ 120 | format: options.format || 'png', 121 | quality: options.format === 'jpeg' ? options.quality || 80 : undefined, 122 | fromSurface: true, 123 | captureBeyondViewport: options.fullPage || false 124 | }); 125 | console.error('ChromeAPI: Screenshot capture successful'); 126 | return result.data; 127 | } 128 | catch (error) { 129 | console.error('ChromeAPI: Screenshot capture failed:', error instanceof Error ? error.message : error); 130 | throw error; 131 | } 132 | finally { 133 | if (client) { 134 | // Reset device metrics if we modified them 135 | if (options.fullPage) { 136 | await client.Emulation.clearDeviceMetricsOverride(); 137 | } 138 | await client.close(); 139 | } 140 | } 141 | } 142 | /** 143 | * Capture network events (XHR/Fetch) from a specific Chrome tab 144 | * @param tabId The ID of the tab to capture events from 145 | * @param options Capture options (duration, filters) 146 | * @returns Promise with the captured network events 147 | * @throws Error if the tab is not found or capture fails 148 | */ 149 | async captureNetworkEvents(tabId, options = {}) { 150 | console.error(`ChromeAPI: Attempting to capture network events from tab ${tabId}`); 151 | let client; 152 | try { 153 | // Connect to the specific tab 154 | client = await CDP({ target: tabId, port: this.port }); 155 | if (!client) { 156 | throw new Error('Failed to connect to Chrome DevTools'); 157 | } 158 | // Enable Network domain 159 | await client.Network.enable(); 160 | const events = []; 161 | const requests = new Map(); 162 | // Set up event handlers 163 | const requestHandler = (params) => { 164 | const request = { 165 | type: (params.type?.toLowerCase() === 'xhr' ? 'xhr' : 'fetch'), 166 | method: params.request.method, 167 | url: params.request.url, 168 | requestHeaders: params.request.headers, 169 | timing: { 170 | requestTime: params.timestamp 171 | } 172 | }; 173 | // Apply filters if specified 174 | if (options.filters) { 175 | if (options.filters.types && !options.filters.types.includes(request.type)) { 176 | return; 177 | } 178 | if (options.filters.urlPattern && !request.url.match(options.filters.urlPattern)) { 179 | return; 180 | } 181 | } 182 | requests.set(params.requestId, request); 183 | }; 184 | const responseHandler = (params) => { 185 | const request = requests.get(params.requestId); 186 | if (request) { 187 | request.status = params.response.status; 188 | request.statusText = params.response.statusText; 189 | request.responseHeaders = params.response.headers; 190 | request.timing.responseTime = params.timestamp; 191 | events.push(request); 192 | } 193 | }; 194 | // Register event handlers 195 | client.Network.requestWillBeSent(requestHandler); 196 | client.Network.responseReceived(responseHandler); 197 | // Wait for specified duration 198 | const duration = options.duration || 10; 199 | await new Promise(resolve => setTimeout(resolve, duration * 1000)); 200 | console.error('ChromeAPI: Network event capture successful'); 201 | return events; 202 | } 203 | catch (error) { 204 | console.error('ChromeAPI: Network event capture failed:', error instanceof Error ? error.message : error); 205 | throw error; 206 | } 207 | finally { 208 | if (client) { 209 | await client.close(); 210 | } 211 | } 212 | } 213 | /** 214 | * Navigate a Chrome tab to a specific URL 215 | * @param tabId The ID of the tab to load the URL in 216 | * @param url The URL to load 217 | * @returns Promise 218 | * @throws Error if the tab is not found or navigation fails 219 | */ 220 | async loadUrl(tabId, url) { 221 | console.error(`ChromeAPI: Attempting to load URL ${url} in tab ${tabId}`); 222 | let client; 223 | try { 224 | // Connect to the specific tab 225 | client = await CDP({ target: tabId, port: this.port }); 226 | if (!client) { 227 | throw new Error('Failed to connect to Chrome DevTools'); 228 | } 229 | // Enable Page domain for navigation 230 | await client.Page.enable(); 231 | // Navigate to the URL and wait for load 232 | await client.Page.navigate({ url }); 233 | await client.Page.loadEventFired(); 234 | console.error('ChromeAPI: URL loading successful'); 235 | } 236 | catch (error) { 237 | console.error('ChromeAPI: URL loading failed:', error instanceof Error ? error.message : error); 238 | throw error; 239 | } 240 | finally { 241 | if (client) { 242 | await client.close(); 243 | } 244 | } 245 | } 246 | /** 247 | * Query DOM elements using a CSS selector 248 | * @param tabId The ID of the tab to query 249 | * @param selector CSS selector to find elements 250 | * @returns Promise Array of matching DOM elements with their properties 251 | * @throws Error if the tab is not found or query fails 252 | */ 253 | async queryDOMElements(tabId, selector) { 254 | console.error(`ChromeAPI: Attempting to query DOM elements in tab ${tabId} with selector "${selector}"`); 255 | let client; 256 | try { 257 | // Connect to the specific tab 258 | client = await CDP({ target: tabId, port: this.port }); 259 | if (!client) { 260 | throw new Error('Failed to connect to Chrome DevTools'); 261 | } 262 | // Enable necessary domains 263 | await client.DOM.enable(); 264 | await client.Runtime.enable(); 265 | // Get the document root 266 | const { root } = await client.DOM.getDocument(); 267 | // Find elements matching the selector 268 | const { nodeIds } = await client.DOM.querySelectorAll({ 269 | nodeId: root.nodeId, 270 | selector: selector 271 | }); 272 | // Get detailed information for each element 273 | const elements = await Promise.all(nodeIds.map(async (nodeId) => { 274 | if (!client) { 275 | throw new Error('Client disconnected'); 276 | } 277 | // Get node details 278 | const { node } = await client.DOM.describeNode({ nodeId }); 279 | // Get node box model for position and dimensions 280 | const boxModel = await client.DOM.getBoxModel({ nodeId }) 281 | .catch(() => null); // Some elements might not have a box model 282 | // Check visibility using Runtime.evaluate 283 | const result = await client.Runtime.evaluate({ 284 | expression: ` 285 | (function(selector) { 286 | const element = document.querySelector(selector); 287 | if (!element) return false; 288 | const style = window.getComputedStyle(element); 289 | return style.display !== 'none' && 290 | style.visibility !== 'hidden' && 291 | style.opacity !== '0'; 292 | })('${selector}') 293 | `, 294 | returnByValue: true 295 | }); 296 | // Extract ARIA attributes 297 | const ariaAttributes = {}; 298 | if (node.attributes) { 299 | for (let i = 0; i < node.attributes.length; i += 2) { 300 | const name = node.attributes[i]; 301 | if (name.startsWith('aria-')) { 302 | ariaAttributes[name] = node.attributes[i + 1]; 303 | } 304 | } 305 | } 306 | // Convert attributes array to object 307 | const attributes = {}; 308 | if (node.attributes) { 309 | for (let i = 0; i < node.attributes.length; i += 2) { 310 | attributes[node.attributes[i]] = node.attributes[i + 1]; 311 | } 312 | } 313 | return { 314 | nodeId, 315 | tagName: node.nodeName.toLowerCase(), 316 | textContent: node.nodeValue || null, 317 | attributes, 318 | boundingBox: boxModel ? { 319 | x: boxModel.model.content[0], 320 | y: boxModel.model.content[1], 321 | width: boxModel.model.width, 322 | height: boxModel.model.height 323 | } : null, 324 | isVisible: result.result.value, 325 | ariaAttributes 326 | }; 327 | })); 328 | console.error(`ChromeAPI: Successfully found ${elements.length} elements matching selector`); 329 | return elements; 330 | } 331 | catch (error) { 332 | console.error('ChromeAPI: DOM query failed:', error instanceof Error ? error.message : error); 333 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 334 | 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.`); 335 | } 336 | finally { 337 | if (client) { 338 | await client.close(); 339 | } 340 | } 341 | } 342 | /** 343 | * Click on a DOM element matching a CSS selector 344 | * @param tabId The ID of the tab containing the element 345 | * @param selector CSS selector to find the element to click 346 | * @returns Promise 347 | * @throws Error if the tab is not found, element is not found, or click fails 348 | */ 349 | async clickElement(tabId, selector) { 350 | console.error(`ChromeAPI: Attempting to click element in tab ${tabId} with selector "${selector}"`); 351 | let client; 352 | try { 353 | // Connect to the specific tab 354 | client = await CDP({ target: tabId, port: this.port }); 355 | if (!client) { 356 | throw new Error('Failed to connect to Chrome DevTools'); 357 | } 358 | // Enable necessary domains 359 | await client.DOM.enable(); 360 | await client.Runtime.enable(); 361 | // Get the document root 362 | const { root } = await client.DOM.getDocument(); 363 | // Find the element 364 | const { nodeIds } = await client.DOM.querySelectorAll({ 365 | nodeId: root.nodeId, 366 | selector: selector 367 | }); 368 | if (nodeIds.length === 0) { 369 | throw new Error(`No element found matching selector: ${selector}`); 370 | } 371 | // Get element's box model for coordinates 372 | const { model } = await client.DOM.getBoxModel({ nodeId: nodeIds[0] }); 373 | // Calculate center point 374 | const centerX = model.content[0] + (model.width / 2); 375 | const centerY = model.content[1] + (model.height / 2); 376 | // Dispatch click event using Runtime.evaluate 377 | await client.Runtime.evaluate({ 378 | expression: ` 379 | (() => { 380 | const element = document.querySelector('${selector}'); 381 | if (!element) throw new Error('Element not found'); 382 | 383 | const clickEvent = new MouseEvent('click', { 384 | bubbles: true, 385 | cancelable: true, 386 | view: window, 387 | clientX: ${Math.round(centerX)}, 388 | clientY: ${Math.round(centerY)} 389 | }); 390 | 391 | element.dispatchEvent(clickEvent); 392 | })() 393 | `, 394 | awaitPromise: true 395 | }); 396 | // Set up console listener before the click 397 | let consoleMessages = []; 398 | const consolePromise = new Promise((resolve) => { 399 | if (!client) 400 | return; 401 | client.Runtime.consoleAPICalled(({ type, args }) => { 402 | const message = args.map(arg => arg.value || arg.description).join(' '); 403 | consoleMessages.push(`[${type}] ${message}`); 404 | console.error(`Chrome Console: ${type}:`, message); 405 | resolve(); // Resolve when we get a console message 406 | }); 407 | }); 408 | // Set up a timeout promise 409 | const timeoutPromise = new Promise((resolve) => { 410 | setTimeout(resolve, 1000); 411 | }); 412 | // Click the element 413 | await client.Runtime.evaluate({ 414 | expression: ` 415 | (() => { 416 | const element = document.querySelector('${selector}'); 417 | if (!element) throw new Error('Element not found'); 418 | 419 | const clickEvent = new MouseEvent('click', { 420 | bubbles: true, 421 | cancelable: true, 422 | view: window, 423 | clientX: ${Math.round(centerX)}, 424 | clientY: ${Math.round(centerY)} 425 | }); 426 | 427 | element.dispatchEvent(clickEvent); 428 | })() 429 | `, 430 | awaitPromise: true 431 | }); 432 | // Wait for either a console message or timeout 433 | await Promise.race([consolePromise, timeoutPromise]); 434 | console.error('ChromeAPI: Successfully clicked element'); 435 | return { consoleOutput: consoleMessages }; 436 | } 437 | catch (error) { 438 | console.error('ChromeAPI: Element click failed:', error instanceof Error ? error.message : error); 439 | throw error; 440 | } 441 | finally { 442 | if (client) { 443 | await client.close(); 444 | } 445 | } 446 | } 447 | get port() { 448 | const portMatch = this.baseUrl.match(/:(\d+)$/); 449 | return portMatch ? parseInt(portMatch[1]) : 9222; 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | export {}; 3 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { ChromeAPI } from './chrome-api.js'; 5 | import { processImage, saveImage } from './image-utils.js'; 6 | import { z } from 'zod'; 7 | // Get Chrome debug URL from environment variable or use default 8 | const chromeDebugUrl = process.env.CHROME_DEBUG_URL || 'http://localhost:9222'; 9 | console.error(`Using Chrome debug URL: ${chromeDebugUrl}`); 10 | const chromeApi = new ChromeAPI({ baseUrl: chromeDebugUrl }); 11 | // Create the MCP server 12 | const server = new McpServer({ 13 | name: 'chrome-tools', 14 | version: '1.3.0' 15 | }); 16 | // Add the list_tabs tool 17 | server.tool('list_tabs', {}, // No input parameters needed 18 | async () => { 19 | try { 20 | console.error('Attempting to list Chrome tabs...'); 21 | const tabs = await chromeApi.listTabs(); 22 | console.error(`Successfully found ${tabs.length} tabs`); 23 | return { 24 | content: [{ 25 | type: 'text', 26 | text: JSON.stringify(tabs, null, 2) 27 | }] 28 | }; 29 | } 30 | catch (error) { 31 | console.error('Error in list_tabs tool:', error); 32 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 33 | return { 34 | content: [{ 35 | type: 'text', 36 | text: `Error: ${errorMessage}` 37 | }], 38 | isError: true 39 | }; 40 | } 41 | }); 42 | // Add the capture_screenshot tool 43 | server.tool('capture_screenshot', { 44 | tabId: z.string().describe('ID of the Chrome tab to capture. Only send this unless you are having issues with the result.'), 45 | format: z.enum(['jpeg', 'png']).optional() 46 | .describe('Initial capture format (jpeg/png). Note: Final output will be WebP with PNG fallback'), 47 | quality: z.number().min(1).max(100).optional() 48 | .describe('Initial capture quality (1-100). Note: Final output uses WebP quality settings'), 49 | fullPage: z.boolean().optional() 50 | .describe('Capture full scrollable page') 51 | }, async (params) => { 52 | try { 53 | console.error(`Attempting to capture screenshot of tab ${params.tabId}...`); 54 | const rawBase64Data = await chromeApi.captureScreenshot(params.tabId, { 55 | format: params.format, 56 | quality: params.quality, 57 | fullPage: params.fullPage 58 | }); 59 | console.error('Screenshot captured, optimizing with WebP...'); 60 | try { 61 | // Process image with the following strategy: 62 | // 1. Try WebP with quality 80 (best balance of quality/size) 63 | // 2. If >1MB, try WebP with quality 60 and near-lossless 64 | // 3. If WebP fails, fall back to PNG with maximum compression 65 | const processedImage = await processImage(rawBase64Data); 66 | console.error(`Image optimized successfully (${processedImage.data.startsWith('data:image/webp') ? 'WebP' : 'PNG'}, ${Math.round(processedImage.size / 1024)}KB)`); 67 | // Save the image and get the filepath 68 | const filepath = await saveImage(processedImage); 69 | console.error(`Screenshot saved to: ${filepath}`); 70 | return { 71 | content: [{ 72 | type: 'text', 73 | text: JSON.stringify({ 74 | status: 'Screenshot successful.', 75 | path: filepath 76 | }) 77 | }] 78 | }; 79 | } 80 | catch (error) { 81 | console.error('Image processing failed:', error); 82 | return { 83 | content: [{ 84 | type: 'text', 85 | text: `Error processing screenshot: ${error instanceof Error ? error.message : 'Unknown error'}` 86 | }], 87 | isError: true 88 | }; 89 | } 90 | } 91 | catch (error) { 92 | console.error('Error in capture_screenshot tool:', error); 93 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 94 | return { 95 | content: [{ 96 | type: 'text', 97 | text: `Error: ${errorMessage}` 98 | }], 99 | isError: true 100 | }; 101 | } 102 | }); 103 | // Add the execute_script tool 104 | server.tool('execute_script', { 105 | tabId: z.string().describe('ID of the Chrome tab to execute the script in'), 106 | script: z.string().describe('JavaScript code to execute in the tab') 107 | }, async (params) => { 108 | try { 109 | console.error(`Attempting to execute script in tab ${params.tabId}...`); 110 | const result = await chromeApi.executeScript(params.tabId, params.script); 111 | console.error('Script execution successful'); 112 | return { 113 | content: [{ 114 | type: 'text', 115 | text: result || 'undefined' 116 | }] 117 | }; 118 | } 119 | catch (error) { 120 | console.error('Error in execute_script tool:', error); 121 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 122 | return { 123 | content: [{ 124 | type: 'text', 125 | text: `Error: ${errorMessage}` 126 | }], 127 | isError: true 128 | }; 129 | } 130 | }); 131 | // Log when server starts 132 | console.error('Chrome Tools MCP Server starting...'); 133 | // Start the server 134 | const transport = new StdioServerTransport(); 135 | server.connect(transport).catch(console.error); 136 | // Add the load_url tool 137 | server.tool('load_url', { 138 | tabId: z.string().describe('ID of the Chrome tab to load the URL in'), 139 | url: z.string().url().describe('URL to load in the tab') 140 | }, async (params) => { 141 | try { 142 | console.error(`Attempting to load URL ${params.url} in tab ${params.tabId}...`); 143 | await chromeApi.loadUrl(params.tabId, params.url); 144 | console.error('URL loading successful'); 145 | return { 146 | content: [{ 147 | type: 'text', 148 | text: `Successfully loaded ${params.url} in tab ${params.tabId}` 149 | }] 150 | }; 151 | } 152 | catch (error) { 153 | console.error('Error in load_url tool:', error); 154 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 155 | return { 156 | content: [{ 157 | type: 'text', 158 | text: `Error: ${errorMessage}` 159 | }], 160 | isError: true 161 | }; 162 | } 163 | }); 164 | // Add the capture_network_events tool 165 | server.tool('capture_network_events', { 166 | tabId: z.string().describe('ID of the Chrome tab to monitor'), 167 | duration: z.number().min(1).max(60).optional() 168 | .describe('Duration in seconds to capture events (default: 10)'), 169 | filters: z.object({ 170 | types: z.array(z.enum(['fetch', 'xhr'])).optional() 171 | .describe('Types of requests to capture'), 172 | urlPattern: z.string().optional() 173 | .describe('Only capture URLs matching this pattern') 174 | }).optional() 175 | }, async (params) => { 176 | try { 177 | console.error(`Attempting to capture network events from tab ${params.tabId}...`); 178 | const events = await chromeApi.captureNetworkEvents(params.tabId, { 179 | duration: params.duration, 180 | filters: params.filters 181 | }); 182 | console.error(`Network event capture successful, captured ${events.length} events`); 183 | return { 184 | content: [{ 185 | type: 'text', 186 | text: JSON.stringify(events, null, 2) 187 | }] 188 | }; 189 | } 190 | catch (error) { 191 | console.error('Error in capture_network_events tool:', error); 192 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 193 | return { 194 | content: [{ 195 | type: 'text', 196 | text: `Error: ${errorMessage}` 197 | }], 198 | isError: true 199 | }; 200 | } 201 | }); 202 | // Add the query_dom_elements tool 203 | server.tool('query_dom_elements', { 204 | tabId: z.string().describe('ID of the Chrome tab to query'), 205 | selector: z.string().describe('CSS selector to find elements') 206 | }, async (params) => { 207 | try { 208 | console.error(`Attempting to query DOM elements in tab ${params.tabId}...`); 209 | const elements = await chromeApi.queryDOMElements(params.tabId, params.selector); 210 | console.error(`Successfully found ${elements.length} elements matching selector`); 211 | return { 212 | content: [{ 213 | type: 'text', 214 | text: JSON.stringify(elements, null, 2) 215 | }] 216 | }; 217 | } 218 | catch (error) { 219 | console.error('Error in query_dom_elements tool:', error); 220 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 221 | return { 222 | content: [{ 223 | type: 'text', 224 | text: `Error: ${errorMessage}` 225 | }], 226 | isError: true 227 | }; 228 | } 229 | }); 230 | // Add the click_element tool 231 | server.tool('click_element', { 232 | tabId: z.string().describe('ID of the Chrome tab containing the element'), 233 | selector: z.string().describe('CSS selector to find the element to click') 234 | }, async (params) => { 235 | try { 236 | console.error(`Attempting to click element in tab ${params.tabId}...`); 237 | const result = await chromeApi.clickElement(params.tabId, params.selector); 238 | console.error('Successfully clicked element'); 239 | return { 240 | content: [{ 241 | type: 'text', 242 | text: JSON.stringify({ 243 | message: 'Successfully clicked element', 244 | consoleOutput: result.consoleOutput 245 | }, null, 2) 246 | }] 247 | }; 248 | } 249 | catch (error) { 250 | console.error('Error in click_element tool:', error); 251 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 252 | return { 253 | content: [{ 254 | type: 'text', 255 | text: `Error: ${errorMessage}` 256 | }], 257 | isError: true 258 | }; 259 | } 260 | }); 261 | // Handle process termination 262 | process.on('SIGINT', () => { 263 | server.close().catch(console.error); 264 | process.exit(0); 265 | }); 266 | -------------------------------------------------------------------------------- /dist/types.d.ts: -------------------------------------------------------------------------------- 1 | export type { Target as ChromeTab } from 'chrome-remote-interface'; 2 | export interface DOMElement { 3 | nodeId: number; 4 | tagName: string; 5 | textContent: string | null; 6 | attributes: Record; 7 | boundingBox: { 8 | x: number; 9 | y: number; 10 | width: number; 11 | height: number; 12 | } | null; 13 | isVisible: boolean; 14 | ariaAttributes: Record; 15 | } 16 | -------------------------------------------------------------------------------- /dist/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nicholmikey/chrome-tools", 3 | "version": "1.2.2", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@nicholmikey/chrome-tools", 9 | "version": "1.2.2", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@modelcontextprotocol/sdk": "^1.5.0", 13 | "@types/ws": "^8.5.14", 14 | "axios": "^1.7.9", 15 | "chrome-remote-interface": "^0.33.2", 16 | "sharp": "^0.32.6", 17 | "ts-node": "^10.9.2", 18 | "typescript": "^5.7.3", 19 | "ws": "^8.18.0", 20 | "zod": "^3.24.2" 21 | }, 22 | "bin": { 23 | "mcp-chrome-tools": "dist/index.js" 24 | }, 25 | "devDependencies": { 26 | "@types/chrome-remote-interface": "^0.31.14" 27 | } 28 | }, 29 | "node_modules/@cspotcode/source-map-support": { 30 | "version": "0.8.1", 31 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 32 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 33 | "dependencies": { 34 | "@jridgewell/trace-mapping": "0.3.9" 35 | }, 36 | "engines": { 37 | "node": ">=12" 38 | } 39 | }, 40 | "node_modules/@jridgewell/resolve-uri": { 41 | "version": "3.1.2", 42 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 43 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 44 | "engines": { 45 | "node": ">=6.0.0" 46 | } 47 | }, 48 | "node_modules/@jridgewell/sourcemap-codec": { 49 | "version": "1.5.0", 50 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 51 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" 52 | }, 53 | "node_modules/@jridgewell/trace-mapping": { 54 | "version": "0.3.9", 55 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 56 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 57 | "dependencies": { 58 | "@jridgewell/resolve-uri": "^3.0.3", 59 | "@jridgewell/sourcemap-codec": "^1.4.10" 60 | } 61 | }, 62 | "node_modules/@modelcontextprotocol/sdk": { 63 | "version": "1.5.0", 64 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.5.0.tgz", 65 | "integrity": "sha512-IJ+5iVVs8FCumIHxWqpwgkwOzyhtHVKy45s6Ug7Dv0MfRpaYisH8QQ87rIWeWdOzlk8sfhitZ7HCyQZk7d6b8w==", 66 | "dependencies": { 67 | "content-type": "^1.0.5", 68 | "eventsource": "^3.0.2", 69 | "raw-body": "^3.0.0", 70 | "zod": "^3.23.8", 71 | "zod-to-json-schema": "^3.24.1" 72 | }, 73 | "engines": { 74 | "node": ">=18" 75 | } 76 | }, 77 | "node_modules/@tsconfig/node10": { 78 | "version": "1.0.11", 79 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", 80 | "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" 81 | }, 82 | "node_modules/@tsconfig/node12": { 83 | "version": "1.0.11", 84 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 85 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" 86 | }, 87 | "node_modules/@tsconfig/node14": { 88 | "version": "1.0.3", 89 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 90 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" 91 | }, 92 | "node_modules/@tsconfig/node16": { 93 | "version": "1.0.4", 94 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 95 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" 96 | }, 97 | "node_modules/@types/chrome-remote-interface": { 98 | "version": "0.31.14", 99 | "resolved": "https://registry.npmjs.org/@types/chrome-remote-interface/-/chrome-remote-interface-0.31.14.tgz", 100 | "integrity": "sha512-H9hTcLu1y+Ms6GDPXXeGhgxaOSD69yEo674vjJw5EeW1tTwYo8fEkf7A9nWlnO6ArJsS7c41iZeX6mRDQ1LhEw==", 101 | "dev": true, 102 | "dependencies": { 103 | "devtools-protocol": "0.0.927104" 104 | } 105 | }, 106 | "node_modules/@types/node": { 107 | "version": "22.13.4", 108 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", 109 | "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", 110 | "dependencies": { 111 | "undici-types": "~6.20.0" 112 | } 113 | }, 114 | "node_modules/@types/ws": { 115 | "version": "8.5.14", 116 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", 117 | "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", 118 | "dependencies": { 119 | "@types/node": "*" 120 | } 121 | }, 122 | "node_modules/acorn": { 123 | "version": "8.14.0", 124 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 125 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 126 | "bin": { 127 | "acorn": "bin/acorn" 128 | }, 129 | "engines": { 130 | "node": ">=0.4.0" 131 | } 132 | }, 133 | "node_modules/acorn-walk": { 134 | "version": "8.3.4", 135 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", 136 | "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", 137 | "dependencies": { 138 | "acorn": "^8.11.0" 139 | }, 140 | "engines": { 141 | "node": ">=0.4.0" 142 | } 143 | }, 144 | "node_modules/arg": { 145 | "version": "4.1.3", 146 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 147 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" 148 | }, 149 | "node_modules/asynckit": { 150 | "version": "0.4.0", 151 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 152 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 153 | }, 154 | "node_modules/axios": { 155 | "version": "1.7.9", 156 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", 157 | "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", 158 | "dependencies": { 159 | "follow-redirects": "^1.15.6", 160 | "form-data": "^4.0.0", 161 | "proxy-from-env": "^1.1.0" 162 | } 163 | }, 164 | "node_modules/b4a": { 165 | "version": "1.6.7", 166 | "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", 167 | "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" 168 | }, 169 | "node_modules/bare-events": { 170 | "version": "2.5.4", 171 | "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", 172 | "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", 173 | "optional": true 174 | }, 175 | "node_modules/bare-fs": { 176 | "version": "4.0.1", 177 | "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.1.tgz", 178 | "integrity": "sha512-ilQs4fm/l9eMfWY2dY0WCIUplSUp7U0CT1vrqMg1MUdeZl4fypu5UP0XcDBK5WBQPJAKP1b7XEodISmekH/CEg==", 179 | "optional": true, 180 | "dependencies": { 181 | "bare-events": "^2.0.0", 182 | "bare-path": "^3.0.0", 183 | "bare-stream": "^2.0.0" 184 | }, 185 | "engines": { 186 | "bare": ">=1.7.0" 187 | } 188 | }, 189 | "node_modules/bare-os": { 190 | "version": "3.4.0", 191 | "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.4.0.tgz", 192 | "integrity": "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==", 193 | "optional": true, 194 | "engines": { 195 | "bare": ">=1.6.0" 196 | } 197 | }, 198 | "node_modules/bare-path": { 199 | "version": "3.0.0", 200 | "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", 201 | "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", 202 | "optional": true, 203 | "dependencies": { 204 | "bare-os": "^3.0.1" 205 | } 206 | }, 207 | "node_modules/bare-stream": { 208 | "version": "2.6.5", 209 | "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", 210 | "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", 211 | "optional": true, 212 | "dependencies": { 213 | "streamx": "^2.21.0" 214 | }, 215 | "peerDependencies": { 216 | "bare-buffer": "*", 217 | "bare-events": "*" 218 | }, 219 | "peerDependenciesMeta": { 220 | "bare-buffer": { 221 | "optional": true 222 | }, 223 | "bare-events": { 224 | "optional": true 225 | } 226 | } 227 | }, 228 | "node_modules/base64-js": { 229 | "version": "1.5.1", 230 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 231 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 232 | "funding": [ 233 | { 234 | "type": "github", 235 | "url": "https://github.com/sponsors/feross" 236 | }, 237 | { 238 | "type": "patreon", 239 | "url": "https://www.patreon.com/feross" 240 | }, 241 | { 242 | "type": "consulting", 243 | "url": "https://feross.org/support" 244 | } 245 | ] 246 | }, 247 | "node_modules/bl": { 248 | "version": "4.1.0", 249 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 250 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 251 | "dependencies": { 252 | "buffer": "^5.5.0", 253 | "inherits": "^2.0.4", 254 | "readable-stream": "^3.4.0" 255 | } 256 | }, 257 | "node_modules/buffer": { 258 | "version": "5.7.1", 259 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 260 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 261 | "funding": [ 262 | { 263 | "type": "github", 264 | "url": "https://github.com/sponsors/feross" 265 | }, 266 | { 267 | "type": "patreon", 268 | "url": "https://www.patreon.com/feross" 269 | }, 270 | { 271 | "type": "consulting", 272 | "url": "https://feross.org/support" 273 | } 274 | ], 275 | "dependencies": { 276 | "base64-js": "^1.3.1", 277 | "ieee754": "^1.1.13" 278 | } 279 | }, 280 | "node_modules/bytes": { 281 | "version": "3.1.2", 282 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 283 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 284 | "engines": { 285 | "node": ">= 0.8" 286 | } 287 | }, 288 | "node_modules/call-bind-apply-helpers": { 289 | "version": "1.0.2", 290 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 291 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 292 | "dependencies": { 293 | "es-errors": "^1.3.0", 294 | "function-bind": "^1.1.2" 295 | }, 296 | "engines": { 297 | "node": ">= 0.4" 298 | } 299 | }, 300 | "node_modules/chownr": { 301 | "version": "1.1.4", 302 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 303 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 304 | }, 305 | "node_modules/chrome-remote-interface": { 306 | "version": "0.33.2", 307 | "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.2.tgz", 308 | "integrity": "sha512-wvm9cOeBTrb218EC+6DteGt92iXr2iY0+XJP30f15JVDhqvWvJEVACh9GvUm8b9Yd8bxQivaLSb8k7mgrbyomQ==", 309 | "dependencies": { 310 | "commander": "2.11.x", 311 | "ws": "^7.2.0" 312 | }, 313 | "bin": { 314 | "chrome-remote-interface": "bin/client.js" 315 | } 316 | }, 317 | "node_modules/chrome-remote-interface/node_modules/ws": { 318 | "version": "7.5.10", 319 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", 320 | "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", 321 | "engines": { 322 | "node": ">=8.3.0" 323 | }, 324 | "peerDependencies": { 325 | "bufferutil": "^4.0.1", 326 | "utf-8-validate": "^5.0.2" 327 | }, 328 | "peerDependenciesMeta": { 329 | "bufferutil": { 330 | "optional": true 331 | }, 332 | "utf-8-validate": { 333 | "optional": true 334 | } 335 | } 336 | }, 337 | "node_modules/color": { 338 | "version": "4.2.3", 339 | "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 340 | "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 341 | "dependencies": { 342 | "color-convert": "^2.0.1", 343 | "color-string": "^1.9.0" 344 | }, 345 | "engines": { 346 | "node": ">=12.5.0" 347 | } 348 | }, 349 | "node_modules/color-convert": { 350 | "version": "2.0.1", 351 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 352 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 353 | "dependencies": { 354 | "color-name": "~1.1.4" 355 | }, 356 | "engines": { 357 | "node": ">=7.0.0" 358 | } 359 | }, 360 | "node_modules/color-name": { 361 | "version": "1.1.4", 362 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 363 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 364 | }, 365 | "node_modules/color-string": { 366 | "version": "1.9.1", 367 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 368 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 369 | "dependencies": { 370 | "color-name": "^1.0.0", 371 | "simple-swizzle": "^0.2.2" 372 | } 373 | }, 374 | "node_modules/combined-stream": { 375 | "version": "1.0.8", 376 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 377 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 378 | "dependencies": { 379 | "delayed-stream": "~1.0.0" 380 | }, 381 | "engines": { 382 | "node": ">= 0.8" 383 | } 384 | }, 385 | "node_modules/commander": { 386 | "version": "2.11.0", 387 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", 388 | "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" 389 | }, 390 | "node_modules/content-type": { 391 | "version": "1.0.5", 392 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 393 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 394 | "engines": { 395 | "node": ">= 0.6" 396 | } 397 | }, 398 | "node_modules/create-require": { 399 | "version": "1.1.1", 400 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 401 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" 402 | }, 403 | "node_modules/decompress-response": { 404 | "version": "6.0.0", 405 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 406 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 407 | "dependencies": { 408 | "mimic-response": "^3.1.0" 409 | }, 410 | "engines": { 411 | "node": ">=10" 412 | }, 413 | "funding": { 414 | "url": "https://github.com/sponsors/sindresorhus" 415 | } 416 | }, 417 | "node_modules/deep-extend": { 418 | "version": "0.6.0", 419 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 420 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 421 | "engines": { 422 | "node": ">=4.0.0" 423 | } 424 | }, 425 | "node_modules/delayed-stream": { 426 | "version": "1.0.0", 427 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 428 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 429 | "engines": { 430 | "node": ">=0.4.0" 431 | } 432 | }, 433 | "node_modules/depd": { 434 | "version": "2.0.0", 435 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 436 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 437 | "engines": { 438 | "node": ">= 0.8" 439 | } 440 | }, 441 | "node_modules/detect-libc": { 442 | "version": "2.0.3", 443 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", 444 | "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", 445 | "engines": { 446 | "node": ">=8" 447 | } 448 | }, 449 | "node_modules/devtools-protocol": { 450 | "version": "0.0.927104", 451 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.927104.tgz", 452 | "integrity": "sha512-5jfffjSuTOv0Lz53wTNNTcCUV8rv7d82AhYcapj28bC2B5tDxEZzVb7k51cNxZP2KHw24QE+sW7ZuSeD9NfMpA==", 453 | "dev": true 454 | }, 455 | "node_modules/diff": { 456 | "version": "4.0.2", 457 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 458 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 459 | "engines": { 460 | "node": ">=0.3.1" 461 | } 462 | }, 463 | "node_modules/dunder-proto": { 464 | "version": "1.0.1", 465 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 466 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 467 | "dependencies": { 468 | "call-bind-apply-helpers": "^1.0.1", 469 | "es-errors": "^1.3.0", 470 | "gopd": "^1.2.0" 471 | }, 472 | "engines": { 473 | "node": ">= 0.4" 474 | } 475 | }, 476 | "node_modules/end-of-stream": { 477 | "version": "1.4.4", 478 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 479 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 480 | "dependencies": { 481 | "once": "^1.4.0" 482 | } 483 | }, 484 | "node_modules/es-define-property": { 485 | "version": "1.0.1", 486 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 487 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 488 | "engines": { 489 | "node": ">= 0.4" 490 | } 491 | }, 492 | "node_modules/es-errors": { 493 | "version": "1.3.0", 494 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 495 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 496 | "engines": { 497 | "node": ">= 0.4" 498 | } 499 | }, 500 | "node_modules/es-object-atoms": { 501 | "version": "1.1.1", 502 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 503 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 504 | "dependencies": { 505 | "es-errors": "^1.3.0" 506 | }, 507 | "engines": { 508 | "node": ">= 0.4" 509 | } 510 | }, 511 | "node_modules/es-set-tostringtag": { 512 | "version": "2.1.0", 513 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 514 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 515 | "dependencies": { 516 | "es-errors": "^1.3.0", 517 | "get-intrinsic": "^1.2.6", 518 | "has-tostringtag": "^1.0.2", 519 | "hasown": "^2.0.2" 520 | }, 521 | "engines": { 522 | "node": ">= 0.4" 523 | } 524 | }, 525 | "node_modules/eventsource": { 526 | "version": "3.0.5", 527 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.5.tgz", 528 | "integrity": "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==", 529 | "dependencies": { 530 | "eventsource-parser": "^3.0.0" 531 | }, 532 | "engines": { 533 | "node": ">=18.0.0" 534 | } 535 | }, 536 | "node_modules/eventsource-parser": { 537 | "version": "3.0.0", 538 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", 539 | "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", 540 | "engines": { 541 | "node": ">=18.0.0" 542 | } 543 | }, 544 | "node_modules/expand-template": { 545 | "version": "2.0.3", 546 | "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 547 | "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 548 | "engines": { 549 | "node": ">=6" 550 | } 551 | }, 552 | "node_modules/fast-fifo": { 553 | "version": "1.3.2", 554 | "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", 555 | "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" 556 | }, 557 | "node_modules/follow-redirects": { 558 | "version": "1.15.9", 559 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 560 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 561 | "funding": [ 562 | { 563 | "type": "individual", 564 | "url": "https://github.com/sponsors/RubenVerborgh" 565 | } 566 | ], 567 | "engines": { 568 | "node": ">=4.0" 569 | }, 570 | "peerDependenciesMeta": { 571 | "debug": { 572 | "optional": true 573 | } 574 | } 575 | }, 576 | "node_modules/form-data": { 577 | "version": "4.0.2", 578 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", 579 | "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", 580 | "dependencies": { 581 | "asynckit": "^0.4.0", 582 | "combined-stream": "^1.0.8", 583 | "es-set-tostringtag": "^2.1.0", 584 | "mime-types": "^2.1.12" 585 | }, 586 | "engines": { 587 | "node": ">= 6" 588 | } 589 | }, 590 | "node_modules/fs-constants": { 591 | "version": "1.0.0", 592 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 593 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 594 | }, 595 | "node_modules/function-bind": { 596 | "version": "1.1.2", 597 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 598 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 599 | "funding": { 600 | "url": "https://github.com/sponsors/ljharb" 601 | } 602 | }, 603 | "node_modules/get-intrinsic": { 604 | "version": "1.2.7", 605 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", 606 | "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", 607 | "dependencies": { 608 | "call-bind-apply-helpers": "^1.0.1", 609 | "es-define-property": "^1.0.1", 610 | "es-errors": "^1.3.0", 611 | "es-object-atoms": "^1.0.0", 612 | "function-bind": "^1.1.2", 613 | "get-proto": "^1.0.0", 614 | "gopd": "^1.2.0", 615 | "has-symbols": "^1.1.0", 616 | "hasown": "^2.0.2", 617 | "math-intrinsics": "^1.1.0" 618 | }, 619 | "engines": { 620 | "node": ">= 0.4" 621 | }, 622 | "funding": { 623 | "url": "https://github.com/sponsors/ljharb" 624 | } 625 | }, 626 | "node_modules/get-proto": { 627 | "version": "1.0.1", 628 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 629 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 630 | "dependencies": { 631 | "dunder-proto": "^1.0.1", 632 | "es-object-atoms": "^1.0.0" 633 | }, 634 | "engines": { 635 | "node": ">= 0.4" 636 | } 637 | }, 638 | "node_modules/github-from-package": { 639 | "version": "0.0.0", 640 | "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 641 | "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" 642 | }, 643 | "node_modules/gopd": { 644 | "version": "1.2.0", 645 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 646 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 647 | "engines": { 648 | "node": ">= 0.4" 649 | }, 650 | "funding": { 651 | "url": "https://github.com/sponsors/ljharb" 652 | } 653 | }, 654 | "node_modules/has-symbols": { 655 | "version": "1.1.0", 656 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 657 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 658 | "engines": { 659 | "node": ">= 0.4" 660 | }, 661 | "funding": { 662 | "url": "https://github.com/sponsors/ljharb" 663 | } 664 | }, 665 | "node_modules/has-tostringtag": { 666 | "version": "1.0.2", 667 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 668 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 669 | "dependencies": { 670 | "has-symbols": "^1.0.3" 671 | }, 672 | "engines": { 673 | "node": ">= 0.4" 674 | }, 675 | "funding": { 676 | "url": "https://github.com/sponsors/ljharb" 677 | } 678 | }, 679 | "node_modules/hasown": { 680 | "version": "2.0.2", 681 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 682 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 683 | "dependencies": { 684 | "function-bind": "^1.1.2" 685 | }, 686 | "engines": { 687 | "node": ">= 0.4" 688 | } 689 | }, 690 | "node_modules/http-errors": { 691 | "version": "2.0.0", 692 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 693 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 694 | "dependencies": { 695 | "depd": "2.0.0", 696 | "inherits": "2.0.4", 697 | "setprototypeof": "1.2.0", 698 | "statuses": "2.0.1", 699 | "toidentifier": "1.0.1" 700 | }, 701 | "engines": { 702 | "node": ">= 0.8" 703 | } 704 | }, 705 | "node_modules/iconv-lite": { 706 | "version": "0.6.3", 707 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 708 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 709 | "dependencies": { 710 | "safer-buffer": ">= 2.1.2 < 3.0.0" 711 | }, 712 | "engines": { 713 | "node": ">=0.10.0" 714 | } 715 | }, 716 | "node_modules/ieee754": { 717 | "version": "1.2.1", 718 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 719 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 720 | "funding": [ 721 | { 722 | "type": "github", 723 | "url": "https://github.com/sponsors/feross" 724 | }, 725 | { 726 | "type": "patreon", 727 | "url": "https://www.patreon.com/feross" 728 | }, 729 | { 730 | "type": "consulting", 731 | "url": "https://feross.org/support" 732 | } 733 | ] 734 | }, 735 | "node_modules/inherits": { 736 | "version": "2.0.4", 737 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 738 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 739 | }, 740 | "node_modules/ini": { 741 | "version": "1.3.8", 742 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 743 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 744 | }, 745 | "node_modules/is-arrayish": { 746 | "version": "0.3.2", 747 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 748 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 749 | }, 750 | "node_modules/make-error": { 751 | "version": "1.3.6", 752 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 753 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" 754 | }, 755 | "node_modules/math-intrinsics": { 756 | "version": "1.1.0", 757 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 758 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 759 | "engines": { 760 | "node": ">= 0.4" 761 | } 762 | }, 763 | "node_modules/mime-db": { 764 | "version": "1.52.0", 765 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 766 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 767 | "engines": { 768 | "node": ">= 0.6" 769 | } 770 | }, 771 | "node_modules/mime-types": { 772 | "version": "2.1.35", 773 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 774 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 775 | "dependencies": { 776 | "mime-db": "1.52.0" 777 | }, 778 | "engines": { 779 | "node": ">= 0.6" 780 | } 781 | }, 782 | "node_modules/mimic-response": { 783 | "version": "3.1.0", 784 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 785 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 786 | "engines": { 787 | "node": ">=10" 788 | }, 789 | "funding": { 790 | "url": "https://github.com/sponsors/sindresorhus" 791 | } 792 | }, 793 | "node_modules/minimist": { 794 | "version": "1.2.8", 795 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 796 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 797 | "funding": { 798 | "url": "https://github.com/sponsors/ljharb" 799 | } 800 | }, 801 | "node_modules/mkdirp-classic": { 802 | "version": "0.5.3", 803 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 804 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 805 | }, 806 | "node_modules/napi-build-utils": { 807 | "version": "2.0.0", 808 | "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", 809 | "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" 810 | }, 811 | "node_modules/node-abi": { 812 | "version": "3.74.0", 813 | "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", 814 | "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", 815 | "dependencies": { 816 | "semver": "^7.3.5" 817 | }, 818 | "engines": { 819 | "node": ">=10" 820 | } 821 | }, 822 | "node_modules/node-addon-api": { 823 | "version": "6.1.0", 824 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", 825 | "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" 826 | }, 827 | "node_modules/once": { 828 | "version": "1.4.0", 829 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 830 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 831 | "dependencies": { 832 | "wrappy": "1" 833 | } 834 | }, 835 | "node_modules/prebuild-install": { 836 | "version": "7.1.3", 837 | "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", 838 | "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", 839 | "dependencies": { 840 | "detect-libc": "^2.0.0", 841 | "expand-template": "^2.0.3", 842 | "github-from-package": "0.0.0", 843 | "minimist": "^1.2.3", 844 | "mkdirp-classic": "^0.5.3", 845 | "napi-build-utils": "^2.0.0", 846 | "node-abi": "^3.3.0", 847 | "pump": "^3.0.0", 848 | "rc": "^1.2.7", 849 | "simple-get": "^4.0.0", 850 | "tar-fs": "^2.0.0", 851 | "tunnel-agent": "^0.6.0" 852 | }, 853 | "bin": { 854 | "prebuild-install": "bin.js" 855 | }, 856 | "engines": { 857 | "node": ">=10" 858 | } 859 | }, 860 | "node_modules/prebuild-install/node_modules/tar-fs": { 861 | "version": "2.1.2", 862 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", 863 | "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", 864 | "dependencies": { 865 | "chownr": "^1.1.1", 866 | "mkdirp-classic": "^0.5.2", 867 | "pump": "^3.0.0", 868 | "tar-stream": "^2.1.4" 869 | } 870 | }, 871 | "node_modules/prebuild-install/node_modules/tar-stream": { 872 | "version": "2.2.0", 873 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 874 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 875 | "dependencies": { 876 | "bl": "^4.0.3", 877 | "end-of-stream": "^1.4.1", 878 | "fs-constants": "^1.0.0", 879 | "inherits": "^2.0.3", 880 | "readable-stream": "^3.1.1" 881 | }, 882 | "engines": { 883 | "node": ">=6" 884 | } 885 | }, 886 | "node_modules/proxy-from-env": { 887 | "version": "1.1.0", 888 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 889 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 890 | }, 891 | "node_modules/pump": { 892 | "version": "3.0.2", 893 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", 894 | "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", 895 | "dependencies": { 896 | "end-of-stream": "^1.1.0", 897 | "once": "^1.3.1" 898 | } 899 | }, 900 | "node_modules/raw-body": { 901 | "version": "3.0.0", 902 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 903 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 904 | "dependencies": { 905 | "bytes": "3.1.2", 906 | "http-errors": "2.0.0", 907 | "iconv-lite": "0.6.3", 908 | "unpipe": "1.0.0" 909 | }, 910 | "engines": { 911 | "node": ">= 0.8" 912 | } 913 | }, 914 | "node_modules/rc": { 915 | "version": "1.2.8", 916 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 917 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 918 | "dependencies": { 919 | "deep-extend": "^0.6.0", 920 | "ini": "~1.3.0", 921 | "minimist": "^1.2.0", 922 | "strip-json-comments": "~2.0.1" 923 | }, 924 | "bin": { 925 | "rc": "cli.js" 926 | } 927 | }, 928 | "node_modules/readable-stream": { 929 | "version": "3.6.2", 930 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 931 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 932 | "dependencies": { 933 | "inherits": "^2.0.3", 934 | "string_decoder": "^1.1.1", 935 | "util-deprecate": "^1.0.1" 936 | }, 937 | "engines": { 938 | "node": ">= 6" 939 | } 940 | }, 941 | "node_modules/safe-buffer": { 942 | "version": "5.2.1", 943 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 944 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 945 | "funding": [ 946 | { 947 | "type": "github", 948 | "url": "https://github.com/sponsors/feross" 949 | }, 950 | { 951 | "type": "patreon", 952 | "url": "https://www.patreon.com/feross" 953 | }, 954 | { 955 | "type": "consulting", 956 | "url": "https://feross.org/support" 957 | } 958 | ] 959 | }, 960 | "node_modules/safer-buffer": { 961 | "version": "2.1.2", 962 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 963 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 964 | }, 965 | "node_modules/semver": { 966 | "version": "7.7.1", 967 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 968 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 969 | "bin": { 970 | "semver": "bin/semver.js" 971 | }, 972 | "engines": { 973 | "node": ">=10" 974 | } 975 | }, 976 | "node_modules/setprototypeof": { 977 | "version": "1.2.0", 978 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 979 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 980 | }, 981 | "node_modules/sharp": { 982 | "version": "0.32.6", 983 | "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", 984 | "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", 985 | "hasInstallScript": true, 986 | "dependencies": { 987 | "color": "^4.2.3", 988 | "detect-libc": "^2.0.2", 989 | "node-addon-api": "^6.1.0", 990 | "prebuild-install": "^7.1.1", 991 | "semver": "^7.5.4", 992 | "simple-get": "^4.0.1", 993 | "tar-fs": "^3.0.4", 994 | "tunnel-agent": "^0.6.0" 995 | }, 996 | "engines": { 997 | "node": ">=14.15.0" 998 | }, 999 | "funding": { 1000 | "url": "https://opencollective.com/libvips" 1001 | } 1002 | }, 1003 | "node_modules/simple-concat": { 1004 | "version": "1.0.1", 1005 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 1006 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 1007 | "funding": [ 1008 | { 1009 | "type": "github", 1010 | "url": "https://github.com/sponsors/feross" 1011 | }, 1012 | { 1013 | "type": "patreon", 1014 | "url": "https://www.patreon.com/feross" 1015 | }, 1016 | { 1017 | "type": "consulting", 1018 | "url": "https://feross.org/support" 1019 | } 1020 | ] 1021 | }, 1022 | "node_modules/simple-get": { 1023 | "version": "4.0.1", 1024 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 1025 | "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 1026 | "funding": [ 1027 | { 1028 | "type": "github", 1029 | "url": "https://github.com/sponsors/feross" 1030 | }, 1031 | { 1032 | "type": "patreon", 1033 | "url": "https://www.patreon.com/feross" 1034 | }, 1035 | { 1036 | "type": "consulting", 1037 | "url": "https://feross.org/support" 1038 | } 1039 | ], 1040 | "dependencies": { 1041 | "decompress-response": "^6.0.0", 1042 | "once": "^1.3.1", 1043 | "simple-concat": "^1.0.0" 1044 | } 1045 | }, 1046 | "node_modules/simple-swizzle": { 1047 | "version": "0.2.2", 1048 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 1049 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 1050 | "dependencies": { 1051 | "is-arrayish": "^0.3.1" 1052 | } 1053 | }, 1054 | "node_modules/statuses": { 1055 | "version": "2.0.1", 1056 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1057 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1058 | "engines": { 1059 | "node": ">= 0.8" 1060 | } 1061 | }, 1062 | "node_modules/streamx": { 1063 | "version": "2.22.0", 1064 | "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", 1065 | "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", 1066 | "dependencies": { 1067 | "fast-fifo": "^1.3.2", 1068 | "text-decoder": "^1.1.0" 1069 | }, 1070 | "optionalDependencies": { 1071 | "bare-events": "^2.2.0" 1072 | } 1073 | }, 1074 | "node_modules/string_decoder": { 1075 | "version": "1.3.0", 1076 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1077 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1078 | "dependencies": { 1079 | "safe-buffer": "~5.2.0" 1080 | } 1081 | }, 1082 | "node_modules/strip-json-comments": { 1083 | "version": "2.0.1", 1084 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1085 | "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", 1086 | "engines": { 1087 | "node": ">=0.10.0" 1088 | } 1089 | }, 1090 | "node_modules/tar-fs": { 1091 | "version": "3.0.8", 1092 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", 1093 | "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", 1094 | "dependencies": { 1095 | "pump": "^3.0.0", 1096 | "tar-stream": "^3.1.5" 1097 | }, 1098 | "optionalDependencies": { 1099 | "bare-fs": "^4.0.1", 1100 | "bare-path": "^3.0.0" 1101 | } 1102 | }, 1103 | "node_modules/tar-stream": { 1104 | "version": "3.1.7", 1105 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", 1106 | "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", 1107 | "dependencies": { 1108 | "b4a": "^1.6.4", 1109 | "fast-fifo": "^1.2.0", 1110 | "streamx": "^2.15.0" 1111 | } 1112 | }, 1113 | "node_modules/text-decoder": { 1114 | "version": "1.2.3", 1115 | "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", 1116 | "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", 1117 | "dependencies": { 1118 | "b4a": "^1.6.4" 1119 | } 1120 | }, 1121 | "node_modules/toidentifier": { 1122 | "version": "1.0.1", 1123 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1124 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1125 | "engines": { 1126 | "node": ">=0.6" 1127 | } 1128 | }, 1129 | "node_modules/ts-node": { 1130 | "version": "10.9.2", 1131 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", 1132 | "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", 1133 | "dependencies": { 1134 | "@cspotcode/source-map-support": "^0.8.0", 1135 | "@tsconfig/node10": "^1.0.7", 1136 | "@tsconfig/node12": "^1.0.7", 1137 | "@tsconfig/node14": "^1.0.0", 1138 | "@tsconfig/node16": "^1.0.2", 1139 | "acorn": "^8.4.1", 1140 | "acorn-walk": "^8.1.1", 1141 | "arg": "^4.1.0", 1142 | "create-require": "^1.1.0", 1143 | "diff": "^4.0.1", 1144 | "make-error": "^1.1.1", 1145 | "v8-compile-cache-lib": "^3.0.1", 1146 | "yn": "3.1.1" 1147 | }, 1148 | "bin": { 1149 | "ts-node": "dist/bin.js", 1150 | "ts-node-cwd": "dist/bin-cwd.js", 1151 | "ts-node-esm": "dist/bin-esm.js", 1152 | "ts-node-script": "dist/bin-script.js", 1153 | "ts-node-transpile-only": "dist/bin-transpile.js", 1154 | "ts-script": "dist/bin-script-deprecated.js" 1155 | }, 1156 | "peerDependencies": { 1157 | "@swc/core": ">=1.2.50", 1158 | "@swc/wasm": ">=1.2.50", 1159 | "@types/node": "*", 1160 | "typescript": ">=2.7" 1161 | }, 1162 | "peerDependenciesMeta": { 1163 | "@swc/core": { 1164 | "optional": true 1165 | }, 1166 | "@swc/wasm": { 1167 | "optional": true 1168 | } 1169 | } 1170 | }, 1171 | "node_modules/tunnel-agent": { 1172 | "version": "0.6.0", 1173 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1174 | "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 1175 | "dependencies": { 1176 | "safe-buffer": "^5.0.1" 1177 | }, 1178 | "engines": { 1179 | "node": "*" 1180 | } 1181 | }, 1182 | "node_modules/typescript": { 1183 | "version": "5.7.3", 1184 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", 1185 | "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", 1186 | "bin": { 1187 | "tsc": "bin/tsc", 1188 | "tsserver": "bin/tsserver" 1189 | }, 1190 | "engines": { 1191 | "node": ">=14.17" 1192 | } 1193 | }, 1194 | "node_modules/undici-types": { 1195 | "version": "6.20.0", 1196 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", 1197 | "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" 1198 | }, 1199 | "node_modules/unpipe": { 1200 | "version": "1.0.0", 1201 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1202 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1203 | "engines": { 1204 | "node": ">= 0.8" 1205 | } 1206 | }, 1207 | "node_modules/util-deprecate": { 1208 | "version": "1.0.2", 1209 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1210 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1211 | }, 1212 | "node_modules/v8-compile-cache-lib": { 1213 | "version": "3.0.1", 1214 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 1215 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" 1216 | }, 1217 | "node_modules/wrappy": { 1218 | "version": "1.0.2", 1219 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1220 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 1221 | }, 1222 | "node_modules/ws": { 1223 | "version": "8.18.0", 1224 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 1225 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 1226 | "engines": { 1227 | "node": ">=10.0.0" 1228 | }, 1229 | "peerDependencies": { 1230 | "bufferutil": "^4.0.1", 1231 | "utf-8-validate": ">=5.0.2" 1232 | }, 1233 | "peerDependenciesMeta": { 1234 | "bufferutil": { 1235 | "optional": true 1236 | }, 1237 | "utf-8-validate": { 1238 | "optional": true 1239 | } 1240 | } 1241 | }, 1242 | "node_modules/yn": { 1243 | "version": "3.1.1", 1244 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 1245 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 1246 | "engines": { 1247 | "node": ">=6" 1248 | } 1249 | }, 1250 | "node_modules/zod": { 1251 | "version": "3.24.2", 1252 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", 1253 | "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", 1254 | "funding": { 1255 | "url": "https://github.com/sponsors/colinhacks" 1256 | } 1257 | }, 1258 | "node_modules/zod-to-json-schema": { 1259 | "version": "3.24.1", 1260 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", 1261 | "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", 1262 | "peerDependencies": { 1263 | "zod": "^3.24.1" 1264 | } 1265 | } 1266 | } 1267 | } 1268 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nicholmikey/chrome-tools", 3 | "version": "1.3.0", 4 | "description": "MCP server for Chrome DevTools Protocol integration - control Chrome tabs, execute JavaScript, capture screenshots, and monitor network traffic", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/index.js", 10 | "dev": "ts-node --esm src/index.ts", 11 | "watch": "tsc --watch", 12 | "prepare": "npm run build", 13 | "test": "echo \"No tests specified\" && exit 0", 14 | "lint": "eslint src --ext .ts", 15 | "format": "prettier --write \"src/**/*.ts\"" 16 | }, 17 | "bin": { 18 | "mcp-chrome-tools": "./dist/index.js" 19 | }, 20 | "files": [ 21 | "dist", 22 | "README.md", 23 | "LICENSE" 24 | ], 25 | "keywords": [ 26 | "mcp", 27 | "chrome", 28 | "devtools", 29 | "debugging", 30 | "automation", 31 | "testing", 32 | "screenshots", 33 | "network-monitoring", 34 | "browser-automation", 35 | "chrome-devtools-protocol" 36 | ], 37 | "author": { 38 | "name": "nicholmikey", 39 | "url": "https://github.com/nicholmikey" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/nicholmikey/chrome-tools-MCP.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/nicholmikey/chrome-tools-MCP/issues" 47 | }, 48 | "homepage": "https://github.com/nicholmikey/chrome-tools-MCP#readme", 49 | "license": "MIT", 50 | "dependencies": { 51 | "@modelcontextprotocol/sdk": "^1.5.0", 52 | "@types/ws": "^8.5.14", 53 | "axios": "^1.7.9", 54 | "chrome-remote-interface": "^0.33.2", 55 | "sharp": "^0.32.6", 56 | "ts-node": "^10.9.2", 57 | "typescript": "^5.7.3", 58 | "ws": "^8.18.0", 59 | "zod": "^3.24.2" 60 | }, 61 | "devDependencies": { 62 | "@types/chrome-remote-interface": "^0.31.14" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/chrome-api.ts: -------------------------------------------------------------------------------- 1 | import CDP from 'chrome-remote-interface'; 2 | import type { Client } from 'chrome-remote-interface'; 3 | import { ChromeTab, DOMElement } from './types.js'; 4 | 5 | type MouseButton = 'none' | 'left' | 'middle' | 'right' | 'back' | 'forward'; 6 | type MouseEventType = 'mousePressed' | 'mouseReleased'; 7 | 8 | export class ChromeAPI { 9 | private baseUrl: string; 10 | 11 | constructor(options: { port?: number; baseUrl?: string } = {}) { 12 | const { port = 9222, baseUrl } = options; 13 | this.baseUrl = baseUrl || `http://localhost:${port}`; 14 | const connectionType = process.env.CHROME_CONNECTION_TYPE || 'direct'; 15 | console.error(`ChromeAPI: Connecting to ${this.baseUrl} (${connectionType} connection)`); 16 | } 17 | 18 | /** 19 | * List all available Chrome tabs 20 | * @returns Promise 21 | * @throws Error if Chrome is not accessible or returns an error 22 | */ 23 | async listTabs(): Promise { 24 | try { 25 | console.error(`ChromeAPI: Attempting to list tabs on port ${this.port}`); 26 | const targets = await CDP.List({ port: this.port }); 27 | console.error(`ChromeAPI: Successfully found ${targets.length} tabs`); 28 | return targets; 29 | } catch (error) { 30 | console.error(`ChromeAPI: Failed to list tabs:`, error instanceof Error ? error.message : error); 31 | const errorHelp = process.env.CHROME_ERROR_HELP || 'Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222)'; 32 | throw new Error(`Failed to connect to Chrome DevTools. ${errorHelp}`); 33 | } 34 | } 35 | 36 | /** 37 | * Execute JavaScript in a specific Chrome tab 38 | * @param tabId The ID of the tab to execute the script in 39 | * @param script The JavaScript code to execute 40 | * @returns Promise with the result of the script execution 41 | * @throws Error if the tab is not found or script execution fails 42 | */ 43 | async executeScript(tabId: string, script: string): Promise { 44 | console.error(`ChromeAPI: Attempting to execute script in tab ${tabId}`); 45 | let client: Client | undefined; 46 | try { 47 | // Connect to the specific tab 48 | client = await CDP({ target: tabId, port: this.port }); 49 | 50 | if (!client) { 51 | throw new Error('Failed to connect to Chrome DevTools'); 52 | } 53 | 54 | // Enable Runtime and set up console listener 55 | await client.Runtime.enable(); 56 | 57 | let consoleMessages: string[] = []; 58 | client.Runtime.consoleAPICalled(({ type, args }) => { 59 | const message = args.map(arg => arg.value || arg.description).join(' '); 60 | consoleMessages.push(`[${type}] ${message}`); 61 | console.error(`Chrome Console: ${type}:`, message); 62 | }); 63 | 64 | // Execute the script using Runtime.evaluate 65 | const result = await client.Runtime.evaluate({ 66 | expression: script, 67 | returnByValue: true, 68 | includeCommandLineAPI: true 69 | }); 70 | 71 | console.error('ChromeAPI: Script execution successful'); 72 | return JSON.stringify({ 73 | result: result.result, 74 | consoleOutput: consoleMessages 75 | }, null, 2); 76 | } catch (error) { 77 | console.error('ChromeAPI: Script execution failed:', error instanceof Error ? error.message : error); 78 | throw error; 79 | } finally { 80 | if (client) { 81 | await client.close(); 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * Check if Chrome debugging port is accessible 88 | * @returns Promise 89 | */ 90 | async isAvailable(): Promise { 91 | try { 92 | await this.listTabs(); 93 | return true; 94 | } catch { 95 | return false; 96 | } 97 | } 98 | 99 | /** 100 | * Capture a screenshot of a specific Chrome tab 101 | * @param tabId The ID of the tab to capture 102 | * @param options Screenshot options (format, quality, fullPage) 103 | * @returns Promise with the base64-encoded screenshot data 104 | * @throws Error if the tab is not found or screenshot capture fails 105 | */ 106 | async captureScreenshot( 107 | tabId: string, 108 | options: { 109 | format?: 'jpeg' | 'png'; 110 | quality?: number; 111 | fullPage?: boolean; 112 | } = {} 113 | ): Promise { 114 | console.error(`ChromeAPI: Attempting to capture screenshot of tab ${tabId}`); 115 | let client: Client | undefined; 116 | try { 117 | // Connect to the specific tab 118 | client = await CDP({ target: tabId, port: this.port }); 119 | 120 | if (!client) { 121 | throw new Error('Failed to connect to Chrome DevTools'); 122 | } 123 | 124 | // Enable Page domain for screenshot capabilities 125 | await client.Page.enable(); 126 | 127 | // If fullPage is requested, we need to get the full page dimensions 128 | if (options.fullPage) { 129 | // Get the full page dimensions 130 | const { root } = await client.DOM.getDocument(); 131 | const { model } = await client.DOM.getBoxModel({ nodeId: root.nodeId }); 132 | const height = model.height; 133 | 134 | // Set viewport to full page height 135 | await client.Emulation.setDeviceMetricsOverride({ 136 | width: 1920, // Standard width 137 | height: Math.ceil(height), 138 | deviceScaleFactor: 1, 139 | mobile: false 140 | }); 141 | } 142 | 143 | // Capture the screenshot 144 | const result = await client.Page.captureScreenshot({ 145 | format: options.format || 'png', 146 | quality: options.format === 'jpeg' ? options.quality || 80 : undefined, 147 | fromSurface: true, 148 | captureBeyondViewport: options.fullPage || false 149 | }); 150 | 151 | console.error('ChromeAPI: Screenshot capture successful'); 152 | return result.data; 153 | } catch (error) { 154 | console.error('ChromeAPI: Screenshot capture failed:', error instanceof Error ? error.message : error); 155 | throw error; 156 | } finally { 157 | if (client) { 158 | // Reset device metrics if we modified them 159 | if (options.fullPage) { 160 | await client.Emulation.clearDeviceMetricsOverride(); 161 | } 162 | await client.close(); 163 | } 164 | } 165 | } 166 | 167 | /** 168 | * Capture network events (XHR/Fetch) from a specific Chrome tab 169 | * @param tabId The ID of the tab to capture events from 170 | * @param options Capture options (duration, filters) 171 | * @returns Promise with the captured network events 172 | * @throws Error if the tab is not found or capture fails 173 | */ 174 | async captureNetworkEvents( 175 | tabId: string, 176 | options: { 177 | duration?: number; 178 | filters?: { 179 | types?: Array<'fetch' | 'xhr'>; 180 | urlPattern?: string; 181 | }; 182 | } = {} 183 | ): Promise; 190 | responseHeaders: Record; 191 | timing: { 192 | requestTime: number; 193 | responseTime: number; 194 | }; 195 | }>> { 196 | console.error(`ChromeAPI: Attempting to capture network events from tab ${tabId}`); 197 | let client: Client | undefined; 198 | try { 199 | // Connect to the specific tab 200 | client = await CDP({ target: tabId, port: this.port }); 201 | 202 | if (!client) { 203 | throw new Error('Failed to connect to Chrome DevTools'); 204 | } 205 | 206 | // Enable Network domain 207 | await client.Network.enable(); 208 | 209 | const events: Array = []; 210 | const requests = new Map(); 211 | 212 | // Set up event handlers 213 | const requestHandler = (params: any) => { 214 | const request = { 215 | type: (params.type?.toLowerCase() === 'xhr' ? 'xhr' : 'fetch') as 'xhr' | 'fetch', 216 | method: params.request.method, 217 | url: params.request.url, 218 | requestHeaders: params.request.headers, 219 | timing: { 220 | requestTime: params.timestamp 221 | } 222 | }; 223 | 224 | // Apply filters if specified 225 | if (options.filters) { 226 | if (options.filters.types && !options.filters.types.includes(request.type)) { 227 | return; 228 | } 229 | if (options.filters.urlPattern && !request.url.match(options.filters.urlPattern)) { 230 | return; 231 | } 232 | } 233 | 234 | requests.set(params.requestId, request); 235 | }; 236 | 237 | const responseHandler = (params: any) => { 238 | const request = requests.get(params.requestId); 239 | if (request) { 240 | request.status = params.response.status; 241 | request.statusText = params.response.statusText; 242 | request.responseHeaders = params.response.headers; 243 | request.timing.responseTime = params.timestamp; 244 | events.push(request); 245 | } 246 | }; 247 | 248 | // Register event handlers 249 | client.Network.requestWillBeSent(requestHandler); 250 | client.Network.responseReceived(responseHandler); 251 | 252 | // Wait for specified duration 253 | const duration = options.duration || 10; 254 | await new Promise(resolve => setTimeout(resolve, duration * 1000)); 255 | 256 | console.error('ChromeAPI: Network event capture successful'); 257 | return events; 258 | } catch (error) { 259 | console.error('ChromeAPI: Network event capture failed:', error instanceof Error ? error.message : error); 260 | throw error; 261 | } finally { 262 | if (client) { 263 | await client.close(); 264 | } 265 | } 266 | } 267 | 268 | /** 269 | * Navigate a Chrome tab to a specific URL 270 | * @param tabId The ID of the tab to load the URL in 271 | * @param url The URL to load 272 | * @returns Promise 273 | * @throws Error if the tab is not found or navigation fails 274 | */ 275 | async loadUrl(tabId: string, url: string): Promise { 276 | console.error(`ChromeAPI: Attempting to load URL ${url} in tab ${tabId}`); 277 | let client: Client | undefined; 278 | try { 279 | // Connect to the specific tab 280 | client = await CDP({ target: tabId, port: this.port }); 281 | 282 | if (!client) { 283 | throw new Error('Failed to connect to Chrome DevTools'); 284 | } 285 | 286 | // Enable Page domain for navigation 287 | await client.Page.enable(); 288 | 289 | // Navigate to the URL and wait for load 290 | await client.Page.navigate({ url }); 291 | await client.Page.loadEventFired(); 292 | 293 | console.error('ChromeAPI: URL loading successful'); 294 | } catch (error) { 295 | console.error('ChromeAPI: URL loading failed:', error instanceof Error ? error.message : error); 296 | throw error; 297 | } finally { 298 | if (client) { 299 | await client.close(); 300 | } 301 | } 302 | } 303 | 304 | /** 305 | * Query DOM elements using a CSS selector 306 | * @param tabId The ID of the tab to query 307 | * @param selector CSS selector to find elements 308 | * @returns Promise Array of matching DOM elements with their properties 309 | * @throws Error if the tab is not found or query fails 310 | */ 311 | async queryDOMElements(tabId: string, selector: string): Promise { 312 | console.error(`ChromeAPI: Attempting to query DOM elements in tab ${tabId} with selector "${selector}"`); 313 | let client: Client | undefined; 314 | try { 315 | // Connect to the specific tab 316 | client = await CDP({ target: tabId, port: this.port }); 317 | 318 | if (!client) { 319 | throw new Error('Failed to connect to Chrome DevTools'); 320 | } 321 | 322 | // Enable necessary domains 323 | await client.DOM.enable(); 324 | await client.Runtime.enable(); 325 | 326 | // Get the document root 327 | const { root } = await client.DOM.getDocument(); 328 | 329 | // Find elements matching the selector 330 | const { nodeIds } = await client.DOM.querySelectorAll({ 331 | nodeId: root.nodeId, 332 | selector: selector 333 | }); 334 | 335 | // Get detailed information for each element 336 | const elements: DOMElement[] = await Promise.all( 337 | nodeIds.map(async (nodeId) => { 338 | if (!client) { 339 | throw new Error('Client disconnected'); 340 | } 341 | 342 | // Get node details 343 | const { node } = await client.DOM.describeNode({ nodeId }); 344 | 345 | // Get node box model for position and dimensions 346 | const boxModel = await client.DOM.getBoxModel({ nodeId }) 347 | .catch(() => null); // Some elements might not have a box model 348 | 349 | // Check visibility using Runtime.evaluate 350 | const result = await client.Runtime.evaluate({ 351 | expression: ` 352 | (function(selector) { 353 | const element = document.querySelector(selector); 354 | if (!element) return false; 355 | const style = window.getComputedStyle(element); 356 | return style.display !== 'none' && 357 | style.visibility !== 'hidden' && 358 | style.opacity !== '0'; 359 | })('${selector}') 360 | `, 361 | returnByValue: true 362 | }); 363 | 364 | // Extract ARIA attributes 365 | const ariaAttributes: Record = {}; 366 | if (node.attributes) { 367 | for (let i = 0; i < node.attributes.length; i += 2) { 368 | const name = node.attributes[i]; 369 | if (name.startsWith('aria-')) { 370 | ariaAttributes[name] = node.attributes[i + 1]; 371 | } 372 | } 373 | } 374 | 375 | // Convert attributes array to object 376 | const attributes: Record = {}; 377 | if (node.attributes) { 378 | for (let i = 0; i < node.attributes.length; i += 2) { 379 | attributes[node.attributes[i]] = node.attributes[i + 1]; 380 | } 381 | } 382 | 383 | return { 384 | nodeId, 385 | tagName: node.nodeName.toLowerCase(), 386 | textContent: node.nodeValue || null, 387 | attributes, 388 | boundingBox: boxModel ? { 389 | x: boxModel.model.content[0], 390 | y: boxModel.model.content[1], 391 | width: boxModel.model.width, 392 | height: boxModel.model.height 393 | } : null, 394 | isVisible: result.result.value as boolean, 395 | ariaAttributes 396 | }; 397 | }) 398 | ); 399 | 400 | console.error(`ChromeAPI: Successfully found ${elements.length} elements matching selector`); 401 | return elements; 402 | } catch (error) { 403 | console.error('ChromeAPI: DOM query failed:', error instanceof Error ? error.message : error); 404 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 405 | 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.`); 406 | } finally { 407 | if (client) { 408 | await client.close(); 409 | } 410 | } 411 | } 412 | 413 | /** 414 | * Click on a DOM element matching a CSS selector 415 | * @param tabId The ID of the tab containing the element 416 | * @param selector CSS selector to find the element to click 417 | * @returns Promise 418 | * @throws Error if the tab is not found, element is not found, or click fails 419 | */ 420 | async clickElement(tabId: string, selector: string): Promise<{consoleOutput: string[]}> { 421 | console.error(`ChromeAPI: Attempting to click element in tab ${tabId} with selector "${selector}"`); 422 | let client: Client | undefined; 423 | try { 424 | // Connect to the specific tab 425 | client = await CDP({ target: tabId, port: this.port }); 426 | 427 | if (!client) { 428 | throw new Error('Failed to connect to Chrome DevTools'); 429 | } 430 | 431 | // Enable necessary domains 432 | await client.DOM.enable(); 433 | await client.Runtime.enable(); 434 | 435 | // Get the document root 436 | const { root } = await client.DOM.getDocument(); 437 | 438 | // Find the element 439 | const { nodeIds } = await client.DOM.querySelectorAll({ 440 | nodeId: root.nodeId, 441 | selector: selector 442 | }); 443 | 444 | if (nodeIds.length === 0) { 445 | throw new Error(`No element found matching selector: ${selector}`); 446 | } 447 | 448 | // Get element's box model for coordinates 449 | const { model } = await client.DOM.getBoxModel({ nodeId: nodeIds[0] }); 450 | 451 | // Calculate center point 452 | const centerX = model.content[0] + (model.width / 2); 453 | const centerY = model.content[1] + (model.height / 2); 454 | 455 | // Dispatch click event using Runtime.evaluate 456 | await client.Runtime.evaluate({ 457 | expression: ` 458 | (() => { 459 | const element = document.querySelector('${selector}'); 460 | if (!element) throw new Error('Element not found'); 461 | 462 | const clickEvent = new MouseEvent('click', { 463 | bubbles: true, 464 | cancelable: true, 465 | view: window, 466 | clientX: ${Math.round(centerX)}, 467 | clientY: ${Math.round(centerY)} 468 | }); 469 | 470 | element.dispatchEvent(clickEvent); 471 | })() 472 | `, 473 | awaitPromise: true 474 | }); 475 | 476 | // Set up console listener before the click 477 | let consoleMessages: string[] = []; 478 | const consolePromise = new Promise((resolve) => { 479 | if (!client) return; 480 | client.Runtime.consoleAPICalled(({ type, args }) => { 481 | const message = args.map(arg => arg.value || arg.description).join(' '); 482 | consoleMessages.push(`[${type}] ${message}`); 483 | console.error(`Chrome Console: ${type}:`, message); 484 | resolve(); // Resolve when we get a console message 485 | }); 486 | }); 487 | 488 | // Set up a timeout promise 489 | const timeoutPromise = new Promise((resolve) => { 490 | setTimeout(resolve, 1000); 491 | }); 492 | 493 | // Click the element 494 | await client.Runtime.evaluate({ 495 | expression: ` 496 | (() => { 497 | const element = document.querySelector('${selector}'); 498 | if (!element) throw new Error('Element not found'); 499 | 500 | const clickEvent = new MouseEvent('click', { 501 | bubbles: true, 502 | cancelable: true, 503 | view: window, 504 | clientX: ${Math.round(centerX)}, 505 | clientY: ${Math.round(centerY)} 506 | }); 507 | 508 | element.dispatchEvent(clickEvent); 509 | })() 510 | `, 511 | awaitPromise: true 512 | }); 513 | 514 | // Wait for either a console message or timeout 515 | await Promise.race([consolePromise, timeoutPromise]); 516 | 517 | console.error('ChromeAPI: Successfully clicked element'); 518 | return { consoleOutput: consoleMessages }; 519 | } catch (error) { 520 | console.error('ChromeAPI: Element click failed:', error instanceof Error ? error.message : error); 521 | throw error; 522 | } finally { 523 | if (client) { 524 | await client.close(); 525 | } 526 | } 527 | } 528 | 529 | private get port(): number { 530 | const portMatch = this.baseUrl.match(/:(\d+)$/); 531 | return portMatch ? parseInt(portMatch[1]) : 9222; 532 | } 533 | } 534 | -------------------------------------------------------------------------------- /src/image-utils.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'module'; 2 | import path from 'path'; 3 | import fs from 'fs/promises'; 4 | const require = createRequire(import.meta.url); 5 | const sharp = require('sharp'); 6 | 7 | export const SCREENSHOT_DIR = path.join('/tmp', 'chrome-tools-screenshots'); 8 | 9 | export interface ProcessedImage { 10 | data: string; 11 | format: 'png'; 12 | size: number; 13 | } 14 | 15 | export async function saveImage(processedImage: ProcessedImage): Promise { 16 | // Ensure screenshots directory exists 17 | await fs.mkdir(SCREENSHOT_DIR, { recursive: true }); 18 | 19 | const filename = `screenshot_${Date.now()}.webp`; 20 | const filepath = path.join(SCREENSHOT_DIR, filename); 21 | 22 | // Extract the base64 data after the "data:image/webp;base64," prefix 23 | const base64Data = processedImage.data.split(',')[1]; 24 | const imageBuffer = Buffer.from(base64Data, 'base64'); 25 | 26 | await fs.writeFile(filepath, imageBuffer); 27 | return filepath; 28 | } 29 | 30 | export async function processImage(base64Data: string): Promise { 31 | try { 32 | // Convert base64 to buffer 33 | const buffer = Buffer.from(base64Data, 'base64'); 34 | 35 | // Create Sharp instance and resize maintaining aspect ratio 36 | const image = sharp(buffer).resize(900, 600, { 37 | fit: 'inside', 38 | withoutEnlargement: true 39 | }); 40 | 41 | // Try WebP first with good quality 42 | try { 43 | const webpBuffer = await image 44 | .webp({ 45 | quality: 80, 46 | effort: 6, // Higher compression effort 47 | lossless: false 48 | }) 49 | .toBuffer(); 50 | 51 | if (webpBuffer.length <= 1024 * 1024) { 52 | return { 53 | data: `data:image/webp;base64,${webpBuffer.toString('base64')}`, 54 | format: 'png', // Keep format as 'png' in interface for backward compatibility 55 | size: webpBuffer.length 56 | }; 57 | } 58 | 59 | // If still too large, try WebP with more aggressive compression 60 | const compressedWebpBuffer = await image 61 | .webp({ 62 | quality: 60, 63 | effort: 6, 64 | lossless: false, 65 | nearLossless: true 66 | }) 67 | .toBuffer(); 68 | 69 | if (compressedWebpBuffer.length <= 1024 * 1024) { 70 | return { 71 | data: `data:image/webp;base64,${compressedWebpBuffer.toString('base64')}`, 72 | format: 'png', // Keep format as 'png' in interface for backward compatibility 73 | size: compressedWebpBuffer.length 74 | }; 75 | } 76 | } catch (webpError) { 77 | console.error('WebP processing failed, falling back to PNG:', webpError); 78 | } 79 | 80 | // Fallback to PNG with compression if WebP fails or is too large 81 | const pngBuffer = await image 82 | .png({ 83 | compressionLevel: 9, 84 | palette: true 85 | }) 86 | .toBuffer(); 87 | 88 | if (pngBuffer.length > 1024 * 1024) { 89 | // If still too large, reduce dimensions further 90 | const scaleFactor = Math.sqrt(1024 * 1024 / pngBuffer.length); 91 | const resizedImage = sharp(buffer).resize( 92 | Math.floor(900 * scaleFactor), 93 | Math.floor(600 * scaleFactor), 94 | { 95 | fit: 'inside', 96 | withoutEnlargement: true 97 | } 98 | ); 99 | 100 | const compressedPngBuffer = await resizedImage 101 | .png({ 102 | compressionLevel: 9, 103 | palette: true, 104 | colors: 128 // Reduce color palette for smaller size 105 | }) 106 | .toBuffer(); 107 | 108 | if (compressedPngBuffer.length > 1024 * 1024) { 109 | throw new Error('Image is too large even after compression'); 110 | } 111 | 112 | return { 113 | data: `data:image/png;base64,${compressedPngBuffer.toString('base64')}`, 114 | format: 'png', 115 | size: compressedPngBuffer.length 116 | }; 117 | } 118 | 119 | return { 120 | data: `data:image/png;base64,${pngBuffer.toString('base64')}`, 121 | format: 'png', 122 | size: pngBuffer.length 123 | }; 124 | } catch (error) { 125 | throw new Error(`Failed to process image: ${error instanceof Error ? error.message : 'Unknown error'}`); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { ChromeAPI } from './chrome-api.js'; 5 | import { processImage, saveImage } from './image-utils.js'; 6 | import { z } from 'zod'; 7 | 8 | // Get Chrome debug URL from environment variable or use default 9 | const chromeDebugUrl = process.env.CHROME_DEBUG_URL || 'http://localhost:9222'; 10 | console.error(`Using Chrome debug URL: ${chromeDebugUrl}`); 11 | 12 | const chromeApi = new ChromeAPI({ baseUrl: chromeDebugUrl }); 13 | 14 | // Create the MCP server 15 | const server = new McpServer({ 16 | name: 'chrome-tools', 17 | version: '1.3.0' 18 | }); 19 | 20 | // Add the list_tabs tool 21 | server.tool( 22 | 'list_tabs', 23 | {}, // No input parameters needed 24 | async () => { 25 | try { 26 | console.error('Attempting to list Chrome tabs...'); 27 | const tabs = await chromeApi.listTabs(); 28 | console.error(`Successfully found ${tabs.length} tabs`); 29 | return { 30 | content: [{ 31 | type: 'text', 32 | text: JSON.stringify(tabs, null, 2) 33 | }] 34 | }; 35 | } catch (error) { 36 | console.error('Error in list_tabs tool:', error); 37 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 38 | return { 39 | content: [{ 40 | type: 'text', 41 | text: `Error: ${errorMessage}` 42 | }], 43 | isError: true 44 | }; 45 | } 46 | } 47 | ); 48 | 49 | // Add the capture_screenshot tool 50 | server.tool( 51 | 'capture_screenshot', 52 | { 53 | tabId: z.string().describe('ID of the Chrome tab to capture. Only send this unless you are having issues with the result.'), 54 | format: z.enum(['jpeg', 'png']).optional() 55 | .describe('Initial capture format (jpeg/png). Note: Final output will be WebP with PNG fallback'), 56 | quality: z.number().min(1).max(100).optional() 57 | .describe('Initial capture quality (1-100). Note: Final output uses WebP quality settings'), 58 | fullPage: z.boolean().optional() 59 | .describe('Capture full scrollable page') 60 | }, 61 | async (params) => { 62 | try { 63 | console.error(`Attempting to capture screenshot of tab ${params.tabId}...`); 64 | const rawBase64Data = await chromeApi.captureScreenshot(params.tabId, { 65 | format: params.format, 66 | quality: params.quality, 67 | fullPage: params.fullPage 68 | }); 69 | console.error('Screenshot captured, optimizing with WebP...'); 70 | 71 | try { 72 | // Process image with the following strategy: 73 | // 1. Try WebP with quality 80 (best balance of quality/size) 74 | // 2. If >1MB, try WebP with quality 60 and near-lossless 75 | // 3. If WebP fails, fall back to PNG with maximum compression 76 | const processedImage = await processImage(rawBase64Data); 77 | console.error(`Image optimized successfully (${processedImage.data.startsWith('data:image/webp') ? 'WebP' : 'PNG'}, ${Math.round(processedImage.size / 1024)}KB)`); 78 | 79 | // Save the image and get the filepath 80 | const filepath = await saveImage(processedImage); 81 | console.error(`Screenshot saved to: ${filepath}`); 82 | 83 | return { 84 | content: [{ 85 | type: 'text', 86 | text: JSON.stringify({ 87 | status: 'Screenshot successful.', 88 | path: filepath 89 | }) 90 | }] 91 | }; 92 | } catch (error) { 93 | console.error('Image processing failed:', error); 94 | return { 95 | content: [{ 96 | type: 'text', 97 | text: `Error processing screenshot: ${error instanceof Error ? error.message : 'Unknown error'}` 98 | }], 99 | isError: true 100 | }; 101 | } 102 | } catch (error) { 103 | console.error('Error in capture_screenshot tool:', error); 104 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 105 | return { 106 | content: [{ 107 | type: 'text', 108 | text: `Error: ${errorMessage}` 109 | }], 110 | isError: true 111 | }; 112 | } 113 | } 114 | ); 115 | 116 | // Add the execute_script tool 117 | server.tool( 118 | 'execute_script', 119 | { 120 | tabId: z.string().describe('ID of the Chrome tab to execute the script in'), 121 | script: z.string().describe('JavaScript code to execute in the tab') 122 | }, 123 | async (params) => { 124 | try { 125 | console.error(`Attempting to execute script in tab ${params.tabId}...`); 126 | const result = await chromeApi.executeScript(params.tabId, params.script); 127 | console.error('Script execution successful'); 128 | return { 129 | content: [{ 130 | type: 'text', 131 | text: result || 'undefined' 132 | }] 133 | }; 134 | } catch (error) { 135 | console.error('Error in execute_script tool:', error); 136 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 137 | return { 138 | content: [{ 139 | type: 'text', 140 | text: `Error: ${errorMessage}` 141 | }], 142 | isError: true 143 | }; 144 | } 145 | } 146 | ); 147 | 148 | // Log when server starts 149 | console.error('Chrome Tools MCP Server starting...'); 150 | 151 | // Start the server 152 | const transport = new StdioServerTransport(); 153 | server.connect(transport).catch(console.error); 154 | 155 | // Add the load_url tool 156 | server.tool( 157 | 'load_url', 158 | { 159 | tabId: z.string().describe('ID of the Chrome tab to load the URL in'), 160 | url: z.string().url().describe('URL to load in the tab') 161 | }, 162 | async (params) => { 163 | try { 164 | console.error(`Attempting to load URL ${params.url} in tab ${params.tabId}...`); 165 | await chromeApi.loadUrl(params.tabId, params.url); 166 | console.error('URL loading successful'); 167 | return { 168 | content: [{ 169 | type: 'text', 170 | text: `Successfully loaded ${params.url} in tab ${params.tabId}` 171 | }] 172 | }; 173 | } catch (error) { 174 | console.error('Error in load_url tool:', error); 175 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 176 | return { 177 | content: [{ 178 | type: 'text', 179 | text: `Error: ${errorMessage}` 180 | }], 181 | isError: true 182 | }; 183 | } 184 | } 185 | ); 186 | 187 | // Add the capture_network_events tool 188 | server.tool( 189 | 'capture_network_events', 190 | { 191 | tabId: z.string().describe('ID of the Chrome tab to monitor'), 192 | duration: z.number().min(1).max(60).optional() 193 | .describe('Duration in seconds to capture events (default: 10)'), 194 | filters: z.object({ 195 | types: z.array(z.enum(['fetch', 'xhr'])).optional() 196 | .describe('Types of requests to capture'), 197 | urlPattern: z.string().optional() 198 | .describe('Only capture URLs matching this pattern') 199 | }).optional() 200 | }, 201 | async (params) => { 202 | try { 203 | console.error(`Attempting to capture network events from tab ${params.tabId}...`); 204 | const events = await chromeApi.captureNetworkEvents(params.tabId, { 205 | duration: params.duration, 206 | filters: params.filters 207 | }); 208 | console.error(`Network event capture successful, captured ${events.length} events`); 209 | return { 210 | content: [{ 211 | type: 'text', 212 | text: JSON.stringify(events, null, 2) 213 | }] 214 | }; 215 | } catch (error) { 216 | console.error('Error in capture_network_events tool:', error); 217 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 218 | return { 219 | content: [{ 220 | type: 'text', 221 | text: `Error: ${errorMessage}` 222 | }], 223 | isError: true 224 | }; 225 | } 226 | } 227 | ); 228 | 229 | // Add the query_dom_elements tool 230 | server.tool( 231 | 'query_dom_elements', 232 | { 233 | tabId: z.string().describe('ID of the Chrome tab to query'), 234 | selector: z.string().describe('CSS selector to find elements') 235 | }, 236 | async (params) => { 237 | try { 238 | console.error(`Attempting to query DOM elements in tab ${params.tabId}...`); 239 | const elements = await chromeApi.queryDOMElements(params.tabId, params.selector); 240 | console.error(`Successfully found ${elements.length} elements matching selector`); 241 | return { 242 | content: [{ 243 | type: 'text', 244 | text: JSON.stringify(elements, null, 2) 245 | }] 246 | }; 247 | } catch (error) { 248 | console.error('Error in query_dom_elements tool:', error); 249 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 250 | return { 251 | content: [{ 252 | type: 'text', 253 | text: `Error: ${errorMessage}` 254 | }], 255 | isError: true 256 | }; 257 | } 258 | } 259 | ); 260 | 261 | // Add the click_element tool 262 | server.tool( 263 | 'click_element', 264 | { 265 | tabId: z.string().describe('ID of the Chrome tab containing the element'), 266 | selector: z.string().describe('CSS selector to find the element to click') 267 | }, 268 | async (params) => { 269 | try { 270 | console.error(`Attempting to click element in tab ${params.tabId}...`); 271 | const result = await chromeApi.clickElement(params.tabId, params.selector); 272 | console.error('Successfully clicked element'); 273 | return { 274 | content: [{ 275 | type: 'text', 276 | text: JSON.stringify({ 277 | message: 'Successfully clicked element', 278 | consoleOutput: result.consoleOutput 279 | }, null, 2) 280 | }] 281 | }; 282 | } catch (error) { 283 | console.error('Error in click_element tool:', error); 284 | const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; 285 | return { 286 | content: [{ 287 | type: 'text', 288 | text: `Error: ${errorMessage}` 289 | }], 290 | isError: true 291 | }; 292 | } 293 | } 294 | ); 295 | 296 | // Handle process termination 297 | process.on('SIGINT', () => { 298 | server.close().catch(console.error); 299 | process.exit(0); 300 | }); 301 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // Re-export the ChromeTab type from chrome-remote-interface for compatibility 2 | export type { Target as ChromeTab } from 'chrome-remote-interface'; 3 | 4 | // Interface for DOM element information 5 | export interface DOMElement { 6 | nodeId: number; 7 | tagName: string; 8 | textContent: string | null; 9 | attributes: Record; 10 | boundingBox: { 11 | x: number; 12 | y: number; 13 | width: number; 14 | height: number; 15 | } | null; 16 | isVisible: boolean; 17 | ariaAttributes: Record; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "declaration": true 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | --------------------------------------------------------------------------------