feat(webmcp): add hook for tool invocation (#14835)

This commit is contained in:
François Beaufort
2026-04-08 11:08:10 +02:00
committed by GitHub
parent cabdb19689
commit 1378f04bf7
7 changed files with 226 additions and 5 deletions
+11
View File
@@ -587,6 +587,17 @@ The constructor for this class is marked as internal. Third-party code should no
</td></tr>
<tr><td>
<span id="webmcptoolcall">[WebMCPToolCall](./puppeteer.webmcptoolcall.md)</span>
</td><td>
**Remarks:**
The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `WebMCPToolCall` class.
</td></tr>
<tr><td>
<span id="webworker">[WebWorker](./puppeteer.webworker.md)</span>
</td><td>
+2 -1
View File
@@ -12,10 +12,11 @@ The WebMCP class provides an API for the WebMCP API.
export declare class WebMCP extends EventEmitter<{
toolsadded: WebMCPToolsAddedEvent;
toolsremoved: WebMCPToolsRemovedEvent;
toolinvoked: WebMCPToolCall;
}>
```
**Extends:** [EventEmitter](./puppeteer.eventemitter.md)&lt;&#123; toolsadded: [WebMCPToolsAddedEvent](./puppeteer.webmcptoolsaddedevent.md); toolsremoved: [WebMCPToolsRemovedEvent](./puppeteer.webmcptoolsremovedevent.md); &#125;&gt;
**Extends:** [EventEmitter](./puppeteer.eventemitter.md)&lt;&#123; toolsadded: [WebMCPToolsAddedEvent](./puppeteer.webmcptoolsaddedevent.md); toolsremoved: [WebMCPToolsRemovedEvent](./puppeteer.webmcptoolsremovedevent.md); toolinvoked: [WebMCPToolCall](./puppeteer.webmcptoolcall.md); &#125;&gt;
## Remarks
+5 -1
View File
@@ -7,9 +7,13 @@ sidebar_label: WebMCPTool
### Signature
```typescript
export declare class WebMCPTool
export declare class WebMCPTool extends EventEmitter<{
toolinvoked: WebMCPToolCall;
}>
```
**Extends:** [EventEmitter](./puppeteer.eventemitter.md)&lt;&#123; toolinvoked: [WebMCPToolCall](./puppeteer.webmcptoolcall.md); &#125;&gt;
## Remarks
The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `WebMCPTool` class.
+75
View File
@@ -0,0 +1,75 @@
---
sidebar_label: WebMCPToolCall
---
# WebMCPToolCall class
### Signature
```typescript
export declare class WebMCPToolCall
```
## Remarks
The constructor for this class is marked as internal. Third-party code should not call the constructor directly or create subclasses that extend the `WebMCPToolCall` class.
## Properties
<table><thead><tr><th>
Property
</th><th>
Modifiers
</th><th>
Type
</th><th>
Description
</th></tr></thead>
<tbody><tr><td>
<span id="id">id</span>
</td><td>
</td><td>
string
</td><td>
</td></tr>
<tr><td>
<span id="input">input</span>
</td><td>
</td><td>
object
</td><td>
</td></tr>
<tr><td>
<span id="tool">tool</span>
</td><td>
</td><td>
[WebMCPTool](./puppeteer.webmcptool.md)
</td><td>
</td></tr>
</tbody></table>
+50 -2
View File
@@ -49,10 +49,20 @@ interface ProtocolWebMCPToolsRemovedEvent {
tools: ProtocolWebMCPTool[];
}
interface ProtocolWebMCPToolInvokedEvent {
toolName: string;
frameId: string;
invocationId: string;
input: string;
}
/**
* @public
*/
export class WebMCPTool {
export class WebMCPTool extends EventEmitter<{
/** Emitted when invocation starts. */
toolinvoked: WebMCPToolCall;
}> {
#backendNodeId?: number;
#formElement?: ElementHandle<HTMLFormElement>;
@@ -71,6 +81,7 @@ export class WebMCPTool {
* @internal
*/
constructor(tool: ProtocolWebMCPTool, frame: Frame) {
super();
this.name = tool.name;
this.description = tool.description;
this.inputSchema = tool.inputSchema;
@@ -119,17 +130,41 @@ export interface WebMCPToolsRemovedEvent {
tools: WebMCPTool[];
}
/**
* @public
*/
export class WebMCPToolCall {
id: string;
tool: WebMCPTool;
input: object;
/**
* @internal
*/
constructor(invocationId: string, tool: WebMCPTool, input: string) {
this.id = invocationId;
this.tool = tool;
try {
this.input = JSON.parse(input);
} catch (error) {
this.input = {};
debugError(error);
}
}
}
/**
* The WebMCP class provides an API for the WebMCP API.
*
* @public
*/
export class WebMCP extends EventEmitter<{
/** Emitted when tools are added. */
toolsadded: WebMCPToolsAddedEvent;
/** Emitted when tools are removed. */
toolsremoved: WebMCPToolsRemovedEvent;
/** Emitted when a tool invocation starts. */
toolinvoked: WebMCPToolCall;
}> {
#client: CDPSession;
#frameManager: FrameManager;
@@ -155,6 +190,7 @@ export class WebMCP extends EventEmitter<{
this.emit('toolsadded', {tools});
};
#onToolsRemoved = (event: ProtocolWebMCPToolsRemovedEvent) => {
const tools: WebMCPTool[] = [];
event.tools.forEach(tool => {
@@ -167,6 +203,16 @@ export class WebMCP extends EventEmitter<{
this.emit('toolsremoved', {tools});
};
#onToolInvoked = (event: ProtocolWebMCPToolInvokedEvent) => {
const tool = this.#tools.get(event.frameId)?.get(event.toolName);
if (!tool) {
return;
}
const call = new WebMCPToolCall(event.invocationId, tool, event.input);
tool.emit('toolinvoked', call);
this.emit('toolinvoked', call);
};
#onFrameNavigated = (frame: Frame) => {
const frameTools = this.#tools.get(frame._id);
if (!frameTools) {
@@ -209,6 +255,7 @@ export class WebMCP extends EventEmitter<{
// Protocol types.
this.#client.on('WebMCP.toolsAdded' as any, this.#onToolsAdded as any);
this.#client.on('WebMCP.toolsRemoved' as any, this.#onToolsRemoved as any);
this.#client.on('WebMCP.toolInvoked' as any, this.#onToolInvoked as any);
}
/**
@@ -217,6 +264,7 @@ export class WebMCP extends EventEmitter<{
updateClient(client: CDPSession): void {
this.#client.off('WebMCP.toolsAdded' as any, this.#onToolsAdded as any);
this.#client.off('WebMCP.toolsRemoved' as any, this.#onToolsRemoved as any);
this.#client.off('WebMCP.toolInvoked' as any, this.#onToolInvoked as any);
this.#client = client;
this.#bindListeners();
}
+7
View File
@@ -1872,6 +1872,13 @@
"expectations": ["FAIL"],
"comment": "Experimental support for WebMCP is not supported in BiDi"
},
{
"testIdPattern": "[webmcp.spec] Page.webmcp should fire toolinvoked events",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "chrome"],
"expectations": ["TIMEOUT"],
"comment": "Experimental support for WebMCP tool invocation requires Chrome 148+."
},
{
"testIdPattern": "[worker.spec] Workers can be closed",
"platforms": ["darwin", "linux", "win32"],
+76 -1
View File
@@ -5,7 +5,10 @@
*/
import expect from 'expect';
import type {WebMCPTool} from 'puppeteer-core/internal/cdp/WebMCP.js';
import type {
WebMCPTool,
WebMCPToolCall,
} from 'puppeteer-core/internal/cdp/WebMCP.js';
import {setupSeparateTestBrowserHooks} from '../mocha-utils.js';
@@ -267,4 +270,76 @@ describe('Page.webmcp', function () {
expect(removedTools[0]!.name).toBe('declarative tool name');
expect(page.webmcp.tools().length).toBe(0);
});
it('should fire toolinvoked events', async () => {
const {page, httpsServer} = state;
await page.goto(httpsServer.EMPTY_PAGE);
expect(page.webmcp).toBeDefined();
const toolAdded = new Promise<WebMCPTool[]>(resolve => {
page.webmcp.once('toolsadded', event => {
return resolve(event.tools);
});
});
// Register a WebMCP tool.
await page.evaluate(() => {
(window as any).navigator.modelContext.registerTool({
name: 'test-tool-1',
description: 'A test tool 1',
inputSchema: {
type: 'object',
properties: {
text: {type: 'string', description: 'Some text'},
},
required: ['text'],
},
execute: () => {},
});
});
const [addedTool] = await toolAdded;
const addedToolCalled = new Promise<WebMCPToolCall>(resolve => {
addedTool!.once('toolinvoked', resolve);
});
const toolCalled = new Promise<WebMCPToolCall>(resolve => {
page.webmcp.once('toolinvoked', resolve);
});
// Execute WebMCP tool.
await page.evaluate(() => {
(window as any).navigator.modelContextTesting.executeTool(
'test-tool-1',
JSON.stringify({text: 'test'}),
);
});
const [addedToolCall, toolCall] = await Promise.all([
addedToolCalled,
toolCalled,
]);
async function expectToolCall(call: WebMCPToolCall) {
expect(call.id).toBeDefined();
expect(call.tool).toBeDefined();
expect(call.tool.name).toBe('test-tool-1');
expect(call.tool.description).toBe('A test tool 1');
expect(call.tool.inputSchema).toStrictEqual({
type: 'object',
properties: {
text: {type: 'string', description: 'Some text'},
},
required: ['text'],
});
expect(call.tool.frame).toBe(page.mainFrame());
expect(await call.tool.formElement).toBeUndefined();
expect(call.tool.location).toBeDefined();
expect(call.input).toStrictEqual({text: 'test'});
}
await expectToolCall(addedToolCall);
await expectToolCall(toolCall);
});
});