Skip to content

Commit 6925f5a

Browse files
committed
feat: create a generic pagination utility
1 parent 9ce7636 commit 6925f5a

File tree

4 files changed

+107
-121
lines changed

4 files changed

+107
-121
lines changed

src/McpResponse.ts

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ import {
1313
} from './formatters/networkFormatter.js';
1414
import { formatA11ySnapshot } from './formatters/snapshotFormatter.js';
1515
import { formatConsoleEvent } from './formatters/consoleFormatter.js';
16-
import {
17-
paginateNetworkRequests,
18-
type NetworkPaginationOptions,
19-
} from './utils/networkPagination.js';
16+
import { paginate, type PaginationOptions } from './utils/pagination.js';
2017

2118
export class McpResponse implements Response {
2219
#includePages: boolean = false;
@@ -27,7 +24,7 @@ export class McpResponse implements Response {
2724
#textResponseLines: string[] = [];
2825
#formattedConsoleData?: string[];
2926
#images: ImageContentData[] = [];
30-
#networkRequestsPaginationOptions?: NetworkPaginationOptions;
27+
#networkRequestsPaginationOptions?: PaginationOptions;
3128

3229
setIncludePages(value: boolean): void {
3330
this.#includePages = value;
@@ -183,25 +180,26 @@ Call browser_handle_dialog to handle it before continuing.`);
183180
const requests = context.getNetworkRequests();
184181
response.push('## Network requests');
185182
if (requests.length) {
186-
const paginationResult = paginateNetworkRequests(
187-
requests,
188-
this.#networkRequestsPaginationOptions,
189-
);
183+
const paginationResult = paginate(requests, this.#networkRequestsPaginationOptions);
190184
if (paginationResult.invalidToken) {
191185
response.push('Invalid page token provided. Showing first page.');
192186
}
193-
const { startIndex, endIndex } = paginationResult;
194-
response.push(
195-
`Showing ${startIndex + 1}-${endIndex} of ${requests.length}.`,
196-
);
197-
for (const request of paginationResult.requests) {
198-
response.push(getShortDescriptionForRequest(request));
199-
}
200-
if (paginationResult.nextPageToken) {
201-
response.push(`Next: ${paginationResult.nextPageToken}`);
187+
188+
if (this.#networkRequestsPaginationOptions) {
189+
const { startIndex, endIndex } = paginationResult;
190+
response.push(
191+
`Showing ${startIndex + 1}-${endIndex} of ${requests.length}.`,
192+
);
193+
if (paginationResult.nextPageToken) {
194+
response.push(`Next: ${paginationResult.nextPageToken}`);
195+
}
196+
if (paginationResult.previousPageToken) {
197+
response.push(`Prev: ${paginationResult.previousPageToken}`);
198+
}
202199
}
203-
if (paginationResult.previousPageToken) {
204-
response.push(`Prev: ${paginationResult.previousPageToken}`);
200+
201+
for (const request of paginationResult.items) {
202+
response.push(getShortDescriptionForRequest(request));
205203
}
206204
} else {
207205
response.push('No requests found.');

src/tools/network.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
*/
66

77
import z from 'zod';
8-
import {defineTool} from './ToolDefinition.js';
9-
import {ToolCategories} from './categories.js';
8+
import { defineTool } from './ToolDefinition.js';
9+
import { ToolCategories } from './categories.js';
1010

1111
export const listNetworkRequests = defineTool({
1212
name: 'list_network_requests',
@@ -20,7 +20,6 @@ export const listNetworkRequests = defineTool({
2020
.number()
2121
.int()
2222
.positive()
23-
.max(100)
2423
.optional()
2524
.describe(
2625
'Maximum number of requests to return. When omitted, returns all requests.',

src/utils/networkPagination.ts

Lines changed: 0 additions & 98 deletions
This file was deleted.

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+
pageToken?: string;
10+
};
11+
12+
export type PaginationResult<TItem> = {
13+
items: readonly TItem[];
14+
nextPageToken?: string;
15+
previousPageToken?: string;
16+
startIndex: number;
17+
endIndex: number;
18+
invalidToken: boolean;
19+
};
20+
21+
const DEFAULT_PAGE_SIZE = 20;
22+
23+
export function paginate<TItem>(
24+
items: readonly TItem[],
25+
options?: PaginationOptions,
26+
): PaginationResult<TItem> {
27+
const total = items.length;
28+
29+
if (!options || noPaginationOptions(options)) {
30+
return {
31+
items,
32+
nextPageToken: undefined,
33+
previousPageToken: undefined,
34+
startIndex: 0,
35+
endIndex: total,
36+
invalidToken: false,
37+
};
38+
}
39+
40+
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
41+
const { startIndex, invalidToken } = resolveStartIndex(options.pageToken, total);
42+
43+
const pageItems = items.slice(startIndex, startIndex + pageSize);
44+
const endIndex = startIndex + pageItems.length;
45+
46+
const nextPageToken = endIndex < total ? String(endIndex) : undefined;
47+
const previousPageToken =
48+
startIndex > 0 ? String(Math.max(startIndex - pageSize, 0)) : undefined;
49+
50+
return {
51+
items: pageItems,
52+
nextPageToken,
53+
previousPageToken,
54+
startIndex,
55+
endIndex,
56+
invalidToken,
57+
};
58+
}
59+
60+
function noPaginationOptions(options: PaginationOptions): boolean {
61+
return (
62+
options.pageSize === undefined &&
63+
(options.pageToken === undefined || options.pageToken === null)
64+
);
65+
}
66+
67+
68+
function resolveStartIndex(
69+
pageToken: string | undefined,
70+
total: number,
71+
): {
72+
startIndex: number;
73+
invalidToken: boolean;
74+
} {
75+
if (pageToken === undefined || pageToken === null) {
76+
return { startIndex: 0, invalidToken: false };
77+
}
78+
79+
const parsed = Number.parseInt(pageToken, 10);
80+
if (Number.isNaN(parsed) || parsed < 0 || parsed >= total) {
81+
return { startIndex: 0, invalidToken: true };
82+
}
83+
84+
return { startIndex: parsed, invalidToken: false };
85+
}
86+
87+

0 commit comments

Comments
 (0)