跳到主要内容

MCP Apps (ext-apps) — 架构与原理

30 秒导读: MCP 工具默认只能返回文本和结构化数据。MCP Apps 这个扩展让一个工具能额外声明一份 HTML UI 资源;当模型调用该工具时,聊天客户端(Claude、ChatGPT、VS Code…)把这份 HTML 渲染成一个沙箱 iframe 内联在对话里——图表、表单、画布、播放器都行。这个仓库(@modelcontextprotocol/ext-apps)是实现这套协议的官方 TypeScript SDK。

1. 这是什么(零基础也能懂)

一句话定义: MCP Apps 是 MCP(Model Context Protocol)的一个扩展,在“工具返回文本”之上加了一条“工具可以返回一块会动的网页 UI”。

它解决谁的什么问题。 假设你写了一个 MCP 服务器,有个 get-weather 工具。在纯 MCP 里,工具调用完只能往对话里塞一段文字(“东京,晴,24°C”)。但天气更适合用一张带图标、可切换城市的卡片来展示。MCP Apps 让你给这个工具配一份 HTML,客户端就把这张卡片直接画在对话气泡里,用户还能点它、它还能反过来再调工具拿新数据。

三个角色,先记住名字(后面每一章都在讲它们):

角色中文它是谁在 SDK 里用哪个类
View视图跑在 iframe 里的那份 UI(你的 HTML+JS)App(src/app.ts)
Host宿主嵌着 iframe 的聊天客户端AppBridge(src/app-bridge.ts)
Server服务器提供工具和 UI 资源的 MCP 后端registerAppTool 等(src/server/index.ts)

一句话直觉/类比: 把 View 想成一个没有网络、被关在玻璃罩里的网页。它什么外部请求都发不出去,只能隔着玻璃跟 Host 喊话(postMessage);需要数据时,喊“帮我调一下服务器的工具”,由 Host 代它去真正的 MCP 服务器跑一趟,再把结果递进来。玻璃罩 = 沙箱 iframe;喊话 = JSON-RPC over postMessage;代跑 = Host 的请求转发。

用起来什么样(最小真实示例)。 服务器端把工具和 UI 资源绑在一起(examples/quickstart/server.ts):

// 示意,改写自 examples/quickstart/server.ts
const resourceUri = "ui://get-time/mcp-app.html"; // UI 资源的地址

registerAppTool(server, "get-time", {
description: "Returns the current server time.",
_meta: { ui: { resourceUri } }, // 把工具链到这份 UI
}, async () => {
return { content: [{ type: "text", text: new Date().toISOString() }] };
});

registerAppResource(server, resourceUri, resourceUri, {}, async () => ({
contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
})); // 返回那份 HTML

UI 端(跑在 iframe 里,examples/quickstart/src/mcp-app.ts):

// 示意,改写自 examples/quickstart/src/mcp-app.ts
const app = new App({ name: "Get Time App", version: "1.0.0" });

app.ontoolresult = (result) => { // 收到工具结果就刷新界面
const time = result.content?.find((c) => c.type === "text")?.text;
document.getElementById("server-time")!.textContent = time ?? "[ERROR]";
};

app.connect(); // 与 Host 握手

本节到此不碰底层。你只要记住:一个工具 + 一份 HTML,客户端就把 UI 内联渲染出来,并保持 UI 和服务器双向通信。

2. 顶层全景(它大概怎么转)

数据流主线

核心是一条三跳链路:View ↔ Host ↔ MCP Server。View 永远不直接连服务器,所有跨边界请求都被 Host 代理。

postMessage(JSON-RPC) MCP 客户端连接
┌────────────┐ ───────────────▶ ┌──────────┐ ──────────▶ ┌────────────┐
│ View │ ui/initialize │ Host │ tools/call │ MCP Server │
│ (iframe) │ tools/call(代调) │AppBridge │ resources/ │ 你的后端 │
│ App 类 │ ◀─────────────── │ │ ◀────────── │ │
└────────────┘ tool-input/result └──────────┘ 结果 └────────────┘
玻璃罩(沙箱) 代理人 真正干活的

怎么读:左边是关在沙箱里的 UI,中间是聊天客户端兼“翻译/守门人”,右边是真正的服务器。 箭头向右是 View 发起的请求(被 Host 转发),箭头向左是 Host 推给 View 的通知(工具输入、结果、主题变化)。

谁负责什么

部件干什么文件关键符号
AppView 的入口类,握手 + 收发消息 + 代调服务器工具src/app.tsAppApp.connect
AppBridgeHost 的代理类,应答握手 + 转发 MCP 请求 + 推送工具数据src/app-bridge.tsAppBridgeAppBridge.connect
PostMessageTransportiframe↔parent 的 JSON-RPC 传输层src/message-transport.tsPostMessageTransport
ProtocolWithEvents给 MCP Protocol 加 DOM 风格事件系统(on* + addEventListener)src/events.tsProtocolWithEvents
server helpers让服务器把 UI 资源绑到工具上src/server/index.tsregisterAppToolregisterAppResourcegetToolUiResourceUri
协议类型所有 ui/* 消息的 TS 类型(Zod schema 由它生成)src/spec.types.tsMcpUiInitializeRequestMcpUiHostContext
样式/React主题变量、自动尺寸、useApp 钩子src/styles.tssrc/react/applyHostStyleVariablesuseApp

走一遍生命周期(高层)

  1. 发现: 服务器在 tools/list 里给某工具挂上 _meta.ui.resourceUri,在 resources/list 里登记那个 ui:// 资源。Host 看到带 UI 元数据的工具,就知道“这个工具有界面”。
  2. 渲染: 模型调用该工具;Host 读出资源 URI,取来 HTML,塞进一个沙箱 iframe。
  3. 握手: iframe 里的 Appconnect(),发 ui/initialize,拿回 Host 能力与上下文(主题、locale、工具信息…),再发 ui/notifications/initialized
  4. 喂数据: Host 在握手完成后,推 ui/notifications/tool-input(完整参数)和 ui/notifications/tool-result(执行结果)给 View。
  5. 交互: 用户点界面 → View 通过 app.callServerTool() 让 Host 代调服务器工具 / 通过 app.sendMessage() 往对话里发消息。
  6. 拆除: Host 卸载 iframe 前发 ui/resource-teardown,给 View 一次保存状态的机会。

3. 阅读地图(建议顺序)

这套文档按“先协议、再两端、再服务器、再安全”的顺序由浅入深。只想懂大概,读完本页即可;要动手写 App/Host/Server,按下面顺序读对应章。

  1. 01-protocol-and-handshake.md —— 一切的地基:消息怎么传、握手怎么走、消息分几类。所有人先读这章。
  2. 02-view-app-class.md —— 写 UI 的人读:App 类的事件、代调工具、发消息、自动尺寸。
  3. 03-host-appbridge.md —— 写聊天客户端的人读:AppBridge 怎么当代理和守门人。
  4. 04-server-helpers.md —— 写 MCP 服务器的人读:怎么把 UI 绑到工具上、可见性、CSP 元数据。
  5. 05-security-and-sandbox.md —— 想懂“为什么安全”的人读:双层 iframe、CSP、权限、能力协商。
  6. 06-cleverness-boundaries-map.md —— 精华、边界、横向对比、代码地图(导航索引)。

关于版本:package.json:8"version": "1.7.4""license": "MIT",而 README 徽章写 Apache 2.0——以仓库内 package.json 为准时是 MIT,二者不一致(未深究)。协议版本固定为 "2026-01-26"(spec.types.ts:29 LATEST_PROTOCOL_VERSION)。