Skip to content

Commit 377aa63

Browse files
committed
feat: get state from the open DevTools window
1 parent e8394ab commit 377aa63

File tree

6 files changed

+104
-2
lines changed

6 files changed

+104
-2
lines changed

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class McpResponse implements Response {
5151
types?: string[];
5252
includePreservedMessages?: boolean;
5353
};
54+
#includeDevtoolsData = false;
5455

5556
setIncludePages(value: boolean): void {
5657
this.#includePages = value;
@@ -62,6 +63,10 @@ export class McpResponse implements Response {
6263
};
6364
}
6465

66+
includeDevtoolsData(value: boolean): void {
67+
this.#includeDevtoolsData = value;
68+
}
69+
6570
setIncludeNetworkRequests(
6671
value: boolean,
6772
options?: PaginationOptions & {
@@ -291,11 +296,17 @@ export class McpResponse implements Response {
291296
);
292297
}
293298

299+
let devToolsData;
300+
if (this.#includeDevtoolsData) {
301+
devToolsData = await context.getDevToolsData();
302+
}
303+
294304
return this.format(toolName, context, {
295305
bodies,
296306
consoleData,
297307
consoleListData,
298308
formattedSnapshot,
309+
devToolsData,
299310
});
300311
}
301312

@@ -310,13 +321,23 @@ export class McpResponse implements Response {
310321
consoleData: ConsoleMessageData | undefined;
311322
consoleListData: ConsoleMessageData[] | undefined;
312323
formattedSnapshot: string | undefined;
324+
devToolsData?: {requestId?: number};
313325
},
314326
): Array<TextContent | ImageContent> {
315327
const response = [`# ${toolName} response`];
316328
for (const line of this.#textResponseLines) {
317329
response.push(line);
318330
}
319331

332+
response.push('## Network requests inspected in DevTools');
333+
if (data.devToolsData?.requestId) {
334+
response.push(`Network request: reqid=${data.devToolsData?.requestId}`);
335+
} else {
336+
response.push(
337+
`Nothing inspected right now. Call list_pages to check if anything is selected by the user in DevTools.`,
338+
);
339+
}
340+
320341
const networkConditions = context.getNetworkConditions();
321342
if (networkConditions) {
322343
response.push(`## Network emulation`);

src/PageCollector.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,22 @@ export class PageCollector<T> {
173173

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

178194
export class NetworkCollector extends PageCollector<HTTPRequest> {

src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from './third_party/index.js';
2222
import {ToolCategory} from './tools/categories.js';
2323
import * as consoleTools from './tools/console.js';
24+
import * as devtoolsTools from './tools/devtools.js';
2425
import * as emulationTools from './tools/emulation.js';
2526
import * as inputTools from './tools/input.js';
2627
import * as networkTools from './tools/network.js';
@@ -129,6 +130,7 @@ function registerTool(tool: ToolDefinition): void {
129130
try {
130131
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
131132
const context = await getContext();
133+
await context.detectOpenDevToolsWindows();
132134
const response = new McpResponse();
133135
await tool.handler(
134136
{
@@ -168,6 +170,7 @@ function registerTool(tool: ToolDefinition): void {
168170

169171
const tools = [
170172
...Object.values(consoleTools),
173+
...Object.values(devtoolsTools),
171174
...Object.values(emulationTools),
172175
...Object.values(inputTools),
173176
...Object.values(networkTools),

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export interface Response {
6565
},
6666
): void;
6767
includeSnapshot(params?: SnapshotParams): void;
68+
includeDevtoolsData(value: boolean): void;
6869
attachImage(value: ImageContentData): void;
6970
attachNetworkRequest(reqid: number): void;
7071
attachConsoleMessage(msgid: number): void;

src/tools/devtools.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {ToolCategory} from './categories.js';
8+
import {defineTool} from './ToolDefinition.js';
9+
10+
export const emulateNetwork = defineTool({
11+
name: 'list_devtools_data',
12+
description: `Returns data (network requests) that the user is currently inspecting in DevTools.`,
13+
annotations: {
14+
category: ToolCategory.DEBUGGING,
15+
readOnlyHint: true,
16+
},
17+
schema: {},
18+
handler: async (_request, response, _context) => {
19+
response.includeDevtoolsData(true);
20+
},
21+
});

0 commit comments

Comments
 (0)