Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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: any = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure types are in order here.

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
22 changes: 22 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,24 @@ 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;
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this code could be simplified a lot, did you mean to parse JSON first and then assert the type of the parsed value?

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

return parsed;
} catch (error) {
if (error instanceof InvalidArgumentError)
throw error;

const errorMessage = error instanceof Error ? error.message : String(error);
throw new InvalidArgumentError(`Invalid JSON format: ${errorMessage}`);
}
}

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