Puppeteer 与 WebMCP 实战指南:让网页工具被浏览器与 LLM Agent 发现与调用
2026/9/8 18:10:23 网站建设 项目流程

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,需要同时满足两个条件:

  1. Chrome 151+:浏览器必须支持 WebMCP CDP 域(WebMCP.*),所有工具发现与调用最终都经由该 CDP 域完成。
  2. 开启实验特性 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):

事件载荷类型触发时机
toolsaddedWebMCPToolsAddedEvent(含tools: WebMCPTool[]页面注册新工具(命令式或声明式)
toolsremovedWebMCPToolsRemovedEvent(含tools: WebMCPTool[]页面注销工具,或工具所在 frame 的执行上下文被销毁
toolinvokedWebMCPToolCall(含idtoolinput某次工具调用开始(调用方是页面或浏览器)
toolrespondedWebMCPToolCallResult工具调用完成、出错或取消

在实现内部,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—— 可选注解(测试中可见readOnlyuntrustedContentautosubmit等能力位);
  • 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,并逐项校验namedescriptioninputSchemaannotationsframeformElement

执行工具

通过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):

字段类型说明
idstring调用标识符
call?WebMCPToolCall对应的调用对象(若可用)
statusWebMCP.InvocationStatus调用状态(如Completed/Canceled/Error
output?any工具输出;仅当statusCompleted时存在
errorText?string错误文本
exception?Runtime.RemoteObject工具抛出 JS 异常时的异常对象

关于失败场景,仓库测试给出了两个可验证的事实:当工具的execute函数直接throw时,响应状态为Erroroutputundefinedexception中携带包含错误消息的description;而面向“工具不存在/参数错误”等错误则通过errorText承载(webmcp.test.ts 中分别断言了异常与 errorText 两种分支)。因此可靠的业务代码应同时检查statuserrorTextexception三种信号,而不只判断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 侧同样能收到toolinvokedtoolresponded。仓库测试正是利用这一能力验证“页面发起调用 → Puppeteer 收到事件 →response.output与注册时execute的返回值一致”的闭环(webmcp.test.ts)。

在页面中注册工具

工具既可命令式注册(JavaScript),也可声明式注册(带特定属性的 HTML 表单)。Puppeteer 页面本身就是一个 WebMCP 页面,因此这两种方式都可直接在页面上下文中演示。

命令式注册

通过page.evaluate在页面内调用 Web 平台侧的document.modelContext?.registerTool(...),注册时需要提供namedescriptioninputSchemaexecute

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 === trueannotations.untrustedContent === true,证明注解信息会原样从页面透传到WebMCPTool.annotations

声明式注册

WebMCP 也能发现带有特定属性的 HTML 表单并把它转成工具。最基本的属性是toolnametooldescription,测试中还出现了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,但有两个区别于命令式工具的可验证特征:

  1. inputSchema会由浏览器根据表单输入字段自动推导(测试中断言空表单对应{type: 'object', properties: {}, required: []});
  2. tool.locationundefined,而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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询