mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
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:
committed by
GitHub
parent
f97b573d70
commit
0aff266111
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user