Skip to content

Commit 41d6a10

Browse files
authored
feat: add WebSocket endpoint and custom headers support (#404)
## Summary Add support for connecting to Chrome via WebSocket endpoint with custom headers, enabling authenticated remote debugging scenarios and providing an alternative to the HTTP-based connection method. ## What's New This PR introduces two new CLI arguments: - **`--wsEndpoint` / `-w`**: Connect directly to Chrome using a WebSocket URL - Example: `ws://127.0.0.1:9222/devtools/browser/<id>` - Alternative to `--browserUrl` (mutually exclusive) - **`--wsHeaders`**: Pass custom headers for WebSocket connections (JSON format) - Example: `'{"Authorization":"Bearer token"}'` - Only works with `--wsEndpoint` ## Use Cases - **Authenticated remote debugging**: Use API keys, tokens, or custom auth headers - **Secured instances**: Connect to browser instances requiring authentication
1 parent 9f9dab0 commit 41d6a10

File tree

5 files changed

+184
-24
lines changed

5 files changed

+184
-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+
- **`--wsEndpoint`, `-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 --wsEndpoint.
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+
"--wsEndpoint=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

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+
wsEndpoint?: string;
47+
wsHeaders?: 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.wsEndpoint) {
61+
connectOptions.browserWSEndpoint = options.wsEndpoint;
62+
if (options.wsHeaders) {
63+
connectOptions.headers = options.wsHeaders;
64+
}
65+
} else if (options.browserURL) {
66+
connectOptions.browserURL = options.browserURL;
67+
} else {
68+
throw new Error('Either browserURL or wsEndpoint 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: 'wsEndpoint',
1718
coerce: (url: string | undefined) => {
1819
if (!url) {
1920
return;
@@ -26,6 +27,54 @@ export const cliOptions = {
2627
return url;
2728
},
2829
},
30+
wsEndpoint: {
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 wsEndpoint ${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 wsEndpoint ${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 --wsEndpoint.',
60+
implies: 'wsEndpoint',
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', 'wsEndpoint'],
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', 'wsEndpoint', '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.wsEndpoint &&
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 --wsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123',
169+
'Connect to an existing browser instance via WebSocket',
170+
],
171+
[
172+
`$0 --wsEndpoint 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.wsEndpoint
63+
? await ensureBrowserConnected({
64+
browserURL: args.browserUrl,
65+
wsEndpoint: args.wsEndpoint,
66+
wsHeaders: 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);

tests/cli.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,55 @@ describe('cli args parsing', () => {
112112
chromeArg: ['--no-sandbox', '--disable-setuid-sandbox'],
113113
});
114114
});
115+
116+
it('parses wsEndpoint with ws:// protocol', async () => {
117+
const args = parseArguments('1.0.0', [
118+
'node',
119+
'main.js',
120+
'--wsEndpoint',
121+
'ws://127.0.0.1:9222/devtools/browser/abc123',
122+
]);
123+
assert.deepStrictEqual(args, {
124+
_: [],
125+
headless: false,
126+
isolated: false,
127+
$0: 'npx chrome-devtools-mcp@latest',
128+
'ws-endpoint': 'ws://127.0.0.1:9222/devtools/browser/abc123',
129+
wsEndpoint: 'ws://127.0.0.1:9222/devtools/browser/abc123',
130+
w: 'ws://127.0.0.1:9222/devtools/browser/abc123',
131+
});
132+
});
133+
134+
it('parses wsEndpoint with wss:// protocol', async () => {
135+
const args = parseArguments('1.0.0', [
136+
'node',
137+
'main.js',
138+
'--wsEndpoint',
139+
'wss://example.com:9222/devtools/browser/abc123',
140+
]);
141+
assert.deepStrictEqual(args, {
142+
_: [],
143+
headless: false,
144+
isolated: false,
145+
$0: 'npx chrome-devtools-mcp@latest',
146+
'ws-endpoint': 'wss://example.com:9222/devtools/browser/abc123',
147+
wsEndpoint: 'wss://example.com:9222/devtools/browser/abc123',
148+
w: 'wss://example.com:9222/devtools/browser/abc123',
149+
});
150+
});
151+
152+
it('parses wsHeaders with valid JSON', async () => {
153+
const args = parseArguments('1.0.0', [
154+
'node',
155+
'main.js',
156+
'--wsEndpoint',
157+
'ws://127.0.0.1:9222/devtools/browser/abc123',
158+
'--wsHeaders',
159+
'{"Authorization":"Bearer token","X-Custom":"value"}',
160+
]);
161+
assert.deepStrictEqual(args.wsHeaders, {
162+
Authorization: 'Bearer token',
163+
'X-Custom': 'value',
164+
});
165+
});
115166
});

0 commit comments

Comments
 (0)