From 17b02fc506f250ed17437f4da3b4b085716d1a1c Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Fri, 22 May 2026 05:56:06 -0700 Subject: [PATCH] feat(mcp): accept ConnectOptions object for remoteEndpoint The CLI / MCP config exposed `remoteEndpoint` as a plain URL string, so there was no way to forward `exposeNetwork`, `headers`, `slowMo`, or `timeout` when connecting to a remote browser. The test runner already accepts a `connectOptions` object via `playwright.config.ts`; this brings the same surface to the MCP config. `remoteEndpoint` now accepts `string | playwright.ConnectOptions & { endpoint: string }`. The string form keeps the existing behavior. The object form is normalized inside `createRemoteBrowser` and forwarded to `connectToBrowser`, so the underlying connect call receives the full set of options including `exposeNetwork: ''` for SOCKS tunneling back to the client. Fixes: https://github.com/microsoft/playwright/issues/40478 --- .../src/tools/mcp/browserFactory.ts | 17 +++++++++----- .../playwright-core/src/tools/mcp/config.d.ts | 12 +++++++--- tests/mcp/remote-endpoint.spec.ts | 22 +++++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/playwright-core/src/tools/mcp/browserFactory.ts b/packages/playwright-core/src/tools/mcp/browserFactory.ts index 2f33851a7c566..ffb04c170885d 100644 --- a/packages/playwright-core/src/tools/mcp/browserFactory.ts +++ b/packages/playwright-core/src/tools/mcp/browserFactory.ts @@ -115,7 +115,16 @@ async function createCDPBrowser(config: FullConfig, clientInfo: ClientInfo): Pro async function createRemoteBrowser(config: FullConfig): Promise { testDebug('create browser (remote)'); - const descriptor = await serverRegistry.find(config.browser.remoteEndpoint!); + // `remoteEndpoint` may be a plain URL string or a ConnectOptions object that + // carries additional fields such as `exposeNetwork`, `headers`, `slowMo`, and + // `timeout`. Normalize once so the rest of the function deals with a single + // shape. + const remote = config.browser.remoteEndpoint!; + const remoteOptions = typeof remote === 'string' + ? { endpoint: remote, headers: config.browser.remoteHeaders } + : remote; + + const descriptor = await serverRegistry.find(remoteOptions.endpoint); if (descriptor) { const browser = await connectToBrowserAcrossVersions(descriptor); return { @@ -131,13 +140,9 @@ async function createRemoteBrowser(config: FullConfig): Promise }; } - const endpoint = config.browser.remoteEndpoint!; const playwrightObject = playwright as Playwright; // Use connectToBrowser instead of playwright[browserName].connect because we don't have browserName. - const browser = await connectToBrowser(playwrightObject, { - endpoint, - headers: config.browser.remoteHeaders, - }); + const browser = await connectToBrowser(playwrightObject, remoteOptions); browser._connectToBrowserType(playwrightObject[browser._browserName], {}, undefined); return { browser, browserInfo: browserInfo(browser, config), canBind: false, ownership: 'attached' }; } diff --git a/packages/playwright-core/src/tools/mcp/config.d.ts b/packages/playwright-core/src/tools/mcp/config.d.ts index 640084178c026..a2fc50caf5c9e 100644 --- a/packages/playwright-core/src/tools/mcp/config.d.ts +++ b/packages/playwright-core/src/tools/mcp/config.d.ts @@ -82,12 +82,18 @@ export type Config = { cdpTimeout?: number; /** - * Remote endpoint to connect to an existing Playwright server. + * Remote endpoint to connect to an existing Playwright server. May be a + * WebSocket URL string, or a [ConnectOptions] object that mirrors the + * `connectOptions` shape used by the test runner. When passed as an object, + * `exposeNetwork`, `headers`, `slowMo`, and `timeout` are forwarded to the + * underlying connect call. */ - remoteEndpoint?: string; + remoteEndpoint?: string | playwright.ConnectOptions & { endpoint: string }; /** - * Headers to send with the remote endpoint connect request. + * Headers to send with the remote endpoint connect request. Ignored when + * `remoteEndpoint` is provided as a [ConnectOptions] object; supply + * `headers` on that object instead. */ remoteHeaders?: Record; diff --git a/tests/mcp/remote-endpoint.spec.ts b/tests/mcp/remote-endpoint.spec.ts index 6aba3e9df20a2..ebd77395d7e39 100644 --- a/tests/mcp/remote-endpoint.spec.ts +++ b/tests/mcp/remote-endpoint.spec.ts @@ -57,3 +57,25 @@ test('connect without remoteHeaders fails on run-server endpoint', async ({ star error: expect.stringContaining(`reading 'launch'`), }); }); + +test('remoteEndpoint accepts ConnectOptions object with headers', async ({ startClient, server, runServerEndpoint }) => { + const { client } = await startClient({ + config: { + browser: { + remoteEndpoint: { + endpoint: runServerEndpoint, + headers: { 'x-playwright-browser': 'chromium' }, + }, + isolated: true, + }, + }, + }); + + const response = await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + }); + expect(response).toHaveResponse({ + page: expect.stringContaining('Page Title: Title'), + }); +});