Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,10 @@

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

**Parameters:** None
**Parameters:**

- **pageSize** (integer) _(optional)_: Maximum number of requests to return. When omitted, returns all requests.
- **pageToken** (string) _(optional)_: Opaque token representing the next page. Use the token returned by a previous call.

---

Expand Down
44 changes: 42 additions & 2 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from './formatters/networkFormatter.js';
import {formatA11ySnapshot} from './formatters/snapshotFormatter.js';
import {formatConsoleEvent} from './formatters/consoleFormatter.js';
import {paginate, type PaginationOptions} from './utils/pagination.js';

export class McpResponse implements Response {
#includePages: boolean = false;
Expand All @@ -23,6 +24,7 @@ export class McpResponse implements Response {
#textResponseLines: string[] = [];
#formattedConsoleData?: string[];
#images: ImageContentData[] = [];
#networkRequestsPaginationOptions?: PaginationOptions;

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

setIncludeNetworkRequests(value: boolean): void {
setIncludeNetworkRequests(
value: boolean,
options?: {pageSize?: number; pageToken?: string | null},
): void {
this.#includeNetworkRequests = value;
if (!value || !options) {
this.#networkRequestsPaginationOptions = undefined;
return;
}

this.#networkRequestsPaginationOptions = {
pageSize: options.pageSize,
pageToken: options.pageToken ?? undefined,
};
}

setIncludeConsoleData(value: boolean): void {
Expand All @@ -58,6 +72,10 @@ export class McpResponse implements Response {
get attachedNetworkRequestUrl(): string | undefined {
return this.#attachedNetworkRequestUrl;
}
get networkRequestsPageToken(): string | undefined {
const token = this.#networkRequestsPaginationOptions?.pageToken;
return token ?? undefined;
}

appendResponseLine(value: string): void {
this.#textResponseLines.push(value);
Expand Down Expand Up @@ -162,7 +180,29 @@ Call browser_handle_dialog to handle it before continuing.`);
const requests = context.getNetworkRequests();
response.push('## Network requests');
if (requests.length) {
for (const request of requests) {
const paginationResult = paginate(
requests,
this.#networkRequestsPaginationOptions,
);
if (paginationResult.invalidToken) {
response.push('Invalid page token provided. Showing first page.');
}

const {startIndex, endIndex} = paginationResult;
response.push(
`Showing ${startIndex + 1}-${endIndex} of ${requests.length}.`,
);

if (this.#networkRequestsPaginationOptions) {
if (paginationResult.nextPageToken) {
response.push(`Next: ${paginationResult.nextPageToken}`);
}
if (paginationResult.previousPageToken) {
response.push(`Prev: ${paginationResult.previousPageToken}`);
}
}

for (const request of paginationResult.items) {
response.push(getShortDescriptionForRequest(request));
}
} else {
Expand Down
5 changes: 4 additions & 1 deletion src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ export type ImageContentData = {
export interface Response {
appendResponseLine(value: string): void;
setIncludePages(value: boolean): void;
setIncludeNetworkRequests(value: boolean): void;
setIncludeNetworkRequests(
value: boolean,
options?: {pageSize?: number; pageToken?: string | null},
): void;
setIncludeConsoleData(value: boolean): void;
setIncludeSnapshot(value: boolean): void;
attachImage(value: ImageContentData): void;
Expand Down
24 changes: 21 additions & 3 deletions src/tools/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,27 @@ export const listNetworkRequests = defineTool({
category: ToolCategories.NETWORK,
readOnlyHint: true,
},
schema: {},
handler: async (_request, response) => {
response.setIncludeNetworkRequests(true);
schema: {
pageSize: z
.number()
.int()
.positive()
.optional()
.describe(
'Maximum number of requests to return. When omitted, returns all requests.',
),
pageToken: z
Comment thread
OrKoN marked this conversation as resolved.
Outdated
.string()
.optional()
.describe(
'Opaque token representing the next page. Use the token returned by a previous call.',
),
},
handler: async (request, response) => {
response.setIncludeNetworkRequests(true, {
pageSize: request.params.pageSize,
pageToken: request.params.pageToken ?? null,
});
},
});

Expand Down
87 changes: 87 additions & 0 deletions src/utils/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

export type PaginationOptions = {
pageSize?: number;
pageToken?: string;
};

export type PaginationResult<TItem> = {
items: readonly TItem[];
nextPageToken?: string;
previousPageToken?: string;
startIndex: number;
endIndex: number;
invalidToken: boolean;
};

const DEFAULT_PAGE_SIZE = 20;

export function paginate<TItem>(
items: readonly TItem[],
options?: PaginationOptions,
): PaginationResult<TItem> {
const total = items.length;

if (!options || noPaginationOptions(options)) {
return {
items,
nextPageToken: undefined,
previousPageToken: undefined,
startIndex: 0,
endIndex: total,
invalidToken: false,
};
}

const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
const {startIndex, invalidToken} = resolveStartIndex(
options.pageToken,
total,
);

const pageItems = items.slice(startIndex, startIndex + pageSize);
const endIndex = startIndex + pageItems.length;

const nextPageToken = endIndex < total ? String(endIndex) : undefined;
const previousPageToken =
startIndex > 0 ? String(Math.max(startIndex - pageSize, 0)) : undefined;

return {
items: pageItems,
nextPageToken,
previousPageToken,
startIndex,
endIndex,
invalidToken,
};
}

function noPaginationOptions(options: PaginationOptions): boolean {
return (
options.pageSize === undefined &&
(options.pageToken === undefined || options.pageToken === null)
);
}

function resolveStartIndex(
pageToken: string | undefined,
total: number,
): {
startIndex: number;
invalidToken: boolean;
} {
if (pageToken === undefined || pageToken === null) {
return {startIndex: 0, invalidToken: false};
}

const parsed = Number.parseInt(pageToken, 10);
if (Number.isNaN(parsed) || parsed < 0 || parsed >= total) {
return {startIndex: 0, invalidToken: true};
}

return {startIndex: parsed, invalidToken: false};
}
69 changes: 69 additions & 0 deletions tests/McpResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ Call browser_handle_dialog to handle it before continuing.`,
result[0].text,
`# test response
## Network requests
Showing 1-1 of 1.
http://example.com GET [pending]`,
);
});
Expand Down Expand Up @@ -217,6 +218,7 @@ Status: [pending]
### Request Headers
- content-size:10
## Network requests
Showing 1-1 of 1.
http://example.com GET [pending]`,
);
});
Expand Down Expand Up @@ -261,3 +263,70 @@ Log>`),
});
});
});

describe('McpResponse network pagination', () => {
it('returns all requests when pagination is not provided', async () => {
await withBrowser(async (response, context) => {
const requests = Array.from({length: 5}, () => getMockRequest());
context.getNetworkRequests = () => requests;
response.setIncludeNetworkRequests(true);
const result = await response.handle('test', context);
const text = (result[0].text as string).toString();
assert.ok(text.includes('Showing 1-5 of 5.'));
assert.ok(!text.includes('Next:'));
assert.ok(!text.includes('Prev:'));
});
});

it('returns first page by default', async () => {
await withBrowser(async (response, context) => {
const requests = Array.from({length: 30}, (_, idx) =>
getMockRequest({method: `GET-${idx}`}),
);
context.getNetworkRequests = () => {
return requests;
};
response.setIncludeNetworkRequests(true, {pageSize: 10});
const result = await response.handle('test', context);
const text = (result[0].text as string).toString();
assert.ok(text.includes('Showing 1-10 of 30.'));
assert.ok(text.includes('Next: 10'));
assert.ok(!text.includes('Prev:'));
});
});

it('returns subsequent page when token provided', async () => {
await withBrowser(async (response, context) => {
const requests = Array.from({length: 25}, (_, idx) =>
getMockRequest({method: `GET-${idx}`}),
);
context.getNetworkRequests = () => requests;
response.setIncludeNetworkRequests(true, {
pageSize: 10,
pageToken: '10',
});
const result = await response.handle('test', context);
const text = (result[0].text as string).toString();
assert.ok(text.includes('Showing 11-20 of 25.'));
assert.ok(text.includes('Next: 20'));
assert.ok(text.includes('Prev: 0'));
});
});

it('handles invalid token by showing first page', async () => {
await withBrowser(async (response, context) => {
const requests = Array.from({length: 5}, () => getMockRequest());
context.getNetworkRequests = () => requests;
response.setIncludeNetworkRequests(true, {
pageSize: 2,
pageToken: 'invalid',
});
const result = await response.handle('test', context);
const text = (result[0].text as string).toString();
assert.ok(
text.includes('Invalid page token provided. Showing first page.'),
);
assert.ok(text.includes('Showing 1-2 of 5.'));
});
});
});
1 change: 1 addition & 0 deletions tests/tools/network.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('network', () => {
await withBrowser(async (response, context) => {
await listNetworkRequests.handler({params: {}}, response, context);
assert.ok(response.includeNetworkRequests);
assert.strictEqual(response.networkRequestsPageToken, undefined);
});
});
});
Expand Down
Loading