From d5cd95bd4d35ceed0297b17d4c205e1ce688d792 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Fri, 17 Oct 2025 19:13:30 +0200 Subject: [PATCH 01/12] feat: expose previous navigations to the MCP server --- src/McpContext.ts | 4 +-- src/McpResponse.ts | 7 ++++- src/PageCollector.ts | 13 ++++++--- src/tools/ToolDefinition.ts | 1 + src/tools/network.ts | 10 +++++++ tests/PageCollector.test.ts | 36 +++++++++++++++++++++++++ tests/tools/network.test.js.snapshot | 14 ++++++++++ tests/tools/network.test.ts | 40 ++++++++++++++++++++++++++++ 8 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 tests/tools/network.test.js.snapshot diff --git a/src/McpContext.ts b/src/McpContext.ts index 4f7641d50..22bf067f0 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -141,9 +141,9 @@ export class McpContext implements Context { return context; } - getNetworkRequests(): HTTPRequest[] { + getNetworkRequests(includePreviousNavigations?: number): HTTPRequest[] { const page = this.getSelectedPage(); - return this.#networkCollector.getData(page); + return this.#networkCollector.getData(page, includePreviousNavigations); } getConsoleData(): Array { diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 31bbdcc5a..222edf4e1 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -42,6 +42,7 @@ export class McpResponse implements Response { include: boolean; pagination?: PaginationOptions; resourceTypes?: ResourceType[]; + includePreviousNavigations?: number; }; #consoleDataOptions?: { include: boolean; @@ -62,6 +63,7 @@ export class McpResponse implements Response { value: boolean, options?: PaginationOptions & { resourceTypes?: ResourceType[]; + includePreviousNavigations?: number; }, ): void { if (!value) { @@ -79,6 +81,7 @@ export class McpResponse implements Response { } : undefined, resourceTypes: options?.resourceTypes, + includePreviousNavigations: options?.includePreviousNavigations, }; } @@ -346,7 +349,9 @@ Call ${handleDialog.name} to handle it before continuing.`); response.push(...this.#formatConsoleData(data.consoleData)); if (this.#networkRequestsOptions?.include) { - let requests = context.getNetworkRequests(); + let requests = context.getNetworkRequests( + this.#networkRequestsOptions?.includePreviousNavigations, + ); // Apply resource type filtering if specified if (this.#networkRequestsOptions.resourceTypes?.length) { diff --git a/src/PageCollector.ts b/src/PageCollector.ts index 44d1363f8..8c5dcf533 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -133,12 +133,18 @@ export class PageCollector { this.storage.delete(page); } - getData(page: Page): T[] { + getData(page: Page, includePreviousNavigations?: number): T[] { const navigations = this.storage.get(page); if (!navigations) { return []; } - return navigations[0]; + + const data: T[] = []; + for (let index = includePreviousNavigations ?? 0; index >= 0; index--) { + console.log('Loop index', index); + data.push(...navigations[index]); + } + return data; } getIdForResource(resource: WithSymbolId): number { @@ -168,6 +174,7 @@ export class NetworkCollector extends PageCollector { super(browser, collect => { return { request: req => { + console.log('Collected'); collect(req); }, } as ListenerMap; @@ -190,7 +197,7 @@ export class NetworkCollector extends PageCollector { // Keep all requests since the last navigation request including that // navigation request itself. // Keep the reference - if (lastRequestIdx) { + if (lastRequestIdx !== -1) { const fromCurrentNavigation = requests.splice(lastRequestIdx); navigations.unshift(fromCurrentNavigation); } else { diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 19415dd27..2fe36d2c5 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -53,6 +53,7 @@ export interface Response { value: boolean, options?: PaginationOptions & { resourceTypes?: string[]; + includePreviousNavigations?: number; }, ): void; setIncludeConsoleData( diff --git a/src/tools/network.ts b/src/tools/network.ts index d47fa9960..20fd22573 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -62,12 +62,22 @@ export const listNetworkRequests = defineTool({ .describe( 'Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests.', ), + includePreviousNavigations: z + .number() + .int() + .positive() + .max(2) + .optional() + .describe( + 'Number of previous navigations to include. Current navigation is always included.', + ), }, handler: async (request, response) => { response.setIncludeNetworkRequests(true, { pageSize: request.params.pageSize, pageIdx: request.params.pageIdx, resourceTypes: request.params.resourceTypes, + includePreviousNavigations: request.params.includePreviousNavigations, }); }, }); diff --git a/tests/PageCollector.test.ts b/tests/PageCollector.test.ts index a916fdc24..5da1d31a1 100644 --- a/tests/PageCollector.test.ts +++ b/tests/PageCollector.test.ts @@ -250,4 +250,40 @@ describe('NetworkCollector', () => { assert.equal(collector.getData(page)[0], navRequest); assert.equal(collector.getData(page)[1], request2); }); + + it('correctly picks up after multiple back to back navigations', async () => { + const browser = getMockBrowser(); + const page = (await browser.pages())[0]; + const mainFrame = page.mainFrame(); + const navRequest = getMockRequest({ + navigationRequest: true, + frame: page.mainFrame(), + }); + const navRequest2 = getMockRequest({ + navigationRequest: true, + frame: page.mainFrame(), + }); + const request = getMockRequest(); + + const collector = new NetworkCollector(browser); + await collector.init(); + page.emit('request', navRequest); + assert.equal(collector.getData(page)[0], navRequest); + + page.emit('framenavigated', mainFrame); + assert.equal(collector.getData(page).length, 1); + assert.equal(collector.getData(page)[0], navRequest); + + page.emit('request', navRequest2); + assert.equal(collector.getData(page).length, 2); + assert.equal(collector.getData(page)[0], navRequest); + assert.equal(collector.getData(page)[1], navRequest2); + + page.emit('framenavigated', mainFrame); + assert.equal(collector.getData(page).length, 1); + assert.equal(collector.getData(page)[0], navRequest2); + + page.emit('request', request); + assert.equal(collector.getData(page).length, 2); + }); }); diff --git a/tests/tools/network.test.js.snapshot b/tests/tools/network.test.js.snapshot new file mode 100644 index 000000000..f051aa41b --- /dev/null +++ b/tests/tools/network.test.js.snapshot @@ -0,0 +1,14 @@ +exports[`network > network_list_requests > list requests form current navigations only 1`] = ` +# list_request response +## Network requests +Showing 1-1 of 1 (Page 1 of 1). +reqid=3 GET data:text/html,
Hello 3
[success - 200] +`; + +exports[`network > network_list_requests > list requests from previous navigations 1`] = ` +# list_request response +## Network requests +Showing 1-2 of 2 (Page 1 of 1). +reqid=2 GET data:text/html,
Hello 2
[success - 200] +reqid=3 GET data:text/html,
Hello 3
[success - 200] +`; diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index fa6439dd1..34ec893bf 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -21,6 +21,45 @@ describe('network', () => { assert.strictEqual(response.networkRequestsPageIdx, undefined); }); }); + + it('list requests form current navigations only', async t => { + await withBrowser(async (response, context) => { + const page = await context.getSelectedPage(); + await page.goto('data:text/html,
Hello 1
'); + await page.goto('data:text/html,
Hello 2
'); + await page.goto('data:text/html,
Hello 3
'); + await listNetworkRequests.handler( + { + params: {}, + }, + response, + context, + ); + const responseData = await response.handle('list_request', context); + t.assert.snapshot?.(responseData[0].text); + }); + }); + + it.only('list requests from previous navigations', async t => { + await withBrowser(async (response, context) => { + const page = await context.getSelectedPage(); + await page.goto('data:text/html,
Hello 1
'); + await page.goto('data:text/html,
Hello 2
'); + console.log('Last navigtation'); + await page.goto('data:text/html,
Hello 3
'); + await listNetworkRequests.handler( + { + params: { + includePreviousNavigations: 1, + }, + }, + response, + context, + ); + const responseData = await response.handle('list_request', context); + t.assert.snapshot?.(responseData[0].text); + }); + }); }); describe('network_get_request', () => { it('attaches request', async () => { @@ -32,6 +71,7 @@ describe('network', () => { response, context, ); + assert.equal(response.attachedNetworkRequestId, 1); }); }); From b6b613224dc9d0ce4f7d3bd74b2d2a4bb51038d1 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Fri, 17 Oct 2025 19:15:56 +0200 Subject: [PATCH 02/12] fix --- src/PageCollector.ts | 2 -- tests/tools/network.test.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/PageCollector.ts b/src/PageCollector.ts index 8c5dcf533..e855d4dd4 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -141,7 +141,6 @@ export class PageCollector { const data: T[] = []; for (let index = includePreviousNavigations ?? 0; index >= 0; index--) { - console.log('Loop index', index); data.push(...navigations[index]); } return data; @@ -174,7 +173,6 @@ export class NetworkCollector extends PageCollector { super(browser, collect => { return { request: req => { - console.log('Collected'); collect(req); }, } as ListenerMap; diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index 34ec893bf..980a4a146 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -45,7 +45,6 @@ describe('network', () => { const page = await context.getSelectedPage(); await page.goto('data:text/html,
Hello 1
'); await page.goto('data:text/html,
Hello 2
'); - console.log('Last navigtation'); await page.goto('data:text/html,
Hello 3
'); await listNetworkRequests.handler( { From 6624dd34fdbb573248d4cb9ccc189605f2318cb3 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Fri, 17 Oct 2025 19:20:01 +0200 Subject: [PATCH 03/12] f --- tests/tools/network.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index 980a4a146..5bc27e7a5 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -40,7 +40,7 @@ describe('network', () => { }); }); - it.only('list requests from previous navigations', async t => { + it('list requests from previous navigations', async t => { await withBrowser(async (response, context) => { const page = await context.getSelectedPage(); await page.goto('data:text/html,
Hello 1
'); From 8cc64b9024a0982a61568ca389410bf44ad8dee9 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Fri, 17 Oct 2025 19:25:00 +0200 Subject: [PATCH 04/12] fix --- src/tools/network.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/network.ts b/src/tools/network.ts index 20fd22573..eff1c78cf 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -62,7 +62,7 @@ export const listNetworkRequests = defineTool({ .describe( 'Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests.', ), - includePreviousNavigations: z + includePreviousNavigations: zod .number() .int() .positive() From b2a08a031c67f0cc43c3530e0556cb7af867ae15 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Fri, 17 Oct 2025 19:33:10 +0200 Subject: [PATCH 05/12] fix docs --- docs/tool-reference.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 68c1f9b39..cce7367dd 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -269,6 +269,7 @@ **Parameters:** +- **includePreviousNavigations** (integer) _(optional)_: Number of previous navigations to include. Current navigation is always included. - **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page. - **pageSize** (integer) _(optional)_: Maximum number of requests to return. When omitted, returns all requests. - **resourceTypes** (array) _(optional)_: Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests. From e714ad9e1879b25e8d0955ed65d0ee31c8b0cdc3 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Mon, 20 Oct 2025 18:05:40 +0200 Subject: [PATCH 06/12] use boolean --- src/McpContext.ts | 2 +- src/McpResponse.ts | 4 ++-- src/PageCollector.ts | 8 ++++++-- src/tools/ToolDefinition.ts | 2 +- src/tools/network.ts | 6 ++---- tests/tools/network.test.js.snapshot | 3 ++- tests/tools/network.test.ts | 2 +- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index 22bf067f0..f2587cf0c 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -141,7 +141,7 @@ export class McpContext implements Context { return context; } - getNetworkRequests(includePreviousNavigations?: number): HTTPRequest[] { + getNetworkRequests(includePreviousNavigations?: boolean): HTTPRequest[] { const page = this.getSelectedPage(); return this.#networkCollector.getData(page, includePreviousNavigations); } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 222edf4e1..03a814658 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -42,7 +42,7 @@ export class McpResponse implements Response { include: boolean; pagination?: PaginationOptions; resourceTypes?: ResourceType[]; - includePreviousNavigations?: number; + includePreviousNavigations?: boolean; }; #consoleDataOptions?: { include: boolean; @@ -63,7 +63,7 @@ export class McpResponse implements Response { value: boolean, options?: PaginationOptions & { resourceTypes?: ResourceType[]; - includePreviousNavigations?: number; + includePreviousNavigations?: boolean; }, ): void { if (!value) { diff --git a/src/PageCollector.ts b/src/PageCollector.ts index e855d4dd4..60b9596cb 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -133,14 +133,18 @@ export class PageCollector { this.storage.delete(page); } - getData(page: Page, includePreviousNavigations?: number): T[] { + getData(page: Page, includePreviousNavigations?: boolean): T[] { const navigations = this.storage.get(page); if (!navigations) { return []; } + if (!includePreviousNavigations) { + return navigations[0]; + } + const data: T[] = []; - for (let index = includePreviousNavigations ?? 0; index >= 0; index--) { + for (let index = this.#maxNavigationSaved; index >= 0; index--) { data.push(...navigations[index]); } return data; diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 2fe36d2c5..966cf77b6 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -53,7 +53,7 @@ export interface Response { value: boolean, options?: PaginationOptions & { resourceTypes?: string[]; - includePreviousNavigations?: number; + includePreviousNavigations?: boolean; }, ): void; setIncludeConsoleData( diff --git a/src/tools/network.ts b/src/tools/network.ts index eff1c78cf..bf4ee5e5f 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -63,10 +63,8 @@ export const listNetworkRequests = defineTool({ 'Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests.', ), includePreviousNavigations: zod - .number() - .int() - .positive() - .max(2) + .boolean() + .default(false) .optional() .describe( 'Number of previous navigations to include. Current navigation is always included.', diff --git a/tests/tools/network.test.js.snapshot b/tests/tools/network.test.js.snapshot index f051aa41b..db5044d90 100644 --- a/tests/tools/network.test.js.snapshot +++ b/tests/tools/network.test.js.snapshot @@ -8,7 +8,8 @@ reqid=3 GET data:text/html,
Hello 3
[success - 200] exports[`network > network_list_requests > list requests from previous navigations 1`] = ` # list_request response ## Network requests -Showing 1-2 of 2 (Page 1 of 1). +Showing 1-3 of 3 (Page 1 of 1). +reqid=1 GET data:text/html,
Hello 1
[success - 200] reqid=2 GET data:text/html,
Hello 2
[success - 200] reqid=3 GET data:text/html,
Hello 3
[success - 200] `; diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index 5bc27e7a5..c249315a6 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -49,7 +49,7 @@ describe('network', () => { await listNetworkRequests.handler( { params: { - includePreviousNavigations: 1, + includePreviousNavigations: true, }, }, response, From f1995aae44a4c275757386ff928a342c0fc4a47b Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Mon, 20 Oct 2025 18:10:41 +0200 Subject: [PATCH 07/12] docs --- 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 cce7367dd..ece25da35 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -269,7 +269,7 @@ **Parameters:** -- **includePreviousNavigations** (integer) _(optional)_: Number of previous navigations to include. Current navigation is always included. +- **includePreviousNavigations** (boolean) _(optional)_: Number of previous navigations to include. Current navigation is always included. - **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page. - **pageSize** (integer) _(optional)_: Maximum number of requests to return. When omitted, returns all requests. - **resourceTypes** (array) _(optional)_: Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests. From 0273d30450b6826351510eb6bbe11bb905127027 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Tue, 21 Oct 2025 11:27:29 +0200 Subject: [PATCH 08/12] move to network --- docs/tool-reference.md | 2 +- src/tools/network.ts | 4 +- tests/tools/network.test.js.snapshot | 27 +++++++++++-- tests/tools/network.test.ts | 60 ++++++++++++++++++++++------ tests/tools/snapshot.test.ts | 4 +- tests/utils.ts | 12 ++++++ 6 files changed, 86 insertions(+), 23 deletions(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index ece25da35..3f7499fbd 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -269,7 +269,7 @@ **Parameters:** -- **includePreviousNavigations** (boolean) _(optional)_: Number of previous navigations to include. Current navigation is always included. +- **includePreviousNavigations** (boolean) _(optional)_: Whether to include request from previous navigations. - **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page. - **pageSize** (integer) _(optional)_: Maximum number of requests to return. When omitted, returns all requests. - **resourceTypes** (array) _(optional)_: Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests. diff --git a/src/tools/network.ts b/src/tools/network.ts index bf4ee5e5f..f69242742 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -66,9 +66,7 @@ export const listNetworkRequests = defineTool({ .boolean() .default(false) .optional() - .describe( - 'Number of previous navigations to include. Current navigation is always included.', - ), + .describe('Whether to include request from previous navigations.'), }, handler: async (request, response) => { response.setIncludeNetworkRequests(true, { diff --git a/tests/tools/network.test.js.snapshot b/tests/tools/network.test.js.snapshot index db5044d90..ff23233a6 100644 --- a/tests/tools/network.test.js.snapshot +++ b/tests/tools/network.test.js.snapshot @@ -1,15 +1,34 @@ +exports[`network > network_get_request > should get request from previous navigations 1`] = ` +# get_request response +## Request http://localhost:/one +Status: [success - 200] +### Request Headers +- accept-language:en-US,en;q=0.9 +- upgrade-insecure-requests:1 +- user-agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/141.0.0.0 Safari/537.36 +- sec-ch-ua:"Chromium";v="141", "Not?A_Brand";v="8" +- sec-ch-ua-mobile:?0 +- sec-ch-ua-platform:"Linux" +### Response Headers +- connection:keep-alive +- content-length:239 +- content-type:text/html; charset=utf-8 +- date: +- keep-alive:timeout=5 +`; + exports[`network > network_list_requests > list requests form current navigations only 1`] = ` # list_request response ## Network requests Showing 1-1 of 1 (Page 1 of 1). -reqid=3 GET data:text/html,
Hello 3
[success - 200] +reqid=4 GET http://localhost:/three [success - 200] `; exports[`network > network_list_requests > list requests from previous navigations 1`] = ` # list_request response ## Network requests Showing 1-3 of 3 (Page 1 of 1). -reqid=1 GET data:text/html,
Hello 1
[success - 200] -reqid=2 GET data:text/html,
Hello 2
[success - 200] -reqid=3 GET data:text/html,
Hello 3
[success - 200] +reqid=1 GET http://localhost:/one [success - 200] +reqid=2 GET http://localhost:/two [success - 200] +reqid=3 GET http://localhost:/three [success - 200] `; diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index c249315a6..52f261d2b 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -10,9 +10,11 @@ import { getNetworkRequest, listNetworkRequests, } from '../../src/tools/network.js'; -import {withBrowser} from '../utils.js'; +import {serverHooks} from '../server.js'; +import {html, withBrowser, stabilizeResponseOutput} from '../utils.js'; describe('network', () => { + const server = serverHooks(); describe('network_list_requests', () => { it('list requests', async () => { await withBrowser(async (response, context) => { @@ -23,11 +25,15 @@ describe('network', () => { }); it('list requests form current navigations only', async t => { + server.addHtmlRoute('/one', html`
First
`); + server.addHtmlRoute('/two', html`
Second
`); + server.addHtmlRoute('/three', html`
Third
`); + await withBrowser(async (response, context) => { - const page = await context.getSelectedPage(); - await page.goto('data:text/html,
Hello 1
'); - await page.goto('data:text/html,
Hello 2
'); - await page.goto('data:text/html,
Hello 3
'); + const page = context.getSelectedPage(); + await page.goto(server.getRoute('/one')); + await page.goto(server.getRoute('/two')); + await page.goto(server.getRoute('/three')); await listNetworkRequests.handler( { params: {}, @@ -36,16 +42,20 @@ describe('network', () => { context, ); const responseData = await response.handle('list_request', context); - t.assert.snapshot?.(responseData[0].text); + t.assert.snapshot?.(stabilizeResponseOutput(responseData[0].text)); }); }); it('list requests from previous navigations', async t => { + server.addHtmlRoute('/one', html`
First
`); + server.addHtmlRoute('/two', html`
Second
`); + server.addHtmlRoute('/three', html`
Third
`); + await withBrowser(async (response, context) => { - const page = await context.getSelectedPage(); - await page.goto('data:text/html,
Hello 1
'); - await page.goto('data:text/html,
Hello 2
'); - await page.goto('data:text/html,
Hello 3
'); + const page = context.getSelectedPage(); + await page.goto(server.getRoute('/one')); + await page.goto(server.getRoute('/two')); + await page.goto(server.getRoute('/three')); await listNetworkRequests.handler( { params: { @@ -56,14 +66,14 @@ describe('network', () => { context, ); const responseData = await response.handle('list_request', context); - t.assert.snapshot?.(responseData[0].text); + t.assert.snapshot?.(stabilizeResponseOutput(responseData[0].text)); }); }); }); describe('network_get_request', () => { it('attaches request', async () => { await withBrowser(async (response, context) => { - const page = await context.getSelectedPage(); + const page = context.getSelectedPage(); await page.goto('data:text/html,
Hello MCP
'); await getNetworkRequest.handler( {params: {reqid: 1}}, @@ -76,7 +86,7 @@ describe('network', () => { }); it('should not add the request list', async () => { await withBrowser(async (response, context) => { - const page = await context.getSelectedPage(); + const page = context.getSelectedPage(); await page.goto('data:text/html,
Hello MCP
'); await getNetworkRequest.handler( {params: {reqid: 1}}, @@ -86,5 +96,29 @@ describe('network', () => { assert(!response.includeNetworkRequests); }); }); + it.only('should get request from previous navigations', async t => { + server.addHtmlRoute('/one', html`
First
`); + server.addHtmlRoute('/two', html`
Second
`); + server.addHtmlRoute('/three', html`
Third
`); + + await withBrowser(async (response, context) => { + const page = context.getSelectedPage(); + await page.goto(server.getRoute('/one')); + await page.goto(server.getRoute('/two')); + await page.goto(server.getRoute('/three')); + await getNetworkRequest.handler( + { + params: { + reqid: 1, + }, + }, + response, + context, + ); + const responseData = await response.handle('get_request', context); + + t.assert.snapshot?.(stabilizeResponseOutput(responseData[0].text)); + }); + }); }); }); diff --git a/tests/tools/snapshot.test.ts b/tests/tools/snapshot.test.ts index 31857ff5a..718ea998c 100644 --- a/tests/tools/snapshot.test.ts +++ b/tests/tools/snapshot.test.ts @@ -21,7 +21,7 @@ describe('snapshot', () => { describe('browser_wait_for', () => { it('should work', async () => { await withBrowser(async (response, context) => { - const page = await context.getSelectedPage(); + const page = context.getSelectedPage(); await page.setContent( html`
Hello
World
`, @@ -98,7 +98,7 @@ describe('snapshot', () => { it('should work with iframe content', async () => { await withBrowser(async (response, context) => { - const page = await context.getSelectedPage(); + const page = context.getSelectedPage(); await page.setContent( html`

Top level

diff --git a/tests/utils.ts b/tests/utils.ts index 7adaaf533..babe5fe2f 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -130,3 +130,15 @@ export function html( `; } + +export function stabilizeResponseOutput(text: unknown) { + if (typeof text !== 'string') { + throw new Error('Input must be string'); + } + let output = text; + const dateRegEx = /.{3}, \d{2} .{3} \d{4} \d{2}:\d{2}:\d{2} [A-Z]{3}/g; + const localhostRegEx = /http:\/\/localhost:\d{5}\//g; + output = output.replaceAll(dateRegEx, ''); + output = output.replaceAll(localhostRegEx, 'http://localhost:/'); + return output; +} From b33ff912cf0b03c39d74c90e1a6e4899f491fe11 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Tue, 21 Oct 2025 11:34:55 +0200 Subject: [PATCH 09/12] stable tests --- src/McpContext.ts | 17 +++++++++++++++++ src/PageCollector.ts | 11 ++++++++--- tests/tools/network.test.js.snapshot | 2 +- tests/tools/network.test.ts | 5 ++++- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/McpContext.ts b/src/McpContext.ts index f2587cf0c..dd9186a6e 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -465,4 +465,21 @@ export class McpContext implements Context { return locator.wait(); } + + /** + * We need to ignore favicon request as they make our test flaky + */ + async setUpNetworkCollectorForTesting() { + this.#networkCollector = new NetworkCollector(this.browser, collect => { + return { + request: req => { + if (req.url().includes('favicon.ico')) { + return; + } + collect(req); + }, + } as ListenerMap; + }); + await this.#networkCollector.init(); + } } diff --git a/src/PageCollector.ts b/src/PageCollector.ts index 60b9596cb..39168dc8b 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -173,14 +173,19 @@ export class PageCollector { } export class NetworkCollector extends PageCollector { - constructor(browser: Browser) { - super(browser, collect => { + constructor( + browser: Browser, + listeners: ( + collector: (item: HTTPRequest) => void, + ) => ListenerMap = collect => { return { request: req => { collect(req); }, } as ListenerMap; - }); + }, + ) { + super(browser, listeners); } override splitAfterNavigation(page: Page) { const navigations = this.storage.get(page) ?? []; diff --git a/tests/tools/network.test.js.snapshot b/tests/tools/network.test.js.snapshot index ff23233a6..cb57de5fd 100644 --- a/tests/tools/network.test.js.snapshot +++ b/tests/tools/network.test.js.snapshot @@ -21,7 +21,7 @@ exports[`network > network_list_requests > list requests form current navigation # list_request response ## Network requests Showing 1-1 of 1 (Page 1 of 1). -reqid=4 GET http://localhost:/three [success - 200] +reqid=3 GET http://localhost:/three [success - 200] `; exports[`network > network_list_requests > list requests from previous navigations 1`] = ` diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index 52f261d2b..c5f666b91 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -30,6 +30,7 @@ describe('network', () => { server.addHtmlRoute('/three', html`
Third
`); await withBrowser(async (response, context) => { + await context.setUpNetworkCollectorForTesting(); const page = context.getSelectedPage(); await page.goto(server.getRoute('/one')); await page.goto(server.getRoute('/two')); @@ -52,6 +53,7 @@ describe('network', () => { server.addHtmlRoute('/three', html`
Third
`); await withBrowser(async (response, context) => { + await context.setUpNetworkCollectorForTesting(); const page = context.getSelectedPage(); await page.goto(server.getRoute('/one')); await page.goto(server.getRoute('/two')); @@ -96,12 +98,13 @@ describe('network', () => { assert(!response.includeNetworkRequests); }); }); - it.only('should get request from previous navigations', async t => { + it('should get request from previous navigations', async t => { server.addHtmlRoute('/one', html`
First
`); server.addHtmlRoute('/two', html`
Second
`); server.addHtmlRoute('/three', html`
Third
`); await withBrowser(async (response, context) => { + await context.setUpNetworkCollectorForTesting(); const page = context.getSelectedPage(); await page.goto(server.getRoute('/one')); await page.goto(server.getRoute('/two')); From fa06d2e0ae5a198e72972a88d762f580c06dbe67 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Tue, 21 Oct 2025 13:56:36 +0200 Subject: [PATCH 10/12] stabilize snapshot --- tests/tools/network.test.js.snapshot | 6 +++--- tests/utils.ts | 13 ++++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/tools/network.test.js.snapshot b/tests/tools/network.test.js.snapshot index cb57de5fd..44c063e66 100644 --- a/tests/tools/network.test.js.snapshot +++ b/tests/tools/network.test.js.snapshot @@ -5,10 +5,10 @@ Status: [success - 200] ### Request Headers - accept-language:en-US,en;q=0.9 - upgrade-insecure-requests:1 -- user-agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/141.0.0.0 Safari/537.36 -- sec-ch-ua:"Chromium";v="141", "Not?A_Brand";v="8" +- user-agent: +- sec-ch-ua:"Chromium";v="", "Not?A_Brand";v="8" - sec-ch-ua-mobile:?0 -- sec-ch-ua-platform:"Linux" +- sec-ch-ua-platform:"" ### Response Headers - connection:keep-alive - content-length:239 diff --git a/tests/utils.ts b/tests/utils.ts index babe5fe2f..17e86ca24 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -137,8 +137,19 @@ export function stabilizeResponseOutput(text: unknown) { } let output = text; const dateRegEx = /.{3}, \d{2} .{3} \d{4} \d{2}:\d{2}:\d{2} [A-Z]{3}/g; - const localhostRegEx = /http:\/\/localhost:\d{5}\//g; output = output.replaceAll(dateRegEx, ''); + + const localhostRegEx = /http:\/\/localhost:\d{5}\//g; output = output.replaceAll(localhostRegEx, 'http://localhost:/'); + + const userAgentRegEx = /user-agent:.*\n/g; + output = output.replaceAll(userAgentRegEx, 'user-agent:\n'); + + const chUaRegEx = /sec-ch-ua:"Chromium";v="\d{3}"/g; + output = output.replaceAll(chUaRegEx, 'sec-ch-ua:"Chromium";v=""'); + + // sec-ch-ua-platform:"Linux" + const chUaPlatformRegEx = /sec-ch-ua-platform:"[a-zA-Z]*"/g; + output = output.replaceAll(chUaPlatformRegEx, 'sec-ch-ua-platform:""'); return output; } From 53a85e21e2a82fecfb6bf93c9da8c195a615b965 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Tue, 21 Oct 2025 14:04:57 +0200 Subject: [PATCH 11/12] fix --- docs/tool-reference.md | 2 +- src/tools/network.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 3f7499fbd..d7cbc2f45 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -269,7 +269,7 @@ **Parameters:** -- **includePreviousNavigations** (boolean) _(optional)_: Whether to include request from previous navigations. +- **includePreviousNavigations** (boolean) _(optional)_: Whether to include requests from previous navigations. - **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page. - **pageSize** (integer) _(optional)_: Maximum number of requests to return. When omitted, returns all requests. - **resourceTypes** (array) _(optional)_: Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests. diff --git a/src/tools/network.ts b/src/tools/network.ts index f69242742..d684d05b4 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -66,7 +66,7 @@ export const listNetworkRequests = defineTool({ .boolean() .default(false) .optional() - .describe('Whether to include request from previous navigations.'), + .describe('Whether to include requests from previous navigations.'), }, handler: async (request, response) => { response.setIncludeNetworkRequests(true, { From fa0baa60e42e8b26f876e6c893e9d896390bcdd3 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Tue, 21 Oct 2025 14:17:56 +0200 Subject: [PATCH 12/12] rebase --- tests/tools/network.test.js.snapshot | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/tools/network.test.js.snapshot b/tests/tools/network.test.js.snapshot index 44c063e66..0a74003c1 100644 --- a/tests/tools/network.test.js.snapshot +++ b/tests/tools/network.test.js.snapshot @@ -15,6 +15,8 @@ Status: [success - 200] - content-type:text/html; charset=utf-8 - date: - keep-alive:timeout=5 +### Response Body + `; exports[`network > network_list_requests > list requests form current navigations only 1`] = `