首页
/ Puppeteer 与 WebMCP 实战指南:让网页工具被浏览器与 LLM Agent 发现与调用

Puppeteer 与 WebMCP 实战指南:让网页工具被浏览器与 LLM Agent 发现与调用

2026-09-07 17:26:29作者:管翌锬

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

事件 载荷类型 触发时机
toolsadded WebMCPToolsAddedEvent(含 tools: WebMCPTool[] 页面注册新工具(命令式或声明式)
toolsremoved WebMCPToolsRemovedEvent(含 tools: WebMCPTool[] 页面注销工具,或工具所在 frame 的执行上下文被销毁
toolinvoked WebMCPToolCall(含 idtoolinput 某次工具调用开始(调用方是页面或浏览器)
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 —— 可选注解(测试中可见 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) 内部执行两步:先通过 CDP WebMCP.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 工具输出;仅当 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)。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391