Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
6 changes: 6 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import fs from 'node:fs/promises';
import path from 'node:path';

import type {WebMCPTool} from 'puppeteer-core';

import type {TargetUniverse} from './DevtoolsUtils.js';
import {UniverseManager} from './DevtoolsUtils.js';
import {McpPage} from './McpPage.js';
Expand Down Expand Up @@ -222,6 +224,10 @@ export class McpContext implements Context {
);
}

getWebMcpTools(page: McpPage): WebMCPTool[] {
return page.pptrPage.webmcp.tools();
}

getDevToolsUniverse(page: McpPage): TargetUniverse | null {
return this.#devtoolsUniverseManager.get(page.pptrPage);
}
Expand Down
35 changes: 35 additions & 0 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -181,6 +183,7 @@ export class McpResponse implements Response {
};
#listExtensions?: boolean;
#listInPageTools?: boolean;
#listWebMcpTools?: boolean;
#devToolsData?: DevToolsData;
#tabId?: string;
#args: ParsedArguments;
Expand Down Expand Up @@ -232,6 +235,12 @@ export class McpResponse implements Response {
}
}

setListWebMcpTools(): void {
if (this.#args.experimentalWebmcp) {
Comment thread
OrKoN marked this conversation as resolved.
Outdated
this.#listWebMcpTools = true;
}
}

setIncludeNetworkRequests(
value: boolean,
options?: PaginationOptions & {
Expand Down Expand Up @@ -488,6 +497,12 @@ export class McpResponse implements Response {
page.inPageTools = inPageTools;
}

let webmcpTools: WebMCPTool[] | undefined;
if (this.#listWebMcpTools) {
const page = this.#page ?? context.getSelectedMcpPage();
webmcpTools = context.getWebMcpTools(page);
Comment thread
beaufortfrancois marked this conversation as resolved.
Outdated
}

let consoleMessages: Array<ConsoleFormatter | IssueFormatter> | undefined;
if (this.#consoleDataOptions?.include) {
if (!this.#page) {
Expand Down Expand Up @@ -592,6 +607,7 @@ export class McpResponse implements Response {
extensions,
lighthouseResult: this.#attachedLighthouseResult,
inPageTools,
webmcpTools,
});
}

Expand All @@ -609,6 +625,7 @@ export class McpResponse implements Response {
extensions?: InstalledExtension[];
lighthouseResult?: LighthouseData;
inPageTools?: ToolGroup<ToolDefinition>;
webmcpTools?: WebMCPTool[];
},
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
const structuredContent: {
Expand All @@ -624,6 +641,7 @@ export class McpResponse implements Response {
lighthouseResult?: object;
extensions?: object[];
inPageTools?: object;
webmcpTools?: object[];
message?: string;
networkConditions?: string;
navigationTimeout?: number;
Expand Down Expand Up @@ -881,6 +899,23 @@ Call ${handleDialog.name} to handle it before continuing.`);
}
}

if (this.#listWebMcpTools && data.webmcpTools) {
structuredContent.webmcpTools = data.webmcpTools;
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;

Expand Down
5 changes: 5 additions & 0 deletions src/bin/chrome-devtools-mcp-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/bin/chrome-devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
4 changes: 4 additions & 0 deletions src/telemetry/tool_call_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -539,5 +539,9 @@
"argType": "number"
}
]
},
{
"name": "list_webmcp_tools",
"args": []
}
]
1 change: 1 addition & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export interface Response {
setListExtensions(): void;
attachLighthouseResult(result: LighthouseData): void;
setListInPageTools(): void;
setListWebMcpTools(): void;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/tools/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const listPages = defineTool(args => {
handler: async (_request, response) => {
response.setIncludePages(true);
response.setListInPageTools();
response.setListWebMcpTools();
Comment thread
beaufortfrancois marked this conversation as resolved.
},
};
});
Expand Down Expand Up @@ -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();
}
Expand Down
2 changes: 2 additions & 0 deletions src/tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,6 +42,7 @@ export const createTools = (args: ParsedArguments) => {
...Object.values(screenshotTools),
...Object.values(scriptTools),
...Object.values(snapshotTools),
...Object.values(webmcpTools),
];

const tools = [];
Expand Down
22 changes: 22 additions & 0 deletions src/tools/webmcp.ts
Original file line number Diff line number Diff line change
@@ -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();
},
});
86 changes: 86 additions & 0 deletions tests/tools/webmcp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import assert from 'node:assert';
import {describe, it} from 'node:test';

import type {ParsedArguments} from '../../src/bin/chrome-devtools-mcp-cli-options.js';
import {listWebMcpTools} from '../../src/tools/webmcp.js';
import {getTextContent, html, withMcpContext} from '../utils.js';

describe('webmcp', () => {
it('list webmcp tools successfully', async () => {
await withMcpContext(
async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
await page.setContent(
html`<form
toolname="test_tool"
tooldescription="A test tool"
></form>`,
);

await listWebMcpTools.handler(
{params: {}, page: context.getSelectedMcpPage()},
response,
context,
);

const formattedResponse = await response.handle('test', context);
const textContent = getTextContent(formattedResponse.content[0]);
Comment thread
beaufortfrancois marked this conversation as resolved.
Outdated
assert.match(
textContent,
/name="test_tool", description="A test tool"/,
);
},
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
{experimentalWebmcp: true} as ParsedArguments,
);
});

it('list no webmcp tools if there are none', async () => {
await withMcpContext(
async (response, context) => {
await listWebMcpTools.handler(
{params: {}, page: context.getSelectedMcpPage()},
response,
context,
);

const formattedResponse = await response.handle('test', context);
const textContent = getTextContent(formattedResponse.content[0]);
assert.match(textContent, /No WebMCP tools available/);
},
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
{experimentalWebmcp: true} as ParsedArguments,
);
});

it('does not list webmcp tools if not enabled', async () => {
await withMcpContext(
async (response, context) => {
const page = context.getSelectedMcpPage().pptrPage;
await page.setContent(
html`<form
toolname="test_tool"
tooldescription="A test tool"
></form>`,
);

await listWebMcpTools.handler(
{params: {}, page: context.getSelectedMcpPage()},
response,
context,
);

const formattedResponse = await response.handle('test', context);
const textContent = getTextContent(formattedResponse.content[0]);
assert.ok(!textContent.includes('name="test_tool"'));
},
{args: ['--enable-features=WebMCPTesting,DevToolsWebMCPSupport']},
);
});
});
4 changes: 3 additions & 1 deletion tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export async function withBrowser(
debug?: boolean;
autoOpenDevTools?: boolean;
executablePath?: string;
args?: string[];
} = {},
) {
const launchOptions: LaunchOptions = {
Expand All @@ -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);
Expand Down Expand Up @@ -104,6 +105,7 @@ export async function withMcpContext(
autoOpenDevTools?: boolean;
performanceCrux?: boolean;
executablePath?: string;
args?: string[];
} = {},
args: ParsedArguments = {} as ParsedArguments,
) {
Expand Down