feat(webmcp): Add experimental tool to execute WebMCP tool (#1873)

<img width="1840" height="1191" alt="Screenshot 2026-04-16 at 11 00 37"
src="https://github.com/user-attachments/assets/6e69dfef-8903-4cbc-ba59-191edd76c0c8"
/>

@OrKoN
This commit is contained in:
François Beaufort
2026-04-16 11:33:32 +02:00
committed by GitHub
parent f97b573d70
commit 0aff266111
6 changed files with 196 additions and 24 deletions
+4
View File
@@ -584,6 +584,10 @@ The Chrome DevTools MCP server supports the following configuration option:
Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
- **Type:** boolean
- **`--experimentalWebmcp`/ `--experimental-webmcp`**
Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`
- **Type:** boolean
- **`--chromeArg`/ `--chrome-arg`**
Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
- **Type:** array
+2 -2
View File
@@ -187,8 +187,8 @@ export const cliOptions = {
},
experimentalWebmcp: {
type: 'boolean',
describe: 'Set to true to enable debugging WebMCP tools.',
hidden: true,
describe:
'Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`',
},
chromeArg: {
type: 'array',
+13
View File
@@ -543,5 +543,18 @@
{
"name": "list_webmcp_tools",
"args": []
},
{
"name": "execute_webmcp_tool",
"args": [
{
"name": "tool_name_length",
"argType": "number"
},
{
"name": "input_length",
"argType": "number"
}
]
}
]
+48
View File
@@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {zod} from '../third_party/index.js';
import {ToolCategory} from './categories.js';
import {definePageTool} from './ToolDefinition.js';
@@ -20,3 +22,49 @@ export const listWebMcpTools = definePageTool({
response.setListWebMcpTools();
},
});
export const executeWebMcpTool = definePageTool({
name: 'execute_webmcp_tool',
description: `Executes a WebMCP tool exposed by the page.`,
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
conditions: ['experimentalWebmcp'],
},
schema: {
toolName: zod.string().describe('The name of the WebMCP tool to execute'),
input: zod
.string()
.optional()
.describe('The JSON-stringified parameters to pass to the WebMCP tool'),
},
handler: async (request, response) => {
const toolName = request.params.toolName;
let input: Record<string, unknown> = {};
if (request.params.input) {
try {
const parsed = JSON.parse(request.params.input);
if (typeof parsed === 'object' && parsed !== null) {
input = parsed;
} else {
throw new Error('Parsed input is not an object');
}
} catch (e) {
const errorMessage = e instanceof Error ? e.message : String(e);
throw new Error(`Failed to parse input as JSON: ${errorMessage}`);
}
}
const tools = request.page.pptrPage.webmcp.tools();
const tool = tools.find(t => t.name === toolName);
if (!tool) {
throw new Error(`Tool ${toolName} not found`);
}
const {status, output, errorText} = await tool.execute(input);
response.appendResponseLine(
JSON.stringify({status, output, errorText}, null, 2),
);
},
});
+15
View File
@@ -162,4 +162,19 @@ describe('e2e', () => {
['--experimental-interop-tools'],
);
});
it('has experimental webmcp', async () => {
await withClient(
async client => {
const {tools} = await client.listTools();
const listWebMcpTools = tools.find(t => t.name === 'list_webmcp_tools');
const executeWebMcpTool = tools.find(
t => t.name === 'execute_webmcp_tool',
);
assert.ok(listWebMcpTools);
assert.ok(executeWebMcpTool);
},
['--experimental-webmcp'],
);
});
});
+114 -22
View File
@@ -7,34 +7,126 @@
import assert from 'node:assert';
import {describe, it} from 'node:test';
import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
import type {McpPage} from '../../src/McpPage.js';
import {listPages, navigatePage, selectPage} from '../../src/tools/pages.js';
import {withMcpContext} from '../utils.js';
import {executeWebMcpTool} from '../../src/tools/webmcp.js';
import {html, 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,
describe('list_webmcp_tools', () => {
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);
});
});
});
describe('execute_webmcp_tool', () => {
async function setupWebMcpTool(page: McpPage) {
await page.pptrPage.setContent(
html`<form
toolname="test_tool"
tooldescription="A test tool"
toolautosubmit
></form
><script>
document.querySelector('form').onsubmit = event => {
event.preventDefault();
event.respondWith('hello');
};
</script>`,
);
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);
});
});
// TODO: Remove `.skip` once Chrome 149 reaches stable channel.
it.skip('executes a tool successfully', async () => {
await withMcpContext(
async (response, context) => {
const page = context.getSelectedMcpPage();
await setupWebMcpTool(page);
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);
await executeWebMcpTool.handler(
{params: {toolName: 'test_tool', input: JSON.stringify({})}, page},
response,
context,
);
assert.strictEqual(
response.responseLines[0],
JSON.stringify({status: 'Completed', output: 'hello'}, null, 2),
);
},
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
{experimentalWebmcp: true} as ParsedArguments,
);
});
it('throws if tool is not found', async () => {
await withMcpContext(
async (response, context) => {
await assert.rejects(
async () => {
await executeWebMcpTool.handler(
{
params: {toolName: 'missing-tool', input: JSON.stringify({})},
page: context.getSelectedMcpPage(),
},
response,
context,
);
},
{message: /Tool missing-tool not found/},
);
},
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
{experimentalWebmcp: true} as ParsedArguments,
);
});
it('throws if input is invalid', async () => {
await withMcpContext(
async (response, context) => {
await assert.rejects(
async () => {
const page = context.getSelectedMcpPage();
await setupWebMcpTool(page);
await executeWebMcpTool.handler(
{params: {toolName: 'test_tool', input: 'invalid'}, page},
response,
context,
);
},
{
message:
/Failed to parse input as JSON: Unexpected token 'i', "invalid" is not valid JSON/,
},
);
},
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
{experimentalWebmcp: true} as ParsedArguments,
);
});
});
});