diff --git a/src/McpContext.ts b/src/McpContext.ts index 32b7413e6..b4bb3dfb6 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -7,6 +7,8 @@ import fs from 'node:fs/promises'; import path from 'node:path'; +import type {WebMCPTool} from 'puppeteer-core'; + import type {TargetUniverse} from './DevtoolsUtils.js'; import {UniverseManager} from './DevtoolsUtils.js'; import {McpPage} from './McpPage.js'; @@ -222,6 +224,10 @@ export class McpContext implements Context { ); } + getWebMcpTools(page: McpPage): WebMCPTool[] { + return page.pptrPage.webmcp.tools(); + } + getDevToolsUniverse(page: McpPage): TargetUniverse | null { return this.#devtoolsUniverseManager.get(page.pptrPage); } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 77898b5df..6cc378851 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -4,6 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type {WebMCPTool} from 'puppeteer-core'; + import type {ParsedArguments} from './bin/chrome-devtools-mcp-cli-options.js'; import {ConsoleFormatter} from './formatters/ConsoleFormatter.js'; import {IssueFormatter} from './formatters/IssueFormatter.js'; @@ -181,6 +183,7 @@ export class McpResponse implements Response { }; #listExtensions?: boolean; #listInPageTools?: boolean; + #listWebMcpTools?: boolean; #devToolsData?: DevToolsData; #tabId?: string; #args: ParsedArguments; @@ -227,6 +230,12 @@ export class McpResponse implements Response { } } + setListWebMcpTools(): void { + if (this.#args.experimentalWebmcp) { + this.#listWebMcpTools = true; + } + } + setIncludeNetworkRequests( value: boolean, options?: PaginationOptions & { @@ -482,6 +491,12 @@ export class McpResponse implements Response { page.inPageTools = inPageTools; } + let webmcpTools: WebMCPTool[] | undefined; + if (this.#listWebMcpTools) { + const page = this.#page ?? context.getSelectedMcpPage(); + webmcpTools = context.getWebMcpTools(page); + } + let consoleMessages: Array | undefined; if (this.#consoleDataOptions?.include) { if (!this.#page) { @@ -585,6 +600,7 @@ export class McpResponse implements Response { extensions, lighthouseResult: this.#attachedLighthouseResult, inPageTools, + webmcpTools, }); } @@ -602,6 +618,7 @@ export class McpResponse implements Response { extensions?: InstalledExtension[]; lighthouseResult?: LighthouseData; inPageTools?: ToolGroup; + webmcpTools?: WebMCPTool[]; }, ): {content: Array; structuredContent: object} { const structuredContent: { @@ -617,6 +634,7 @@ export class McpResponse implements Response { lighthouseResult?: object; extensions?: object[]; inPageTools?: object; + webmcpTools?: object[]; message?: string; networkConditions?: string; navigationTimeout?: number; @@ -874,6 +892,23 @@ Call ${handleDialog.name} to handle it before continuing.`); } } + if (this.#listWebMcpTools && data.webmcpTools) { + structuredContent.webmcpTools = data.webmcpTools; + response.push('## WebMCP tools'); + if (data.webmcpTools.length === 0) { + response.push('No WebMCP tools available.'); + } else { + const webmcpToolsMessage = data.webmcpTools + .map(tool => { + return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify( + tool.inputSchema, + )}, annotations=${JSON.stringify(tool.annotations)}`; + }) + .join('\n'); + response.push(webmcpToolsMessage); + } + } + if (this.#networkRequestsOptions?.include && data.networkRequests) { const requests = data.networkRequests; diff --git a/src/bin/chrome-devtools-mcp-cli-options.ts b/src/bin/chrome-devtools-mcp-cli-options.ts index 3cfbacac0..94034bf79 100644 --- a/src/bin/chrome-devtools-mcp-cli-options.ts +++ b/src/bin/chrome-devtools-mcp-cli-options.ts @@ -185,6 +185,11 @@ export const cliOptions = { describe: 'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.', }, + experimentalWebmcp: { + type: 'boolean', + describe: 'Set to true to enable debugging WebMCP tools.', + hidden: true, + }, chromeArg: { type: 'array', describe: diff --git a/src/bin/chrome-devtools.ts b/src/bin/chrome-devtools.ts index e3aab5f45..979533e84 100644 --- a/src/bin/chrome-devtools.ts +++ b/src/bin/chrome-devtools.ts @@ -51,6 +51,7 @@ delete startCliOptions.viewport; // tools, they need to be enabled during CLI generation. delete startCliOptions.experimentalPageIdRouting; delete startCliOptions.experimentalVision; +delete startCliOptions.experimentalWebmcp; delete startCliOptions.experimentalInteropTools; delete startCliOptions.experimentalScreencast; delete startCliOptions.categoryEmulation; diff --git a/src/index.ts b/src/index.ts index 362f2348a..65e3a6c76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -164,6 +164,12 @@ export async function createMcpServer( ) { return; } + if ( + tool.annotations.conditions?.includes('experimentalWebmcp') && + !serverArgs.experimentalWebmcp + ) { + return; + } const schema = 'pageScoped' in tool && tool.pageScoped && diff --git a/src/telemetry/tool_call_metrics.json b/src/telemetry/tool_call_metrics.json index 496045ff6..b6a85e697 100644 --- a/src/telemetry/tool_call_metrics.json +++ b/src/telemetry/tool_call_metrics.json @@ -539,5 +539,9 @@ "argType": "number" } ] + }, + { + "name": "list_webmcp_tools", + "args": [] } ] diff --git a/src/tools/ToolDefinition.ts b/src/tools/ToolDefinition.ts index 8001e18c7..4a229bd09 100644 --- a/src/tools/ToolDefinition.ts +++ b/src/tools/ToolDefinition.ts @@ -134,6 +134,7 @@ export interface Response { setListExtensions(): void; attachLighthouseResult(result: LighthouseData): void; setListInPageTools(): void; + setListWebMcpTools(): void; } /** diff --git a/src/tools/pages.ts b/src/tools/pages.ts index fa5a40857..33a8977af 100644 --- a/src/tools/pages.ts +++ b/src/tools/pages.ts @@ -28,6 +28,7 @@ export const listPages = defineTool(args => { handler: async (_request, response) => { response.setIncludePages(true); response.setListInPageTools(); + response.setListWebMcpTools(); }, }; }); diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 3c74115c3..b3477b906 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -22,6 +22,7 @@ import * as scriptTools from './script.js'; import * as slimTools from './slim/tools.js'; import * as snapshotTools from './snapshot.js'; import type {ToolDefinition} from './ToolDefinition.js'; +import * as webmcpTools from './webmcp.js'; export const createTools = (args: ParsedArguments) => { const rawTools = args.slim @@ -41,6 +42,7 @@ export const createTools = (args: ParsedArguments) => { ...Object.values(screenshotTools), ...Object.values(scriptTools), ...Object.values(snapshotTools), + ...Object.values(webmcpTools), ]; const tools = []; diff --git a/src/tools/webmcp.ts b/src/tools/webmcp.ts new file mode 100644 index 000000000..e52a5ac60 --- /dev/null +++ b/src/tools/webmcp.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {ToolCategory} from './categories.js'; +import {definePageTool} from './ToolDefinition.js'; + +export const listWebMcpTools = definePageTool({ + name: 'list_webmcp_tools', + description: `Lists all WebMCP tools the page exposes.`, + annotations: { + category: ToolCategory.DEBUGGING, + readOnlyHint: true, + conditions: ['experimentalWebmcp'], + }, + schema: {}, + handler: async (_request, response, _context) => { + response.setListWebMcpTools(); + }, +}); diff --git a/tests/tools/webmcp.test.ts b/tests/tools/webmcp.test.ts new file mode 100644 index 000000000..e7ec45f48 --- /dev/null +++ b/tests/tools/webmcp.test.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; +import {describe, it} from 'node:test'; + +import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js'; +import {listWebMcpTools} from '../../src/tools/webmcp.js'; +import {getTextContent, html, withMcpContext} from '../utils.js'; + +describe('webmcp', () => { + it('list webmcp tools', async () => { + await withMcpContext( + async (response, context) => { + const page = context.getSelectedMcpPage().pptrPage; + await page.setContent( + html`
`, + ); + + await listWebMcpTools.handler( + {params: {}, page: context.getSelectedMcpPage()}, + response, + context, + ); + + const formattedResponse = await response.handle('test', context); + const textContent = getTextContent(formattedResponse.content[0]); + assert.match( + textContent, + /name="test_tool", description="A test tool"/, + ); + }, + {}, + {experimentalWebmcp: true} as ParsedArguments, + ); + }); + + it('does not list webmcp tools if not enabled', async () => { + await withMcpContext(async (response, context) => { + const page = context.getSelectedMcpPage().pptrPage; + await page.setContent( + html`
`, + ); + + await listWebMcpTools.handler( + {params: {}, page: context.getSelectedMcpPage()}, + response, + context, + ); + + const formattedResponse = await response.handle('test', context); + const textContent = getTextContent(formattedResponse.content[0]); + assert.ok(!textContent.includes('name="test_tool"')); + }, {}); + }); +});