|
1 | 1 | #!/usr/bin/env node |
| 2 | + |
2 | 3 | /** |
3 | 4 | * @license |
4 | 5 | * Copyright 2025 Google LLC |
5 | 6 | * SPDX-License-Identifier: Apache-2.0 |
6 | 7 | */ |
7 | 8 |
|
8 | | -import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; |
9 | | -import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; |
10 | | -import { |
11 | | - CallToolResult, |
12 | | - SetLevelRequestSchema, |
13 | | -} from '@modelcontextprotocol/sdk/types.js'; |
14 | | -import yargs from 'yargs'; |
15 | | -import {hideBin} from 'yargs/helpers'; |
16 | | - |
17 | | -import {McpResponse} from './McpResponse.js'; |
18 | | -import {McpContext} from './McpContext.js'; |
19 | | - |
20 | | -import {ToolDefinition} from './tools/ToolDefinition.js'; |
21 | | -import {logger, saveLogsToFile} from './logger.js'; |
22 | | -import {Channel, resolveBrowser} from './browser.js'; |
23 | | -import * as emulationTools from './tools/emulation.js'; |
24 | | -import * as consoleTools from './tools/console.js'; |
25 | | -import * as inputTools from './tools/input.js'; |
26 | | -import * as networkTools from './tools/network.js'; |
27 | | -import * as pagesTools from './tools/pages.js'; |
28 | | -import * as performanceTools from './tools/performance.js'; |
29 | | -import * as screenshotTools from './tools/screenshot.js'; |
30 | | -import * as scriptTools from './tools/script.js'; |
31 | | -import * as snapshotTools from './tools/snapshot.js'; |
32 | | - |
33 | | -import path from 'node:path'; |
34 | | -import fs from 'node:fs'; |
35 | | -import assert from 'node:assert'; |
36 | | -import {Mutex} from './Mutex.js'; |
37 | | - |
38 | | -export const cliOptions = { |
39 | | - browserUrl: { |
40 | | - type: 'string' as const, |
41 | | - description: |
42 | | - 'Connect to a running Chrome instance using port forwarding. For more details see: https://developer.chrome.com/docs/devtools/remote-debugging/local-server.', |
43 | | - alias: 'u', |
44 | | - coerce: (url: string) => { |
45 | | - new URL(url); |
46 | | - return url; |
47 | | - }, |
48 | | - }, |
49 | | - headless: { |
50 | | - type: 'boolean' as const, |
51 | | - description: 'Whether to run in headless (no UI) mode.', |
52 | | - default: false, |
53 | | - }, |
54 | | - executablePath: { |
55 | | - type: 'string' as const, |
56 | | - description: 'Path to custom Chrome executable.', |
57 | | - conflicts: 'browserUrl', |
58 | | - alias: 'e', |
59 | | - }, |
60 | | - isolated: { |
61 | | - type: 'boolean' as const, |
62 | | - description: |
63 | | - 'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed.', |
64 | | - default: false, |
65 | | - }, |
66 | | - customDevtools: { |
67 | | - type: 'string' as const, |
68 | | - description: 'Path to custom DevTools.', |
69 | | - hidden: true, |
70 | | - conflicts: 'browserUrl', |
71 | | - alias: 'd', |
72 | | - }, |
73 | | - channel: { |
74 | | - type: 'string' as const, |
75 | | - description: |
76 | | - 'Specify a different Chrome channel that should be used. The default is the stable channel version.', |
77 | | - choices: ['stable', 'canary', 'beta', 'dev'] as const, |
78 | | - conflicts: ['browserUrl', 'executablePath'], |
79 | | - }, |
80 | | - logFile: { |
81 | | - type: 'string' as const, |
82 | | - describe: 'Save the logs to file.', |
83 | | - hidden: true, |
84 | | - }, |
85 | | -}; |
86 | | - |
87 | | -function readPackageJson(): {version?: string} { |
88 | | - const currentDir = import.meta.dirname; |
89 | | - const packageJsonPath = path.join(currentDir, '..', '..', 'package.json'); |
90 | | - if (!fs.existsSync(packageJsonPath)) { |
91 | | - return {}; |
92 | | - } |
93 | | - try { |
94 | | - const json = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); |
95 | | - assert.strict(json['name'], 'chrome-devtools-mcp'); |
96 | | - return json; |
97 | | - } catch { |
98 | | - return {}; |
99 | | - } |
100 | | -} |
101 | | - |
102 | | -const version = readPackageJson().version ?? 'unknown'; |
103 | | - |
104 | | -const yargsInstance = yargs(hideBin(process.argv)) |
105 | | - .scriptName('npx chrome-devtools-mcp@latest') |
106 | | - .options(cliOptions) |
107 | | - .check(args => { |
108 | | - // We can't set default in the options else |
109 | | - // Yargs will complain |
110 | | - if (!args.channel && !args.browserUrl) { |
111 | | - args.channel = 'stable'; |
112 | | - } |
113 | | - return true; |
114 | | - }) |
115 | | - .example([ |
116 | | - [ |
117 | | - '$0 --browserUrl http://127.0.0.1:9222', |
118 | | - 'Connect to an existing browser instance', |
119 | | - ], |
120 | | - ['$0 --channel beta', 'Use Chrome Beta installed on this system'], |
121 | | - ['$0 --channel canary', 'Use Chrome Canary installed on this system'], |
122 | | - ['$0 --channel dev', 'Use Chrome Dev installed on this system'], |
123 | | - ['$0 --channel stable', 'Use stable Chrome installed on this system'], |
124 | | - ['$0 --logFile /tmp/log.txt', 'Save logs to a file'], |
125 | | - ['$0 --help', 'Print CLI options'], |
126 | | - ]); |
127 | | - |
128 | | -export const args = yargsInstance |
129 | | - .wrap(Math.min(120, yargsInstance.terminalWidth())) |
130 | | - .help() |
131 | | - .version(version) |
132 | | - .parseSync(); |
| 9 | +const [major, minor] = process.version.substring(1).split('.').map(Number); |
133 | 10 |
|
134 | | -const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined; |
135 | | - |
136 | | -logger(`Starting Chrome DevTools MCP Server v${version}`); |
137 | | -const server = new McpServer( |
138 | | - { |
139 | | - name: 'chrome_devtools', |
140 | | - title: 'Chrome DevTools MCP server', |
141 | | - version, |
142 | | - }, |
143 | | - {capabilities: {logging: {}}}, |
144 | | -); |
145 | | -server.server.setRequestHandler(SetLevelRequestSchema, () => { |
146 | | - return {}; |
147 | | -}); |
148 | | - |
149 | | -let context: McpContext; |
150 | | -async function getContext(): Promise<McpContext> { |
151 | | - const browser = await resolveBrowser({ |
152 | | - browserUrl: args.browserUrl, |
153 | | - headless: args.headless, |
154 | | - executablePath: args.executablePath, |
155 | | - customDevTools: args.customDevtools, |
156 | | - channel: args.channel as Channel, |
157 | | - isolated: args.isolated, |
158 | | - logFile, |
159 | | - }); |
160 | | - if (context?.browser !== browser) { |
161 | | - context = await McpContext.from(browser, logger); |
162 | | - } |
163 | | - return context; |
164 | | -} |
165 | | - |
166 | | -const logDisclaimers = () => { |
| 11 | +if (major < 22 || (major === 22 && minor < 12)) { |
167 | 12 | console.error( |
168 | | - `chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect, |
169 | | -debug, and modify any data in the browser or DevTools. |
170 | | -Avoid sharing sensitive or personal information that you do want to share with MCP clients.`, |
171 | | - ); |
172 | | -}; |
173 | | - |
174 | | -const toolMutex = new Mutex(); |
175 | | - |
176 | | -function registerTool(tool: ToolDefinition): void { |
177 | | - server.registerTool( |
178 | | - tool.name, |
179 | | - { |
180 | | - description: tool.description, |
181 | | - inputSchema: tool.schema, |
182 | | - annotations: tool.annotations, |
183 | | - }, |
184 | | - async (params): Promise<CallToolResult> => { |
185 | | - const guard = await toolMutex.acquire(); |
186 | | - try { |
187 | | - logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`); |
188 | | - const context = await getContext(); |
189 | | - const response = new McpResponse(); |
190 | | - await tool.handler( |
191 | | - { |
192 | | - params, |
193 | | - }, |
194 | | - response, |
195 | | - context, |
196 | | - ); |
197 | | - try { |
198 | | - const content = await response.handle(tool.name, context); |
199 | | - return { |
200 | | - content, |
201 | | - }; |
202 | | - } catch (error) { |
203 | | - const errorText = |
204 | | - error instanceof Error ? error.message : String(error); |
205 | | - |
206 | | - return { |
207 | | - content: [ |
208 | | - { |
209 | | - type: 'text', |
210 | | - text: errorText, |
211 | | - }, |
212 | | - ], |
213 | | - isError: true, |
214 | | - }; |
215 | | - } |
216 | | - } finally { |
217 | | - guard.dispose(); |
218 | | - } |
219 | | - }, |
| 13 | + `ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 22+.`, |
220 | 14 | ); |
| 15 | + process.exit(1); |
221 | 16 | } |
222 | 17 |
|
223 | | -const tools = [ |
224 | | - ...Object.values(consoleTools), |
225 | | - ...Object.values(emulationTools), |
226 | | - ...Object.values(inputTools), |
227 | | - ...Object.values(networkTools), |
228 | | - ...Object.values(pagesTools), |
229 | | - ...Object.values(performanceTools), |
230 | | - ...Object.values(screenshotTools), |
231 | | - ...Object.values(scriptTools), |
232 | | - ...Object.values(snapshotTools), |
233 | | -]; |
234 | | -for (const tool of tools) { |
235 | | - registerTool(tool as unknown as ToolDefinition); |
236 | | -} |
237 | | - |
238 | | -const transport = new StdioServerTransport(); |
239 | | -await server.connect(transport); |
240 | | -logger('Chrome DevTools MCP Server connected'); |
241 | | -logDisclaimers(); |
| 18 | +await import('./main.js'); |
0 commit comments