跳到主要内容

Host 侧:AppBridge

本章讲写聊天客户端的人怎么用 AppBridge(src/app-bridge.ts)。它是 View 那座 App 的对端,既当代理又当守门人。

1. AppBridge 是什么:一座桥 + 一个守门人

AppBridge(app-bridge.ts:306)和 App 一样继承 ProtocolWithEvents,但它面向的是一个 View(每个 iframe 一座桥)。架构是四跳:

View ↔ AppBridge ↔ Host 应用 ↔ MCP Server
postMessage (你的代码) MCP 客户端

它干三件事:

  1. 应答握手 —— 回 hostInfo/hostCapabilities/hostContext(_oninitialize,app-bridge.ts:1461)。
  2. 转发 —— 把 View 发来的标准 MCP 请求(tools/callresources/*prompts/*)代发给真正的 MCP 服务器。
  3. 推送 + 守门 —— 主动推工具输入/结果给 View;对 ui/open-linkui/messagesampling 这类请求,Host 可以审、可以改、可以拒。

2. 两种用法:自动转发 vs 手动接管

构造时第一个参数是 MCP Client | null(app-bridge.ts:387),这一个参数决定两种模式:

模式 A:传入 client → 自动转发。 connect() 里读服务器能力,按能力自动给 View 的请求装转发器(app-bridge.ts:1853-1920):

// 示意,核心来自 app-bridge.ts:1861-1875
if (serverCapabilities.tools) {
this.oncalltool = async (params, extra) =>
this._client!.request({ method: "tools/call", params },
CallToolResultSchema, { signal: extra.signal }); // View 的 tools/call → 真服务器
if (serverCapabilities.tools.listChanged)
this._client.setNotificationHandler(ToolListChangedNotificationSchema,
(n) => this.sendToolListChanged(n.params)); // 服务器变更 → 转告 View
}

资源(resources/listresources/readresources/templates/list)和 prompts(prompts/list)同理(app-bridge.ts:1876-1919)。注意: 若传了 client 但服务器能力还没就绪(client 没连完)就 connect,直接抛错(app-bridge.ts:1857-1859)。

模式 B:传 null → 手动接管。 你自己设 bridge.oncalltool = ... 等处理器(app-bridge.ts:381-385 的示例)。适合需要插入审批、缓存、改写的宿主。

3. 守门人:每个 View→Host 请求都能被 Host 审

这是 Host 的核心价值。AppBridge 为每个敏感请求提供 on* 设置器,Host 注册后即可决定放行/拒绝:

View 发的请求Host 的处理器守门点源码
ui/messageonmessage是否把消息加进对话;为防泄露不应回传对话内容app-bridge.ts:645
ui/open-linkonopenlink域名白名单、确认弹窗、直接拒app-bridge.ts:714
ui/download-fileondownloadfile确认后再下载app-bridge.ts:782
sampling/createMessageoncreatesamplingmessage限流、成本控制、人审app-bridge.ts:1084
ui/update-model-contextonupdatemodelcontext存快照供下轮模型用app-bridge.ts:979
ui/request-display-modeonrequestdisplaymode默认回当前模式,可覆盖app-bridge.ts:882

拒绝的惯例是返回 { isError: true }(如 onopenlink 示例,app-bridge.ts:683-701),而非抛错——让 View 能优雅降级。

4. 推送:Host→View 的工具数据流

握手完成后(oninitialized 触发,app-bridge.ts:591),Host 按这个顺序推工具数据:

sendToolInputPartial(...) × 0..n ← 流式参数(可选)
sendToolInput({ arguments }) ← 完整参数(必发一次,恰好一次)
sendToolResult(result) ← 执行结果(必发,除非 View 已关)
或 sendToolCancelled({ reason }) ← 被取消时必发

源码:sendToolInput(app-bridge.ts:1584)、sendToolInputPartial(app-bridge.ts:1620)、sendToolResult(app-bridge.ts:1649)、sendToolCancelled(app-bridge.ts:1686)。文档里把这些 MUST/MAY 约束写得很死:sendToolInput 必须在握手后、恰好一次、且在 sendToolResult 之前(app-bridge.ts:1565-1567)。

5. 上下文变更:setHostContext 的差量推送

要解决的小问题: 用户切了深色模式、窗口变大了——Host 要通知 View,但只该推变了的字段

setHostContext(app-bridge.ts:1524)逐字段和当前上下文比对(用 JSON.stringifydeepEqual,app-bridge.ts:1930),只把改动过的字段打包成 host-context-changed 通知发出去;没变就不发。这样 View 的 onhostcontextchanged 只收到差量(app.ts:450-453 把差量合并进缓存)。

6. 生命周期收尾:teardownResource

Host 卸载 iframe 前应发 ui/resource-teardown(teardownResource,app-bridge.ts:1741),等 View 的 onteardown 完成保存再 iframe.remove()

反向也支持:View 可主动发 ui/notifications/request-teardown,Host 的 onrequestteardown(app-bridge.ts:835)收到后决定是否走同一套 teardown 流程——这样 View 只需实现一个关闭过程(app.ts:1718-1760 说明这一“复用单一关闭逻辑”的设计)。

关键细节 / 坑

  • 握手次序的护栏在 Host 侧也有。 AppBridge 重写 replaceRequestHandler,包一层检查:若在 ui/notifications/initialized 之前收到 host 方法调用,就告警(app-bridge.ts:328-344)——只警告不抛,以兼容宽松宿主。
  • 二次 initialize 容忍 StrictMode。 收到第二次 ui/initialize(React 开发期双挂载)只告警,用最新的 appInfo 覆盖(app-bridge.ts:1466-1473)。
  • onsandboxready / sendSandboxResourceReady 是内部 API,只给 web 宿主的双层沙箱用(见 05 章),普通宿主不碰。

下一步:04-server-helpers.md 看服务器怎么把 UI 绑到工具上。