From cfcca40b90fa0569904d1a7e1224f8390a8350c0 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 21 Jul 2025 17:57:38 -0700 Subject: [PATCH] chore(extension): find installed chrome (#728) --- src/extension/cdpRelay.ts | 38 ++++++++++++++++++++++++++------------ src/extension/main.ts | 8 +++++--- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/extension/cdpRelay.ts b/src/extension/cdpRelay.ts index 101f2ad..417570a 100644 --- a/src/extension/cdpRelay.ts +++ b/src/extension/cdpRelay.ts @@ -23,13 +23,15 @@ */ import http from 'http'; -import { promisify } from 'util'; -import { exec } from 'child_process'; +import { spawn } from 'child_process'; import { WebSocket, WebSocketServer } from 'ws'; import debug from 'debug'; import * as playwright from 'playwright'; import { httpAddressToString, startHttpServer } from '../transport.js'; import { BrowserContextFactory } from '../browserContextFactory.js'; +// @ts-ignore +const { registry } = await import('playwright-core/lib/server/registry/index'); + import type websocket from 'ws'; const debugLogger = debug('pw:mcp:relay'); @@ -52,6 +54,7 @@ type CDPResponse = { export class CDPRelayServer { private _wsHost: string; + private _browserChannel: string; private _cdpPath: string; private _extensionPath: string; private _wss: WebSocketServer; @@ -65,8 +68,9 @@ export class CDPRelayServer { private _extensionConnectionPromise: Promise; private _extensionConnectionResolve: (() => void) | null = null; - constructor(server: http.Server) { + constructor(server: http.Server, browserChannel: string) { this._wsHost = httpAddressToString(server.address()).replace(/^http/, 'ws'); + this._browserChannel = browserChannel; const uuid = crypto.randomUUID(); this._cdpPath = `/cdp/${uuid}`; @@ -88,10 +92,13 @@ export class CDPRelayServer { } async ensureExtensionConnectionForMCPContext(clientInfo: { name: string, version: string }) { + debugLogger('Ensuring extension connection for MCP context'); if (this._extensionConnection) return; await this._connectBrowser(clientInfo); + debugLogger('Waiting for incoming extension connection'); await this._extensionConnectionPromise; + debugLogger('Extension connection established'); } private async _connectBrowser(clientInfo: { name: string, version: string }) { @@ -101,12 +108,19 @@ export class CDPRelayServer { url.searchParams.set('mcpRelayUrl', mcpRelayEndpoint); url.searchParams.set('client', JSON.stringify(clientInfo)); const href = url.toString(); - const command = `'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' '${href}'`; - try { - await promisify(exec)(command); - } catch (err) { - debugLogger('Failed to run command:', err); - } + 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.`); + + spawn(executablePath, [href], { + windowsHide: true, + detached: true, + shell: false, + stdio: 'ignore', + }); } stop(): void { @@ -307,9 +321,9 @@ class ExtensionContextFactory implements BrowserContextFactory { } } -export async function startCDPRelayServer(port: number) { +export async function startCDPRelayServer(port: number, browserChannel: string) { const httpServer = await startHttpServer({ port }); - const cdpRelayServer = new CDPRelayServer(httpServer); + const cdpRelayServer = new CDPRelayServer(httpServer, browserChannel); process.on('exit', () => cdpRelayServer.stop()); debugLogger(`CDP relay server started, extension endpoint: ${cdpRelayServer.extensionEndpoint()}.`); return new ExtensionContextFactory(cdpRelayServer); @@ -332,7 +346,7 @@ class ExtensionConnection { async send(method: string, params?: any, sessionId?: string): Promise { if (this._ws.readyState !== WebSocket.OPEN) - throw new Error('WebSocket closed'); + throw new Error(`Unexpected WebSocket state: ${this._ws.readyState}`); const id = ++this._lastId; this._ws.send(JSON.stringify({ id, method, params, sessionId })); return new Promise((resolve, reject) => { diff --git a/src/extension/main.ts b/src/extension/main.ts index 93cdc62..9d629bb 100644 --- a/src/extension/main.ts +++ b/src/extension/main.ts @@ -19,9 +19,11 @@ import { startHttpServer, startHttpTransport, startStdioTransport } from '../tra import { Server } from '../server.js'; import { startCDPRelayServer } from './cdpRelay.js'; -export async function runWithExtension(options: any) { - const config = await resolveCLIConfig({ }); - const contextFactory = await startCDPRelayServer(9225); +import type { CLIOptions } from '../config.js'; + +export async function runWithExtension(options: CLIOptions) { + const config = await resolveCLIConfig(options); + const contextFactory = await startCDPRelayServer(9225, config.browser.launchOptions.channel || 'chrome'); const server = new Server(config, contextFactory); server.setupExitWatchdog();