From e30768a1b3238dceb335fff963d8f647e52cbc4d Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Thu, 16 Oct 2025 17:51:49 +0700 Subject: [PATCH 1/2] docs: plan v1 --- plan.md | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 000000000..b7b38aea5 --- /dev/null +++ b/plan.md @@ -0,0 +1,116 @@ +# Plan: Add WebSocket Endpoint and Headers Support + +## Overview +Add support for connecting to Chrome via WebSocket endpoint with custom headers, in addition to the existing `--browserUrl` argument. + +## Puppeteer Support Verification ✅ +Based on Puppeteer documentation, the `puppeteer.connect()` method supports: +- **`browserWSEndpoint`** (string): WebSocket URL to connect directly to the browser (e.g., `ws://127.0.0.1:9222/devtools/browser/`) +- **`headers`** (Record): Custom headers for the WebSocket connection (Node.js only) + +Source: `ConnectOptions` interface in Puppeteer Core + +## Current Implementation +- `--browserUrl` / `-u`: Connects using `puppeteer.connect({ browserURL: ... })` +- Uses HTTP endpoint which Puppeteer converts to WebSocket internally +- No support for custom headers + +## Proposed Changes + +### 1. CLI Arguments (src/cli.ts) +Add two new arguments: +```typescript +browserWsEndpoint: { + 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', +} + +wsHeaders: { + type: 'string', + description: 'Custom headers for WebSocket connection in JSON format (e.g., \'{"Authorization":"Bearer token"}\'). Only works with --browserWsEndpoint.', + coerce: (val: string | undefined) => { + if (!val) return undefined; + try { + const parsed = JSON.parse(val); + if (typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('Headers must be a JSON object'); + } + return parsed; + } catch (error) { + throw new Error(`Invalid JSON for wsHeaders: ${error.message}`); + } + }, + implies: 'browserWsEndpoint', +} +``` + +### 2. Browser Connection (src/browser.ts) +Update `ensureBrowserConnected` function: +```typescript +export async function ensureBrowserConnected(options: { + browserURL?: string; + browserWSEndpoint?: string; + headers?: Record; + devtools: boolean; +}) { + if (browser?.connected) { + return browser; + } + + const connectOptions: ConnectOptions = { + targetFilter: makeTargetFilter(options.devtools), + defaultViewport: null, + handleDevToolsAsPage: options.devtools, + }; + + if (options.browserWSEndpoint) { + connectOptions.browserWSEndpoint = options.browserWSEndpoint; + if (options.headers) { + connectOptions.headers = options.headers; + } + } else if (options.browserURL) { + connectOptions.browserURL = options.browserURL; + } + + browser = await puppeteer.connect(connectOptions); + return browser; +} +``` + +### 3. Main Entry Point (src/main.ts) +Update `getContext()` to pass new options: +```typescript +const browser = args.browserUrl || args.browserWsEndpoint + ? await ensureBrowserConnected({ + browserURL: args.browserUrl, + browserWSEndpoint: args.browserWsEndpoint, + headers: args.wsHeaders, + devtools, + }) + : await ensureBrowserLaunched({...}); +``` + +### 4. Documentation (README.md) +Add new options to the configuration section: +- `--browserWsEndpoint`, `-w`: WebSocket endpoint +- `--wsHeaders`: Custom headers in JSON format + +Add usage examples showing: +1. Basic WebSocket connection +2. WebSocket with authentication headers +3. Comparison with browserUrl approach + +## Benefits +1. **Direct WebSocket connection**: Bypass HTTP endpoint resolution +2. **Authentication support**: Pass custom headers for secured browser instances +3. **Flexibility**: Choose between HTTP URL or WebSocket endpoint based on use case +4. **API keys/tokens**: Support authenticated remote debugging scenarios + +## Testing Considerations +- Verify WebSocket URL validation +- Test header JSON parsing (valid/invalid cases) +- Ensure headers only work with WebSocket endpoint +- Test mutual exclusivity of browserUrl and browserWsEndpoint +- Verify wsHeaders requires browserWsEndpoint From c25e1d922fb4ee02c895501c22e99b7235667816 Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Thu, 16 Oct 2025 18:02:36 +0700 Subject: [PATCH 2/2] feat: add WebSocket endpoint and custom headers support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for connecting to Chrome via WebSocket endpoint with custom headers: - Add --browserWsEndpoint/-w argument for direct WebSocket connections - Add --wsHeaders argument for custom WebSocket headers (e.g., auth tokens) - Update ensureBrowserConnected to support both browserURL and browserWSEndpoint - Add validation for WebSocket protocol (ws:// or wss://) - Add JSON validation for custom headers - Update documentation with usage examples This enables authenticated remote debugging scenarios and provides an alternative to the HTTP-based --browserUrl connection method. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 29 +++++++++++++++++++++ plan.md | 15 +++++++++++ src/browser.ts | 23 ++++++++++++++--- src/cli.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++++--- src/main.ts | 35 +++++++++++++------------ 5 files changed, 148 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1aaab3e66..68b68de63 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 +- **`--browserWsEndpoint`, `-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 --browserWsEndpoint. + - **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", + "--browserWsEndpoint=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/plan.md b/plan.md index b7b38aea5..149c517b5 100644 --- a/plan.md +++ b/plan.md @@ -1,16 +1,20 @@ # Plan: Add WebSocket Endpoint and Headers Support ## Overview + Add support for connecting to Chrome via WebSocket endpoint with custom headers, in addition to the existing `--browserUrl` argument. ## Puppeteer Support Verification ✅ + Based on Puppeteer documentation, the `puppeteer.connect()` method supports: + - **`browserWSEndpoint`** (string): WebSocket URL to connect directly to the browser (e.g., `ws://127.0.0.1:9222/devtools/browser/`) - **`headers`** (Record): Custom headers for the WebSocket connection (Node.js only) Source: `ConnectOptions` interface in Puppeteer Core ## Current Implementation + - `--browserUrl` / `-u`: Connects using `puppeteer.connect({ browserURL: ... })` - Uses HTTP endpoint which Puppeteer converts to WebSocket internally - No support for custom headers @@ -18,7 +22,9 @@ Source: `ConnectOptions` interface in Puppeteer Core ## Proposed Changes ### 1. CLI Arguments (src/cli.ts) + Add two new arguments: + ```typescript browserWsEndpoint: { type: 'string', @@ -47,7 +53,9 @@ wsHeaders: { ``` ### 2. Browser Connection (src/browser.ts) + Update `ensureBrowserConnected` function: + ```typescript export async function ensureBrowserConnected(options: { browserURL?: string; @@ -80,7 +88,9 @@ export async function ensureBrowserConnected(options: { ``` ### 3. Main Entry Point (src/main.ts) + Update `getContext()` to pass new options: + ```typescript const browser = args.browserUrl || args.browserWsEndpoint ? await ensureBrowserConnected({ @@ -93,22 +103,27 @@ const browser = args.browserUrl || args.browserWsEndpoint ``` ### 4. Documentation (README.md) + Add new options to the configuration section: + - `--browserWsEndpoint`, `-w`: WebSocket endpoint - `--wsHeaders`: Custom headers in JSON format Add usage examples showing: + 1. Basic WebSocket connection 2. WebSocket with authentication headers 3. Comparison with browserUrl approach ## Benefits + 1. **Direct WebSocket connection**: Bypass HTTP endpoint resolution 2. **Authentication support**: Pass custom headers for secured browser instances 3. **Flexibility**: Choose between HTTP URL or WebSocket endpoint based on use case 4. **API keys/tokens**: Support authenticated remote debugging scenarios ## Testing Considerations + - Verify WebSocket URL validation - Test header JSON parsing (valid/invalid cases) - Ensure headers only work with WebSocket endpoint diff --git a/src/browser.ts b/src/browser.ts index a54d4e4b2..4b48468a2 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; + browserWSEndpoint?: string; + headers?: 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.browserWSEndpoint) { + connectOptions.browserWSEndpoint = options.browserWSEndpoint; + if (options.headers) { + connectOptions.headers = options.headers; + } + } else if (options.browserURL) { + connectOptions.browserURL = options.browserURL; + } else { + throw new Error('Either browserURL or browserWSEndpoint must be provided'); + } + + browser = await puppeteer.connect(connectOptions); return browser; } diff --git a/src/cli.ts b/src/cli.ts index 513fff338..f7a0c80db 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: 'browserWsEndpoint', coerce: (url: string | undefined) => { if (!url) { return; @@ -26,6 +27,54 @@ export const cliOptions = { return url; }, }, + browserWsEndpoint: { + 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 browserWsEndpoint ${url} must use ws:// or wss:// protocol.`, + ); + } + return url; + } catch (error) { + if ((error as Error).message.includes('ws://')) { + throw error; + } + throw new Error(`Provided browserWsEndpoint ${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 --browserWsEndpoint.', + implies: 'browserWsEndpoint', + 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', 'browserWsEndpoint'], 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', 'browserWsEndpoint', '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.browserWsEndpoint && + !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 --browserWsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123', + 'Connect to an existing browser instance via WebSocket', + ], + [ + `$0 --browserWsEndpoint 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 d86ad6dc9..09bcfa736 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.browserWsEndpoint + ? await ensureBrowserConnected({ + browserURL: args.browserUrl, + browserWSEndpoint: args.browserWsEndpoint, + headers: 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);