diff --git a/packages/playwright-core/src/tools/mcp/browserFactory.ts b/packages/playwright-core/src/tools/mcp/browserFactory.ts index 70cf0878e55d7..c98b6cc9e516a 100644 --- a/packages/playwright-core/src/tools/mcp/browserFactory.ts +++ b/packages/playwright-core/src/tools/mcp/browserFactory.ts @@ -109,6 +109,7 @@ async function createCDPBrowser(config: FullConfig, clientInfo: ClientInfo): Pro headers: config.browser.cdpHeaders, timeout: config.browser.cdpTimeout, artifactsDir, + noDefaults: config.browser.cdpNoDefaults, }); return browser; } diff --git a/packages/playwright-core/src/tools/mcp/config.d.ts b/packages/playwright-core/src/tools/mcp/config.d.ts index bd9835c2e236b..f9aa7d726d849 100644 --- a/packages/playwright-core/src/tools/mcp/config.d.ts +++ b/packages/playwright-core/src/tools/mcp/config.d.ts @@ -81,6 +81,18 @@ export type Config = { */ cdpTimeout?: number; + /** + * When true, forwarded as `noDefaults` to + * @see https://playwright.dev/docs/api/class-browsertype#browser-type-connect-over-cdp + * Skips Playwright's default overrides (download behavior, focus emulation, media emulation) + * on the pre-existing default browser context. Useful when connecting to CDP endpoints that + * don't implement Chromium's full browser-context-management surface (e.g. some non-Chromium + * remote debugging targets). Note this only affects the default context; it has no effect + * when combined with `isolated`, since that creates a new context via `browser.newContext()`. + * Defaults to false. + */ + cdpNoDefaults?: boolean; + /** * Remote endpoint to connect to an existing Playwright server. May be a * WebSocket URL string, or a [ConnectOptions] object that mirrors the diff --git a/packages/playwright-core/src/tools/mcp/config.ts b/packages/playwright-core/src/tools/mcp/config.ts index 47c8c40335f5e..9a48ebda58d7f 100644 --- a/packages/playwright-core/src/tools/mcp/config.ts +++ b/packages/playwright-core/src/tools/mcp/config.ts @@ -42,6 +42,7 @@ export type CLIOptions = { caps?: string[]; cdpEndpoint?: string; cdpHeader?: Record; + cdpNoDefaults?: boolean; cdpTimeout?: number; codegen?: 'typescript' | 'none'; config?: string; @@ -346,6 +347,7 @@ function configFromCLIOptions(cliOptions: CLIOptions): Config & { configFile?: s contextOptions, cdpEndpoint: cliOptions.cdpEndpoint, cdpHeaders: cliOptions.cdpHeader, + cdpNoDefaults: cliOptions.cdpNoDefaults, cdpTimeout: cliOptions.cdpTimeout, initPage: cliOptions.initPage, initScript: cliOptions.initScript, @@ -401,6 +403,7 @@ export function configFromEnv(env?: NodeJS.ProcessEnv): Config & { configFile?: options.caps = commaSeparatedList(e.PLAYWRIGHT_MCP_CAPS); options.cdpEndpoint = envToString(e.PLAYWRIGHT_MCP_CDP_ENDPOINT); options.cdpHeader = headerParser(envToString(e.PLAYWRIGHT_MCP_CDP_HEADERS)); + options.cdpNoDefaults = envToBoolean(e.PLAYWRIGHT_MCP_CDP_NO_DEFAULTS); options.cdpTimeout = numberParser(e.PLAYWRIGHT_MCP_CDP_TIMEOUT); options.config = envToString(e.PLAYWRIGHT_MCP_CONFIG); if (e.PLAYWRIGHT_MCP_CONSOLE_LEVEL) diff --git a/packages/playwright-core/src/tools/mcp/configIni.ts b/packages/playwright-core/src/tools/mcp/configIni.ts index 5b66a113b075d..bfbe5bbc123f0 100644 --- a/packages/playwright-core/src/tools/mcp/configIni.ts +++ b/packages/playwright-core/src/tools/mcp/configIni.ts @@ -106,6 +106,7 @@ const longhandTypes: Record = { 'browser.isolated': 'boolean', 'browser.userDataDir': 'string', 'browser.cdpEndpoint': 'string', + 'browser.cdpNoDefaults': 'boolean', 'browser.cdpTimeout': 'number', 'browser.remoteEndpoint': 'string', 'browser.initPage': 'string[]', diff --git a/packages/playwright-core/src/tools/mcp/program.ts b/packages/playwright-core/src/tools/mcp/program.ts index 8501160c1a3a5..efd7e56b4bfeb 100644 --- a/packages/playwright-core/src/tools/mcp/program.ts +++ b/packages/playwright-core/src/tools/mcp/program.ts @@ -41,6 +41,7 @@ export function decorateMCPCommand(command: Command) { .option('--caps ', 'comma-separated list of additional capabilities to enable, possible values: vision, pdf, devtools.', commaSeparatedList) .option('--cdp-endpoint ', 'CDP endpoint to connect to.') .option('--cdp-header ', 'CDP headers to send with the connect request, multiple can be specified.', headerParser) + .option('--cdp-no-defaults', 'skip Playwright\'s default overrides (download behavior, focus/media emulation) on the existing default context when connecting via --cdp-endpoint. Useful for CDP targets that don\'t implement Chromium\'s full browser-context-management surface.') .option('--cdp-timeout ', 'timeout in milliseconds for connecting to CDP endpoint, defaults to 30000ms', numberParser) .option('--codegen ', 'specify the language to use for code generation, possible values: "typescript", "none". Default is "typescript".', enumParser.bind(null, '--codegen', ['none', 'typescript'])) .option('--config ', 'path to the configuration file.') diff --git a/tests/mcp/cdp.spec.ts b/tests/mcp/cdp.spec.ts index a316baff86286..8563c0f5b1aee 100644 --- a/tests/mcp/cdp.spec.ts +++ b/tests/mcp/cdp.spec.ts @@ -15,7 +15,7 @@ */ import { spawnSync } from 'child_process'; -import { test, expect, mcpServerPath } from './fixtures'; +import { test, expect, mcpServerPath, parseResponse } from './fixtures'; test.describe.configure({ retries: 1, @@ -168,3 +168,39 @@ test('cdp server with empty and complex headers', async ({ startClient, server } expect(customHeader).toBe('value:with:colons'); expect(emptyHeader).toBe(''); }); + +test('cdp server with --cdp-no-defaults skips download override', async ({ cdpServer, startClient, server }, testInfo) => { + await cdpServer.start(); + const { client } = await startClient({ + args: [`--cdp-endpoint=${cdpServer.endpoint}`, '--cdp-no-defaults'], + config: { outputDir: testInfo.outputPath('output') }, + }); + + server.setContent('/', `Download`, 'text/html'); + server.setContent('/download', 'Data', 'text/plain'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + snapshot: expect.stringContaining(`- link "Download" [ref=e2]`), + }); + + const response = await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Download link', + target: 'e2', + }, + }); + const parsed = parseResponse(response); + + // With --cdp-no-defaults, Playwright never calls Browser.setDownloadBehavior on the + // default context on connect, so downloads are not routed through the page's + // 'download' event and the MCP server has nothing to report. + await new Promise(resolve => setTimeout(resolve, 500)); + const snapshotAfter = parseResponse(await client.callTool({ name: 'browser_snapshot' })); + const events = [parsed.events, snapshotAfter.events].filter(Boolean).join('\n'); + expect(events).not.toContain('Downloading file test.txt'); + expect(events).not.toContain('Downloaded file test.txt'); +});