diff --git a/extension/tests/extension.spec.ts b/extension/tests/extension.spec.ts index 07ddf6d..60cc4fb 100644 --- a/extension/tests/extension.spec.ts +++ b/extension/tests/extension.spec.ts @@ -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?'); +}); diff --git a/src/extension/cdpRelay.ts b/src/extension/cdpRelay.ts index dcd1980..79e8290 100644 --- a/src/extension/cdpRelay.ts +++ b/src/extension/cdpRelay.ts @@ -60,6 +60,7 @@ export class CDPRelayServer { private _wsHost: string; private _browserChannel: string; private _userDataDir?: string; + private _executablePath?: string; private _cdpPath: string; private _extensionPath: string; private _wss: WebSocketServer; @@ -73,10 +74,11 @@ export class CDPRelayServer { private _nextSessionId: number = 1; private _extensionConnectionPromise!: ManualPromise; - 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._browserChannel = browserChannel; this._userDataDir = userDataDir; + this._executablePath = executablePath; const uuid = crypto.randomUUID(); this._cdpPath = `/cdp/${uuid}`; @@ -125,12 +127,16 @@ export class CDPRelayServer { if (toolName) url.searchParams.set('newTab', String(toolName === 'browser_navigate')); const href = url.toString(); - const executableInfo = registry.findExecutable(this._browserChannel); - if (!executableInfo) - throw new Error(`Unsupported channel: "${this._browserChannel}"`); - const executablePath = executableInfo.executablePath(); - if (!executablePath) - throw new Error(`"${this._browserChannel}" executable not found. Make sure it is installed at a standard location.`); + + let executablePath = this._executablePath; + if (!executablePath) { + const executableInfo = registry.findExecutable(this._browserChannel); + if (!executableInfo) + 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[] = []; if (this._userDataDir) diff --git a/src/extension/extensionContextFactory.ts b/src/extension/extensionContextFactory.ts index 397aaa8..bff7726 100644 --- a/src/extension/extensionContextFactory.ts +++ b/src/extension/extensionContextFactory.ts @@ -26,10 +26,12 @@ const debugLogger = debug('pw:mcp:relay'); export class ExtensionContextFactory implements BrowserContextFactory { private _browserChannel: 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._userDataDir = userDataDir; + this._executablePath = executablePath; } async createContext(clientInfo: ClientInfo, abortSignal: AbortSignal, toolName: string | undefined): Promise<{ browserContext: playwright.BrowserContext, close: () => Promise }> { @@ -55,7 +57,7 @@ export class ExtensionContextFactory implements BrowserContextFactory { httpServer.close(); 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()); debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`); return cdpRelayServer; diff --git a/src/program.ts b/src/program.ts index 4a1cda9..dde0cca 100644 --- a/src/program.ts +++ b/src/program.ts @@ -72,7 +72,7 @@ program const config = await resolveCLIConfig(options); 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) { const serverBackendFactory: mcpServer.ServerBackendFactory = {