Skip to content
This repository was archived by the owner on Apr 16, 2026. It is now read-only.

Commit c83239a

Browse files
committed
tool discovery prototype
1 parent 1c14c0e commit c83239a

10 files changed

Lines changed: 478 additions & 1 deletion

File tree

src/McpContext.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import type {
3030
PredefinedNetworkConditions,
3131
Viewport,
3232
} from './third_party/index.js';
33+
import {type ToolGroup} from './tools/inPage.js';
3334
import {listPages} from './tools/pages.js';
3435
import {takeSnapshot} from './tools/snapshot.js';
3536
import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
@@ -126,6 +127,7 @@ export class McpContext implements Context {
126127
#userAgentMap = new WeakMap<Page, string>();
127128
#colorSchemeMap = new WeakMap<Page, 'dark' | 'light'>();
128129
#dialog?: Dialog;
130+
#inPageTools?: ToolGroup;
129131

130132
#pageIdMap = new WeakMap<Page, number>();
131133
#nextPageId = 1;
@@ -433,6 +435,14 @@ export class McpContext implements Context {
433435
});
434436
}
435437

438+
setInPageTools(toolGroup: ToolGroup|undefined) {
439+
this.#inPageTools = toolGroup;
440+
}
441+
442+
getInPageTools(): ToolGroup | undefined {
443+
return this.#inPageTools;
444+
}
445+
436446
#updateSelectedPageTimeouts() {
437447
const page = this.getSelectedPage();
438448
// For waiters 5sec timeout should be sufficient.

src/McpResponse.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
ResourceType,
1717
TextContent,
1818
} from './third_party/index.js';
19+
import type {ToolGroup} from './tools/inPage.js';
1920
import {handleDialog} from './tools/pages.js';
2021
import type {
2122
DevToolsData,
@@ -62,6 +63,7 @@ export class McpResponse implements Response {
6263
includePreservedMessages?: boolean;
6364
};
6465
#listExtensions?: boolean;
66+
#listInPageTools?: boolean;
6567
#devToolsData?: DevToolsData;
6668
#tabId?: string;
6769

@@ -87,6 +89,10 @@ export class McpResponse implements Response {
8789
this.#listExtensions = true;
8890
}
8991

92+
setListInPageTools(): void {
93+
this.#listInPageTools = true;
94+
}
95+
9096
setIncludeNetworkRequests(
9197
value: boolean,
9298
options?: PaginationOptions & {
@@ -307,6 +313,12 @@ export class McpResponse implements Response {
307313
if (this.#listExtensions) {
308314
extensions = context.listExtensions();
309315
}
316+
317+
let inPageTools: ToolGroup | undefined;
318+
if (this.#listInPageTools) {
319+
inPageTools = context.getInPageTools();
320+
}
321+
310322
let consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
311323
if (this.#consoleDataOptions?.include) {
312324
let messages = context.getConsoleData(
@@ -401,6 +413,7 @@ export class McpResponse implements Response {
401413
traceInsight: this.#attachedTraceInsight,
402414
traceSummary: this.#attachedTraceSummary,
403415
extensions,
416+
inPageTools,
404417
});
405418
}
406419

@@ -416,6 +429,7 @@ export class McpResponse implements Response {
416429
traceSummary?: TraceResult;
417430
traceInsight?: TraceInsightData;
418431
extensions?: InstalledExtension[];
432+
inPageTools?: ToolGroup;
419433
},
420434
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
421435
const structuredContent: {
@@ -429,6 +443,7 @@ export class McpResponse implements Response {
429443
traceSummary?: string;
430444
traceInsights?: Array<{insightName: string; insightKey: string}>;
431445
extensions?: object[];
446+
inPageTools?: object;
432447
message?: string;
433448
networkConditions?: string;
434449
navigationTimeout?: number;
@@ -593,6 +608,26 @@ Call ${handleDialog.name} to handle it before continuing.`);
593608
}
594609
}
595610

611+
if (this.#listInPageTools) {
612+
structuredContent.inPageTools = data.inPageTools;
613+
response.push('## In-page tools');
614+
if (!data.inPageTools) {
615+
response.push('No in-page tools installed.');
616+
} else {
617+
const toolGroup = data.inPageTools;
618+
response.push(`${toolGroup.name}: ${toolGroup.description}`);
619+
response.push('Available tools:');
620+
const toolDefinitionsMessage = toolGroup.tools
621+
.map(tool => {
622+
return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(
623+
tool.inputSchema,
624+
)}`;
625+
})
626+
.join('\n');
627+
response.push(toolDefinitionsMessage);
628+
}
629+
}
630+
596631
if (this.#networkRequestsOptions?.include) {
597632
let requests = context.getNetworkRequests(
598633
this.#networkRequestsOptions?.includePreservedRequests,

src/third_party/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export {
2222
type TextContent,
2323
} from '@modelcontextprotocol/sdk/types.js';
2424
export {z as zod} from 'zod';
25+
export {default as ajv} from 'ajv';
2526
export {
2627
Locator,
2728
PredefinedNetworkConditions,
@@ -31,6 +32,7 @@ export {
3132
export {default as puppeteer} from 'puppeteer-core';
3233
export type * from 'puppeteer-core';
3334
export type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js';
35+
export type {JSONSchema7} from 'json-schema';
3436
export {
3537
resolveDefaultUserDataDir,
3638
detectBrowserPlatform,

src/tools/ToolDefinition.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {InstalledExtension} from '../utils/ExtensionRegistry.js';
1717
import type {PaginationOptions} from '../utils/types.js';
1818

1919
import type {ToolCategory} from './categories.js';
20+
import type {ToolGroup} from './inPage.js';
2021

2122
export interface ToolDefinition<
2223
Schema extends zod.ZodRawShape = zod.ZodRawShape,
@@ -94,6 +95,7 @@ export interface Response {
9495
insightName: InsightName,
9596
): void;
9697
setListExtensions(): void;
98+
setListInPageTools(): void;
9799
}
98100

99101
/**
@@ -149,6 +151,8 @@ export type Context = Readonly<{
149151
uninstallExtension(id: string): Promise<void>;
150152
listExtensions(): InstalledExtension[];
151153
getExtension(id: string): InstalledExtension | undefined;
154+
setInPageTools(toolGroup: ToolGroup|undefined): void;
155+
getInPageTools(): ToolGroup|undefined;
152156
}>;
153157

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

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: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {type Page, zod, ajv, type JSONSchema7} from '../third_party/index.js';
8+
9+
import {ToolCategory} from './categories.js';
10+
import {defineTool} from './ToolDefinition.js';
11+
12+
export interface ToolDefinition {
13+
name: string;
14+
description: string;
15+
inputSchema: JSONSchema7;
16+
execute: (input: any) => any;
17+
}
18+
19+
export interface ToolGroup {
20+
name: string;
21+
description: string;
22+
tools: ToolDefinition[];
23+
}
24+
25+
declare global {
26+
interface Window {
27+
__mcp_tool_group?: ToolGroup;
28+
}
29+
}
30+
31+
async function getToolGroup(page: Page) {
32+
return await page.evaluate(() => {
33+
return new Promise<ToolGroup | undefined>(resolve => {
34+
const event = new CustomEvent('devtoolstooldiscovery');
35+
// @ts-expect-error adding custom property
36+
event.respondWith = (toolGroup: ToolGroup) => {
37+
window.__mcp_tool_group = toolGroup;
38+
resolve(toolGroup);
39+
};
40+
window.dispatchEvent(event);
41+
// TODO: replace with checking for existence of event listener?
42+
// Can `respondWith` be called asynchronously?
43+
setTimeout(() => {
44+
resolve(undefined);
45+
}, 0);
46+
});
47+
});
48+
}
49+
50+
export const listInPageTools = defineTool({
51+
name: 'list_in_page_tools',
52+
description: `Lists all tools the page exposes for providing runtime information.`,
53+
annotations: {
54+
category: ToolCategory.IN_PAGE,
55+
readOnlyHint: true,
56+
},
57+
schema: {},
58+
handler: async (_request, response, context) => {
59+
const page = context.getSelectedPage();
60+
const toolGroup = await getToolGroup(page);
61+
context.setInPageTools(toolGroup);
62+
response.setListInPageTools();
63+
},
64+
});
65+
66+
export const executeInPageTool = defineTool({
67+
name: 'execute_in_page_tool',
68+
description: `Executes a tool exposed by the page.`,
69+
annotations: {
70+
category: ToolCategory.IN_PAGE,
71+
readOnlyHint: false,
72+
},
73+
schema: {
74+
toolName: zod.string().describe('The name of the tool to execute'),
75+
params: zod.record(zod.string(), zod.unknown()).optional().describe('The parameters to pass to the tool'),
76+
},
77+
handler: async (request, response, context) => {
78+
const page = context.getSelectedPage();
79+
const toolName = request.params.toolName;
80+
const params = request.params.params ?? {};
81+
82+
// Get tools from context
83+
// const toolGroup = context.getInPageTools();
84+
// Alternatively: get tools from page
85+
const toolGroup = await getToolGroup(page);
86+
context.setInPageTools(toolGroup);
87+
const tool = toolGroup?.tools.find(t => t.name === toolName);
88+
if (!tool) {
89+
throw new Error(`Tool ${toolName} not found`);
90+
}
91+
const ajvInstance = new ajv();
92+
const validate = ajvInstance.compile(tool.inputSchema);
93+
const valid = validate(params);
94+
if (!valid) {
95+
throw new Error(`Invalid parameters for tool ${toolName}: ${ajvInstance.errorsText(validate.errors)}`);
96+
}
97+
98+
const result = await page.evaluate(async (name, args) => {
99+
if (!window.__mcp_tool_group) {
100+
throw new Error('No tools found on the page');
101+
}
102+
const tool = window.__mcp_tool_group.tools.find(t => t.name === name);
103+
if (tool) {
104+
return await tool.execute(args);
105+
}
106+
throw new Error(`Tool ${name} not found`);
107+
}, toolName, params);
108+
response.appendResponseLine(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
109+
},
110+
});

src/tools/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import * as consoleTools from './console.js';
88
import * as emulationTools from './emulation.js';
99
import * as extensionTools from './extensions.js';
10+
import * as inPageTools from './inPage.js';
1011
import * as inputTools from './input.js';
1112
import * as networkTools from './network.js';
1213
import * as pagesTools from './pages.js';
@@ -20,6 +21,7 @@ const tools = [
2021
...Object.values(consoleTools),
2122
...Object.values(emulationTools),
2223
...Object.values(extensionTools),
24+
...Object.values(inPageTools),
2325
...Object.values(inputTools),
2426
...Object.values(networkTools),
2527
...Object.values(pagesTools),

tests/McpResponse.test.js.snapshot

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,3 +1473,34 @@ exports[`extensions > lists extensions 2`] = `
14731473
]
14741474
}
14751475
`;
1476+
1477+
exports[`in-page tools > lists in-page tools 1`] = `
1478+
# test response
1479+
## In-page tools
1480+
My Tool Group: A group of tools
1481+
Available tools:
1482+
name="myTool", description="Does something", inputSchema={"type":"object","properties":{"foo":{"type":"string"}}}
1483+
`;
1484+
1485+
exports[`in-page tools > lists in-page tools 2`] = `
1486+
{
1487+
"inPageTools": {
1488+
"name": "My Tool Group",
1489+
"description": "A group of tools",
1490+
"tools": [
1491+
{
1492+
"name": "myTool",
1493+
"description": "Does something",
1494+
"inputSchema": {
1495+
"type": "object",
1496+
"properties": {
1497+
"foo": {
1498+
"type": "string"
1499+
}
1500+
}
1501+
}
1502+
}
1503+
]
1504+
}
1505+
}
1506+
`;

0 commit comments

Comments
 (0)