create-mcp-app

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Create 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:
  1. Tool - Called by the LLM/host, returns data
  2. Resource - Serves the bundled HTML UI that displays the data
  3. Link - The tool's
    _meta.ui.resourceUri
    references the resource
Host calls tool → Server returns result → Host renders resource UI → UI receives result
每个MCP App都需要两个相互关联的部分:
  1. 工具 - 由LLM/主机调用,返回数据
  2. 资源 - 提供打包后的HTML UI以展示数据
  3. 关联 - 工具的
    _meta.ui.resourceUri
    字段指向对应的资源
主机调用工具 → 服务器返回结果 → 主机渲染资源UI → UI接收结果

Quick Start Decision Tree

快速入门决策树

Framework Selection

框架选择

FrameworkSDK SupportBest For
React
useApp
hook provided
Teams familiar with React
Vanilla JSManual lifecycleSimple apps, no build complexity
Vue/Svelte/Preact/SolidManual lifecycleFramework 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提供
useApp
钩子
熟悉React的团队
Vanilla JS手动管理生命周期简单应用,无构建复杂度需求
Vue/Svelte/Preact/Solid手动管理生命周期有特定框架偏好的场景
注意:如果用户偏好使用Golang或Rust编写MCP服务器,应询问用户是否有该语言下的特定框架需求,若没有则协助其搜索支持MCP Apps的合适框架/库。

Project Context

项目场景

Adding to existing MCP server:
  • Import
    registerAppTool
    ,
    registerAppResource
    from SDK
  • 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中导入
    registerAppTool
    registerAppResource
  • 添加工具注册,并配置
    _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-apps

Framework Templates

框架模板

Learn and adapt from
/tmp/mcp-ext-apps/examples/basic-server-{framework}/
:
TemplateKey Files
basic-server-vanillajs/
server.ts
,
src/mcp-app.ts
,
mcp-app.html
basic-server-react/
server.ts
,
src/mcp-app.tsx
(uses
useApp
hook)
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
Each template includes:
  • Complete
    server.ts
    with
    registerAppTool
    and
    registerAppResource
  • Client-side app with all lifecycle handlers
  • vite.config.ts
    with
    vite-plugin-singlefile
  • package.json
    with all required dependencies
  • .gitignore
    excluding
    node_modules/
    and
    dist/
学习并参考
/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 Reference (Source Files)

API参考(源文件)

Read JSDoc documentation directly from
/tmp/mcp-ext-apps/src/
:
FileContents
src/app.ts
App
class, handlers (
ontoolinput
,
ontoolresult
,
onhostcontextchanged
,
onteardown
), lifecycle
src/server/index.ts
registerAppTool
,
registerAppResource
, tool visibility options
src/spec.types.ts
All type definitions:
McpUiHostContext
, CSS variable keys, display modes
src/styles.ts
applyDocumentTheme
,
applyHostStyleVariables
,
applyHostFonts
src/react/useApp.tsx
useApp
hook for React apps
src/react/useHostStyles.ts
useHostStyles
,
useHostStyleVariables
,
useHostFonts
hooks
直接从
/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
钩子

Advanced Examples

进阶示例

ExamplePattern Demonstrated
examples/shadertoy-server/
Streaming partial input + visibility-based pause/play (best practice for large inputs)
examples/wiki-explorer-server/
callServerTool
for interactive data fetching
examples/system-monitor-server/
Polling pattern with interval management
examples/video-resource-server/
Binary/blob resources
examples/sheet-music-server/
ontoolinput
- processing tool args before execution completes
examples/threejs-server/
ontoolinputpartial
- streaming/progressive rendering
examples/map-server/
updateModelContext
- keeping model informed of UI state
examples/transcript-server/
updateModelContext
+
sendMessage
- background context updates + user-initiated messages
examples/basic-host/
Reference host implementation using
AppBridge
示例演示模式
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
的参考主机实现

Critical Implementation Notes

关键实现注意事项

Adding Dependencies

添加依赖

Use
npm install
to add dependencies rather than manually writing version numbers:
bash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
使用
npm install
添加依赖,而非手动编写版本号:
bash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
此方式可让npm自动解析最新兼容版本,切勿凭记忆指定版本号。

TypeScript Server Execution

TypeScript服务器执行

Use
tsx
as a devDependency for running TypeScript server files:
bash
npm install -D tsx
json
"scripts": {
  "serve": "tsx server.ts"
}
Note: The SDK examples use
bun
but generated projects should use
tsx
for broader compatibility.
tsx
作为开发依赖,用于运行TypeScript服务器文件:
bash
npm install -D tsx
json
"scripts": {
  "serve": "tsx server.ts"
}
注意:SDK示例使用
bun
,但生成项目应使用
tsx
以获得更广泛的兼容性。

Handler 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.visibility
:
typescript
// 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.visibility
控制工具的访问权限:
typescript
// 默认:对模型和应用均可见
_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(--*) available
Using 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:
--color-background-*
,
--color-text-*
,
--color-border-*
,
--font-sans
,
--font-mono
,
--font-text-*-size
,
--font-heading-*-size
,
--border-radius-*
. See
src/spec.types.ts
for full list.
Vanilla 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.ts

Safe Area Handling

安全区域处理

Always respect
safeAreaInsets
:
typescript
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`;
  }
};
始终尊重
safeAreaInsets
typescript
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
ontoolinputpartial
to show progress during LLM generation. The partial JSON is healed (always valid), enabling progressive UI updates.
typescript
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:
PatternExample
Code previewShow streaming code in
<pre>
, render on complete (
examples/shadertoy-server/
)
Progressive formFill form fields as they stream in
Live chartAdd data points to chart as array grows
Partial renderRender 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);
};
对于大型工具输入,使用
ontoolinputpartial
在LLM生成过程中展示进度。部分JSON会被自动修复(始终保持有效),支持渐进式UI更新。
typescript
app.ontoolinputpartial = (params) => {
  const args = params.arguments; // 已修复的部分JSON - 始终有效,字段随生成过程逐步出现
  // 直接使用args进行渐进式渲染
};

app.ontoolinput = (params) => {
  // 最终完整输入 - 从预览切换为完整渲染
};
适用场景:
模式示例
代码预览
<pre>
中展示流式代码,完成后再渲染(
examples/shadertoy-server/
渐进式表单随输入流填充表单字段
实时图表随数组增长添加数据点
部分渲染渲染不完整的结构化数据(表格、列表、树状结构)
简单模式(代码预览):
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
app.requestDisplayMode()
. Check availability in host context:
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
examples/shadertoy-server/
for complete implementation.
通过
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

需避免的常见错误

  1. Handlers after connect() - Register ALL handlers BEFORE calling
    app.connect()
  2. Missing single-file bundling - Must use
    vite-plugin-singlefile
  3. Forgetting resource registration - Both tool AND resource must be registered
  4. Missing resourceUri link - Tool must have
    _meta.ui.resourceUri
  5. Ignoring safe area insets - Always handle
    ctx.safeAreaInsets
  6. No text fallback - Always provide
    content
    array for non-UI hosts
  7. Hardcoded styles - Use host CSS variables for theme integration
  8. No streaming for large inputs - Use
    ontoolinputpartial
    to show progress during generation
  1. 连接后注册处理器 - 必须在调用
    app.connect()
    前注册所有处理器
  2. 缺少单文件打包 - 必须使用
    vite-plugin-singlefile
  3. 忘记注册资源 - 工具与资源都必须注册
  4. 缺少resourceUri关联 - 工具必须配置
    _meta.ui.resourceUri
  5. 忽略安全区域内边距 - 始终处理
    ctx.safeAreaInsets
  6. 无文本回退 - 始终为非UI主机提供
    content
    数组
  7. 硬编码样式 - 使用主机CSS变量实现主题集成
  8. 大输入未做流式处理 - 使用
    ontoolinputpartial
    在生成过程中展示进度

Testing

测试

Using basic-host

使用basic-host

Test MCP Apps locally with the basic-host example:
bash
undefined
通过basic-host示例在本地测试MCP App:
bash
undefined

Terminal 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 } });