关于 create-mcp-app
create-mcp-app 是用于构建嵌入 MCP 宿主的交互式界面的技能,面向开发 MCP 应用的开发者。它围绕 MCP 工具与 HTML 资源的关联,提供多种前端框架示例和实现指导。
主要功能
- 01工具与 HTML 资源配对
- 02React、Vue、Svelte 等示例
- 03处理生命周期与宿主样式
- 04支持渐进式输入渲染
- 05管理资源可见性与全屏模式
适用场景
- 01为现有 MCP 服务添加交互界面
- 02基于模板创建新的 MCP 应用
- 03在工具输入生成时展示预览
- 04按界面可见状态暂停轮询或动画
MCP 相关
指导开发者将 MCP 工具与 HTML 资源关联,使 MCP 宿主能够调用工具并呈现交互式界面。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sammcj/ agentic-coding --skill create-mcp-app安装后在 Claude Code 中输入/create-mcp-app确认已加载。
详细介绍
创建 MCP App
构建可在 Claude Desktop 等支持 MCP 的宿主中运行的交互式 UI。MCP App 将 MCP 工具与 HTML 资源结合起来,以显示丰富的交互式内容。
核心概念:工具 + 资源
每个 MCP App 都需要两个关联部分:
- 工具 - 由大模型/宿主调用,并返回数据
- 资源 - 提供打包好的 HTML UI,用于显示数据
- 关联 - 工具的
_meta.ui.resourceUri引用该资源
Host calls tool → Server returns result → Host renders resource UI → UI receives result
快速入门决策树
框架选择
| 框架 | SDK 支持 | 最适合 |
|---|---|---|
| React | 提供 useApp 钩子 | 熟悉 React 的团队 |
| Vanilla JS | 手动管理生命周期 | 简单应用,无需复杂的构建流程 |
| Vue/Svelte/Preact/Solid | 手动管理生命周期 | 偏好使用这些框架 |
请注意,如果用户更倾向于使用 Golang 或 Rust 编写 MCP 服务,应询问用户是否对该语言有特定的框架要求;如果没有,则帮助他们搜索支持 MCP Apps 的合适框架/库。
项目背景
添加到现有 MCP 服务:
- 从 SDK 导入
registerAppTool、registerAppResource - 添加带有
_meta.ui.resourceUri的工具注册 - 添加提供打包后 HTML 的资源注册
创建新的 MCP 服务:
- 使用传输方式(stdio 或 HTTP)设置服务
- 注册工具和资源
- 使用
vite-plugin-singlefile配置构建系统
获取参考代码
克隆 SDK 仓库,查看可运行的示例和 API 文档:
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
框架模板
从 /tmp/mcp-ext-apps/examples/basic-server-{framework}/ 中学习并改编:
| 模板 | 关键文件 |
|---|---|
basic-server-vanillajs/ | server.ts、src/mcp-app.ts、mcp-app.html |
basic-server-react/ | server.ts、src/mcp-app.tsx(使用 useApp 钩子) |
basic-server-vue/ | server.ts、src/App.vue |
basic-server-svelte/ | server.ts、src/App.svelte |
basic-server-preact/ | server.ts、src/mcp-app.tsx |
basic-server-solid/ | server.ts、src/mcp-app.tsx |
每个模板都包含:
- 完整的
server.ts,其中包含registerAppTool和registerAppResource - 带有所有生命周期处理程序的客户端应用
- 使用
vite-plugin-singlefile的vite.config.ts - 包含所有必需依赖项的
package.json - 排除
node_modules/和dist/的.gitignore
API 参考(源文件)
直接阅读 /tmp/mcp-ext-apps/src/ 中的 JSDoc 文档:
| 文件 | 内容 |
|---|---|
src/app.ts | App 类、处理程序(ontoolinput、ontoolresult、onhostcontextchanged、onteardown)、生命周期 |
src/server/index.ts | registerAppTool、registerAppResource、工具可见性选项 |
src/spec.types.ts | 所有类型定义:McpUiHostContext、CSS 变量键、显示模式 |
src/styles.ts | applyDocumentTheme、applyHostStyleVariables、applyHostFonts |
src/react/useApp.tsx | React 应用的 useApp 钩子 |
src/react/useHostStyles.ts | useHostStyles、useHostStyleVariables、useHostFonts 钩子 |
高级示例
| 示例 | 展示的模式 |
|---|---|
examples/shadertoy-server/ | 流式传输部分输入 + 基于可见性的暂停/播放(大型输入的最佳实践) |
examples/wiki-explorer-server/ | 使用 callServerTool 交互式获取数据 |
examples/system-monitor-server/ | 带间隔管理的轮询模式 |
examples/video-resource-server/ | 二进制/blob 资源 |
examples/sheet-music-server/ | ontoolinput - 在执行完成前处理工具参数 |
examples/threejs-server/ | ontoolinputpartial - 流式/渐进式渲染 |
examples/map-server/ | updateModelContext - 让模型了解 UI 状态 |
examples/transcript-server/ | updateModelContext + sendMessage - 后台上下文更新 + 用户发起的消息 |
examples/basic-host/ | 使用 AppBridge 的参考宿主实现 |
关键实现说明
添加依赖项
使用 npm install 添加依赖项,而不是手动填写版本号:
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
这样 npm 就能解析最新的兼容版本。切勿凭记忆指定版本号。
TypeScript 服务执行
使用 tsx 作为 devDependency 来运行 TypeScript 服务文件:
npm install -D tsx
"scripts": {
"serve": "tsx server.ts"
}
注意:SDK 示例使用 bun,但生成的项目应使用 tsx,以获得更广泛的兼容性。
处理程序注册顺序
调用 app.connect() 之前,先注册所有处理程序:
const app = new App({ name: "My App", version: "1.0.0" });
// Register handlers first
app.ontoolinput = (params) => { /* handle input */ };
app.ontoolresult = (result) => { /* handle result */ };
app.onhostcontextchanged = (ctx) => { /* handle context */ };
app.onteardown = async () => { return {}; };
// Then connect
await app.connect();
工具可见性
通过 _meta.ui.visibility 控制哪些人可以访问工具:
// Default: visible to both model and app
_meta: { ui: { resourceUri, visibility: ["model", "app"] } }
// UI-only (hidden from model) - for refresh buttons, form submissions
_meta: { ui: { resourceUri, visibility: ["app"] } }
// Model-only (app cannot call)
_meta: { ui: { resourceUri, visibility: ["model"] } }
宿主样式集成
原生 JS - 使用辅助函数:
import { applyDocumentTheme, applyHostStyleVariables, applyHostFonts } from "@modelcontextprotocol/ext-apps";
app.onhostcontextchanged = (ctx) => {
if (ctx.theme) applyDocumentTheme(ctx.theme);
if (ctx.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
if (ctx.styles?.css?.fonts) applyHostFonts(ctx.styles.css.fonts);
};
React - 使用 Hooks:
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
const { app } = useApp({ appInfo, capabilities, onAppCreated });
useHostStyles(app); // Injects CSS variables to document, making var(--*) available
在 CSS 中使用变量 - 应用后,使用 var():
.container {
background: var(--color-background-secondary);
color: var(--color-text-primary);
font-family: var(--font-sans);
border-radius: var(--border-radius-md);
}
.code {
font-family: var(--font-mono);
font-size: var(--font-text-sm-size);
line-height: var(--font-text-sm-line-height);
color: var(--color-text-secondary);
}
.heading {
font-size: var(--font-heading-lg-size);
font-weight: var(--font-weight-semibold);
}
主要变量组:--color-background-*、--color-text-*、--color-border-*、--font-sans、--font-mono、--font-text-*-size、--font-heading-*-size、--border-radius-*。完整列表请参阅 src/spec.types.ts。
安全区域处理
始终遵循 safeAreaInsets:
app.onhostcontextchanged = (ctx) => {
if (ctx.safeAreaInsets) {
const { top, right, bottom, left } = ctx.safeAreaInsets;
document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
}
};
流式传输部分输入
对于较大的工具输入,使用 ontoolinputpartial 在大模型生成期间显示进度。部分 JSON 会经过修复(始终有效),因此可以渐进式更新 UI。
规范: ui/notifications/tool-input-partial
app.ontoolinputpartial = (params) => {
const args = params.arguments; // Healed partial JSON - always valid, fields appear as generated
// Use args directly for progressive rendering
};
app.ontoolinput = (params) => {
// Final complete input - switch from preview to full render
};
适用场景:
| 模式 | 示例 |
|---|---|
| 代码预览 | 在 <pre> 中显示流式代码,完成后再渲染(examples/shadertoy-server/) |
| 渐进式表单 | 随着内容流入逐步填充表单字段 |
| 实时图表 | 随着数组增长向图表添加数据点 |
| 部分渲染 | 渲染尚不完整的结构化数据(表格、列表、树) |
简单模式(代码预览):
app.ontoolinputpartial = (params) => {
codePreview.textContent = params.arguments?.code ?? "";
codePreview.style.display = "block";
canvas.style.display = "none";
};
app.ontoolinput = (params) => {
codePreview.style.display = "none";
canvas.style.display = "block";
render(params.arguments);
};
基于可见性的资源管理
当视图滚出视口时,暂停开销较大的操作(动画、WebGL、轮询):
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
animation.play(); // or: startPolling(), shaderToy.play()
} else {
animation.pause(); // or: stopPolling(), shaderToy.pause()
}
});
});
observer.observe(document.querySelector(".main"));
全屏模式
通过 app.requestDisplayMode() 请求全屏。在宿主上下文中检查是否可用:
let currentMode: "inline" | "fullscreen" = "inline";
app.onhostcontextchanged = (ctx) => {
// Check if fullscreen available
if (ctx.availableDisplayModes?.includes("fullscreen")) {
fullscreenBtn.style.display = "block";
}
// Track current mode
if (ctx.displayMode) {
currentMode = ctx.displayMode;
container.classList.toggle("fullscreen", currentMode === "fullscreen");
}
};
async function toggleFullscreen() {
const newMode = currentMode === "fullscreen" ? "inline" : "fullscreen";
const result = await app.requestDisplayMode({ mode: newMode });
currentMode = result.mode;
}
CSS 模式 - 全屏时移除圆角:
.main { border-radius: var(--border-radius-lg); overflow: hidden; }
.main.fullscreen { border-radius: 0; }
完整实现请参阅 examples/shadertoy-server/。
应避免的常见错误
- 在 connect() 后注册处理程序 - 调用
app.connect()之前,先注册所有处理程序 - 缺少单文件打包 - 必须使用
vite-plugin-singlefile - 忘记注册资源 - 必须同时注册工具和资源
- 缺少 resourceUri 链接 - 工具必须包含
_meta.ui.resourceUri - 忽略安全区域边距 - 始终处理
ctx.safeAreaInsets - 没有文本回退 - 始终为非 UI 宿主提供
content数组 - 硬编码样式 - 使用宿主 CSS 变量集成主题
- 大型输入没有流式传输 - 使用
ontoolinputpartial在生成期间显示进度
测试
使用 basic-host
使用 basic-host 示例在本地测试 MCP Apps:
# Terminal 1: Build and run your server
npm run build && npm run serve
# Terminal 2: Run basic-host (from cloned repo)
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
# Open http://localhost:8080
将 SERVERS 配置为包含服务器 URL 的 JSON 数组(默认值:http://localhost:3001/mcp)。
使用 sendLog 调试
将调试日志发送到宿主应用程序(而不只是 iframe 的开发者控制台):
await app.sendLog({ level: "info", data: "Debug message" });
await app.sendLog({ level: "error", data: { error: err.message } });