diff --git a/README.md b/README.md index 5f97f269..e4e2017c 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,9 @@ Playwright MCP server supports following arguments. They can be provided in the --caps comma-separated list of additional capabilities to enable, possible values: vision, pdf. --cdp-endpoint CDP endpoint to connect to. + --cdp-headers JSON string of headers to send with CDP + connection, e.g. '{"Authorization": "Bearer + token"}' --config path to the configuration file. --device device to emulate, for example: "iPhone 15" --executable-path path to the browser executable. diff --git a/config.d.ts b/config.d.ts index d63b0616..594603e1 100644 --- a/config.d.ts +++ b/config.d.ts @@ -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; + /** * Remote endpoint to connect to an existing Playwright server. */ diff --git a/src/browserContextFactory.ts b/src/browserContextFactory.ts index 81072e11..0149ce4e 100644 --- a/src/browserContextFactory.ts +++ b/src/browserContextFactory.ts @@ -128,7 +128,11 @@ class CdpContextFactory extends BaseContextFactory { } protected override async _doObtainBrowser(): Promise { - 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 { diff --git a/src/config.ts b/src/config.ts index e5790028..495ed06f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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'; @@ -30,6 +31,7 @@ export type CLIOptions = { browser?: string; caps?: string[]; cdpEndpoint?: string; + cdpHeaders?: Record; config?: string; device?: string; executablePath?: string; @@ -178,6 +180,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config { launchOptions, contextOptions, cdpEndpoint: cliOptions.cdpEndpoint, + cdpHeaders: cliOptions.cdpHeaders, }, server: { port: cliOptions.port, @@ -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); @@ -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 | 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; +} + export function commaSeparatedList(value: string | undefined): string[] | undefined { if (!value) return undefined; diff --git a/src/program.ts b/src/program.ts index 4a1cda9a..475ec7c7 100644 --- a/src/program.ts +++ b/src/program.ts @@ -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'; @@ -37,6 +37,7 @@ program .option('--browser ', 'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.') .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf.', commaSeparatedList) .option('--cdp-endpoint ', 'CDP endpoint to connect to.') + .option('--cdp-headers ', 'JSON string of headers to send with CDP connection, e.g. \'{"Authorization": "Bearer token"}\'', parseJsonObject) .option('--config ', 'path to the configuration file.') .option('--device ', 'device to emulate, for example: "iPhone 15"') .option('--executable-path ', 'path to the browser executable.') diff --git a/tests/cdp.spec.ts b/tests/cdp.spec.ts index 4ab95714..1d435996 100644 --- a/tests/cdp.spec.ts +++ b/tests/cdp.spec.ts @@ -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 \' 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);