mirror of
https://github.com/hanzoai/puppeteer.git
synced 2026-09-24 17:02:14 +00:00
feat(webmcp): add hook for tool invocation (#14835)
This commit is contained in:
committed by
GitHub
parent
cabdb19689
commit
1378f04bf7
@@ -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>
|
||||
|
||||
@@ -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)<{ toolsadded: [WebMCPToolsAddedEvent](./puppeteer.webmcptoolsaddedevent.md); toolsremoved: [WebMCPToolsRemovedEvent](./puppeteer.webmcptoolsremovedevent.md); }>
|
||||
**Extends:** [EventEmitter](./puppeteer.eventemitter.md)<{ toolsadded: [WebMCPToolsAddedEvent](./puppeteer.webmcptoolsaddedevent.md); toolsremoved: [WebMCPToolsRemovedEvent](./puppeteer.webmcptoolsremovedevent.md); toolinvoked: [WebMCPToolCall](./puppeteer.webmcptoolcall.md); }>
|
||||
|
||||
## Remarks
|
||||
|
||||
|
||||
@@ -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)<{ toolinvoked: [WebMCPToolCall](./puppeteer.webmcptoolcall.md); }>
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user