feat(webmcp): Add experimental tool to list WebMCP tools page exposes (#1845)
<img width="1305" height="735" alt="image" src="https://github.com/user-attachments/assets/ea154d81-c1cf-4758-98c2-7dce25d6ad8b" /> ``` npx @modelcontextprotocol/inspector node build/src/bin/chrome-devtools-mcp.js --channel=canary --experimentalWebmcp --chrome-arg='--enable-features=WebMCPTesting,DevToolsWebMCPSupport' ``` <img width="1728" height="1117" alt="image" src="https://github.com/user-attachments/assets/133ac10e-7bd3-4eaa-b1e9-25b1283e5761" /> cc @OrKoN
This commit is contained in:
committed by
GitHub
parent
e308e4a5b8
commit
f97b573d70
@@ -9,6 +9,7 @@ import type {
|
||||
ElementHandle,
|
||||
Page,
|
||||
Viewport,
|
||||
WebMCPTool,
|
||||
} from './third_party/index.js';
|
||||
import type {ToolGroup, ToolDefinition} from './tools/inPage.js';
|
||||
import {takeSnapshot} from './tools/snapshot.js';
|
||||
@@ -78,6 +79,10 @@ export class McpPage implements ContextPage {
|
||||
return this.inPageTools;
|
||||
}
|
||||
|
||||
getWebMcpTools(): WebMCPTool[] {
|
||||
return this.pptrPage.webmcp.tools();
|
||||
}
|
||||
|
||||
get networkConditions(): string | null {
|
||||
return this.emulationSettings.networkConditions ?? null;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {WebMCPTool} from 'puppeteer-core';
|
||||
|
||||
import type {ParsedArguments} from './bin/chrome-devtools-mcp-cli-options.js';
|
||||
import {ConsoleFormatter} from './formatters/ConsoleFormatter.js';
|
||||
import {IssueFormatter} from './formatters/IssueFormatter.js';
|
||||
@@ -181,6 +183,7 @@ export class McpResponse implements Response {
|
||||
};
|
||||
#listExtensions?: boolean;
|
||||
#listInPageTools?: boolean;
|
||||
#listWebMcpTools?: boolean;
|
||||
#devToolsData?: DevToolsData;
|
||||
#tabId?: string;
|
||||
#args: ParsedArguments;
|
||||
@@ -232,6 +235,10 @@ export class McpResponse implements Response {
|
||||
}
|
||||
}
|
||||
|
||||
setListWebMcpTools(): void {
|
||||
this.#listWebMcpTools = true;
|
||||
}
|
||||
|
||||
setIncludeNetworkRequests(
|
||||
value: boolean,
|
||||
options?: PaginationOptions & {
|
||||
@@ -374,6 +381,10 @@ export class McpResponse implements Response {
|
||||
return this.#snapshotParams;
|
||||
}
|
||||
|
||||
get listWebMcpTools(): boolean | undefined {
|
||||
return this.#listWebMcpTools;
|
||||
}
|
||||
|
||||
async handle(
|
||||
toolName: string,
|
||||
context: McpContext,
|
||||
@@ -490,6 +501,12 @@ export class McpResponse implements Response {
|
||||
page.inPageTools = inPageTools;
|
||||
}
|
||||
|
||||
let webmcpTools: WebMCPTool[] | undefined;
|
||||
if (this.#listWebMcpTools && this.#args.experimentalWebmcp) {
|
||||
const page = this.#page ?? context.getSelectedMcpPage();
|
||||
webmcpTools = page.getWebMcpTools();
|
||||
}
|
||||
|
||||
let consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
|
||||
if (this.#consoleDataOptions?.include) {
|
||||
if (!this.#page) {
|
||||
@@ -595,6 +612,7 @@ export class McpResponse implements Response {
|
||||
extensions,
|
||||
lighthouseResult: this.#attachedLighthouseResult,
|
||||
inPageTools,
|
||||
webmcpTools,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -612,6 +630,7 @@ export class McpResponse implements Response {
|
||||
extensions?: InstalledExtension[];
|
||||
lighthouseResult?: LighthouseData;
|
||||
inPageTools?: ToolGroup<ToolDefinition>;
|
||||
webmcpTools?: WebMCPTool[];
|
||||
},
|
||||
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
|
||||
const structuredContent: {
|
||||
@@ -627,6 +646,7 @@ export class McpResponse implements Response {
|
||||
lighthouseResult?: object;
|
||||
extensions?: object[];
|
||||
inPageTools?: object;
|
||||
webmcpTools?: object[];
|
||||
message?: string;
|
||||
networkConditions?: string;
|
||||
navigationTimeout?: number;
|
||||
@@ -884,6 +904,30 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#listWebMcpTools && data.webmcpTools) {
|
||||
structuredContent.webmcpTools = data.webmcpTools.map(
|
||||
({name, description, inputSchema, annotations}) => ({
|
||||
name,
|
||||
description,
|
||||
inputSchema,
|
||||
annotations,
|
||||
}),
|
||||
);
|
||||
response.push('## WebMCP tools');
|
||||
if (data.webmcpTools.length === 0) {
|
||||
response.push('No WebMCP tools available.');
|
||||
} else {
|
||||
const webmcpToolsMessage = data.webmcpTools
|
||||
.map(tool => {
|
||||
return `name="${tool.name}", description="${tool.description}", inputSchema=${JSON.stringify(
|
||||
tool.inputSchema,
|
||||
)}, annotations=${JSON.stringify(tool.annotations)}`;
|
||||
})
|
||||
.join('\n');
|
||||
response.push(webmcpToolsMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#networkRequestsOptions?.include && data.networkRequests) {
|
||||
const requests = data.networkRequests;
|
||||
|
||||
|
||||
@@ -185,6 +185,11 @@ export const cliOptions = {
|
||||
describe:
|
||||
'Exposes experimental screencast tools (requires ffmpeg). Install ffmpeg https://www.ffmpeg.org/download.html and ensure it is available in the MCP server PATH.',
|
||||
},
|
||||
experimentalWebmcp: {
|
||||
type: 'boolean',
|
||||
describe: 'Set to true to enable debugging WebMCP tools.',
|
||||
hidden: true,
|
||||
},
|
||||
chromeArg: {
|
||||
type: 'array',
|
||||
describe:
|
||||
|
||||
@@ -51,6 +51,7 @@ delete startCliOptions.viewport;
|
||||
// tools, they need to be enabled during CLI generation.
|
||||
delete startCliOptions.experimentalPageIdRouting;
|
||||
delete startCliOptions.experimentalVision;
|
||||
delete startCliOptions.experimentalWebmcp;
|
||||
delete startCliOptions.experimentalInteropTools;
|
||||
delete startCliOptions.experimentalScreencast;
|
||||
delete startCliOptions.categoryEmulation;
|
||||
|
||||
@@ -164,6 +164,12 @@ export async function createMcpServer(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
tool.annotations.conditions?.includes('experimentalWebmcp') &&
|
||||
!serverArgs.experimentalWebmcp
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const schema =
|
||||
'pageScoped' in tool &&
|
||||
tool.pageScoped &&
|
||||
|
||||
@@ -539,5 +539,9 @@
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "list_webmcp_tools",
|
||||
"args": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -134,6 +134,7 @@ export interface Response {
|
||||
setListExtensions(): void;
|
||||
attachLighthouseResult(result: LighthouseData): void;
|
||||
setListInPageTools(): void;
|
||||
setListWebMcpTools(): void;
|
||||
}
|
||||
|
||||
export type SupportedExtensions =
|
||||
|
||||
@@ -28,6 +28,7 @@ export const listPages = defineTool(args => {
|
||||
handler: async (_request, response) => {
|
||||
response.setIncludePages(true);
|
||||
response.setListInPageTools();
|
||||
response.setListWebMcpTools();
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -55,6 +56,7 @@ export const selectPage = defineTool({
|
||||
context.selectPage(page);
|
||||
response.setIncludePages(true);
|
||||
response.setListInPageTools();
|
||||
response.setListWebMcpTools();
|
||||
if (request.params.bringToFront) {
|
||||
await page.pptrPage.bringToFront();
|
||||
}
|
||||
@@ -280,6 +282,7 @@ export const navigatePage = definePageTool({
|
||||
|
||||
response.setIncludePages(true);
|
||||
response.setListInPageTools();
|
||||
response.setListWebMcpTools();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import * as scriptTools from './script.js';
|
||||
import * as slimTools from './slim/tools.js';
|
||||
import * as snapshotTools from './snapshot.js';
|
||||
import type {ToolDefinition} from './ToolDefinition.js';
|
||||
import * as webmcpTools from './webmcp.js';
|
||||
|
||||
export const createTools = (args: ParsedArguments) => {
|
||||
const rawTools = args.slim
|
||||
@@ -41,6 +42,7 @@ export const createTools = (args: ParsedArguments) => {
|
||||
...Object.values(screenshotTools),
|
||||
...Object.values(scriptTools),
|
||||
...Object.values(snapshotTools),
|
||||
...Object.values(webmcpTools),
|
||||
];
|
||||
|
||||
const tools = [];
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {ToolCategory} from './categories.js';
|
||||
import {definePageTool} from './ToolDefinition.js';
|
||||
|
||||
export const listWebMcpTools = definePageTool({
|
||||
name: 'list_webmcp_tools',
|
||||
description: `Lists all WebMCP tools the page exposes.`,
|
||||
annotations: {
|
||||
category: ToolCategory.DEBUGGING,
|
||||
readOnlyHint: true,
|
||||
conditions: ['experimentalWebmcp'],
|
||||
},
|
||||
schema: {},
|
||||
handler: async (_request, response, _context) => {
|
||||
response.setListWebMcpTools();
|
||||
},
|
||||
});
|
||||
@@ -1281,3 +1281,113 @@ exports[`lighthouse > includes lighthouse report paths 2`] = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`webmcp > includes webmcp tools in list_pages response 1`] = `
|
||||
## Pages
|
||||
1: about:blank [selected]
|
||||
## WebMCP tools
|
||||
name="test_tool", description="A test tool", inputSchema={}, annotations=undefined
|
||||
`;
|
||||
|
||||
exports[`webmcp > includes webmcp tools in list_pages response 2`] = `
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"id": 1,
|
||||
"url": "about:blank",
|
||||
"selected": true
|
||||
}
|
||||
],
|
||||
"webmcpTools": [
|
||||
{
|
||||
"name": "test_tool",
|
||||
"description": "A test tool",
|
||||
"inputSchema": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`webmcp > includes webmcp tools in navigate_page response 1`] = `
|
||||
Successfully navigated to about:blank.
|
||||
## Pages
|
||||
1: about:blank [selected]
|
||||
## WebMCP tools
|
||||
name="test_tool", description="A test tool", inputSchema={}, annotations=undefined
|
||||
`;
|
||||
|
||||
exports[`webmcp > includes webmcp tools in navigate_page response 2`] = `
|
||||
{
|
||||
"message": "Successfully navigated to about:blank.",
|
||||
"pages": [
|
||||
{
|
||||
"id": 1,
|
||||
"url": "about:blank",
|
||||
"selected": true
|
||||
}
|
||||
],
|
||||
"webmcpTools": [
|
||||
{
|
||||
"name": "test_tool",
|
||||
"description": "A test tool",
|
||||
"inputSchema": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`webmcp > includes webmcp tools in select_page response 1`] = `
|
||||
## Pages
|
||||
1: about:blank [selected]
|
||||
## WebMCP tools
|
||||
name="test_tool", description="A test tool", inputSchema={}, annotations=undefined
|
||||
`;
|
||||
|
||||
exports[`webmcp > includes webmcp tools in select_page response 2`] = `
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"id": 1,
|
||||
"url": "about:blank",
|
||||
"selected": true
|
||||
}
|
||||
],
|
||||
"webmcpTools": [
|
||||
{
|
||||
"name": "test_tool",
|
||||
"description": "A test tool",
|
||||
"inputSchema": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`webmcp > list no webmcp tools if experimentalWebmcp is false 1`] = `
|
||||
Successfully navigated to about:blank.
|
||||
## Pages
|
||||
1: about:blank [selected]
|
||||
`;
|
||||
|
||||
exports[`webmcp > list no webmcp tools if experimentalWebmcp is false 2`] = `
|
||||
{
|
||||
"message": "Successfully navigated to about:blank.",
|
||||
"pages": [
|
||||
{
|
||||
"id": 1,
|
||||
"url": "about:blank",
|
||||
"selected": true
|
||||
}
|
||||
]
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`webmcp > list no webmcp tools if there are none 1`] = `
|
||||
## WebMCP tools
|
||||
No WebMCP tools available.
|
||||
`;
|
||||
|
||||
exports[`webmcp > list no webmcp tools if there are none 2`] = `
|
||||
{
|
||||
"webmcpTools": []
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1455,3 +1455,130 @@ describe('replaceHtmlElementsWithUids', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('webmcp', () => {
|
||||
async function testIncludesWebmcpTools(
|
||||
t: it.TestContext,
|
||||
parseArguments: ParsedArguments,
|
||||
handlerAction: (
|
||||
response: McpResponse,
|
||||
context: McpContext,
|
||||
) => Promise<void>,
|
||||
toolName: string,
|
||||
) {
|
||||
await withMcpContext(
|
||||
async (response, context) => {
|
||||
response.setListWebMcpTools();
|
||||
|
||||
await handlerAction(response, context);
|
||||
|
||||
const page = context.getSelectedMcpPage().pptrPage;
|
||||
await page.setContent(
|
||||
html`<form
|
||||
toolname="test_tool"
|
||||
tooldescription="A test tool"
|
||||
></form>`,
|
||||
);
|
||||
|
||||
const {content, structuredContent} = await response.handle(
|
||||
toolName,
|
||||
context,
|
||||
);
|
||||
assert.ok(getTextContent(content[0]));
|
||||
t.assert.snapshot?.(getTextContent(content[0]));
|
||||
t.assert.snapshot?.(
|
||||
JSON.stringify(
|
||||
stabilizeStructuredContent(structuredContent),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
},
|
||||
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
|
||||
parseArguments,
|
||||
);
|
||||
}
|
||||
|
||||
it('includes webmcp tools in list_pages response', async t => {
|
||||
await testIncludesWebmcpTools(
|
||||
t,
|
||||
{experimentalWebmcp: true} as ParsedArguments,
|
||||
async (response, context) => {
|
||||
await listPages().handler({params: {}}, response, context);
|
||||
},
|
||||
'list_pages',
|
||||
);
|
||||
});
|
||||
|
||||
it('includes webmcp tools in select_page response', async t => {
|
||||
await testIncludesWebmcpTools(
|
||||
t,
|
||||
{experimentalWebmcp: true} as ParsedArguments,
|
||||
async (response, context) => {
|
||||
const pageId =
|
||||
context.getPageId(context.getSelectedMcpPage().pptrPage) ?? 1;
|
||||
await selectPage.handler({params: {pageId}}, response, context);
|
||||
},
|
||||
'select_page',
|
||||
);
|
||||
});
|
||||
|
||||
it('includes webmcp tools in navigate_page response', async t => {
|
||||
await testIncludesWebmcpTools(
|
||||
t,
|
||||
{experimentalWebmcp: true} as ParsedArguments,
|
||||
async (response, context) => {
|
||||
await navigatePage.handler(
|
||||
{
|
||||
params: {type: 'url', url: 'about:blank'},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
},
|
||||
'navigate_page',
|
||||
);
|
||||
});
|
||||
|
||||
it('list no webmcp tools if there are none', async t => {
|
||||
await withMcpContext(
|
||||
async (response, context) => {
|
||||
response.setListWebMcpTools();
|
||||
const {content, structuredContent} = await response.handle(
|
||||
'test',
|
||||
context,
|
||||
);
|
||||
assert.ok(getTextContent(content[0]));
|
||||
t.assert.snapshot?.(getTextContent(content[0]));
|
||||
t.assert.snapshot?.(
|
||||
JSON.stringify(
|
||||
stabilizeStructuredContent(structuredContent),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
},
|
||||
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
|
||||
{experimentalWebmcp: true} as ParsedArguments,
|
||||
);
|
||||
});
|
||||
|
||||
it('list no webmcp tools if experimentalWebmcp is false', async t => {
|
||||
await testIncludesWebmcpTools(
|
||||
t,
|
||||
{experimentalWebmcp: false} as ParsedArguments,
|
||||
async (response, context) => {
|
||||
await navigatePage.handler(
|
||||
{
|
||||
params: {type: 'url', url: 'about:blank'},
|
||||
page: context.getSelectedMcpPage(),
|
||||
},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
},
|
||||
'navigate_page',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import assert from 'node:assert';
|
||||
import {describe, it} from 'node:test';
|
||||
|
||||
import {listPages, navigatePage, selectPage} from '../../src/tools/pages.js';
|
||||
import {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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+3
-1
@@ -64,6 +64,7 @@ export async function withBrowser(
|
||||
debug?: boolean;
|
||||
autoOpenDevTools?: boolean;
|
||||
executablePath?: string;
|
||||
args?: string[];
|
||||
} = {},
|
||||
) {
|
||||
const launchOptions: LaunchOptions = {
|
||||
@@ -74,7 +75,7 @@ export async function withBrowser(
|
||||
devtools: options.autoOpenDevTools ?? false,
|
||||
pipe: true,
|
||||
handleDevToolsAsPage: true,
|
||||
args: ['--screen-info={3840x2160}'],
|
||||
args: [...(options.args || []), '--screen-info={3840x2160}'],
|
||||
enableExtensions: true,
|
||||
};
|
||||
const key = JSON.stringify(launchOptions);
|
||||
@@ -104,6 +105,7 @@ export async function withMcpContext(
|
||||
autoOpenDevTools?: boolean;
|
||||
performanceCrux?: boolean;
|
||||
executablePath?: string;
|
||||
args?: string[];
|
||||
} = {},
|
||||
args: ParsedArguments = {} as ParsedArguments,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user