Skip to content

Commit 7a956ab

Browse files
daohoangsonOrKoN
authored andcommitted
feat: add WebSocket endpoint and custom headers support
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. This commit 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`
1 parent 9f9dab0 commit 7a956ab

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)