diff --git a/README.md b/README.md index 0b78b85b..bf87b8df 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,14 @@ The Chrome DevTools MCP server supports the following configuration option: Connect to a running Chrome instance using port forwarding. For more details see: https://developer.chrome.com/docs/devtools/remote-debugging/local-server. - **Type:** string +- **`--wsEndpoint`, `-w`** + WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/). Alternative to --browserUrl. + - **Type:** string + +- **`--wsHeaders`** + Custom headers for WebSocket connection in JSON format (e.g., '{"Authorization":"Bearer token"}'). Only works with --wsEndpoint. + - **Type:** string + - **`--headless`** Whether to run in headless (no UI) mode. - **Type:** boolean @@ -343,6 +351,27 @@ Pass them via the `args` property in the JSON configuration. For example: } ``` +### Connecting via WebSocket with custom headers + +You can connect directly to a Chrome WebSocket endpoint and include custom headers (e.g., for authentication): + +```json +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": [ + "chrome-devtools-mcp@latest", + "--wsEndpoint=ws://127.0.0.1:9222/devtools/browser/", + "--wsHeaders={\"Authorization\":\"Bearer YOUR_TOKEN\"}" + ] + } + } +} +``` + +To get the WebSocket endpoint from a running Chrome instance, visit `http://127.0.0.1:9222/json/version` and look for the `webSocketDebuggerUrl` field. + You can also run `npx chrome-devtools-mcp@latest --help` to see all available configuration options. ## Concepts diff --git a/src/browser.ts b/src/browser.ts index a54d4e4b..d76d5a9f 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -42,18 +42,33 @@ function makeTargetFilter(devtools: boolean) { } export async function ensureBrowserConnected(options: { - browserURL: string; + browserURL?: string; + wsEndpoint?: string; + wsHeaders?: Record; devtools: boolean; }) { if (browser?.connected) { return browser; } - browser = await puppeteer.connect({ + + const connectOptions: Parameters[0] = { targetFilter: makeTargetFilter(options.devtools), - browserURL: options.browserURL, defaultViewport: null, handleDevToolsAsPage: options.devtools, - }); + }; + + if (options.wsEndpoint) { + connectOptions.browserWSEndpoint = options.wsEndpoint; + if (options.wsHeaders) { + connectOptions.headers = options.wsHeaders; + } + } else if (options.browserURL) { + connectOptions.browserURL = options.browserURL; + } else { + throw new Error('Either browserURL or wsEndpoint must be provided'); + } + + browser = await puppeteer.connect(connectOptions); return browser; } diff --git a/src/cli.ts b/src/cli.ts index 513fff33..89c8097d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,6 +14,7 @@ export const cliOptions = { description: 'Connect to a running Chrome instance using port forwarding. For more details see: https://developer.chrome.com/docs/devtools/remote-debugging/local-server.', alias: 'u', + conflicts: 'wsEndpoint', coerce: (url: string | undefined) => { if (!url) { return; @@ -26,6 +27,54 @@ export const cliOptions = { return url; }, }, + wsEndpoint: { + type: 'string', + description: + 'WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/). Alternative to --browserUrl.', + alias: 'w', + conflicts: 'browserUrl', + coerce: (url: string | undefined) => { + if (!url) { + return; + } + try { + const parsed = new URL(url); + if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') { + throw new Error( + `Provided wsEndpoint ${url} must use ws:// or wss:// protocol.`, + ); + } + return url; + } catch (error) { + if ((error as Error).message.includes('ws://')) { + throw error; + } + throw new Error(`Provided wsEndpoint ${url} is not valid URL.`); + } + }, + }, + wsHeaders: { + type: 'string', + description: + 'Custom headers for WebSocket connection in JSON format (e.g., \'{"Authorization":"Bearer token"}\'). Only works with --wsEndpoint.', + implies: 'wsEndpoint', + coerce: (val: string | undefined) => { + if (!val) { + return; + } + try { + const parsed = JSON.parse(val); + if (typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('Headers must be a JSON object'); + } + return parsed as Record; + } catch (error) { + throw new Error( + `Invalid JSON for wsHeaders: ${(error as Error).message}`, + ); + } + }, + }, headless: { type: 'boolean', description: 'Whether to run in headless (no UI) mode.', @@ -34,7 +83,7 @@ export const cliOptions = { executablePath: { type: 'string', description: 'Path to custom Chrome executable.', - conflicts: 'browserUrl', + conflicts: ['browserUrl', 'wsEndpoint'], alias: 'e', }, isolated: { @@ -48,7 +97,7 @@ export const cliOptions = { description: 'Specify a different Chrome channel that should be used. The default is the stable channel version.', choices: ['stable', 'canary', 'beta', 'dev'] as const, - conflicts: ['browserUrl', 'executablePath'], + conflicts: ['browserUrl', 'wsEndpoint', 'executablePath'], }, logFile: { type: 'string', @@ -100,7 +149,12 @@ export function parseArguments(version: string, argv = process.argv) { .check(args => { // We can't set default in the options else // Yargs will complain - if (!args.channel && !args.browserUrl && !args.executablePath) { + if ( + !args.channel && + !args.browserUrl && + !args.wsEndpoint && + !args.executablePath + ) { args.channel = 'stable'; } return true; @@ -108,7 +162,15 @@ export function parseArguments(version: string, argv = process.argv) { .example([ [ '$0 --browserUrl http://127.0.0.1:9222', - 'Connect to an existing browser instance', + 'Connect to an existing browser instance via HTTP', + ], + [ + '$0 --wsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123', + 'Connect to an existing browser instance via WebSocket', + ], + [ + `$0 --wsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123 --wsHeaders '{"Authorization":"Bearer token"}'`, + 'Connect via WebSocket with custom headers', ], ['$0 --channel beta', 'Use Chrome Beta installed on this system'], ['$0 --channel canary', 'Use Chrome Canary installed on this system'], diff --git a/src/main.ts b/src/main.ts index d86ad6dc..a9e63f95 100644 --- a/src/main.ts +++ b/src/main.ts @@ -58,22 +58,25 @@ async function getContext(): Promise { extraArgs.push(`--proxy-server=${args.proxyServer}`); } const devtools = args.experimentalDevtools ?? false; - const browser = args.browserUrl - ? await ensureBrowserConnected({ - browserURL: args.browserUrl, - devtools, - }) - : await ensureBrowserLaunched({ - headless: args.headless, - executablePath: args.executablePath, - channel: args.channel as Channel, - isolated: args.isolated, - logFile, - viewport: args.viewport, - args: extraArgs, - acceptInsecureCerts: args.acceptInsecureCerts, - devtools, - }); + const browser = + args.browserUrl || args.wsEndpoint + ? await ensureBrowserConnected({ + browserURL: args.browserUrl, + wsEndpoint: args.wsEndpoint, + wsHeaders: args.wsHeaders, + devtools, + }) + : await ensureBrowserLaunched({ + headless: args.headless, + executablePath: args.executablePath, + channel: args.channel as Channel, + isolated: args.isolated, + logFile, + viewport: args.viewport, + args: extraArgs, + acceptInsecureCerts: args.acceptInsecureCerts, + devtools, + }); if (context?.browser !== browser) { context = await McpContext.from(browser, logger); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index ab46582f..2c66ac13 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -112,4 +112,55 @@ describe('cli args parsing', () => { chromeArg: ['--no-sandbox', '--disable-setuid-sandbox'], }); }); + + it('parses wsEndpoint with ws:// protocol', async () => { + const args = parseArguments('1.0.0', [ + 'node', + 'main.js', + '--wsEndpoint', + 'ws://127.0.0.1:9222/devtools/browser/abc123', + ]); + assert.deepStrictEqual(args, { + _: [], + headless: false, + isolated: false, + $0: 'npx chrome-devtools-mcp@latest', + 'ws-endpoint': 'ws://127.0.0.1:9222/devtools/browser/abc123', + wsEndpoint: 'ws://127.0.0.1:9222/devtools/browser/abc123', + w: 'ws://127.0.0.1:9222/devtools/browser/abc123', + }); + }); + + it('parses wsEndpoint with wss:// protocol', async () => { + const args = parseArguments('1.0.0', [ + 'node', + 'main.js', + '--wsEndpoint', + 'wss://example.com:9222/devtools/browser/abc123', + ]); + assert.deepStrictEqual(args, { + _: [], + headless: false, + isolated: false, + $0: 'npx chrome-devtools-mcp@latest', + 'ws-endpoint': 'wss://example.com:9222/devtools/browser/abc123', + wsEndpoint: 'wss://example.com:9222/devtools/browser/abc123', + w: 'wss://example.com:9222/devtools/browser/abc123', + }); + }); + + it('parses wsHeaders with valid JSON', async () => { + const args = parseArguments('1.0.0', [ + 'node', + 'main.js', + '--wsEndpoint', + 'ws://127.0.0.1:9222/devtools/browser/abc123', + '--wsHeaders', + '{"Authorization":"Bearer token","X-Custom":"value"}', + ]); + assert.deepStrictEqual(args.wsHeaders, { + Authorization: 'Bearer token', + 'X-Custom': 'value', + }); + }); });