chore(extension): support custom executablePath (#947)

Fixes https://github.com/microsoft/playwright-mcp/issues/941
This commit is contained in:
Yury Semikhatsky
2025-08-25 13:48:52 -07:00
committed by GitHub
parent 1a64a51812
commit 7774ad93ca
4 changed files with 47 additions and 10 deletions

View File

@@ -276,3 +276,32 @@ for (const [mode, startClientMethod] of [
}); });
} }
test(`custom executablePath`, async ({ startClient, server, useShortConnectionTimeout }) => {
useShortConnectionTimeout(1000);
const executablePath = test.info().outputPath('echo.sh');
await fs.promises.writeFile(executablePath, '#!/bin/bash\necho "Custom exec args: $@" > "$(dirname "$0")/output.txt"', { mode: 0o755 });
const { client } = await startClient({
args: [`--extension`],
config: {
browser: {
launchOptions: {
executablePath,
},
}
},
});
const navigateResponse = await client.callTool({
name: 'browser_navigate',
arguments: { url: server.HELLO_WORLD },
timeout: 1000,
});
expect(await navigateResponse).toHaveResponse({
result: expect.stringContaining('Extension connection timeout.'),
isError: true,
});
expect(await fs.promises.readFile(test.info().outputPath('output.txt'), 'utf8')).toContain('Custom exec args: chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html?');
});

View File

@@ -60,6 +60,7 @@ export class CDPRelayServer {
private _wsHost: string; private _wsHost: string;
private _browserChannel: string; private _browserChannel: string;
private _userDataDir?: string; private _userDataDir?: string;
private _executablePath?: string;
private _cdpPath: string; private _cdpPath: string;
private _extensionPath: string; private _extensionPath: string;
private _wss: WebSocketServer; private _wss: WebSocketServer;
@@ -73,10 +74,11 @@ export class CDPRelayServer {
private _nextSessionId: number = 1; private _nextSessionId: number = 1;
private _extensionConnectionPromise!: ManualPromise<void>; private _extensionConnectionPromise!: ManualPromise<void>;
constructor(server: http.Server, browserChannel: string, userDataDir?: string) { constructor(server: http.Server, browserChannel: string, userDataDir?: string, executablePath?: string) {
this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws'); this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws');
this._browserChannel = browserChannel; this._browserChannel = browserChannel;
this._userDataDir = userDataDir; this._userDataDir = userDataDir;
this._executablePath = executablePath;
const uuid = crypto.randomUUID(); const uuid = crypto.randomUUID();
this._cdpPath = `/cdp/${uuid}`; this._cdpPath = `/cdp/${uuid}`;
@@ -125,12 +127,16 @@ export class CDPRelayServer {
if (toolName) if (toolName)
url.searchParams.set('newTab', String(toolName === 'browser_navigate')); url.searchParams.set('newTab', String(toolName === 'browser_navigate'));
const href = url.toString(); const href = url.toString();
const executableInfo = registry.findExecutable(this._browserChannel);
if (!executableInfo) let executablePath = this._executablePath;
throw new Error(`Unsupported channel: "${this._browserChannel}"`); if (!executablePath) {
const executablePath = executableInfo.executablePath(); const executableInfo = registry.findExecutable(this._browserChannel);
if (!executablePath) if (!executableInfo)
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`); throw new Error(`Unsupported channel: "${this._browserChannel}"`);
executablePath = executableInfo.executablePath();
if (!executablePath)
throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`);
}
const args: string[] = []; const args: string[] = [];
if (this._userDataDir) if (this._userDataDir)

View File

@@ -26,10 +26,12 @@ const debugLogger = debug('pw:mcp:relay');
export class ExtensionContextFactory implements BrowserContextFactory { export class ExtensionContextFactory implements BrowserContextFactory {
private _browserChannel: string; private _browserChannel: string;
private _userDataDir?: string; private _userDataDir?: string;
private _executablePath?: string;
constructor(browserChannel: string, userDataDir: string | undefined) { constructor(browserChannel: string, userDataDir: string | undefined, executablePath: string | undefined) {
this._browserChannel = browserChannel; this._browserChannel = browserChannel;
this._userDataDir = userDataDir; this._userDataDir = userDataDir;
this._executablePath = executablePath;
} }
async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> { async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> {
@@ -55,7 +57,7 @@ export class ExtensionContextFactory implements BrowserContextFactory {
httpServer.close(); httpServer.close();
throw new Error(abortSignal.reason); throw new Error(abortSignal.reason);
} }
const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir); const cdpRelayServer = new CDPRelayServer(httpServer, this._browserChannel, this._userDataDir, this._executablePath);
abortSignal.addEventListener('abort', () => cdpRelayServer.stop()); abortSignal.addEventListener('abort', () => cdpRelayServer.stop());
debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`); debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`);
return cdpRelayServer; return cdpRelayServer;

View File

@@ -72,7 +72,7 @@ program
const config = await resolveCLIConfig(options); const config = await resolveCLIConfig(options);
const browserContextFactory = contextFactory(config); const browserContextFactory = contextFactory(config);
const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir); const extensionContextFactory = new ExtensionContextFactory(config.browser.launchOptions.channel || 'chrome', config.browser.userDataDir, config.browser.launchOptions.executablePath);
if (options.extension) { if (options.extension) {
const serverBackendFactory: mcpServer.ServerBackendFactory = { const serverBackendFactory: mcpServer.ServerBackendFactory = {