feat(webmcp): Add experimental tool to list WebMCP tools page exposes (#1845)

<img width="1305" height="735" alt="image"
src="https://github.com/user-attachments/assets/ea154d81-c1cf-4758-98c2-7dce25d6ad8b"
/>


```
npx @modelcontextprotocol/inspector node build/src/bin/chrome-devtools-mcp.js --channel=canary --experimentalWebmcp --chrome-arg='--enable-features=WebMCPTesting,DevToolsWebMCPSupport'
```

<img width="1728" height="1117" alt="image"
src="https://github.com/user-attachments/assets/133ac10e-7bd3-4eaa-b1e9-25b1283e5761"
/>

cc @OrKoN
This commit is contained in:
François Beaufort
2026-04-16 09:42:32 +02:00
committed by GitHub
parent e308e4a5b8
commit f97b573d70
14 changed files with 373 additions and 1 deletions
+5
View File
@@ -9,6 +9,7 @@ import type {
ElementHandle,
Page,
Viewport,
WebMCPTool,
} from './third_party/index.js';
import type {ToolGroup, ToolDefinition} from './tools/inPage.js';
import {takeSnapshot} from './tools/snapshot.js';
@@ -78,6 +79,10 @@ export class McpPage implements ContextPage {
return this.inPageTools;
}
getWebMcpTools(): WebMCPTool[] {
return this.pptrPage.webmcp.tools();
}
get networkConditions(): string | null {
return this.emulationSettings.networkConditions ?? null;
}
+44
View File
@@ -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;
@@ -232,6 +235,10 @@ export class McpResponse implements Response {
}
}
setListWebMcpTools(): void {
this.#listWebMcpTools = true;
}
setIncludeNetworkRequests(
value: boolean,
options?: PaginationOptions & {
@@ -374,6 +381,10 @@ export class McpResponse implements Response {
return this.#snapshotParams;
}
get listWebMcpTools(): boolean | undefined {
return this.#listWebMcpTools;
}
async handle(
toolName: string,
context: McpContext,
@@ -490,6 +501,12 @@ export class McpResponse implements Response {
page.inPageTools = inPageTools;
}
let webmcpTools: WebMCPTool[] | undefined;
if (this.#listWebMcpTools && this.#args.experimentalWebmcp) {
const page = this.#page ?? context.getSelectedMcpPage();
webmcpTools = page.getWebMcpTools();
}
let consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
if (this.#consoleDataOptions?.include) {
if (!this.#page) {
@@ -595,6 +612,7 @@ export class McpResponse implements Response {
extensions,
lighthouseResult: this.#attachedLighthouseResult,
inPageTools,
webmcpTools,
});
}
@@ -612,6 +630,7 @@ export class McpResponse implements Response {
extensions?: InstalledExtension[];
lighthouseResult?: LighthouseData;
inPageTools?: ToolGroup<ToolDefinition>;
webmcpTools?: WebMCPTool[];
},
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
const structuredContent: {
@@ -627,6 +646,7 @@ export class McpResponse implements Response {
lighthouseResult?: object;
extensions?: object[];
inPageTools?: object;
webmcpTools?: object[];
message?: string;
networkConditions?: string;
navigationTimeout?: number;
@@ -884,6 +904,30 @@ Call ${handleDialog.name} to handle it before continuing.`);
}
}
if (this.#listWebMcpTools && data.webmcpTools) {
structuredContent.webmcpTools = data.webmcpTools.map(
({name, description, inputSchema, annotations}) => ({
name,
description,
inputSchema,
annotations,
}),
);
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;
@@ -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:
+1
View File
@@ -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;
+6
View File
@@ -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 &&
+4
View File
@@ -539,5 +539,9 @@
"argType": "number"
}
]
},
{
"name": "list_webmcp_tools",
"args": []
}
]
+1
View File
@@ -134,6 +134,7 @@ export interface Response {
setListExtensions(): void;
attachLighthouseResult(result: LighthouseData): void;
setListInPageTools(): void;
setListWebMcpTools(): void;
}
export type SupportedExtensions =
+3
View File
@@ -28,6 +28,7 @@ export const listPages = defineTool(args => {
handler: async (_request, response) => {
response.setIncludePages(true);
response.setListInPageTools();
response.setListWebMcpTools();
},
};
});
@@ -55,6 +56,7 @@ export const selectPage = defineTool({
context.selectPage(page);
response.setIncludePages(true);
response.setListInPageTools();
response.setListWebMcpTools();
if (request.params.bringToFront) {
await page.pptrPage.bringToFront();
}
@@ -280,6 +282,7 @@ export const navigatePage = definePageTool({
response.setIncludePages(true);
response.setListInPageTools();
response.setListWebMcpTools();
},
});
+2
View File
@@ -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 = [];
+22
View File
@@ -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();
},
});
+110
View File
@@ -1281,3 +1281,113 @@ exports[`lighthouse > includes lighthouse report paths 2`] = `
}
}
`;
exports[`webmcp > includes webmcp tools in list_pages response 1`] = `
## Pages
1: about:blank [selected]
## WebMCP tools
name="test_tool", description="A test tool", inputSchema={}, annotations=undefined
`;
exports[`webmcp > includes webmcp tools in list_pages response 2`] = `
{
"pages": [
{
"id": 1,
"url": "about:blank",
"selected": true
}
],
"webmcpTools": [
{
"name": "test_tool",
"description": "A test tool",
"inputSchema": {}
}
]
}
`;
exports[`webmcp > includes webmcp tools in navigate_page response 1`] = `
Successfully navigated to about:blank.
## Pages
1: about:blank [selected]
## WebMCP tools
name="test_tool", description="A test tool", inputSchema={}, annotations=undefined
`;
exports[`webmcp > includes webmcp tools in navigate_page response 2`] = `
{
"message": "Successfully navigated to about:blank.",
"pages": [
{
"id": 1,
"url": "about:blank",
"selected": true
}
],
"webmcpTools": [
{
"name": "test_tool",
"description": "A test tool",
"inputSchema": {}
}
]
}
`;
exports[`webmcp > includes webmcp tools in select_page response 1`] = `
## Pages
1: about:blank [selected]
## WebMCP tools
name="test_tool", description="A test tool", inputSchema={}, annotations=undefined
`;
exports[`webmcp > includes webmcp tools in select_page response 2`] = `
{
"pages": [
{
"id": 1,
"url": "about:blank",
"selected": true
}
],
"webmcpTools": [
{
"name": "test_tool",
"description": "A test tool",
"inputSchema": {}
}
]
}
`;
exports[`webmcp > list no webmcp tools if experimentalWebmcp is false 1`] = `
Successfully navigated to about:blank.
## Pages
1: about:blank [selected]
`;
exports[`webmcp > list no webmcp tools if experimentalWebmcp is false 2`] = `
{
"message": "Successfully navigated to about:blank.",
"pages": [
{
"id": 1,
"url": "about:blank",
"selected": true
}
]
}
`;
exports[`webmcp > list no webmcp tools if there are none 1`] = `
## WebMCP tools
No WebMCP tools available.
`;
exports[`webmcp > list no webmcp tools if there are none 2`] = `
{
"webmcpTools": []
}
`;
+127
View File
@@ -1455,3 +1455,130 @@ describe('replaceHtmlElementsWithUids', () => {
}
});
});
describe('webmcp', () => {
async function testIncludesWebmcpTools(
t: it.TestContext,
parseArguments: ParsedArguments,
handlerAction: (
response: McpResponse,
context: McpContext,
) => Promise<void>,
toolName: string,
) {
await withMcpContext(
async (response, context) => {
response.setListWebMcpTools();
await handlerAction(response, context);
const page = context.getSelectedMcpPage().pptrPage;
await page.setContent(
html`<form
toolname="test_tool"
tooldescription="A test tool"
></form>`,
);
const {content, structuredContent} = await response.handle(
toolName,
context,
);
assert.ok(getTextContent(content[0]));
t.assert.snapshot?.(getTextContent(content[0]));
t.assert.snapshot?.(
JSON.stringify(
stabilizeStructuredContent(structuredContent),
null,
2,
),
);
},
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
parseArguments,
);
}
it('includes webmcp tools in list_pages response', async t => {
await testIncludesWebmcpTools(
t,
{experimentalWebmcp: true} as ParsedArguments,
async (response, context) => {
await listPages().handler({params: {}}, response, context);
},
'list_pages',
);
});
it('includes webmcp tools in select_page response', async t => {
await testIncludesWebmcpTools(
t,
{experimentalWebmcp: true} as ParsedArguments,
async (response, context) => {
const pageId =
context.getPageId(context.getSelectedMcpPage().pptrPage) ?? 1;
await selectPage.handler({params: {pageId}}, response, context);
},
'select_page',
);
});
it('includes webmcp tools in navigate_page response', async t => {
await testIncludesWebmcpTools(
t,
{experimentalWebmcp: true} as ParsedArguments,
async (response, context) => {
await navigatePage.handler(
{
params: {type: 'url', url: 'about:blank'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
'navigate_page',
);
});
it('list no webmcp tools if there are none', async t => {
await withMcpContext(
async (response, context) => {
response.setListWebMcpTools();
const {content, structuredContent} = await response.handle(
'test',
context,
);
assert.ok(getTextContent(content[0]));
t.assert.snapshot?.(getTextContent(content[0]));
t.assert.snapshot?.(
JSON.stringify(
stabilizeStructuredContent(structuredContent),
null,
2,
),
);
},
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
{experimentalWebmcp: true} as ParsedArguments,
);
});
it('list no webmcp tools if experimentalWebmcp is false', async t => {
await testIncludesWebmcpTools(
t,
{experimentalWebmcp: false} as ParsedArguments,
async (response, context) => {
await navigatePage.handler(
{
params: {type: 'url', url: 'about:blank'},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
'navigate_page',
);
});
});
+40
View File
@@ -0,0 +1,40 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import {describe, it} from 'node:test';
import {listPages, navigatePage, selectPage} from '../../src/tools/pages.js';
import {withMcpContext} from '../utils.js';
describe('webmcp', () => {
it('list webmcp tools in navigate_page response', async () => {
await withMcpContext(async (response, context) => {
await navigatePage.handler(
{params: {url: 'about:blank'}, page: context.getSelectedMcpPage()},
response,
context,
);
assert.ok(response.listWebMcpTools);
});
});
it('list webmcp tools in list_pages response', async () => {
await withMcpContext(async (response, context) => {
await listPages().handler({params: {}}, response, context);
assert.ok(response.listWebMcpTools);
});
});
it('list webmcp tools in select_page response', async () => {
await withMcpContext(async (response, context) => {
const pageId =
context.getPageId(context.getSelectedMcpPage().pptrPage) ?? 1;
await selectPage.handler({params: {pageId}}, response, context);
assert.ok(response.listWebMcpTools);
});
});
});
+3 -1
View File
@@ -64,6 +64,7 @@ export async function withBrowser(
debug?: boolean;
autoOpenDevTools?: boolean;
executablePath?: string;
args?: string[];
} = {},
) {
const launchOptions: LaunchOptions = {
@@ -74,7 +75,7 @@ export async function withBrowser(
devtools: options.autoOpenDevTools ?? false,
pipe: true,
handleDevToolsAsPage: true,
args: ['--screen-info={3840x2160}'],
args: [...(options.args || []), '--screen-info={3840x2160}'],
enableExtensions: true,
};
const key = JSON.stringify(launchOptions);
@@ -104,6 +105,7 @@ export async function withMcpContext(
autoOpenDevTools?: boolean;
performanceCrux?: boolean;
executablePath?: string;
args?: string[];
} = {},
args: ParsedArguments = {} as ParsedArguments,
) {