Skip to content
1 change: 1 addition & 0 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@

- **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.

---

Expand Down
56 changes: 41 additions & 15 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ImageContent,
TextContent,
} from '@modelcontextprotocol/sdk/types.js';
import type {ResourceType} from 'puppeteer-core';

import {formatConsoleEvent} from './formatters/consoleFormatter.js';
import {
Expand All @@ -22,13 +23,16 @@ import {paginate, type PaginationOptions} from './utils/pagination.js';
export class McpResponse implements Response {
#includePages = false;
#includeSnapshot = false;
#includeNetworkRequests = false;
#attachedNetworkRequestUrl?: string;
#includeConsoleData = false;
#textResponseLines: string[] = [];
#formattedConsoleData?: string[];
#images: ImageContentData[] = [];
#networkRequestsPaginationOptions?: PaginationOptions;
#networkRequestsOptions?: {
include: boolean;
pagination?: PaginationOptions;
resourceTypes?: ResourceType[];
};

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

setIncludeNetworkRequests(
value: boolean,
options?: {pageSize?: number; pageIdx?: number},
options?: {
pageSize?: number;
pageIdx?: number;
resourceTypes?: ResourceType[];
},
): void {
this.#includeNetworkRequests = value;
if (!value || !options) {
this.#networkRequestsPaginationOptions = undefined;
if (!value) {
this.#networkRequestsOptions = undefined;
return;
}

this.#networkRequestsPaginationOptions = {
pageSize: options.pageSize,
pageIdx: options.pageIdx,
this.#networkRequestsOptions = {
include: value,
pagination:
options?.pageSize || options?.pageIdx
? {
pageSize: options.pageSize,
pageIdx: options.pageIdx,
}
: undefined,
resourceTypes: options?.resourceTypes,
};
}

Expand All @@ -67,7 +81,7 @@ export class McpResponse implements Response {
}

get includeNetworkRequests(): boolean {
return this.#includeNetworkRequests;
return this.#networkRequestsOptions?.include ?? false;
}

get includeConsoleData(): boolean {
Expand All @@ -77,7 +91,7 @@ export class McpResponse implements Response {
return this.#attachedNetworkRequestUrl;
}
get networkRequestsPageIdx(): number | undefined {
return this.#networkRequestsPaginationOptions?.pageIdx;
return this.#networkRequestsOptions?.pagination?.pageIdx;
}

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

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

if (this.#includeNetworkRequests) {
const requests = context.getNetworkRequests();
if (this.#networkRequestsOptions?.include) {
let requests = context.getNetworkRequests();

// Apply resource type filtering if specified
if (this.#networkRequestsOptions.resourceTypes?.length) {
const normalizedTypes = new Set(
this.#networkRequestsOptions.resourceTypes,
);
requests = requests.filter(request => {
const type = request.resourceType();
return normalizedTypes.has(type);
});
}

response.push('## Network requests');
if (requests.length) {
const paginationResult = paginate(
requests,
this.#networkRequestsPaginationOptions,
this.#networkRequestsOptions.pagination,
);
if (paginationResult.invalidPage) {
response.push('Invalid page number provided. Showing first page.');
Expand All @@ -197,7 +223,7 @@ Call browser_handle_dialog to handle it before continuing.`);
`Showing ${startIndex + 1}-${endIndex} of ${requests.length} (Page ${currentPage + 1} of ${totalPages}).`,
);

if (this.#networkRequestsPaginationOptions) {
if (this.#networkRequestsOptions.pagination) {
if (paginationResult.hasNextPage) {
response.push(`Next page: ${currentPage + 1}`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface Response {
setIncludePages(value: boolean): void;
setIncludeNetworkRequests(
value: boolean,
options?: {pageSize?: number; pageIdx?: number},
options?: {pageSize?: number; pageIdx?: number; resourceTypes?: string[]},
): void;
setIncludeConsoleData(value: boolean): void;
setIncludeSnapshot(value: boolean): void;
Expand Down
30 changes: 30 additions & 0 deletions src/tools/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,34 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type {ResourceType} from 'puppeteer-core';
import z from 'zod';

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

const FILTERABLE_RESOURCE_TYPES: readonly [ResourceType, ...ResourceType[]] = [
'document',
'stylesheet',
'image',
'media',
'font',
'script',
'texttrack',
'xhr',
'fetch',
'prefetch',
'eventsource',
'websocket',
'manifest',
'signedexchange',
'ping',
'cspviolationreport',
'preflight',
'fedcm',
'other',
];

export const listNetworkRequests = defineTool({
name: 'list_network_requests',
description: `List all requests for the currently selected page`,
Expand All @@ -33,11 +56,18 @@ export const listNetworkRequests = defineTool({
.describe(
'Page number to return (0-based). When omitted, returns the first page.',
),
resourceTypes: z
.array(z.enum(FILTERABLE_RESOURCE_TYPES))
.optional()
.describe(
'Filter requests to only return requests of the specified resource types. When omitted or empty, returns all requests.',
),
},
handler: async (request, response) => {
response.setIncludeNetworkRequests(true, {
pageSize: request.params.pageSize,
pageIdx: request.params.pageIdx,
resourceTypes: request.params.resourceTypes,
});
},
});
Expand Down
129 changes: 129 additions & 0 deletions tests/McpResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ http://example.com GET [pending]`,
);
});
});

it('does not include network requests when setting is false', async () => {
await withBrowser(async (response, context) => {
response.setIncludeNetworkRequests(false);
Expand Down Expand Up @@ -264,6 +265,134 @@ Log>`),
});
});

describe('McpResponse network request filtering', () => {
it('filters network requests by resource type', async () => {
await withBrowser(async (response, context) => {
response.setIncludeNetworkRequests(true, {
resourceTypes: ['script', 'stylesheet'],
});
context.getNetworkRequests = () => {
return [
getMockRequest({resourceType: 'script'}),
getMockRequest({resourceType: 'image'}),
getMockRequest({resourceType: 'stylesheet'}),
getMockRequest({resourceType: 'document'}),
];
};
const result = await response.handle('test', context);
assert.strictEqual(
result[0].text,
`# test response
## Network requests
Showing 1-2 of 2 (Page 1 of 1).
http://example.com GET [pending]
http://example.com GET [pending]`,
);
});
});

it('filters network requests by single resource type', async () => {
await withBrowser(async (response, context) => {
response.setIncludeNetworkRequests(true, {
resourceTypes: ['image'],
});
context.getNetworkRequests = () => {
return [
getMockRequest({resourceType: 'script'}),
getMockRequest({resourceType: 'image'}),
getMockRequest({resourceType: 'stylesheet'}),
];
};
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]`,
);
});
});

it('shows no requests when filter matches nothing', async () => {
await withBrowser(async (response, context) => {
response.setIncludeNetworkRequests(true, {
resourceTypes: ['font'],
});
context.getNetworkRequests = () => {
return [
getMockRequest({resourceType: 'script'}),
getMockRequest({resourceType: 'image'}),
getMockRequest({resourceType: 'stylesheet'}),
];
};
const result = await response.handle('test', context);
assert.strictEqual(
result[0].text,
`# test response
## Network requests
No requests found.`,
);
});
});

it('shows all requests when no filters are provided', async () => {
await withBrowser(async (response, context) => {
response.setIncludeNetworkRequests(true);
context.getNetworkRequests = () => {
return [
getMockRequest({resourceType: 'script'}),
getMockRequest({resourceType: 'image'}),
getMockRequest({resourceType: 'stylesheet'}),
getMockRequest({resourceType: 'document'}),
getMockRequest({resourceType: 'font'}),
];
};
const result = await response.handle('test', context);
assert.strictEqual(
result[0].text,
`# 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]`,
);
});
});

it('shows all requests when empty resourceTypes array is provided', async () => {
await withBrowser(async (response, context) => {
response.setIncludeNetworkRequests(true, {
resourceTypes: [],
});
context.getNetworkRequests = () => {
return [
getMockRequest({resourceType: 'script'}),
getMockRequest({resourceType: 'image'}),
getMockRequest({resourceType: 'stylesheet'}),
getMockRequest({resourceType: 'document'}),
getMockRequest({resourceType: 'font'}),
];
};
const result = await response.handle('test', context);
assert.strictEqual(
result[0].text,
`# 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]`,
);
});
});
});

describe('McpResponse network pagination', () => {
it('returns all requests when pagination is not provided', async () => {
await withBrowser(async (response, context) => {
Expand Down
4 changes: 4 additions & 0 deletions tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function getMockRequest(
method?: string;
response?: HTTPResponse;
failure?: HTTPRequest['failure'];
resourceType?: string;
} = {},
): HTTPRequest {
return {
Expand All @@ -61,6 +62,9 @@ export function getMockRequest(
failure() {
return options.failure?.() ?? null;
},
resourceType() {
return options.resourceType ?? 'document';
},
headers(): Record<string, string> {
return {
'content-size': '10',
Expand Down
Loading