Skip to content

Commit 899c7a5

Browse files
feat(mcp): accept ConnectOptions object for remoteEndpoint (#40964)
Co-authored-by: Simon Knott <info@simonknott.de>
1 parent 1aa42fa commit 899c7a5

6 files changed

Lines changed: 28 additions & 64 deletions

File tree

packages/playwright-core/src/tools/mcp/browserFactory.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,16 @@ async function createCDPBrowser(config: FullConfig, clientInfo: ClientInfo): Pro
115115

116116
async function createRemoteBrowser(config: FullConfig): Promise<BrowserWithInfo> {
117117
testDebug('create browser (remote)');
118-
const descriptor = await serverRegistry.find(config.browser.remoteEndpoint!);
118+
// `remoteEndpoint` may be a plain URL string or a ConnectOptions object that
119+
// carries additional fields such as `exposeNetwork`, `headers`, `slowMo`, and
120+
// `timeout`. Normalize once so the rest of the function deals with a single
121+
// shape.
122+
const remote = config.browser.remoteEndpoint!;
123+
const remoteOptions = typeof remote === 'string'
124+
? { endpoint: remote }
125+
: remote;
126+
127+
const descriptor = await serverRegistry.find(remoteOptions.endpoint);
119128
if (descriptor) {
120129
const browser = await connectToBrowserAcrossVersions(descriptor);
121130
return {
@@ -131,13 +140,9 @@ async function createRemoteBrowser(config: FullConfig): Promise<BrowserWithInfo>
131140
};
132141
}
133142

134-
const endpoint = config.browser.remoteEndpoint!;
135143
const playwrightObject = playwright as Playwright;
136144
// Use connectToBrowser instead of playwright[browserName].connect because we don't have browserName.
137-
const browser = await connectToBrowser(playwrightObject, {
138-
endpoint,
139-
headers: config.browser.remoteHeaders,
140-
});
145+
const browser = await connectToBrowser(playwrightObject, remoteOptions);
141146
browser._connectToBrowserType(playwrightObject[browser._browserName], {}, undefined);
142147
return { browser, browserInfo: browserInfo(browser, config), canBind: false, ownership: 'attached' };
143148
}

packages/playwright-core/src/tools/mcp/config.d.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,13 @@ export type Config = {
8282
cdpTimeout?: number;
8383

8484
/**
85-
* Remote endpoint to connect to an existing Playwright server.
85+
* Remote endpoint to connect to an existing Playwright server. May be a
86+
* WebSocket URL string, or a [ConnectOptions] object that mirrors the
87+
* `connectOptions` shape used by the test runner. When passed as an object,
88+
* `exposeNetwork`, `headers`, `slowMo`, and `timeout` are forwarded to the
89+
* underlying connect call.
8690
*/
87-
remoteEndpoint?: string;
88-
89-
/**
90-
* Headers to send with the remote endpoint connect request.
91-
*/
92-
remoteHeaders?: Record<string, string>;
91+
remoteEndpoint?: string | playwright.ConnectOptions & { endpoint: string };
9392

9493
/**
9594
* Paths to TypeScript files to add as initialization scripts for Playwright page.

packages/playwright-core/src/tools/mcp/config.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export type CLIOptions = {
6464
port?: number;
6565
proxyBypass?: string;
6666
proxyServer?: string;
67-
remoteHeader?: Record<string, string>;
6867
saveSession?: boolean;
6968
secrets?: Record<string, string>;
7069
sharedBrowserContext?: boolean;
@@ -334,7 +333,6 @@ function configFromCLIOptions(cliOptions: CLIOptions): Config & { configFile?: s
334333
initPage: cliOptions.initPage,
335334
initScript: cliOptions.initScript,
336335
remoteEndpoint: cliOptions.endpoint,
337-
remoteHeaders: cliOptions.remoteHeader,
338336
},
339337
extension: cliOptions.extension,
340338
server: {
@@ -407,7 +405,6 @@ export function configFromEnv(env?: NodeJS.ProcessEnv): Config & { configFile?:
407405
options.port = numberParser(e.PLAYWRIGHT_MCP_PORT);
408406
options.proxyBypass = envToString(e.PLAYWRIGHT_MCP_PROXY_BYPASS);
409407
options.proxyServer = envToString(e.PLAYWRIGHT_MCP_PROXY_SERVER);
410-
options.remoteHeader = headerParser(envToString(e.PLAYWRIGHT_MCP_REMOTE_HEADERS));
411408
options.secrets = dotenvFileLoader(e.PLAYWRIGHT_MCP_SECRETS_FILE);
412409
options.storageState = envToString(e.PLAYWRIGHT_MCP_STORAGE_STATE);
413410
options.testIdAttribute = envToString(e.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE);

packages/playwright-core/src/tools/mcp/program.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export function decorateMCPCommand(command: Command) {
6464
.option('--port <port>', 'port to listen on for SSE transport.')
6565
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
6666
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
67-
.option('--remote-header <headers...>', 'headers to send with the remote endpoint connect request, multiple can be specified.', headerParser)
6867
.option('--sandbox', 'enable the sandbox for all process types that are normally not sandboxed.')
6968
.option('--save-session', 'Whether to save the Playwright MCP session into the output directory.')
7069
.option('--secrets <path>', 'path to a file containing secrets in the dotenv format', dotenvFileLoader)

tests/mcp/cli-remote.spec.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.

tests/mcp/remote-endpoint.spec.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,42 +18,44 @@ import { test, expect } from './fixtures';
1818

1919
test.skip(({ mcpBrowser }) => mcpBrowser !== 'chromium', 'Run only on the chromium project; the remote server connection is browser-agnostic.');
2020

21-
test('remoteHeaders selects the browser on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => {
21+
test('connect without headers fails on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => {
2222
const { client } = await startClient({
2323
config: {
2424
browser: {
2525
remoteEndpoint: runServerEndpoint,
26-
remoteHeaders: { 'x-playwright-browser': 'chromium' },
2726
isolated: true,
2827
},
2928
},
3029
});
3130

3231
const response = await client.callTool({
3332
name: 'browser_navigate',
34-
arguments: { url: server.HELLO_WORLD },
33+
arguments: { url: server.EMPTY_PAGE },
3534
});
3635
expect(response).toHaveResponse({
37-
page: expect.stringContaining('Page Title: Title'),
36+
isError: true,
37+
error: expect.stringContaining(`reading 'launch'`),
3838
});
3939
});
4040

41-
test('connect without remoteHeaders fails on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => {
41+
test('remoteEndpoint accepts ConnectOptions object with headers', async ({ startClient, server, runServerEndpoint }) => {
4242
const { client } = await startClient({
4343
config: {
4444
browser: {
45-
remoteEndpoint: runServerEndpoint,
45+
remoteEndpoint: {
46+
endpoint: runServerEndpoint,
47+
headers: { 'x-playwright-browser': 'chromium' },
48+
},
4649
isolated: true,
4750
},
4851
},
4952
});
5053

5154
const response = await client.callTool({
5255
name: 'browser_navigate',
53-
arguments: { url: server.EMPTY_PAGE },
56+
arguments: { url: server.HELLO_WORLD },
5457
});
5558
expect(response).toHaveResponse({
56-
isError: true,
57-
error: expect.stringContaining(`reading 'launch'`),
59+
page: expect.stringContaining('Page Title: Title'),
5860
});
5961
});

0 commit comments

Comments
 (0)