diff --git a/README.md b/README.md index e04f91eb4..e75e08a7c 100644 --- a/README.md +++ b/README.md @@ -633,6 +633,9 @@ In these cases, start Chrome first and let the Chrome DevTools MCP server connec - **Automatic connection (available in Chrome 144)**: best for sharing state between manual and agent-driven testing. - **Manual connection via remote debugging port**: best when running inside a sandboxed environment. +> [!NOTE] +> To include extension pages and extension service workers while connecting to an existing Chrome instance, add `--category-extensions`. This applies to `--autoConnect`, `--browserUrl`, and `--wsEndpoint`. + #### Automatically connecting to a running Chrome instance **Step 1:** Set up remote debugging in Chrome diff --git a/src/index.ts b/src/index.ts index 1e731521c..d7542b78a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,8 +28,65 @@ import {pageIdSchema} from './tools/ToolDefinition.js'; import {createTools} from './tools/tools.js'; import {VERSION} from './version.js'; +type ServerArgs = ReturnType; + +interface BrowserResolvers { + ensureBrowserConnected: typeof ensureBrowserConnected; + ensureBrowserLaunched: typeof ensureBrowserLaunched; +} + +export async function resolveBrowser( + serverArgs: ServerArgs, + options: { + logFile?: fs.WriteStream; + }, + resolvers: BrowserResolvers = { + ensureBrowserConnected, + ensureBrowserLaunched, + }, +) { + const chromeArgs: string[] = (serverArgs.chromeArg ?? []).map(String); + const ignoreDefaultChromeArgs: string[] = ( + serverArgs.ignoreDefaultChromeArg ?? [] + ).map(String); + if (serverArgs.proxyServer) { + chromeArgs.push(`--proxy-server=${serverArgs.proxyServer}`); + } + const devtools = serverArgs.experimentalDevtools ?? false; + return serverArgs.browserUrl || + serverArgs.wsEndpoint || + serverArgs.autoConnect + ? await resolvers.ensureBrowserConnected({ + browserURL: serverArgs.browserUrl, + wsEndpoint: serverArgs.wsEndpoint, + wsHeaders: serverArgs.wsHeaders, + // Important: only pass channel, if autoConnect is true. + channel: serverArgs.autoConnect + ? (serverArgs.channel as Channel) + : undefined, + userDataDir: serverArgs.userDataDir, + devtools, + enableExtensions: serverArgs.categoryExtensions, + }) + : await resolvers.ensureBrowserLaunched({ + headless: serverArgs.headless, + executablePath: serverArgs.executablePath, + channel: serverArgs.channel as Channel, + isolated: serverArgs.isolated ?? false, + userDataDir: serverArgs.userDataDir, + logFile: options.logFile, + viewport: serverArgs.viewport, + chromeArgs, + ignoreDefaultChromeArgs, + acceptInsecureCerts: serverArgs.acceptInsecureCerts, + devtools, + enableExtensions: serverArgs.categoryExtensions, + viaCli: serverArgs.viaCli, + }); +} + export async function createMcpServer( - serverArgs: ReturnType, + serverArgs: ServerArgs, options: { logFile?: fs.WriteStream; }, @@ -59,42 +116,8 @@ export async function createMcpServer( let context: McpContext; async function getContext(): Promise { - const chromeArgs: string[] = (serverArgs.chromeArg ?? []).map(String); - const ignoreDefaultChromeArgs: string[] = ( - serverArgs.ignoreDefaultChromeArg ?? [] - ).map(String); - if (serverArgs.proxyServer) { - chromeArgs.push(`--proxy-server=${serverArgs.proxyServer}`); - } const devtools = serverArgs.experimentalDevtools ?? false; - const browser = - serverArgs.browserUrl || serverArgs.wsEndpoint || serverArgs.autoConnect - ? await ensureBrowserConnected({ - browserURL: serverArgs.browserUrl, - wsEndpoint: serverArgs.wsEndpoint, - wsHeaders: serverArgs.wsHeaders, - // Important: only pass channel, if autoConnect is true. - channel: serverArgs.autoConnect - ? (serverArgs.channel as Channel) - : undefined, - userDataDir: serverArgs.userDataDir, - devtools, - }) - : await ensureBrowserLaunched({ - headless: serverArgs.headless, - executablePath: serverArgs.executablePath, - channel: serverArgs.channel as Channel, - isolated: serverArgs.isolated ?? false, - userDataDir: serverArgs.userDataDir, - logFile: options.logFile, - viewport: serverArgs.viewport, - chromeArgs, - ignoreDefaultChromeArgs, - acceptInsecureCerts: serverArgs.acceptInsecureCerts, - devtools, - enableExtensions: serverArgs.categoryExtensions, - viaCli: serverArgs.viaCli, - }); + const browser = await resolveBrowser(serverArgs, options); if (context?.browser !== browser) { context = await McpContext.from(browser, logger, { diff --git a/tests/resolveBrowser.test.ts b/tests/resolveBrowser.test.ts new file mode 100644 index 000000000..b3cd1eb74 --- /dev/null +++ b/tests/resolveBrowser.test.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; +import {describe, it} from 'node:test'; + +import {parseArguments} from '../src/bin/chrome-devtools-mcp-cli-options.js'; +import {resolveBrowser} from '../src/index.js'; +import type {Browser} from '../src/third_party/index.js'; + +describe('resolveBrowser', () => { + for (const [label, args] of [ + [ + 'browserUrl', + ['--browser-url', 'http://127.0.0.1:9222', '--category-extensions'], + ], + [ + 'wsEndpoint', + [ + '--ws-endpoint', + 'ws://127.0.0.1:9222/devtools/browser/test', + '--category-extensions', + ], + ], + [ + 'autoConnect', + [ + '--auto-connect', + '--user-data-dir', + '/tmp/profile', + '--category-extensions', + ], + ], + ] as const) { + it(`passes enableExtensions on the connected-browser path via ${label}`, async () => { + const serverArgs = parseArguments('0.0.0', [ + 'node', + 'chrome-devtools-mcp', + ...args, + ]); + const browser = {} as Browser; + const connectedCalls: Array> = []; + let launchedCallCount = 0; + + const resolvedBrowser = await resolveBrowser( + serverArgs, + {}, + { + ensureBrowserConnected: async options => { + connectedCalls.push(options as Record); + return browser; + }, + ensureBrowserLaunched: async () => { + launchedCallCount += 1; + return browser; + }, + }, + ); + + assert.strictEqual(resolvedBrowser, browser); + assert.strictEqual(launchedCallCount, 0); + assert.strictEqual(connectedCalls.length, 1); + assert.strictEqual(connectedCalls[0]?.['enableExtensions'], true); + }); + } +});