From 03e6a826704fc09fde897ec8a2c1c0f8adbfa9ac Mon Sep 17 00:00:00 2001 From: alinavarkki Date: Tue, 14 Oct 2025 20:14:50 +0200 Subject: [PATCH 1/5] add filters and pagination to the console messages tool --- src/McpResponse.ts | 87 ++++++++++++++------ src/formatters/consoleFormatter.ts | 85 ++++++-------------- src/tools/ToolDefinition.ts | 5 +- src/tools/console.ts | 63 ++++++++++++++- tests/McpResponse.test.ts | 3 +- tests/formatters/consoleFormatter.test.ts | 97 +++++------------------ tests/tools/console.test.ts | 13 +++ 7 files changed, 188 insertions(+), 165 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 4ea9f796..c728c3b3 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -33,15 +33,18 @@ export class McpResponse implements Response { #includePages = false; #includeSnapshot = false; #attachedNetworkRequestData?: NetworkRequestData; - #includeConsoleData = false; #textResponseLines: string[] = []; - #formattedConsoleData?: string[]; #images: ImageContentData[] = []; #networkRequestsOptions?: { include: boolean; pagination?: PaginationOptions; resourceTypes?: ResourceType[]; }; + #consoleDataOptions?: { + include: boolean; + pagination?: PaginationOptions; + types?: string[]; + }; setIncludePages(value: boolean): void { this.#includePages = value; @@ -77,8 +80,30 @@ export class McpResponse implements Response { }; } - setIncludeConsoleData(value: boolean): void { - this.#includeConsoleData = value; + setIncludeConsoleData( + value: boolean, + options?: { + pageSize?: number; + pageIdx?: number; + types?: string[]; + }, + ): void { + if (!value) { + this.#consoleDataOptions = undefined; + return; + } + + this.#consoleDataOptions = { + include: value, + pagination: + options?.pageSize || options?.pageIdx + ? { + pageSize: options.pageSize, + pageIdx: options.pageIdx, + } + : undefined, + types: options?.types, + }; } attachNetworkRequest(url: string): void { @@ -96,7 +121,7 @@ export class McpResponse implements Response { } get includeConsoleData(): boolean { - return this.#includeConsoleData; + return this.#consoleDataOptions?.include ?? false; } get attachedNetworkRequestUrl(): string | undefined { return this.#attachedNetworkRequestData?.networkRequestUrl; @@ -104,6 +129,12 @@ export class McpResponse implements Response { get networkRequestsPageIdx(): number | undefined { return this.#networkRequestsOptions?.pagination?.pageIdx; } + get consoleMessagesPageIdx(): number | undefined { + return this.#consoleDataOptions?.pagination?.pageIdx; + } + get consoleMessagesTypes(): string[] | undefined { + return this.#consoleDataOptions?.types; + } appendResponseLine(value: string): void { this.#textResponseLines.push(value); @@ -136,8 +167,6 @@ export class McpResponse implements Response { await context.createTextSnapshot(); } - let formattedConsoleMessages: string[]; - if (this.#attachedNetworkRequestData?.networkRequestUrl) { const request = context.getNetworkRequestByUrl( this.#attachedNetworkRequestData.networkRequestUrl, @@ -153,23 +182,13 @@ export class McpResponse implements Response { } } - if (this.#includeConsoleData) { - const consoleMessages = context.getConsoleData(); - if (consoleMessages) { - formattedConsoleMessages = await Promise.all( - consoleMessages.map(message => formatConsoleEvent(message)), - ); - this.#formattedConsoleData = formattedConsoleMessages; - } - } - - return this.format(toolName, context); + return await this.format(toolName, context); } - format( + async format( toolName: string, context: McpContext, - ): Array { + ): Promise> { const response = [`# ${toolName} response`]; for (const line of this.#textResponseLines) { response.push(line); @@ -253,10 +272,32 @@ Call ${handleDialog.name} to handle it before continuing.`); } } - if (this.#includeConsoleData && this.#formattedConsoleData) { + if (this.#consoleDataOptions?.include) { + let messages = context.getConsoleData(); + + if (this.#consoleDataOptions.types?.length) { + const normalizedTypes = new Set(this.#consoleDataOptions.types); + messages = messages.filter(message => { + if (!('type' in message)) { + return normalizedTypes.has('error'); + } + const type = message.type(); + return normalizedTypes.has(type); + }); + } + response.push('## Console messages'); - if (this.#formattedConsoleData.length) { - response.push(...this.#formattedConsoleData); + if (messages.length) { + const data = this.#dataWithPagination( + messages, + this.#consoleDataOptions.pagination, + ); + response.push(...data.info); + response.push( + ...(await Promise.all( + data.items.map(message => formatConsoleEvent(message)), + )), + ); } else { response.push(''); } diff --git a/src/formatters/consoleFormatter.ts b/src/formatters/consoleFormatter.ts index b6627496..6b74b0b5 100644 --- a/src/formatters/consoleFormatter.ts +++ b/src/formatters/consoleFormatter.ts @@ -4,11 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { - ConsoleMessage, - JSHandle, - ConsoleMessageLocation, -} from 'puppeteer-core'; +import type {ConsoleMessage, JSHandle} from 'puppeteer-core'; const logLevels: Record = { log: 'Log', @@ -31,66 +27,37 @@ export async function formatConsoleEvent( async function formatConsoleMessage(msg: ConsoleMessage): Promise { const logLevel = logLevels[msg.type()]; + const text = msg.text(); const args = msg.args(); - if (logLevel === 'Error') { - let message = `${logLevel}> `; - if (msg.text() === 'JSHandle@error') { - const errorHandle = args[0] as JSHandle; - message += await errorHandle - .evaluate(error => { - return error.toString(); - }) - .catch(() => { - return 'Error occurred'; - }); - void errorHandle.dispose().catch(); + const formattedArgs = await formatArgs(args, text); + return `${logLevel}> ${text} ${formattedArgs}`.trim(); +} - const formattedArgs = await formatArgs(args.slice(1)); - if (formattedArgs) { - message += ` ${formattedArgs}`; - } - } else { - message += msg.text(); - const formattedArgs = await formatArgs(args); - if (formattedArgs) { - message += ` ${formattedArgs}`; - } - for (const frame of msg.stackTrace()) { - message += '\n' + formatStackFrame(frame); - } - } - return message; +// Only includes the first arg and indicates that there are more args +async function formatArgs( + args: readonly JSHandle[], + messageText: string, +): Promise { + if (args.length === 0) { + return ''; } - const formattedArgs = await formatArgs(args); - const text = msg.text(); - - return `${logLevel}> ${formatStackFrame( - msg.location(), - )}: ${text} ${formattedArgs}`.trim(); -} - -async function formatArgs(args: readonly JSHandle[]): Promise { - const argValues = await Promise.all( - args.map(arg => - arg.jsonValue().catch(() => { - // Ignore errors - }), - ), - ); + let formattedArgs = ''; + const firstArg = await args[0].jsonValue().catch(() => { + // Ignore errors + }); - return argValues - .map(value => { - return typeof value === 'object' ? JSON.stringify(value) : String(value); - }) - .join(' '); -} + if (firstArg !== messageText) { + formattedArgs += + typeof firstArg === 'object' + ? JSON.stringify(firstArg) + : String(firstArg); + } -function formatStackFrame(stackFrame: ConsoleMessageLocation): string { - if (!stackFrame?.url) { - return ''; + if (args.length > 1) { + return `${formattedArgs} ...`; } - const filename = stackFrame.url.replace(/^.*\//, ''); - return `${filename}:${stackFrame.lineNumber}:${stackFrame.columnNumber}`; + + return formattedArgs; } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 56fdb53a..7952fc56 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -47,7 +47,10 @@ export interface Response { value: boolean, options?: {pageSize?: number; pageIdx?: number; resourceTypes?: string[]}, ): void; - setIncludeConsoleData(value: boolean): void; + setIncludeConsoleData( + value: boolean, + options?: {pageSize?: number; pageIdx?: number; types?: string[]}, + ): void; setIncludeSnapshot(value: boolean): void; attachImage(value: ImageContentData): void; attachNetworkRequest(url: string): void; diff --git a/src/tools/console.ts b/src/tools/console.ts index 4fb752f2..47a1ed17 100644 --- a/src/tools/console.ts +++ b/src/tools/console.ts @@ -4,19 +4,74 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type {ConsoleMessageType} from 'puppeteer-core'; +import z from 'zod'; + import {ToolCategories} from './categories.js'; import {defineTool} from './ToolDefinition.js'; +const FILTERABLE_MESSAGE_TYPES: readonly [ + ConsoleMessageType, + ...ConsoleMessageType[], +] = [ + 'log', + 'debug', + 'info', + 'error', + 'warn', + 'dir', + 'dirxml', + 'table', + 'trace', + 'clear', + 'startGroup', + 'startGroupCollapsed', + 'endGroup', + 'assert', + 'profile', + 'profileEnd', + 'count', + 'timeEnd', + 'verbose', +]; + export const consoleTool = defineTool({ name: 'list_console_messages', description: 'List all console messages for the currently selected page since the last navigation.', annotations: { - category: ToolCategories.DEBUGGING, + category: ToolCategories.NETWORK, readOnlyHint: true, }, - schema: {}, - handler: async (_request, response) => { - response.setIncludeConsoleData(true); + schema: { + pageSize: z + .number() + .int() + .positive() + .optional() + .describe( + 'Maximum number of messages to return. When omitted, returns all requests.', + ), + pageIdx: z + .number() + .int() + .min(0) + .optional() + .describe( + 'Page number to return (0-based). When omitted, returns the first page.', + ), + types: z + .array(z.enum(FILTERABLE_MESSAGE_TYPES)) + .optional() + .describe( + 'Filter messages to only return messages of the specified resource types. When omitted or empty, returns all messages.', + ), + }, + handler: async (request, response) => { + response.setIncludeConsoleData(true, { + pageSize: request.params.pageSize, + pageIdx: request.params.pageIdx, + types: request.params.types, + }); }, }); diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 11416447..67822a68 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -312,8 +312,7 @@ http://example.com GET [pending]`, // Cannot check the full text because it contains local file path assert.ok( result[0].text.toString().startsWith(`# test response -## Console messages -Log>`), +## Console messages`), ); assert.ok(result[0].text.toString().includes('Hello from the test')); }); diff --git a/tests/formatters/consoleFormatter.test.ts b/tests/formatters/consoleFormatter.test.ts index 4fd6213c..7f34d1c3 100644 --- a/tests/formatters/consoleFormatter.test.ts +++ b/tests/formatters/consoleFormatter.test.ts @@ -65,17 +65,22 @@ describe('consoleFormatter', () => { const message = getMockConsoleMessage({ type: 'log', text: 'Hello, world!', - location: { - url: 'http://example.com/script.js', - lineNumber: 10, - columnNumber: 5, - }, }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Log> script.js:10:5: Hello, world!'); + assert.equal(result, 'Log> Hello, world!'); }); - it('formats a console.log message with arguments', async () => { + it('formats a console.log message with one argument', async () => { + const message = getMockConsoleMessage({ + type: 'log', + text: 'Processing file:', + args: ['file.txt'], + }); + const result = await formatConsoleEvent(message); + assert.equal(result, 'Log> Processing file: file.txt'); + }); + + it('formats a console.log message with multiple arguments', async () => { const message = getMockConsoleMessage({ type: 'log', text: 'Processing file:', @@ -87,10 +92,7 @@ describe('consoleFormatter', () => { }, }); const result = await formatConsoleEvent(message); - assert.equal( - result, - 'Log> script.js:10:5: Processing file: file.txt {"id":1,"status":"done"}', - ); + assert.equal(result, 'Log> Processing file: file.txt ...'); }); it('formats a console.error message', async () => { @@ -102,76 +104,42 @@ describe('consoleFormatter', () => { assert.equal(result, 'Error> Something went wrong'); }); - it('formats a console.error message with arguments', async () => { + it('formats a console.error message with one argument', async () => { const message = getMockConsoleMessage({ type: 'error', text: 'Something went wrong:', args: ['details', {code: 500}], }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Error> Something went wrong: details {"code":500}'); + assert.equal(result, 'Error> Something went wrong: details'); }); - it('formats a console.error message with a stack trace', async () => { + it('formats a console.error message with multiple arguments', async () => { const message = getMockConsoleMessage({ type: 'error', - text: 'Something went wrong', - stackTrace: [ - { - url: 'http://example.com/script.js', - lineNumber: 10, - columnNumber: 5, - }, - { - url: 'http://example.com/script2.js', - lineNumber: 20, - columnNumber: 10, - }, - ], - }); - const result = await formatConsoleEvent(message); - assert.equal( - result, - 'Error> Something went wrong\nscript.js:10:5\nscript2.js:20:10', - ); - }); - - it('formats a console.error message with a JSHandle@error', async () => { - const message = getMockConsoleMessage({ - type: 'error', - text: 'JSHandle@error', - args: [new Error('mock stack')], + text: 'Something went wrong:', + args: ['details', {code: 500}], }); const result = await formatConsoleEvent(message); - assert.ok(result.startsWith('Error> Error: mock stack')); + assert.equal(result, 'Error> Something went wrong: details ...'); }); it('formats a console.warn message', async () => { const message = getMockConsoleMessage({ type: 'warning', text: 'This is a warning', - location: { - url: 'http://example.com/script.js', - lineNumber: 10, - columnNumber: 5, - }, }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Warning> script.js:10:5: This is a warning'); + assert.equal(result, 'Warning> This is a warning'); }); it('formats a console.info message', async () => { const message = getMockConsoleMessage({ type: 'info', text: 'This is an info message', - location: { - url: 'http://example.com/script.js', - lineNumber: 10, - columnNumber: 5, - }, }); const result = await formatConsoleEvent(message); - assert.equal(result, 'Info> script.js:10:5: This is an info message'); + assert.equal(result, 'Info> This is an info message'); }); it('formats a page error', async () => { @@ -187,28 +155,5 @@ describe('consoleFormatter', () => { const result = await formatConsoleEvent(error); assert.equal(result, 'Error: Page crashed'); }); - - it('formats a console.log message from a removed iframe - no location', async () => { - const message = getMockConsoleMessage({ - type: 'log', - text: 'Hello from iframe', - location: {}, - }); - const result = await formatConsoleEvent(message); - assert.equal(result, 'Log> : Hello from iframe'); - }); - - it('formats a console.log message from a removed iframe with partial location', async () => { - const message = getMockConsoleMessage({ - type: 'log', - text: 'Hello from iframe', - location: { - lineNumber: 10, - columnNumber: 5, - }, - }); - const result = await formatConsoleEvent(message); - assert.equal(result, 'Log> : Hello from iframe'); - }); }); }); diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index b25ef15b..170b7959 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -17,5 +17,18 @@ describe('console', () => { assert.ok(response.includeConsoleData); }); }); + + it('lists error messages', async () => { + await withBrowser(async (response, context) => { + const page = await context.newPage(); + await page.setContent( + '', + ); + await consoleTool.handler({params: {}}, response, context); + const formattedResponse = await response.format('test', context); + const textContent = formattedResponse[0] as {text: string}; + assert.ok(textContent.text.includes('This is an error')); + }); + }); }); }); From a37c9fdfcbe5dc08687fbd399366f8271d9eb179 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:45:16 +0200 Subject: [PATCH 2/5] feat: support stable id for network requests (#375) This is part 1, we just expose the uid here, in a follow up PR we will be used for finding the request. --- src/McpContext.ts | 4 +++ src/McpResponse.ts | 9 ++++-- src/PageCollector.ts | 30 +++++++++++++++--- src/formatters/networkFormatter.ts | 8 +++-- tests/McpResponse.test.ts | 37 ++++++++++++----------- tests/PageCollector.test.ts | 25 ++++++++++++++- tests/formatters/networkFormatter.test.ts | 24 +++++++-------- tests/utils.ts | 5 ++- 8 files changed, 102 insertions(+), 40 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 4cf6b661..6bf2fa20 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -426,4 +426,8 @@ export class McpContext implements Context { ); return waitForHelper.waitForEventsAfterAction(action); } + + getNetworkRequestStableId(request: HTTPRequest): number { + return this.#networkCollector.getIdForResource(request); + } } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index c728c3b3..6653452c 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -265,7 +265,12 @@ Call ${handleDialog.name} to handle it before continuing.`); ); response.push(...data.info); for (const request of data.items) { - response.push(getShortDescriptionForRequest(request)); + response.push( + getShortDescriptionForRequest( + request, + context.getNetworkRequestStableId(request), + ), + ); } } else { response.push('No requests found.'); @@ -388,7 +393,7 @@ Call ${handleDialog.name} to handle it before continuing.`); let indent = 0; for (const request of redirectChain.reverse()) { response.push( - `${' '.repeat(indent)}${getShortDescriptionForRequest(request)}`, + `${' '.repeat(indent)}${getShortDescriptionForRequest(request, context.getNetworkRequestStableId(request))}`, ); indent++; } diff --git a/src/PageCollector.ts b/src/PageCollector.ts index acbf3239..fa10c215 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -17,6 +17,21 @@ export type ListenerMap = { [K in keyof EventMap]?: (event: EventMap[K]) => void; }; +function createIdGenerator() { + let i = 1; + return () => { + if (i === Number.MAX_SAFE_INTEGER) { + i = 0; + } + return i++; + }; +} + +export const stableIdSymbol = Symbol('stableIdSymbol'); +type WithSymbolId = T & { + [stableIdSymbol]?: number; +}; + export class PageCollector { #browser: Browser; #listenersInitializer: ( @@ -28,7 +43,7 @@ export class PageCollector { * As we use the reference to it. * Use methods that manipulate the array in place. */ - protected storage = new WeakMap(); + protected storage = new WeakMap>>(); constructor( browser: Browser, @@ -56,7 +71,6 @@ export class PageCollector { if (!page) { return; } - console.log('destro'); this.#cleanupPageDestroyed(page); }); } @@ -70,10 +84,14 @@ export class PageCollector { return; } - const stored: T[] = []; + const idGenerator = createIdGenerator(); + const stored: Array> = []; this.storage.set(page, stored); + const listeners = this.#listenersInitializer(value => { - stored.push(value); + const withId = value as WithSymbolId; + withId[stableIdSymbol] = idGenerator(); + stored.push(withId); }); listeners['framenavigated'] = (frame: Frame) => { // Only reset the storage on main frame navigation @@ -111,6 +129,10 @@ export class PageCollector { getData(page: Page): T[] { return this.storage.get(page) ?? []; } + + getIdForResource(resource: WithSymbolId): number { + return resource[stableIdSymbol] ?? -1; + } } export class NetworkCollector extends PageCollector { diff --git a/src/formatters/networkFormatter.ts b/src/formatters/networkFormatter.ts index 7796f01a..43be8ae6 100644 --- a/src/formatters/networkFormatter.ts +++ b/src/formatters/networkFormatter.ts @@ -10,8 +10,12 @@ import type {HTTPRequest, HTTPResponse} from 'puppeteer-core'; const BODY_CONTEXT_SIZE_LIMIT = 10000; -export function getShortDescriptionForRequest(request: HTTPRequest): string { - return `${request.url()} ${request.method()} ${getStatusFromRequest(request)}`; +export function getShortDescriptionForRequest( + request: HTTPRequest, + id: number, +): string { + // TODO truncate the URL + return `reqid ${id} - ${request.url()} ${request.method()} ${getStatusFromRequest(request)}`; } export function getStatusFromRequest(request: HTTPRequest): string { diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 67822a68..69cc56b8 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -202,15 +202,16 @@ Call handle_dialog to handle it before continuing.`, await withBrowser(async (response, context) => { response.setIncludeNetworkRequests(true); context.getNetworkRequests = () => { - return [getMockRequest()]; + return [getMockRequest({stableId: 1}), getMockRequest({stableId: 2})]; }; const result = await response.handle('test', context); assert.strictEqual( result[0].text, `# test response ## Network requests -Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +Showing 1-2 of 2 (Page 1 of 1). +reqid 1 - http://example.com GET [pending] +reqid 2 - http://example.com GET [pending]`, ); }); }); @@ -266,7 +267,7 @@ ${JSON.stringify({request: 'body'})} ${JSON.stringify({response: 'body'})} ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com POST [success - 200]`, +reqid 1 - http://example.com POST [success - 200]`, ); }); }); @@ -289,7 +290,7 @@ Status: [pending] - content-size:10 ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -353,8 +354,8 @@ describe('McpResponse network request filtering', () => { `# test response ## Network requests Showing 1-2 of 2 (Page 1 of 1). -http://example.com GET [pending] -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -377,7 +378,7 @@ http://example.com GET [pending]`, `# test response ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -422,11 +423,11 @@ No requests found.`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -451,11 +452,11 @@ http://example.com GET [pending]`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending]`, ); }); }); diff --git a/tests/PageCollector.test.ts b/tests/PageCollector.test.ts index 43c82380..431687aa 100644 --- a/tests/PageCollector.test.ts +++ b/tests/PageCollector.test.ts @@ -6,7 +6,7 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; -import type {Browser, Frame, Page, Target} from 'puppeteer-core'; +import type {Browser, Frame, HTTPRequest, Page, Target} from 'puppeteer-core'; import type {ListenerMap} from '../src/PageCollector.js'; import {PageCollector} from '../src/PageCollector.js'; @@ -196,4 +196,27 @@ describe('PageCollector', () => { assert.equal(collector.getData(page).length, 0); }); + + it('should assign ids to requests', async () => { + const browser = getMockBrowser(); + const page = (await browser.pages())[0]; + const request1 = getMockRequest(); + const request2 = getMockRequest(); + const collector = new PageCollector(browser, collect => { + return { + request: req => { + collect(req); + }, + } as ListenerMap; + }); + await collector.init(); + + page.emit('request', request1); + page.emit('request', request2); + + assert.equal(collector.getData(page).length, 2); + + assert.equal(collector.getIdForResource(request1), 1); + assert.equal(collector.getIdForResource(request2), 2); + }); }); diff --git a/tests/formatters/networkFormatter.test.ts b/tests/formatters/networkFormatter.test.ts index 23c8a323..7f2fddc4 100644 --- a/tests/formatters/networkFormatter.test.ts +++ b/tests/formatters/networkFormatter.test.ts @@ -21,40 +21,40 @@ describe('networkFormatter', () => { describe('getShortDescriptionForRequest', () => { it('works', async () => { const request = getMockRequest(); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [pending]'); + assert.equal(result, 'reqid 1 - http://example.com GET [pending]'); }); it('shows correct method', async () => { const request = getMockRequest({method: 'POST'}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com POST [pending]'); + assert.equal(result, 'reqid 1 - http://example.com POST [pending]'); }); it('shows correct status for request with response code in 200', async () => { const response = getMockResponse(); const request = getMockRequest({response}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [success - 200]'); + assert.equal(result, 'reqid 1 - http://example.com GET [success - 200]'); }); it('shows correct status for request with response code in 100', async () => { const response = getMockResponse({ status: 199, }); const request = getMockRequest({response}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [failed - 199]'); + assert.equal(result, 'reqid 1 - http://example.com GET [failed - 199]'); }); it('shows correct status for request with response code above 200', async () => { const response = getMockResponse({ status: 300, }); const request = getMockRequest({response}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [failed - 300]'); + assert.equal(result, 'reqid 1 - http://example.com GET [failed - 300]'); }); it('shows correct status for request that failed', async () => { const request = getMockRequest({ @@ -64,11 +64,11 @@ describe('networkFormatter', () => { }; }, }); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); assert.equal( result, - 'http://example.com GET [failed - Error in Network]', + 'reqid 1 - http://example.com GET [failed - Error in Network]', ); }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 0197e181..48cd6b77 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -10,6 +10,7 @@ import type {HTTPRequest, HTTPResponse} from 'puppeteer-core'; import {McpContext} from '../src/McpContext.js'; import {McpResponse} from '../src/McpResponse.js'; +import {stableIdSymbol} from '../src/PageCollector.js'; let browser: Browser | undefined; @@ -49,6 +50,7 @@ export function getMockRequest( hasPostData?: boolean; postData?: string; fetchPostData?: Promise; + stableId?: number; } = {}, ): HTTPRequest { return { @@ -84,7 +86,8 @@ export function getMockRequest( redirectChain(): HTTPRequest[] { return []; }, - } as HTTPRequest; + [stableIdSymbol]: options.stableId ?? 1, + } as unknown as HTTPRequest; } export function getMockResponse( From f3ffda34426173dc811dad524ee65a99ec4baeb5 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:54:55 +0200 Subject: [PATCH 3/5] chore: use a name for job (#379) --- .github/workflows/pre-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index e7d7906f..198f1e8a 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -9,6 +9,7 @@ on: jobs: pre-release: + name: 'Verify MCP server schema unchanged' runs-on: ubuntu-latest steps: - name: Check out repository From 58aa99e1bd74e5e3a1338771782ee66fc76c8b9e Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:23:19 +0200 Subject: [PATCH 4/5] refactor: use different format for reqid (#380) --- src/formatters/networkFormatter.ts | 2 +- tests/McpResponse.test.ts | 34 +++++++++++------------ tests/formatters/networkFormatter.test.ts | 12 ++++---- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/formatters/networkFormatter.ts b/src/formatters/networkFormatter.ts index 43be8ae6..57279733 100644 --- a/src/formatters/networkFormatter.ts +++ b/src/formatters/networkFormatter.ts @@ -15,7 +15,7 @@ export function getShortDescriptionForRequest( id: number, ): string { // TODO truncate the URL - return `reqid ${id} - ${request.url()} ${request.method()} ${getStatusFromRequest(request)}`; + return `reqid=${id} ${request.method()} ${request.url()} ${getStatusFromRequest(request)}`; } export function getStatusFromRequest(request: HTTPRequest): string { diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 69cc56b8..846c4d48 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -210,8 +210,8 @@ Call handle_dialog to handle it before continuing.`, `# test response ## Network requests Showing 1-2 of 2 (Page 1 of 1). -reqid 1 - http://example.com GET [pending] -reqid 2 - http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending] +reqid=2 GET http://example.com [pending]`, ); }); }); @@ -267,7 +267,7 @@ ${JSON.stringify({request: 'body'})} ${JSON.stringify({response: 'body'})} ## Network requests Showing 1-1 of 1 (Page 1 of 1). -reqid 1 - http://example.com POST [success - 200]`, +reqid=1 POST http://example.com [success - 200]`, ); }); }); @@ -290,7 +290,7 @@ Status: [pending] - content-size:10 ## Network requests Showing 1-1 of 1 (Page 1 of 1). -reqid 1 - http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending]`, ); }); }); @@ -354,8 +354,8 @@ describe('McpResponse network request filtering', () => { `# test response ## Network requests Showing 1-2 of 2 (Page 1 of 1). -reqid 1 - http://example.com GET [pending] -reqid 1 - http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending]`, ); }); }); @@ -378,7 +378,7 @@ reqid 1 - http://example.com GET [pending]`, `# test response ## Network requests Showing 1-1 of 1 (Page 1 of 1). -reqid 1 - http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending]`, ); }); }); @@ -423,11 +423,11 @@ No requests found.`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -reqid 1 - http://example.com GET [pending] -reqid 1 - http://example.com GET [pending] -reqid 1 - http://example.com GET [pending] -reqid 1 - http://example.com GET [pending] -reqid 1 - http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending]`, ); }); }); @@ -452,11 +452,11 @@ reqid 1 - http://example.com GET [pending]`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -reqid 1 - http://example.com GET [pending] -reqid 1 - http://example.com GET [pending] -reqid 1 - http://example.com GET [pending] -reqid 1 - http://example.com GET [pending] -reqid 1 - http://example.com GET [pending]`, +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending] +reqid=1 GET http://example.com [pending]`, ); }); }); diff --git a/tests/formatters/networkFormatter.test.ts b/tests/formatters/networkFormatter.test.ts index 7f2fddc4..e9608f50 100644 --- a/tests/formatters/networkFormatter.test.ts +++ b/tests/formatters/networkFormatter.test.ts @@ -23,20 +23,20 @@ describe('networkFormatter', () => { const request = getMockRequest(); const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'reqid 1 - http://example.com GET [pending]'); + assert.equal(result, 'reqid=1 GET http://example.com [pending]'); }); it('shows correct method', async () => { const request = getMockRequest({method: 'POST'}); const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'reqid 1 - http://example.com POST [pending]'); + assert.equal(result, 'reqid=1 POST http://example.com [pending]'); }); it('shows correct status for request with response code in 200', async () => { const response = getMockResponse(); const request = getMockRequest({response}); const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'reqid 1 - http://example.com GET [success - 200]'); + assert.equal(result, 'reqid=1 GET http://example.com [success - 200]'); }); it('shows correct status for request with response code in 100', async () => { const response = getMockResponse({ @@ -45,7 +45,7 @@ describe('networkFormatter', () => { const request = getMockRequest({response}); const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'reqid 1 - http://example.com GET [failed - 199]'); + assert.equal(result, 'reqid=1 GET http://example.com [failed - 199]'); }); it('shows correct status for request with response code above 200', async () => { const response = getMockResponse({ @@ -54,7 +54,7 @@ describe('networkFormatter', () => { const request = getMockRequest({response}); const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'reqid 1 - http://example.com GET [failed - 300]'); + assert.equal(result, 'reqid=1 GET http://example.com [failed - 300]'); }); it('shows correct status for request that failed', async () => { const request = getMockRequest({ @@ -68,7 +68,7 @@ describe('networkFormatter', () => { assert.equal( result, - 'reqid 1 - http://example.com GET [failed - Error in Network]', + 'reqid=1 GET http://example.com [failed - Error in Network]', ); }); }); From ee1fad7835f70344052d4809a4bf409ff95571ea Mon Sep 17 00:00:00 2001 From: alinavarkki Date: Wed, 15 Oct 2025 10:49:23 +0200 Subject: [PATCH 5/5] rebase --- docs/tool-reference.md | 2 +- src/McpContext.ts | 15 ++------------- src/McpResponse.ts | 20 ++++++++++---------- src/PageCollector.ts | 15 +++++++++++++++ src/tools/ToolDefinition.ts | 2 +- src/tools/network.ts | 8 ++++++-- tests/McpResponse.test.ts | 10 ++++++++-- tests/tools/network.test.ts | 9 +++------ 8 files changed, 46 insertions(+), 35 deletions(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index be12f101..216a37fa 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -258,7 +258,7 @@ **Parameters:** -- **url** (string) **(required)**: The URL of the request. +- **reqid** (number) **(required)**: The reqid of a request on the page from the listed network requests --- diff --git a/src/McpContext.ts b/src/McpContext.ts index 6bf2fa20..99b82696 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -155,19 +155,8 @@ export class McpContext implements Context { await page.close({runBeforeUnload: false}); } - getNetworkRequestByUrl(url: string): HTTPRequest { - const requests = this.getNetworkRequests(); - if (!requests.length) { - throw new Error('No requests found for selected page'); - } - - for (const request of requests) { - if (request.url() === url) { - return request; - } - } - - throw new Error('Request not found for selected page'); + getNetworkRequestById(reqid: number): HTTPRequest { + return this.#networkCollector.getById(this.getSelectedPage(), reqid); } setNetworkConditions(conditions: string | null): void { diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 6653452c..366cca1b 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -24,7 +24,7 @@ import type {ImageContentData, Response} from './tools/ToolDefinition.js'; import {paginate, type PaginationOptions} from './utils/pagination.js'; interface NetworkRequestData { - networkRequestUrl: string; + networkRequestStableId: number; requestBody?: string; responseBody?: string; } @@ -106,9 +106,9 @@ export class McpResponse implements Response { }; } - attachNetworkRequest(url: string): void { + attachNetworkRequest(reqid: number): void { this.#attachedNetworkRequestData = { - networkRequestUrl: url, + networkRequestStableId: reqid, }; } @@ -123,8 +123,8 @@ export class McpResponse implements Response { get includeConsoleData(): boolean { return this.#consoleDataOptions?.include ?? false; } - get attachedNetworkRequestUrl(): string | undefined { - return this.#attachedNetworkRequestData?.networkRequestUrl; + get attachedNetworkRequestId(): number | undefined { + return this.#attachedNetworkRequestData?.networkRequestStableId; } get networkRequestsPageIdx(): number | undefined { return this.#networkRequestsOptions?.pagination?.pageIdx; @@ -167,9 +167,9 @@ export class McpResponse implements Response { await context.createTextSnapshot(); } - if (this.#attachedNetworkRequestData?.networkRequestUrl) { - const request = context.getNetworkRequestByUrl( - this.#attachedNetworkRequestData.networkRequestUrl, + if (this.#attachedNetworkRequestData?.networkRequestStableId) { + const request = context.getNetworkRequestById( + this.#attachedNetworkRequestData.networkRequestStableId, ); this.#attachedNetworkRequestData.requestBody = @@ -350,12 +350,12 @@ Call ${handleDialog.name} to handle it before continuing.`); #getIncludeNetworkRequestsData(context: McpContext): string[] { const response: string[] = []; - const url = this.#attachedNetworkRequestData?.networkRequestUrl; + const url = this.#attachedNetworkRequestData?.networkRequestStableId; if (!url) { return response; } - const httpRequest = context.getNetworkRequestByUrl(url); + const httpRequest = context.getNetworkRequestById(url); response.push(`## Request ${httpRequest.url()}`); response.push(`Status: ${getStatusFromRequest(httpRequest)}`); response.push(`### Request Headers`); diff --git a/src/PageCollector.ts b/src/PageCollector.ts index fa10c215..69615c50 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -133,6 +133,21 @@ export class PageCollector { getIdForResource(resource: WithSymbolId): number { return resource[stableIdSymbol] ?? -1; } + + getById(page: Page, stableId: number): T { + const data = this.storage.get(page); + if (!data || !data.length) { + throw new Error('No requests found for selected page'); + } + + for (const collected of data) { + if (collected[stableIdSymbol] === stableId) { + return collected; + } + } + + throw new Error('Request not found for selected page'); + } } export class NetworkCollector extends PageCollector { diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 7952fc56..7e902255 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -53,7 +53,7 @@ export interface Response { ): void; setIncludeSnapshot(value: boolean): void; attachImage(value: ImageContentData): void; - attachNetworkRequest(url: string): void; + attachNetworkRequest(reqid: number): void; } /** diff --git a/src/tools/network.ts b/src/tools/network.ts index 1fc82a5a..23b2c1f6 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -80,9 +80,13 @@ export const getNetworkRequest = defineTool({ readOnlyHint: true, }, schema: { - url: z.string().describe('The URL of the request.'), + reqid: z + .number() + .describe( + 'The reqid of a request on the page from the listed network requests', + ), }, handler: async (request, response, _context) => { - response.attachNetworkRequest(request.params.url); + response.attachNetworkRequest(request.params.reqid); }, }); diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 846c4d48..17cd4ae4 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -248,7 +248,10 @@ reqid=2 GET http://example.com [pending]`, context.getNetworkRequests = () => { return [request]; }; - response.attachNetworkRequest(request.url()); + context.getNetworkRequestById = () => { + return request; + }; + response.attachNetworkRequest(1); const result = await response.handle('test', context); @@ -279,7 +282,10 @@ reqid=1 POST http://example.com [success - 200]`, context.getNetworkRequests = () => { return [request]; }; - response.attachNetworkRequest(request.url()); + context.getNetworkRequestById = () => { + return request; + }; + response.attachNetworkRequest(1); const result = await response.handle('test', context); assert.strictEqual( result[0].text, diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index c53cbc1d..fa6439dd 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -28,14 +28,11 @@ describe('network', () => { const page = await context.getSelectedPage(); await page.goto('data:text/html,
Hello MCP
'); await getNetworkRequest.handler( - {params: {url: 'data:text/html,
Hello MCP
'}}, + {params: {reqid: 1}}, response, context, ); - assert.equal( - response.attachedNetworkRequestUrl, - 'data:text/html,
Hello MCP
', - ); + assert.equal(response.attachedNetworkRequestId, 1); }); }); it('should not add the request list', async () => { @@ -43,7 +40,7 @@ describe('network', () => { const page = await context.getSelectedPage(); await page.goto('data:text/html,
Hello MCP
'); await getNetworkRequest.handler( - {params: {url: 'data:text/html,
Hello MCP
'}}, + {params: {reqid: 1}}, response, context, );