Skip to content
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ Playwright MCP server supports following arguments. They can be provided in the
--caps <caps> comma-separated list of additional capabilities
to enable, possible values: vision, pdf.
--cdp-endpoint <endpoint> CDP endpoint to connect to.
--cdp-headers <headers> JSON string of headers to send with CDP
connection, e.g. '{"Authorization": "Bearer
token"}'
--config <path> path to the configuration file.
--device <device> device to emulate, for example: "iPhone 15"
--executable-path <path> path to the browser executable.
Expand Down
6 changes: 6 additions & 0 deletions config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export type Config = {
*/
cdpEndpoint?: string;

/**
* Additional HTTP headers to be sent with CDP connect request.
* Only used when cdpEndpoint is specified.
*/
cdpHeaders?: Record<string, string>;

/**
* Remote endpoint to connect to an existing Playwright server.
*/
Expand Down
6 changes: 5 additions & 1 deletion src/browserContextFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ class CdpContextFactory extends BaseContextFactory {
}

protected override async _doObtainBrowser(): Promise<playwright.Browser> {
return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!);
const options: playwright.ConnectOverCDPOptions = {};
if (this.config.browser.cdpHeaders)
options.headers = this.config.browser.cdpHeaders;

return playwright.chromium.connectOverCDP(this.config.browser.cdpEndpoint!, options);
}

protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {
Expand Down
23 changes: 23 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import { devices } from 'playwright';
import { InvalidArgumentError } from 'commander';
import { sanitizeForFilePath } from './utils/fileUtils.js';

import type { Config, ToolCapability } from '../config.js';
Expand All @@ -30,6 +31,7 @@ export type CLIOptions = {
browser?: string;
caps?: string[];
cdpEndpoint?: string;
cdpHeaders?: Record<string, string>;
config?: string;
device?: string;
executablePath?: string;
Expand Down Expand Up @@ -178,6 +180,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
launchOptions,
contextOptions,
cdpEndpoint: cliOptions.cdpEndpoint,
cdpHeaders: cliOptions.cdpHeaders,
},
server: {
port: cliOptions.port,
Expand Down Expand Up @@ -205,6 +208,7 @@ function configFromEnv(): Config {
options.browser = envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
options.caps = commaSeparatedList(process.env.PLAYWRIGHT_MCP_CAPS);
options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
options.cdpHeaders = parseJsonObject(process.env.PLAYWRIGHT_MCP_CDP_HEADERS);
options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
Expand Down Expand Up @@ -295,6 +299,25 @@ export function semicolonSeparatedList(value: string | undefined): string[] | un
return value.split(';').map(v => v.trim());
}

export function parseJsonObject(value: string | undefined): Record<string, string> | undefined {
if (!value)
return undefined;

let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new InvalidArgumentError(`Invalid JSON format: ${errorMessage}`);
}

if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new InvalidArgumentError('Expected JSON object');
}

return parsed as Record<string, string>;
}

export function commaSeparatedList(value: string | undefined): string[] | undefined {
if (!value)
return undefined;
Expand Down
3 changes: 2 additions & 1 deletion src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { program, Option } from 'commander';
import * as mcpServer from './mcp/server.js';
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from './config.js';
import { commaSeparatedList, parseJsonObject, resolveCLIConfig, semicolonSeparatedList } from './config.js';
import { packageJSON } from './utils/package.js';
import { Context } from './context.js';
import { contextFactory } from './browserContextFactory.js';
Expand All @@ -37,6 +37,7 @@ program
.option('--browser <browser>', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
.option('--caps <caps>', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList)
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
.option('--cdp-headers <headers>', 'JSON string of headers to send with CDP connection, e.g. \'{"Authorization": "Bearer token"}\'', parseJsonObject)
.option('--config <path>', 'path to the configuration file.')
.option('--device <device>', 'device to emulate, for example: "iPhone 15"')
.option('--executable-path <path>', 'path to the browser executable.')
Expand Down
25 changes: 25 additions & 0 deletions tests/cdp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,31 @@ test('should throw connection error and allow re-connecting', async ({ cdpServer
});
});

test('cdp server with headers', async ({ cdpServer, startClient, server }) => {
await cdpServer.start();
const headers = { 'X-Test-Header': 'test-value' };
const { client } = await startClient({
args: [`--cdp-endpoint=${cdpServer.endpoint}`, `--cdp-headers=${JSON.stringify(headers)}`]
});
expect(await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
})).toHaveResponse({
pageState: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
});
});

test('cdp server with invalid headers JSON', async () => {
const result = spawnSync('node', [
path.join(__filename, '../../cli.js'),
'--cdp-endpoint=http://localhost:1234',
'--cdp-headers=invalid-json'
]);
expect(result.error).toBeUndefined();
expect(result.status).toBe(1);
expect(result.stderr.toString()).toContain('option \'--cdp-headers <headers>\' argument \'invalid-json\' is invalid');
});

// NOTE: Can be removed when we drop Node.js 18 support and changed to import.meta.filename.
const __filename = url.fileURLToPath(import.meta.url);

Expand Down