rea-reverse

WebMCP 入门:用 REA 查看网页注册了哪些 AI 工具

基于 REA 6.4.0更新于 约 8 分钟读完
本页目录

有了 WebMCP,网页可以直接递给 AI agent 一份工具清单,比如“搜索我的笔记”、“往购物车里加一件商品”,每个工具都带名字、说明和一份描述输入参数的 JSON Schema。这很方便,但也意味着页面多了一套面向机器的接口,而做评审的人很少会去看它。本文先讲清楚 WebMCP 是什么,再写一个用两种方式注册工具的小 demo 页,最后用 REA(morluto 的开源项目)把页面声明的所有工具列出来,全程不调用其中任何一个。

适合谁读: 准备在自己的应用里接入 WebMCP 的前端开发,以及想在上线前弄清页面到底给 agent 开放了什么的评审和测试人员。

你会学到:

  1. WebMCP 是什么,和普通的 MCP server 有什么不同
  2. 页面注册工具的两种方式:JavaScript 调用和带属性的表单
  3. 怎样启动开启了 WebMCP 的 Chrome,并让 REA 6.4.0 连上去
  4. 逐个字段读懂 discover-webmcp-tools 的输出
  5. 用 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 调用。

下一步