工具调用不再只能返回文字——服务器可以把仪表盘、表单、可视化直接渲染进对话里。这是 MCP 首个官方扩展(SEP-1865),已被 Claude、ChatGPT、VS Code、Goose 支持,并作为官方扩展纳入 2026-07-28 新规范。
MCP 让模型能调用外部工具,但工具的返回一直只有文字和结构化数据。用户想筛选一下表格、点开一条记录,都得再打一轮字。MCP Apps 补上了这块:MCP 服务器可以随工具附带一份 HTML 界面,宿主(Claude、ChatGPT 等)把它渲染在对话内的沙箱 iframe 里,用户直接点击交互,界面再通过标准 JSON-RPC 与宿主、模型双向沟通。
数据来源:MCP 官方博客(2026-01-26)、GitHub ext-apps 仓库页面。
先说痛点。MCP 把模型连上了数据和工具,但官方博客里举了一个很典型的例子:一个查数据库的工具返回几百行数据,模型能做摘要,可用户真正想做的是探索——按列排序、按日期筛、点进某条记录。纯文本模式下,每个动作都要再发一条消息:「只看上周的」「按收入排」「第 47 行是什么」。能用,但慢。
社区早就动手了,于是出现了两条先行路线:
问题在于两套方案互不兼容:同一个功能,开发者要为不同宿主维护两份实现;各宿主行为细节不一致;安全与审计模式也各搞一套。SEP-1865 的目标就是把两家的经验合并成一个开放标准——规范由 MCP-UI 作者(Ido Salomon、Liad Yosef)与 OpenAI、Anthropic 的工程师共同起草。
它在新规范里的位置:2026-07-28 规范把「扩展」变成一等公民——反向域名 ID、独立仓库、独立版本号。MCP Apps(io.modelcontextprotocol/ui)和 Tasks 是首批两个官方扩展。也就是说,Apps 不绑定协议大版本,可以按自己的节奏演进。
MCP Apps 整个规范可以浓缩为四个概念,理解了它们就理解了一切:
ui:// 开头的 URI 声明的 MCP 资源,内容是一个完整的 HTML5 文档(MIME 类型固定为 text/html;profile=mcp-app),通过标准的 resources/read 获取。ui:// 前缀已在 MCP 中为 Apps 保留。_meta.ui.resourceUri,指向某个 ui:// 资源。宿主看到这个字段,就知道「这个工具的结果应该用那份界面来展示」。tools/call,读资源用 resources/read,外加一组 ui/* 专用消息。把上面四个概念串起来,一个带界面的工具调用是这样跑完全程的(依据规范 Lifecycle 章节):
界面拿到数据靠两条通知(而不是自己去请求):宿主在 View 初始化完成后推送 ui/notifications/tool-input(完整的工具入参,还可选流式推送 partial 版本做加载动画),工具执行完毕后推送 ui/notifications/tool-result(标准 CallToolResult)。规范对结果的三个字段有明确分工:
content:给模型和纯文本宿主看的文字版——必须有意义,这是降级保底;structuredContent:给 UI 渲染用的结构化数据,不进入模型上下文;_meta:时间戳等附加元数据,也不给模型。反向通道同样重要:界面可以用 ui/update-model-context 把「用户在界面里做了什么」(如选了方案 B)写回模型上下文,供下一轮对话使用——每次调用会覆盖上一次的内容;也可以用 ui/message 直接以用户身份往对话里发消息推动下一步。这就是官方说的「模型保持在环」(model stays in the loop)。
官方 SDK 是 npm 包 @modelcontextprotocol/ext-apps,配合标准 MCP SDK 使用。服务器侧的接入只有三件事(以下代码均来自官方规范与博客,做了注释汉化):
text/html;profile=mcp-app,并在 _meta.ui.csp 里声明需要访问的外部域名。_meta.ui.resourceUri 指向该资源;需要的话用 visibility 控制工具对模型/界面的可见性。getUiCapability 检查宿主是否声明了 io.modelcontextprotocol/ui 扩展能力,不支持就注册纯文本版工具。// ① 资源内容(resources/read 返回)
{
"contents": [{
"uri": "ui://weather-server/dashboard-template",
"mimeType": "text/html;profile=mcp-app",
"text": "<!DOCTYPE html><html>...</html>",
"_meta": { "ui": {
"csp": {
"connectDomains": ["https://api.openweathermap.org"], // 允许 fetch 的域名
"resourceDomains": ["https://cdn.jsdelivr.net"] // 允许加载静态资源的域名
},
"prefersBorder": true
}}
}]
}
// ② 工具声明:结果用上面的模板渲染
{
"name": "get_weather",
"description": "Get current weather for a location",
"inputSchema": { "type": "object",
"properties": { "location": { "type": "string" } } },
"_meta": { "ui": {
"resourceUri": "ui://weather-server/dashboard-template",
"visibility": ["model", "app"] // 默认值:模型和界面都能调
}}
}
工具可见性数组解决了「界面内部按钮」问题:把工具标成 visibility: ["app"],它就对模型隐藏(不会出现在 tools/list 里),只允许同一服务器的界面调用——刷新按钮、表单提交这类实现细节就不会污染模型的工具列表。宿主必须拒绝跨服务器的 app-only 工具调用。
// ③ 能力协商 + 降级(官方推荐写法)
import { getUiCapability, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
const uiCap = getUiCapability(clientCapabilities);
if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) {
// 宿主支持 → 注册带 UI 的工具
} else {
// 宿主不支持 → 注册纯文本版本
}
iframe 里的界面代码用同一个包的 App 类与宿主通信(不用 SDK、裸写 postMessage + JSON-RPC 也完全可以,规范里给了等价实现):
import { App } from "@modelcontextprotocol/ext-apps";
const app = new App();
await app.connect(); // ui/initialize 握手
// 接收工具结果并渲染
app.ontoolresult = (result) => {
renderChart(result.structuredContent);
};
// 从界面反向调用服务器工具(走宿主代理,可被审计/拦截)
const response = await app.callServerTool({
name: "fetch_details",
arguments: { id: "123" },
});
// 把用户操作写回模型上下文
await app.updateModelContext({
content: [{ type: "text", text: "User selected option B" }],
});
ui/initialize 的返回里,宿主会给界面一份 hostContext,包括:明暗主题、一组标准化 CSS 变量(背景/文字/边框色、字体、圆角、阴影等,宿主用 light-dark() 提供双主题值)、容器尺寸(固定或弹性,弹性时界面用 ResizeObserver 上报 size-changed)、显示模式(inline 内嵌 / fullscreen 全屏 / pip 画中画)、语言时区、平台类型(web/desktop/mobile)。界面按 var(--color-background-primary) 这样取用并自带兜底默认值,就能在 Claude 和 ChatGPT 里都长得「像原生」。
| 消息 | 方向 | 作用 |
|---|---|---|
ui/open-link | View → 宿主 | 请求在用户浏览器打开外链(宿主可拒绝) |
ui/message | View → 宿主 | 以用户身份向对话发消息,驱动下一轮(宿主可要求用户同意) |
ui/update-model-context | View → 宿主 | 更新模型上下文,覆盖式,下轮生效 |
ui/request-display-mode | View → 宿主 | 请求切换 inline/fullscreen/pip,以宿主返回为准 |
ui/notifications/tool-input(-partial) | 宿主 → View | 推送(流式)工具入参 |
ui/notifications/tool-result / tool-cancelled | 宿主 → View | 推送工具结果 / 取消原因 |
ui/notifications/size-changed | View → 宿主 | 内容尺寸变化,宿主据此调 iframe 大小 |
ui/notifications/host-context-changed | 宿主 → View | 主题切换、窗口变化等增量更新 |
ui/resource-teardown | 宿主 → View | 销毁前通知,给界面保存状态的机会 |
MCP Apps 的本质风险一句话就能说清:宿主要运行不是自己写的 HTML/JS。规范的威胁模型明确列了五类攻击:恶意 HTML、逃逸沙箱、未授权调工具、窃取宿主数据、钓鱼/社工。对应四层防御:
服务器不声明 CSP 时,宿主必须套用最严默认值:default-src 'none'; connect-src 'none'——即完全断网,只能用内联脚本和 data: 图片。声明了域名,宿主也只能收紧、不得放宽。权限(摄像头/麦克风/定位/剪贴板写入)同理:服务器申请、宿主决定,界面必须做特性检测兜底。
规范也坦承两个「防不住」:一是社交工程——沙箱管不了界面显示误导性内容,所以宿主应清晰标示「这是第三方 UI」的边界;二是资源消耗——恶意界面可以烧 CPU/内存,宿主要自行做资源限制。另外整个 MCP 生态 2026 年初曾集中曝出 30+ CVE(如影响 mcp-remote 的 CVE-2025-6514,CVSS 9.6),这提醒我们:Apps 的沙箱再好,接入不可信服务器本身仍是最大风险源。(CVE 数据来自 Aembit 安全报告,属 MCP 整体生态而非 Apps 扩展本身)
官方博客给出的四个典型场景,可以当作「什么时候值得上 Apps」的判断样板:
| 场景 | 界面做什么 | 为什么文本做不好 |
|---|---|---|
| 数据探索 | 销售分析工具返回可交互仪表盘,按地区筛选、下钻、导出 | 每次筛选都要一轮对话,慢 |
| 配置向导 | 部署工具的表单带联动字段:选 production 出现安全选项 | 依赖关系用文字描述容易错 |
| 文档审阅 | 合同分析工具内嵌 PDF 高亮条款,点击批准/标记,模型实时看到决定 | 「第三段第二句」式引用效率低 |
| 实时监控 | 服务器健康面板持续自更新 | 文本结果是静态快照,得反复重跑 |
官方 ext-apps 仓库提供了一批可直接起步的示例服务器:threejs-server(3D 可视化)、map-server(地图)、pdf-server(文档查看)、system-monitor-server(实时仪表盘)、sheet-music-server(乐谱)等。
截至 2026 年 1 月官方公告:Claude(网页版 + 桌面版)、Goose、VS Code Insiders 已上线,ChatGPT 同周跟进;JetBrains、AWS(Kiro)、Google DeepMind(Antigravity)公开表态探索接入。对开发者的意义是官方那句话:写一次交互体验,跑在所有兼容宿主上,不用写任何宿主专属代码。
| MCP Apps(官方扩展) | MCP-UI(社区先驱) | OpenAI Apps SDK | |
|---|---|---|---|
| 定位 | 开放标准,跨宿主 | 社区项目 / 试验场 | ChatGPT 专属生态 |
| UI 交付方式 | 预声明 ui:// 资源,可预取/审查/缓存 | 资源内嵌在工具结果里返回 | 组件随响应返回 |
| 通信协议 | 标准 MCP JSON-RPC over postMessage | 自定义消息类型(tool、prompt 等) | SDK 专有 API |
| 内容类型 | MVP 仅 HTML(profile=mcp-app) | HTML / 外部 URL / Remote DOM | HTML 组件 |
| 工具可见性 | 单一 visibility 数组 | — | widgetAccessible + visibility 两字段 |
| 现状 | Stable,首个官方扩展 | 继续维护,SDK 已兼容 Apps 模式,官方推荐宿主用其 Client SDK | 持续存在,ChatGPT 同时支持 Apps 标准 |
怎么选:新项目直接按 MCP Apps 标准写——这是三方(MCP-UI 作者 + OpenAI + Anthropic)共同收敛的结果;已有 MCP-UI 项目官方明确「不用慌」,SDK 支持平滑迁移;只有深度绑定 ChatGPT 分发渠道的应用才需要额外关注 Apps SDK 的专属能力。注意 MCP-UI 的「外部 URL 嵌入」能力在 Apps MVP 中被刻意推迟(模型看不见内容、无法截图、难以审查),依赖它的场景暂时只能留在 MCP-UI。
content 文字版必须独立成立,否则纯文本宿主和模型都会「失明」。(官方最佳实践)connect-src 'none'。界面里 fetch 外部 API 失败,先检查 _meta.ui.csp.connectDomains。_meta["ui/resourceUri"] 将在 GA 前移除,一律改用嵌套的 _meta.ui.resourceUri。(官方弃用通知)tool-input-partial 是「尽力恢复」的截断 JSON,规范禁止用它做关键操作,只能拿来画加载态。availableDisplayModes,且必须处理「要了但没给」的返回。