Puppeteer 与 WebMCP 实战指南:让网页工具被浏览器与 LLM Agent 发现与调用
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
WebMCP(Web Machine Context Protocol)是一项实验性 API,允许网页注册可供浏览器或外部 Agent(如 LLM)发现和调用的工具;本指南讲解如何在 Puppeteer 中启用 WebMCP、发现页面已注册的工具、执行与取消工具调用、监听调用全过程,以及如何在页面内以命令式或声明式方式注册工具。读完本文,你将能够基于 Chrome 151+ 构建“网页暴露能力 → Puppeteer/Agent 自动发现 → 程序化调用并回收结果”的完整链路。
⚠️ 实验性说明:WebMCP 是实验性 API,随时可能变更。目前仅在 Chrome 151+ 中支持,并且需要显式开启对应 Flag(
--enable-features=WebMCP)。本指南依据当前仓库 docs/guides/webmcp.md 及其底层实现编写。
前提条件
要在 Puppeteer 中使用 WebMCP,需要同时满足两个条件:
- Chrome 151+:浏览器必须支持 WebMCP CDP 域(
WebMCP.*),所有工具发现与调用最终都经由该 CDP 域完成。 - 开启实验特性 Flag:启动浏览器时必须携带:
--enable-features=WebMCP
在 Puppeteer 中启用 WebMCP
WebMCP 支持通过page.webmcp属性暴露给用户,它属于WebMCP类,继承自EventEmitter(事件映射见 puppeteer.webmcp.md):
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch({ args: ['--enable-features=WebMCP'], }); const page = await browser.newPage(); // page.webmcp 现在可用 console.log(page.webmcp);当你导航到某个页面时,若浏览器支持 WebMCP,page.webmcp会被自动初始化。从源码看,这一过程发生在页面初始化阶段:packages/puppeteer-core/src/cdp/Page.ts 中,#initialize()会与FrameManager初始化等任务并行调用this.#webmcp.initialize(),后者向 CDP 会话发送WebMCP.enable命令(见 packages/puppeteer-core/src/cdp/WebMCP.ts)。值得注意的是,WebMCP.enable失败时错误会被捕获并仅记录日志而不会中断页面加载,因此在不支持的浏览器上访问该属性不会抛出致命异常;page.webmcp的抽象 getter 声明在 packages/puppeteer-core/src/api/Page.ts。
提示:
page.webmcp在页面初次导航前可能尚未完成工具枚举,建议先await page.goto(...)等待工具事件(见下文)或就绪后再读取工具列表。
WebMCP 类概览:事件与底层数据流
WebMCP类把 CDP 的 4 类协议事件翻译成 4 个可供 Puppeteer 订阅的领域事件。下表汇总了事件签名与触发时机(依据 WebMCP.ts 及 API 文档 puppeteer.webmcptoolsaddedevent.md):
| 事件 | 载荷类型 | 触发时机 |
|---|---|---|
toolsadded | WebMCPToolsAddedEvent(含tools: WebMCPTool[]) | 页面注册新工具(命令式或声明式) |
toolsremoved | WebMCPToolsRemovedEvent(含tools: WebMCPTool[]) | 页面注销工具,或工具所在 frame 的执行上下文被销毁 |
toolinvoked | WebMCPToolCall(含id、tool、input) | 某次工具调用开始(调用方是页面或浏览器) |
toolresponded | WebMCPToolCallResult | 工具调用完成、出错或取消 |
在实现内部,WebMCP使用两层 Map 按 frame 维度维护每个工具:#tools(frameId → (toolName →WebMCPTool))和#pendingCalls(invocationId →WebMCPToolCall)。CDP 的WebMCP.toolsAdded / toolsRemoved / toolInvoked / toolResponded事件在构造时被绑定到对应处理器(见 WebMCP.ts)。当 frame 的上下文销毁时,#onContextDisposed会清空挂起调用,并把该 frame 的所有工具作为一次toolsremoved广播出去(WebMCP.ts),这保证了不会残留“僵尸工具”状态。
WebMCPToolCall中的input是 JSON 字符串解析后的对象:若解析失败,input会退化为{}并记录错误日志(WebMCP.ts),实际开发中最好确保输入总是合法 JSON。
发现页面上的工具
用page.webmcp.tools()获取当前页面所有已注册工具;它把内部 Map 展平为WebMCPTool[]数组(WebMCP.ts)。每个WebMCPTool暴露以下属性(参见 puppeteer.webmcptool.md):
name: string—— 工具名称;description: string—— 工具用途描述(供 Agent 决策);inputSchema?: object—— 输入参数的 JSON Schema,用于生成符合规范的调用参数;annotations?: object—— 可选注解(测试中可见readOnly、untrustedContent、autosubmit等能力位);frame: Frame—— 工具定义所在 frame;location?: ConsoleMessageLocation—— 定义该工具的源码位置(命令式注册且能取到堆栈时才有值);formElement(getter)—— 若由 HTML 表单注册,返回对应的ElementHandle<HTMLFormElement>。
// 获取当前已注册的工具 const tools = page.webmcp.tools(); for (const tool of tools) { console.log(`Tool found: ${tool.name} - ${tool.description}`); } // 监听新增工具 page.webmcp.on('toolsadded', event => { for (const tool of event.tools) { console.log(`New tool added: ${tool.name}`); } }); // 监听工具被移除 page.webmcp.on('toolsremoved', event => { for (const tool of event.tools) { console.log(`Tool removed: ${tool.name}`); } });tools()是同步快照方法,因此典型的“先注册、后发现”模式需要借助toolsadded事件做同步。仓库测试 test/src/cdp/webmcp.test.ts 展示了标准做法:先在页面中分别注册命令式工具与声明式工具,等待toolsadded计数达到预期后再调用page.webmcp.tools()断言长度为 2,并逐项校验name、description、inputSchema、annotations、frame与formElement。
执行工具
通过WebMCPTool上的execute方法调用已发现的工具,该方法返回一个以调用结果为值的 Promise(WebMCPTool.execute):
const tools = page.webmcp.tools(); const tool = tools.find(t => t.name === 'calculate_sum'); if (tool) { const result = await tool.execute({a: 5, b: 10}); if (result.status === 'Completed') { console.log('Result:', result.output); } else { console.error('Error:', result.errorText); } }execute(input, options)内部执行两步:先通过 CDPWebMCP.invokeTool拿到invocationId,再挂起等待与invocationId匹配的toolresponded事件到来。这解释了两个细节:
execute返回的WebMCPToolCallResult.id与内部WebMCPToolCall.id是同一个调用标识,事件监听者可用它把调用与响应一一对应;- 由于实现建立在事件匹配上,同一时刻多个工具并发执行也互不干扰。
WebMCPToolCallResult的完整字段如下(参见 puppeteer.webmcptoolcallresult.md):
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 调用标识符 |
call? | WebMCPToolCall | 对应的调用对象(若可用) |
status | WebMCP.InvocationStatus | 调用状态(如Completed/Canceled/Error) |
output? | any | 工具输出;仅当status为Completed时存在 |
errorText? | string | 错误文本 |
exception? | Runtime.RemoteObject | 工具抛出 JS 异常时的异常对象 |
关于失败场景,仓库测试给出了两个可验证的事实:当工具的execute函数直接throw时,响应状态为Error,output为undefined,exception中携带包含错误消息的description;而面向“工具不存在/参数错误”等错误则通过errorText承载(webmcp.test.ts 中分别断言了异常与 errorText 两种分支)。因此可靠的业务代码应同时检查status、errorText与exception三种信号,而不只判断status === 'Completed'。
取消正在执行的工具
execute接受第二个参数WebMCPToolExecuteOptions,目前仅支持signal?: AbortSignal。传入AbortSignal后,一旦信号触发,Puppeteer 会向浏览器发送WebMCP.cancelInvocation请求取消对应调用(WebMCP.ts)。如果调用发起时信号已处于 aborted 状态,也会立即触发取消:
const controller = new AbortController(); // 2 秒后取消执行 setTimeout(() => { controller.abort(); }, 2000); const result = await tool.execute( {query: 'large data processing'}, {signal: controller.signal}, ); if (result.status === 'Canceled') { console.log('Tool execution was canceled.'); }取消成功时,响应状态为Canceled。取消逻辑的实际挂接点位于execute内部:它在注册toolresponded监听器的同时为signal挂上 abort 处理器,并在得到与本次invocationId匹配的响应后自动解除监听与取消回调,避免内存泄漏。
观察工具被调用并读取响应
除主动execute外,你还可以监听页面自身或浏览器发起的工具调用,这在审计、埋点与调试 Agent 行为时非常有用:
page.webmcp.on('toolinvoked', call => { console.log(`Tool ${call.tool.name} was invoked with input:`, call.input); }); page.webmcp.on('toolresponded', response => { console.log( `Tool ${response.call?.tool.name} responded with status: ${response.status}`, ); if (response.status === 'Completed') { console.log('Output:', response.output); } else if (response.status === 'Canceled') { console.log('Invocation was canceled'); } else { console.log('Error:', response.errorText); } });注意两点语义:
toolinvoked同时在两级上触发:WebMCP实例(page.webmcp.on(...))以及被调用的WebMCPTool实例(tool.once('toolinvoked', ...)),后者便于只关注某个具体工具的调用;- 当调用由页面内部发起(例如
modelContext.executeTool(...)),只要工具已注册,Puppeteer 侧同样能收到toolinvoked与toolresponded。仓库测试正是利用这一能力验证“页面发起调用 → Puppeteer 收到事件 →response.output与注册时execute的返回值一致”的闭环(webmcp.test.ts)。
在页面中注册工具
工具既可命令式注册(JavaScript),也可声明式注册(带特定属性的 HTML 表单)。Puppeteer 页面本身就是一个 WebMCP 页面,因此这两种方式都可直接在页面上下文中演示。
命令式注册
通过page.evaluate在页面内调用 Web 平台侧的document.modelContext?.registerTool(...),注册时需要提供name、description、inputSchema与execute:
await page.evaluate(async () => { await document.modelContext?.registerTool({ name: 'calculate_sum', description: 'Calculates the sum of two numbers', inputSchema: { type: 'object', properties: { a: {type: 'number'}, b: {type: 'number'}, }, required: ['a', 'b'], }, execute: ({a, b}) => { return a + b; }, }); });命令式注册还支持可选的annotations与取消信号({signal: controller.signal})。仓库测试中注册了{readOnlyHint: true, untrustedContentHint: true},随后在 Puppeteer 侧断言工具annotations.readOnly === true与annotations.untrustedContent === true,证明注解信息会原样从页面透传到WebMCPTool.annotations。
声明式注册
WebMCP 也能发现带有特定属性的 HTML 表单并把它转成工具。最基本的属性是toolname与tooldescription,测试中还出现了toolautosubmit(允许表单自动提交执行);表单内的<input name="...">即工具的输入字段:
await page.setContent(` <form toolname="search_products" tooldescription="Search for products in the catalog" > <input name="query" type="text" /> <button type="submit">Search</button> </form> `);声明式工具在 Puppeteer 侧同样表现为WebMCPTool,但有两个区别于命令式工具的可验证特征:
inputSchema会由浏览器根据表单输入字段自动推导(测试中断言空表单对应{type: 'object', properties: {}, required: []});tool.location为undefined,而tool.formElement返回对应的表单元素句柄(因为底层是通过backendNodeId在主世界隔离域中adoptBackendNode得到的 ElementHandle,见 WebMCP.ts)。
工具通过表单注册后,可以这样取得对应的ElementHandle做进一步 DOM 操作:
const tools = page.webmcp.tools(); const searchTool = tools.find(t => t.name === 'search_products'); const formHandle = await searchTool.formElement;formElement返回类型为Promise<ElementHandle<HTMLFormElement> | undefined>:仅在工具由表单声明且表单尚未被移除时才返回有效句柄,命令式注册的工具取值为undefined。若要移除工具,命令式场景调用modelContext上对应的注销 API,声明式场景则把表单从 DOM 中移除——两者都会触发toolsremoved事件,测试用例对此均有覆盖。
一个端到端综合示例
把上文各环节串起来:启动带 Flag 的浏览器 → 打开页面 → 注册/等待工具 → 发现 → 执行 → 观察响应:
import puppeteer from 'puppeteer'; const browser = await puppeteer.launch({ args: ['--enable-features=WebMCP'], }); const page = await browser.newPage(); const toolsAdded = new Promise(resolve => { page.webmcp.once('toolsadded', resolve); }); await page.goto('https://example.com'); // 页面内已注册若干工具 await toolsAdded; const tool = page.webmcp.tools().find(t => t.name === 'calculate_sum'); if (!tool) { throw new Error('tool not found'); } page.webmcp.on('toolresponded', response => { console.log(response.status, response.output ?? response.errorText); }); const result = await tool.execute({a: 5, b: 10}); console.log(result); await browser.close();常见问题与注意事项
- 为什么
page.webmcp.tools()返回空数组?可能原因:Chrome 版本低于 151;启动时未传--enable-features=WebMCP;页面尚未加载完成或页面本身没有注册任何工具。可以注册toolsadded事件做异步等待,避免在toolsadded派发前读取快照。 execute的返回与监听事件重复?这不是重复,而是两种互补的消费方式:execute的 Promise 只解析与你发起的调用匹配的那一次toolresponded;全局toolresponded监听则覆盖页面或浏览器发起的全部调用,适合统一记录。- 取消是尽力而为的:
WebMCP.cancelInvocation出错时错误仅被记录而不抛出,若工具本身无法中断,状态可能不会变为Canceled,业务上需保留超时兜底。 - 受限与实验性:整套 API 属于实验特性,接口可能随 Chrome/WebMCP 标准演进而变化。集成到生产环境前,务必基于锁定的 Chrome 版本回归验证(仓库测试通过独立的带 Flag 浏览器实例执行,见 webmcp.test.ts)。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考