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 基本元素:
- 通过
_meta.ui.resourceUri声明 UI 资源的工具 - 将数据渲染为交互式 HTML 界面的 UI 资源
工作流程
UI 预加载:工具描述包含指向
ui://资源的_meta.ui.resourceUri字段。主机可以在工具被调用之前预加载此资源。资源获取:主机从服务器获取 UI 资源。此资源包含 HTML 页面,通常与其 JavaScript 和 CSS 一起打包。
沙盒渲染:Web 主机在对话内的沙盒 iframe 中渲染 HTML。沙盒限制应用对父页面的访问。
双向通信:应用和主机通过 JSON-RPC 协议进行通信。应用可以请求工具调用、发送消息、更新模型的上下文,并从主机接收数据。
快速开始
前置条件
- Node.js 18 或更高版本
- 熟悉 MCP 工具 和 资源
- 有 MCP TypeScript SDK 使用经验会有帮助
使用 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 中添加为自定义连接器:
- 点击你的个人资料 → 设置 → 连接器
- 点击"添加自定义连接器"
- 粘贴 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 协议。
客户端支持
目前支持:
- Claude(网页版和桌面版)
- Visual Studio Code (Insiders)
- Goose、Postman、MCPJam
构建 MCP 客户端支持时:
- 使用
@mcp-ui/client获取 React 组件 - 基于 App Bridge 从 SDK 构建 iframe 渲染和消息传递