mcpField
C
create-mcp-app

create-mcp-app

github.com/sammcj开发16736 次安装2 天前更新
代码仓库

关于 create-mcp-app

create-mcp-app 是用于构建嵌入 MCP 宿主的交互式界面的技能,面向开发 MCP 应用的开发者。它围绕 MCP 工具与 HTML 资源的关联,提供多种前端框架示例和实现指导。

主要功能

  1. 01工具与 HTML 资源配对
  2. 02React、Vue、Svelte 等示例
  3. 03处理生命周期与宿主样式
  4. 04支持渐进式输入渲染
  5. 05管理资源可见性与全屏模式

适用场景

  1. 01为现有 MCP 服务添加交互界面
  2. 02基于模板创建新的 MCP 应用
  3. 03在工具输入生成时展示预览
  4. 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 都需要两个关联部分:

  1. 工具 - 由大模型/宿主调用,并返回数据
  2. 资源 - 提供打包好的 HTML UI,用于显示数据
  3. 关联 - 工具的 _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.tsApp 类、处理程序(ontoolinput、ontoolresult、onhostcontextchanged、onteardown)、生命周期
src/server/index.tsregisterAppTool、registerAppResource、工具可见性选项
src/spec.types.ts所有类型定义:McpUiHostContext、CSS 变量键、显示模式
src/styles.tsapplyDocumentTheme、applyHostStyleVariables、applyHostFonts
src/react/useApp.tsxReact 应用的 useApp 钩子
src/react/useHostStyles.tsuseHostStyles、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/。

应避免的常见错误

  1. 在 connect() 后注册处理程序 - 调用 app.connect() 之前,先注册所有处理程序
  2. 缺少单文件打包 - 必须使用 vite-plugin-singlefile
  3. 忘记注册资源 - 必须同时注册工具和资源
  4. 缺少 resourceUri 链接 - 工具必须包含 _meta.ui.resourceUri
  5. 忽略安全区域边距 - 始终处理 ctx.safeAreaInsets
  6. 没有文本回退 - 始终为非 UI 宿主提供 content 数组
  7. 硬编码样式 - 使用宿主 CSS 变量集成主题
  8. 大型输入没有流式传输 - 使用 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 } });

评论

支持 Markdown