跳到主要内容

WebMCP — 命令式 API 与工具生命周期

本章讲 WebMCP 的核心:网页怎么用 document.modelContext.registerTool() 登记一个工具,一次工具调用从头到尾经历哪五步,以及 registerTool 规范算法每一步在校验/防什么。

1. 入口:每个 Document 一个 modelContext

WebMCP 把入口挂在 Document 上。规范给 Document 加了一个只读属性 modelContext,且仅在**安全上下文(SecureContext,即 HTTPS / localhost)**可用:

// index.bs:294-298
partial interface Document {
[SecureContext, SameObject] readonly attribute ModelContext modelContext;
};

每个 Document 一创建,就被赋予一个关联的 ModelContext 对象(index.bs:285-290,associated ModelContext)。这个对象内部挂着一个 model context struct,里面最关键的就是一张 tool map——「工具名 → 工具定义」的映射(index.bs:125-131,model context/tool map)。

一句话:工具不是全局的,而是每个文档各存一份。这为后面的跨源隔离(§03)埋了伏笔。

2. 工具定义长什么样

开发者传给 registerTool 的是一个 ModelContextTool 字典(index.bs:470-488):

// index.bs:471-480
dictionary ModelContextTool {
required DOMString name; // 唯一标识,agent 用它指名调用
USVString title; // 给人看的标题(可本地化)
required DOMString description; // 自然语言描述,agent 据此判断何时用
object inputSchema; // JSON Schema,描述参数
required ToolExecuteCallback execute; // 被调用时跑的回调
ToolAnnotations annotations; // 可选元数据(见下)
};

execute 的签名是 Promise<any> (object input)(index.bs:487)——可以是异步的,返回 promise,agent 会等它 resolve 再拿结果(index.bs:517)。

浏览器内部不直接存这个字典,而是把它转成一个 tool definition struct(index.bs:134-182)。两者的差异值得注意:

  • inputSchema 在内部被存成字符串(index.bs:154-156,“input schema :: a string”),即把 JS 对象 JSON.stringify 后的结果。
  • 多了几个内部字段:read-only hintuntrusted content hint(都初始 false,index.bs:174-178)、exposed origins(初始空,index.bs:180-181)。

两个 annotation 是给安全用的(index.bs:482-485529-534):

annotation含义
readOnlyHinttrue = 此工具只读、不改状态,帮 agent 判断「调它安全吗」
untrustedContentHinttrue = 此工具的输出含不可信内容,提示 client 做加固(见 §05)

3. 主线:一次工具调用的五步生命周期

README 把一次工具调用拆成清晰的五步(README.md:292-297):

[1] 注册 Registration 页面调 registerTool() 登记工具

[2] 发现 Discovery agent 向浏览器查询当前活跃工具列表 + schema

[3] 调用 Invocation agent 发起调用,带上匹配 inputSchema 的结构化参数

[4] 执行 Execution 浏览器做中介,用参数调 execute 回调,在页面上跑客户端逻辑

[5] 响应 Response 回调返回结构化结果给 agent,agent 继续与用户协作

重点看「浏览器做中介(mediates)」这一句(README.md:296):agent 从不直接碰页面的函数;它把请求交给浏览器,浏览器才去调 execute。这个中介层是后面所有安全/权限机制能成立的前提。

4. 深入:registerTool 规范算法逐行走读

规范用一段算法精确定义了 registerTool 干什么(index.bs:336-464)。下面挑出每一步在「校验什么 / 防什么」:

第一组:前置守卫(谁能登记)

  1. 文档必须 fully active,否则 reject InvalidStateError(index.bs:343-344)——不能给一个已经不活跃的文档登记工具。
  2. 若 agent cluster 不是 origin-keyed,且当前源 scheme 不是 file,则 reject SecurityError(index.bs:346-349)。
  3. 文档必须被允许使用 tools 权限特性,否则 reject NotAllowedError(index.bs:351-352)——这是 §03 权限策略的落点。

第二组:参数校验(工具本身合法吗)

  1. 同名工具已存在 → reject InvalidStateError(index.bs:360-361):工具名在一个 Document 内必须唯一
  2. namedescription 为空串 → reject(index.bs:363-364)。
  3. name 长度必须 1–128,且只能含 ASCII 字母数字、_-.(index.bs:366-369,同 tool definition/name 定义 index.bs:141-143)。这个 128 字符上限本身也是一条安全缓解(限制注入文本长度,见 §05)。

第三组:序列化 schema

  1. 若有 inputSchema,把它 JSON.stringify 成字符串存起来;序列化抛错就把异常 reject 出去(index.bs:371-376)。规范特意列了两种抛错情形:toJSON 返回 undefinedTypeError、循环引用等由 JSON.stringify 自己抛(index.bs:378-390)。

第四组:读 hint、建 promise、挂 AbortSignal

  1. annotations 读出 readOnlyHintuntrustedContentHint(index.bs:392-396)。
  2. 若传了 options.signal(AbortSignal):已 aborted 就直接 reject;否则注册 abort 步骤——一旦 signal 被 abort,就注销该工具并 reject promise(index.bs:402-411)。这就是 README 里 controller.abort() 注销工具的机制(README.md:288-289)。

这一条是最近才加的:commit bc93f03(“Reject registerTool() promise when signal is aborted”)。

第五组:处理 exposedTo 跨源授权

  1. 若传了 options.exposedTo,逐个 URL 解析:解析失败或源不是 potentially trustworthy(非安全源)就 reject SecurityError;否则把源加进 exposed origins(index.bs:415-425)。详见 §03。

第六组:落库 + 并行通知

  1. 用上面所有字段建一个 tool definition,写进 tool map(index.bs:427-453)。
  2. in parallel 地:通知相关文档发生了 tool change(触发 toolchange 事件),并在 webmcp task source 上排一个任务 resolve promise(index.bs:455-462)。

注意 resolve 是排在任务队列上的,不是同步的——这造成了一个微妙时序,§03 会专门讲。

5. 注销:三种方式

工具怎么被移除?规范定义了 unregister a tool 算法(index.bs:251-272):从 tool map 里删掉该名字,然后 in parallel 通知文档 tool change。触发它的入口有:

  • AbortSignal abort(index.bs:408-409)——最常见,见上文。
  • (声明式)表单被 reset、移除、或 toolname/tooldescription 改变,会取消在途调用并注销工具(详见 §02)。

6. 巧妙之处

schema 当字符串存。 内部把 inputSchema 序列化成 string 而非保留 JS 对象(index.bs:154-156371-376)。好处:跨进程/跨 agent 传递时是稳定的纯文本,且登记时就强制做一次序列化校验(循环引用当场暴露)。

用 AbortSignal 统一生命周期。 注销不另设 API,而是复用 web 平台标准的 AbortController(index.bs:402-411)。一个 controller 可同时管理多个工具的生命周期,符合 web 平台惯例。

128 字符名长上限既是规范也是防线。 name 上限不只是工程约束,安全章明确把它列为对抗 prompt 注入的缓解之一(index.bs:1080-1086)。

7. 边界与局限

  • 发现/调用 API 还没定。 getTools() / executeTool() 在 README 里明写 TODO(README.md:344-345)。本章的「发现」「调用」两步描述的是设计意图,具体接口待定。
  • 仅 SecureContext + Window。 ModelContext 标了 [Exposed=Window, SecureContext](index.bs:312-313),普通页面 worker 不可用(Service Worker 是另一条线,见 §04)。
  • 同名即拒绝,无覆盖语义。 想更新工具得先注销再登记。

8. 代码地图

主题文件符号
Document.modelContext getterindex.bs:294-306modelContextDocument/associated ModelContext
ModelContext 接口index.bs:308-322ModelContextontoolchange
登记算法index.bs:336-464registerTool(tool, options)
工具字典index.bs:470-488ModelContextToolToolExecuteCallbackToolAnnotations
工具定义 structindex.bs:134-182tool definition
注销算法index.bs:251-272unregister a tool
注册选项index.bs:543-556ModelContextRegisterToolOptions(signalexposedTo)