diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 7e51181bc..b9bc6d286 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -255,11 +255,12 @@ ### `get_network_request` -**Description:** Gets a network request by URL. You can get all requests by calling [`list_network_requests`](#list_network_requests). +**Description:** Gets a network request by reqid. You can get all requests by calling [`list_network_requests`](#list_network_requests). +Get the request currently selected in the DevTools UI by ommitting reqid **Parameters:** -- **reqid** (number) **(required)**: The reqid of a request on the page from the listed network requests +- **reqid** (number) _(optional)_: The reqid of the network request. If omitted, looks up the current request selected in DevTools UI. --- diff --git a/src/McpContext.ts b/src/McpContext.ts index 586c2ba71..ceae35136 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -337,12 +337,13 @@ export class McpContext implements Context { ); }); - await this.#detectOpenDevToolsWindows(allPages); + await this.detectOpenDevToolsWindows(); return this.#pages; } - async #detectOpenDevToolsWindows(pages: Page[]) { + async detectOpenDevToolsWindows() { + const pages = await this.browser.pages(); this.#pageToDevToolsPage = new Map(); for (const devToolsPage of pages) { if (devToolsPage.url().startsWith('devtools://')) { @@ -377,6 +378,45 @@ export class McpContext implements Context { return this.#pageToDevToolsPage.get(page); } + async getDevToolsData(): Promise { + try { + const selectedPage = this.getSelectedPage(); + const devtoolsPage = this.getDevToolsPage(selectedPage); + if (devtoolsPage) { + const cdpRequestId = await devtoolsPage.evaluate(async () => { + // @ts-expect-error no types + const UI = await import('/bundled/ui/legacy/legacy.js'); + // @ts-expect-error no types + const SDK = await import('/bundled/core/sdk/sdk.js'); + const request = UI.Context.Context.instance().flavor( + SDK.NetworkRequest.NetworkRequest, + ); + return request?.requestId(); + }); + if (!cdpRequestId) { + this.logger('no context request'); + return; + } + const request = this.#networkCollector.find(selectedPage, request => { + // @ts-expect-error id is internal. + return request.id === cdpRequestId; + }); + if (!request) { + this.logger('no collected request for ' + cdpRequestId); + return; + } + return { + requestId: this.#networkCollector.getIdForResource(request), + }; + } else { + this.logger('no devtools page deteched'); + } + } catch (err) { + this.logger('error getting devtools data', err); + } + return; + } + /** * Creates a text snapshot of a page. */ diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 24f2b3f8a..6798c90b3 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -44,6 +44,7 @@ export class McpResponse implements Response { pagination?: PaginationOptions; resourceTypes?: ResourceType[]; includePreservedRequests?: boolean; + networkRequestIdInDevToolsUI?: number; }; #consoleDataOptions?: { include: boolean; @@ -67,6 +68,7 @@ export class McpResponse implements Response { options?: PaginationOptions & { resourceTypes?: ResourceType[]; includePreservedRequests?: boolean; + networkRequestIdInDevToolsUI?: number; }, ): void { if (!value) { @@ -85,6 +87,7 @@ export class McpResponse implements Response { : undefined, resourceTypes: options?.resourceTypes, includePreservedRequests: options?.includePreservedRequests, + networkRequestIdInDevToolsUI: options?.networkRequestIdInDevToolsUI, }; } @@ -391,6 +394,8 @@ Call ${handleDialog.name} to handle it before continuing.`); getShortDescriptionForRequest( request, context.getNetworkRequestStableId(request), + context.getNetworkRequestStableId(request) === + this.#networkRequestsOptions?.networkRequestIdInDevToolsUI, ), ); } diff --git a/src/PageCollector.ts b/src/PageCollector.ts index 8cb433501..22b9c7a82 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -163,16 +163,32 @@ export class PageCollector { throw new Error('No requests found for selected page'); } - for (const navigation of navigations) { - for (const collected of navigation) { - if (collected[stableIdSymbol] === stableId) { - return collected; - } - } + const item = this.find(page, item => item[stableIdSymbol] === stableId); + + if (item) { + return item; } throw new Error('Request not found for selected page'); } + + find( + page: Page, + filter: (item: WithSymbolId) => boolean, + ): WithSymbolId | undefined { + const navigations = this.storage.get(page); + if (!navigations) { + return; + } + + for (const navigation of navigations) { + const item = navigation.find(filter); + if (item) { + return item; + } + } + return; + } } export class NetworkCollector extends PageCollector { diff --git a/src/formatters/networkFormatter.ts b/src/formatters/networkFormatter.ts index 3c1abe58c..d3945130a 100644 --- a/src/formatters/networkFormatter.ts +++ b/src/formatters/networkFormatter.ts @@ -13,9 +13,10 @@ const BODY_CONTEXT_SIZE_LIMIT = 10000; export function getShortDescriptionForRequest( request: HTTPRequest, id: number, + selectedInDevToolsUI = false, ): string { // TODO truncate the URL - return `reqid=${id} ${request.method()} ${request.url()} ${getStatusFromRequest(request)}`; + return `reqid=${id} ${request.method()} ${request.url()} ${getStatusFromRequest(request)}${selectedInDevToolsUI ? ` [selected in DevTools UI]` : ''}`; } export function getStatusFromRequest(request: HTTPRequest): string { diff --git a/src/main.ts b/src/main.ts index 3443cecc4..9dad338e9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -129,6 +129,7 @@ function registerTool(tool: ToolDefinition): void { try { logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`); const context = await getContext(); + await context.detectOpenDevToolsWindows(); const response = new McpResponse(); await tool.handler( { diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 68f292603..8df0f720c 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -55,6 +55,7 @@ export interface Response { options?: PaginationOptions & { resourceTypes?: string[]; includePreservedRequests?: boolean; + networkRequestIdInDevToolsUI?: number; }, ): void; setIncludeConsoleData( @@ -102,6 +103,7 @@ export type Context = Readonly<{ text: string; timeout?: number | undefined; }): Promise; + getDevToolsData(): Promise; }>; export function defineTool( diff --git a/src/tools/network.ts b/src/tools/network.ts index fe51f72f2..1df945500 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -70,19 +70,22 @@ export const listNetworkRequests = defineTool({ 'Set to true to return the preserved requests over the last 3 navigations.', ), }, - handler: async (request, response) => { + handler: async (request, response, context) => { + const data = await context.getDevToolsData(); response.setIncludeNetworkRequests(true, { pageSize: request.params.pageSize, pageIdx: request.params.pageIdx, resourceTypes: request.params.resourceTypes, includePreservedRequests: request.params.includePreservedRequests, + networkRequestIdInDevToolsUI: data?.requestId, }); }, }); export const getNetworkRequest = defineTool({ name: 'get_network_request', - description: `Gets a network request by URL. You can get all requests by calling ${listNetworkRequests.name}.`, + description: `Gets a network request by reqid. You can get all requests by calling ${listNetworkRequests.name}. +Get the request currently selected in the DevTools UI by ommitting reqid`, annotations: { category: ToolCategory.NETWORK, readOnlyHint: true, @@ -90,11 +93,23 @@ export const getNetworkRequest = defineTool({ schema: { reqid: zod .number() + .optional() .describe( - 'The reqid of a request on the page from the listed network requests', + 'The reqid of the network request. If omitted, looks up the current request selected in DevTools UI.', ), }, - handler: async (request, response, _context) => { - response.attachNetworkRequest(request.params.reqid); + handler: async (request, response, context) => { + if (request.params.reqid) { + response.attachNetworkRequest(request.params.reqid); + } else { + const data = await context.getDevToolsData(); + if (data?.requestId) { + response.attachNetworkRequest(data?.requestId); + } else { + response.appendResponseLine( + `Nothing is currently selected in DevTools UI.`, + ); + } + } }, }); diff --git a/tests/formatters/networkFormatter.test.ts b/tests/formatters/networkFormatter.test.ts index e22b01406..2bd1bbd64 100644 --- a/tests/formatters/networkFormatter.test.ts +++ b/tests/formatters/networkFormatter.test.ts @@ -71,6 +71,16 @@ describe('networkFormatter', () => { 'reqid=1 GET http://example.com [failed - Error in Network]', ); }); + + it('marks requests selected in DevTools UI', async () => { + const request = getMockRequest(); + const result = getShortDescriptionForRequest(request, 1, true); + + assert.equal( + result, + 'reqid=1 GET http://example.com [pending] [selected in DevTools UI]', + ); + }); }); describe('getFormattedHeaderValue', () => {