create-mcp-app
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCreate MCP App
创建MCP App
Build interactive UIs that run inside MCP-enabled hosts like Claude Desktop. An MCP App combines an MCP tool with an HTML resource to display rich, interactive content.
构建可在支持MCP的主机(如Claude Desktop)中运行的交互式UI。MCP App将MCP工具与HTML资源相结合,以展示丰富的交互式内容。
Core Concept: Tool + Resource
核心概念:工具 + 资源
Every MCP App requires two parts linked together:
- Tool - Called by the LLM/host, returns data
- Resource - Serves the bundled HTML UI that displays the data
- Link - The tool's references the resource
_meta.ui.resourceUri
Host calls tool → Server returns result → Host renders resource UI → UI receives result每个MCP App都需要两个相互关联的部分:
- 工具 - 由LLM/主机调用,返回数据
- 资源 - 提供打包后的HTML UI以展示数据
- 关联 - 工具的字段指向对应的资源
_meta.ui.resourceUri
主机调用工具 → 服务器返回结果 → 主机渲染资源UI → UI接收结果Quick Start Decision Tree
快速入门决策树
Framework Selection
框架选择
| Framework | SDK Support | Best For |
|---|---|---|
| React | | Teams familiar with React |
| Vanilla JS | Manual lifecycle | Simple apps, no build complexity |
| Vue/Svelte/Preact/Solid | Manual lifecycle | Framework preference |
Note that if the user prefers to write MCP Servers in Golang or Rust you should ask the user if they have a specific framework in mind for that language and if not help them search for a suitable framework / library that supports MCP Apps.
| 框架 | SDK支持情况 | 适用场景 |
|---|---|---|
| React | 提供 | 熟悉React的团队 |
| Vanilla JS | 手动管理生命周期 | 简单应用,无构建复杂度需求 |
| Vue/Svelte/Preact/Solid | 手动管理生命周期 | 有特定框架偏好的场景 |
注意:如果用户偏好使用Golang或Rust编写MCP服务器,应询问用户是否有该语言下的特定框架需求,若没有则协助其搜索支持MCP Apps的合适框架/库。
Project Context
项目场景
Adding to existing MCP server:
- Import ,
registerAppToolfrom SDKregisterAppResource - Add tool registration with
_meta.ui.resourceUri - Add resource registration serving bundled HTML
Creating new MCP server:
- Set up server with transport (stdio or HTTP)
- Register tools and resources
- Configure build system with
vite-plugin-singlefile
为现有MCP服务器添加功能:
- 从SDK中导入、
registerAppToolregisterAppResource - 添加工具注册,并配置
_meta.ui.resourceUri - 添加资源注册,提供打包后的HTML内容
创建全新MCP服务器:
- 配置服务器传输方式(stdio或HTTP)
- 注册工具与资源
- 使用配置构建系统
vite-plugin-singlefile
Getting Reference Code
获取参考代码
Clone the SDK repository for working examples and API documentation:
bash
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps克隆SDK仓库获取可用示例与API文档:
bash
git clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-appsFramework Templates
框架模板
Learn and adapt from :
/tmp/mcp-ext-apps/examples/basic-server-{framework}/| Template | Key Files |
|---|---|
| |
| |
| |
| |
| |
| |
Each template includes:
- Complete with
server.tsandregisterAppToolregisterAppResource - Client-side app with all lifecycle handlers
- with
vite.config.tsvite-plugin-singlefile - with all required dependencies
package.json - excluding
.gitignoreandnode_modules/dist/
学习并参考下的模板:
/tmp/mcp-ext-apps/examples/basic-server-{framework}/| 模板 | 核心文件 |
|---|---|
| |
| |
| |
| |
| |
| |
每个模板包含:
- 完整的,含
server.ts与registerAppTool配置registerAppResource - 包含所有生命周期处理器的客户端应用
- 配置了的
vite-plugin-singlefilevite.config.ts - 包含所有必要依赖的
package.json - 排除与
node_modules/的dist/.gitignore
API Reference (Source Files)
API参考(源文件)
Read JSDoc documentation directly from :
/tmp/mcp-ext-apps/src/| File | Contents |
|---|---|
| |
| |
| All type definitions: |
| |
| |
| |
直接从读取JSDoc文档:
/tmp/mcp-ext-apps/src/| 文件 | 内容说明 |
|---|---|
| |
| |
| 所有类型定义: |
| |
| 面向React应用的 |
| |
Advanced Examples
进阶示例
| Example | Pattern Demonstrated |
|---|---|
| Streaming partial input + visibility-based pause/play (best practice for large inputs) |
| |
| Polling pattern with interval management |
| Binary/blob resources |
| |
| |
| |
| |
| Reference host implementation using |
| 示例 | 演示模式 |
|---|---|
| 流式部分输入 + 基于可见性的暂停/播放(处理大输入的最佳实践) |
| 使用 |
| 带间隔管理的轮询模式 |
| 二进制/blob资源处理 |
| |
| |
| |
| |
| 使用 |
Critical Implementation Notes
关键实现注意事项
Adding Dependencies
添加依赖
Use to add dependencies rather than manually writing version numbers:
npm installbash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zodThis lets npm resolve the latest compatible versions. Never specify version numbers from memory.
使用添加依赖,而非手动编写版本号:
npm installbash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod此方式可让npm自动解析最新兼容版本,切勿凭记忆指定版本号。
TypeScript Server Execution
TypeScript服务器执行
Use as a devDependency for running TypeScript server files:
tsxbash
npm install -D tsxjson
"scripts": {
"serve": "tsx server.ts"
}Note: The SDK examples use but generated projects should use for broader compatibility.
buntsx将作为开发依赖,用于运行TypeScript服务器文件:
tsxbash
npm install -D tsxjson
"scripts": {
"serve": "tsx server.ts"
}注意:SDK示例使用,但生成项目应使用以获得更广泛的兼容性。
buntsxHandler Registration Order
处理器注册顺序
Register ALL handlers BEFORE calling :
app.connect()typescript
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();在调用之前注册所有处理器:
app.connect()typescript
const app = new App({ name: "My App", version: "1.0.0" });
// 先注册处理器
app.ontoolinput = (params) => { /* 处理输入 */ };
app.ontoolresult = (result) => { /* 处理结果 */ };
app.onhostcontextchanged = (ctx) => { /* 处理上下文变化 */ };
app.onteardown = async () => { return {}; };
// 再执行连接
await app.connect();Tool Visibility
工具可见性
Control who can access tools via :
_meta.ui.visibilitytypescript
// 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"] } }通过控制工具的访问权限:
_meta.ui.visibilitytypescript
// 默认:对模型和应用均可见
_meta: { ui: { resourceUri, visibility: ["model", "app"] } }
// 仅UI可见(对模型隐藏)- 适用于刷新按钮、表单提交等场景
_meta: { ui: { resourceUri, visibility: ["app"] } }
// 仅模型可见(应用无法调用)
_meta: { ui: { resourceUri, visibility: ["model"] } }Host Styling Integration
主机样式集成
Vanilla JS - Use helper functions:
typescript
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 - Use hooks:
typescript
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
const { app } = useApp({ appInfo, capabilities, onAppCreated });
useHostStyles(app); // Injects CSS variables to document, making var(--*) availableUsing variables in CSS - After applying, use :
var()css
.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);
}Key variable groups: , , , , , , , . See for full list.
--color-background-*--color-text-*--color-border-*--font-sans--font-mono--font-text-*-size--font-heading-*-size--border-radius-*src/spec.types.tsVanilla JS - 使用辅助函数:
typescript
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 - 使用钩子:
typescript
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react";
const { app } = useApp({ appInfo, capabilities, onAppCreated });
useHostStyles(app); // 向文档注入CSS变量,使var(--*)可用在CSS中使用变量 - 应用后,使用引用:
var()css
.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.tsSafe Area Handling
安全区域处理
Always respect :
safeAreaInsetstypescript
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`;
}
};始终尊重:
safeAreaInsetstypescript
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`;
}
};Streaming Partial Input
流式部分输入
For large tool inputs, use to show progress during LLM generation. The partial JSON is healed (always valid), enabling progressive UI updates.
ontoolinputpartialtypescript
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
};Use cases:
| Pattern | Example |
|---|---|
| Code preview | Show streaming code in |
| Progressive form | Fill form fields as they stream in |
| Live chart | Add data points to chart as array grows |
| Partial render | Render incomplete structured data (tables, lists, trees) |
Simple pattern (code preview):
typescript
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);
};对于大型工具输入,使用在LLM生成过程中展示进度。部分JSON会被自动修复(始终保持有效),支持渐进式UI更新。
ontoolinputpartialtypescript
app.ontoolinputpartial = (params) => {
const args = params.arguments; // 已修复的部分JSON - 始终有效,字段随生成过程逐步出现
// 直接使用args进行渐进式渲染
};
app.ontoolinput = (params) => {
// 最终完整输入 - 从预览切换为完整渲染
};适用场景:
| 模式 | 示例 |
|---|---|
| 代码预览 | 在 |
| 渐进式表单 | 随输入流填充表单字段 |
| 实时图表 | 随数组增长添加数据点 |
| 部分渲染 | 渲染不完整的结构化数据(表格、列表、树状结构) |
简单模式(代码预览):
typescript
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);
};Visibility-Based Resource Management
基于可见性的资源管理
Pause expensive operations (animations, WebGL, polling) when view scrolls out of viewport:
typescript
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"));当视图滚动出视口时,暂停高开销操作(动画、WebGL、轮询):
typescript
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
animation.play(); // 或:startPolling(), shaderToy.play()
} else {
animation.pause(); // 或:stopPolling(), shaderToy.pause()
}
});
});
observer.observe(document.querySelector(".main"));Fullscreen Mode
全屏模式
Request fullscreen via . Check availability in host context:
app.requestDisplayMode()typescript
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 pattern - Remove border radius in fullscreen:
css
.main { border-radius: var(--border-radius-lg); overflow: hidden; }
.main.fullscreen { border-radius: 0; }See for complete implementation.
examples/shadertoy-server/通过请求全屏。在主机上下文中检查可用性:
app.requestDisplayMode()typescript
let currentMode: "inline" | "fullscreen" = "inline";
app.onhostcontextchanged = (ctx) => {
// 检查是否支持全屏
if (ctx.availableDisplayModes?.includes("fullscreen")) {
fullscreenBtn.style.display = "block";
}
// 跟踪当前模式
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模式 - 全屏时移除圆角:
css
.main { border-radius: var(--border-radius-lg); overflow: hidden; }
.main.fullscreen { border-radius: 0; }完整实现请查看。
examples/shadertoy-server/Common Mistakes to Avoid
需避免的常见错误
- Handlers after connect() - Register ALL handlers BEFORE calling
app.connect() - Missing single-file bundling - Must use
vite-plugin-singlefile - Forgetting resource registration - Both tool AND resource must be registered
- Missing resourceUri link - Tool must have
_meta.ui.resourceUri - Ignoring safe area insets - Always handle
ctx.safeAreaInsets - No text fallback - Always provide array for non-UI hosts
content - Hardcoded styles - Use host CSS variables for theme integration
- No streaming for large inputs - Use to show progress during generation
ontoolinputpartial
- 连接后注册处理器 - 必须在调用前注册所有处理器
app.connect() - 缺少单文件打包 - 必须使用
vite-plugin-singlefile - 忘记注册资源 - 工具与资源都必须注册
- 缺少resourceUri关联 - 工具必须配置
_meta.ui.resourceUri - 忽略安全区域内边距 - 始终处理
ctx.safeAreaInsets - 无文本回退 - 始终为非UI主机提供数组
content - 硬编码样式 - 使用主机CSS变量实现主题集成
- 大输入未做流式处理 - 使用在生成过程中展示进度
ontoolinputpartial
Testing
测试
Using basic-host
使用basic-host
Test MCP Apps locally with the basic-host example:
bash
undefined通过basic-host示例在本地测试MCP App:
bash
undefinedTerminal 1: Build and run your server
终端1:构建并运行你的服务器
npm run build && npm run serve
npm run build && npm run serve
Terminal 2: Run basic-host (from cloned repo)
终端2:运行basic-host(从克隆的仓库中)
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
cd /tmp/mcp-ext-apps/examples/basic-host
npm install
SERVERS='["http://localhost:3001/mcp"]' npm run start
Configure `SERVERS` with a JSON array of your server URLs (default: `http://localhost:3001/mcp`).
使用JSON数组配置`SERVERS`,填入你的服务器URL(默认:`http://localhost:3001/mcp`)。Debug with sendLog
使用sendLog调试
Send debug logs to the host application (rather than just the iframe's dev console):
typescript
await app.sendLog({ level: "info", data: "Debug message" });
await app.sendLog({ level: "error", data: { error: err.message } });将调试日志发送到主机应用(而非仅iframe的开发者控制台):
typescript
await app.sendLog({ level: "info", data: "Debug message" });
await app.sendLog({ level: "error", data: { error: err.message } });