Skip to content
Closed
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

- **`--browserWsEndpoint`, `-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 --browserWsEndpoint.
- **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",
"--browserWsEndpoint=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
131 changes: 131 additions & 0 deletions plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# 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/<id>`)
- **`headers`** (Record<string, string>): 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/<id>). 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<string, string>;
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
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;
browserWSEndpoint?: string;
headers?: 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.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;
}

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: 'browserWsEndpoint',
coerce: (url: string | undefined) => {
if (!url) {
return;
Expand All @@ -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/<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 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<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', 'browserWsEndpoint'],
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', 'browserWsEndpoint', '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.browserWsEndpoint &&
!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 --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'],
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.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);
Expand Down