Skip to content

Commit 199a1d0

Browse files
feat(webmcp): Add experimental tool to execut WebMCP tool
1 parent 2969ce2 commit 199a1d0

3 files changed

Lines changed: 176 additions & 20 deletions

File tree

src/tools/webmcp.ts

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

7+
import {
8+
zod,
9+
} from '../third_party/index.js';
10+
711
import {ToolCategory} from './categories.js';
812
import {definePageTool} from './ToolDefinition.js';
913

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

tests/index.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,17 @@ 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(t => t.name === 'execute_webmcp_tool');
172+
assert.ok(listWebMcpTools);
173+
assert.ok(executeWebMcpTool);
174+
},
175+
['--experimental-webmcp'],
176+
);
177+
});
165178
});

tests/tools/webmcp.test.ts

Lines changed: 115 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,128 @@ import assert from 'node:assert';
88
import {describe, it} from 'node:test';
99

1010
import {listPages, navigatePage, selectPage} from '../../src/tools/pages.js';
11-
import {withMcpContext} from '../utils.js';
11+
import {html, withMcpContext} from '../utils.js';
12+
import {executeWebMcpTool} from '../../src/tools/webmcp.js';
13+
import {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
14+
import {McpPage} from '../../src/McpPage.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+
it('executes a tool successfully', async () => {
64+
await withMcpContext(
65+
async (response, context) => {
66+
const page = context.getSelectedMcpPage();
67+
await setupWebMcpTool(page);
68+
69+
await executeWebMcpTool.handler(
70+
{params: {toolName: 'test_tool', input: JSON.stringify({})}, page},
71+
response,
72+
context,
73+
);
74+
assert.strictEqual(
75+
response.responseLines[0],
76+
JSON.stringify({status: 'Completed', output: 'hello'}, null, 2),
77+
);
78+
},
79+
{
80+
args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport'],
81+
executablePath:
82+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
83+
},
84+
{experimentalWebmcp: true} as ParsedArguments,
85+
);
86+
});
87+
88+
it('throws if tool is not found', async () => {
89+
await withMcpContext(
90+
async (response, context) => {
91+
await assert.rejects(
92+
async () => {
93+
await executeWebMcpTool.handler(
94+
{
95+
params: {toolName: 'missing-tool', input: JSON.stringify({})},
96+
page: context.getSelectedMcpPage(),
97+
},
98+
response,
99+
context,
100+
);
101+
},
102+
{message: /Tool missing-tool not found/},
103+
);
104+
},
105+
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
106+
{experimentalWebmcp: true} as ParsedArguments,
107+
);
108+
});
109+
110+
it('throws if input is invalid', async () => {
111+
await withMcpContext(
112+
async (response, context) => {
113+
await assert.rejects(
114+
async () => {
115+
const page = context.getSelectedMcpPage();
116+
await setupWebMcpTool(page);
117+
118+
await executeWebMcpTool.handler(
119+
{params: {toolName: 'test_tool', input: 'invalid'}, page},
120+
response,
121+
context,
122+
);
123+
},
124+
{
125+
message:
126+
/Failed to parse input as JSON: Unexpected token 'i', "invalid" is not valid JSON/,
127+
},
128+
);
129+
},
130+
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
131+
{experimentalWebmcp: true} as ParsedArguments,
132+
);
38133
});
39134
});
40135
});

0 commit comments

Comments
 (0)