From 5be157eac31e72c51cd35a51ad6dcfac413fa5b4 Mon Sep 17 00:00:00 2001 From: Oz Tamir Date: Wed, 24 Sep 2025 10:49:26 +0300 Subject: [PATCH 1/6] feat: add pagination to `list_network_requests` --- src/McpResponse.ts | 63 ++++++++++++++++++++--- src/tools/ToolDefinition.ts | 13 +++-- src/tools/network.ts | 29 +++++++++-- src/utils/networkPagination.ts | 92 ++++++++++++++++++++++++++++++++++ tests/McpResponse.test.ts | 83 +++++++++++++++++++++++++++--- tests/tools/network.test.ts | 9 ++-- 6 files changed, 260 insertions(+), 29 deletions(-) create mode 100644 src/utils/networkPagination.ts diff --git a/src/McpResponse.ts b/src/McpResponse.ts index e1af6f9c..aeecbd8d 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -3,16 +3,20 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import type {ImageContentData, Response} from './tools/ToolDefinition.js'; -import type {McpContext} from './McpContext.js'; -import {ImageContent, TextContent} from '@modelcontextprotocol/sdk/types.js'; +import type { ImageContentData, Response } from './tools/ToolDefinition.js'; +import type { McpContext } from './McpContext.js'; +import { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; import { getFormattedHeaderValue, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js'; -import {formatA11ySnapshot} from './formatters/snapshotFormatter.js'; -import {formatConsoleEvent} from './formatters/consoleFormatter.js'; +import { formatA11ySnapshot } from './formatters/snapshotFormatter.js'; +import { formatConsoleEvent } from './formatters/consoleFormatter.js'; +import { + paginateNetworkRequests, + type NetworkPaginationOptions, +} from './utils/networkPagination.js'; export class McpResponse implements Response { #includePages: boolean = false; @@ -23,6 +27,7 @@ export class McpResponse implements Response { #textResponseLines: string[] = []; #formattedConsoleData?: string[]; #images: ImageContentData[] = []; + #networkRequestsPaginationOptions?: NetworkPaginationOptions; setIncludePages(value: boolean): void { this.#includePages = value; @@ -32,8 +37,31 @@ 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) { + this.#networkRequestsPaginationOptions = undefined; + return; + } + + if (!options) { + this.#networkRequestsPaginationOptions = undefined; + return; + } + + const sanitizedOptions: NetworkPaginationOptions = {}; + if (options.pageSize !== undefined) { + sanitizedOptions.pageSize = options.pageSize; + } + if (options.pageToken !== undefined) { + sanitizedOptions.pageToken = options.pageToken ?? undefined; + } + + this.#networkRequestsPaginationOptions = + Object.keys(sanitizedOptions).length > 0 ? sanitizedOptions : undefined; } setIncludeConsoleData(value: boolean): void { @@ -58,6 +86,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); @@ -162,9 +194,26 @@ 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 = paginateNetworkRequests( + 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}.`, + ); + for (const request of paginationResult.requests) { response.push(getShortDescriptionForRequest(request)); } + if (paginationResult.nextPageToken) { + response.push(`Next: ${paginationResult.nextPageToken}`); + } + if (paginationResult.previousPageToken) { + response.push(`Prev: ${paginationResult.previousPageToken}`); + } } else { response.push('No requests found.'); } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 37b0f50d..5dec40c2 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -5,9 +5,9 @@ */ import z from 'zod'; -import {Dialog, ElementHandle, Page} from 'puppeteer-core'; -import {ToolCategories} from './categories.js'; -import {TraceResult} from '../trace-processing/parse.js'; +import { Dialog, ElementHandle, Page } from 'puppeteer-core'; +import { ToolCategories } from './categories.js'; +import { TraceResult } from '../trace-processing/parse.js'; export interface ToolDefinition< Schema extends Zod.ZodRawShape = Zod.ZodRawShape, @@ -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; @@ -69,7 +72,7 @@ export type Context = Readonly<{ saveTemporaryFile( data: Uint8Array, mimeType: 'image/png' | 'image/jpeg', - ): Promise<{filename: string}>; + ): Promise<{ filename: string }>; waitForEventsAfterAction(action: () => Promise): Promise; }>; diff --git a/src/tools/network.ts b/src/tools/network.ts index c14fe9ff..21ba515d 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -5,8 +5,8 @@ */ import z from 'zod'; -import {defineTool} from './ToolDefinition.js'; -import {ToolCategories} from './categories.js'; +import { defineTool } from './ToolDefinition.js'; +import { ToolCategories } from './categories.js'; export const listNetworkRequests = defineTool({ name: 'list_network_requests', @@ -15,9 +15,28 @@ export const listNetworkRequests = defineTool({ category: ToolCategories.NETWORK, readOnlyHint: true, }, - schema: {}, - handler: async (_request, response) => { - response.setIncludeNetworkRequests(true); + schema: { + pageSize: z + .number() + .int() + .positive() + .max(100) + .optional() + .describe( + 'Maximum number of requests to return. When omitted, returns all requests.', + ), + pageToken: z + .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, + }); }, }); diff --git a/src/utils/networkPagination.ts b/src/utils/networkPagination.ts new file mode 100644 index 00000000..948fcb4b --- /dev/null +++ b/src/utils/networkPagination.ts @@ -0,0 +1,92 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import { type HTTPRequest } from 'puppeteer-core'; + +export type NetworkPaginationOptions = { + pageSize?: number; + pageToken?: string; +}; + +export type NetworkPaginationResult = { + requests: readonly HTTPRequest[]; + nextPageToken?: string; + previousPageToken?: string; + startIndex: number; + endIndex: number; + invalidToken: boolean; +}; + +const DEFAULT_PAGE_SIZE = 20; + +export function paginateNetworkRequests( + requests: readonly HTTPRequest[], + options?: NetworkPaginationOptions, +): NetworkPaginationResult { + const total = requests.length; + + if (!options || noPaginationOptions(options)) { + return { + requests, + nextPageToken: undefined, + previousPageToken: undefined, + startIndex: 0, + endIndex: total, + invalidToken: false, + }; + } + + const pageSize = validatePageSize(options.pageSize, total); + const { startIndex, invalidToken } = resolveStartIndex(options.pageToken, total); + + const pageRequests = requests.slice(startIndex, startIndex + pageSize); + const endIndex = startIndex + pageRequests.length; + + const nextPageToken = endIndex < total ? String(endIndex) : undefined; + const previousPageToken = + startIndex > 0 ? String(Math.max(startIndex - pageSize, 0)) : undefined; + + return { + requests: pageRequests, + nextPageToken, + previousPageToken, + startIndex, + endIndex, + invalidToken, + }; +} + +function noPaginationOptions(options: NetworkPaginationOptions): boolean { + return ( + options.pageSize === undefined && + (options.pageToken === undefined || options.pageToken === null) + ); +} + +function validatePageSize(pageSize: number | undefined, total: number): number { + if (pageSize === undefined) { + return total || DEFAULT_PAGE_SIZE; + } + if (!Number.isInteger(pageSize) || pageSize <= 0) { + return DEFAULT_PAGE_SIZE; + } + return Math.min(pageSize, Math.max(total, 1)); +} + +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 }; +} diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 40f775f7..e8220d51 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -3,10 +3,10 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import {describe, it} from 'node:test'; +import { describe, it } from 'node:test'; import assert from 'assert'; -import {getMockRequest, html, withBrowser} from './utils.js'; +import { getMockRequest, html, withBrowser } from './utils.js'; describe('McpResponse', () => { it('list pages', async () => { @@ -120,7 +120,7 @@ Navigation timeout set to 100000 ms`, }); it('adds image when image is attached', async () => { await withBrowser(async (response, context) => { - response.attachImage({data: 'imageBase64', mimeType: 'image/png'}); + response.attachImage({ data: 'imageBase64', mimeType: 'image/png' }); const result = await response.handle('test', context); assert.strictEqual(result[0].text, `# test response`); assert.equal(result[1].type, 'image'); @@ -181,12 +181,11 @@ Call browser_handle_dialog to handle it before continuing.`, return [getMockRequest()]; }; const result = await response.handle('test', context); - assert.strictEqual( - result[0].text, - `# test response -## Network requests -http://example.com GET [pending]`, + const text = result[0].text as string; + assert.ok( + text.includes(`## Network requests`), ); + assert.ok(text.includes('http://example.com GET [pending]')); }); }); it('does not include network requests when setting is false', async () => { @@ -217,6 +216,7 @@ Status: [pending] ### Request Headers - content-size:10 ## Network requests +Showing 1-1 of 1. http://example.com GET [pending]`, ); }); @@ -261,3 +261,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.')); + }); + }); +}); diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index a563d051..88c7fefc 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -3,10 +3,10 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import {describe, it} from 'node:test'; +import { describe, it } from 'node:test'; import assert from 'assert'; -import {withBrowser} from '../utils.js'; +import { withBrowser } from '../utils.js'; import { getNetworkRequest, listNetworkRequests, @@ -16,8 +16,9 @@ describe('network', () => { describe('network_list_requests', () => { it('list requests', async () => { await withBrowser(async (response, context) => { - await listNetworkRequests.handler({params: {}}, response, context); + await listNetworkRequests.handler({ params: {} }, response, context); assert.ok(response.includeNetworkRequests); + assert.strictEqual(response.networkRequestsPageToken, undefined); }); }); }); @@ -27,7 +28,7 @@ describe('network', () => { const page = await context.getSelectedPage(); await page.goto('data:text/html,
Hello MCP
'); await getNetworkRequest.handler( - {params: {url: 'data:text/html,
Hello MCP
'}}, + { params: { url: 'data:text/html,
Hello MCP
' } }, response, context, ); From bbcc6db77dbf05a71bb175c62b00155cb147eecd Mon Sep 17 00:00:00 2001 From: Oz Tamir Date: Wed, 24 Sep 2025 11:27:36 +0300 Subject: [PATCH 2/6] feat: Support filtering by request type in `list_network_requests` --- src/McpResponse.ts | 58 +++++++---- src/tools/ToolDefinition.ts | 9 +- src/tools/network.ts | 8 ++ src/utils/networkPagination.ts | 92 ----------------- src/utils/networkUtils.ts | 182 +++++++++++++++++++++++++++++++++ tests/McpResponse.test.ts | 104 ++++++++++++++++++- tests/utils.ts | 19 ++-- 7 files changed, 352 insertions(+), 120 deletions(-) delete mode 100644 src/utils/networkPagination.ts create mode 100644 src/utils/networkUtils.ts diff --git a/src/McpResponse.ts b/src/McpResponse.ts index aeecbd8d..c280761c 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -15,8 +15,9 @@ import { formatA11ySnapshot } from './formatters/snapshotFormatter.js'; import { formatConsoleEvent } from './formatters/consoleFormatter.js'; import { paginateNetworkRequests, - type NetworkPaginationOptions, -} from './utils/networkPagination.js'; + type NetworkRequestsListingOptions, + sanitizeRequestTypeFilter, +} from './utils/networkUtils.js'; export class McpResponse implements Response { #includePages: boolean = false; @@ -27,7 +28,7 @@ export class McpResponse implements Response { #textResponseLines: string[] = []; #formattedConsoleData?: string[]; #images: ImageContentData[] = []; - #networkRequestsPaginationOptions?: NetworkPaginationOptions; + #networkRequestsPaginationOptions?: NetworkRequestsListingOptions; setIncludePages(value: boolean): void { this.#includePages = value; @@ -39,7 +40,7 @@ export class McpResponse implements Response { setIncludeNetworkRequests( value: boolean, - options?: { pageSize?: number; pageToken?: string | null }, + options?: NetworkRequestsListingOptions, ): void { this.#includeNetworkRequests = value; if (!value) { @@ -52,13 +53,21 @@ export class McpResponse implements Response { return; } - const sanitizedOptions: NetworkPaginationOptions = {}; + const sanitizedOptions: NetworkRequestsListingOptions = {}; if (options.pageSize !== undefined) { sanitizedOptions.pageSize = options.pageSize; } if (options.pageToken !== undefined) { sanitizedOptions.pageToken = options.pageToken ?? undefined; } + if (options.requestType !== undefined) { + const sanitizedRequestType = sanitizeRequestTypeFilter( + options.requestType, + ); + if (sanitizedRequestType !== undefined) { + sanitizedOptions.requestType = sanitizedRequestType; + } + } this.#networkRequestsPaginationOptions = Object.keys(sanitizedOptions).length > 0 ? sanitizedOptions : undefined; @@ -193,18 +202,33 @@ Call browser_handle_dialog to handle it before continuing.`); if (this.#includeNetworkRequests) { const requests = context.getNetworkRequests(); response.push('## Network requests'); - if (requests.length) { - const paginationResult = paginateNetworkRequests( - requests, - this.#networkRequestsPaginationOptions, - ); - if (paginationResult.invalidToken) { - response.push('Invalid page token provided. Showing first page.'); + const paginationOptions = this.#networkRequestsPaginationOptions; + const paginationResult = paginateNetworkRequests( + requests, + paginationOptions, + ); + + if (paginationResult.appliedRequestType) { + const summary = Array.isArray(paginationResult.appliedRequestType) + ? paginationResult.appliedRequestType.join(', ') + : paginationResult.appliedRequestType; + response.push(`Filtered by type: ${summary}`); + } + + if (paginationResult.invalidToken) { + response.push('Invalid page token provided. Showing first page.'); + } + + const { startIndex, endIndex, total } = paginationResult; + if (total === 0) { + if (paginationOptions?.requestType) { + response.push('No requests found for the selected type(s).'); + } else { + response.push('No requests found.'); } - const { startIndex, endIndex } = paginationResult; - response.push( - `Showing ${startIndex + 1}-${endIndex} of ${requests.length}.`, - ); + } + else { + response.push(`Showing ${startIndex + 1}-${endIndex} of ${total}.`); for (const request of paginationResult.requests) { response.push(getShortDescriptionForRequest(request)); } @@ -214,8 +238,6 @@ Call browser_handle_dialog to handle it before continuing.`); if (paginationResult.previousPageToken) { response.push(`Prev: ${paginationResult.previousPageToken}`); } - } else { - response.push('No requests found.'); } } diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 5dec40c2..503cf6c5 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -6,6 +6,7 @@ import z from 'zod'; import { Dialog, ElementHandle, Page } from 'puppeteer-core'; +import type { FilterableResourceType } from '../utils/networkUtils.js'; import { ToolCategories } from './categories.js'; import { TraceResult } from '../trace-processing/parse.js'; @@ -39,12 +40,18 @@ export type ImageContentData = { mimeType: string; }; +export type NetworkRequestsOptions = { + pageSize?: number; + pageToken?: string | null; + requestType?: FilterableResourceType | FilterableResourceType[] | null; +}; + export interface Response { appendResponseLine(value: string): void; setIncludePages(value: boolean): void; setIncludeNetworkRequests( value: boolean, - options?: { pageSize?: number; pageToken?: string | null }, + options?: NetworkRequestsOptions, ): void; setIncludeConsoleData(value: boolean): void; setIncludeSnapshot(value: boolean): void; diff --git a/src/tools/network.ts b/src/tools/network.ts index 21ba515d..fb273d90 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -5,6 +5,7 @@ */ import z from 'zod'; +import { FILTERABLE_RESOURCE_TYPES } from '../utils/networkUtils.js'; import { defineTool } from './ToolDefinition.js'; import { ToolCategories } from './categories.js'; @@ -31,11 +32,18 @@ export const listNetworkRequests = defineTool({ .describe( 'Opaque token representing the next page. Use the token returned by a previous call.', ), + requestType: z + .enum(FILTERABLE_RESOURCE_TYPES) + .optional() + .describe( + `Type of request to return. When omitted, returns all requests. Available types are: ${FILTERABLE_RESOURCE_TYPES.join(', ')}.`, + ), }, handler: async (request, response) => { response.setIncludeNetworkRequests(true, { pageSize: request.params.pageSize, pageToken: request.params.pageToken ?? null, + requestType: request.params.requestType ?? null, }); }, }); diff --git a/src/utils/networkPagination.ts b/src/utils/networkPagination.ts deleted file mode 100644 index 948fcb4b..00000000 --- a/src/utils/networkPagination.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ -import { type HTTPRequest } from 'puppeteer-core'; - -export type NetworkPaginationOptions = { - pageSize?: number; - pageToken?: string; -}; - -export type NetworkPaginationResult = { - requests: readonly HTTPRequest[]; - nextPageToken?: string; - previousPageToken?: string; - startIndex: number; - endIndex: number; - invalidToken: boolean; -}; - -const DEFAULT_PAGE_SIZE = 20; - -export function paginateNetworkRequests( - requests: readonly HTTPRequest[], - options?: NetworkPaginationOptions, -): NetworkPaginationResult { - const total = requests.length; - - if (!options || noPaginationOptions(options)) { - return { - requests, - nextPageToken: undefined, - previousPageToken: undefined, - startIndex: 0, - endIndex: total, - invalidToken: false, - }; - } - - const pageSize = validatePageSize(options.pageSize, total); - const { startIndex, invalidToken } = resolveStartIndex(options.pageToken, total); - - const pageRequests = requests.slice(startIndex, startIndex + pageSize); - const endIndex = startIndex + pageRequests.length; - - const nextPageToken = endIndex < total ? String(endIndex) : undefined; - const previousPageToken = - startIndex > 0 ? String(Math.max(startIndex - pageSize, 0)) : undefined; - - return { - requests: pageRequests, - nextPageToken, - previousPageToken, - startIndex, - endIndex, - invalidToken, - }; -} - -function noPaginationOptions(options: NetworkPaginationOptions): boolean { - return ( - options.pageSize === undefined && - (options.pageToken === undefined || options.pageToken === null) - ); -} - -function validatePageSize(pageSize: number | undefined, total: number): number { - if (pageSize === undefined) { - return total || DEFAULT_PAGE_SIZE; - } - if (!Number.isInteger(pageSize) || pageSize <= 0) { - return DEFAULT_PAGE_SIZE; - } - return Math.min(pageSize, Math.max(total, 1)); -} - -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 }; -} diff --git a/src/utils/networkUtils.ts b/src/utils/networkUtils.ts new file mode 100644 index 00000000..b8b96dcb --- /dev/null +++ b/src/utils/networkUtils.ts @@ -0,0 +1,182 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import { type HTTPRequest, type ResourceType } from 'puppeteer-core'; + +export const FILTERABLE_RESOURCE_TYPES = [ + 'document', + 'stylesheet', + 'image', + 'media', + 'font', + 'script', + 'xhr', + 'fetch', + 'prefetch', + 'websocket', + 'preflight', + 'other', +] as const satisfies readonly ResourceType[]; + +export type FilterableResourceType = (typeof FILTERABLE_RESOURCE_TYPES)[number]; + +export type NetworkRequestsListingOptions = { + pageSize?: number; + pageToken?: string; + requestType?: FilterableResourceType | FilterableResourceType[]; +}; + +export type NetworkRequestsListingResult = { + requests: readonly HTTPRequest[]; + nextPageToken?: string; + previousPageToken?: string; + startIndex: number; + endIndex: number; + invalidToken: boolean; + total: number; + appliedRequestType?: FilterableResourceType | FilterableResourceType[]; +}; + +const DEFAULT_PAGE_SIZE = 20; +const FILTERABLE_RESOURCE_TYPES_SET = new Set( + FILTERABLE_RESOURCE_TYPES, +); + +export function isFilterableResourceType( + value: ResourceType | string, +): value is FilterableResourceType { + return FILTERABLE_RESOURCE_TYPES_SET.has(value as FilterableResourceType); +} + +export function sanitizeRequestTypeFilter( + requestType?: string | string[] | null, +): FilterableResourceType | FilterableResourceType[] | undefined { + if (requestType === undefined || requestType === null) { + return undefined; + } + + const values = Array.isArray(requestType) ? requestType : [requestType]; + const sanitized = values.filter(isFilterableResourceType); + + if (!sanitized.length) { + return undefined; + } + + return Array.isArray(requestType) ? sanitized : sanitized[0]; +} + +export function filterNetworkRequests( + requests: readonly HTTPRequest[], + requestType?: FilterableResourceType | FilterableResourceType[], +): readonly HTTPRequest[] { + if (!requestType) { + return requests; + } + + const normalizedTypes = new Set( + Array.isArray(requestType) ? requestType : [requestType], + ); + + if (!normalizedTypes.size) { + return requests; + } + + return requests.filter(request => { + const type = request.resourceType(); + if (!isFilterableResourceType(type)) { + return false; + } + return normalizedTypes.has(type); + }); +} + +export function paginateNetworkRequests( + requests: readonly HTTPRequest[], + options?: NetworkRequestsListingOptions, +): NetworkRequestsListingResult { + const sanitizedOptions = options ?? {}; + const filteredRequests = filterNetworkRequests( + requests, + sanitizedOptions.requestType, + ); + const total = filteredRequests.length; + + const hasPaginationOptions = hasPagination(sanitizedOptions); + + if (!hasPaginationOptions) { + return { + requests: filteredRequests, + nextPageToken: undefined, + previousPageToken: undefined, + startIndex: 0, + endIndex: total, + invalidToken: false, + total, + appliedRequestType: sanitizedOptions.requestType, + }; + } + + const pageSize = validatePageSize(sanitizedOptions.pageSize, total); + const { startIndex, invalidToken } = resolveStartIndex( + sanitizedOptions.pageToken, + total, + ); + + const pageRequests = filteredRequests.slice(startIndex, startIndex + pageSize); + const endIndex = startIndex + pageRequests.length; + + const nextPageToken = endIndex < total ? String(endIndex) : undefined; + const previousPageToken = + startIndex > 0 ? String(Math.max(startIndex - pageSize, 0)) : undefined; + + return { + requests: pageRequests, + nextPageToken, + previousPageToken, + startIndex, + endIndex, + invalidToken, + total, + appliedRequestType: sanitizedOptions.requestType, + }; +} + +function hasPagination(options: NetworkRequestsListingOptions): boolean { + return ( + options.pageSize !== undefined || + (options.pageToken !== undefined && options.pageToken !== null) + ); +} + +function validatePageSize(pageSize: number | undefined, total: number): number { + if (pageSize === undefined) { + return total || DEFAULT_PAGE_SIZE; + } + if (!Number.isInteger(pageSize) || pageSize <= 0) { + return DEFAULT_PAGE_SIZE; + } + return Math.min(pageSize, Math.max(total, 1)); +} + +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: total > 0 }; + } + + return { startIndex: parsed, invalidToken: false }; +} + + diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index e8220d51..1b8d630c 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -265,7 +265,9 @@ 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()); + const requests = Array.from({ length: 5 }, (_, idx) => + getMockRequest({ method: `GET-${idx}` }), + ); context.getNetworkRequests = () => requests; response.setIncludeNetworkRequests(true); const result = await response.handle('test', context); @@ -313,7 +315,9 @@ describe('McpResponse network pagination', () => { it('handles invalid token by showing first page', async () => { await withBrowser(async (response, context) => { - const requests = Array.from({ length: 5 }, () => getMockRequest()); + const requests = Array.from({ length: 5 }, (_, idx) => + getMockRequest({ method: `GET-${idx}` }), + ); context.getNetworkRequests = () => requests; response.setIncludeNetworkRequests(true, { pageSize: 2, @@ -328,3 +332,99 @@ describe('McpResponse network pagination', () => { }); }); }); + +describe('McpResponse network filtering', () => { + it('filters requests by provided type', async () => { + await withBrowser(async (response, context) => { + const requests = [ + getMockRequest({ + resourceType: 'document', + method: 'GET', + url: 'https://example.com/document', + }), + getMockRequest({ + resourceType: 'image', + method: 'GET', + url: 'https://example.com/image.png', + }), + getMockRequest({ + resourceType: 'xhr', + method: 'GET', + url: 'https://example.com/api', + }), + ]; + + context.getNetworkRequests = () => requests; + response.setIncludeNetworkRequests(true, { + requestType: 'image', + }); + + const result = await response.handle('test', context); + const text = result[0].text as string; + + assert.ok(text.includes('Filtered by type: image')); + assert.ok(text.includes('Showing 1-1 of 1.')); + assert.ok(text.includes('https://example.com/image.png')); + assert.ok(!text.includes('https://example.com/document')); + assert.ok(!text.includes('https://example.com/api')); + }); + }); + + it('supports array request type filters', async () => { + await withBrowser(async (response, context) => { + const requests = [ + getMockRequest({ + resourceType: 'document', + method: 'GET', + url: 'https://example.com/document', + }), + getMockRequest({ + resourceType: 'image', + method: 'GET', + url: 'https://example.com/image.png', + }), + getMockRequest({ + resourceType: 'script', + method: 'GET', + url: 'https://example.com/script.js', + }), + ]; + + context.getNetworkRequests = () => requests; + response.setIncludeNetworkRequests(true, { + requestType: ['image', 'script'], + }); + + const result = await response.handle('test', context); + const text = result[0].text as string; + + assert.ok(text.includes('Filtered by type: image, script')); + assert.ok(text.includes('Showing 1-2 of 2.')); + assert.ok(text.includes('https://example.com/image.png')); + assert.ok(text.includes('https://example.com/script.js')); + assert.ok(!text.includes('https://example.com/document')); + }); + }); + + it('returns message when no matching requests exist', async () => { + await withBrowser(async (response, context) => { + const requests = [ + getMockRequest({ + resourceType: 'document', + url: 'https://example.com/document', + }), + ]; + + context.getNetworkRequests = () => requests; + response.setIncludeNetworkRequests(true, { + requestType: 'image', + }); + + const result = await response.handle('test', context); + const text = result[0].text as string; + + assert.ok(text.includes('Filtered by type: image')); + assert.ok(text.includes('No requests found for the selected type(s).')); + }); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 0ee9da59..47ac4375 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -4,19 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import puppeteer, {Browser} from 'puppeteer'; -import {HTTPRequest, HTTPResponse} from 'puppeteer-core'; -import {McpResponse} from '../src/McpResponse.js'; -import {McpContext} from '../src/McpContext.js'; +import puppeteer, { Browser } from 'puppeteer'; +import { HTTPRequest, HTTPResponse, type ResourceType } from 'puppeteer-core'; +import { McpResponse } from '../src/McpResponse.js'; +import { McpContext } from '../src/McpContext.js'; import logger from 'debug'; let browser: Browser | undefined; export async function withBrowser( cb: (response: McpResponse, context: McpContext) => Promise, - options: {debug?: boolean} = {}, + options: { debug?: boolean } = {}, ) { - const {debug = false} = options; + const { debug = false } = options; if (!browser) { browser = await puppeteer.launch({ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, @@ -44,11 +44,13 @@ export function getMockRequest( method?: string; response?: HTTPResponse; failure?: HTTPRequest['failure']; + resourceType?: ResourceType; + url?: string; } = {}, ): HTTPRequest { return { url() { - return 'http://example.com'; + return options.url ?? 'http://example.com'; }, method() { return options.method ?? 'GET'; @@ -67,6 +69,9 @@ export function getMockRequest( redirectChain(): HTTPRequest[] { return []; }, + resourceType() { + return options.resourceType ?? 'document'; + }, } as HTTPRequest; } From 08b56619fa9e6c78f25aaa322eb53e66038be65f Mon Sep 17 00:00:00 2001 From: Oz Tamir Date: Wed, 24 Sep 2025 11:28:45 +0300 Subject: [PATCH 3/6] docs: Add new `list_network_requests` parameters --- docs/tool-reference.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index dd661efc..ee45f442 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -262,7 +262,11 @@ **Description:** List all requests for the currently selected page -**Parameters:** None +**Parameters:** + +- **pageSize** (number) _(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. +- **requestType** (enum) _(optional)_: Type of request to return. When omitted, returns all requests. Available types are: Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR, Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping, CSPViolationReport, Preflight, Other. --- From 5922f00c11910c4aa7ba600400cc38a0954a2b1f Mon Sep 17 00:00:00 2001 From: Oz Tamir Date: Wed, 24 Sep 2025 11:38:41 +0300 Subject: [PATCH 4/6] fix: code formatting --- src/McpResponse.ts | 15 +-- src/tools/ToolDefinition.ts | 10 +- src/tools/network.ts | 6 +- src/utils/networkUtils.ts | 251 ++++++++++++++++++------------------ tests/McpResponse.test.ts | 28 ++-- tests/tools/network.test.ts | 8 +- tests/utils.ts | 12 +- 7 files changed, 164 insertions(+), 166 deletions(-) diff --git a/src/McpResponse.ts b/src/McpResponse.ts index c280761c..ea959978 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -3,16 +3,16 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import type { ImageContentData, Response } from './tools/ToolDefinition.js'; -import type { McpContext } from './McpContext.js'; -import { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; +import type {ImageContentData, Response} from './tools/ToolDefinition.js'; +import type {McpContext} from './McpContext.js'; +import {ImageContent, TextContent} from '@modelcontextprotocol/sdk/types.js'; import { getFormattedHeaderValue, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js'; -import { formatA11ySnapshot } from './formatters/snapshotFormatter.js'; -import { formatConsoleEvent } from './formatters/consoleFormatter.js'; +import {formatA11ySnapshot} from './formatters/snapshotFormatter.js'; +import {formatConsoleEvent} from './formatters/consoleFormatter.js'; import { paginateNetworkRequests, type NetworkRequestsListingOptions, @@ -219,15 +219,14 @@ Call browser_handle_dialog to handle it before continuing.`); response.push('Invalid page token provided. Showing first page.'); } - const { startIndex, endIndex, total } = paginationResult; + const {startIndex, endIndex, total} = paginationResult; if (total === 0) { if (paginationOptions?.requestType) { response.push('No requests found for the selected type(s).'); } else { response.push('No requests found.'); } - } - else { + } else { response.push(`Showing ${startIndex + 1}-${endIndex} of ${total}.`); for (const request of paginationResult.requests) { response.push(getShortDescriptionForRequest(request)); diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 503cf6c5..e195c87e 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -5,10 +5,10 @@ */ import z from 'zod'; -import { Dialog, ElementHandle, Page } from 'puppeteer-core'; -import type { FilterableResourceType } from '../utils/networkUtils.js'; -import { ToolCategories } from './categories.js'; -import { TraceResult } from '../trace-processing/parse.js'; +import {Dialog, ElementHandle, Page} from 'puppeteer-core'; +import type {FilterableResourceType} from '../utils/networkUtils.js'; +import {ToolCategories} from './categories.js'; +import {TraceResult} from '../trace-processing/parse.js'; export interface ToolDefinition< Schema extends Zod.ZodRawShape = Zod.ZodRawShape, @@ -79,7 +79,7 @@ export type Context = Readonly<{ saveTemporaryFile( data: Uint8Array, mimeType: 'image/png' | 'image/jpeg', - ): Promise<{ filename: string }>; + ): Promise<{filename: string}>; waitForEventsAfterAction(action: () => Promise): Promise; }>; diff --git a/src/tools/network.ts b/src/tools/network.ts index fb273d90..72c8b06d 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -5,9 +5,9 @@ */ import z from 'zod'; -import { FILTERABLE_RESOURCE_TYPES } from '../utils/networkUtils.js'; -import { defineTool } from './ToolDefinition.js'; -import { ToolCategories } from './categories.js'; +import {FILTERABLE_RESOURCE_TYPES} from '../utils/networkUtils.js'; +import {defineTool} from './ToolDefinition.js'; +import {ToolCategories} from './categories.js'; export const listNetworkRequests = defineTool({ name: 'list_network_requests', diff --git a/src/utils/networkUtils.ts b/src/utils/networkUtils.ts index b8b96dcb..18cd92ac 100644 --- a/src/utils/networkUtils.ts +++ b/src/utils/networkUtils.ts @@ -3,180 +3,181 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import { type HTTPRequest, type ResourceType } from 'puppeteer-core'; +import {type HTTPRequest, type ResourceType} from 'puppeteer-core'; export const FILTERABLE_RESOURCE_TYPES = [ - 'document', - 'stylesheet', - 'image', - 'media', - 'font', - 'script', - 'xhr', - 'fetch', - 'prefetch', - 'websocket', - 'preflight', - 'other', + 'document', + 'stylesheet', + 'image', + 'media', + 'font', + 'script', + 'xhr', + 'fetch', + 'prefetch', + 'websocket', + 'preflight', + 'other', ] as const satisfies readonly ResourceType[]; export type FilterableResourceType = (typeof FILTERABLE_RESOURCE_TYPES)[number]; export type NetworkRequestsListingOptions = { - pageSize?: number; - pageToken?: string; - requestType?: FilterableResourceType | FilterableResourceType[]; + pageSize?: number; + pageToken?: string; + requestType?: FilterableResourceType | FilterableResourceType[]; }; export type NetworkRequestsListingResult = { - requests: readonly HTTPRequest[]; - nextPageToken?: string; - previousPageToken?: string; - startIndex: number; - endIndex: number; - invalidToken: boolean; - total: number; - appliedRequestType?: FilterableResourceType | FilterableResourceType[]; + requests: readonly HTTPRequest[]; + nextPageToken?: string; + previousPageToken?: string; + startIndex: number; + endIndex: number; + invalidToken: boolean; + total: number; + appliedRequestType?: FilterableResourceType | FilterableResourceType[]; }; const DEFAULT_PAGE_SIZE = 20; const FILTERABLE_RESOURCE_TYPES_SET = new Set( - FILTERABLE_RESOURCE_TYPES, + FILTERABLE_RESOURCE_TYPES, ); export function isFilterableResourceType( - value: ResourceType | string, + value: ResourceType | string, ): value is FilterableResourceType { - return FILTERABLE_RESOURCE_TYPES_SET.has(value as FilterableResourceType); + return FILTERABLE_RESOURCE_TYPES_SET.has(value as FilterableResourceType); } export function sanitizeRequestTypeFilter( - requestType?: string | string[] | null, + requestType?: string | string[] | null, ): FilterableResourceType | FilterableResourceType[] | undefined { - if (requestType === undefined || requestType === null) { - return undefined; - } + if (requestType === undefined || requestType === null) { + return undefined; + } - const values = Array.isArray(requestType) ? requestType : [requestType]; - const sanitized = values.filter(isFilterableResourceType); + const values = Array.isArray(requestType) ? requestType : [requestType]; + const sanitized = values.filter(isFilterableResourceType); - if (!sanitized.length) { - return undefined; - } + if (!sanitized.length) { + return undefined; + } - return Array.isArray(requestType) ? sanitized : sanitized[0]; + return Array.isArray(requestType) ? sanitized : sanitized[0]; } export function filterNetworkRequests( - requests: readonly HTTPRequest[], - requestType?: FilterableResourceType | FilterableResourceType[], + requests: readonly HTTPRequest[], + requestType?: FilterableResourceType | FilterableResourceType[], ): readonly HTTPRequest[] { - if (!requestType) { - return requests; - } - - const normalizedTypes = new Set( - Array.isArray(requestType) ? requestType : [requestType], - ); - - if (!normalizedTypes.size) { - return requests; + if (!requestType) { + return requests; + } + + const normalizedTypes = new Set( + Array.isArray(requestType) ? requestType : [requestType], + ); + + if (!normalizedTypes.size) { + return requests; + } + + return requests.filter(request => { + const type = request.resourceType(); + if (!isFilterableResourceType(type)) { + return false; } - - return requests.filter(request => { - const type = request.resourceType(); - if (!isFilterableResourceType(type)) { - return false; - } - return normalizedTypes.has(type); - }); + return normalizedTypes.has(type); + }); } export function paginateNetworkRequests( - requests: readonly HTTPRequest[], - options?: NetworkRequestsListingOptions, + requests: readonly HTTPRequest[], + options?: NetworkRequestsListingOptions, ): NetworkRequestsListingResult { - const sanitizedOptions = options ?? {}; - const filteredRequests = filterNetworkRequests( - requests, - sanitizedOptions.requestType, - ); - const total = filteredRequests.length; - - const hasPaginationOptions = hasPagination(sanitizedOptions); - - if (!hasPaginationOptions) { - return { - requests: filteredRequests, - nextPageToken: undefined, - previousPageToken: undefined, - startIndex: 0, - endIndex: total, - invalidToken: false, - total, - appliedRequestType: sanitizedOptions.requestType, - }; - } - - const pageSize = validatePageSize(sanitizedOptions.pageSize, total); - const { startIndex, invalidToken } = resolveStartIndex( - sanitizedOptions.pageToken, - total, - ); + const sanitizedOptions = options ?? {}; + const filteredRequests = filterNetworkRequests( + requests, + sanitizedOptions.requestType, + ); + const total = filteredRequests.length; - const pageRequests = filteredRequests.slice(startIndex, startIndex + pageSize); - const endIndex = startIndex + pageRequests.length; - - const nextPageToken = endIndex < total ? String(endIndex) : undefined; - const previousPageToken = - startIndex > 0 ? String(Math.max(startIndex - pageSize, 0)) : undefined; + const hasPaginationOptions = hasPagination(sanitizedOptions); + if (!hasPaginationOptions) { return { - requests: pageRequests, - nextPageToken, - previousPageToken, - startIndex, - endIndex, - invalidToken, - total, - appliedRequestType: sanitizedOptions.requestType, + requests: filteredRequests, + nextPageToken: undefined, + previousPageToken: undefined, + startIndex: 0, + endIndex: total, + invalidToken: false, + total, + appliedRequestType: sanitizedOptions.requestType, }; + } + + const pageSize = validatePageSize(sanitizedOptions.pageSize, total); + const {startIndex, invalidToken} = resolveStartIndex( + sanitizedOptions.pageToken, + total, + ); + + const pageRequests = filteredRequests.slice( + startIndex, + startIndex + pageSize, + ); + const endIndex = startIndex + pageRequests.length; + + const nextPageToken = endIndex < total ? String(endIndex) : undefined; + const previousPageToken = + startIndex > 0 ? String(Math.max(startIndex - pageSize, 0)) : undefined; + + return { + requests: pageRequests, + nextPageToken, + previousPageToken, + startIndex, + endIndex, + invalidToken, + total, + appliedRequestType: sanitizedOptions.requestType, + }; } function hasPagination(options: NetworkRequestsListingOptions): boolean { - return ( - options.pageSize !== undefined || - (options.pageToken !== undefined && options.pageToken !== null) - ); + return ( + options.pageSize !== undefined || + (options.pageToken !== undefined && options.pageToken !== null) + ); } function validatePageSize(pageSize: number | undefined, total: number): number { - if (pageSize === undefined) { - return total || DEFAULT_PAGE_SIZE; - } - if (!Number.isInteger(pageSize) || pageSize <= 0) { - return DEFAULT_PAGE_SIZE; - } - return Math.min(pageSize, Math.max(total, 1)); + if (pageSize === undefined) { + return total || DEFAULT_PAGE_SIZE; + } + if (!Number.isInteger(pageSize) || pageSize <= 0) { + return DEFAULT_PAGE_SIZE; + } + return Math.min(pageSize, Math.max(total, 1)); } function resolveStartIndex( - pageToken: string | undefined, - total: number, + pageToken: string | undefined, + total: number, ): { - startIndex: number; - invalidToken: boolean; + startIndex: number; + invalidToken: boolean; } { - if (pageToken === undefined || pageToken === null) { - return { startIndex: 0, invalidToken: false }; - } + 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: total > 0 }; - } + const parsed = Number.parseInt(pageToken, 10); + if (Number.isNaN(parsed) || parsed < 0 || parsed >= total) { + return {startIndex: 0, invalidToken: total > 0}; + } - return { startIndex: parsed, invalidToken: false }; + return {startIndex: parsed, invalidToken: false}; } - - diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 1b8d630c..2eac1ebb 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -3,10 +3,10 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it } from 'node:test'; +import {describe, it} from 'node:test'; import assert from 'assert'; -import { getMockRequest, html, withBrowser } from './utils.js'; +import {getMockRequest, html, withBrowser} from './utils.js'; describe('McpResponse', () => { it('list pages', async () => { @@ -120,7 +120,7 @@ Navigation timeout set to 100000 ms`, }); it('adds image when image is attached', async () => { await withBrowser(async (response, context) => { - response.attachImage({ data: 'imageBase64', mimeType: 'image/png' }); + response.attachImage({data: 'imageBase64', mimeType: 'image/png'}); const result = await response.handle('test', context); assert.strictEqual(result[0].text, `# test response`); assert.equal(result[1].type, 'image'); @@ -182,9 +182,7 @@ Call browser_handle_dialog to handle it before continuing.`, }; const result = await response.handle('test', context); const text = result[0].text as string; - assert.ok( - text.includes(`## Network requests`), - ); + assert.ok(text.includes(`## Network requests`)); assert.ok(text.includes('http://example.com GET [pending]')); }); }); @@ -265,8 +263,8 @@ 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 }, (_, idx) => - getMockRequest({ method: `GET-${idx}` }), + const requests = Array.from({length: 5}, (_, idx) => + getMockRequest({method: `GET-${idx}`}), ); context.getNetworkRequests = () => requests; response.setIncludeNetworkRequests(true); @@ -280,13 +278,13 @@ describe('McpResponse network pagination', () => { it('returns first page by default', async () => { await withBrowser(async (response, context) => { - const requests = Array.from({ length: 30 }, (_, idx) => - getMockRequest({ method: `GET-${idx}` }), + const requests = Array.from({length: 30}, (_, idx) => + getMockRequest({method: `GET-${idx}`}), ); context.getNetworkRequests = () => { return requests; }; - response.setIncludeNetworkRequests(true, { pageSize: 10 }); + 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.')); @@ -297,8 +295,8 @@ describe('McpResponse network pagination', () => { it('returns subsequent page when token provided', async () => { await withBrowser(async (response, context) => { - const requests = Array.from({ length: 25 }, (_, idx) => - getMockRequest({ method: `GET-${idx}` }), + const requests = Array.from({length: 25}, (_, idx) => + getMockRequest({method: `GET-${idx}`}), ); context.getNetworkRequests = () => requests; response.setIncludeNetworkRequests(true, { @@ -315,8 +313,8 @@ describe('McpResponse network pagination', () => { it('handles invalid token by showing first page', async () => { await withBrowser(async (response, context) => { - const requests = Array.from({ length: 5 }, (_, idx) => - getMockRequest({ method: `GET-${idx}` }), + const requests = Array.from({length: 5}, (_, idx) => + getMockRequest({method: `GET-${idx}`}), ); context.getNetworkRequests = () => requests; response.setIncludeNetworkRequests(true, { diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index 88c7fefc..bcf2eb65 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -3,10 +3,10 @@ * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it } from 'node:test'; +import {describe, it} from 'node:test'; import assert from 'assert'; -import { withBrowser } from '../utils.js'; +import {withBrowser} from '../utils.js'; import { getNetworkRequest, listNetworkRequests, @@ -16,7 +16,7 @@ describe('network', () => { describe('network_list_requests', () => { it('list requests', async () => { await withBrowser(async (response, context) => { - await listNetworkRequests.handler({ params: {} }, response, context); + await listNetworkRequests.handler({params: {}}, response, context); assert.ok(response.includeNetworkRequests); assert.strictEqual(response.networkRequestsPageToken, undefined); }); @@ -28,7 +28,7 @@ describe('network', () => { const page = await context.getSelectedPage(); await page.goto('data:text/html,
Hello MCP
'); await getNetworkRequest.handler( - { params: { url: 'data:text/html,
Hello MCP
' } }, + {params: {url: 'data:text/html,
Hello MCP
'}}, response, context, ); diff --git a/tests/utils.ts b/tests/utils.ts index 47ac4375..e027596b 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -4,19 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import puppeteer, { Browser } from 'puppeteer'; -import { HTTPRequest, HTTPResponse, type ResourceType } from 'puppeteer-core'; -import { McpResponse } from '../src/McpResponse.js'; -import { McpContext } from '../src/McpContext.js'; +import puppeteer, {Browser} from 'puppeteer'; +import {HTTPRequest, HTTPResponse, type ResourceType} from 'puppeteer-core'; +import {McpResponse} from '../src/McpResponse.js'; +import {McpContext} from '../src/McpContext.js'; import logger from 'debug'; let browser: Browser | undefined; export async function withBrowser( cb: (response: McpResponse, context: McpContext) => Promise, - options: { debug?: boolean } = {}, + options: {debug?: boolean} = {}, ) { - const { debug = false } = options; + const {debug = false} = options; if (!browser) { browser = await puppeteer.launch({ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, From fb2b8c9d529cf7e349efbc99053877a6b30d98c8 Mon Sep 17 00:00:00 2001 From: Oz Tamir Date: Wed, 24 Sep 2025 12:30:43 +0300 Subject: [PATCH 5/6] docs: use auto-generated docs --- docs/tool-reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 085dbea3..db6ceaba 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -264,9 +264,9 @@ **Parameters:** -- **pageSize** (number) _(optional)_: Maximum number of requests to return. When omitted, returns all requests. +- **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. -- **requestType** (enum) _(optional)_: Type of request to return. When omitted, returns all requests. Available types are: Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR, Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping, CSPViolationReport, Preflight, Other. +- **requestType** (enum: "document", "stylesheet", "image", "media", "font", "script", "xhr", "fetch", "prefetch", "websocket", "preflight", "other") _(optional)_: Type of request to return. When omitted, returns all requests. Available types are: document, stylesheet, image, media, font, script, xhr, fetch, prefetch, websocket, preflight, other. --- From bc307363e6c6256ef123635783b3bb832315d641 Mon Sep 17 00:00:00 2001 From: Oz Tamir Date: Wed, 24 Sep 2025 12:32:29 +0300 Subject: [PATCH 6/6] fix: change docs workflow error message to be accurate --- .github/workflows/presubmit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/presubmit.yml b/.github/workflows/presubmit.yml index b7faaf61..4c3e67ee 100644 --- a/.github/workflows/presubmit.yml +++ b/.github/workflows/presubmit.yml @@ -56,7 +56,7 @@ jobs: diff_file=$(mktemp doc_diff_XXXXXX) git diff --color > $diff_file if [[ -s $diff_file ]]; then - echo "Please update the documentation by running 'npm run generate-docs'. The following was the diff" + echo "Please update the documentation by running 'npm run docs'. The following was the diff" cat $diff_file rm $diff_file exit 1