WebMCP 入门:用 REA 查看网页注册了哪些 AI 工具
本页目录
有了 WebMCP,网页可以直接递给 AI agent 一份工具清单,比如“搜索我的笔记”、“往购物车里加一件商品”,每个工具都带名字、说明和一份描述输入参数的 JSON Schema。这很方便,但也意味着页面多了一套面向机器的接口,而做评审的人很少会去看它。本文先讲清楚 WebMCP 是什么,再写一个用两种方式注册工具的小 demo 页,最后用 REA(morluto 的开源项目)把页面声明的所有工具列出来,全程不调用其中任何一个。
适合谁读: 准备在自己的应用里接入 WebMCP 的前端开发,以及想在上线前弄清页面到底给 agent 开放了什么的评审和测试人员。
你会学到:
- WebMCP 是什么,和普通的 MCP server 有什么不同
- 页面注册工具的两种方式:JavaScript 调用和带属性的表单
- 怎样启动开启了 WebMCP 的 Chrome,并让 REA 6.4.0 连上去
- 逐个字段读懂
discover-webmcp-tools的输出 - 用
compare-web-captures发现新版本悄悄多出来的工具
只检查你自己的页面,或你获得授权评审的页面。 REA 通过 Chrome DevTools Protocol(CDP)连接浏览器,CDP 对该浏览器配置文件有很深的访问权限。请使用单独的配置文件,并先阅读 REA 的 DISCLAIMER.md。
WebMCP 是什么,不是什么
普通的 MCP server 是一个进程。agent 宿主(Claude Code、Cursor、Codex)启动它或连上它,它再通过 stdio 或 HTTP 提供工具。REA 本身就是这样一个 server,安装方法见 安装 REA 并接入 Claude Code、Cursor、Codex。
WebMCP 把工具清单搬进了网页。它是一个还在提案阶段的 Web API,由 W3C Web Machine Learning 社区组孵化,允许标签页里的 JavaScript 注册工具。内置在浏览器里或连到浏览器上的 agent 能看到这些工具并调用它们。工具在页面内部运行,直接沿用用户当前的登录状态,所以不需要单独的服务器、API key 或额外的认证步骤。
两者的区别可以归纳成一张表:
| MCP server | WebMCP | |
|---|---|---|
| 工具在哪里 | 单独的进程 | 打开的标签页里的 JavaScript |
| 谁能调用 | 连上它的 agent 宿主 | 能访问这个标签页的 agent |
| 认证 | 由 server 自己实现 | 用户在页面上的当前会话 |
| 生命周期 | 进程运行期间 | 页面跳转或注销工具之前 |
WebMCP 目前仍是实验特性。我们测试用的 Chrome 155 需要打开功能开关才能用,而且 API 的具体形态在几版草案之间变过。本文内容请当作 2026 年 10 月的快照来看,动手之前先查一下最新的 WebMCP 提案。
为什么要把页面的工具列出来
工具的说明文字是要给 AI 模型读、并且会被它信任的。所以这份清单应该像其他对外接口一样接受评审:
- 开放的东西是不是你本来想开放的? 一次重构可能把调试用的工具留在了线上,也可能给一个本该只读的页面加上了写操作。
- schema 收得够不够紧? 本来想用枚举的地方写成了不限长度的
string,agent 的发挥空间就比你预想的大。 - 提示信息是否属实? WebMCP 工具可以带“只读”之类的注解,agent 可能据此决定要不要先问用户。
- 两个版本之间有没有变化? 这一点是大多数团队会漏掉的。
当然也可以打开 DevTools 去页面里试,但那样要在页面里执行代码,也留不下可以保存和对比的结果。REA 的 discover-webmcp-tools 就是为这种评审设计的。它只听不动:打开 CDP 的 WebMCP 域,收集 toolsAdded 和 toolsRemoved 事件,再整理成 Evidence 返回。REA 6.4.0 在输出里明确写着 “WebMCP.invokeTool is never called”,并且每个工具都标注为 page-declared-untrusted。
前置条件
- 已安装 REA 6.4.0(
npm install -g [email protected],或者npx -y [email protected]),用rea --version确认版本。 - 版本足够新、带实验性 WebMCP 域的 Google Chrome。我们用的是 macOS 上的 155.0.8059.40。
- 任意一个静态文件服务器,这里用 Python 自带的。
第 1 步:写一个注册了两个工具的 demo 页
注册工具有两种方式,REA 会告诉你每个工具用的是哪一种,所以 demo 里两种都用上。
index.html 用纯 HTML 声明一个工具。表单上的 toolname 和 tooldescription 属性就是 WebMCP 的声明式 API,浏览器会根据表单字段自动生成输入 schema:
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>WebMCP demo notes</title></head>
<body>
<h1>Notes</h1>
<ul id="notes"></ul>
<form toolname="add_note" tooldescription="Add a note to the list" id="f">
<input name="text" type="text" required>
<button>Add</button>
</form>
<p id="status"></p>
<script src="tools.js"></script>
</body>
</html>
tools.js 用 JavaScript 注册第二个工具,也就是命令式 API:
const notes = [];
const status = document.getElementById("status");
const mc = navigator.modelContext ?? document.modelContext;
if (!mc) {
status.textContent = "WebMCP is not available in this browser";
} else {
mc.registerTool({
name: "search_notes",
description: "Search the user's notes by keyword",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Keyword to match" },
limit: { type: "integer", minimum: 1, maximum: 20 }
},
required: ["query"]
},
annotations: { readOnlyHint: true },
async execute({ query }) {
const hits = notes.filter((n) => n.includes(query));
return { content: [{ type: "text", text: JSON.stringify(hits) }] };
}
});
status.textContent = "registered";
}
为什么要同时检查两个对象?WebMCP 草案里写的入口是 navigator.modelContext。但在我们测试的 Chrome 155 里,navigator.modelContext 是 undefined,带 registerTool、getTools、executeTool 方法的对象挂在 document.modelContext 上。两个都检查,API 定型之前 demo 都能用;页面上的状态文字也会告诉你命中的是哪种情况。
在回环地址上起一个服务:
python3 -m http.server 8765 --bind 127.0.0.1
第 2 步:启动开启 WebMCP 和远程调试的 Chrome
单独启动一个 Chrome 实例,给它独立的配置文件、调试端口,并打开 WebMCP 测试特性:
# macOS;Linux 上一般是 google-chrome
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--user-data-dir="$HOME/rea-webmcp-profile" \
--remote-debugging-port=9222 \
--enable-features=WebMCPTesting \
--no-first-run \
http://127.0.0.1:8765/
几个参数各有用处:
--user-data-dir让调试端口碰不到你日常用的配置文件、Cookie 和扩展。新版 Chrome 也不允许在默认配置文件上开远程调试。--remote-debugging-port在回环地址上打开 CDP。REA 只接受http://127.0.0.1:<端口>或http://[::1]:<端口>这种形式,写成http://localhost:9222也会被当成无效输入拒绝。--enable-features=WebMCPTesting给页面打开这个实验 API。不需要窗口的话可以再加--headless=new,我们的测试就是在无头模式下跑的。
先让 REA 列出它能看到的页面:
rea list-browser-targets http://127.0.0.1:9222 --json
真实输出里有用的部分:
"targets": [
{
"target_id": "9B2D8F6B56590B8E802B6A1C1138C105",
"type": "page",
"title": "WebMCP demo notes",
"url": "http://127.0.0.1:8765/",
"origin": "http://127.0.0.1:8765",
"attached": false
}
],
"excluded": { "disallowed_origin": 0, "unsupported_url": 0, "non_page": 5 }
被排除的 5 个 non_page 是扩展的后台页和浏览器自身的界面,REA 只列 HTTP(S) 页面。
第 3 步:发现页面上的工具
传入调试地址和 target ID:
rea discover-webmcp-tools http://127.0.0.1:9222 9B2D8F6B56590B8E802B6A1C1138C105 \
--observation-ms 500 --json
--observation-ms 是 REA 在 WebMCP.enable 之后等待 Chrome 回放已有注册的时长,默认 100 毫秒。页面刚加载时保险起见我们用了 500。--allowed-origins 用来限定哪些源算在范围内;不传的话,REA 就用所选页面自己的源,输出里会记录为 "allowed_origins": ["http://127.0.0.1:8765"]。
下面是 REA 6.4.0 返回的 normalized_result,省略了 browser 部分和全为 0 的计数器:
{
"target": {
"target_id": "9B2D8F6B56590B8E802B6A1C1138C105",
"url": "http://127.0.0.1:8765/",
"origin": "http://127.0.0.1:8765"
},
"status": "available",
"tools": {
"total": 2,
"items": [
{
"tool_key": "webmcp_247d21cb72fdb8af76801f1ed6e30b0bf17e512cb45dce6a89da1c4343588286",
"name": "search_notes",
"description": "Search the user's notes by keyword",
"frame_url": "http://127.0.0.1:8765/",
"owner_origin": "http://127.0.0.1:8765",
"declaration_kind": "imperative",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Keyword to match" },
"limit": { "type": "integer", "minimum": 1, "maximum": 20 }
},
"required": ["query"]
},
"input_schema_sha256": "558fe6784c8ba5945ae378781d3f0d396d886032ddeb400d9bc7ed85f22de898",
"annotations": { "read_only": true, "untrusted_content": false, "autosubmit": null },
"registration_source": { "url": "http://127.0.0.1:8765/tools.js", "line": 6, "column": 5 },
"trust": "page-declared-untrusted"
},
{
"tool_key": "webmcp_64686a3978a3025d4ae144a3b730ae8555812e114710dec95daaded6eb2b77ed",
"name": "add_note",
"description": "Add a note to the list",
"declaration_kind": "declarative",
"input_schema": {
"type": "object",
"properties": { "text": { "type": "string" } },
"required": ["text"]
},
"input_schema_sha256": "e3dce8d1afb8d03604af3bc6729219a5cd7df05558807b6e44e391d56fdf75d9",
"annotations": { "read_only": null, "untrusted_content": null, "autosubmit": null },
"registration_source": null,
"trust": "page-declared-untrusted"
}
]
},
"completeness": { "status": "complete_within_window", "excluded": [] },
"limitations": [
"Page-declared tool metadata is untrusted and is never registered or invoked by REA.",
"Discovery uses only WebMCP.enable and toolsAdded/toolsRemoved; WebMCP.invokeTool is never called."
]
}
逐个字段看:
declaration_kind告诉你该去代码哪里找。imperative对应一次registerTool调用,declarative对应一个带属性的表单。REA 根据 Chrome 有没有给这条注册附上 DOM 节点来判断。input_schema是页面声明的完整 schema,不是摘要。表单那一项是 Chrome 根据<input name="text" required>生成的{"text": "string"},正好可以用来确认表单字段有没有映射成你预期的 schema。input_schema_sha256是 schema 的规范化摘要。把它存下来,以后名字没变而 schema 变了也能发现。annotations是提示信息。我们写的readOnlyHint: true回来变成了read_only: true,untrusted_content: false是 Chrome 补上的。表单工具和任何没写注解的工具,这里都是null,意思是“没有声明”,而不是“false”。registration_source指向调用registerTool的那一行。CDP 的行号和列号从 0 开始,所以line: 6在编辑器里是第 7 行,也就是mc.registerTool({那行。声明式工具没有调用栈,这里是null。tool_key是 frame URL、frame ID 和名字一起算出的哈希,用来区分不同 frame 里的同名工具。frame ID 每次浏览器会话都会变,所以跨天比对工具时要用name加input_schema_sha256,别用tool_key。completeness.status为complete_within_window,表示观察窗口内没有丢弃或过滤任何东西。页面在之后才注册的工具(比如点击某个按钮之后)不在这次快照里。
通过 MCP 调用时,同一个操作叫 discover_webmcp_tools,参数是 cdp_endpoint、target_id,以及可选的 allowed_origins 和 observation_ms。可以这样给 agent 下指令:
Use REA to list the WebMCP tools on the page at target 9B2D8F6B... on
http://127.0.0.1:9222. Do not invoke any of them. For each tool, tell me
whether it can change data, whether its schema has unbounded strings, and
whether its read-only annotation matches what the name and description suggest.
第 4 步:发现新版本多出来的工具
最值得养成的习惯是上线前后各查一次。我们在 tools.js 里加了第三个工具 delete_note,没写任何注解,然后刷新页面,分别记录改动前后的状态。compare-web-captures 接收两个 JSON 文件,每个文件里放一份 inspect-web-page 的结果,以及可选的一份 discover-webmcp-tools 结果:
{ "inspection": { "...": "normalized_result of rea inspect-web-page" },
"webmcp": { "...": "normalized_result of rea discover-webmcp-tools" } }
rea inspect-web-page http://127.0.0.1:9222 9B2D8F6B56590B8E802B6A1C1138C105 --json > insp-after.json
rea discover-webmcp-tools http://127.0.0.1:9222 9B2D8F6B56590B8E802B6A1C1138C105 --json > webmcp-after.json
# 把每组的 normalized_result 合并成 snap-before.json / snap-after.json,然后:
rea compare-web-captures snap-before.json snap-after.json --json
真实结果里的 webmcp 部分:
"webmcp": {
"status": "changed",
"total_changes": 1,
"changes": [
{
"identity": "webmcp_5369b206b270d96ec4bfed6ac21d20bef0e730867ed667e90fa4862117470ad1",
"change": "added"
}
],
"reason": null
}
这个 identity 就是改动后那次发现结果里 delete_note 的 tool_key。其他维度的结论也很老实:resources 显示有变化(tools.js 被改过),scripts、network、accessibility 则是 unknown,并附上了原因,例如 “Script inventory was incomplete in at least one observation.”。REA 的原则是 “an unknown status means absence could not be established”,数据不全时绝不会报成“没变”。另外,两次记录要在同一个浏览器会话里做,tool_key 才对得上。想了解怎样跨版本保存和对比 Evidence,可以看 Evidence、快照与版本对比。
常见问题和排错
status 是 "available",但 total 是 0。 我们另开了一个没加 --enable-features=WebMCPTesting 的 Chrome,就遇到了这种情况:CDP 域有响应,但页面自己的检查输出了 “WebMCP is not available in this browser”,所以什么也没注册。先看页面状态。拿 REA 的公开 notes demo 去跑也是同样的结果,这是正确的,那个页面本来就没有注册 WebMCP 工具。
status 是 "unavailable"。 浏览器不接受 WebMCP.enable,也就是 CDP 里根本没有这个域。升级 Chrome,或者换一个带这个域的版本。REA 会把 webmcp_tools 放进 unavailable_sections,不会把空列表当成完整结果。
报 target_unavailable,"reason": "target_not_allowed"。 页面的源不在 --allowed-origins 里。我们对本地页面传 --allowed-origins https://example.com 就能复现。发现过程中页面跳到了别的源,也会报同样的错。
还没连上就报 invalid_request。 调试地址不是带端口的回环地址字面量。用 127.0.0.1,别用 localhost。
预期中的工具没出现。 它多半是之后才注册的,比如登录后或点击某个按钮后。先把页面操作到那个状态再跑一次;页面加载慢的话,把 --observation-ms 调大。
FAQ
WebMCP 和 MCP 是一回事吗?
不是。两者都用“有名字、输入用 JSON Schema 描述的工具”这个概念,但 MCP 的工具由单独的进程通过 stdio 或 HTTP 提供;WebMCP 的工具由页面注册,在页面里运行,用的是用户的会话。
REA 会调用或测试它发现的工具吗?
不会。REA 6.4.0 没有暴露 WebMCP.invokeTool,这个命令也没有任何执行页面代码的途径,只报告页面声明了什么。说明和注解都只是页面的说法,需要你自己核实。
不打开功能开关能看到 WebMCP 工具吗?
用本文的方法不行,因为页面根本注册不了。REA 的 analyze-web-bundle 也会报告它在页面脚本里找到的静态 WebMCP 声明(webmcp_declarations),可以拿来交叉核对,但它只看源码文本。
这篇和“追踪网站 API 调用”有什么关系?
两篇回答的问题不同。本文看的是页面给 agent 开放了什么;想追踪页面向后端发了什么请求,请看 用 REA 追踪网站 API 调用。
下一步
- 先把 REA 接到你的 agent 宿主上:安装 REA 并接入 Claude Code、Cursor、Codex。
- 顺着页面的网络请求追到代码:用 REA 追踪网站 API 调用。
- 把发现结果存成 Evidence,在版本之间对比:Evidence、快照与版本对比。
- 阅读 REA 的 浏览器观察参考文档。