Skip to content

Commit ea8b6da

Browse files
committed
feat: Add CDP headers support for authenticated connections
- Add cdpHeaders option to browser configuration - Support --cdp-headers CLI option for JSON header specification - Add PLAYWRIGHT_MCP_CDP_HEADERS environment variable support - Update CdpContextFactory to pass headers to connectOverCDP - Add JSON parsing with error handling for CLI headers - Add tests for headers functionality and invalid JSON handling - Update documentation with usage examples This enables connecting to CDP endpoints that require authentication, such as AWS Bedrock AgentCore Browser instances. Unify CDP headers parsing and improve error messages - Remove duplicate CDP headers documentation from README - Replace manual JSON parsing with parseJsonObject function - Use InvalidArgumentError for user-friendly error messages - Maintain consistency with other option parsers like commaSeparatedList Addresses review feedback on standardizing parse methods. fix: resolve ESLint violations
1 parent 0812df2 commit ea8b6da

File tree

6 files changed

+63
-2
lines changed

6 files changed

+63
-2
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ Playwright MCP server supports following arguments. They can be provided in the
193193
--caps <caps> comma-separated list of additional capabilities
194194
to enable, possible values: vision, pdf.
195195
--cdp-endpoint <endpoint> CDP endpoint to connect to.
196+
--cdp-headers <headers> JSON string of headers to send with CDP
197+
connection, e.g. '{"Authorization": "Bearer
198+
token"}'
196199
--config <path> path to the configuration file.
197200
--device <device> device to emulate, for example: "iPhone 15"
198201
--executable-path <path> path to the browser executable.

config.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export type Config = {
5959
*/
6060
cdpEndpoint?: string;
6161

62+
/**
63+
* Additional HTTP headers to be sent with CDP connect request.
64+
* Only used when cdpEndpoint is specified.
65+
*/
66+
cdpHeaders?: Record<string, string>;
67+
6268
/**
6369
* Remote endpoint to connect to an existing Playwright server.
6470
*/

src/browserContextFactory.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ class CdpContextFactory extends BaseContextFactory {
128128
}
129129

130130
protected override async _doObtainBrowser(): Promise<playwright.Browser> {
131-
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!);
131+
const options: any = {};
132+
if (this.config.browser.cdpHeaders)
133+
options.headers = this.config.browser.cdpHeaders;
134+
135+
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!, options);
132136
}
133137

134138
protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {

src/config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import fs from 'fs';
1818
import os from 'os';
1919
import path from 'path';
2020
import { devices } from 'playwright';
21+
import { InvalidArgumentError } from 'commander';
2122
import { sanitizeForFilePath } from './utils/fileUtils.js';
2223

2324
import type { Config, ToolCapability } from '../config.js';
@@ -30,6 +31,7 @@ export type CLIOptions = {
3031
browser?: string;
3132
caps?: string[];
3233
cdpEndpoint?: string;
34+
cdpHeaders?: Record<string, string>;
3335
config?: string;
3436
device?: string;
3537
executablePath?: string;
@@ -178,6 +180,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
178180
launchOptions,
179181
contextOptions,
180182
cdpEndpoint: cliOptions.cdpEndpoint,
183+
cdpHeaders: cliOptions.cdpHeaders,
181184
},
182185
server: {
183186
port: cliOptions.port,
@@ -205,6 +208,7 @@ function configFromEnv(): Config {
205208
options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
206209
options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS);
207210
options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
211+
options.cdpHeaders = parseJsonObject(process.env.PLAYWRIGHT_MCP_CDP_HEADERS);
208212
options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
209213
options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
210214
options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
@@ -295,6 +299,24 @@ export function semicolonSeparatedList(value: string | undefined): string[] | un
295299
return value.split(';').map(v => v.trim());
296300
}
297301

302+
export function parseJsonObject(value: string | undefined): Record<string, string> | undefined {
303+
if (!value)
304+
return undefined;
305+
try {
306+
const parsed = JSON.parse(value);
307+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
308+
throw new InvalidArgumentError('Expected JSON object');
309+
310+
return parsed;
311+
} catch (error) {
312+
if (error instanceof InvalidArgumentError)
313+
throw error;
314+
315+
const errorMessage = error instanceof Error ? error.message : String(error);
316+
throw new InvalidArgumentError(`Invalid JSON format: ${errorMessage}`);
317+
}
318+
}
319+
298320
export function commaSeparatedList(value: string | undefined): string[] | undefined {
299321
if (!value)
300322
return undefined;

src/program.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { program, Option } from 'commander';
1818
import * as mcpServer from './mcp/server.js';
19-
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
19+
import { commaSeparatedList, parseJsonObject, resolveCLIConfig, semicolonSeparatedList } from './config.js';
2020
import { packageJSON } from './utils/package.js';
2121
import { Context } from './context.js';
2222
import { contextFactory } from './browserContextFactory.js';
@@ -37,6 +37,7 @@ program
3737
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
3838
.option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList)
3939
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
40+
.option('--cdp-headers <headers>', 'JSON string of headers to send with CDP connection, e.g. \'{"Authorization": "Bearer token"}\'', parseJsonObject)
4041
.option('--config <path>', 'path to the configuration file.')
4142
.option('--device <device>', 'device to emulate, for example: "iPhone 15"')
4243
.option('--executable-path <path>', 'path to the browser executable.')

tests/cdp.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,31 @@ test('should throw connection error and allow re-connecting', async ({ cdpServer
8484
});
8585
});
8686

87+
test('cdp server with headers', async ({ cdpServer, startClient, server }) => {
88+
await cdpServer.start();
89+
const headers = { 'X-Test-Header': 'test-value' };
90+
const { client } = await startClient({
91+
args: [`--cdp-endpoint=${cdpServer.endpoint}`, `--cdp-headers=${JSON.stringify(headers)}`]
92+
});
93+
expect(await client.callTool({
94+
name: 'browser_navigate',
95+
arguments: { url: server.HELLO_WORLD },
96+
})).toHaveResponse({
97+
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
98+
});
99+
});
100+
101+
test('cdp server with invalid headers JSON', async () => {
102+
const result = spawnSync('node', [
103+
path.join(__filename, '../../cli.js'),
104+
'--cdp-endpoint=http://localhost:1234',
105+
'--cdp-headers=invalid-json'
106+
]);
107+
expect(result.error).toBeUndefined();
108+
expect(result.status).toBe(1);
109+
expect(result.stderr.toString()).toContain('option \'--cdp-headers <headers>\' argument \'invalid-json\' is invalid');
110+
});
111+
87112
// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
88113
const __filename = url.fileURLToPath(import.meta.url);
89114

0 commit comments

Comments
 (0)