Puppeteer 中的 WebMCP 类:通过 page.webmcp 发现与调用页面暴露的 Agent 工具
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
WebMCP 是 Puppeteer 中一处尚处于实验阶段的 API 面,它让 Node.js 侧的脚本能够发现当前页面中注册的"工具"(tools),并监听工具的注册、移除与调用事件。读完本文,你将掌握page.webmcp的接入前提、WebMCP类的四个核心事件与tools()方法的使用方式,以及与之配套的WebMCPTool、WebMCPToolCall等类型的字段语义,并能在真实页面中驱动一次完整的工具调用。
本文以 WebMCP 类 API 文档 为主体展开,结合 Puppeteer 仓库中 CDP 层实现 与 测试用例 进行源码级印证。
WebMCP 类概览:页面工具与自动化端的桥梁
WebMCP类是 Puppeteer 对 WebMCP(Web Model Context Protocol)能力在页面侧的封装。按照 API 索引 的定位,它"provides an API for the WebMCP API"——即让自动化代码访问页面定义的 WebMCP 工具。
从 类签名文档 可以看到它的完整声明:
export declare class WebMCP extends EventEmitter<{ toolsadded: WebMCPToolsAddedEvent; toolsremoved: WebMCPToolsRemovedEvent; toolinvoked: WebMCPToolCall; toolresponded: WebMCPToolCallResult; }>它继承自 EventEmitter,并把四类事件类型参数化到泛型中。在实际源码里,对应的类定义位于 packages/puppeteer-core/src/cdp/WebMCP.ts#L255-L264,内部持有CDPSession、FrameManager以及用于缓存工具、追踪进行中调用的几个Map字段:
export class WebMCP extends EventEmitter<{ toolsadded: WebMCPToolsAddedEvent; toolsremoved: WebMCPToolsRemovedEvent; toolinvoked: WebMCPToolCall; toolresponded: WebMCPToolCallResult; }> { #client: CDPSession; #frameManager: FrameManager; #tools = new Map<string, Map<string, WebMCPTool>>(); #pendingCalls = new Map<string, WebMCPToolCall>(); #subscriptions = new DisposableStack(); // ... }从源码结构看,WebMCP 是典型的 CDP 驱动实现:initialize()时向浏览器发送WebMCP.enable命令(WebMCP.ts#L371-L375),随后把WebMCP.toolsAdded、WebMCP.toolsRemoved、WebMCP.toolInvoked、WebMCP.toolResponded四条 CDP 事件通过 DisposableStack 订阅并翻译成上面的四个 EventEmitter 事件。
访问入口:page.webmcp 与其环境前提
WebMCP实例不需要也不能由第三方代码直接构造——文档的 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
WebMCPclass.
在 API 文档中它属于公开类型(@public),但用@experimental标注(见 WebMCP.ts 源码注释),构造器仅供 Puppeteer 内部使用。
应用侧统一通过Page上的webmcp属性访问。这一抽象 getter 定义在 Page.ts#L1004-L1010:
/** * Experimental API for WebMCP. * Requires Chrome 151+ with the `--enable-features=WebMCP` flag enabled. * @experimental */ abstract get webmcp(): WebMCP;对应的 page.webmcp 属性文档 也给出了两个关键限制,这两点是实际运行前必须满足的前提:
- 浏览器版本:需要 Chrome 151 及以上的版本;
- 启动参数:必须以
--enable-features=WebMCP启动 Chromium。
测试代码正是这样配置的。在 webmcp.test.ts#L20-L24 中,整个测试套件通过setupSeparateTestBrowserHooks为浏览器注入args: ['--enable-features=WebMCP']:
describe('Page.webmcp', function () { const state = setupSeparateTestBrowserHooks({ args: ['--enable-features=WebMCP'], acceptInsecureCerts: true, });因此在你的代码里,启动浏览器时需要类似写法:
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch({ headless: true, args: ['--enable-features=WebMCP'], }); const page = await browser.newPage();如果使用了 Puppeteer 的connect连接远端浏览器,同样需要确保远端 Chrome 版本 ≥ 151 且以该特性开关启动。由于目前是实验特性,后续版本中 API 形态可能变化,建议在使用时固定 Puppeteer 版本。
枚举页面工具:tools() 方法
WebMCP目前对外暴露的唯一方法就是tools()。其签名与返回类型见 WebMCP.tools() 方法文档:
class WebMCP { tools(): WebMCPTool[]; }它返回页面上全部已注册的 WebMCP 工具数组,每个元素是一个 WebMCPTool 实例。底层实现是对内部两级Map(frameId → 工具名 → 工具)做扁平化(WebMCP.ts#L404-L411)。
原文档给出了最典型的用法——页面加载完成后枚举工具并打印其名称与描述(示例):
await page.goto('https://www.example.com'); const tools = page.webmcp.tools(); for (const tool of tools) { console.log(`Tool found: ${tool.name} - ${tool.description}`); }在 测试用例 中可以看到tools()返回结果的完整断言:当页面分别通过"命令式注册"(调用document.modelContext.registerTool)与"声明式声明"(往 DOM 中追加带toolname/tooldescription属性的<form>)两种方式暴露工具后,page.webmcp.tools()会返回两条工具记录,其name、description、inputSchema、annotations、frame、formElement、location等字段会被逐一校验。
需要留意的生命周期语义
结合测试可以发现两个与工具集合管理直接相关的边界行为:
- 整页导航会清空工具:
toolsremoved事件在 frame 上下文销毁时被触发(源码见 onContextDisposed),测试 should remove tools on frame navigation 验证了 reload 后tools()长度回到 0; - 同文档导航(hash 跳转)不会清空工具:上下文未被销毁,工具集合保持不变(测试用例 L363-L387)。
事件体系:toolsadded / toolsremoved / toolinvoked / toolresponded
WebMCP 是一套事件驱动模型,四项事件分别对应"工具上架、工具下架、工具被调用、调用有结果"四个阶段。原文档通过类签名声明了这四个事件,测试用例则逐个验证了它们的触发时机与载荷。
| 事件 | 载荷类型 | 触发时机 | 对应文档 |
|---|---|---|---|
toolsadded | WebMCPToolsAddedEvent({tools: WebMCPTool[]}) | 页面注册了新的工具 | 源码 |
toolsremoved | WebMCPToolsRemovedEvent({tools: WebMCPTool[]}) | 页面移除工具,或所在 frame 被销毁 | 源码 |
toolinvoked | WebMCPToolCall | 页面侧发起一次工具调用 | 源码 |
toolresponded | WebMCPToolCallResult | 工具调用完成、失败或被取消 | 源码 |
监听方式与 EventEmitter 完全一致,例如在 toolinvoked 事件测试 中所示:
const tools = page.webmcp.tools(); page.webmcp.on('toolsadded', event => { console.log('added', event.tools.map(t => t.name)); }); page.webmcp.on('toolsremoved', event => { console.log('removed', event.tools.map(t => t.name)); }); page.webmcp.on('toolinvoked', call => { console.log('invoked', call.tool.name, call.input); }); page.webmcp.on('toolresponded', response => { console.log('responded', response.id, response.status, response.output); });注意同一把事件也会"穿透"到具体工具上:WebMCPTool本身也继承EventEmitter<{toolinvoked: WebMCPToolCall}>(声明见 docs/api/puppeteer.webmcptool.md),因此既可以page.webmcp.on('toolinvoked', ...)全局监听,也可以tool.once('toolinvoked', ...)针对某个工具监听(测试中两种方式都被使用,见 test L419-L425)。
WebMCPTool:单个工具的对象化表示
tools()返回的每个 WebMCPTool 是一个 EventEmitter 子类,把页面暴露的工具元数据完整地对象化。其公开属性如下:
| 属性 | 类型 | 说明 |
|---|---|---|
name | string | 工具名称 |
description | string | 工具描述 |
inputSchema(可选) | object | 工具输入参数对应的 JSON Schema |
annotations(可选) | Protocol.WebMCP.Annotation | 工具的可选标注(如只读提示、不可信内容提示) |
frame | Frame | 该工具被定义所在的 frame |
location(可选) | ConsoleMessageLocation | 定义工具的源码位置(若可用) |
formElement(只读) | Promise<ElementHandle<HTMLFormElement> \| undefined> | 工具若通过<form>声明式注册,则对应其表单元素句柄 |
rawStackTrace | Protocol.Runtime.StackTrace | (内部字段)原始调用栈 |
对应实现见 WebMCP.ts#L26-L132。其中location是从工具注册时的stackTrace首帧解析而来(L75-L82),所以"命令式"注册的工具通常能拿到定义位置,而声明式(纯 HTML)注册的工具该项为空——测试断言也印证了这一差异。
formElement属于懒加载属性:只有当工具经由表单注册(携带 backendNodeId)时才有值,否则返回undefined;有值时它会把 backend node 采纳为主世界中的 ElementHandle(实现见 L88-L103)。
发起调用:execute() 与工具调用结果
虽然页面侧的工具通常由页面自己的逻辑(或页面内的 agent)来触发,WebMCPTool也提供了从自动化侧主动调用工具的方法。签名见 WebMCPTool.execute() 方法文档:
class WebMCPTool { execute( input?: object, options?: WebMCPToolExecuteOptions, ): Promise<WebMCPToolCallResult>; }input:调用参数对象,需与工具的inputSchema匹配;options:可传{signal: AbortSignal}(类型定义见 WebMCPToolExecuteOptions),用于取消仍在执行的调用。
其内部流程(WebMCP.ts#L108-L131)分两步:先经invokeTool()发送 CDP 命令WebMCP.invokeTool(携带frameId、toolName与input,见 L380-L389)拿到invocationId,随后挂起等待toolresponded事件中与invocationId匹配的结果;若传入的AbortSignal被触发,则回退到WebMCP.cancelInvocationCDP 命令请求取消。
返回的 WebMCPToolCallResult 字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 调用标识(与WebMCPToolCall.id对应) |
call(可选) | WebMCPToolCall | 本次调用对应的调用对象(若在 pending 表中可找到) |
status | Protocol.WebMCP.InvocationStatus | 调用状态 |
output(可选) | any | 结果输出;仅当status为Completed时存在 |
errorText(可选) | string | 错误文本 |
exception(可选) | Protocol.Runtime.RemoteObject | 若工具内 JS 抛异常,则为对应的异常远程对象 |
用 execute() 驱动一次完整调用
结合 should invoke tool 测试,完整流程如下:
// 页面内提前注册工具(命令式 WebMCP 工具) await page.evaluate(async () => { await document.modelContext?.registerTool({ name: 'test-tool-1', description: 'A test tool 1', inputSchema: { type: 'object', properties: {text: {type: 'string', description: 'Some text'}}, required: ['text'], }, execute: (params: {text: string}) => { return `hello ${params.text}`; }, }); }); // 等待工具被发现 await new Promise(resolve => { page.webmcp.once('toolsadded', resolve); }); // 自动化侧直接调用 const [tool] = page.webmcp.tools(); const response = await tool!.execute({text: 'world'}); console.log(response.status); // 'Completed' console.log(response.output); // 'hello world'status 的三种状态
从测试中可以归纳出status的取值语义,它们与工具本身的执行结果一一对应:
Completed:工具成功返回,output携带结果(测试 L462-L512);Error:工具内部抛出了 JS 异常(此时exception.description含错误信息,errorText为空字符串),或输入参数解析失败(如传入非法 JSON,此时errorText为'Failed to parse input arguments'),参见 L514-L602;Canceled:调用通过 AbortSignal 被取消(L653-L767)。
取消既可以在调用中途执行(controller.abort()在调用已经开始后才触发),也可以在调用前就把 signal 置为已中止——两种情况下结果状态都是Canceled:
const controller = new AbortController(); const executePromise = tool!.execute({text: 'world'}, {signal: controller.signal}); // …一段时间后决定取消 controller.abort(); const response = await executePromise; // status === 'Canceled'从事件到调用的完整协作视图
把上面各节串起来,一次 WebMCP 交互的生命周期是:
- 页面脚本调用
document.modelContext.registerTool(...)或向 DOM 追加带toolname的<form>,注册工具; - 浏览器发出
WebMCP.toolsAdded,Puppeteer 包装成toolsadded事件并更新内部工具表,之后page.webmcp.tools()可枚举到该工具; - 页面(或自动化侧调用
tool.execute())发起调用,toolinvoked事件先于结果到达,携带 WebMCPToolCall(含id、tool、input); - 工具执行完成/出错/被取消后,
toolresponded事件携带 WebMCPToolCallResult 到达,其id与对应的WebMCPToolCall.id一致; - 若用户导航离开或重新加载页面,frame 上下文销毁,触发
toolsremoved并清空待处理调用表(源码见 onContextDisposed)。
在 webmcp.test.ts 中,should fire toolinvoked events、should fire toolresponded event with success / with exception / with errorText、should invoke tool、should cancel tool execution等一系列用例完整覆盖了上述链路,是理解该实验特性最直观的可运行参考。
小结
page.webmcp为 Puppeteer 提供了一块访问"页面声明的 WebMCP 工具"的实验性入口。核心使用要点可归纳为:
- 环境:Chrome 151+,且启动时携带
--enable-features=WebMCP; - 发现:
page.webmcp.tools()枚举当前页面全部工具,得到 WebMCPTool 数组; - 订阅:监听
toolsadded/toolsremoved跟踪工具上/下架,监听toolinvoked/toolresponded跟踪调用过程; - 执行与取消:
tool.execute(input, {signal})从自动化侧主动调用,结果含Completed/Error/Canceled三种状态。
由于该特性仍处于实验阶段(构造器内部化、API 标注@experimental),使用时请以 docs/api/puppeteer.webmcp.md 及 Page.webmcp 属性文档 为基准,并及时跟进新版本 Puppeteer 的 CHANGELOG 以应对可能的接口调整。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考