MCP Apps

MCP Apps 使服务器能够返回在 AI 对话中直接渲染的交互式 HTML 界面(数据可视化、表单、仪表板)。

为什么需要 MCP Apps

传统的文本响应有其局限性。有时用户需要与数据交互,而不仅仅是阅读。MCP Apps 提供了独立 Web 应用无法比拟的优势:

上下文保留

应用位于对话内部。用户无需切换标签页、丢失位置或忘记哪个聊天线程包含该仪表板。UI 就在那里,紧邻导致它的讨论。

双向数据流

你的应用可以调用 MCP 服务器上的任何工具,主机也可以将新鲜结果推送到你的应用。这通过现有的 MCP 模式实现,无需单独的 API、身份验证或状态管理。

主机集成

应用可以委托操作给主机,主机随后调用用户已连接的能力和工具。应用无需为每个功能实现直接集成,只需请求结果(如"安排此会议"),主机会通过用户现有的连接能力进行路由。

安全保证

MCP Apps 在主机控制的沙盒 iframe 中运行。它们无法访问父页面、窃取 cookie 或逃离容器。这意味着主机可以安全地渲染第三方应用,而无需完全信任服务器作者。

  sequenceDiagram
    participant User
    participant Agent
    participant App as MCP App iframe
    participant Server as MCP Server

    User->>Agent: "显示分析数据"
    Note over User,App: 交互式应用在聊天中渲染
    Agent->>Server: tools/call
    Server-->>Agent: 工具输入/结果
    Agent-->>App: 工具结果推送到应用
    User->>App: 用户交互
    App->>Agent: tools/call 请求
    Agent->>Server: tools/call (转发)
    Server-->>Agent: 新数据
    Agent-->>App: 新数据
    Note over User,App: 应用使用新数据更新
    App-->>Agent: 上下文更新

何时使用 MCP Apps

使用场景说明
复杂数据探索交互式地图、下钻图表、可切换指标
多选项配置带验证、默认值和相互依赖选项的表单
富媒体查看PDF 预览、3D 模型、带平移/缩放的图像预览
实时监控实时仪表板、指标、日志、系统状态
多步骤工作流费用报告审批、代码审查、问题分类

MCP Apps 工作原理

传统 MCP 工具返回文本、图像或结构化数据。MCP Apps 扩展了这一模式,允许工具在描述中声明对交互式 UI 的引用,主机会在原地渲染。

核心模式结合了两个 MCP 基本元素:

  1. 通过 _meta.ui.resourceUri 声明 UI 资源的工具
  2. 将数据渲染为交互式 HTML 界面的 UI 资源

工作流程

  1. UI 预加载:工具描述包含指向 ui:// 资源的 _meta.ui.resourceUri 字段。主机可以在工具被调用之前预加载此资源。

  2. 资源获取:主机从服务器获取 UI 资源。此资源包含 HTML 页面,通常与其 JavaScript 和 CSS 一起打包。

  3. 沙盒渲染:Web 主机在对话内的沙盒 iframe 中渲染 HTML。沙盒限制应用对父页面的访问。

  4. 双向通信:应用和主机通过 JSON-RPC 协议进行通信。应用可以请求工具调用、发送消息、更新模型的上下文,并从主机接收数据。

快速开始

前置条件

使用 AI 编码助手

创建 MCP Apps 最快的方式是使用带有 MCP Apps 技能的 AI 编码助手。

安装技能

对于 Claude Code:

/plugin marketplace add modelcontextprotocol/ext-apps
/plugin install mcp-apps@modelcontextprotocol-ext-apps

或使用 Vercel Skills CLI:

npx skills add modelcontextprotocol/ext-apps

创建你的应用

向 AI 编码助手提问:

创建一个显示颜色选择器的 MCP App

助手将生成包含服务器、UI 和配置文件的完整项目。

运行你的应用

npm install && npm run build && npm run serve

手动设置

如果你更喜欢手动设置,请按照以下步骤操作:

项目结构

my-mcp-app/
├── package.json
├── tsconfig.json
├── vite.config.ts
├── server.ts          # 带工具 + 资源的 MCP 服务器
├── mcp-app.html       # UI 入口点
└── src/
    └── mcp-app.ts     # UI 逻辑

安装依赖

npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
npm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx

配置项目

package.json:

{
  "type": "module",
  "scripts": {
    "build": "INPUT=mcp-app.html vite build",
    "serve": "npx tsx server.ts"
  }
}

tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "outDir": "dist"
  },
  "include": ["*.ts", "src/**/*.ts"]
}

vite.config.ts:

import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [viteSingleFile()],
  build: {
    outDir: "dist",
    rollupOptions: {
      input: process.env.INPUT,
    },
  },
});

构建 MCP App

让我们构建一个显示当前服务器时间的简单应用。

服务器实现

服务器需要注册带有 _meta.ui.resourceUri 的工具,并将打包的 HTML 作为资源提供。

// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
  registerAppTool,
  registerAppResource,
  RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import cors from "cors";
import express from "express";
import fs from "node:fs/promises";
import path from "node:path";

const server = new McpServer({
  name: "My MCP App Server",
  version: "1.0.0",
});

// ui:// 方案告诉主机这是 MCP App 资源
const resourceUri = "ui://get-time/mcp-app.html";

// 注册带有 UI 元数据的工具
registerAppTool(
  server,
  "get-time",
  {
    title: "获取时间",
    description: "返回当前服务器时间。",
    inputSchema: {},
    _meta: { ui: { resourceUri } },
  },
  async () => {
    const time = new Date().toISOString();
    return {
      content: [{ type: "text", text: time }],
    };
  },
);

// 注册提供打包 HTML 的资源
registerAppResource(
  server,
  resourceUri,
  resourceUri,
  { mimeType: RESOURCE_MIME_TYPE },
  async () => {
    const html = await fs.readFile(
      path.join(import.meta.dirname, "dist", "mcp-app.html"),
      "utf-8",
    );
    return {
      contents: [
        { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
      ],
    };
  },
);

// 通过 HTTP 暴露 MCP 服务器
const expressApp = express();
expressApp.use(cors());
expressApp.use(express.json());

expressApp.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true,
  });
  res.on("close", () => transport.close());
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

expressApp.listen(3001, () => {
  console.log("服务器监听 http://localhost:3001/mcp");
});

UI 实现

mcp-app.html:

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>获取时间应用</title>
  </head>
  <body>
    <p>
      <strong>服务器时间:</strong>
      <code id="server-time">加载中...</code>
    </p>
    <button id="get-time-btn">获取服务器时间</button>
    <script type="module" src="/src/mcp-app.ts"></script>
  </body>
</html>

src/mcp-app.ts:

import { App } from "@modelcontextprotocol/ext-apps";

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

const app = new App({ name: "获取时间应用", version: "1.0.0" });

// 与主机建立通信
app.connect();

// 处理主机推送的初始工具结果
app.ontoolresult = (result) => {
  const time = result.content?.find((c) => c.type === "text")?.text;
  serverTimeEl.textContent = time ?? "[错误]";
};

// 按钮点击时主动调用工具
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 ?? "[错误]";
});

测试你的应用

构建和运行

npm run build && npm run serve

你的服务器将在 http://localhost:3001/mcp 上可用。

使用 Claude 测试

对于本地开发,使用 cloudflared 将服务器暴露到互联网:

npx cloudflared tunnel --url http://localhost:3001

复制生成的 URL 并在 Claude 中添加为自定义连接器:

  1. 点击你的个人资料 → 设置 → 连接器
  2. 点击"添加自定义连接器"
  3. 粘贴 URL

使用 Basic Host 测试

ext-apps 仓库包含一个测试主机:

git clone https://github.com/modelcontextprotocol/ext-apps.git
cd ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm start

导航到 http://localhost:8080 测试你的应用。

安全模型

MCP Apps 在沙盒 iframe 中运行,提供与主机应用的强隔离。沙盒防止:

  • 访问父窗口的 DOM
  • 读取主机的 cookie 或本地存储
  • 导航父页面
  • 在父上下文中执行脚本

所有通信都通过 postMessage API 进行,App 类对此进行了抽象。主机控制你的应用可以访问的功能。

框架支持

MCP Apps 使用标准 Web 原语(postMessage、iframe)。你可以使用任何框架:

  • React、Vue、Svelte、Preact、Solid
  • 原生 JavaScript

App 类是一个便捷包装器,而非必需品。如果你更喜欢避免依赖或需要更紧密的控制,可以直接实现 postMessage 协议。

客户端支持

MCP Apps 是核心 MCP 规范的扩展。主机支持因客户端而异。

目前支持:

  • Claude(网页版和桌面版)
  • Visual Studio Code (Insiders)
  • Goose、Postman、MCPJam

构建 MCP 客户端支持时:

  1. 使用 @mcp-ui/client 获取 React 组件
  2. 基于 App Bridge 从 SDK 构建 iframe 渲染和消息传递

相关资源