Skip to content

Commit 796aed7

Browse files
authored
feat: detect network requests inspected in DevTools UI (#477)
If you manually inspect a network request in the DevTools UI, the Chrome DevTools MCP server is now able to detect it and allow you to refer to the selected request. The approach was tested in a couple of models. In the list of the network requests we have a short indication about which request is selected in DevTools UI. We refer to the DevTools as "DevTools UI" to disambiguate from the DevTools MCP server.
1 parent 40e1753 commit 796aed7

File tree

9 files changed

+107
-16
lines changed

9 files changed

+107
-16
lines changed

docs/tool-reference.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,12 @@
255255

256256
### `get_network_request`
257257

258-
**Description:** Gets a network request by URL. You can get all requests by calling [`list_network_requests`](#list_network_requests).
258+
**Description:** Gets a network request by reqid. You can get all requests by calling [`list_network_requests`](#list_network_requests).
259+
Get the request currently selected in the DevTools UI by ommitting reqid
259260

260261
**Parameters:**
261262

262-
- **reqid** (number) **(required)**: The reqid of a request on the page from the listed network requests
263+
- **reqid** (number) _(optional)_: The reqid of the network request. If omitted, looks up the current request selected in DevTools UI.
263264

264265
---
265266

src/McpContext.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,12 +337,13 @@ export class McpContext implements Context {
337337
);
338338
});
339339

340-
await this.#detectOpenDevToolsWindows(allPages);
340+
await this.detectOpenDevToolsWindows();
341341

342342
return this.#pages;
343343
}
344344

345-
async #detectOpenDevToolsWindows(pages: Page[]) {
345+
async detectOpenDevToolsWindows() {
346+
const pages = await this.browser.pages();
346347
this.#pageToDevToolsPage = new Map<Page, Page>();
347348
for (const devToolsPage of pages) {
348349
if (devToolsPage.url().startsWith('devtools://')) {
@@ -377,6 +378,45 @@ export class McpContext implements Context {
377378
return this.#pageToDevToolsPage.get(page);
378379
}
379380

381+
async getDevToolsData(): Promise<undefined | {requestId?: number}> {
382+
try {
383+
const selectedPage = this.getSelectedPage();
384+
const devtoolsPage = this.getDevToolsPage(selectedPage);
385+
if (devtoolsPage) {
386+
const cdpRequestId = await devtoolsPage.evaluate(async () => {
387+
// @ts-expect-error no types
388+
const UI = await import('/bundled/ui/legacy/legacy.js');
389+
// @ts-expect-error no types
390+
const SDK = await import('/bundled/core/sdk/sdk.js');
391+
const request = UI.Context.Context.instance().flavor(
392+
SDK.NetworkRequest.NetworkRequest,
393+
);
394+
return request?.requestId();
395+
});
396+
if (!cdpRequestId) {
397+
this.logger('no context request');
398+
return;
399+
}
400+
const request = this.#networkCollector.find(selectedPage, request => {
401+
// @ts-expect-error id is internal.
402+
return request.id === cdpRequestId;
403+
});
404+
if (!request) {
405+
this.logger('no collected request for ' + cdpRequestId);
406+
return;
407+
}
408+
return {
409+
requestId: this.#networkCollector.getIdForResource(request),
410+
};
411+
} else {
412+
this.logger('no devtools page deteched');
413+
}
414+
} catch (err) {
415+
this.logger('error getting devtools data', err);
416+
}
417+
return;
418+
}
419+
380420
/**
381421
* Creates a text snapshot of a page.
382422
*/

src/McpResponse.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export class McpResponse implements Response {
4444
pagination?: PaginationOptions;
4545
resourceTypes?: ResourceType[];
4646
includePreservedRequests?: boolean;
47+
networkRequestIdInDevToolsUI?: number;
4748
};
4849
#consoleDataOptions?: {
4950
include: boolean;
@@ -67,6 +68,7 @@ export class McpResponse implements Response {
6768
options?: PaginationOptions & {
6869
resourceTypes?: ResourceType[];
6970
includePreservedRequests?: boolean;
71+
networkRequestIdInDevToolsUI?: number;
7072
},
7173
): void {
7274
if (!value) {
@@ -85,6 +87,7 @@ export class McpResponse implements Response {
8587
: undefined,
8688
resourceTypes: options?.resourceTypes,
8789
includePreservedRequests: options?.includePreservedRequests,
90+
networkRequestIdInDevToolsUI: options?.networkRequestIdInDevToolsUI,
8891
};
8992
}
9093

@@ -391,6 +394,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
391394
getShortDescriptionForRequest(
392395
request,
393396
context.getNetworkRequestStableId(request),
397+
context.getNetworkRequestStableId(request) ===
398+
this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
394399
),
395400
);
396401
}

src/PageCollector.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,16 +163,32 @@ export class PageCollector<T> {
163163
throw new Error('No requests found for selected page');
164164
}
165165

166-
for (const navigation of navigations) {
167-
for (const collected of navigation) {
168-
if (collected[stableIdSymbol] === stableId) {
169-
return collected;
170-
}
171-
}
166+
const item = this.find(page, item => item[stableIdSymbol] === stableId);
167+
168+
if (item) {
169+
return item;
172170
}
173171

174172
throw new Error('Request not found for selected page');
175173
}
174+
175+
find(
176+
page: Page,
177+
filter: (item: WithSymbolId<T>) => boolean,
178+
): WithSymbolId<T> | undefined {
179+
const navigations = this.storage.get(page);
180+
if (!navigations) {
181+
return;
182+
}
183+
184+
for (const navigation of navigations) {
185+
const item = navigation.find(filter);
186+
if (item) {
187+
return item;
188+
}
189+
}
190+
return;
191+
}
176192
}
177193

178194
export class NetworkCollector extends PageCollector<HTTPRequest> {

src/formatters/networkFormatter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ const BODY_CONTEXT_SIZE_LIMIT = 10000;
1313
export function getShortDescriptionForRequest(
1414
request: HTTPRequest,
1515
id: number,
16+
selectedInDevToolsUI = false,
1617
): string {
1718
// TODO truncate the URL
18-
return `reqid=${id} ${request.method()} ${request.url()} ${getStatusFromRequest(request)}`;
19+
return `reqid=${id} ${request.method()} ${request.url()} ${getStatusFromRequest(request)}${selectedInDevToolsUI ? ` [selected in DevTools UI]` : ''}`;
1920
}
2021

2122
export function getStatusFromRequest(request: HTTPRequest): string {

src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ function registerTool(tool: ToolDefinition): void {
129129
try {
130130
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
131131
const context = await getContext();
132+
await context.detectOpenDevToolsWindows();
132133
const response = new McpResponse();
133134
await tool.handler(
134135
{

src/tools/ToolDefinition.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface Response {
5555
options?: PaginationOptions & {
5656
resourceTypes?: string[];
5757
includePreservedRequests?: boolean;
58+
networkRequestIdInDevToolsUI?: number;
5859
},
5960
): void;
6061
setIncludeConsoleData(
@@ -102,6 +103,7 @@ export type Context = Readonly<{
102103
text: string;
103104
timeout?: number | undefined;
104105
}): Promise<Element>;
106+
getDevToolsData(): Promise<undefined | {requestId?: number}>;
105107
}>;
106108

107109
export function defineTool<Schema extends zod.ZodRawShape>(

src/tools/network.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,31 +70,46 @@ export const listNetworkRequests = defineTool({
7070
'Set to true to return the preserved requests over the last 3 navigations.',
7171
),
7272
},
73-
handler: async (request, response) => {
73+
handler: async (request, response, context) => {
74+
const data = await context.getDevToolsData();
7475
response.setIncludeNetworkRequests(true, {
7576
pageSize: request.params.pageSize,
7677
pageIdx: request.params.pageIdx,
7778
resourceTypes: request.params.resourceTypes,
7879
includePreservedRequests: request.params.includePreservedRequests,
80+
networkRequestIdInDevToolsUI: data?.requestId,
7981
});
8082
},
8183
});
8284

8385
export const getNetworkRequest = defineTool({
8486
name: 'get_network_request',
85-
description: `Gets a network request by URL. You can get all requests by calling ${listNetworkRequests.name}.`,
87+
description: `Gets a network request by reqid. You can get all requests by calling ${listNetworkRequests.name}.
88+
Get the request currently selected in the DevTools UI by ommitting reqid`,
8689
annotations: {
8790
category: ToolCategory.NETWORK,
8891
readOnlyHint: true,
8992
},
9093
schema: {
9194
reqid: zod
9295
.number()
96+
.optional()
9397
.describe(
94-
'The reqid of a request on the page from the listed network requests',
98+
'The reqid of the network request. If omitted, looks up the current request selected in DevTools UI.',
9599
),
96100
},
97-
handler: async (request, response, _context) => {
98-
response.attachNetworkRequest(request.params.reqid);
101+
handler: async (request, response, context) => {
102+
if (request.params.reqid) {
103+
response.attachNetworkRequest(request.params.reqid);
104+
} else {
105+
const data = await context.getDevToolsData();
106+
if (data?.requestId) {
107+
response.attachNetworkRequest(data?.requestId);
108+
} else {
109+
response.appendResponseLine(
110+
`Nothing is currently selected in DevTools UI.`,
111+
);
112+
}
113+
}
99114
},
100115
});

tests/formatters/networkFormatter.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ describe('networkFormatter', () => {
7171
'reqid=1 GET http://example.com [failed - Error in Network]',
7272
);
7373
});
74+
75+
it('marks requests selected in DevTools UI', async () => {
76+
const request = getMockRequest();
77+
const result = getShortDescriptionForRequest(request, 1, true);
78+
79+
assert.equal(
80+
result,
81+
'reqid=1 GET http://example.com [pending] [selected in DevTools UI]',
82+
);
83+
});
7484
});
7585

7686
describe('getFormattedHeaderValue', () => {

0 commit comments

Comments
 (0)