Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>). 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
Expand Down Expand Up @@ -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/<id>",
"--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
Expand Down
23 changes: 19 additions & 4 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,33 @@ function makeTargetFilter(devtools: boolean) {
}

export async function ensureBrowserConnected(options: {
browserURL: string;
browserURL?: string;
wsEndpoint?: string;
wsHeaders?: Record<string, string>;
devtools: boolean;
}) {
if (browser?.connected) {
return browser;
}
browser = await puppeteer.connect({

const connectOptions: Parameters<typeof puppeteer.connect>[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;
}

Expand Down
70 changes: 66 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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/<id>). 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<string, string>;
} 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.',
Expand All @@ -34,7 +83,7 @@ export const cliOptions = {
executablePath: {
type: 'string',
description: 'Path to custom Chrome executable.',
conflicts: 'browserUrl',
conflicts: ['browserUrl', 'wsEndpoint'],
alias: 'e',
},
isolated: {
Expand All @@ -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',
Expand Down Expand Up @@ -100,15 +149,28 @@ 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;
})
.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'],
Expand Down
35 changes: 19 additions & 16 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,25 @@ async function getContext(): Promise<McpContext> {
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);
Expand Down
51 changes: 51 additions & 0 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});
});