WebSocket Connection Flow for MCP to Chrome with Custom Headers: A Complete Technical Guide
The WebSocket connection flow routes custom headers from CLI arguments through src/cli.ts, src/main.ts, and src/browser.ts before Puppeteer injects them into the HTTP upgrade handshake with Chrome's DevTools Protocol endpoint.
The chrome-devtools-mcp repository enables Model Context Protocol (MCP) servers to control Chrome via the DevTools Protocol. When connecting to a remote or authenticated Chrome instance, you often need to pass custom HTTP headers—such as authorization tokens—during the WebSocket handshake. Understanding the WebSocket connection flow for MCP to Chrome with custom headers ensures you can securely authenticate and establish stable debugging sessions.
Architecture Overview
The connection pipeline consists of four distinct layers. Each layer transforms the header data before passing it to the next stage:
- CLI Parser (
src/cli.ts) – Validates and parses the JSON header string - Main Entry (
src/main.ts) – Transports parsed arguments to the browser module - Browser Connector (
src/browser.ts) – Constructs Puppeteer connection options - Puppeteer CDP – Executes the WebSocket handshake with custom headers
Step-by-Step Connection Flow
CLI Argument Parsing and Validation
The flow begins in src/cli.ts where the command-line interface defines the --wsHeaders option. Lines 68-88 implement a custom coerce function that parses the incoming JSON string into a Record<string, string> type.
If the JSON is malformed or contains non-string values, the parser throws an error immediately. The --wsHeaders flag implicitly requires --wsEndpoint, ensuring headers are only processed when a WebSocket connection is explicitly requested.
Main Entry Point Processing
Once parsed, the arguments flow into src/main.ts. Lines 90-95 export the args object containing the validated wsHeaders property. The main function passes these headers directly to the browser connection helper without transformation or validation, acting as a transparent transport layer.
This design keeps the main entry point agnostic of connection specifics while ensuring the header data reaches the browser module intact.
Browser Connection Setup
The critical transformation occurs in src/browser.ts within the ensureBrowserConnected function. Around line 48, the function signature accepts an options object containing the wsHeaders property.
Lines 63-69 construct the Puppeteer ConnectOptions object. When wsHeaders is present, the code attaches it directly to connectOptions.headers. This is the final preparation step before the WebSocket handshake occurs.
Puppeteer WebSocket Handshake
At line 20, the code invokes puppeteer.connect(connectOptions). Puppeteer's internal CDPConnection implementation uses the supplied headers when performing the HTTP upgrade request to the Chrome remote-debugging WebSocket endpoint specified by wsEndpoint.
The custom headers—such as Authorization: Bearer <token>—are transmitted during the handshake phase, allowing Chrome or intermediate proxies to authenticate the connection before the DevTools Protocol session begins.
DevTools Adapter Initialization
Once the WebSocket connection succeeds, src/DevToolsConnectionAdapter.ts wraps the raw Puppeteer session in a PuppeteerDevToolsConnection instance. This adapter exposes the standard Chrome DevTools Protocol interface to the rest of the MCP toolset.
No further handling of the custom headers occurs at this stage; the connection is established and authenticated, and the MCP server proceeds with standard CDP commands.
Practical Implementation Examples
Connecting via CLI with Authentication
The most common use case involves connecting to a Chrome instance behind an authentication proxy:
npx chrome-devtools-mcp@latest \
--wsEndpoint ws://127.0.0.1:9222/devtools/browser/abc123 \
--wsHeaders '{"Authorization":"Bearer my-token","X-Custom-Id":"session-456"}'
The JSON string must contain only string keys and values. The CLI parser validates this before attempting the connection.
Programmatic Connection in Node.js
For custom MCP server implementations, import the browser connector directly:
import { ensureBrowserConnected } from './src/browser.js';
const browser = await ensureBrowserConnected({
wsEndpoint: 'ws://127.0.0.1:9222/devtools/browser/abc123',
wsHeaders: {
Authorization: 'Bearer my-token',
'X-Request-Source': 'mcp-server'
},
devtools: false,
});
// browser is now a connected Puppeteer Browser instance
Header Injection Point
The actual attachment occurs in src/browser.ts before Puppeteer consumes the options:
// From src/browser.ts, lines 63-69
const connectOptions: puppeteer.ConnectOptions = {
browserWSEndpoint: options.wsEndpoint,
};
if (options.wsHeaders) {
connectOptions.headers = options.wsHeaders; // Headers injected here
}
browser = await puppeteer.connect(connectOptions);
Key Files in the Connection Pipeline
| File | Responsibility | Critical Lines |
|---|---|---|
src/cli.ts |
Defines --wsHeaders flag, parses JSON validation |
68-88 |
src/main.ts |
Transports parsed arguments to browser module | 90-95 |
src/browser.ts |
Constructs Puppeteer options, attaches headers, initiates handshake | 48, 63-69, 20 |
src/DevToolsConnectionAdapter.ts |
Wraps established connection for MCP tooling | Full file |
Summary
- The WebSocket connection flow for MCP to Chrome with custom headers begins with CLI argument parsing in
src/cli.ts, which validates the JSON header format. - The
src/main.tsentry point acts as a transparent transport layer, passing headers to the browser connection helper without modification. - In
src/browser.ts, theensureBrowserConnectedfunction attaches custom headers to Puppeteer'sconnectOptions.headersbefore callingpuppeteer.connect(). - Puppeteer injects these headers during the HTTP upgrade handshake to Chrome's DevTools Protocol WebSocket endpoint, enabling authentication and custom routing.
- Once established, the connection is wrapped by
src/DevToolsConnectionAdapter.tsfor standard MCP tooling access.
Frequently Asked Questions
How do I format the --wsHeaders JSON string for the CLI?
The --wsHeaders flag expects a JSON object with string keys and string values. Use single quotes around the entire argument to prevent shell escaping issues, and double quotes inside the JSON. For example: '{"Authorization":"Bearer token123","X-Custom":"value"}'. The parser in src/cli.ts validates that all values are strings and rejects malformed JSON.
Can I use custom headers when connecting to a local Chrome instance without authentication?
Yes, you can supply custom headers to any WebSocket endpoint, but they are typically only necessary when Chrome runs behind a reverse proxy or requires authentication. If connecting to localhost:9222 without security restrictions, you can omit --wsHeaders entirely. The connection flow supports headers optionally; they are only injected when explicitly provided.
What happens if the custom headers cause the WebSocket handshake to fail?
If Chrome or an intermediate proxy rejects the headers (for example, due to an invalid authorization token), Puppeteer's puppeteer.connect() call will throw a connection error. The error propagates up from src/browser.ts and typically manifests as a timeout or HTTP 401/403 response during the upgrade handshake. Verify your headers are correct and that the Chrome endpoint accepts them.
Is there a way to dynamically change headers after the initial connection?
No, the custom headers are only used during the initial WebSocket handshake in src/browser.ts. Once puppeteer.connect() establishes the session, the connection is maintained over the WebSocket protocol, which does not support mid-stream header modification. If you need to change authentication credentials, you must close the existing connection and create a new one with updated headers via the ensureBrowserConnected function.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →