Skip to content
Open
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
17 changes: 11 additions & 6 deletions packages/playwright-core/src/tools/mcp/browserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,16 @@ async function createCDPBrowser(config: FullConfig, clientInfo: ClientInfo): Pro

async function createRemoteBrowser(config: FullConfig): Promise<BrowserWithInfo> {
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 }
: remote;

const descriptor = await serverRegistry.find(remoteOptions.endpoint);
if (descriptor) {
const browser = await connectToBrowserAcrossVersions(descriptor);
return {
Expand All @@ -131,13 +140,9 @@ 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,
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' };
}
Expand Down
13 changes: 6 additions & 7 deletions packages/playwright-core/src/tools/mcp/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,13 @@ 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;

/**
* Headers to send with the remote endpoint connect request.
*/
remoteHeaders?: Record<string, string>;
remoteEndpoint?: string | playwright.ConnectOptions & { endpoint: string };

/**
* Paths to TypeScript files to add as initialization scripts for Playwright page.
Expand Down
3 changes: 0 additions & 3 deletions packages/playwright-core/src/tools/mcp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ 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 @@ -333,7 +332,6 @@ 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 @@ -404,7 +402,6 @@ 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: 0 additions & 1 deletion packages/playwright-core/src/tools/mcp/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ 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
8 changes: 5 additions & 3 deletions tests/mcp/cli-remote.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ 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) => {
test('attach to run-server endpoint with headers 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' },
remoteEndpoint: {
endpoint: runServerEndpoint,
headers: { 'x-playwright-browser': 'chromium' },
},
isolated: true,
},
}, null, 2));
Expand Down
20 changes: 11 additions & 9 deletions tests/mcp/remote-endpoint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,44 @@ 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 }) => {
test('connect without headers fails 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 },
arguments: { url: server.EMPTY_PAGE },
});
expect(response).toHaveResponse({
page: expect.stringContaining('Page Title: Title'),
isError: true,
error: expect.stringContaining(`reading 'launch'`),
});
});

test('connect without remoteHeaders fails on run-server endpoint', async ({ startClient, server, runServerEndpoint }) => {
test('remoteEndpoint accepts ConnectOptions object with headers', async ({ startClient, server, runServerEndpoint }) => {
const { client } = await startClient({
config: {
browser: {
remoteEndpoint: runServerEndpoint,
remoteEndpoint: {
endpoint: runServerEndpoint,
headers: { 'x-playwright-browser': 'chromium' },
},
isolated: true,
},
},
});

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