From aaf56cb43c578f434b46c029ba03bedfea3fbdb3 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Tue, 24 Feb 2026 20:53:53 +0100 Subject: [PATCH 1/6] feat: update listPages to return extension service workers --- src/McpContext.ts | 61 ++++++++++++++++++- src/McpResponse.ts | 30 +++++++++ src/tools/ToolDefinition.ts | 1 + src/tools/pages.ts | 32 ++++++---- src/tools/tools.ts | 3 +- .../tools/fixtures/extension-sw/manifest.json | 11 ++++ tests/tools/fixtures/extension-sw/popup.html | 6 ++ tests/tools/fixtures/extension-sw/sw.js | 1 + tests/tools/pages.test.ts | 57 ++++++++++++++++- 9 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 tests/tools/fixtures/extension-sw/manifest.json create mode 100644 tests/tools/fixtures/extension-sw/popup.html create mode 100644 tests/tools/fixtures/extension-sw/sw.js diff --git a/src/McpContext.ts b/src/McpContext.ts index a36f0c197..8f7118a07 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -8,6 +8,7 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; +import type {ParsedArguments} from './cli.js'; import type {TargetUniverse} from './DevtoolsUtils.js'; import { extractUrlLikeFromDevToolsTitle, @@ -29,6 +30,7 @@ import type { ScreenRecorder, SerializedAXNode, Viewport, + Target, } from './third_party/index.js'; import {Locator} from './third_party/index.js'; import {PredefinedNetworkConditions} from './third_party/index.js'; @@ -50,6 +52,12 @@ export interface TextSnapshotNode extends SerializedAXNode { children: TextSnapshotNode[]; } +export interface ExtensionServiceWorker { + url: string; + target: Target; + id: string; +} + export interface GeolocationOptions { latitude: number; longitude: number; @@ -129,6 +137,8 @@ export class McpContext implements Context { #nextIsolatedContextId = 1; #pages: Page[] = []; + #extensionServiceWorkers: ExtensionServiceWorker[] = []; + #pageToDevToolsPage = new Map(); #selectedPage?: Page; #textSnapshot: TextSnapshot | null = null; @@ -146,6 +156,9 @@ export class McpContext implements Context { #pageIdMap = new WeakMap(); #nextPageId = 1; + #extensionServiceWorkerMap = new WeakMap(); + #nextExtensionServiceWorkerId = 1; + #nextSnapshotId = 1; #traceResults: TraceResult[] = []; @@ -185,6 +198,7 @@ export class McpContext implements Context { async #init() { const pages = await this.createPagesSnapshot(); + await this.createExtensionServiceWorkersSnapshot(); await this.#networkCollector.init(pages); await this.#consoleCollector.init(pages); await this.#devtoolsUniverseManager.init(pages); @@ -494,7 +508,7 @@ export class McpContext implements Context { } if (page.isClosed()) { throw new Error( - `The selected page has been closed. Call ${listPages.name} to see open pages.`, + `The selected page has been closed. Call ${listPages({} as ParsedArguments).name} to see open pages.`, ); } return page; @@ -584,6 +598,41 @@ export class McpContext implements Context { } } + /** + * Creates a snapshot of the extension service workers. + */ + async createExtensionServiceWorkersSnapshot(): Promise< + ExtensionServiceWorker[] + > { + const allTargets = await this.browser.targets(); + + const serviceWorkers = allTargets.filter(target => { + return ( + target.type() === 'service_worker' && + target.url().includes('chrome-extension://') + ); + }); + + for (const serviceWorker of serviceWorkers) { + if (!this.#extensionServiceWorkerMap.has(serviceWorker)) { + this.#extensionServiceWorkerMap.set( + serviceWorker, + 'sw-' + this.#nextExtensionServiceWorkerId++, + ); + } + } + + this.#extensionServiceWorkers = serviceWorkers.map(serviceWorker => { + return { + target: serviceWorker, + id: this.#extensionServiceWorkerMap.get(serviceWorker)!, + url: serviceWorker.url(), + }; + }); + + return this.#extensionServiceWorkers; + } + async createPagesSnapshot(): Promise { const allPages = await this.#getAllPages(); @@ -677,6 +726,16 @@ export class McpContext implements Context { } } + getExtensionServiceWorkers(): ExtensionServiceWorker[] { + return this.#extensionServiceWorkers; + } + + getExtensionServiceWorkerId( + extensionServiceWorker: ExtensionServiceWorker, + ): string | undefined { + return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target); + } + getPages(): Page[] { return this.#pages; } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 5cdad8e5b..e14e17a90 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -38,6 +38,7 @@ interface TraceInsightData { export class McpResponse implements Response { #includePages = false; + #includeExtensionServiceWorkers = false; #snapshotParams?: SnapshotParams; #attachedNetworkRequestId?: number; #attachedNetworkRequestOptions?: { @@ -88,6 +89,10 @@ export class McpResponse implements Response { this.#listExtensions = true; } + setIncludeExtensionServiceWorkers(value: boolean): void { + this.#includeExtensionServiceWorkers = value; + } + setIncludeNetworkRequests( value: boolean, options?: PaginationOptions & { @@ -233,6 +238,10 @@ export class McpResponse implements Response { await context.createPagesSnapshot(); } + if (this.#includeExtensionServiceWorkers) { + await context.createExtensionServiceWorkersSnapshot(); + } + let snapshot: SnapshotFormatter | string | undefined; if (this.#snapshotParams) { await context.createTextSnapshot( @@ -438,6 +447,7 @@ export class McpResponse implements Response { }; pages?: object[]; pagination?: object; + extensionServiceWorkers?: object[]; } = {}; const response = [`# ${toolName} response`]; @@ -532,6 +542,26 @@ Call ${handleDialog.name} to handle it before continuing.`); }); } + if (this.#includeExtensionServiceWorkers) { + if (!context.getExtensionServiceWorkers().length) { + response.push(`## Extension Service Workers`); + } + + for (const extensionServiceWorker of context.getExtensionServiceWorkers()) { + response.push( + `${extensionServiceWorker.id}: ${extensionServiceWorker.url}`, + ); + } + structuredContent.extensionServiceWorkers = context + .getExtensionServiceWorkers() + .map(extensionServiceWorker => { + return { + id: extensionServiceWorker.id, + url: extensionServiceWorker.url, + }; + }); + } + if (this.#tabId) { structuredContent.tabId = this.#tabId; } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 70bfbfcbe..cbe08b5d8 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -96,6 +96,7 @@ export interface Response { insightName: InsightName, ): void; setListExtensions(): void; + setIncludeExtensionServiceWorkers(value: boolean): void; } /** diff --git a/src/tools/pages.ts b/src/tools/pages.ts index b63590dfe..2f911cc43 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type {ParsedArguments} from '../cli.js'; import {logger} from '../logger.js'; import type {Dialog} from '../third_party/index.js'; import {zod} from '../third_party/index.js'; @@ -11,17 +12,22 @@ import {zod} from '../third_party/index.js'; import {ToolCategory} from './categories.js'; import {CLOSE_PAGE_ERROR, defineTool, timeoutSchema} from './ToolDefinition.js'; -export const listPages = defineTool({ - name: 'list_pages', - description: `Get a list of pages open in the browser.`, - annotations: { - category: ToolCategory.NAVIGATION, - readOnlyHint: true, - }, - schema: {}, - handler: async (_request, response) => { - response.setIncludePages(true); - }, +export const listPages = defineTool((args) => { + return { + name: 'list_pages', + description: `Get a list of pages ${args.categoryExtensions ? 'including extension service workers' : ''} open in the browser.`, + annotations: { + category: ToolCategory.NAVIGATION, + readOnlyHint: true, + }, + schema: {}, + handler: async (_request, response) => { + response.setIncludePages(true); + if(args.categoryExtensions) { + response.setIncludeExtensionServiceWorkers(true); + } + }, + }; }); export const selectPage = defineTool({ @@ -35,7 +41,7 @@ export const selectPage = defineTool({ pageId: zod .number() .describe( - `The ID of the page to select. Call ${listPages.name} to get available pages.`, + `The ID of the page to select. Call ${listPages({} as ParsedArguments).name} to get available pages.`, ), bringToFront: zod .boolean() @@ -372,7 +378,7 @@ export const getTabId = defineTool({ pageId: zod .number() .describe( - `The ID of the page to get the tab ID for. Call ${listPages.name} to get available pages.`, + `The ID of the page to get the tab ID for. Call ${listPages({} as ParsedArguments).name} to get available pages.`, ), }, handler: async (request, response, context) => { diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 8fb8659d9..238b684fc 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -42,8 +42,7 @@ export const createTools = (args: ParsedArguments) => { const tools: ToolDefinition[] = []; for (const tool of rawTools) { if (typeof tool === 'function') { - // @ts-expect-error none of the tools for now implement the function type tool has type "never" - tools.push(tool(args) as ToolDefinition); + tools.push(tool(args)); } else { tools.push(tool as ToolDefinition); } diff --git a/tests/tools/fixtures/extension-sw/manifest.json b/tests/tools/fixtures/extension-sw/manifest.json new file mode 100644 index 000000000..d1d926c48 --- /dev/null +++ b/tests/tools/fixtures/extension-sw/manifest.json @@ -0,0 +1,11 @@ +{ + "manifest_version": 3, + "name": "Test Extension with SW", + "version": "1.0", + "background": { + "service_worker": "sw.js" + }, + "action": { + "default_popup": "popup.html" + } +} diff --git a/tests/tools/fixtures/extension-sw/popup.html b/tests/tools/fixtures/extension-sw/popup.html new file mode 100644 index 000000000..9f3116253 --- /dev/null +++ b/tests/tools/fixtures/extension-sw/popup.html @@ -0,0 +1,6 @@ + + + +

Extension With Service Worker

+ + diff --git a/tests/tools/fixtures/extension-sw/sw.js b/tests/tools/fixtures/extension-sw/sw.js new file mode 100644 index 000000000..f44ddb739 --- /dev/null +++ b/tests/tools/fixtures/extension-sw/sw.js @@ -0,0 +1 @@ +console.log('Service worker loaded'); diff --git a/tests/tools/pages.test.ts b/tests/tools/pages.test.ts index 12fcb364a..6eadfb76d 100644 --- a/tests/tools/pages.test.ts +++ b/tests/tools/pages.test.ts @@ -5,11 +5,14 @@ */ import assert from 'node:assert'; +import path from 'node:path'; import {afterEach, describe, it} from 'node:test'; import type {Dialog} from 'puppeteer-core'; import sinon from 'sinon'; +import type {ParsedArguments} from '../../src/cli.js'; +import {installExtension} from '../../src/tools/extensions.js'; import { listPages, newPage, @@ -22,6 +25,11 @@ import { } from '../../src/tools/pages.js'; import {html, withMcpContext} from '../utils.js'; +const EXTENSION_PATH = path.join( + import.meta.dirname, + '../../../tests/tools/fixtures/extension-sw', +); + describe('pages', () => { afterEach(() => { sinon.restore(); @@ -30,10 +38,57 @@ describe('pages', () => { describe('list_pages', () => { it('list pages', async () => { await withMcpContext(async (response, context) => { - await listPages.handler({params: {}}, response, context); + await listPages({} as ParsedArguments).handler( + {params: {}}, + response, + context, + ); assert.ok(response.includePages); }); }); + for (const categoryExtensions of [true, false]) { + it(`list pages ${categoryExtensions ? 'with' : 'without'} --category-extensions`, async () => { + await withMcpContext(async (response, context) => { + await installExtension.handler( + {params: {path: EXTENSION_PATH}}, + response, + context, + ); + + const swTarget = await context.browser.waitForTarget( + t => + t.type() === 'service_worker' && + t.url().includes('chrome-extension://'), + ); + const swUrl = swTarget.url(); + + response.resetResponseLineForTesting(); + + const listPageDef = listPages({categoryExtensions} as ParsedArguments); + await listPageDef.handler({params: {}}, response, context); + + const result = await response.handle(listPageDef.name, context); + const textContent = result.content.find(c => c.type === 'text') as { + type: 'text'; + text: string; + }; + assert.ok(textContent); + + if (categoryExtensions) { + assert.ok(textContent.text.includes(swUrl)); + const structured = result.structuredContent as { + extensionServiceWorkers: Array<{url: string}>; + }; + assert.deepStrictEqual( + structured.extensionServiceWorkers.map(sw => sw.url), + [swUrl], + ); + } else { + assert.ok(!textContent.text.includes(swUrl)); + } + }); + }); + } }); describe('new_page', () => { it('create a page', async () => { From 79bd3f947302c28a96b976527ad534db1def2d21 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Tue, 24 Feb 2026 21:13:36 +0100 Subject: [PATCH 2/6] chore: make tool definition arguments optional --- src/McpContext.ts | 96 ++++++++++++++++++------------------- src/tools/ToolDefinition.ts | 8 ++-- src/tools/pages.ts | 10 ++-- tests/tools/pages.test.ts | 10 ++-- 4 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 8f7118a07..fddd80541 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -53,9 +53,9 @@ export interface TextSnapshotNode extends SerializedAXNode { } export interface ExtensionServiceWorker { - url: string; - target: Target; - id: string; + url: string; + target: Target; + id: string; } export interface GeolocationOptions { @@ -156,8 +156,8 @@ export class McpContext implements Context { #pageIdMap = new WeakMap(); #nextPageId = 1; - #extensionServiceWorkerMap = new WeakMap(); - #nextExtensionServiceWorkerId = 1; + #extensionServiceWorkerMap = new WeakMap(); + #nextExtensionServiceWorkerId = 1; #nextSnapshotId = 1; #traceResults: TraceResult[] = []; @@ -508,7 +508,7 @@ export class McpContext implements Context { } if (page.isClosed()) { throw new Error( - `The selected page has been closed. Call ${listPages({} as ParsedArguments).name} to see open pages.`, + `The selected page has been closed. Call ${listPages().name} to see open pages.`, ); } return page; @@ -598,40 +598,40 @@ export class McpContext implements Context { } } - /** - * Creates a snapshot of the extension service workers. - */ - async createExtensionServiceWorkersSnapshot(): Promise< - ExtensionServiceWorker[] - > { - const allTargets = await this.browser.targets(); - - const serviceWorkers = allTargets.filter(target => { - return ( - target.type() === 'service_worker' && - target.url().includes('chrome-extension://') - ); - }); - - for (const serviceWorker of serviceWorkers) { - if (!this.#extensionServiceWorkerMap.has(serviceWorker)) { - this.#extensionServiceWorkerMap.set( - serviceWorker, - 'sw-' + this.#nextExtensionServiceWorkerId++, - ); - } - } - - this.#extensionServiceWorkers = serviceWorkers.map(serviceWorker => { - return { - target: serviceWorker, - id: this.#extensionServiceWorkerMap.get(serviceWorker)!, - url: serviceWorker.url(), - }; - }); - - return this.#extensionServiceWorkers; - } + /** + * Creates a snapshot of the extension service workers. + */ + async createExtensionServiceWorkersSnapshot(): Promise< + ExtensionServiceWorker[] + > { + const allTargets = await this.browser.targets(); + + const serviceWorkers = allTargets.filter(target => { + return ( + target.type() === 'service_worker' && + target.url().includes('chrome-extension://') + ); + }); + + for (const serviceWorker of serviceWorkers) { + if (!this.#extensionServiceWorkerMap.has(serviceWorker)) { + this.#extensionServiceWorkerMap.set( + serviceWorker, + 'sw-' + this.#nextExtensionServiceWorkerId++, + ); + } + } + + this.#extensionServiceWorkers = serviceWorkers.map(serviceWorker => { + return { + target: serviceWorker, + id: this.#extensionServiceWorkerMap.get(serviceWorker)!, + url: serviceWorker.url(), + }; + }); + + return this.#extensionServiceWorkers; + } async createPagesSnapshot(): Promise { const allPages = await this.#getAllPages(); @@ -726,15 +726,15 @@ export class McpContext implements Context { } } - getExtensionServiceWorkers(): ExtensionServiceWorker[] { - return this.#extensionServiceWorkers; - } + getExtensionServiceWorkers(): ExtensionServiceWorker[] { + return this.#extensionServiceWorkers; + } - getExtensionServiceWorkerId( - extensionServiceWorker: ExtensionServiceWorker, - ): string | undefined { - return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target); - } + getExtensionServiceWorkerId( + extensionServiceWorker: ExtensionServiceWorker, + ): string | undefined { + return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target); + } getPages(): Page[] { return this.#pages; diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index cbe08b5d8..536a005a6 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -165,14 +165,16 @@ export function defineTool< Schema extends zod.ZodRawShape, Args extends ParsedArguments = ParsedArguments, >( - definition: (args: Args) => ToolDefinition, -): (args: Args) => ToolDefinition; + definition: (args?: Args) => ToolDefinition, +): (args?: Args) => ToolDefinition; export function defineTool< Schema extends zod.ZodRawShape, Args extends ParsedArguments = ParsedArguments, >( - definition: ToolDefinition | ((args: Args) => ToolDefinition), + definition: + | ToolDefinition + | ((args?: Args) => ToolDefinition), ) { return definition; } diff --git a/src/tools/pages.ts b/src/tools/pages.ts index 2f911cc43..65cc8f6b5 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -12,10 +12,10 @@ import {zod} from '../third_party/index.js'; import {ToolCategory} from './categories.js'; import {CLOSE_PAGE_ERROR, defineTool, timeoutSchema} from './ToolDefinition.js'; -export const listPages = defineTool((args) => { +export const listPages = defineTool(args => { return { name: 'list_pages', - description: `Get a list of pages ${args.categoryExtensions ? 'including extension service workers' : ''} open in the browser.`, + description: `Get a list of pages ${args?.categoryExtensions ? 'including extension service workers' : ''} open in the browser.`, annotations: { category: ToolCategory.NAVIGATION, readOnlyHint: true, @@ -23,7 +23,7 @@ export const listPages = defineTool((args) => { schema: {}, handler: async (_request, response) => { response.setIncludePages(true); - if(args.categoryExtensions) { + if (args?.categoryExtensions) { response.setIncludeExtensionServiceWorkers(true); } }, @@ -41,7 +41,7 @@ export const selectPage = defineTool({ pageId: zod .number() .describe( - `The ID of the page to select. Call ${listPages({} as ParsedArguments).name} to get available pages.`, + `The ID of the page to select. Call ${listPages().name} to get available pages.`, ), bringToFront: zod .boolean() @@ -378,7 +378,7 @@ export const getTabId = defineTool({ pageId: zod .number() .describe( - `The ID of the page to get the tab ID for. Call ${listPages({} as ParsedArguments).name} to get available pages.`, + `The ID of the page to get the tab ID for. Call ${listPages().name} to get available pages.`, ), }, handler: async (request, response, context) => { diff --git a/tests/tools/pages.test.ts b/tests/tools/pages.test.ts index 6eadfb76d..6a0a9f99f 100644 --- a/tests/tools/pages.test.ts +++ b/tests/tools/pages.test.ts @@ -38,11 +38,7 @@ describe('pages', () => { describe('list_pages', () => { it('list pages', async () => { await withMcpContext(async (response, context) => { - await listPages({} as ParsedArguments).handler( - {params: {}}, - response, - context, - ); + await listPages().handler({params: {}}, response, context); assert.ok(response.includePages); }); }); @@ -64,7 +60,9 @@ describe('pages', () => { response.resetResponseLineForTesting(); - const listPageDef = listPages({categoryExtensions} as ParsedArguments); + const listPageDef = listPages({ + categoryExtensions, + } as ParsedArguments); await listPageDef.handler({params: {}}, response, context); const result = await response.handle(listPageDef.name, context); From ffb7f25c14e620753484038fb97a1b34ab627bbd Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Wed, 25 Feb 2026 09:15:37 +0100 Subject: [PATCH 3/6] chore: fixing lint issues --- eslint.config.mjs | 2 +- src/McpContext.ts | 1 - src/tools/pages.ts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index db6ddd675..2c1cd0214 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,7 +14,7 @@ import tseslint from 'typescript-eslint'; import localPlugin from './scripts/eslint_rules/local-plugin.js'; export default defineConfig([ - globalIgnores(['**/node_modules', '**/build/']), + globalIgnores(['**/node_modules', '**/build/', 'tests/tools/fixtures/']), importPlugin.flatConfigs.typescript, { languageOptions: { diff --git a/src/McpContext.ts b/src/McpContext.ts index fddd80541..56ea1f5a8 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -8,7 +8,6 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import type {ParsedArguments} from './cli.js'; import type {TargetUniverse} from './DevtoolsUtils.js'; import { extractUrlLikeFromDevToolsTitle, diff --git a/src/tools/pages.ts b/src/tools/pages.ts index 65cc8f6b5..0febfe9f0 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -4,7 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type {ParsedArguments} from '../cli.js'; import {logger} from '../logger.js'; import type {Dialog} from '../third_party/index.js'; import {zod} from '../third_party/index.js'; From c64f3aaa305f29ba357ca04abb6c6dd0d3a25e06 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Wed, 25 Feb 2026 09:21:00 +0100 Subject: [PATCH 4/6] chore: update doc --- docs/tool-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index f2b4e6234..6d36c4ce7 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -1,6 +1,6 @@ -# Chrome DevTools MCP Tool Reference (~7094 cl100k_base tokens) +# Chrome DevTools MCP Tool Reference (~7095 cl100k_base tokens) - **[Input automation](#input-automation)** (9 tools) - [`click`](#click) From 925ebf7e81fd0f9c91814ba8851267dc9d6477b5 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Wed, 25 Feb 2026 12:08:54 +0100 Subject: [PATCH 5/6] chore: move conditional service worker logic inside setIncludePages --- src/McpResponse.ts | 10 +++++----- src/tools/ToolDefinition.ts | 3 +-- src/tools/pages.ts | 5 +---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index e14e17a90..c7e154c45 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -75,8 +75,12 @@ export class McpResponse implements Response { this.#tabId = tabId; } - setIncludePages(value: boolean): void { + setIncludePages(value: boolean, includeServiceWorkers?: boolean): void { this.#includePages = value; + + if (includeServiceWorkers) { + this.#includeExtensionServiceWorkers = value; + } } includeSnapshot(params?: SnapshotParams): void { @@ -89,10 +93,6 @@ export class McpResponse implements Response { this.#listExtensions = true; } - setIncludeExtensionServiceWorkers(value: boolean): void { - this.#includeExtensionServiceWorkers = value; - } - setIncludeNetworkRequests( value: boolean, options?: PaginationOptions & { diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 536a005a6..52f22c730 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -63,7 +63,7 @@ export interface DevToolsData { export interface Response { appendResponseLine(value: string): void; - setIncludePages(value: boolean): void; + setIncludePages(value: boolean, shouldIncludeExtension?: boolean): void; setIncludeNetworkRequests( value: boolean, options?: PaginationOptions & { @@ -96,7 +96,6 @@ export interface Response { insightName: InsightName, ): void; setListExtensions(): void; - setIncludeExtensionServiceWorkers(value: boolean): void; } /** diff --git a/src/tools/pages.ts b/src/tools/pages.ts index 0febfe9f0..6c1855ddf 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -21,10 +21,7 @@ export const listPages = defineTool(args => { }, schema: {}, handler: async (_request, response) => { - response.setIncludePages(true); - if (args?.categoryExtensions) { - response.setIncludeExtensionServiceWorkers(true); - } + response.setIncludePages(true, args?.categoryExtensions); }, }; }); From 8ad292e1d3761aefe704ae3ec61b59a6424ef253 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Wed, 25 Feb 2026 13:44:17 +0100 Subject: [PATCH 6/6] chore: pass args to McpResponse --- src/McpResponse.ts | 10 ++++- src/main.ts | 5 ++- src/tools/ToolDefinition.ts | 2 +- src/tools/pages.ts | 2 +- tests/tools/console.test.ts | 5 ++- tests/tools/input.test.ts | 5 ++- tests/tools/pages.test.ts | 80 ++++++++++++++++++++----------------- tests/utils.ts | 4 +- 8 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index c7e154c45..243cd81cf 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type {ParsedArguments} from './cli.js'; import {ConsoleFormatter} from './formatters/ConsoleFormatter.js'; import {IssueFormatter} from './formatters/IssueFormatter.js'; import {NetworkFormatter} from './formatters/NetworkFormatter.js'; @@ -66,6 +67,11 @@ export class McpResponse implements Response { #listExtensions?: boolean; #devToolsData?: DevToolsData; #tabId?: string; + #args: ParsedArguments; + + constructor(args: ParsedArguments) { + this.#args = args; + } attachDevToolsData(data: DevToolsData): void { this.#devToolsData = data; @@ -75,10 +81,10 @@ export class McpResponse implements Response { this.#tabId = tabId; } - setIncludePages(value: boolean, includeServiceWorkers?: boolean): void { + setIncludePages(value: boolean): void { this.#includePages = value; - if (includeServiceWorkers) { + if (this.#args.categoryExtensions) { this.#includeExtensionServiceWorkers = value; } } diff --git a/src/main.ts b/src/main.ts index a8f9b5f2f..e646cc96e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -203,7 +203,10 @@ function registerTool(tool: ToolDefinition): void { const context = await getContext(); logger(`${tool.name} context: resolved`); await context.detectOpenDevToolsWindows(); - const response = args.slim ? new SlimMcpResponse() : new McpResponse(); + const response = args.slim + ? new SlimMcpResponse(args) + : new McpResponse(args); + await tool.handler( { params, diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 52f22c730..b2bea87a1 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -63,7 +63,7 @@ export interface DevToolsData { export interface Response { appendResponseLine(value: string): void; - setIncludePages(value: boolean, shouldIncludeExtension?: boolean): void; + setIncludePages(value: boolean): void; setIncludeNetworkRequests( value: boolean, options?: PaginationOptions & { diff --git a/src/tools/pages.ts b/src/tools/pages.ts index 6c1855ddf..b3afe6192 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -21,7 +21,7 @@ export const listPages = defineTool(args => { }, schema: {}, handler: async (_request, response) => { - response.setIncludePages(true, args?.categoryExtensions); + response.setIncludePages(true); }, }; }); diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index 64ed12dce..813625cd5 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -7,6 +7,7 @@ import assert from 'node:assert'; import {before, describe, it} from 'node:test'; +import type {ParsedArguments} from '../../src/cli.js'; import {loadIssueDescriptions} from '../../src/issue-descriptions.js'; import {McpResponse} from '../../src/McpResponse.js'; import {DevTools} from '../../src/third_party/index.js'; @@ -170,7 +171,7 @@ describe('console', () => { await context.createTextSnapshot(); await issuePromise; await listConsoleMessages.handler({params: {}}, response, context); - const response2 = new McpResponse(); + const response2 = new McpResponse({} as ParsedArguments); await getConsoleMessage.handler( {params: {msgid: 1}}, response2, @@ -225,7 +226,7 @@ describe('console', () => { response, context, ); - const response2 = new McpResponse(); + const response2 = new McpResponse({} as ParsedArguments); await getConsoleMessage.handler( {params: {msgid: id}}, response2, diff --git a/tests/tools/input.test.ts b/tests/tools/input.test.ts index b50d46b0d..bc21df87e 100644 --- a/tests/tools/input.test.ts +++ b/tests/tools/input.test.ts @@ -9,6 +9,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import {describe, it} from 'node:test'; +import type {ParsedArguments} from '../../src/cli.js'; import {McpResponse} from '../../src/McpResponse.js'; import { click, @@ -514,7 +515,7 @@ describe('input', () => { await context.createTextSnapshot(); // Fill email - const response1 = new McpResponse(); + const response1 = new McpResponse({} as ParsedArguments); await fill.handler( { params: { @@ -531,7 +532,7 @@ describe('input', () => { ); // Fill password - const response2 = new McpResponse(); + const response2 = new McpResponse({} as ParsedArguments); await fill.handler( { params: { diff --git a/tests/tools/pages.test.ts b/tests/tools/pages.test.ts index 6a0a9f99f..3a740e250 100644 --- a/tests/tools/pages.test.ts +++ b/tests/tools/pages.test.ts @@ -44,47 +44,53 @@ describe('pages', () => { }); for (const categoryExtensions of [true, false]) { it(`list pages ${categoryExtensions ? 'with' : 'without'} --category-extensions`, async () => { - await withMcpContext(async (response, context) => { - await installExtension.handler( - {params: {path: EXTENSION_PATH}}, - response, - context, - ); + await withMcpContext( + async (response, context) => { + await installExtension.handler( + {params: {path: EXTENSION_PATH}}, + response, + context, + ); - const swTarget = await context.browser.waitForTarget( - t => - t.type() === 'service_worker' && - t.url().includes('chrome-extension://'), - ); - const swUrl = swTarget.url(); + const swTarget = await context.browser.waitForTarget( + t => + t.type() === 'service_worker' && + t.url().includes('chrome-extension://'), + ); + const swUrl = swTarget.url(); - response.resetResponseLineForTesting(); + response.resetResponseLineForTesting(); - const listPageDef = listPages({ - categoryExtensions, - } as ParsedArguments); - await listPageDef.handler({params: {}}, response, context); - - const result = await response.handle(listPageDef.name, context); - const textContent = result.content.find(c => c.type === 'text') as { - type: 'text'; - text: string; - }; - assert.ok(textContent); - - if (categoryExtensions) { - assert.ok(textContent.text.includes(swUrl)); - const structured = result.structuredContent as { - extensionServiceWorkers: Array<{url: string}>; + const listPageDef = listPages({ + categoryExtensions, + } as ParsedArguments); + await listPageDef.handler({params: {}}, response, context); + + const result = await response.handle(listPageDef.name, context); + const textContent = result.content.find(c => c.type === 'text') as { + type: 'text'; + text: string; }; - assert.deepStrictEqual( - structured.extensionServiceWorkers.map(sw => sw.url), - [swUrl], - ); - } else { - assert.ok(!textContent.text.includes(swUrl)); - } - }); + assert.ok(textContent); + + if (categoryExtensions) { + assert.ok(textContent.text.includes(swUrl)); + const structured = result.structuredContent as { + extensionServiceWorkers: Array<{url: string}>; + }; + assert.deepStrictEqual( + structured.extensionServiceWorkers.map(sw => sw.url), + [swUrl], + ); + } else { + assert.ok(!textContent.text.includes(swUrl)); + } + }, + {}, + { + categoryExtensions, + } as ParsedArguments, + ); }); } }); diff --git a/tests/utils.ts b/tests/utils.ts index 977b223dd..d19679811 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -17,6 +17,7 @@ import type { } from 'puppeteer-core'; import sinon from 'sinon'; +import type {ParsedArguments} from '../src/cli.js'; import {McpContext} from '../src/McpContext.js'; import {McpResponse} from '../src/McpResponse.js'; import {stableIdSymbol} from '../src/PageCollector.js'; @@ -85,9 +86,10 @@ export async function withMcpContext( autoOpenDevTools?: boolean; performanceCrux?: boolean; } = {}, + args: ParsedArguments = {} as ParsedArguments, ) { await withBrowser(async browser => { - const response = new McpResponse(); + const response = new McpResponse(args); if (context) { context.dispose(); }