STUDY · Agent UI
Agent-UI 协议解剖 · MCP Apps vs A2UI
当模型不再只吐文本
让 Agent 驱动界面的两条路线:发网页,还是发蓝图
模型输出:token, token, token, …
MCP Apps<html>…</html>发「网页」· 沙箱执行
A2UI{ components: […] }发「蓝图」· 原生渲染
render
可交互的真实界面
Agent-UI 协议解剖 · 知其所以然

当模型不再只吐文本

两个协议在解决同一个难题:模型的本质输出是 token 流,但产品需要按钮、表单与图表。面对「怎么把界面送到屏幕上」,它们给出了截然相反的答案——一个发可执行的网页,一个发声明式的蓝图。读完这篇,你能彻底分清这两条路线,并按场景选对。

覆盖协议MCP Apps · A2UI
视角实现原理 · 取舍 · 选型
阅读约 14 分钟 · 6 章
01

它们都在解决的那个难题

模型只会吐文字,产品却要按钮、表单、图表——这中间差了什么?

大语言模型的本质输出是 token 流——一段纯文本。但真实产品需要的是按钮、表单、图表、可点击的卡片,还要能把用户的点击回传给模型、把模型的进度实时反映到屏幕上。

纯文本和富交互界面之间,横着两道核心鸿沟。MCP Apps 和 A2UI,正是用两种思路去补这两道沟:

缺口 1 · 结构鸿沟界面长什么样?文本没有「这是按钮、那是输入框」的结构信息,需要一种方式让模型描述界面。
缺口 2 · 交互鸿沟点击之后发生什么?界面被点击后,事件必须安全地回传给模型 / 服务端,再触发下一步动作。
关键分野记住这一点,后面全部豁然开朗:两者都在回答「渲染什么」,但思路截然相反——MCP Apps 发一段可执行的网页(HTML/JS),在沙箱里跑A2UI 发一份不可执行的 JSON 蓝图,由客户端原生渲染。一个把灵活性拉满,一个把安全性拉满。
02

两条路线:发网页,还是发蓝图

既然都是「让模型描述一个界面」,两套协议到底差在哪?

差在送来的是什么、由谁来渲染。一条送「可执行的网页」、由沙箱执行;一条送「声明式的蓝图」、由客户端原生渲染。就这一个选择,决定了它们各自的灵活性、安全性和跨平台能力。

MCP Apps · 发「网页」→ 沙箱执行 工具 server ui:// HTML 资源 iframe 沙箱执行 可交互界面 ① 点击 →   postMessage(JSON-RPC)回调工具 —— 可审计、可授权 A2UI · 发「蓝图」→ 原生渲染 Agent JSON 蓝图 客户端组件库 原生控件 ② 输入 →   JSON Pointer 双向绑定,数据回写、组件自动刷新 —— 绝不执行代码
图 1 · 同一个目标、两条管线:上行把界面当代码送进沙箱跑,下行把界面当数据交给客户端渲染。
路线 A · Web 路线MCP Apps
发「网页」:一段可执行的 HTML / JS
服务端把界面作为 UI 资源送来,宿主在双层 iframe 沙箱里执行。灵活、能复用现成前端代码;代价是本质是 Web,要靠沙箱兜安全。
路线 B · 原生路线A2UI
发「蓝图」:一份不可执行的纯 JSON
Agent 送来声明式 JSON,客户端用本地组件库渲染成原生控件。安全(绝不执行模型生成的代码)、真·跨平台;代价是只能用客户端预置的组件。
一句话同一个目标——把界面送到用户面前——MCP Apps 把渲染能力交给服务端的代码,A2UI 把渲染能力留在客户端。下面两章分别拆开它们的实现原理。
03
Anthropic · OpenAI · MCP-UI 社区 · SEP-1865

MCP Apps:发一段「网页」,沙箱里跑

怎么让一个工具返回的不是一句话,而是一整块能点能填的界面?

核心思路:把界面当成 MCP 的一种资源。工具不再只返回文本,而是返回一段预先声明好的 HTML,宿主在双层 iframe 沙箱里渲染它,界面再通过 JSON-RPC over postMessage 反过来调用工具。

实现原理:六步生命周期

注意「声明」和「数据」是分开的——这是它做缓存和安全审查的根基。

  1. 预声明 UI 资源:服务端用 ui:// 协议把 HTML 模板注册成静态资源,工具定义里用 _meta.ui.resourceUri 指向它。结构(模板)和动态数据(工具结果)就此解耦。
  2. 宿主预取与审查:因为模板是静态的,宿主可以在工具真正执行之前就拉取、缓存、并对 HTML 做安全审查。
  3. 沙箱渲染:工具执行后,宿主把 HTML 放进一个受限权限的 iframe 渲染。采用双层 iframe:外层(sandbox proxy)把内层(guest UI)和宿主页面隔离开。
  4. 初始化握手:内层 UI 用标准 @modelcontextprotocol/sdk 与宿主建立双向通道(ui/initialize)。
  5. 回调工具:UI 里点了按钮要干活时,通过 postMessage 发一条 JSON-RPC 2.0 消息给宿主请求调用工具——所有交互都结构化、可审计、可记录。
  6. 用户授权:宿主可以要求对「UI 发起的工具调用」做显式批准,敏感操作执行前留有人类监督这道闸。
交互 · 一次 UI 调用如何穿过双层沙箱
① 宿主页面 host
② 外层 sandbox proxy(隔离)
③ 内层 guest UI(你的 HTML)
天气小组件 · guest UI 想调用 get_weather(city:"上海")
宿主:等待 UI 事件…
收到 postMessage:JSON-RPC tools/call
? 这是敏感调用,请求用户授权
已批准,执行 get_weather
把结果回传给 guest UI(晴 24°C)

关键点:guest UI 永远碰不到宿主页面,消息只能以 JSON-RPC 形式经外层代理转发——所以可被记录、可被审查、可被授权拦截。

代码长什么样

server.ts — 声明 UI 资源 + 工具绑定TypeScript
// 1) 把 HTML 注册成 ui:// 资源(静态、可预取审查)
server.registerResource("ui://charts/weather", {
  mimeType: "text/html+skybridge",
  text: "<div id=app>…</div><script>…</script>"
});

// 2) 工具用 _meta 指向那段 UI
server.registerTool("get_weather", {
  inputSchema: { city: z.string() },
  _meta: { "ui.resourceUri": "ui://charts/weather" }   // ← 关键
}, async ({ city }) => ({
  content: [{ type: "text", text: `${city} 24°C 晴` }]   // 不支持 UI 的宿主仍看到纯文本(优雅降级)
}));

它的取舍

04
Google · 开源 Apache-2.0 · 规范 v0.9

A2UI:发一份 JSON「蓝图」,客户端原生渲染

连一行 HTML 都不传,怎么照样渲染出原生界面?

核心思路与 MCP Apps 正相反:绝不传可执行代码。Agent 只发一份纯 JSON(MIME 类型 application/json+a2ui),描述「界面由哪些组件构成、数据是什么、怎么绑定」。客户端拿自己本地的组件库把它渲染成原生控件。

实现原理:三块拼图

① 组件树是一张「扁平邻接表」,不是嵌套结构

组件不是嵌套的 JSON,而是一张扁平列表 + ID 引用来表达父子关系。为什么?LLM 生成扁平列表比生成深层嵌套更可靠(少出括号不匹配的错),且支持渐进式渲染——组件可以乱序、分批到达,先到先渲染。

② 数据与界面分离,用 JSON Pointer 绑定

组件不写死内容,而是绑定到数据模型里的路径,比如 /user/profile/name数据模型一变,绑定的组件自动刷新——不用重发整棵组件树。这就是它高效的来源。

③ 输入组件是双向绑定

展示型组件是单向的;而输入框这类组件与数据模型建立双向绑定——用户一输入,客户端数据模型立刻更新,无需往返服务端。

交互 · 左边是 Agent 发来的 JSON,右边是客户端原生渲染
Agent 发来的 A2UI(数据模型)
客户端原生渲染
点赞计数器
0
数据模型 /name(空)

点「+」:只有数据模型 /count 变了,组件树(结构)一行没动——这就是「数据驱动、无需重发 UI」。在输入框打字:看左边 /name 实时回写——这就是双向绑定。

代码长什么样

Agent 输出的 A2UI 消息(节选)application/json+a2ui
{
  "components": [                       // 扁平邻接表,靠 id 连父子
    { "id":"root", "type":"Column", "children":["title","count"] },
    { "id":"title", "type":"Text", "text":"点赞计数器" },
    { "id":"count", "type":"Text", "text":{ "$bind":"/count" } }  // ← JSON Pointer 绑定
  ],
  "dataModel": { "count": 0, "name": "" }   // 数据与界面分离
}

它的取舍

05

并排对比

所以归根结底,它俩的取舍是什么?

两个协议放在一张表里,差异一目了然——本质上是「灵活 vs 安全」「Web vs 原生」的取舍。

维度MCP Apps / MCP-UIA2UI
主导方Anthropic + OpenAI + 社区Google(Apache-2.0)
传输内容预构建的 HTML/CSS/JS(可执行)纯声明式 JSON 蓝图(不可执行)
谁来渲染宿主在双层 iframe 沙箱里执行客户端组件库渲染成原生控件
核心机制ui:// 资源 + JSON-RPC over postMessage扁平邻接表 + JSON Pointer 数据绑定
交互回传postMessage 回调工具 + 用户授权门JSON Pointer 双向绑定,数据回写
安全模型iframe 沙箱隔离 + 授权拦截不传可执行代码,天然无注入风险
跨平台Web 为主(原生需 WebView)真·原生跨平台
最适合复杂自定义界面、复用 Web 代码原生多端、高安全、标准化 UI
06

那我该用哪个?

具体到我的项目,到底走哪条路线?

一句话定调:要灵活、要复用现成 Web,选 MCP Apps;要安全、要上原生多端,选 A2UI。

Agent 已在 MCP 生态里,想让某个工具顺手返回一块复杂交互界面
MCP Apps
UI 复杂、要图表 / 动画 / 自定义逻辑,客户端主要是桌面 / Web
MCP Apps
要复用大量现成前端组件、第三方 Web SDK
MCP Apps
要覆盖原生移动端,且不想在客户端执行模型生成的任意代码
A2UI
界面形态相对标准(表单 / 卡片 / 列表),要对齐已有设计系统
A2UI
对安全 / 合规要求高,绝不能跑模型生成的代码
A2UI
本质这不是「谁取代谁」,而是同一道题的两种最优解:把界面当代码(灵活,但要靠沙箱兜安全),还是当数据(安全,但受限于客户端组件集)。看清你的项目更怕「不够灵活」还是「不够安全」,答案就出来了。
··

原始资料

本页技术细节均来自以下权威来源,建议深入阅读规范原文。

阅读 · 右上角 ◐ 切换深浅色 · STUDY 技术笔记