Skip to content

Commit cea963b

Browse files
authored
feat: Add a new tool for listing in-page tools (gated behind a command line flag) (#1239)
This adds a `list_in_page_tools` MCP tool. When called, it dispatches a `devtoolstooldiscovery` event on the active page. The page announces its exposing tools by calling the event's `respondWith` method, which causes the exposed tools to be stashed on the page's `window` object. This list of in-page tools is then appended to the `list_in_page_tools` response. Calling the exposed in-page tools from the MCP server will be handled in a follow-up.
1 parent 2664455 commit cea963b

13 files changed

Lines changed: 427 additions & 4 deletions

src/McpResponse.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ import {SnapshotFormatter} from './formatters/SnapshotFormatter.js';
1212
import type {McpContext} from './McpContext.js';
1313
import type {McpPage} from './McpPage.js';
1414
import {UncaughtError} from './PageCollector.js';
15-
import {DevTools} from './third_party/index.js';
15+
import {DevTools, type Protocol} from './third_party/index.js';
1616
import type {
1717
ConsoleMessage,
1818
ImageContent,
1919
Page,
2020
ResourceType,
2121
TextContent,
2222
} from './third_party/index.js';
23+
import type {ToolGroup, ToolDefinition} from './tools/inPage.js';
2324
import {handleDialog} from './tools/pages.js';
2425
import type {
2526
DevToolsData,
@@ -40,6 +41,59 @@ interface TraceInsightData {
4041
insightName: InsightName;
4142
}
4243

44+
async function getToolGroup(
45+
page: McpPage,
46+
): Promise<ToolGroup<ToolDefinition> | undefined> {
47+
// Check if there is a `devtoolstooldiscovery` event listener
48+
const windowHandle = await page.pptrPage.evaluateHandle(() => window);
49+
// @ts-expect-error internal API
50+
const client = page.pptrPage._client();
51+
const {listeners}: {listeners: Protocol.DOMDebugger.EventListener[]} =
52+
await client.send('DOMDebugger.getEventListeners', {
53+
objectId: windowHandle.remoteObject().objectId,
54+
});
55+
if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
56+
return;
57+
}
58+
59+
const toolGroup = await page.pptrPage.evaluate(() => {
60+
return new Promise<ToolGroup<ToolDefinition> | undefined>(resolve => {
61+
const event = new CustomEvent('devtoolstooldiscovery');
62+
// @ts-expect-error Adding custom property
63+
event.respondWith = (toolGroup: ToolGroup) => {
64+
if (!window.__dtmcp) {
65+
window.__dtmcp = {};
66+
}
67+
window.__dtmcp.toolGroup = toolGroup;
68+
69+
// When receiving a toolGroup for the first time, expose a simple execution helper
70+
if (!window.__dtmcp.executeTool) {
71+
window.__dtmcp.executeTool = async (toolName, args) => {
72+
if (!window.__dtmcp?.toolGroup) {
73+
throw new Error('No tools found on the page');
74+
}
75+
const tool = window.__dtmcp.toolGroup.tools.find(
76+
t => t.name === toolName,
77+
);
78+
if (!tool) {
79+
throw new Error(`Tool ${toolName} not found`);
80+
}
81+
return await tool.execute(args);
82+
};
83+
}
84+
85+
resolve(toolGroup);
86+
};
87+
window.dispatchEvent(event);
88+
// If the page does not synchronously call `event.respondWith`, return instead of timing out
89+
setTimeout(() => {
90+
resolve(undefined);
91+
}, 0);
92+
});
93+
});
94+
return toolGroup;
95+
}
96+
4397
export class McpResponse implements Response {
4498
#includePages = false;
4599
#includeExtensionServiceWorkers = false;
@@ -70,6 +124,7 @@ export class McpResponse implements Response {
70124
includePreservedMessages?: boolean;
71125
};
72126
#listExtensions?: boolean;
127+
#listInPageTools?: boolean;
73128
#devToolsData?: DevToolsData;
74129
#tabId?: string;
75130
#args: ParsedArguments;
@@ -110,6 +165,12 @@ export class McpResponse implements Response {
110165
this.#listExtensions = true;
111166
}
112167

168+
setListInPageTools(): void {
169+
if (this.#args.categoryInPageTools) {
170+
this.#listInPageTools = true;
171+
}
172+
}
173+
113174
setIncludeNetworkRequests(
114175
value: boolean,
115176
options?: PaginationOptions & {
@@ -357,6 +418,12 @@ export class McpResponse implements Response {
357418
if (this.#listExtensions) {
358419
extensions = context.listExtensions();
359420
}
421+
422+
let inPageTools: ToolGroup<ToolDefinition> | undefined;
423+
if (this.#listInPageTools) {
424+
inPageTools = await getToolGroup(context.getSelectedMcpPage());
425+
}
426+
360427
let consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
361428
if (this.#consoleDataOptions?.include) {
362429
if (!this.#page) {
@@ -459,6 +526,7 @@ export class McpResponse implements Response {
459526
traceSummary: this.#attachedTraceSummary,
460527
extensions,
461528
lighthouseResult: this.#attachedLighthouseResult,
529+
inPageTools,
462530
});
463531
}
464532

@@ -475,6 +543,7 @@ export class McpResponse implements Response {
475543
traceInsight?: TraceInsightData;
476544
extensions?: InstalledExtension[];
477545
lighthouseResult?: LighthouseData;
546+
inPageTools?: ToolGroup<ToolDefinition>;
478547
},
479548
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
480549
const structuredContent: {
@@ -489,6 +558,7 @@ export class McpResponse implements Response {
489558
traceInsights?: Array<{insightName: string; insightKey: string}>;
490559
lighthouseResult?: object;
491560
extensions?: object[];
561+
inPageTools?: object;
492562
message?: string;
493563
networkConditions?: string;
494564
navigationTimeout?: number;
@@ -726,6 +796,26 @@ Call ${handleDialog.name} to handle it before continuing.`);
726796
}
727797
}
728798

799+
if (this.#listInPageTools) {
800+
structuredContent.inPageTools = data.inPageTools ?? undefined;
801+
response.push('## In-page tools');
802+
if (!data.inPageTools || !data.inPageTools.tools) {
803+
response.push('No in-page tools available.');
804+
} else {
805+
const toolGroup = data.inPageTools;
806+
response.push(`${toolGroup.name}: ${toolGroup.description}`);
807+
response.push('Available tools:');
808+
const toolDefinitionsMessage = toolGroup.tools
809+
.map(tool => {
810+
return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(
811+
tool.inputSchema,
812+
)}`;
813+
})
814+
.join('\n');
815+
response.push(toolDefinitionsMessage);
816+
}
817+
}
818+
729819
if (this.#networkRequestsOptions?.include && data.networkRequests) {
730820
const requests = data.networkRequests;
731821

src/bin/chrome-devtools-mcp-cli-options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ export const cliOptions = {
216216
describe:
217217
'Set to true to include tools related to extensions. Note: This feature is only supported with a pipe connection. autoConnect is not supported.',
218218
},
219+
categoryInPageTools: {
220+
type: 'boolean',
221+
hidden: true,
222+
describe:
223+
'Set to true to enable tools exposed by the inspected page itself',
224+
},
219225
performanceCrux: {
220226
type: 'boolean',
221227
default: true,

src/bin/chrome-devtools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ delete startCliOptions.autoConnect;
4141
// Missing CLI serialization.
4242
delete startCliOptions.viewport;
4343
// CLI is generated based on the default tool definitions. To enable conditional
44-
// tools, they needs to be enabled during CLI generation.
44+
// tools, they need to be enabled during CLI generation.
4545
delete startCliOptions.experimentalPageIdRouting;
4646
delete startCliOptions.experimentalVision;
4747
delete startCliOptions.experimentalInteropTools;

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ export async function createMcpServer(
140140
) {
141141
return;
142142
}
143+
if (
144+
tool.annotations.category === ToolCategory.IN_PAGE &&
145+
!serverArgs.categoryInPageTools
146+
) {
147+
return;
148+
}
143149
if (
144150
tool.annotations.conditions?.includes('computerVision') &&
145151
!serverArgs.experimentalVision

src/third_party/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export {default as puppeteer} from 'puppeteer-core';
3939
export type * from 'puppeteer-core';
4040
export {PipeTransport} from 'puppeteer-core/internal/node/PipeTransport.js';
4141
export type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js';
42+
export type {JSONSchema7} from 'json-schema';
4243
export {
4344
resolveDefaultUserDataDir,
4445
detectBrowserPlatform,

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export interface Response {
129129
): void;
130130
setListExtensions(): void;
131131
attachLighthouseResult(result: LighthouseData): void;
132+
setListInPageTools(): void;
132133
}
133134

134135
/**

src/tools/categories.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export enum ToolCategory {
1212
NETWORK = 'network',
1313
DEBUGGING = 'debugging',
1414
EXTENSIONS = 'extensions',
15+
IN_PAGE = 'in-page',
1516
}
1617

1718
export const labels = {
@@ -22,4 +23,5 @@ export const labels = {
2223
[ToolCategory.NETWORK]: 'Network',
2324
[ToolCategory.DEBUGGING]: 'Debugging',
2425
[ToolCategory.EXTENSIONS]: 'Extensions',
26+
[ToolCategory.IN_PAGE]: 'In-page tools',
2527
};

src/tools/inPage.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {type JSONSchema7} from '../third_party/index.js';
8+
9+
import {ToolCategory} from './categories.js';
10+
import {definePageTool} from './ToolDefinition.js';
11+
12+
export interface ToolDefinition {
13+
name: string;
14+
description: string;
15+
inputSchema: JSONSchema7;
16+
}
17+
18+
export interface ToolGroup<T extends ToolDefinition> {
19+
name: string;
20+
description: string;
21+
tools: T[];
22+
}
23+
24+
declare global {
25+
interface Window {
26+
__dtmcp?: {
27+
toolGroup?: ToolGroup<
28+
ToolDefinition & {execute: (args: Record<string, unknown>) => unknown}
29+
>;
30+
executeTool?: (
31+
toolName: string,
32+
args: Record<string, unknown>,
33+
) => unknown;
34+
};
35+
}
36+
}
37+
38+
export const listInPageTools = definePageTool({
39+
name: 'list_in_page_tools',
40+
description: `Lists all in-page-tools the page exposes for providing runtime information.
41+
To call 'list_in_page_tools', call 'evaluate_script' with
42+
'window.__dtmcp.executeTool("list_in_page_tools", {})'.`,
43+
annotations: {
44+
category: ToolCategory.IN_PAGE,
45+
readOnlyHint: true,
46+
conditions: ['inPageTools'],
47+
},
48+
schema: {},
49+
handler: async (_request, response, _context) => {
50+
response.setListInPageTools();
51+
},
52+
});

src/tools/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {ParsedArguments} from '../bin/chrome-devtools-mcp-cli-options.js';
99
import * as consoleTools from './console.js';
1010
import * as emulationTools from './emulation.js';
1111
import * as extensionTools from './extensions.js';
12+
import * as inPageTools from './inPage.js';
1213
import * as inputTools from './input.js';
1314
import * as lighthouseTools from './lighthouse.js';
1415
import * as memoryTools from './memory.js';
@@ -29,6 +30,7 @@ export const createTools = (args: ParsedArguments) => {
2930
...Object.values(consoleTools),
3031
...Object.values(emulationTools),
3132
...Object.values(extensionTools),
33+
...Object.values(inPageTools),
3234
...Object.values(inputTools),
3335
...Object.values(lighthouseTools),
3436
...Object.values(memoryTools),

tests/McpResponse.test.js.snapshot

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,36 @@ exports[`extensions > lists extensions 2`] = `
12061206
}
12071207
`;
12081208

1209+
exports[`inPage tools > lists in-page tools 1`] = `
1210+
## In-page tools
1211+
My Tool Group: A group of tools
1212+
Available tools:
1213+
name="myTool", description="Does something", inputSchema={"type":"object","properties":{"foo":{"type":"string"}}}
1214+
`;
1215+
1216+
exports[`inPage tools > lists in-page tools 2`] = `
1217+
{
1218+
"inPageTools": {
1219+
"name": "My Tool Group",
1220+
"description": "A group of tools",
1221+
"tools": [
1222+
{
1223+
"name": "myTool",
1224+
"description": "Does something",
1225+
"inputSchema": {
1226+
"type": "object",
1227+
"properties": {
1228+
"foo": {
1229+
"type": "string"
1230+
}
1231+
}
1232+
}
1233+
}
1234+
]
1235+
}
1236+
}
1237+
`;
1238+
12091239
exports[`lighthouse > includes lighthouse report paths 1`] = `
12101240
## Lighthouse Audit Results
12111241
Mode: navigation

0 commit comments

Comments
 (0)