Skip to content

Commit 4c909bb

Browse files
authored
feat: Add pagination list_network_requests (#145)
## Summary This PR enhances the `list_network_requests` tool with pagination support to handle large numbers of network requests efficiently. See [this issue](#136). ## Motivation In my experience, the `list_network_requests` tool frequently hits LLM token limits on pages with many requests, making it unusable for modern web applications. I wanted to add pagination to allow agents to be more flexible and manage token limits on their own. ## Changes ### Pagination Support - Added `pageSize` parameter to limit requests per call - Added `pageToken` parameter for navigation between pages - Added pagination metadata in responses (nextPageToken, previousPageToken, startIndex, endIndex, total) ### Implementation Details - **New utility**: `src/utils/pagination.ts` - generic pagination function - **Enhanced McpResponse**: Added pagination options to `setIncludeNetworkRequests()` - **Updated network tool**: Added pagination parameters to schema - **Offset-based pagination**: Uses numeric tokens, handles invalid tokens gracefully ## Testing - Comprehensive test coverage for pagination scenarios - Tests for first page, subsequent pages, invalid tokens, and edge cases - All existing tests continue to pass ## Backward Compatibility - If no pagination parameters are provided, the MCP will return all requests (same as before)
1 parent d64ba0d commit 4c909bb

File tree

7 files changed

+230
-7
lines changed

7 files changed

+230
-7
lines changed

docs/tool-reference.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,10 @@
262262

263263
**Description:** List all requests for the currently selected page
264264

265-
**Parameters:** None
265+
**Parameters:**
266+
267+
- **pageIdx** (integer) _(optional)_: Page number to return (0-based). When omitted, returns the first page.
268+
- **pageSize** (integer) _(optional)_: Maximum number of requests to return. When omitted, returns all requests.
266269

267270
---
268271

src/McpResponse.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from './formatters/networkFormatter.js';
1414
import {formatA11ySnapshot} from './formatters/snapshotFormatter.js';
1515
import {formatConsoleEvent} from './formatters/consoleFormatter.js';
16+
import {paginate, type PaginationOptions} from './utils/pagination.js';
1617

1718
export class McpResponse implements Response {
1819
#includePages: boolean = false;
@@ -23,6 +24,7 @@ export class McpResponse implements Response {
2324
#textResponseLines: string[] = [];
2425
#formattedConsoleData?: string[];
2526
#images: ImageContentData[] = [];
27+
#networkRequestsPaginationOptions?: PaginationOptions;
2628

2729
setIncludePages(value: boolean): void {
2830
this.#includePages = value;
@@ -32,8 +34,20 @@ export class McpResponse implements Response {
3234
this.#includeSnapshot = value;
3335
}
3436

35-
setIncludeNetworkRequests(value: boolean): void {
37+
setIncludeNetworkRequests(
38+
value: boolean,
39+
options?: {pageSize?: number; pageIdx?: number},
40+
): void {
3641
this.#includeNetworkRequests = value;
42+
if (!value || !options) {
43+
this.#networkRequestsPaginationOptions = undefined;
44+
return;
45+
}
46+
47+
this.#networkRequestsPaginationOptions = {
48+
pageSize: options.pageSize,
49+
pageIdx: options.pageIdx,
50+
};
3751
}
3852

3953
setIncludeConsoleData(value: boolean): void {
@@ -58,6 +72,9 @@ export class McpResponse implements Response {
5872
get attachedNetworkRequestUrl(): string | undefined {
5973
return this.#attachedNetworkRequestUrl;
6074
}
75+
get networkRequestsPageIdx(): number | undefined {
76+
return this.#networkRequestsPaginationOptions?.pageIdx;
77+
}
6178

6279
appendResponseLine(value: string): void {
6380
this.#textResponseLines.push(value);
@@ -162,7 +179,30 @@ Call browser_handle_dialog to handle it before continuing.`);
162179
const requests = context.getNetworkRequests();
163180
response.push('## Network requests');
164181
if (requests.length) {
165-
for (const request of requests) {
182+
const paginationResult = paginate(
183+
requests,
184+
this.#networkRequestsPaginationOptions,
185+
);
186+
if (paginationResult.invalidPage) {
187+
response.push('Invalid page number provided. Showing first page.');
188+
}
189+
190+
const {startIndex, endIndex, currentPage, totalPages} =
191+
paginationResult;
192+
response.push(
193+
`Showing ${startIndex + 1}-${endIndex} of ${requests.length} (Page ${currentPage + 1} of ${totalPages}).`,
194+
);
195+
196+
if (this.#networkRequestsPaginationOptions) {
197+
if (paginationResult.hasNextPage) {
198+
response.push(`Next page: ${currentPage + 1}`);
199+
}
200+
if (paginationResult.hasPreviousPage) {
201+
response.push(`Previous page: ${currentPage - 1}`);
202+
}
203+
}
204+
205+
for (const request of paginationResult.items) {
166206
response.push(getShortDescriptionForRequest(request));
167207
}
168208
} else {

src/tools/ToolDefinition.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@ export type ImageContentData = {
4242
export interface Response {
4343
appendResponseLine(value: string): void;
4444
setIncludePages(value: boolean): void;
45-
setIncludeNetworkRequests(value: boolean): void;
45+
setIncludeNetworkRequests(
46+
value: boolean,
47+
options?: {pageSize?: number; pageIdx?: number},
48+
): void;
4649
setIncludeConsoleData(value: boolean): void;
4750
setIncludeSnapshot(value: boolean): void;
4851
attachImage(value: ImageContentData): void;

src/tools/network.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,29 @@ export const listNetworkRequests = defineTool({
1515
category: ToolCategories.NETWORK,
1616
readOnlyHint: true,
1717
},
18-
schema: {},
19-
handler: async (_request, response) => {
20-
response.setIncludeNetworkRequests(true);
18+
schema: {
19+
pageSize: z
20+
.number()
21+
.int()
22+
.positive()
23+
.optional()
24+
.describe(
25+
'Maximum number of requests to return. When omitted, returns all requests.',
26+
),
27+
pageIdx: z
28+
.number()
29+
.int()
30+
.min(0)
31+
.optional()
32+
.describe(
33+
'Page number to return (0-based). When omitted, returns the first page.',
34+
),
35+
},
36+
handler: async (request, response) => {
37+
response.setIncludeNetworkRequests(true, {
38+
pageSize: request.params.pageSize,
39+
pageIdx: request.params.pageIdx,
40+
});
2141
},
2242
});
2343

src/utils/pagination.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
export type PaginationOptions = {
8+
pageSize?: number;
9+
pageIdx?: number;
10+
};
11+
12+
export type PaginationResult<TItem> = {
13+
items: readonly TItem[];
14+
currentPage: number;
15+
totalPages: number;
16+
hasNextPage: boolean;
17+
hasPreviousPage: boolean;
18+
startIndex: number;
19+
endIndex: number;
20+
invalidPage: boolean;
21+
};
22+
23+
const DEFAULT_PAGE_SIZE = 20;
24+
25+
export function paginate<TItem>(
26+
items: readonly TItem[],
27+
options?: PaginationOptions,
28+
): PaginationResult<TItem> {
29+
const total = items.length;
30+
31+
if (!options || noPaginationOptions(options)) {
32+
return {
33+
items,
34+
currentPage: 0,
35+
totalPages: 1,
36+
hasNextPage: false,
37+
hasPreviousPage: false,
38+
startIndex: 0,
39+
endIndex: total,
40+
invalidPage: false,
41+
};
42+
}
43+
44+
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
45+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
46+
const {currentPage, invalidPage} = resolvePageIndex(
47+
options.pageIdx,
48+
totalPages,
49+
);
50+
51+
const startIndex = currentPage * pageSize;
52+
const pageItems = items.slice(startIndex, startIndex + pageSize);
53+
const endIndex = startIndex + pageItems.length;
54+
55+
return {
56+
items: pageItems,
57+
currentPage,
58+
totalPages,
59+
hasNextPage: currentPage < totalPages - 1,
60+
hasPreviousPage: currentPage > 0,
61+
startIndex,
62+
endIndex,
63+
invalidPage,
64+
};
65+
}
66+
67+
function noPaginationOptions(options: PaginationOptions): boolean {
68+
return options.pageSize === undefined && options.pageIdx === undefined;
69+
}
70+
71+
function resolvePageIndex(
72+
pageIdx: number | undefined,
73+
totalPages: number,
74+
): {
75+
currentPage: number;
76+
invalidPage: boolean;
77+
} {
78+
if (pageIdx === undefined) {
79+
return {currentPage: 0, invalidPage: false};
80+
}
81+
82+
if (pageIdx < 0 || pageIdx >= totalPages) {
83+
return {currentPage: 0, invalidPage: true};
84+
}
85+
86+
return {currentPage: pageIdx, invalidPage: false};
87+
}

tests/McpResponse.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ Call browser_handle_dialog to handle it before continuing.`,
185185
result[0].text,
186186
`# test response
187187
## Network requests
188+
Showing 1-1 of 1 (Page 1 of 1).
188189
http://example.com GET [pending]`,
189190
);
190191
});
@@ -217,6 +218,7 @@ Status: [pending]
217218
### Request Headers
218219
- content-size:10
219220
## Network requests
221+
Showing 1-1 of 1 (Page 1 of 1).
220222
http://example.com GET [pending]`,
221223
);
222224
});
@@ -261,3 +263,70 @@ Log>`),
261263
});
262264
});
263265
});
266+
267+
describe('McpResponse network pagination', () => {
268+
it('returns all requests when pagination is not provided', async () => {
269+
await withBrowser(async (response, context) => {
270+
const requests = Array.from({length: 5}, () => getMockRequest());
271+
context.getNetworkRequests = () => requests;
272+
response.setIncludeNetworkRequests(true);
273+
const result = await response.handle('test', context);
274+
const text = (result[0].text as string).toString();
275+
assert.ok(text.includes('Showing 1-5 of 5 (Page 1 of 1).'));
276+
assert.ok(!text.includes('Next page:'));
277+
assert.ok(!text.includes('Previous page:'));
278+
});
279+
});
280+
281+
it('returns first page by default', async () => {
282+
await withBrowser(async (response, context) => {
283+
const requests = Array.from({length: 30}, (_, idx) =>
284+
getMockRequest({method: `GET-${idx}`}),
285+
);
286+
context.getNetworkRequests = () => {
287+
return requests;
288+
};
289+
response.setIncludeNetworkRequests(true, {pageSize: 10});
290+
const result = await response.handle('test', context);
291+
const text = (result[0].text as string).toString();
292+
assert.ok(text.includes('Showing 1-10 of 30 (Page 1 of 3).'));
293+
assert.ok(text.includes('Next page: 1'));
294+
assert.ok(!text.includes('Previous page:'));
295+
});
296+
});
297+
298+
it('returns subsequent page when pageIdx provided', async () => {
299+
await withBrowser(async (response, context) => {
300+
const requests = Array.from({length: 25}, (_, idx) =>
301+
getMockRequest({method: `GET-${idx}`}),
302+
);
303+
context.getNetworkRequests = () => requests;
304+
response.setIncludeNetworkRequests(true, {
305+
pageSize: 10,
306+
pageIdx: 1,
307+
});
308+
const result = await response.handle('test', context);
309+
const text = (result[0].text as string).toString();
310+
assert.ok(text.includes('Showing 11-20 of 25 (Page 2 of 3).'));
311+
assert.ok(text.includes('Next page: 2'));
312+
assert.ok(text.includes('Previous page: 0'));
313+
});
314+
});
315+
316+
it('handles invalid page number by showing first page', async () => {
317+
await withBrowser(async (response, context) => {
318+
const requests = Array.from({length: 5}, () => getMockRequest());
319+
context.getNetworkRequests = () => requests;
320+
response.setIncludeNetworkRequests(true, {
321+
pageSize: 2,
322+
pageIdx: 10, // Invalid page number
323+
});
324+
const result = await response.handle('test', context);
325+
const text = (result[0].text as string).toString();
326+
assert.ok(
327+
text.includes('Invalid page number provided. Showing first page.'),
328+
);
329+
assert.ok(text.includes('Showing 1-2 of 5 (Page 1 of 3).'));
330+
});
331+
});
332+
});

tests/tools/network.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('network', () => {
1818
await withBrowser(async (response, context) => {
1919
await listNetworkRequests.handler({params: {}}, response, context);
2020
assert.ok(response.includeNetworkRequests);
21+
assert.strictEqual(response.networkRequestsPageIdx, undefined);
2122
});
2223
});
2324
});

0 commit comments

Comments
 (0)