From 55f71de5d50de486b0a02f73eab68e8b1cbd2a04 Mon Sep 17 00:00:00 2001 From: kensei Date: Sun, 15 Mar 2026 19:00:20 +0800 Subject: [PATCH] feat(browser): support Chrome 144+ auto-discovery via DevToolsActivePort Chrome 144+ allows enabling remote debugging from chrome://inspect without any command-line flags (--remote-debugging-port). Chrome writes the active port and browser GUID to a DevToolsActivePort file in the user data directory. This change reads that file to auto-discover the CDP WebSocket endpoint, eliminating the need for the Playwright MCP Bridge browser extension in most cases. Connection priority: 1. OPENCLI_CDP_ENDPOINT env var (manual override) 2. DevToolsActivePort auto-discovery (Chrome/Edge, cross-platform) 3. --extension mode fallback (original behavior) Supports Windows, macOS, and Linux with Chrome, Edge, and Chromium. Co-Authored-By: Claude Opus 4.6 --- src/browser.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/src/browser.ts b/src/browser.ts index adbe82d..44fce0a 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,6 +1,6 @@ /** - * Browser interaction via Playwright MCP Bridge extension. - * Connects to an existing Chrome browser through the extension's stdio JSON-RPC. + * Browser interaction via Chrome DevTools Protocol. + * Connects to an existing Chrome browser through CDP auto-discovery or extension bridge. */ import { spawn, execSync, type ChildProcess } from 'node:child_process'; @@ -10,6 +10,54 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { formatSnapshot } from './snapshotFormatter.js'; +/** + * Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint. + * + * Starting with Chrome 144, users can enable remote debugging from + * chrome://inspect#remote-debugging without any command-line flags. + * Chrome writes the active port and browser GUID to a DevToolsActivePort file + * in the user data directory, which we read to construct the WebSocket endpoint. + * + * Priority: OPENCLI_CDP_ENDPOINT env > DevToolsActivePort auto-discovery > --extension fallback + */ +function discoverChromeEndpoint(): string | null { + const candidates: string[] = []; + + // User-specified Chrome data dir takes highest priority + if (process.env.CHROME_USER_DATA_DIR) { + candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort')); + } + + // Standard Chrome/Edge user data dirs per platform + if (process.platform === 'win32') { + const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local'); + candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort')); + candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort')); + } else if (process.platform === 'darwin') { + candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort')); + candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort')); + } else { + candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort')); + candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort')); + candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort')); + } + + for (const filePath of candidates) { + try { + const content = fs.readFileSync(filePath, 'utf-8').trim(); + const lines = content.split('\n'); + if (lines.length >= 2) { + const port = parseInt(lines[0], 10); + const browserPath = lines[1]; // e.g. /devtools/browser/ + if (port > 0 && browserPath.startsWith('/devtools/browser/')) { + return `ws://127.0.0.1:${port}${browserPath}`; + } + } + } catch {} + } + return null; +} + // Read version from package.json (single source of truth) const __browser_dirname = path.dirname(fileURLToPath(import.meta.url)); const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.resolve(__browser_dirname, '..', 'package.json'), 'utf-8')).version; } catch { return '0.0.0'; } })(); @@ -248,14 +296,22 @@ export class PlaywrightMCP { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error(`Timed out connecting to browser (${timeout}s)`)), timeout * 1000); - const mcpArgs = [mcpPath, '--extension']; + // Chrome 144+ auto-discovery via DevToolsActivePort file. + // Falls back to --extension mode if no running Chrome is detected. + const cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT ?? discoverChromeEndpoint(); + const mcpArgs: string[] = [mcpPath]; + if (cdpEndpoint) { + mcpArgs.push('--cdp-endpoint', cdpEndpoint); + } else { + mcpArgs.push('--extension'); + } if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) { mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH); } this._proc = spawn('node', mcpArgs, { stdio: ['pipe', 'pipe', 'pipe'], - env: { ...process.env, ...(process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN ? { PLAYWRIGHT_MCP_EXTENSION_TOKEN: process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN } : {}) }, + env: { ...process.env }, }); // Increase max listeners to avoid warnings