MCP 官方扩展 · io.modelcontextprotocol/ui

MCP Apps
让 MCP 服务器「长出界面」的交互 UI 扩展

工具调用不再只能返回文字——服务器可以把仪表盘、表单、可视化直接渲染进对话里。这是 MCP 首个官方扩展(SEP-1865),已被 Claude、ChatGPT、VS Code、Goose 支持,并作为官方扩展纳入 2026-07-28 新规范。

⚡ 30 秒速览

MCP 让模型能调用外部工具,但工具的返回一直只有文字和结构化数据。用户想筛选一下表格、点开一条记录,都得再打一轮字。MCP Apps 补上了这块:MCP 服务器可以随工具附带一份 HTML 界面,宿主(Claude、ChatGPT 等)把它渲染在对话内的沙箱 iframe 里,用户直接点击交互,界面再通过标准 JSON-RPC 与宿主、模型双向沟通。

以前:纯文本往返 「帮我按收入排序」 「只看上周的」 「第 47 行详情是什么?」 每个动作都要再等一轮对话 现在:对话内交互 UI 📊 可点击的仪表盘 / 表单 直接排序、筛选、点选 模型全程「看得见」用户操作
MCP Apps 解决的核心问题:把「每个动作一轮对话」变成「界面直接操作、模型保持在环」
2026-01-26
Stable 版本发布
首个 MCP 官方扩展
4+
已支持宿主:Claude、ChatGPT、Goose、VS Code Insiders
2.1k★
ext-apps 官方仓库
(截至调研时)

数据来源:MCP 官方博客(2026-01-26)、GitHub ext-apps 仓库页面。

🧭 背景与动机:为什么需要 MCP Apps

先说痛点。MCP 把模型连上了数据和工具,但官方博客里举了一个很典型的例子:一个查数据库的工具返回几百行数据,模型能做摘要,可用户真正想做的是探索——按列排序、按日期筛、点进某条记录。纯文本模式下,每个动作都要再发一条消息:「只看上周的」「按收入排」「第 47 行是什么」。能用,但慢。

社区早就动手了,于是出现了两条先行路线:

问题在于两套方案互不兼容:同一个功能,开发者要为不同宿主维护两份实现;各宿主行为细节不一致;安全与审计模式也各搞一套。SEP-1865 的目标就是把两家的经验合并成一个开放标准——规范由 MCP-UI 作者(Ido Salomon、Liad Yosef)与 OpenAI、Anthropic 的工程师共同起草。

2025-11SEP-1865 提案(MCP-UI + Apps SDK 合流) 2026-01-26Stable 发布Claude / ChatGPT 等上线 2026-05-21进入 07-28 规范 RCExtensions 框架转正 2026-07-28新规范定稿
从社区实验到官方标准:MCP Apps 是 Extensions 机制下第一个「转正」的扩展

它在新规范里的位置:2026-07-28 规范把「扩展」变成一等公民——反向域名 ID、独立仓库、独立版本号。MCP Apps(io.modelcontextprotocol/ui)和 Tasks 是首批两个官方扩展。也就是说,Apps 不绑定协议大版本,可以按自己的节奏演进。

🧩 核心概念:四个关键词

MCP Apps 整个规范可以浓缩为四个概念,理解了它们就理解了一切:

① UI 资源(ui:// 资源)
一份用 ui:// 开头的 URI 声明的 MCP 资源,内容是一个完整的 HTML5 文档(MIME 类型固定为 text/html;profile=mcp-app),通过标准的 resources/read 获取。
大白话:界面模板就是服务器上的一个「特殊文件」,宿主用读资源的老办法把它拿下来。ui:// 前缀已在 MCP 中为 Apps 保留。
② 工具-UI 关联(_meta.ui.resourceUri)
工具在定义里加一个元数据字段 _meta.ui.resourceUri,指向某个 ui:// 资源。宿主看到这个字段,就知道「这个工具的结果应该用那份界面来展示」。
大白话:工具举个牌子说「我的结果请用 3 号模板渲染」。不支持 Apps 的宿主直接无视牌子,退回纯文本——完全向后兼容。
③ 沙箱 iframe 渲染
宿主把 HTML 渲染在权限受限的沙箱 iframe 中;网页版宿主还必须再套一层不同源的「沙箱代理」(双 iframe 结构),并按服务器声明的 CSP 白名单加载。
大白话:第三方界面被关进「玻璃房」里运行——能看、能点,但摸不到宿主页面,想连哪个外部域名必须提前报备。
④ JSON-RPC over postMessage 双向通信
iframe 里的界面(规范里叫 View)概念上就是一个 MCP 客户端,通过 postMessage 与宿主说 MCP 的标准话:调工具用 tools/call,读资源用 resources/read,外加一组 ui/* 专用消息。
大白话:界面不发明新语言,继续说 MCP 的普通话。好处是:界面发起的每个动作都走和模型调工具同一条审计与同意通道,宿主可以逐条记录、逐条拦截。
🍜 一个类比
以前的 MCP 工具像「电话点餐」:你说一句,后厨做一步,想改口味再打一次电话。MCP Apps 相当于餐厅递给你一台点餐平板:菜单(UI 模板)是餐厅预先做好、经过审查的;你在平板上点的每一下,都会生成一张标准格式的订单小票(JSON-RPC 消息)送进后厨——服务员(宿主)全程能看到每张小票,可疑的可以拦下。

⚙️ 工作机制:一次 UI 工具调用的完整旅程

把上面四个概念串起来,一个带界面的工具调用是这样跑完全程的(依据规范 Lifecycle 章节):

1. 发现 tools/list 带 _meta.ui 2. 预取模板 resources/read ui://… 3. 审查 + 缓存 宿主可先安全审查 HTML 4. 模型调用工具 tools/call,同时渲染 iframe 5. UI 握手 ui/initialize → 拿到主题/尺寸等上下文 6. 数据注入 tool-input / tool-result 通知 7. 交互循环(核心阶段) 用户点击 → tools/call / ui/message / 更新模型上下文 8. ui/resource-teardown 优雅收尾
生命周期八步:模板「预声明 → 预取 → 审查」发生在工具执行之前,这是与旧方案最大的架构差异

数据是怎么进入界面的?

界面拿到数据靠两条通知(而不是自己去请求):宿主在 View 初始化完成后推送 ui/notifications/tool-input(完整的工具入参,还可选流式推送 partial 版本做加载动画),工具执行完毕后推送 ui/notifications/tool-result(标准 CallToolResult)。规范对结果的三个字段有明确分工:

反向通道同样重要:界面可以用 ui/update-model-context 把「用户在界面里做了什么」(如选了方案 B)写回模型上下文,供下一轮对话使用——每次调用会覆盖上一次的内容;也可以用 ui/message 直接以用户身份往对话里发消息推动下一步。这就是官方说的「模型保持在环」(model stays in the loop)。

🛠️ 怎么用(一):服务器侧三步接入

官方 SDK 是 npm 包 @modelcontextprotocol/ext-apps,配合标准 MCP SDK 使用。服务器侧的接入只有三件事(以下代码均来自官方规范与博客,做了注释汉化):

注册 ui:// 资源——把打包好的 HTML(建议单文件、内联 JS/CSS)作为资源暴露,MIME 用 text/html;profile=mcp-app,并在 _meta.ui.csp 里声明需要访问的外部域名。
给工具挂上 UI 元数据——在工具定义的 _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:一个容易被忽略的好设计

工具可见性数组解决了「界面内部按钮」问题:把工具标成 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 {
  // 宿主不支持 → 注册纯文本版本
}

🎨 怎么用(二):UI 侧的 App API

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/* 专用消息速查表

消息方向作用
ui/open-linkView → 宿主请求在用户浏览器打开外链(宿主可拒绝)
ui/messageView → 宿主以用户身份向对话发消息,驱动下一轮(宿主可要求用户同意)
ui/update-model-contextView → 宿主更新模型上下文,覆盖式,下轮生效
ui/request-display-modeView → 宿主请求切换 inline/fullscreen/pip,以宿主返回为准
ui/notifications/tool-input(-partial)宿主 → View推送(流式)工具入参
ui/notifications/tool-result / tool-cancelled宿主 → View推送工具结果 / 取消原因
ui/notifications/size-changedView → 宿主内容尺寸变化,宿主据此调 iframe 大小
ui/notifications/host-context-changed宿主 → View主题切换、窗口变化等增量更新
ui/resource-teardown宿主 → View销毁前通知,给界面保存状态的机会

🔐 安全模型:怎么放心运行「别人写的界面」

MCP Apps 的本质风险一句话就能说清:宿主要运行不是自己写的 HTML/JS。规范的威胁模型明确列了五类攻击:恶意 HTML、逃逸沙箱、未授权调工具、窃取宿主数据、钓鱼/社工。对应四层防御:

四层防御 ① 沙箱 iframe(强制) 权限受限,碰不到宿主 DOM ② 模板预声明 渲染前可审查 / 计算哈希 / 建白名单 ③ 通信全程可审计 JSON-RPC 逐条校验、记录、可拦截 ④ CSP 白名单 未声明的域名一律连不上 网页宿主:双 iframe 结构(必须) 宿主页面(claude.ai 等) 沙箱代理 iframe(不同源!) 只转发消息,不生成请求 内层 iframe:View 服务器的 HTML 在这里运行 CSP 按 _meta.ui.csp 构造 postMessage ↕ JSON-RPC
左:规范的四层防御。右:网页宿主强制的「双 iframe」——代理层与宿主不同源,浏览器同源策略从物理上隔断了对宿主数据的访问

CSP 的「默认拒绝」哲学

服务器不声明 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(网页版 + 桌面版)、GooseVS Code Insiders 已上线,ChatGPT 同周跟进;JetBrains、AWS(Kiro)、Google DeepMind(Antigravity)公开表态探索接入。对开发者的意义是官方那句话:写一次交互体验,跑在所有兼容宿主上,不用写任何宿主专属代码。

⚖️ 对比与选型:MCP Apps vs MCP-UI vs OpenAI Apps SDK

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 DOMHTML 组件
工具可见性单一 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。

⚠️ 常见坑与限制

📚 学习资源