Skip to content

Commit 59d81a3

Browse files
authored
feat: add network request filtering by resource type (#162)
### Summary This PR build upon #145 to also adds filtering by resource type to `list_network_requests` (Also see: #137 and #107). ### Motivation Agents often need specific request types (e.g., scripts, stylesheets, images). Filtering reduces noise and improves performance. ### Changes - **New parameter**: `resourceType` (array) to filter by resource types - **Supported types**: all resource types supported by Puppeteer - **Backward compatible**: when omitted, returns all requests Filtering runs before pagination, so pagination applies to the filtered results.
1 parent 6cfc977 commit 59d81a3

File tree

6 files changed

+206
-16
lines changed

6 files changed

+206
-16
lines changed

docs/tool-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@
266266

267267
- **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page.
268268
- **pageSize** (integer) _(optional)_: Maximum number of requests to return. When omitted, returns all requests.
269+
- **resourceTypes** (array) _(optional)_: Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests.
269270

270271
---
271272

src/McpResponse.ts

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
ImageContent,
88
TextContent,
99
} from '@modelcontextprotocol/sdk/types.js';
10+
import type {ResourceType} from 'puppeteer-core';
1011

1112
import {formatConsoleEvent} from './formatters/consoleFormatter.js';
1213
import {
@@ -22,13 +23,16 @@ import {paginate, type PaginationOptions} from './utils/pagination.js';
2223
export class McpResponse implements Response {
2324
#includePages = false;
2425
#includeSnapshot = false;
25-
#includeNetworkRequests = false;
2626
#attachedNetworkRequestUrl?: string;
2727
#includeConsoleData = false;
2828
#textResponseLines: string[] = [];
2929
#formattedConsoleData?: string[];
3030
#images: ImageContentData[] = [];
31-
#networkRequestsPaginationOptions?: PaginationOptions;
31+
#networkRequestsOptions?: {
32+
include: boolean;
33+
pagination?: PaginationOptions;
34+
resourceTypes?: ResourceType[];
35+
};
3236

3337
setIncludePages(value: boolean): void {
3438
this.#includePages = value;
@@ -40,17 +44,27 @@ export class McpResponse implements Response {
4044

4145
setIncludeNetworkRequests(
4246
value: boolean,
43-
options?: {pageSize?: number; pageIdx?: number},
47+
options?: {
48+
pageSize?: number;
49+
pageIdx?: number;
50+
resourceTypes?: ResourceType[];
51+
},
4452
): void {
45-
this.#includeNetworkRequests = value;
46-
if (!value || !options) {
47-
this.#networkRequestsPaginationOptions = undefined;
53+
if (!value) {
54+
this.#networkRequestsOptions = undefined;
4855
return;
4956
}
5057

51-
this.#networkRequestsPaginationOptions = {
52-
pageSize: options.pageSize,
53-
pageIdx: options.pageIdx,
58+
this.#networkRequestsOptions = {
59+
include: value,
60+
pagination:
61+
options?.pageSize || options?.pageIdx
62+
? {
63+
pageSize: options.pageSize,
64+
pageIdx: options.pageIdx,
65+
}
66+
: undefined,
67+
resourceTypes: options?.resourceTypes,
5468
};
5569
}
5670

@@ -67,7 +81,7 @@ export class McpResponse implements Response {
6781
}
6882

6983
get includeNetworkRequests(): boolean {
70-
return this.#includeNetworkRequests;
84+
return this.#networkRequestsOptions?.include ?? false;
7185
}
7286

7387
get includeConsoleData(): boolean {
@@ -77,7 +91,7 @@ export class McpResponse implements Response {
7791
return this.#attachedNetworkRequestUrl;
7892
}
7993
get networkRequestsPageIdx(): number | undefined {
80-
return this.#networkRequestsPaginationOptions?.pageIdx;
94+
return this.#networkRequestsOptions?.pagination?.pageIdx;
8195
}
8296

8397
appendResponseLine(value: string): void {
@@ -179,13 +193,25 @@ Call browser_handle_dialog to handle it before continuing.`);
179193

180194
response.push(...this.#getIncludeNetworkRequestsData(context));
181195

182-
if (this.#includeNetworkRequests) {
183-
const requests = context.getNetworkRequests();
196+
if (this.#networkRequestsOptions?.include) {
197+
let requests = context.getNetworkRequests();
198+
199+
// Apply resource type filtering if specified
200+
if (this.#networkRequestsOptions.resourceTypes?.length) {
201+
const normalizedTypes = new Set(
202+
this.#networkRequestsOptions.resourceTypes,
203+
);
204+
requests = requests.filter(request => {
205+
const type = request.resourceType();
206+
return normalizedTypes.has(type);
207+
});
208+
}
209+
184210
response.push('## Network requests');
185211
if (requests.length) {
186212
const paginationResult = paginate(
187213
requests,
188-
this.#networkRequestsPaginationOptions,
214+
this.#networkRequestsOptions.pagination,
189215
);
190216
if (paginationResult.invalidPage) {
191217
response.push('Invalid page number provided. Showing first page.');
@@ -197,7 +223,7 @@ Call browser_handle_dialog to handle it before continuing.`);
197223
`Showing ${startIndex + 1}-${endIndex} of ${requests.length} (Page ${currentPage + 1} of ${totalPages}).`,
198224
);
199225

200-
if (this.#networkRequestsPaginationOptions) {
226+
if (this.#networkRequestsOptions.pagination) {
201227
if (paginationResult.hasNextPage) {
202228
response.push(`Next page: ${currentPage + 1}`);
203229
}

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export interface Response {
4444
setIncludePages(value: boolean): void;
4545
setIncludeNetworkRequests(
4646
value: boolean,
47-
options?: {pageSize?: number; pageIdx?: number},
47+
options?: {pageSize?: number; pageIdx?: number; resourceTypes?: string[]},
4848
): void;
4949
setIncludeConsoleData(value: boolean): void;
5050
setIncludeSnapshot(value: boolean): void;

src/tools/network.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,34 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import type {ResourceType} from 'puppeteer-core';
78
import z from 'zod';
89

910
import {ToolCategories} from './categories.js';
1011
import {defineTool} from './ToolDefinition.js';
1112

13+
const FILTERABLE_RESOURCE_TYPES: readonly [ResourceType, ...ResourceType[]] = [
14+
'document',
15+
'stylesheet',
16+
'image',
17+
'media',
18+
'font',
19+
'script',
20+
'texttrack',
21+
'xhr',
22+
'fetch',
23+
'prefetch',
24+
'eventsource',
25+
'websocket',
26+
'manifest',
27+
'signedexchange',
28+
'ping',
29+
'cspviolationreport',
30+
'preflight',
31+
'fedcm',
32+
'other',
33+
];
34+
1235
export const listNetworkRequests = defineTool({
1336
name: 'list_network_requests',
1437
description: `List all requests for the currently selected page`,
@@ -33,11 +56,18 @@ export const listNetworkRequests = defineTool({
3356
.describe(
3457
'Page number to return (0-based). When omitted, returns the first page.',
3558
),
59+
resourceTypes: z
60+
.array(z.enum(FILTERABLE_RESOURCE_TYPES))
61+
.optional()
62+
.describe(
63+
'Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests.',
64+
),
3665
},
3766
handler: async (request, response) => {
3867
response.setIncludeNetworkRequests(true, {
3968
pageSize: request.params.pageSize,
4069
pageIdx: request.params.pageIdx,
70+
resourceTypes: request.params.resourceTypes,
4171
});
4272
},
4373
});

tests/McpResponse.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ http://example.com GET [pending]`,
190190
);
191191
});
192192
});
193+
193194
it('does not include network requests when setting is false', async () => {
194195
await withBrowser(async (response, context) => {
195196
response.setIncludeNetworkRequests(false);
@@ -264,6 +265,134 @@ Log>`),
264265
});
265266
});
266267

268+
describe('McpResponse network request filtering', () => {
269+
it('filters network requests by resource type', async () => {
270+
await withBrowser(async (response, context) => {
271+
response.setIncludeNetworkRequests(true, {
272+
resourceTypes: ['script', 'stylesheet'],
273+
});
274+
context.getNetworkRequests = () => {
275+
return [
276+
getMockRequest({resourceType: 'script'}),
277+
getMockRequest({resourceType: 'image'}),
278+
getMockRequest({resourceType: 'stylesheet'}),
279+
getMockRequest({resourceType: 'document'}),
280+
];
281+
};
282+
const result = await response.handle('test', context);
283+
assert.strictEqual(
284+
result[0].text,
285+
`# test response
286+
## Network requests
287+
Showing 1-2 of 2 (Page 1 of 1).
288+
http://example.com GET [pending]
289+
http://example.com GET [pending]`,
290+
);
291+
});
292+
});
293+
294+
it('filters network requests by single resource type', async () => {
295+
await withBrowser(async (response, context) => {
296+
response.setIncludeNetworkRequests(true, {
297+
resourceTypes: ['image'],
298+
});
299+
context.getNetworkRequests = () => {
300+
return [
301+
getMockRequest({resourceType: 'script'}),
302+
getMockRequest({resourceType: 'image'}),
303+
getMockRequest({resourceType: 'stylesheet'}),
304+
];
305+
};
306+
const result = await response.handle('test', context);
307+
assert.strictEqual(
308+
result[0].text,
309+
`# test response
310+
## Network requests
311+
Showing 1-1 of 1 (Page 1 of 1).
312+
http://example.com GET [pending]`,
313+
);
314+
});
315+
});
316+
317+
it('shows no requests when filter matches nothing', async () => {
318+
await withBrowser(async (response, context) => {
319+
response.setIncludeNetworkRequests(true, {
320+
resourceTypes: ['font'],
321+
});
322+
context.getNetworkRequests = () => {
323+
return [
324+
getMockRequest({resourceType: 'script'}),
325+
getMockRequest({resourceType: 'image'}),
326+
getMockRequest({resourceType: 'stylesheet'}),
327+
];
328+
};
329+
const result = await response.handle('test', context);
330+
assert.strictEqual(
331+
result[0].text,
332+
`# test response
333+
## Network requests
334+
No requests found.`,
335+
);
336+
});
337+
});
338+
339+
it('shows all requests when no filters are provided', async () => {
340+
await withBrowser(async (response, context) => {
341+
response.setIncludeNetworkRequests(true);
342+
context.getNetworkRequests = () => {
343+
return [
344+
getMockRequest({resourceType: 'script'}),
345+
getMockRequest({resourceType: 'image'}),
346+
getMockRequest({resourceType: 'stylesheet'}),
347+
getMockRequest({resourceType: 'document'}),
348+
getMockRequest({resourceType: 'font'}),
349+
];
350+
};
351+
const result = await response.handle('test', context);
352+
assert.strictEqual(
353+
result[0].text,
354+
`# test response
355+
## Network requests
356+
Showing 1-5 of 5 (Page 1 of 1).
357+
http://example.com GET [pending]
358+
http://example.com GET [pending]
359+
http://example.com GET [pending]
360+
http://example.com GET [pending]
361+
http://example.com GET [pending]`,
362+
);
363+
});
364+
});
365+
366+
it('shows all requests when empty resourceTypes array is provided', async () => {
367+
await withBrowser(async (response, context) => {
368+
response.setIncludeNetworkRequests(true, {
369+
resourceTypes: [],
370+
});
371+
context.getNetworkRequests = () => {
372+
return [
373+
getMockRequest({resourceType: 'script'}),
374+
getMockRequest({resourceType: 'image'}),
375+
getMockRequest({resourceType: 'stylesheet'}),
376+
getMockRequest({resourceType: 'document'}),
377+
getMockRequest({resourceType: 'font'}),
378+
];
379+
};
380+
const result = await response.handle('test', context);
381+
assert.strictEqual(
382+
result[0].text,
383+
`# test response
384+
## Network requests
385+
Showing 1-5 of 5 (Page 1 of 1).
386+
http://example.com GET [pending]
387+
http://example.com GET [pending]
388+
http://example.com GET [pending]
389+
http://example.com GET [pending]
390+
http://example.com GET [pending]`,
391+
);
392+
});
393+
});
394+
});
395+
267396
describe('McpResponse network pagination', () => {
268397
it('returns all requests when pagination is not provided', async () => {
269398
await withBrowser(async (response, context) => {

tests/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function getMockRequest(
4646
method?: string;
4747
response?: HTTPResponse;
4848
failure?: HTTPRequest['failure'];
49+
resourceType?: string;
4950
} = {},
5051
): HTTPRequest {
5152
return {
@@ -61,6 +62,9 @@ export function getMockRequest(
6162
failure() {
6263
return options.failure?.() ?? null;
6364
},
65+
resourceType() {
66+
return options.resourceType ?? 'document';
67+
},
6468
headers(): Record<string, string> {
6569
return {
6670
'content-size': '10',

0 commit comments

Comments
 (0)