Skip to content

Commit f6a7c80

Browse files
feat(webmcp): Add experimental tool to list WebMCP tools the page exposes
1 parent f8142e2 commit f6a7c80

File tree

12 files changed

+248
-1
lines changed

12 files changed

+248
-1
lines changed

src/McpContext.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {McpPage} from './McpPage.js';
1313
import {
1414
NetworkCollector,
1515
ConsoleCollector,
16+
WebMcpCollector,
1617
type ListenerMap,
1718
type UncaughtError,
1819
} from './PageCollector.js';
@@ -41,6 +42,7 @@ import type {
4142
TextSnapshot,
4243
TextSnapshotNode,
4344
ExtensionServiceWorker,
45+
WebMcpTool,
4446
} from './types.js';
4547
import {
4648
ExtensionRegistry,
@@ -77,6 +79,7 @@ export class McpContext implements Context {
7779
#selectedPage?: McpPage;
7880
#networkCollector: NetworkCollector;
7981
#consoleCollector: ConsoleCollector;
82+
#webmcpCollector: WebMcpCollector;
8083
#devtoolsUniverseManager: UniverseManager;
8184
#extensionRegistry = new ExtensionRegistry();
8285

@@ -122,6 +125,13 @@ export class McpContext implements Context {
122125
},
123126
} as ListenerMap;
124127
});
128+
this.#webmcpCollector = new WebMcpCollector(this.browser, collect => {
129+
return {
130+
webmcpToolAdded: event => {
131+
collect(event);
132+
},
133+
} as ListenerMap;
134+
});
125135
this.#devtoolsUniverseManager = new UniverseManager(this.browser);
126136
}
127137

@@ -130,12 +140,14 @@ export class McpContext implements Context {
130140
await this.createExtensionServiceWorkersSnapshot();
131141
await this.#networkCollector.init(pages);
132142
await this.#consoleCollector.init(pages);
143+
await this.#webmcpCollector.init(pages);
133144
await this.#devtoolsUniverseManager.init(pages);
134145
}
135146

136147
dispose() {
137148
this.#networkCollector.dispose();
138149
this.#consoleCollector.dispose();
150+
this.#webmcpCollector.dispose();
139151
this.#devtoolsUniverseManager.dispose();
140152
for (const mcpPage of this.#mcpPages.values()) {
141153
mcpPage.dispose();
@@ -222,6 +234,10 @@ export class McpContext implements Context {
222234
);
223235
}
224236

237+
getWebMcpTools(page: McpPage): WebMcpTool[] {
238+
return this.#webmcpCollector.getData(page.pptrPage) ?? [];
239+
}
240+
225241
getDevToolsUniverse(page: McpPage): TargetUniverse | null {
226242
return this.#devtoolsUniverseManager.get(page.pptrPage);
227243
}

src/McpResponse.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
} from './tools/ToolDefinition.js';
3333
import type {InsightName, TraceResult} from './trace-processing/parse.js';
3434
import {getInsightOutput, getTraceSummary} from './trace-processing/parse.js';
35+
import type {WebMcpTool} from './types.js';
3536
import type {InstalledExtension} from './utils/ExtensionRegistry.js';
3637
import {paginate} from './utils/pagination.js';
3738
import type {PaginationOptions} from './utils/types.js';
@@ -181,6 +182,7 @@ export class McpResponse implements Response {
181182
};
182183
#listExtensions?: boolean;
183184
#listInPageTools?: boolean;
185+
#listWebMcpTools?: boolean;
184186
#devToolsData?: DevToolsData;
185187
#tabId?: string;
186188
#args: ParsedArguments;
@@ -227,6 +229,12 @@ export class McpResponse implements Response {
227229
}
228230
}
229231

232+
setListWebMcpTools(): void {
233+
if (this.#args.experimentalWebmcp) {
234+
this.#listWebMcpTools = true;
235+
}
236+
}
237+
230238
setIncludeNetworkRequests(
231239
value: boolean,
232240
options?: PaginationOptions & {
@@ -482,6 +490,12 @@ export class McpResponse implements Response {
482490
page.inPageTools = inPageTools;
483491
}
484492

493+
let webmcpTools: WebMcpTool[] | undefined;
494+
if (this.#listWebMcpTools) {
495+
const page = this.#page ?? context.getSelectedMcpPage();
496+
webmcpTools = context.getWebMcpTools(page);
497+
}
498+
485499
let consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
486500
if (this.#consoleDataOptions?.include) {
487501
if (!this.#page) {
@@ -585,6 +599,7 @@ export class McpResponse implements Response {
585599
extensions,
586600
lighthouseResult: this.#attachedLighthouseResult,
587601
inPageTools,
602+
webmcpTools,
588603
});
589604
}
590605

@@ -602,6 +617,7 @@ export class McpResponse implements Response {
602617
extensions?: InstalledExtension[];
603618
lighthouseResult?: LighthouseData;
604619
inPageTools?: ToolGroup<ToolDefinition>;
620+
webmcpTools?: WebMcpTool[];
605621
},
606622
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
607623
const structuredContent: {
@@ -617,6 +633,7 @@ export class McpResponse implements Response {
617633
lighthouseResult?: object;
618634
extensions?: object[];
619635
inPageTools?: object;
636+
webmcpTools?: object[];
620637
message?: string;
621638
networkConditions?: string;
622639
navigationTimeout?: number;
@@ -874,6 +891,23 @@ Call ${handleDialog.name} to handle it before continuing.`);
874891
}
875892
}
876893

894+
if (this.#listWebMcpTools && data.webmcpTools) {
895+
structuredContent.webmcpTools = data.webmcpTools;
896+
response.push('## WebMCP tools');
897+
if (data.webmcpTools.length === 0) {
898+
response.push('No WebMCP tools available.');
899+
} else {
900+
const webmcpToolsMessage = data.webmcpTools
901+
.map(tool => {
902+
return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(
903+
tool.inputSchema,
904+
)}, annotations=${JSON.stringify(tool.annotations)}`;
905+
})
906+
.join('\n');
907+
response.push(webmcpToolsMessage);
908+
}
909+
}
910+
877911
if (this.#networkRequestsOptions?.include && data.networkRequests) {
878912
const requests = data.networkRequests;
879913

src/PageCollector.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
type Page,
2222
type PageEvents as PuppeteerPageEvents,
2323
} from './third_party/index.js';
24+
import type {WebMcpTool} from './types.js';
2425

2526
export class UncaughtError {
2627
readonly details: Protocol.Runtime.ExceptionDetails;
@@ -35,6 +36,7 @@ export class UncaughtError {
3536
interface PageEvents extends PuppeteerPageEvents {
3637
issue: DevTools.AggregatedIssue;
3738
uncaughtError: UncaughtError;
39+
webmcpToolAdded: WebMcpTool;
3840
}
3941

4042
export type ListenerMap<EventMap extends PageEvents = PageEvents> = {
@@ -412,3 +414,59 @@ export class NetworkCollector extends PageCollector<HTTPRequest> {
412414
navigations.splice(this.maxNavigationSaved);
413415
}
414416
}
417+
418+
export class WebMcpCollector extends PageCollector<WebMcpTool> {
419+
#subscribedPages = new WeakMap<Page, WebMcpSubscriber>();
420+
421+
override addPage(page: Page): void {
422+
super.addPage(page);
423+
if (!this.#subscribedPages.has(page)) {
424+
const subscriber = new WebMcpSubscriber(page);
425+
this.#subscribedPages.set(page, subscriber);
426+
void subscriber.subscribe();
427+
}
428+
}
429+
430+
protected override cleanupPageDestroyed(page: Page): void {
431+
super.cleanupPageDestroyed(page);
432+
void this.#subscribedPages.get(page)?.unsubscribe();
433+
this.#subscribedPages.delete(page);
434+
}
435+
}
436+
437+
class WebMcpSubscriber {
438+
#page: Page;
439+
#session: CDPSession;
440+
#onToolsAdded: (data: unknown) => void;
441+
442+
constructor(page: Page) {
443+
this.#page = page;
444+
// @ts-expect-error use existing CDP client (internal Puppeteer API).
445+
this.#session = this.#page._client() as CDPSession;
446+
this.#onToolsAdded = (data: unknown) => {
447+
for (const tool of (data as {tools: WebMcpTool[]}).tools) {
448+
this.#page.emit('webmcpToolAdded', tool);
449+
}
450+
};
451+
}
452+
453+
async subscribe() {
454+
this.#session.on('WebMCP.toolsAdded', this.#onToolsAdded);
455+
try {
456+
// @ts-expect-error WebMCP is an experimental domain
457+
await this.#session.send('WebMCP.enable');
458+
} catch (error) {
459+
logger('Error subscribing to WebMCP', error);
460+
}
461+
}
462+
463+
async unsubscribe() {
464+
this.#session.off('WebMCP.toolsAdded', this.#onToolsAdded);
465+
try {
466+
// @ts-expect-error WebMCP is an experimental domain
467+
await this.#session.send('WebMCP.disable');
468+
} catch (error) {
469+
logger('Error unsubscribing to WebMCP', error);
470+
}
471+
}
472+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ export const cliOptions = {
185185
describe:
186186
'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
187187
},
188+
experimentalWebmcp: {
189+
type: 'boolean',
190+
describe: 'Set to true to enable debugging WebMCP tools.',
191+
hidden: true,
192+
},
188193
chromeArg: {
189194
type: 'array',
190195
describe:

src/bin/chrome-devtools.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ delete startCliOptions.viewport;
5151
// tools, they need to be enabled during CLI generation.
5252
delete startCliOptions.experimentalPageIdRouting;
5353
delete startCliOptions.experimentalVision;
54+
delete startCliOptions.experimentalWebmcp;
5455
delete startCliOptions.experimentalInteropTools;
5556
delete startCliOptions.experimentalScreencast;
5657
delete startCliOptions.categoryEmulation;

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,12 @@ export async function createMcpServer(
164164
) {
165165
return;
166166
}
167+
if (
168+
tool.annotations.conditions?.includes('experimentalWebmcp') &&
169+
!serverArgs.experimentalWebmcp
170+
) {
171+
return;
172+
}
167173
const schema =
168174
'pageScoped' in tool &&
169175
tool.pageScoped &&

src/telemetry/tool_call_metrics.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,5 +539,9 @@
539539
"argType": "number"
540540
}
541541
]
542+
},
543+
{
544+
"name": "list_webmcp_tools",
545+
"args": []
542546
}
543547
]

src/tools/ToolDefinition.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
TextSnapshotNode,
2020
GeolocationOptions,
2121
ExtensionServiceWorker,
22+
WebMcpTool,
2223
} from '../types.js';
2324
import type {InstalledExtension} from '../utils/ExtensionRegistry.js';
2425
import type {PaginationOptions} from '../utils/types.js';
@@ -134,6 +135,7 @@ export interface Response {
134135
setListExtensions(): void;
135136
attachLighthouseResult(result: LighthouseData): void;
136137
setListInPageTools(): void;
138+
setListWebMcpTools(): void;
137139
}
138140

139141
/**
@@ -199,6 +201,7 @@ export type Context = Readonly<{
199201
getExtensionServiceWorkerId(
200202
extensionServiceWorker: ExtensionServiceWorker,
201203
): string | undefined;
204+
getWebMcpTools(page: ContextPage): WebMcpTool[];
202205
}>;
203206

204207
export type ContextPage = Readonly<{

src/tools/tools.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as scriptTools from './script.js';
2222
import * as slimTools from './slim/tools.js';
2323
import * as snapshotTools from './snapshot.js';
2424
import type {ToolDefinition} from './ToolDefinition.js';
25+
import * as webmcpTools from './webmcp.js';
2526

2627
export const createTools = (args: ParsedArguments) => {
2728
const rawTools = args.slim
@@ -41,6 +42,7 @@ export const createTools = (args: ParsedArguments) => {
4142
...Object.values(screenshotTools),
4243
...Object.values(scriptTools),
4344
...Object.values(snapshotTools),
45+
...Object.values(webmcpTools),
4446
];
4547

4648
const tools = [];

src/tools/webmcp.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {ToolCategory} from './categories.js';
8+
import {definePageTool} from './ToolDefinition.js';
9+
10+
export const listWebMcpTools = definePageTool({
11+
name: 'list_webmcp_tools',
12+
description: `Lists all WebMCP tools the page exposes.`,
13+
annotations: {
14+
category: ToolCategory.IN_PAGE,
15+
readOnlyHint: true,
16+
conditions: ['experimentalWebmcp'],
17+
},
18+
schema: {},
19+
handler: async (_request, response, _context) => {
20+
response.setListWebMcpTools();
21+
},
22+
});

0 commit comments

Comments
 (0)