Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 4 additions & 1 deletion packages/playwright-core/src/tools/mcp/browserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ async function createRemoteBrowser(config: FullConfig): Promise<BrowserWithInfo>
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 });
const browser = await connectToBrowser(playwrightObject, {
endpoint,
headers: config.browser.remoteHeaders,
});
browser._connectToBrowserType(playwrightObject[browser._browserName], {}, undefined);
return { browser, browserInfo: browserInfo(browser, config), canBind: false, ownership: 'attached' };
}
Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/tools/mcp/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export type Config = {
*/
remoteEndpoint?: string;

/**
* Headers to send with the remote endpoint connect request.
*/
remoteHeaders?: Record<string, string>;

/**
* Paths to TypeScript files to add as initialization scripts for Playwright page.
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright-core/src/tools/mcp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type CLIOptions = {
port?: number;
proxyBypass?: string;
proxyServer?: string;
remoteHeader?: Record<string, string>;
saveSession?: boolean;
secrets?: Record<string, string>;
sharedBrowserContext?: boolean;
Expand Down Expand Up @@ -332,6 +333,7 @@ function configFromCLIOptions(cliOptions: CLIOptions): Config & { configFile?: s
initPage: cliOptions.initPage,
initScript: cliOptions.initScript,
remoteEndpoint: cliOptions.endpoint,
remoteHeaders: cliOptions.remoteHeader,
},
extension: cliOptions.extension,
server: {
Expand Down Expand Up @@ -402,6 +404,7 @@ export function configFromEnv(env?: NodeJS.ProcessEnv): Config & { configFile?:
options.port = numberParser(e.PLAYWRIGHT_MCP_PORT);
options.proxyBypass = envToString(e.PLAYWRIGHT_MCP_PROXY_BYPASS);
options.proxyServer = envToString(e.PLAYWRIGHT_MCP_PROXY_SERVER);
options.remoteHeader = headerParser(envToString(e.PLAYWRIGHT_MCP_REMOTE_HEADERS));
options.secrets = dotenvFileLoader(e.PLAYWRIGHT_MCP_SECRETS_FILE);
options.storageState = envToString(e.PLAYWRIGHT_MCP_STORAGE_STATE);
options.testIdAttribute = envToString(e.PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE);
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/tools/mcp/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function decorateMCPCommand(command: Command) {
.option('--port <port>', 'port to listen on for SSE transport.')
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
.option('--remote-header <headers...>', 'headers to send with the remote endpoint connect request, multiple can be specified.', headerParser)
.option('--sandbox', 'enable the sandbox for all process types that are normally not sandboxed.')
.option('--save-session', 'Whether to save the Playwright MCP session into the output directory.')
.option('--secrets <path>', 'path to a file containing secrets in the dotenv format', dotenvFileLoader)
Expand Down
38 changes: 38 additions & 0 deletions tests/mcp/cli-remote.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import fs from 'fs';
import { test, expect } from './cli-fixtures';

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

test('attach to run-server endpoint with remoteHeaders from config', async ({ cli, runServerEndpoint, server }, testInfo) => {
const configPath = testInfo.outputPath('config.json');
await fs.promises.writeFile(configPath, JSON.stringify({
browser: {
remoteEndpoint: runServerEndpoint,
remoteHeaders: { 'x-playwright-browser': 'chromium' },
isolated: true,
},
}, null, 2));

const { exitCode } = await cli('attach', runServerEndpoint, '-s=remote', `--config=${configPath}`);
expect(exitCode).toBe(0);

await cli('-s=remote', 'goto', server.HELLO_WORLD);
const { inlineSnapshot } = await cli('-s=remote', 'snapshot');
expect(inlineSnapshot).toContain('Hello, world!');
});
9 changes: 9 additions & 0 deletions tests/mcp/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { TestServer } from '../config/testserver';
import { serverFixtures } from '../config/serverFixtures';
import { tools } from '../../packages/playwright-core/lib/coreBundle';
import { commonFixtures } from '../config/commonFixtures';
import { RunServer } from '../config/remoteServer';
import { inheritAndCleanEnv } from '../config/utils';

import type { CommonFixtures, CommonWorkerFixtures } from '../config/commonFixtures';
Expand Down Expand Up @@ -67,6 +68,7 @@ type TestFixtures = {
client: Client;
startClient: StartClient;
wsEndpoint: string;
runServerEndpoint: string;
cdpServer: CDPServer;
server: TestServer;
httpsServer: TestServer;
Expand Down Expand Up @@ -161,6 +163,13 @@ export const test = serverTest.extend<TestFixtures & TestOptions, WorkerFixtures
await browserServer.close();
},

runServerEndpoint: async ({ childProcess }, use) => {
const runServer = new RunServer();
await runServer.start(childProcess);
await use(runServer.wsEndpoint());
await runServer.close();
},

cdpServer: async ({ mcpBrowser }, use, testInfo) => {
test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser!), 'CDP is not supported for non-Chromium browsers');

Expand Down
59 changes: 59 additions & 0 deletions tests/mcp/remote-endpoint.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { test, expect } from './fixtures';

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

test('remoteHeaders selects the browser on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => {
const { client } = await startClient({
config: {
browser: {
remoteEndpoint: runServerEndpoint,
remoteHeaders: { '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'),
});
});

test('connect without remoteHeaders fails on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => {
const { client } = await startClient({
config: {
browser: {
remoteEndpoint: runServerEndpoint,
isolated: true,
},
},
});

const response = await client.callTool({
name: 'browser_navigate',
arguments: { url: server.EMPTY_PAGE },
});
expect(response).toHaveResponse({
isError: true,
error: expect.stringContaining(`reading 'launch'`),
});
});
Loading