Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/bin/chrome-devtools-mcp-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions src/telemetry/tool_call_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 48 additions & 0 deletions src/tools/webmcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 changes: 15 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
);
});
});
132 changes: 112 additions & 20 deletions tests/tools/webmcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
assert.ok(response.listWebMcpTools);
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 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);
});
});
});

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>`,
);
}

// 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);

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,
);
});
});
});