From ff55a8a20c95e0c9d285b4094a4354ea74ef261e Mon Sep 17 00:00:00 2001 From: Alex Rudenko Date: Wed, 17 Sep 2025 16:47:51 +0200 Subject: [PATCH] chore: experimental devtools --- src/McpContext.ts | 32 +++++++++++++++++++--- src/browser.ts | 64 +++++++++++++++++++++++++------------------ src/index.ts | 11 +++++++- tests/browser.test.ts | 2 ++ tests/utils.ts | 4 ++- 5 files changed, 81 insertions(+), 32 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 53aabfa..0a97c0e 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -51,8 +51,16 @@ export class McpContext implements Context { #nextSnapshotId = 1; #traceResults: TraceResult[] = []; - - private constructor(browser: Browser, logger: Debugger) { + #devtools = false; + + private constructor( + browser: Browser, + logger: Debugger, + options: { + devtools: boolean; + }, + ) { + this.#devtools = options.devtools; this.browser = browser; this.logger = logger; @@ -85,8 +93,14 @@ export class McpContext implements Context { await this.#consoleCollector.init(); } - static async from(browser: Browser, logger: Debugger) { - const context = new McpContext(browser, logger); + static async from( + browser: Browser, + logger: Debugger, + options: { + devtools: boolean; + }, + ) { + const context = new McpContext(browser, logger, options); await context.#init(); return context; } @@ -229,6 +243,16 @@ export class McpContext implements Context { */ async createPagesSnapshot(): Promise { this.#pages = await this.browser.pages(); + if (this.#devtools) { + for (const target of this.browser.targets()) { + if ( + target.type() === 'other' && + target.url().startsWith('devtools://') + ) { + this.#pages.push(await target.asPage()); + } + } + } return this.#pages; } diff --git a/src/browser.ts b/src/browser.ts index 09527fc..fa3c8d5 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -17,38 +17,45 @@ import fs from 'fs'; 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, // We do not expect any single CDP command to take more than 10sec. protocolTimeout: 10_000, }; -async function ensureBrowserConnected(browserURL: string) { +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, }); return browser; @@ -61,6 +68,7 @@ type McpLaunchOptions = { userDataDir?: string; headless: boolean; isolated: boolean; + devtools: boolean; }; export async function launch(options: McpLaunchOptions): Promise { @@ -91,6 +99,9 @@ export async function launch(options: McpLaunchOptions): Promise { if (customDevTools) { args.push(`--custom-devtools-frontend=file://${customDevTools}`); } + if (options.devtools) { + args.push('--auto-open-devtools-for-tabs'); + } let puppeterChannel: ChromeReleaseChannel | undefined; if (!executablePath) { puppeterChannel = @@ -102,6 +113,7 @@ export async function launch(options: McpLaunchOptions): Promise { try { return await puppeteer.launch({ ...connectOptions, + targetFilter: makeTargetFilter(options.devtools), channel: puppeterChannel, executablePath, defaultViewport: null, @@ -138,16 +150,16 @@ async function ensureBrowserLaunched( return browser; } -export async function resolveBrowser(options: { - browserUrl?: string; - executablePath?: string; - customDevTools?: string; - channel?: Channel; - headless: boolean; - isolated: boolean; -}) { +export async function resolveBrowser( + options: McpLaunchOptions & { + browserUrl?: string; + }, +) { const browser = options.browserUrl - ? await ensureBrowserConnected(options.browserUrl) + ? await ensureBrowserConnected({ + browserURL: options.browserUrl, + devtools: options.devtools, + }) : await ensureBrowserLaunched(options); return browser; diff --git a/src/index.ts b/src/index.ts index 0585f85..a6dec56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -82,6 +82,12 @@ export const cliOptions = { describe: 'Save the logs to file.', hidden: true, }, + experimentalDevTools: { + type: 'boolean' as const, + describe: 'Whether to enable automation over DevTools targets', + default: false, + hidden: true, + }, }; const yargsInstance = yargs(hideBin(process.argv)) @@ -155,9 +161,12 @@ async function getContext(): Promise { customDevTools: args.customDevtools, channel: args.channel as Channel, isolated: args.isolated, + devtools: args.experimentalDevTools, }); if (context?.browser !== browser) { - context = await McpContext.from(browser, logger); + context = await McpContext.from(browser, logger, { + devtools: args.experimentalDevTools, + }); } return context; } diff --git a/tests/browser.test.ts b/tests/browser.test.ts index fd2e392..a3e8369 100644 --- a/tests/browser.test.ts +++ b/tests/browser.test.ts @@ -17,6 +17,7 @@ describe('browser', () => { headless: true, isolated: false, userDataDir: folderPath, + devtools: false, }); try { try { @@ -24,6 +25,7 @@ describe('browser', () => { headless: true, isolated: false, userDataDir: folderPath, + devtools: false, }); await browser2.close(); assert.fail('not reached'); diff --git a/tests/utils.ts b/tests/utils.ts index 0ee9da5..42104e2 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -34,7 +34,9 @@ export async function withBrowser( }), ); const response = new McpResponse(); - const context = await McpContext.from(browser, logger('test')); + const context = await McpContext.from(browser, logger('test'), { + devtools: false, + }); await cb(response, context); }