Skip to content

Commit a4ed1be

Browse files
authored
fix(mcp): resolve extension channel/executablePath from CLI and env (#40572)
1 parent 37a5895 commit a4ed1be

5 files changed

Lines changed: 42 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 } {
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: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ 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+
// 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.
30+
if (!executablePath) {
31+
const userDataDir = process.env.PWTEST_EXTENSION_USER_DATA_DIR ?? defaultUserDataDirForChannel(channel);
32+
if (userDataDir && !await isPlaywrightExtensionInstalled(userDataDir))
33+
throw new Error(`Playwright Extension not found in "${userDataDir}". Install it from ${playwrightExtensionInstallUrl}`);
34+
}
3235

3336
const httpServer = createHttpServer();
3437
await startHttpServer(httpServer, {});
35-
const relay = new CDPRelayServer(httpServer, channel, userDataDir);
38+
const relay = new CDPRelayServer(httpServer, channel, executablePath);
3639
debugLogger(`CDP relay server started, extension endpoint: ${relay.extensionEndpoint()}.`);
3740

3841
try {

tests/extension/extension.spec.ts

Lines changed: 24 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,28 @@ 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+
// Empty profile would normally fail the extension-installed check; it is skipped when executablePath is set.
243+
const { client } = await startClient({
244+
args: [`--extension`, `--executable-path=${executablePath}`],
245+
env: { PWTEST_EXTENSION_USER_DATA_DIR: test.info().outputPath('empty-profile') },
246+
});
247+
248+
client.callTool({
249+
name: 'browser_navigate',
250+
arguments: { url: server.HELLO_WORLD },
251+
}).catch(() => {});
252+
await expect(async () => {
253+
const output = await fs.readFile(test.info().outputPath('output.txt'), 'utf8');
254+
expect(output).toMatch(new RegExp(`Custom exec args.*chrome-extension://${extensionId}/connect\\.html\\?`));
255+
}).toPass();
256+
});
257+
234258
test(`fails when extension is missing in custom userDataDir`, async ({ startClient, server }) => {
235259
const userDataDir = test.info().outputPath('empty-profile');
236260

0 commit comments

Comments
 (0)