Skip to content

Commit 0aff266

Browse files
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
1 parent f97b573 commit 0aff266

File tree

6 files changed

+194
-22
lines changed

6 files changed

+194
-22
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,10 @@ The Chrome DevTools MCP server supports the following configuration option:
584584
Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.
585585
- **Type:** boolean
586586

587+
- **`--experimentalWebmcp`/ `--experimental-webmcp`**
588+
Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`
589+
- **Type:** boolean
590+
587591
- **`--chromeArg`/ `--chrome-arg`**
588592
Additional arguments for Chrome. Only applies when Chrome is launched by chrome-devtools-mcp.
589593
- **Type:** array

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,8 @@ export const cliOptions = {
187187
},
188188
experimentalWebmcp: {
189189
type: 'boolean',
190-
describe: 'Set to true to enable debugging WebMCP tools.',
191-
hidden: true,
190+
describe:
191+
'Set to true to enable debugging WebMCP tools. Requires Chrome 149+ with the following flags: `--enable-features=WebMCPTesting,DevToolsWebMCPSupport`',
192192
},
193193
chromeArg: {
194194
type: 'array',

src/telemetry/tool_call_metrics.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,5 +543,18 @@
543543
{
544544
"name": "list_webmcp_tools",
545545
"args": []
546+
},
547+
{
548+
"name": "execute_webmcp_tool",
549+
"args": [
550+
{
551+
"name": "tool_name_length",
552+
"argType": "number"
553+
},
554+
{
555+
"name": "input_length",
556+
"argType": "number"
557+
}
558+
]
546559
}
547560
]

src/tools/webmcp.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import {zod} from '../third_party/index.js';
8+
79
import {ToolCategory} from './categories.js';
810
import {definePageTool} from './ToolDefinition.js';
911

@@ -20,3 +22,49 @@ export const listWebMcpTools = definePageTool({
2022
response.setListWebMcpTools();
2123
},
2224
});
25+
26+
export const executeWebMcpTool = definePageTool({
27+
name: 'execute_webmcp_tool',
28+
description: `Executes a WebMCP tool exposed by the page.`,
29+
annotations: {
30+
category: ToolCategory.DEBUGGING,
31+
readOnlyHint: false,
32+
conditions: ['experimentalWebmcp'],
33+
},
34+
schema: {
35+
toolName: zod.string().describe('The name of the WebMCP tool to execute'),
36+
input: zod
37+
.string()
38+
.optional()
39+
.describe('The JSON-stringified parameters to pass to the WebMCP tool'),
40+
},
41+
handler: async (request, response) => {
42+
const toolName = request.params.toolName;
43+
44+
let input: Record<string, unknown> = {};
45+
if (request.params.input) {
46+
try {
47+
const parsed = JSON.parse(request.params.input);
48+
if (typeof parsed === 'object' && parsed !== null) {
49+
input = parsed;
50+
} else {
51+
throw new Error('Parsed input is not an object');
52+
}
53+
} catch (e) {
54+
const errorMessage = e instanceof Error ? e.message : String(e);
55+
throw new Error(`Failed to parse input as JSON: ${errorMessage}`);
56+
}
57+
}
58+
59+
const tools = request.page.pptrPage.webmcp.tools();
60+
const tool = tools.find(t => t.name === toolName);
61+
if (!tool) {
62+
throw new Error(`Tool ${toolName} not found`);
63+
}
64+
65+
const {status, output, errorText} = await tool.execute(input);
66+
response.appendResponseLine(
67+
JSON.stringify({status, output, errorText}, null, 2),
68+
);
69+
},
70+
});

tests/index.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,19 @@ describe('e2e', () => {
162162
['--experimental-interop-tools'],
163163
);
164164
});
165+
166+
it('has experimental webmcp', async () => {
167+
await withClient(
168+
async client => {
169+
const {tools} = await client.listTools();
170+
const listWebMcpTools = tools.find(t => t.name === 'list_webmcp_tools');
171+
const executeWebMcpTool = tools.find(
172+
t => t.name === 'execute_webmcp_tool',
173+
);
174+
assert.ok(listWebMcpTools);
175+
assert.ok(executeWebMcpTool);
176+
},
177+
['--experimental-webmcp'],
178+
);
179+
});
165180
});

tests/tools/webmcp.test.ts

Lines changed: 112 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,126 @@
77
import assert from 'node:assert';
88
import {describe, it} from 'node:test';
99

10+
import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
11+
import type {McpPage} from '../../src/McpPage.js';
1012
import {listPages, navigatePage, selectPage} from '../../src/tools/pages.js';
11-
import {withMcpContext} from '../utils.js';
13+
import {executeWebMcpTool} from '../../src/tools/webmcp.js';
14+
import {html, withMcpContext} from '../utils.js';
1215

1316
describe('webmcp', () => {
14-
it('list webmcp tools in navigate_page response', async () => {
15-
await withMcpContext(async (response, context) => {
16-
await navigatePage.handler(
17-
{params: {url: 'about:blank'}, page: context.getSelectedMcpPage()},
18-
response,
19-
context,
20-
);
21-
assert.ok(response.listWebMcpTools);
17+
describe('list_webmcp_tools', () => {
18+
it('list webmcp tools in navigate_page response', async () => {
19+
await withMcpContext(async (response, context) => {
20+
await navigatePage.handler(
21+
{params: {url: 'about:blank'}, page: context.getSelectedMcpPage()},
22+
response,
23+
context,
24+
);
25+
assert.ok(response.listWebMcpTools);
26+
});
27+
});
28+
29+
it('list webmcp tools in list_pages response', async () => {
30+
await withMcpContext(async (response, context) => {
31+
await listPages().handler({params: {}}, response, context);
32+
assert.ok(response.listWebMcpTools);
33+
});
2234
});
23-
});
2435

25-
it('list webmcp tools in list_pages response', async () => {
26-
await withMcpContext(async (response, context) => {
27-
await listPages().handler({params: {}}, response, context);
28-
assert.ok(response.listWebMcpTools);
36+
it('list webmcp tools in select_page response', async () => {
37+
await withMcpContext(async (response, context) => {
38+
const pageId =
39+
context.getPageId(context.getSelectedMcpPage().pptrPage) ?? 1;
40+
await selectPage.handler({params: {pageId}}, response, context);
41+
assert.ok(response.listWebMcpTools);
42+
});
2943
});
3044
});
3145

32-
it('list webmcp tools in select_page response', async () => {
33-
await withMcpContext(async (response, context) => {
34-
const pageId =
35-
context.getPageId(context.getSelectedMcpPage().pptrPage) ?? 1;
36-
await selectPage.handler({params: {pageId}}, response, context);
37-
assert.ok(response.listWebMcpTools);
46+
describe('execute_webmcp_tool', () => {
47+
async function setupWebMcpTool(page: McpPage) {
48+
await page.pptrPage.setContent(
49+
html`<form
50+
toolname="test_tool"
51+
tooldescription="A test tool"
52+
toolautosubmit
53+
></form
54+
><script>
55+
document.querySelector('form').onsubmit = event => {
56+
event.preventDefault();
57+
event.respondWith('hello');
58+
};
59+
</script>`,
60+
);
61+
}
62+
63+
// TODO: Remove `.skip` once Chrome 149 reaches stable channel.
64+
it.skip('executes a tool successfully', async () => {
65+
await withMcpContext(
66+
async (response, context) => {
67+
const page = context.getSelectedMcpPage();
68+
await setupWebMcpTool(page);
69+
70+
await executeWebMcpTool.handler(
71+
{params: {toolName: 'test_tool', input: JSON.stringify({})}, page},
72+
response,
73+
context,
74+
);
75+
assert.strictEqual(
76+
response.responseLines[0],
77+
JSON.stringify({status: 'Completed', output: 'hello'}, null, 2),
78+
);
79+
},
80+
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
81+
{experimentalWebmcp: true} as ParsedArguments,
82+
);
83+
});
84+
85+
it('throws if tool is not found', async () => {
86+
await withMcpContext(
87+
async (response, context) => {
88+
await assert.rejects(
89+
async () => {
90+
await executeWebMcpTool.handler(
91+
{
92+
params: {toolName: 'missing-tool', input: JSON.stringify({})},
93+
page: context.getSelectedMcpPage(),
94+
},
95+
response,
96+
context,
97+
);
98+
},
99+
{message: /Tool missing-tool not found/},
100+
);
101+
},
102+
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
103+
{experimentalWebmcp: true} as ParsedArguments,
104+
);
105+
});
106+
107+
it('throws if input is invalid', async () => {
108+
await withMcpContext(
109+
async (response, context) => {
110+
await assert.rejects(
111+
async () => {
112+
const page = context.getSelectedMcpPage();
113+
await setupWebMcpTool(page);
114+
115+
await executeWebMcpTool.handler(
116+
{params: {toolName: 'test_tool', input: 'invalid'}, page},
117+
response,
118+
context,
119+
);
120+
},
121+
{
122+
message:
123+
/Failed to parse input as JSON: Unexpected token 'i', "invalid" is not valid JSON/,
124+
},
125+
);
126+
},
127+
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
128+
{experimentalWebmcp: true} as ParsedArguments,
129+
);
38130
});
39131
});
40132
});

0 commit comments

Comments
 (0)