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
6 changes: 3 additions & 3 deletions packages/playwright-core/src/tools/mcp/browserFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { outputDir } from '../backend/context';
import { createExtensionBrowser } from './extensionContextFactory';
import { connectToBrowserAcrossVersions } from '../utils/connect';
import { serverRegistry } from '../../serverRegistry';
import { resolveChannelForExtension } from './config';
import { resolveExtensionOptions } from './config';
// eslint-disable-next-line no-restricted-imports
import { connectToBrowser } from '../../client/connect';

Expand Down Expand Up @@ -59,8 +59,8 @@ export async function createBrowserWithInfo(config: FullConfig, clientInfo: Clie
canBind = true;
ownership = 'own';
} else if (config.extension) {
const channel = resolveChannelForExtension(cliOptions);
browser = await createExtensionBrowser(channel, clientInfo.clientName);
const { channel, executablePath } = resolveExtensionOptions(cliOptions);
browser = await createExtensionBrowser(channel, executablePath, clientInfo.clientName);
ownership = 'attached';
} else {
browser = await createPersistentBrowser(config, clientInfo);
Expand Down
9 changes: 4 additions & 5 deletions packages/playwright-core/src/tools/mcp/cdpRelay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ type CDPResponse = CDPMessage;
export class CDPRelayServer {
private _wsHost: string;
private _browserChannel: string;
private _userDataDir?: string;
private _executablePath?: string;
private _cdpPath: string;
private _extensionPath: string;
Expand All @@ -73,10 +72,9 @@ export class CDPRelayServer {
private _handler: ExtensionProtocolHandler;
private _extensionConnectionPromise = new ManualPromise<void>();

constructor(server: http.Server, browserChannel: string, userDataDir?: string, executablePath?: string) {
constructor(server: http.Server, browserChannel: string, executablePath?: string) {
this._wsHost = addressToString(server.address(), { protocol: 'ws' });
this._browserChannel = browserChannel;
this._userDataDir = userDataDir;
this._executablePath = executablePath;
this._protocolVersion = parseInt(process.env.PLAYWRIGHT_EXTENSION_PROTOCOL ?? protocol.DEFAULT_VERSION.toString(), 10);

Expand Down Expand Up @@ -145,8 +143,9 @@ export class CDPRelayServer {
}

const args: string[] = [];
if (this._userDataDir)
args.push(`--user-data-dir=${this._userDataDir}`);
const userDataDir = process.env.PWTEST_EXTENSION_USER_DATA_DIR;
if (userDataDir)
args.push(`--user-data-dir=${userDataDir}`);
if (os.platform() === 'linux' && channel === 'chromium')
args.push('--no-sandbox');
args.push(href);
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/tools/mcp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,11 @@ export async function resolveCLIConfigForCLI(daemonProfilesDir: string, sessionN
return { ...result, browser, configFile, skillMode: true };
}

export function resolveChannelForExtension(cliOptions: CLIOptions): string {
export function resolveExtensionOptions(cliOptions: CLIOptions): { channel: string, executablePath?: string } {
const browser = cliOptions.browser ?? envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
const { channel } = resolveBrowserParam(browser);
return channel ?? 'chrome';
const executablePath = cliOptions.executablePath ?? envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
return { channel: channel ?? 'chrome', executablePath };
}

async function validateBrowserConfig(browser: MergedConfig['browser']): Promise<FullConfig['browser']> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ import type * as playwrightTypes from '../../..';

const debugLogger = debug('pw:mcp:relay');

export async function createExtensionBrowser(channel: string, clientName: string): Promise<playwrightTypes.Browser> {
const userDataDir = process.env.PWTEST_EXTENSION_USER_DATA_DIR ?? defaultUserDataDirForChannel(channel);
if (userDataDir && !await isPlaywrightExtensionInstalled(userDataDir))
throw new Error(`Playwright Extension not found in "${userDataDir}". Install it from ${playwrightExtensionInstallUrl}`);
export async function createExtensionBrowser(channel: string, executablePath: string | undefined, clientName: string): Promise<playwrightTypes.Browser> {
// Custom executablePath may target a browser in a different filesystem (e.g. Windows chrome.exe from WSL2), so the local profile path is not meaningful.
if (!executablePath) {
const userDataDir = process.env.PWTEST_EXTENSION_USER_DATA_DIR ?? defaultUserDataDirForChannel(channel);
if (userDataDir && !await isPlaywrightExtensionInstalled(userDataDir))
throw new Error(`Playwright Extension not found in "${userDataDir}". Install it from ${playwrightExtensionInstallUrl}`);
}

const httpServer = createHttpServer();
await startHttpServer(httpServer, {});
const relay = new CDPRelayServer(httpServer, channel, userDataDir);
const relay = new CDPRelayServer(httpServer, channel, executablePath);
debugLogger(`CDP relay server started, extension endpoint: ${relay.extensionEndpoint()}.`);

try {
Expand Down
24 changes: 24 additions & 0 deletions tests/extension/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import fs from 'fs/promises';

import { test, testWithOldExtensionVersion, expect, extensionId, clickAllowAndSelect, startWithExtensionFlag } from './extension-fixtures';
import { utils } from '../../packages/playwright-core/lib/coreBundle';

Expand Down Expand Up @@ -231,6 +233,28 @@ test(`extension needs update`, async ({ startExtensionClient, server }) => {
await expect(confirmationPage.locator('.status-banner')).toContainText(`Playwright client trying to connect requires newer extension version`);
});

test(`custom executablePath skips local extension check`, {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/1590' },
}, async ({ startClient, server }) => {
const executablePath = test.info().outputPath('echo.sh');
await fs.writeFile(executablePath, '#!/bin/bash\necho "Custom exec args: $@" > "$(dirname "$0")/output.txt"', { mode: 0o755 });

// Empty profile would normally fail the extension-installed check; it is skipped when executablePath is set.
const { client } = await startClient({
args: [`--extension`, `--executable-path=${executablePath}`],
env: { PWTEST_EXTENSION_USER_DATA_DIR: test.info().outputPath('empty-profile') },
});

client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
}).catch(() => {});
await expect(async () => {
const output = await fs.readFile(test.info().outputPath('output.txt'), 'utf8');
expect(output).toMatch(new RegExp(`Custom exec args.*chrome-extension://${extensionId}/connect\\.html\\?`));
}).toPass();
});

test(`fails when extension is missing in custom userDataDir`, async ({ startClient, server }) => {
const userDataDir = test.info().outputPath('empty-profile');

Expand Down
Loading