From d6db7801531eb8cfaf63ef1132f31facecf0b22d Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Mon, 20 Apr 2026 22:25:19 +0200 Subject: [PATCH 1/5] feat: implement console logs listener for extension workers --- src/McpContext.ts | 17 +- src/McpResponse.ts | 30 ++- src/PageCollector.ts | 6 +- src/ServiceWorkerCollector.ts | 202 ++++++++++++++++++ src/tools/ToolDefinition.ts | 1 + src/tools/console.ts | 7 + tests/ServiceWorkerCollector.test.ts | 75 +++++++ tests/tools/console.test.js.snapshot | 8 + tests/tools/console.test.ts | 66 +++++- tests/tools/extensions.test.ts | 1 + .../fixtures/extension-logging/manifest.json | 8 + tests/tools/fixtures/extension-logging/sw.js | 6 + tests/tools/script.test.ts | 2 + 13 files changed, 415 insertions(+), 14 deletions(-) create mode 100644 src/ServiceWorkerCollector.ts create mode 100644 tests/ServiceWorkerCollector.test.ts create mode 100644 tests/tools/fixtures/extension-logging/manifest.json create mode 100644 tests/tools/fixtures/extension-logging/sw.js diff --git a/src/McpContext.ts b/src/McpContext.ts index 1aacb391c..a00fd93ae 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -18,6 +18,8 @@ import { type ListenerMap, type UncaughtError, } from './PageCollector.js'; +import {ServiceWorkerConsoleCollector} from './ServiceWorkerCollector.js'; +import type {DevTools} from './third_party/index.js'; import type { Browser, BrowserContext, @@ -31,7 +33,6 @@ import type { Target, Extension, } from './third_party/index.js'; -import type {DevTools} from './third_party/index.js'; import {Locator} from './third_party/index.js'; import {PredefinedNetworkConditions} from './third_party/index.js'; import {listPages} from './tools/pages.js'; @@ -81,6 +82,7 @@ export class McpContext implements Context { #networkCollector: NetworkCollector; #consoleCollector: ConsoleCollector; #devtoolsUniverseManager: UniverseManager; + #serviceWorkerConsoleCollector: ServiceWorkerConsoleCollector; #isRunningTrace = false; #screenRecorderData: {recorder: ScreenRecorder; filePath: string} | null = @@ -125,21 +127,26 @@ export class McpContext implements Context { }, } as ListenerMap; }); + this.#serviceWorkerConsoleCollector = new ServiceWorkerConsoleCollector( + this.browser, + ); this.#devtoolsUniverseManager = new UniverseManager(this.browser); } async #init() { const pages = await this.createPagesSnapshot(); - await this.createExtensionServiceWorkersSnapshot(); + const workers = await this.createExtensionServiceWorkersSnapshot(); await this.#networkCollector.init(pages); await this.#consoleCollector.init(pages); await this.#devtoolsUniverseManager.init(pages); + await this.#serviceWorkerConsoleCollector.init(workers); } dispose() { this.#networkCollector.dispose(); this.#consoleCollector.dispose(); this.#devtoolsUniverseManager.dispose(); + this.#serviceWorkerConsoleCollector.dispose(); for (const mcpPage of this.#mcpPages.values()) { mcpPage.dispose(); } @@ -515,6 +522,12 @@ export class McpContext implements Context { return this.#extensionServiceWorkers; } + getServiceWorkerConsoleData( + extensionId: string, + ): Array { + return this.#serviceWorkerConsoleCollector.getData(extensionId); + } + async createPagesSnapshot(): Promise { const {pages: allPages, isolatedContextNames} = await this.#getAllPages(); diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 0d5f926ec..88d0799a8 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -191,6 +191,7 @@ export class McpResponse implements Response { pagination?: PaginationOptions; types?: string[]; includePreservedMessages?: boolean; + serviceWorkerId?: string; }; #listExtensions?: boolean; #listInPageTools?: boolean; @@ -283,6 +284,7 @@ export class McpResponse implements Response { options?: PaginationOptions & { types?: string[]; includePreservedMessages?: boolean; + serviceWorkerId?: string; }, ): void { if (!value) { @@ -301,6 +303,7 @@ export class McpResponse implements Response { : undefined, types: options?.types, includePreservedMessages: options?.includePreservedMessages, + serviceWorkerId: options?.serviceWorkerId, }; } @@ -547,14 +550,23 @@ export class McpResponse implements Response { let consoleMessages: Array | undefined; if (this.#consoleDataOptions?.include) { - if (!this.#page) { - throw new Error(`Response must have an McpPage`); + let messages; + let page: McpPage | undefined; + + if (this.#consoleDataOptions.serviceWorkerId) { + messages = context.getServiceWorkerConsoleData( + this.#consoleDataOptions.serviceWorkerId, + ); + } else { + page = this.#page; + if (!page) { + throw new Error(`Response must have an McpPage`); + } + messages = context.getConsoleData( + page, + this.#consoleDataOptions.includePreservedMessages, + ); } - const page = this.#page; - let messages = context.getConsoleData( - this.#page, - this.#consoleDataOptions.includePreservedMessages, - ); if (this.#consoleDataOptions.types?.length) { const normalizedTypes = new Set(this.#consoleDataOptions.types); @@ -577,7 +589,9 @@ export class McpResponse implements Response { context.getConsoleMessageStableId(item); if ('args' in item || item instanceof UncaughtError) { const consoleMessage = item as ConsoleMessage | UncaughtError; - const devTools = context.getDevToolsUniverse(page); + const devTools = page + ? context.getDevToolsUniverse(page) + : null; return await ConsoleFormatter.from(consoleMessage, { id: consoleMessageStableId, fetchDetailedData: false, diff --git a/src/PageCollector.ts b/src/PageCollector.ts index 962026491..5b26bf2ac 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -194,11 +194,11 @@ export class PageCollector { const item = this.find(page, item => item[stableIdSymbol] === stableId); - if (item) { - return item; + if (!item) { + throw new Error('Request not found for selected page'); } - throw new Error('Request not found for selected page'); + return item; } find( diff --git a/src/ServiceWorkerCollector.ts b/src/ServiceWorkerCollector.ts new file mode 100644 index 000000000..b6cbb4b3c --- /dev/null +++ b/src/ServiceWorkerCollector.ts @@ -0,0 +1,202 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {UncaughtError} from './PageCollector.js'; +import type { + ConsoleMessage, + WebWorker, + Target, + CDPSession, + Protocol, + Browser, +} from './third_party/index.js'; +import type {ExtensionServiceWorker} from './types.js'; +import type {WithSymbolId} from './utils/id.js'; +import {createIdGenerator, stableIdSymbol} from './utils/id.js'; + +const CHROME_EXTENSION_PREFIX = 'chrome-extension://'; + +export class ServiceWorkerSubscriber { + #target: Target; + #callback: (item: ConsoleMessage | UncaughtError) => void; + #session?: CDPSession; + #worker?: WebWorker; + + constructor( + target: Target, + callback: (item: ConsoleMessage | UncaughtError) => void, + ) { + this.#target = target; + this.#callback = callback; + } + + async subscribe() { + this.#session = await this.#target.createCDPSession(); + await this.#session.send('Runtime.enable'); + this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown); + + this.#worker = (await this.#target.worker()) ?? undefined; + if (this.#worker) { + this.#worker.on('console', this.#onConsole); + } + } + + async unsubscribe() { + if (this.#worker) { + this.#worker.off('console', this.#onConsole); + } + if (this.#session) { + this.#session.off('Runtime.exceptionThrown', this.#onExceptionThrown); + await this.#session.send('Runtime.disable'); + } + } + + #onConsole = (message: ConsoleMessage) => { + this.#callback(message); + }; + + #onExceptionThrown = (event: Protocol.Runtime.ExceptionThrownEvent) => { + const url = this.#target.url(); + + const extensionId = extractExtensionId(url); + + if (extensionId) { + this.#callback(new UncaughtError(event.exceptionDetails, extensionId)); + } + }; +} + +export class ServiceWorkerConsoleCollector { + #storage = new Map< + string, + Array> + >(); + #maxLogs: number; + #browser?: Browser; + #serviceWorkerSubscribers = new Map(); + #idGenerator = createIdGenerator(); + + constructor(browser?: Browser, maxLogs = 1000) { + this.#browser = browser; + this.#maxLogs = maxLogs; + } + + async init(workers: ExtensionServiceWorker[]) { + if (!this.#browser) { + return; + } + this.#browser.on('targetcreated', this.#onTargetCreated); + this.#browser.on('targetdestroyed', this.#onTargetDestroyed); + + for (const worker of workers) { + void this.#onTargetCreated(worker.target); + } + } + + dispose() { + if (!this.#browser) { + return; + } + this.#browser.off('targetcreated', this.#onTargetCreated); + this.#browser.off('targetdestroyed', this.#onTargetDestroyed); + for (const subscriber of this.#serviceWorkerSubscribers.values()) { + void subscriber.unsubscribe(); + } + this.#serviceWorkerSubscribers.clear(); + } + + #onTargetCreated = async (target: Target) => { + if (this.#serviceWorkerSubscribers.has(target)) { + return; + } + const origin = target.url(); + if (target.type() === 'service_worker' && isExtensionOrigin(origin)) { + const extensionId = extractExtensionId(origin); + + if (!extensionId) { + return; + } + + const subscriber = new ServiceWorkerSubscriber(target, item => { + this.addLog(extensionId, item); + }); + void subscriber.subscribe(); + this.#serviceWorkerSubscribers.set(target, subscriber); + } + }; + + #onTargetDestroyed = async (target: Target) => { + const subscriber = this.#serviceWorkerSubscribers.get(target); + if (subscriber) { + void subscriber.unsubscribe(); + this.#serviceWorkerSubscribers.delete(target); + } + }; + + addLog(extensionId: string, log: ConsoleMessage | UncaughtError) { + const logs = this.#storage.get(extensionId) ?? []; + const withId = log as WithSymbolId; + withId[stableIdSymbol] = this.#idGenerator(); + logs.push(withId); + if (logs.length > this.#maxLogs) { + logs.shift(); + } + this.#storage.set(extensionId, logs); + } + + getData( + extensionId: string, + ): Array> { + return this.#storage.get(extensionId) ?? []; + } + + getById( + extensionId: string, + stableId: number, + ): WithSymbolId { + const logs = this.#storage.get(extensionId); + if (!logs) { + throw new Error('No logs found for selected extension'); + } + const item = logs.find(item => item[stableIdSymbol] === stableId); + if (item) { + return item; + } + throw new Error('Log not found for selected extension'); + } + + find( + extensionId: string, + filter: (item: WithSymbolId) => boolean, + ): WithSymbolId | undefined { + const logs = this.#storage.get(extensionId); + if (!logs) { + return; + } + return logs.find(filter); + } + + clearLogs(extensionId: string) { + this.#storage.delete(extensionId); + } +} + +function extractExtensionId(origin: string): string | null { + if (!origin || !isExtensionOrigin(origin)) { + return null; + } + + const pathPart = origin.substring(CHROME_EXTENSION_PREFIX.length); + const slashIndex = pathPart.indexOf('/'); + + // if there's no / it means that pathPart is now the extensionId, otherwise + // we take everything until the first / + return slashIndex === -1 ? pathPart : pathPart.substring(0, slashIndex); +} + +function isExtensionOrigin(origin: string) { + return origin.startsWith(CHROME_EXTENSION_PREFIX); +} diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 1c42915d5..87b4ed81f 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -126,6 +126,7 @@ export interface Response { options?: PaginationOptions & { types?: string[]; includePreservedMessages?: boolean; + serviceWorkerId?: string; }, ): void; includeSnapshot(params?: SnapshotParams): void; diff --git a/src/tools/console.ts b/src/tools/console.ts index dd8059bbf..8e72ffa5c 100644 --- a/src/tools/console.ts +++ b/src/tools/console.ts @@ -75,6 +75,12 @@ export const listConsoleMessages = definePageTool({ .describe( 'Set to true to return the preserved messages over the last 3 navigations.', ), + serviceWorkerId: zod + .string() + .optional() + .describe( + 'The ID of the service worker to list messages for. When omitted, returns messages for the currently selected page.', + ), }, handler: async (request, response) => { response.setIncludeConsoleData(true, { @@ -82,6 +88,7 @@ export const listConsoleMessages = definePageTool({ pageIdx: request.params.pageIdx, types: request.params.types, includePreservedMessages: request.params.includePreservedMessages, + serviceWorkerId: request.params.serviceWorkerId, }); }, }); diff --git a/tests/ServiceWorkerCollector.test.ts b/tests/ServiceWorkerCollector.test.ts new file mode 100644 index 000000000..2417e7a62 --- /dev/null +++ b/tests/ServiceWorkerCollector.test.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; +import {describe, it} from 'node:test'; + +import {UncaughtError} from '../src/PageCollector.js'; +import {ServiceWorkerConsoleCollector} from '../src/ServiceWorkerCollector.js'; +import type {Protocol} from '../src/third_party/index.js'; +import {stableIdSymbol} from '../src/utils/id.js'; + +describe('ServiceWorkerConsoleCollector', () => { + it('limits logs to 1000 per extension', () => { + const collector = new ServiceWorkerConsoleCollector(undefined, 10); + const extensionId = 'test-extension'; + + const mockDetails: Protocol.Runtime.ExceptionDetails = { + exceptionId: 1, + text: 'Error', + lineNumber: 1, + columnNumber: 1, + }; + + for (let i = 0; i < 15; i++) { + const error = new UncaughtError( + {...mockDetails, exceptionId: i}, + extensionId, + ); + collector.addLog(extensionId, error); + } + + const logs = collector.getData(extensionId); + assert.strictEqual(logs.length, 10, 'Should limit logs to 10'); + + const firstLog = logs[0] as UncaughtError; + assert.strictEqual( + firstLog.details.exceptionId, + 5, + 'Oldest log should be Log 5', + ); + + const lastLog = logs[logs.length - 1] as UncaughtError; + assert.strictEqual( + lastLog.details.exceptionId, + 14, + 'Last log should be Log 14', + ); + + const data = collector.getData(extensionId); + assert.strictEqual(data.length, 10, 'getData should return limited logs'); + + const logToFind = data[0]; + const logId = logToFind[stableIdSymbol]; + assert.ok(logId, 'Log should have a stable ID'); + + const foundLog = collector.getById(extensionId, logId); + assert.strictEqual( + foundLog, + logToFind, + 'getById should return correct log', + ); + + const foundViaFind = collector.find(extensionId, item => { + return item[stableIdSymbol] === logId; + }); + assert.strictEqual( + foundViaFind, + logToFind, + 'find should return correct log', + ); + }); +}); diff --git a/tests/tools/console.test.js.snapshot b/tests/tools/console.test.js.snapshot index bc8e82d88..5689f52ba 100644 --- a/tests/tools/console.test.js.snapshot +++ b/tests/tools/console.test.js.snapshot @@ -1,3 +1,11 @@ +exports[`console > captures logs and errors from extension service worker 1`] = ` +## Console messages +Showing 1-3 of 3 (Page 1 of 1). +msgid=1 [log] Service Worker starting... (1 args) +msgid=2 [warn] This is a warning from Service Worker (1 args) +msgid=3 [error] Uncaught Error: Intentional error from Service Worker (0 args) +`; + exports[`console > get_console_message > applies source maps to stack traces of Error object (with cause) console.log arguments 1`] = ` ID: 1 Message: log> foo failed Error: bar failed diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index 26b760db7..d64d76002 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -16,12 +16,76 @@ import { listConsoleMessages, } from '../../src/tools/console.js'; import {serverHooks} from '../server.js'; -import {getTextContent, withMcpContext} from '../utils.js'; +import {extractExtensionId, getTextContent, withMcpContext} from '../utils.js'; +import {installExtension} from '../../src/tools/extensions.js'; +import path from 'node:path'; + +const EXTENSION_LOGGING_PATH = path.join( + import.meta.dirname, + '../../../tests/tools/fixtures/extension-logging', +); describe('console', () => { before(async () => { await loadIssueDescriptions(); }); + + it('captures logs and errors from extension service worker', async t => { + await withMcpContext( + async (response, context) => { + await installExtension.handler( + {params: {path: EXTENSION_LOGGING_PATH}}, + response, + context, + ); + + const extensionId = extractExtensionId(response); + assert.ok(extensionId, 'Extension ID should be returned'); + + // This is important to wait logs from extension. + await new Promise(resolve => setTimeout(resolve, 500)); + + const response2 = new McpResponse({} as ParsedArguments); + + await listConsoleMessages.handler( + { + params: {serviceWorkerId: extensionId}, + page: context.getSelectedMcpPage(), + }, + response2, + context, + ); + + const formattedResponse = await response2.handle('test', context); + const textContent = getTextContent(formattedResponse.content[0]); + + const sanitizedText = textContent.replaceAll( + new RegExp(extensionId, 'g'), + '', + ); + + t.assert.snapshot?.(sanitizedText); + + assert.ok( + sanitizedText.includes('Service Worker starting...'), + 'Should contain start log', + ); + assert.ok( + sanitizedText.includes('This is a warning from Service Worker'), + 'Should contain warning log', + ); + assert.ok( + sanitizedText.includes('Intentional error from Service Worker'), + 'Should contain error log', + ); + }, + {}, + { + categoryExtensions: true, + } as ParsedArguments, + ); + }); + describe('list_console_messages', () => { it('list messages', async () => { await withMcpContext(async (response, context) => { diff --git a/tests/tools/extensions.test.ts b/tests/tools/extensions.test.ts index c7374ea2d..bc2d2bdd7 100644 --- a/tests/tools/extensions.test.ts +++ b/tests/tools/extensions.test.ts @@ -128,6 +128,7 @@ describe('extension', () => { assert.ok(list.length === 1, 'List should have only one extension'); const reinstalled = list.find(e => e.id === extensionId); assert.ok(reinstalled, 'Extension should be present after reload'); + await context.uninstallExtension(extensionId!); }, {}, { diff --git a/tests/tools/fixtures/extension-logging/manifest.json b/tests/tools/fixtures/extension-logging/manifest.json new file mode 100644 index 000000000..a11cf0ac2 --- /dev/null +++ b/tests/tools/fixtures/extension-logging/manifest.json @@ -0,0 +1,8 @@ +{ + "manifest_version": 3, + "name": "Test Extension for Logging", + "version": "1.0", + "background": { + "service_worker": "sw.js" + } +} diff --git a/tests/tools/fixtures/extension-logging/sw.js b/tests/tools/fixtures/extension-logging/sw.js new file mode 100644 index 000000000..c3290dfcd --- /dev/null +++ b/tests/tools/fixtures/extension-logging/sw.js @@ -0,0 +1,6 @@ +console.log('Service Worker starting...'); +console.warn('This is a warning from Service Worker'); + +setTimeout(() => { + throw new Error('Intentional error from Service Worker'); +}, 100); diff --git a/tests/tools/script.test.ts b/tests/tools/script.test.ts index b0d57678a..7cded6e8b 100644 --- a/tests/tools/script.test.ts +++ b/tests/tools/script.test.ts @@ -296,6 +296,8 @@ describe('script', () => { const swId = context.getExtensionServiceWorkerId(sw); + await context.triggerExtensionAction(extensionId); + response.resetResponseLineForTesting(); await evaluateScript({ categoryExtensions: true, From bf441fbec617df595d840f2c789850ff1a3727c7 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Mon, 20 Apr 2026 22:35:51 +0200 Subject: [PATCH 2/5] chore: make serviceWorkerId present only under categoryExtension --- src/tools/console.ts | 114 +++++++++++++++++++----------------- tests/tools/console.test.ts | 26 ++++---- 2 files changed, 74 insertions(+), 66 deletions(-) diff --git a/src/tools/console.ts b/src/tools/console.ts index 8e72ffa5c..0f8ca007a 100644 --- a/src/tools/console.ts +++ b/src/tools/console.ts @@ -37,60 +37,66 @@ const FILTERABLE_MESSAGE_TYPES: [ 'issue', ]; -export const listConsoleMessages = definePageTool({ - name: 'list_console_messages', - description: - 'List all console messages for the currently selected page since the last navigation.', - annotations: { - category: ToolCategory.DEBUGGING, - readOnlyHint: true, - }, - schema: { - pageSize: zod - .number() - .int() - .positive() - .optional() - .describe( - 'Maximum number of messages to return. When omitted, returns all messages.', - ), - pageIdx: zod - .number() - .int() - .min(0) - .optional() - .describe( - 'Page number to return (0-based). When omitted, returns the first page.', - ), - types: zod - .array(zod.enum(FILTERABLE_MESSAGE_TYPES)) - .optional() - .describe( - 'Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.', - ), - includePreservedMessages: zod - .boolean() - .default(false) - .optional() - .describe( - 'Set to true to return the preserved messages over the last 3 navigations.', - ), - serviceWorkerId: zod - .string() - .optional() - .describe( - 'The ID of the service worker to list messages for. When omitted, returns messages for the currently selected page.', - ), - }, - handler: async (request, response) => { - response.setIncludeConsoleData(true, { - pageSize: request.params.pageSize, - pageIdx: request.params.pageIdx, - types: request.params.types, - includePreservedMessages: request.params.includePreservedMessages, - serviceWorkerId: request.params.serviceWorkerId, - }); - }, +export const listConsoleMessages = definePageTool(cliArgs => { + return { + name: 'list_console_messages', + description: + 'List all console messages for the currently selected page since the last navigation.', + annotations: { + category: ToolCategory.DEBUGGING, + readOnlyHint: true, + }, + schema: { + pageSize: zod + .number() + .int() + .positive() + .optional() + .describe( + 'Maximum number of messages to return. When omitted, returns all messages.', + ), + pageIdx: zod + .number() + .int() + .min(0) + .optional() + .describe( + 'Page number to return (0-based). When omitted, returns the first page.', + ), + types: zod + .array(zod.enum(FILTERABLE_MESSAGE_TYPES)) + .optional() + .describe( + 'Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.', + ), + includePreservedMessages: zod + .boolean() + .default(false) + .optional() + .describe( + 'Set to true to return the preserved messages over the last 3 navigations.', + ), + ...(cliArgs?.categoryExtensions + ? { + serviceWorkerId: zod + .string() + .optional() + .describe( + `The ID of the service worker to list messages for. When omitted, returns messages for the currently selected page.`, + ), + } + : {}), + }, + handler: async (request, response) => { + response.setIncludeConsoleData(true, { + pageSize: request.params.pageSize, + pageIdx: request.params.pageIdx, + types: request.params.types, + includePreservedMessages: request.params.includePreservedMessages, + serviceWorkerId: request.params.serviceWorkerId, + }); + }, + }; }); export const getConsoleMessage = definePageTool({ diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index d64d76002..6e17f266d 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -5,6 +5,7 @@ */ import assert from 'node:assert'; +import path from 'node:path'; import {before, describe, it} from 'node:test'; import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js'; @@ -15,10 +16,9 @@ import { getConsoleMessage, listConsoleMessages, } from '../../src/tools/console.js'; +import {installExtension} from '../../src/tools/extensions.js'; import {serverHooks} from '../server.js'; import {extractExtensionId, getTextContent, withMcpContext} from '../utils.js'; -import {installExtension} from '../../src/tools/extensions.js'; -import path from 'node:path'; const EXTENSION_LOGGING_PATH = path.join( import.meta.dirname, @@ -47,7 +47,9 @@ describe('console', () => { const response2 = new McpResponse({} as ParsedArguments); - await listConsoleMessages.handler( + await listConsoleMessages({ + categoryExtensions: true, + } as ParsedArguments).handler( { params: {serviceWorkerId: extensionId}, page: context.getSelectedMcpPage(), @@ -89,7 +91,7 @@ describe('console', () => { describe('list_console_messages', () => { it('list messages', async () => { await withMcpContext(async (response, context) => { - await listConsoleMessages.handler( + await listConsoleMessages().handler( {params: {}, page: context.getSelectedMcpPage()}, response, context, @@ -104,7 +106,7 @@ describe('console', () => { await page.pptrPage.setContent( '', ); - await listConsoleMessages.handler( + await listConsoleMessages().handler( {params: {}, page: context.getSelectedMcpPage()}, response, context, @@ -121,7 +123,7 @@ describe('console', () => { await page.pptrPage.setContent( '', ); - await listConsoleMessages.handler( + await listConsoleMessages().handler( {params: {}, page: context.getSelectedMcpPage()}, response, context, @@ -136,7 +138,7 @@ describe('console', () => { await withMcpContext(async (response, context) => { const page = context.getSelectedMcpPage(); await page.pptrPage.setContent(''); - await listConsoleMessages.handler( + await listConsoleMessages().handler( {params: {}, page: context.getSelectedMcpPage()}, response, context, @@ -160,7 +162,7 @@ describe('console', () => { '', ); await issuePromise; - await listConsoleMessages.handler( + await listConsoleMessages().handler( {params: {}, page: context.getSelectedMcpPage()}, response, context, @@ -189,7 +191,7 @@ describe('console', () => { '', ); await issuePromise; - await listConsoleMessages.handler( + await listConsoleMessages().handler( {params: {}, page: context.getSelectedMcpPage()}, response, context, @@ -238,7 +240,7 @@ describe('console', () => { '', ); // The list is needed to populate the console messages in the context. - await listConsoleMessages.handler( + await listConsoleMessages().handler( {params: {}, page: context.getSelectedMcpPage()}, response, context, @@ -271,7 +273,7 @@ describe('console', () => { ); await context.createTextSnapshot(page); await issuePromise; - await listConsoleMessages.handler( + await listConsoleMessages().handler( {params: {}, page: context.getSelectedMcpPage()}, response, context, @@ -327,7 +329,7 @@ describe('console', () => { assert.ok(issueMsg); const id = context.getConsoleMessageStableId(issueMsg); assert.ok(id); - await listConsoleMessages.handler( + await listConsoleMessages().handler( {params: {types: ['issue']}, page: context.getSelectedMcpPage()}, response, context, From 27831ff9a4dbc3319f74dd058c65c9ebc06a1e29 Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Mon, 20 Apr 2026 22:39:31 +0200 Subject: [PATCH 3/5] docs: first doc update --- docs/tool-reference.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 74fa725fa..0978c2827 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -1,6 +1,6 @@ -# Chrome DevTools MCP Tool Reference (~7005 cl100k_base tokens) +# Chrome DevTools MCP Tool Reference (~7003 cl100k_base tokens) - **[Input automation](#input-automation)** (9 tools) - [`click`](#click) @@ -314,12 +314,12 @@ so returned values have to be JSON-serializable. **Parameters:** - **function** (string) **(required)**: A JavaScript function declaration to be executed by the tool in the currently selected page. - Example without arguments: `() => { +Example without arguments: `() => { return document.title }` or `async () => { return await fetch("example.com") }`. - Example with arguments: `(el) => { +Example with arguments: `(el) => { return el.innerText; }` @@ -330,7 +330,7 @@ so returned values have to be JSON-serializable. ### `get_console_message` -**Description:** Gets a console message by its ID. You can get all messages by calling [`list_console_messages`](#list_console_messages). +**Description:** Gets a console message by its ID. You can get all messages by calling . **Parameters:** From 37e1f8a1a6414d56bdd14d4e3bec3cbe58dced9f Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Tue, 21 Apr 2026 09:42:54 +0200 Subject: [PATCH 4/5] chore: improve tests --- tests/tools/console.test.ts | 28 +++++++++++++++++++- tests/tools/fixtures/extension-logging/sw.js | 7 +---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index 6e17f266d..c9f60fc83 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -42,10 +42,36 @@ describe('console', () => { const extensionId = extractExtensionId(response); assert.ok(extensionId, 'Extension ID should be returned'); + const swTarget = await context.browser.waitForTarget( + t => t.type() === 'service_worker' && t.url().includes(extensionId), + ); + + const swList = await context.createExtensionServiceWorkersSnapshot(); + const sw = swList.find(s => s.target === swTarget); + if (!sw) { + assert.fail('Service worker not found in context list'); + } + const swId = context.getExtensionServiceWorkerId(sw); + + const response2 = new McpResponse({} as ParsedArguments); + + await context.triggerExtensionAction(extensionId); + const worker = await swTarget.worker(); + + await worker?.evaluate( + ` + console.log('Service Worker starting...'); + console.warn('This is a warning from Service Worker'); + setTimeout(() => { + throw new Error('Intentional error from Service Worker'); + }, 100); + `, + ); + // This is important to wait logs from extension. await new Promise(resolve => setTimeout(resolve, 500)); - const response2 = new McpResponse({} as ParsedArguments); + response2.resetResponseLineForTesting(); await listConsoleMessages({ categoryExtensions: true, diff --git a/tests/tools/fixtures/extension-logging/sw.js b/tests/tools/fixtures/extension-logging/sw.js index c3290dfcd..e4ddc27a8 100644 --- a/tests/tools/fixtures/extension-logging/sw.js +++ b/tests/tools/fixtures/extension-logging/sw.js @@ -1,6 +1 @@ -console.log('Service Worker starting...'); -console.warn('This is a warning from Service Worker'); - -setTimeout(() => { - throw new Error('Intentional error from Service Worker'); -}, 100); +// Minimal service worker for testing From f938b25beb770983248c8163053a17d2077df99a Mon Sep 17 00:00:00 2001 From: Nicholas Roscino Date: Tue, 21 Apr 2026 10:22:54 +0200 Subject: [PATCH 5/5] chore: attempt test fix on windows --- tests/tools/console.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index c9f60fc83..a0932ccc4 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -62,7 +62,7 @@ describe('console', () => { ` console.log('Service Worker starting...'); console.warn('This is a warning from Service Worker'); - setTimeout(() => { + globalThis.setTimeout(() => { throw new Error('Intentional error from Service Worker'); }, 100); `,