Skip to content

Commit c25e1d9

Browse files
daohoangsonclaude
andcommitted
feat: add WebSocket endpoint and custom headers support
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 <[email protected]>
1 parent e30768a commit c25e1d9

File tree

5 files changed

+148
-24
lines changed

5 files changed

+148
-24
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,14 @@ The Chrome DevTools MCP server supports the following configuration option:
284284
Connect to a running Chrome instance using port forwarding. For more details see: https://developer.chrome.com/docs/devtools/remote-debugging/local-server.
285285
- **Type:** string
286286

287+
- **`--browserWsEndpoint`, `-w`**
288+
WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.
289+
- **Type:** string
290+
291+
- **`--wsHeaders`**
292+
Custom headers for WebSocket connection in JSON format (e.g., '{"Authorization":"Bearer token"}'). Only works with --browserWsEndpoint.
293+
- **Type:** string
294+
287295
- **`--headless`**
288296
Whether to run in headless (no UI) mode.
289297
- **Type:** boolean
@@ -343,6 +351,27 @@ Pass them via the `args` property in the JSON configuration. For example:
343351
}
344352
```
345353

354+
### Connecting via WebSocket with custom headers
355+
356+
You can connect directly to a Chrome WebSocket endpoint and include custom headers (e.g., for authentication):
357+
358+
```json
359+
{
360+
"mcpServers": {
361+
"chrome-devtools": {
362+
"command": "npx",
363+
"args": [
364+
"chrome-devtools-mcp@latest",
365+
"--browserWsEndpoint=ws://127.0.0.1:9222/devtools/browser/<id>",
366+
"--wsHeaders={\"Authorization\":\"Bearer YOUR_TOKEN\"}"
367+
]
368+
}
369+
}
370+
}
371+
```
372+
373+
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.
374+
346375
You can also run `npx chrome-devtools-mcp@latest --help` to see all available configuration options.
347376

348377
## Concepts

plan.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
# Plan: Add WebSocket Endpoint and Headers Support
22

33
## Overview
4+
45
Add support for connecting to Chrome via WebSocket endpoint with custom headers, in addition to the existing `--browserUrl` argument.
56

67
## Puppeteer Support Verification ✅
8+
79
Based on Puppeteer documentation, the `puppeteer.connect()` method supports:
10+
811
- **`browserWSEndpoint`** (string): WebSocket URL to connect directly to the browser (e.g., `ws://127.0.0.1:9222/devtools/browser/<id>`)
912
- **`headers`** (Record<string, string>): Custom headers for the WebSocket connection (Node.js only)
1013

1114
Source: `ConnectOptions` interface in Puppeteer Core
1215

1316
## Current Implementation
17+
1418
- `--browserUrl` / `-u`: Connects using `puppeteer.connect({ browserURL: ... })`
1519
- Uses HTTP endpoint which Puppeteer converts to WebSocket internally
1620
- No support for custom headers
1721

1822
## Proposed Changes
1923

2024
### 1. CLI Arguments (src/cli.ts)
25+
2126
Add two new arguments:
27+
2228
```typescript
2329
browserWsEndpoint: {
2430
type: 'string',
@@ -47,7 +53,9 @@ wsHeaders: {
4753
```
4854

4955
### 2. Browser Connection (src/browser.ts)
56+
5057
Update `ensureBrowserConnected` function:
58+
5159
```typescript
5260
export async function ensureBrowserConnected(options: {
5361
browserURL?: string;
@@ -80,7 +88,9 @@ export async function ensureBrowserConnected(options: {
8088
```
8189

8290
### 3. Main Entry Point (src/main.ts)
91+
8392
Update `getContext()` to pass new options:
93+
8494
```typescript
8595
const browser = args.browserUrl || args.browserWsEndpoint
8696
? await ensureBrowserConnected({
@@ -93,22 +103,27 @@ const browser = args.browserUrl || args.browserWsEndpoint
93103
```
94104

95105
### 4. Documentation (README.md)
106+
96107
Add new options to the configuration section:
108+
97109
- `--browserWsEndpoint`, `-w`: WebSocket endpoint
98110
- `--wsHeaders`: Custom headers in JSON format
99111

100112
Add usage examples showing:
113+
101114
1. Basic WebSocket connection
102115
2. WebSocket with authentication headers
103116
3. Comparison with browserUrl approach
104117

105118
## Benefits
119+
106120
1. **Direct WebSocket connection**: Bypass HTTP endpoint resolution
107121
2. **Authentication support**: Pass custom headers for secured browser instances
108122
3. **Flexibility**: Choose between HTTP URL or WebSocket endpoint based on use case
109123
4. **API keys/tokens**: Support authenticated remote debugging scenarios
110124

111125
## Testing Considerations
126+
112127
- Verify WebSocket URL validation
113128
- Test header JSON parsing (valid/invalid cases)
114129
- Ensure headers only work with WebSocket endpoint

src/browser.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,33 @@ function makeTargetFilter(devtools: boolean) {
4242
}
4343

4444
export async function ensureBrowserConnected(options: {
45-
browserURL: string;
45+
browserURL?: string;
46+
browserWSEndpoint?: string;
47+
headers?: Record<string, string>;
4648
devtools: boolean;
4749
}) {
4850
if (browser?.connected) {
4951
return browser;
5052
}
51-
browser = await puppeteer.connect({
53+
54+
const connectOptions: Parameters<typeof puppeteer.connect>[0] = {
5255
targetFilter: makeTargetFilter(options.devtools),
53-
browserURL: options.browserURL,
5456
defaultViewport: null,
5557
handleDevToolsAsPage: options.devtools,
56-
});
58+
};
59+
60+
if (options.browserWSEndpoint) {
61+
connectOptions.browserWSEndpoint = options.browserWSEndpoint;
62+
if (options.headers) {
63+
connectOptions.headers = options.headers;
64+
}
65+
} else if (options.browserURL) {
66+
connectOptions.browserURL = options.browserURL;
67+
} else {
68+
throw new Error('Either browserURL or browserWSEndpoint must be provided');
69+
}
70+
71+
browser = await puppeteer.connect(connectOptions);
5772
return browser;
5873
}
5974

src/cli.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const cliOptions = {
1414
description:
1515
'Connect to a running Chrome instance using port forwarding. For more details see: https://developer.chrome.com/docs/devtools/remote-debugging/local-server.',
1616
alias: 'u',
17+
conflicts: 'browserWsEndpoint',
1718
coerce: (url: string | undefined) => {
1819
if (!url) {
1920
return;
@@ -26,6 +27,54 @@ export const cliOptions = {
2627
return url;
2728
},
2829
},
30+
browserWsEndpoint: {
31+
type: 'string',
32+
description:
33+
'WebSocket endpoint to connect to a running Chrome instance (e.g., ws://127.0.0.1:9222/devtools/browser/<id>). Alternative to --browserUrl.',
34+
alias: 'w',
35+
conflicts: 'browserUrl',
36+
coerce: (url: string | undefined) => {
37+
if (!url) {
38+
return;
39+
}
40+
try {
41+
const parsed = new URL(url);
42+
if (parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') {
43+
throw new Error(
44+
`Provided browserWsEndpoint ${url} must use ws:// or wss:// protocol.`,
45+
);
46+
}
47+
return url;
48+
} catch (error) {
49+
if ((error as Error).message.includes('ws://')) {
50+
throw error;
51+
}
52+
throw new Error(`Provided browserWsEndpoint ${url} is not valid URL.`);
53+
}
54+
},
55+
},
56+
wsHeaders: {
57+
type: 'string',
58+
description:
59+
'Custom headers for WebSocket connection in JSON format (e.g., \'{"Authorization":"Bearer token"}\'). Only works with --browserWsEndpoint.',
60+
implies: 'browserWsEndpoint',
61+
coerce: (val: string | undefined) => {
62+
if (!val) {
63+
return;
64+
}
65+
try {
66+
const parsed = JSON.parse(val);
67+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
68+
throw new Error('Headers must be a JSON object');
69+
}
70+
return parsed as Record<string, string>;
71+
} catch (error) {
72+
throw new Error(
73+
`Invalid JSON for wsHeaders: ${(error as Error).message}`,
74+
);
75+
}
76+
},
77+
},
2978
headless: {
3079
type: 'boolean',
3180
description: 'Whether to run in headless (no UI) mode.',
@@ -34,7 +83,7 @@ export const cliOptions = {
3483
executablePath: {
3584
type: 'string',
3685
description: 'Path to custom Chrome executable.',
37-
conflicts: 'browserUrl',
86+
conflicts: ['browserUrl', 'browserWsEndpoint'],
3887
alias: 'e',
3988
},
4089
isolated: {
@@ -48,7 +97,7 @@ export const cliOptions = {
4897
description:
4998
'Specify a different Chrome channel that should be used. The default is the stable channel version.',
5099
choices: ['stable', 'canary', 'beta', 'dev'] as const,
51-
conflicts: ['browserUrl', 'executablePath'],
100+
conflicts: ['browserUrl', 'browserWsEndpoint', 'executablePath'],
52101
},
53102
logFile: {
54103
type: 'string',
@@ -100,15 +149,28 @@ export function parseArguments(version: string, argv = process.argv) {
100149
.check(args => {
101150
// We can't set default in the options else
102151
// Yargs will complain
103-
if (!args.channel && !args.browserUrl && !args.executablePath) {
152+
if (
153+
!args.channel &&
154+
!args.browserUrl &&
155+
!args.browserWsEndpoint &&
156+
!args.executablePath
157+
) {
104158
args.channel = 'stable';
105159
}
106160
return true;
107161
})
108162
.example([
109163
[
110164
'$0 --browserUrl http://127.0.0.1:9222',
111-
'Connect to an existing browser instance',
165+
'Connect to an existing browser instance via HTTP',
166+
],
167+
[
168+
'$0 --browserWsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123',
169+
'Connect to an existing browser instance via WebSocket',
170+
],
171+
[
172+
`$0 --browserWsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123 --wsHeaders '{"Authorization":"Bearer token"}'`,
173+
'Connect via WebSocket with custom headers',
112174
],
113175
['$0 --channel beta', 'Use Chrome Beta installed on this system'],
114176
['$0 --channel canary', 'Use Chrome Canary installed on this system'],

src/main.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,25 @@ async function getContext(): Promise<McpContext> {
5858
extraArgs.push(`--proxy-server=${args.proxyServer}`);
5959
}
6060
const devtools = args.experimentalDevtools ?? false;
61-
const browser = args.browserUrl
62-
? await ensureBrowserConnected({
63-
browserURL: args.browserUrl,
64-
devtools,
65-
})
66-
: await ensureBrowserLaunched({
67-
headless: args.headless,
68-
executablePath: args.executablePath,
69-
channel: args.channel as Channel,
70-
isolated: args.isolated,
71-
logFile,
72-
viewport: args.viewport,
73-
args: extraArgs,
74-
acceptInsecureCerts: args.acceptInsecureCerts,
75-
devtools,
76-
});
61+
const browser =
62+
args.browserUrl || args.browserWsEndpoint
63+
? await ensureBrowserConnected({
64+
browserURL: args.browserUrl,
65+
browserWSEndpoint: args.browserWsEndpoint,
66+
headers: args.wsHeaders,
67+
devtools,
68+
})
69+
: await ensureBrowserLaunched({
70+
headless: args.headless,
71+
executablePath: args.executablePath,
72+
channel: args.channel as Channel,
73+
isolated: args.isolated,
74+
logFile,
75+
viewport: args.viewport,
76+
args: extraArgs,
77+
acceptInsecureCerts: args.acceptInsecureCerts,
78+
devtools,
79+
});
7780

7881
if (context?.browser !== browser) {
7982
context = await McpContext.from(browser, logger);

0 commit comments

Comments
 (0)