diff --git a/src/browser.ts b/src/browser.ts index 083e9c09..b0a7aaa5 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -11,7 +11,6 @@ import path from 'node:path'; import type { Browser, ChromeReleaseChannel, - ConnectOptions, LaunchOptions, Target, } from 'puppeteer-core'; @@ -19,37 +18,41 @@ import puppeteer from 'puppeteer-core'; let browser: Browser | undefined; -const ignoredPrefixes = new Set([ - 'chrome://', - 'chrome-extension://', - 'chrome-untrusted://', - 'devtools://', -]); +function makeTargetFilter(devtools: boolean) { + const ignoredPrefixes = new Set([ + 'chrome://', + 'chrome-extension://', + 'chrome-untrusted://', + ]); -function targetFilter(target: Target): boolean { - if (target.url() === 'chrome://newtab/') { - return true; + if (!devtools) { + ignoredPrefixes.add('devtools://'); } - for (const prefix of ignoredPrefixes) { - if (target.url().startsWith(prefix)) { - return false; + return function targetFilter(target: Target): boolean { + if (target.url() === 'chrome://newtab/') { + return true; } - } - return true; + for (const prefix of ignoredPrefixes) { + if (target.url().startsWith(prefix)) { + return false; + } + } + return true; + }; } -const connectOptions: ConnectOptions = { - targetFilter, -}; - -export async function ensureBrowserConnected(browserURL: string) { +export async function ensureBrowserConnected(options: { + browserURL: string; + devtools: boolean; +}) { if (browser?.connected) { return browser; } browser = await puppeteer.connect({ - ...connectOptions, - browserURL, + targetFilter: makeTargetFilter(options.devtools), + browserURL: options.browserURL, defaultViewport: null, + handleDevToolsAsPage: options.devtools, }); return browser; } @@ -68,6 +71,7 @@ interface McpLaunchOptions { height: number; }; args?: string[]; + devtools: boolean; } export async function launch(options: McpLaunchOptions): Promise { @@ -101,6 +105,9 @@ export async function launch(options: McpLaunchOptions): Promise { args.push('--screen-info={3840x2160}'); } let puppeteerChannel: ChromeReleaseChannel | undefined; + if (options.devtools) { + args.push('--auto-open-devtools-for-tabs'); + } if (!executablePath) { puppeteerChannel = channel && channel !== 'stable' @@ -110,8 +117,8 @@ export async function launch(options: McpLaunchOptions): Promise { try { const browser = await puppeteer.launch({ - ...connectOptions, channel: puppeteerChannel, + targetFilter: makeTargetFilter(options.devtools), executablePath, defaultViewport: null, userDataDir, @@ -119,6 +126,7 @@ export async function launch(options: McpLaunchOptions): Promise { headless, args, acceptInsecureCerts: options.acceptInsecureCerts, + handleDevToolsAsPage: options.devtools, }); if (options.logFile) { // FIXME: we are probably subscribing too late to catch startup logs. We diff --git a/src/cli.ts b/src/cli.ts index 909d1a21..17b1df63 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -88,6 +88,11 @@ export const cliOptions = { type: 'boolean', description: `If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.`, }, + experimentalDevtools: { + type: 'boolean' as const, + describe: 'Whether to enable automation over DevTools targets', + hidden: true, + }, } satisfies Record; export function parseArguments(version: string, argv = process.argv) { diff --git a/src/main.ts b/src/main.ts index 2663d3c1..fdf18550 100644 --- a/src/main.ts +++ b/src/main.ts @@ -73,8 +73,12 @@ async function getContext(): Promise { if (args.proxyServer) { extraArgs.push(`--proxy-server=${args.proxyServer}`); } + const devtools = args.experimentalDevtools ?? false; const browser = args.browserUrl - ? await ensureBrowserConnected(args.browserUrl) + ? await ensureBrowserConnected({ + browserURL: args.browserUrl, + devtools, + }) : await ensureBrowserLaunched({ headless: args.headless, executablePath: args.executablePath, @@ -85,6 +89,7 @@ async function getContext(): Promise { viewport: args.viewport, args: extraArgs, acceptInsecureCerts: args.acceptInsecureCerts, + devtools, }); if (context?.browser !== browser) { @@ -143,6 +148,9 @@ function registerTool(tool: ToolDefinition): void { isError: true, }; } + } catch (err) { + logger(`${tool.name} error: ${err.message}`); + throw err; } finally { guard.dispose(); } diff --git a/tests/browser.test.ts b/tests/browser.test.ts index b4811202..8066da5f 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -21,6 +21,7 @@ describe('browser', () => { isolated: false, userDataDir: folderPath, executablePath: executablePath(), + devtools: false, }); try { try { @@ -29,6 +30,7 @@ describe('browser', () => { isolated: false, userDataDir: folderPath, executablePath: executablePath(), + devtools: false, }); await browser2.close(); assert.fail('not reached'); @@ -55,6 +57,7 @@ describe('browser', () => { width: 1501, height: 801, }, + devtools: false, }); try { const [page] = await browser.pages();