MCP.so
登录
返回博客

MCP Apps 完全指南:MCP 交互式 UI 如何工作(2026)

2026年10月15日MCP.so TeamMCP.so Team

MCP Apps 让 MCP 服务器以 ui:// 资源的形式交付交互式 UI,并在 Claude、ChatGPT、VS Code 和 Goose 的沙箱 iframe 中渲染。本文讲清规范现状、SDK 和一个可运行的最小示例。

MCP Apps 是 Model Context Protocol 的官方扩展,让 MCP 服务器可以返回交互式 UI(图表、表单、地图、仪表盘),而不只是纯文本。工具通过元数据指向一个装着 HTML 页面的 ui:// 资源,宿主(Claude、ChatGPT、VS Code、Goose 等)把这个页面放进沙箱 iframe 里渲染,页面再通过 JSON-RPC 和宿主通信。同一份代码可以在所有支持该扩展的宿主中运行。

本文介绍规范目前的状态、哪些客户端能渲染 MCP Apps、取自官方 SDK 的最小可运行示例,以及 MCP Apps 和 OpenAI Apps SDK、MCP-UI 之间的关系。

现状速览(2026 年 10 月)

项目当前状态
规范SEP-1865「MCP Apps: Interactive User Interfaces for MCP」,Extensions 轨道
状态Stable(稳定版),版本 2026-01-26;另有 draft 版本在开发中
发布2026 年 1 月 26 日,第一个官方 MCP 扩展
仓库modelcontextprotocol/ext-apps
SDK@modelcontextprotocol/ext-apps(子路径 /server、/react、/app-bridge),v2.x
MIME 类型text/html;profile=mcp-app
关联字段工具上的 _meta.ui.resourceUri

这份 SEP 最早发布于 2025 年 11 月 21 日,作者包括 MCP-UI 的创建者以及来自 Anthropic 和 OpenAI 的成员。

MCP Apps 如何工作

ext-apps README 把流程概括为四步:

  1. 定义工具:工具声明一个装着 HTML 界面的 ui:// 资源。
  2. 调用工具:模型照常调用你服务器上的工具。
  3. 宿主渲染:宿主拉取该资源,并在沙箱 iframe 中显示。
  4. 双向通信:宿主通过通知把工具的输入和结果传给 UI,UI 也可以经由宿主调用其他工具。

也就是说,UI 只是又一个 MCP 资源,所以 MCP Apps 能自然融入现有协议。想复习工具和资源的区别,可以看 MCP 资源与工具对比。

协议的组成部分

  • UI 资源:URI 为 ui://、MIME 类型为 text/html;profile=mcp-app 的资源。它的 _meta.ui 可以声明 csp(允许的来源)、permissions、domain 和 prefersBorder。
  • 工具关联:工具的 _meta.ui.resourceUri 指定要渲染的资源。_meta.ui.visibility 可以是 ["model", "app"](默认),也可以是 ["app"],用于只给 UI 调用、模型看不到的辅助工具。
  • 握手:View 发送 ui/initialize,收到 hostContext(主题、显示模式、语言等),然后发送 ui/notifications/initialized。
  • 数据输入:ui/notifications/tool-input 和 ui/notifications/tool-result 传递本次调用的参数和结果。
  • 对外动作:View 可以发送 tools/call、ui/message(往对话里发消息)、ui/open-link 和 ui/update-model-context。
  • 显示模式:inline(默认)、fullscreen 和 pip(画中画)。宿主不必三种都支持。

安全模型

所有内容都运行在沙箱 iframe 里,CSP 默认拒绝一切。如果资源没有声明 ui.csp,规范要求宿主使用 connect-src 'none',也就是说 UI 无法调用任何外部 API。要放行外部来源,需要分别列在 connectDomains(fetch/WebSocket)、resourceDomains(脚本、图片、字体、样式)、frameDomains(嵌套 iframe)和 baseUriDomains 中。宿主可以把规则收得更紧,但绝不能放行你没有声明的域名。发布公告还补充说,UI 与宿主之间的所有消息都是可审计的 JSON-RPC,宿主也可以要求用户批准由 UI 发起的工具调用。更完整的威胁模型请看 MCP 安全最佳实践。

哪些客户端支持 MCP Apps

下表只列出我们能对照官方文档核实的宿主:

宿主支持情况说明
Claude(网页版和桌面版)支持,发布当天起Claude 文档:显示 App 前会先征求许可(Allow / Always allow)。设置了 ui.domain 的远程 App 必须使用 Claude 规定的哈希格式 *.claudemcpcontent.com
ChatGPT支持OpenAI 更新日志称,自 2026-02-22 起 ChatGPT「fully compatible with the MCP Apps spec」。见 Add UI to your MCP server
VS Code(GitHub Copilot agent)支持2026-01-26 宣布,先上 Insiders,随后进入 Stable
Goose支持官方公告中的首批宿主之一
Postman、MCPJam、mcp-use Inspector、Alpic Playground已列出ext-apps README 中列为支持的宿主

各宿主对具体功能的支持仍有差异,尤其是显示模式和宿主专属的扩展能力。工具本身仍应返回有用的文本,这样不支持 MCP Apps 的客户端也能完成任务。完整的支持矩阵见 modelcontextprotocol.io/clients,我们的 /clients 目录也收录了各类 MCP 客户端。

动手做一个最小 MCP App

下面的代码直接来自官方 quickstart 示例(ext-apps 2.x,Node.js 20+):一个 get-time 工具,它的 UI 显示服务器时间,并带一个刷新按钮。如果你从没写过服务器,先看 如何构建 MCP 服务器。

1. 安装依赖

npm install -S @modelcontextprotocol/ext-apps \
  @modelcontextprotocol/client@^2.0.0 \
  @modelcontextprotocol/server@^2.0.0 \
  @modelcontextprotocol/node@^2.0.0 \
  @modelcontextprotocol/express@^2.0.0 \
  zod@^4.2.0
npm install -D typescript vite vite-plugin-singlefile tsx

宿主把 UI 当作单个 HTML 资源来加载,所以 quickstart 用 Vite 配合 vite-plugin-singlefile 把 JavaScript 内联打包进去。

2. 注册工具和 UI 资源(server.ts)

import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
  registerAppResource,
  registerAppTool,
  RESOURCE_MIME_TYPE,
} from '@modelcontextprotocol/ext-apps/server';
import { McpServer } from '@modelcontextprotocol/server';
import { z } from 'zod';

const DIST_DIR = path.join(
  path.dirname(fileURLToPath(import.meta.url)),
  'dist'
);

export function createServer(): McpServer {
  const server = new McpServer({
    name: 'Quickstart MCP App Server',
    version: '1.0.0',
  });

  // Tool + resource, tied together by the resource URI.
  const resourceUri = 'ui://get-time/mcp-app.html';

  registerAppTool(
    server,
    'get-time',
    {
      title: 'Get Time',
      description: 'Returns the current server time.',
      inputSchema: z.object({}),
      _meta: { ui: { resourceUri } }, // links this tool to its UI
    },
    async () => {
      const time = new Date().toISOString();
      return { content: [{ type: 'text', text: time }] };
    }
  );

  registerAppResource(
    server,
    resourceUri,
    resourceUri,
    { mimeType: RESOURCE_MIME_TYPE },
    async () => {
      const html = await fs.readFile(
        path.join(DIST_DIR, 'mcp-app.html'),
        'utf-8'
      );
      return {
        contents: [
          { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
        ],
      };
    }
  );

  return server;
}

RESOURCE_MIME_TYPE 就是 text/html;profile=mcp-app 这个常量,用它代替手写字符串。registerAppTool 和 registerAppResource 这两个辅助函数还会替你生成各宿主需要的元数据,这也是 Claude 文档 推荐使用它们的原因。

3. 编写 View(mcp-app.html + src/mcp-app.ts)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Get Time App</title>
  </head>
  <body>
    <p>
      <strong>Server Time:</strong> <code id="server-time">Loading...</code>
    </p>
    <button id="get-time-btn">Get Server Time</button>
    <script type="module" src="/src/mcp-app.ts"></script>
  </body>
</html>
import { App } from '@modelcontextprotocol/ext-apps';

const serverTimeEl = document.getElementById('server-time')!;
const getTimeBtn = document.getElementById('get-time-btn')!;

const app = new App({ name: 'Get Time App', version: '1.0.0' });

// Register handlers BEFORE connect() so you don't miss the first result.
app.ontoolresult = (result) => {
  const time = result.content?.find((c) => c.type === 'text')?.text;
  serverTimeEl.textContent = time ?? '[ERROR]';
};

getTimeBtn.addEventListener('click', async () => {
  const result = await app.callServerTool({ name: 'get-time', arguments: {} });
  const time = result.content?.find((c) => c.type === 'text')?.text;
  serverTimeEl.textContent = time ?? '[ERROR]';
});

app.connect();

main.ts(在 3001 端口的 /mcp 上跑 Streamable HTTP,或用 --stdio 走 stdio)、vite.config.ts 和 package.json 里的脚本,从 quickstart 目录 复制即可。React 用户可以从 @modelcontextprotocol/ext-apps/react 拿到 useApp 和 useHostStyles。

4. 测试

  • basic-host:克隆 ext-apps,运行 npm install && npm start,打开 http://localhost:8080。这个参考宿主默认连接 http://localhost:3001/mcp。
  • Claude Desktop:在 Settings → Developer → Edit Config 中添加 stdio 配置。想先试试官方发布的示例,可以这样写:
{
  "mcpServers": {
    "map": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-map", "--stdio"]
    }
  }
}
  • ChatGPT:只能连接远程 HTTPS 服务器。先部署服务器或开隧道,再把它添加为自定义 MCP 服务器。详见 ChatGPT MCP 配置指南 和 部署 MCP 服务器。
  • 调试:在排查渲染问题之前,先用 MCP Inspector 确认工具的 _meta 和资源都能正确返回。

在用 AI 编程助手?仓库自带 Agent Skills(create-mcp-app、add-app-to-server、migrate-oai-app、convert-web-app)。在 Claude Code 中先运行 /plugin marketplace add modelcontextprotocol/ext-apps,再运行 /plugin install mcp-apps@mcp-apps 即可安装。

MCP Apps、OpenAI Apps SDK 与 MCP-UI 对比

三者出自同一条技术脉络。发布公告明确写道,MCP Apps 建立在 MCP-UI 和 OpenAI Apps SDK 的基础之上。

MCP-UIOpenAI Apps SDK(ChatGPT)MCP Apps
是什么率先在 MCP 上实现 UI 的社区项目OpenAI 为 ChatGPT 构建应用的方式,现归入 Plugins 文档官方、厂商中立的 MCP 扩展
当前状态@mcp-ui/server 和 @mcp-ui/client 现已实现 MCP Apps 标准ChatGPT 实现了 MCP Apps,window.openai 额外提供 ChatGPT 专属能力稳定版规范 2026-01-26
工具与 UI 的关联内嵌 UI 资源_meta["openai/outputTemplate"](作为别名保留)_meta.ui.resourceUri
运行环境MCP-UI 宿主,以及 MCP Apps 宿主ChatGPTClaude、ChatGPT、VS Code、Goose 等

落到实际开发上:

  • 新项目:直接用 MCP Apps(ui.resourceUri、text/html;profile=mcp-app、App 类)。OpenAI 的 文档 要求开发者使用通用名称,只有规范没覆盖的能力才用 window.openai,包括 requestCheckout、文件上传与选择、requestModal 和 widgetState。请用特性检测判断它们是否可用,不要按宿主名称写分支。
  • 已有的 ChatGPT 应用:旧的 window.openai.toolInput、toolOutput、callTool 和 sendFollowUpMessage 仍作为别名可用。migrate-oai-app 技能和 迁移指南 会把它们映射到 ontoolinput、ontoolresult、callServerTool 和 ui/message。
  • 自己做宿主:MCP-UI 的客户端 SDK(AppRenderer)是一个完整的 MCP Apps 渲染器;也可以用 @modelcontextprotocol/ext-apps/app-bridge 自行嵌入 View。

常见问题排查

  • 只显示文本、没有 UI:检查工具是否带有 _meta.ui.resourceUri,资源注册时 MIME 是否为 text/html;profile=mcp-app,以及宿主是否支持 MCP Apps。
  • iframe 空白或请求失败:几乎都是 CSP 的问题。打包产物加载或调用的每个来源都必须写进 resourceDomains 或 connectDomains,开发环境的 localhost 也不例外。漏掉的来源会静默失败,浏览器控制台里能看到违规记录。
  • 第一次的结果没显示:要在 app.connect() 之前 设置 ontoolresult 和 ontoolinput。
  • 部署后 UI 还是旧的:OpenAI 文档建议把资源 URI 当作缓存键。做了破坏性改动时,发布新的 URI,并更新引用它的工具。
  • Claude 拒绝 ui.domain:Claude 要求的值是服务器 URL 的 SHA-256 前 32 位十六进制字符,再加上 .claudemcpcontent.com,具体见 Claude 文档。

下一步

MCP Apps 让工具结果变成用户可以直接点击操作的界面,但背后的工具本身仍然得好用。浏览 MCP 服务器目录,看看现有服务器是怎么设计工具的;也可以从 最佳 MCP 服务器 中找灵感。等你的 App 上线后,把它提交到 mcp.so,让更多人发现它。

常见问题

什么是 MCP Apps?

MCP Apps 是 Model Context Protocol 的官方扩展(SEP-1865),让 MCP 服务器可以交付交互式 UI。工具指向一个 ui:// HTML 资源,宿主在沙箱 iframe 中渲染它,页面通过 JSON-RPC 与宿主通信。

MCP Apps 规范已经稳定了吗?

是的。MCP Apps 规范以 2026-01-26 版本进入稳定状态,并于 2026 年 1 月 26 日作为第一个官方 MCP 扩展发布。draft 版本仍在 modelcontextprotocol/ext-apps 仓库中持续演进。

哪些客户端支持 MCP Apps?

Claude(网页版和桌面版)、ChatGPT、带 GitHub Copilot 的 VS Code 以及 Goose 都支持 MCP Apps,Postman、MCPJam 等工具也能渲染。全屏、画中画等具体功能的支持程度仍因宿主而异。

MCP Apps 和 OpenAI Apps SDK 有什么区别?

OpenAI Apps SDK 是 ChatGPT 自有的应用 UI 方案,MCP Apps 则是部分基于它制定的开放标准。如今 ChatGPT 已实现 MCP Apps,保留 window.openai 名称作为别名,并额外提供结账、文件上传等 ChatGPT 专属能力。