Skip to content

Commit eed086f

Browse files
committed
fix(mcp): resolve extension channel/executablePath from CLI and env
Restores --executable-path support for extension mode, which was dropped in 5ec1b3f. Both channel and executablePath are now resolved from CLI options and PLAYWRIGHT_MCP_* env vars, bypassing the merged config so attaching to a Windows chrome.exe from WSL works again. Fixes: microsoft/playwright-mcp#1590
1 parent 0c04e83 commit eed086f

5 files changed

Lines changed: 46 additions & 15 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { outputDir } from '../backend/context';
2525
import { createExtensionBrowser } from './extensionContextFactory';
2626
import { connectToBrowserAcrossVersions } from '../utils/connect';
2727
import { serverRegistry } from '../../serverRegistry';
28-
import { resolveChannelForExtension } from './config';
28+
import { resolveExtensionOptions } from './config';
2929
// eslint-disable-next-line no-restricted-imports
3030
import { connectToBrowser } from '../../client/connect';
3131

@@ -59,8 +59,8 @@ export async function createBrowserWithInfo(config: FullConfig, clientInfo: Clie
5959
canBind = true;
6060
ownership = 'own';
6161
} else if (config.extension) {
62-
const channel = resolveChannelForExtension(cliOptions);
63-
browser = await createExtensionBrowser(channel, clientInfo.clientName);
62+
const { channel, executablePath } = resolveExtensionOptions(cliOptions);
63+
browser = await createExtensionBrowser(channel, executablePath, clientInfo.clientName);
6464
ownership = 'attached';
6565
} else {
6666
browser = await createPersistentBrowser(config, clientInfo);

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ type CDPResponse = CDPMessage;
6262
export class CDPRelayServer {
6363
private _wsHost: string;
6464
private _browserChannel: string;
65-
private _userDataDir?: string;
6665
private _executablePath?: string;
6766
private _cdpPath: string;
6867
private _extensionPath: string;
@@ -73,10 +72,9 @@ export class CDPRelayServer {
7372
private _handler: ExtensionProtocolHandler;
7473
private _extensionConnectionPromise = new ManualPromise<void>();
7574

76-
constructor(server: http.Server, browserChannel: string, userDataDir?: string, executablePath?: string) {
75+
constructor(server: http.Server, browserChannel: string, executablePath?: string) {
7776
this._wsHost = addressToString(server.address(), { protocol: 'ws' });
7877
this._browserChannel = browserChannel;
79-
this._userDataDir = userDataDir;
8078
this._executablePath = executablePath;
8179
this._protocolVersion = parseInt(process.env.PLAYWRIGHT_EXTENSION_PROTOCOL ?? protocol.DEFAULT_VERSION.toString(), 10);
8280

@@ -145,8 +143,9 @@ export class CDPRelayServer {
145143
}
146144

147145
const args: string[] = [];
148-
if (this._userDataDir)
149-
args.push(`--user-data-dir=${this._userDataDir}`);
146+
const userDataDir = process.env.PWTEST_EXTENSION_USER_DATA_DIR;
147+
if (userDataDir)
148+
args.push(`--user-data-dir=${userDataDir}`);
150149
if (os.platform() === 'linux' && channel === 'chromium')
151150
args.push('--no-sandbox');
152151
args.push(href);

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,11 @@ export async function resolveCLIConfigForCLI(daemonProfilesDir: string, sessionN
194194
return { ...result, browser, configFile, skillMode: true };
195195
}
196196

197-
export function resolveChannelForExtension(cliOptions: CLIOptions): string {
197+
export function resolveExtensionOptions(cliOptions: CLIOptions): { channel: string, executablePath: string | undefined } {
198198
const browser = cliOptions.browser ?? envToString(process.env.PLAYWRIGHT_MCP_BROWSER);
199199
const { channel } = resolveBrowserParam(browser);
200-
return channel ?? 'chrome';
200+
const executablePath = cliOptions.executablePath ?? envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
201+
return { channel: channel ?? 'chrome', executablePath };
201202
}
202203

203204
async function validateBrowserConfig(browser: MergedConfig['browser']): Promise<FullConfig['browser']> {

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,19 @@ import type * as playwrightTypes from '../../..';
2525

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

28-
export async function createExtensionBrowser(channel: string, clientName: string): Promise<playwrightTypes.Browser> {
29-
const userDataDir = process.env.PWTEST_EXTENSION_USER_DATA_DIR ?? defaultUserDataDirForChannel(channel);
30-
if (userDataDir && !await isPlaywrightExtensionInstalled(userDataDir))
31-
throw new Error(`Playwright Extension not found in "${userDataDir}". Install it from ${playwrightExtensionInstallUrl}`);
28+
export async function createExtensionBrowser(channel: string, executablePath: string | undefined, clientName: string): Promise<playwrightTypes.Browser> {
29+
// Skip the local profile check when a custom executablePath is provided: the browser may run
30+
// in a different filesystem (e.g. Windows chrome.exe launched from WSL2) where the Linux-side
31+
// default user data dir does not correspond to the actual profile.
32+
if (!executablePath) {
33+
const userDataDir = process.env.PWTEST_EXTENSION_USER_DATA_DIR ?? defaultUserDataDirForChannel(channel);
34+
if (userDataDir && !await isPlaywrightExtensionInstalled(userDataDir))
35+
throw new Error(`Playwright Extension not found in "${userDataDir}". Install it from ${playwrightExtensionInstallUrl}`);
36+
}
3237

3338
const httpServer = createHttpServer();
3439
await startHttpServer(httpServer, {});
35-
const relay = new CDPRelayServer(httpServer, channel, userDataDir);
40+
const relay = new CDPRelayServer(httpServer, channel, executablePath);
3641
debugLogger(`CDP relay server started, extension endpoint: ${relay.extensionEndpoint()}.`);
3742

3843
try {

tests/extension/extension.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import fs from 'fs/promises';
18+
1719
import { test, testWithOldExtensionVersion, expect, extensionId, clickAllowAndSelect, startWithExtensionFlag } from './extension-fixtures';
1820
import { utils } from '../../packages/playwright-core/lib/coreBundle';
1921

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

236+
test(`custom executablePath skips local extension check`, {
237+
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/1590' },
238+
}, async ({ startClient, server }) => {
239+
const executablePath = test.info().outputPath('echo.sh');
240+
await fs.writeFile(executablePath, '#!/bin/bash\necho "Custom exec args: $@" > "$(dirname "$0")/output.txt"', { mode: 0o755 });
241+
242+
// Point user data dir at an empty profile to prove the local extension check is
243+
// skipped when executablePath is set (the browser may live in a different
244+
// filesystem, e.g. Windows chrome.exe launched from WSL2).
245+
const { client } = await startClient({
246+
args: [`--extension`, `--executable-path=${executablePath}`],
247+
env: { PWTEST_EXTENSION_USER_DATA_DIR: test.info().outputPath('empty-profile') },
248+
});
249+
250+
client.callTool({
251+
name: 'browser_navigate',
252+
arguments: { url: server.HELLO_WORLD },
253+
}).catch(() => {});
254+
await expect(async () => {
255+
const output = await fs.readFile(test.info().outputPath('output.txt'), 'utf8');
256+
expect(output).toMatch(new RegExp(`Custom exec args.*chrome-extension://${extensionId}/connect\\.html\\?`));
257+
}).toPass();
258+
});
259+
234260
test(`fails when extension is missing in custom userDataDir`, async ({ startClient, server }) => {
235261
const userDataDir = test.info().outputPath('empty-profile');
236262

0 commit comments

Comments
 (0)