From 5c37dc9cd959804cd6d80a237ff2fd3b93a0bc07 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Tue, 14 Oct 2025 12:02:23 +0200 Subject: [PATCH 1/4] feat: support stable id for requests --- src/McpContext.ts | 4 +++ src/McpResponse.ts | 9 ++++-- src/PageCollector.ts | 29 +++++++++++++++--- 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, 101 insertions(+), 40 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 4cf6b6610..6bf2fa20e 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 4ea9f7963..ab36a511e 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -246,7 +246,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.'); @@ -347,7 +352,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 acbf3239d..5d76676b1 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -17,6 +17,19 @@ export type ListenerMap = { [K in keyof EventMap]?: (event: EventMap[K]) => void; }; +function createIdGenerator() { + // TODO: Reset after max + let i = 1; + return () => { + return i++; + }; +} + +export const stableIdSymbol = Symbol('stableIdSymbol'); +type WithSymbolId = T & { + [stableIdSymbol]?: number; +}; + export class PageCollector { #browser: Browser; #listenersInitializer: ( @@ -28,7 +41,8 @@ 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>>(); + protected idGenerator = new WeakMap number>(); constructor( browser: Browser, @@ -56,7 +70,6 @@ export class PageCollector { if (!page) { return; } - console.log('destro'); this.#cleanupPageDestroyed(page); }); } @@ -70,10 +83,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 +128,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 7796f01a7..294e3af03 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 `uid ${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 114164474..f88305673 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). +uid 1 - http://example.com GET [pending] +uid 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]`, +uid 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]`, +uid 1 - http://example.com GET [pending]`, ); }); }); @@ -354,8 +355,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]`, +uid 1 - http://example.com GET [pending] +uid 1 - http://example.com GET [pending]`, ); }); }); @@ -378,7 +379,7 @@ http://example.com GET [pending]`, `# test response ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +uid 1 - http://example.com GET [pending]`, ); }); }); @@ -423,11 +424,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]`, +uid 1 - http://example.com GET [pending] +uid 1 - http://example.com GET [pending] +uid 1 - http://example.com GET [pending] +uid 1 - http://example.com GET [pending] +uid 1 - http://example.com GET [pending]`, ); }); }); @@ -452,11 +453,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]`, +uid 1 - http://example.com GET [pending] +uid 1 - http://example.com GET [pending] +uid 1 - http://example.com GET [pending] +uid 1 - http://example.com GET [pending] +uid 1 - http://example.com GET [pending]`, ); }); }); diff --git a/tests/PageCollector.test.ts b/tests/PageCollector.test.ts index 43c82380a..431687aa5 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 23c8a3239..71fa538e3 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, 'uid 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, 'uid 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, 'uid 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, 'uid 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, 'uid 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]', + 'uid 1 - http://example.com GET [failed - Error in Network]', ); }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 0197e1812..48cd6b770 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 ec6776591faa779a4fb9953eb3a1bc4088654389 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Tue, 14 Oct 2025 12:04:55 +0200 Subject: [PATCH 2/4] fix --- src/PageCollector.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PageCollector.ts b/src/PageCollector.ts index 5d76676b1..aabba0d5b 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -18,9 +18,11 @@ export type ListenerMap = { }; function createIdGenerator() { - // TODO: Reset after max let i = 1; return () => { + if (i === Number.MAX_SAFE_INTEGER) { + i = 0; + } return i++; }; } From f406fffe903167e5a88ced241fdfb6d5332589ee Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Tue, 14 Oct 2025 12:18:52 +0200 Subject: [PATCH 3/4] fix --- src/PageCollector.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PageCollector.ts b/src/PageCollector.ts index aabba0d5b..fa10c2153 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -44,7 +44,6 @@ export class PageCollector { * Use methods that manipulate the array in place. */ protected storage = new WeakMap>>(); - protected idGenerator = new WeakMap number>(); constructor( browser: Browser, From 459fc2f1f831611766b9c7342aed3be708ddd943 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Tue, 14 Oct 2025 12:21:01 +0200 Subject: [PATCH 4/4] rename --- 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 294e3af03..43be8ae69 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 `uid ${id} - ${request.url()} ${request.method()} ${getStatusFromRequest(request)}`; + 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 f88305673..266495f8c 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). -uid 1 - http://example.com GET [pending] -uid 2 - http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending] +reqid 2 - http://example.com GET [pending]`, ); }); }); @@ -267,7 +267,7 @@ ${JSON.stringify({request: 'body'})} ${JSON.stringify({response: 'body'})} ## Network requests Showing 1-1 of 1 (Page 1 of 1). -uid 1 - http://example.com POST [success - 200]`, +reqid 1 - http://example.com POST [success - 200]`, ); }); }); @@ -290,7 +290,7 @@ Status: [pending] - content-size:10 ## Network requests Showing 1-1 of 1 (Page 1 of 1). -uid 1 - http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -355,8 +355,8 @@ describe('McpResponse network request filtering', () => { `# test response ## Network requests Showing 1-2 of 2 (Page 1 of 1). -uid 1 - http://example.com GET [pending] -uid 1 - http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -379,7 +379,7 @@ uid 1 - http://example.com GET [pending]`, `# test response ## Network requests Showing 1-1 of 1 (Page 1 of 1). -uid 1 - http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -424,11 +424,11 @@ No requests found.`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -uid 1 - http://example.com GET [pending] -uid 1 - http://example.com GET [pending] -uid 1 - http://example.com GET [pending] -uid 1 - http://example.com GET [pending] -uid 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 - http://example.com GET [pending]`, ); }); }); @@ -453,11 +453,11 @@ uid 1 - http://example.com GET [pending]`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -uid 1 - http://example.com GET [pending] -uid 1 - http://example.com GET [pending] -uid 1 - http://example.com GET [pending] -uid 1 - http://example.com GET [pending] -uid 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 - http://example.com GET [pending]`, ); }); }); diff --git a/tests/formatters/networkFormatter.test.ts b/tests/formatters/networkFormatter.test.ts index 71fa538e3..7f2fddc4f 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, 'uid 1 - 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, 1); - assert.equal(result, 'uid 1 - 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, 1); - assert.equal(result, 'uid 1 - 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({ @@ -45,7 +45,7 @@ describe('networkFormatter', () => { const request = getMockRequest({response}); const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'uid 1 - 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({ @@ -54,7 +54,7 @@ describe('networkFormatter', () => { const request = getMockRequest({response}); const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'uid 1 - 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({ @@ -68,7 +68,7 @@ describe('networkFormatter', () => { assert.equal( result, - 'uid 1 - http://example.com GET [failed - Error in Network]', + 'reqid 1 - http://example.com GET [failed - Error in Network]', ); }); });