返回 Skill 列表
extension
分类: 开发与工程无需 API Key

Create MCP App

当用户要求“创建一个MCP应用程序”、“为MCP工具添加UI”、“构建交互式的MCP视图”、“搭建MCP应用框架”,或者需要关于MCP应用程序SDK模式、UI资源注册、MCP应用生命周期或主机集成的指导时,应使用此技能。提供全面的指导来帮助构建具有交互界面的MCP应用程序。

person作者: jakexiaohubgithub

Create 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.

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

Quick Start Decision Tree

Framework Selection

| Framework | SDK Support | Best For | |-----------|-------------|----------| | React | useApp hook provided | Teams familiar with React | | Vanilla JS | Manual lifecycle | Simple apps, no build complexity | | Vue/Svelte/Preact/Solid | Manual lifecycle | Framework preference |

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

Getting Reference Code

Clone the SDK repository for working examples and API documentation:

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}/:

| Template | Key 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/

API Reference (Source Files)

Read JSDoc documentation directly from /tmp/mcp-ext-apps/src/:

| File | Contents | |------|----------| | 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 |

Advanced Examples

| Example | Pattern 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 |

Critical Implementation Notes

Adding Dependencies

Use npm install to add dependencies rather than manually writing version numbers:

npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod

This lets npm resolve the latest compatible versions. Never specify version numbers from memory.

TypeScript Server Execution

Use tsx as a devDependency for running TypeScript server files:

npm install -D tsx
"scripts": {
  "serve": "tsx server.ts"
}

Note: The SDK examples use bun but generated projects should use tsx for broader compatibility.

Handler Registration Order

Register ALL handlers BEFORE calling 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();

Tool Visibility

Control who can access tools via _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"] } }

Host Styling Integration

Vanilla JS - Use helper functions:

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:

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():

.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.

Safe Area Handling

Always respect 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`;
  }
};

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.

Spec: 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
};

Use cases: | Pattern | Example | |---------|---------| | Code preview | Show streaming code in <pre>, render on complete (examples/shadertoy-server/) | | 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):

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:

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"));

Fullscreen Mode

Request fullscreen via app.requestDisplayMode(). Check availability in host context:

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:

.main { border-radius: var(--border-radius-lg); overflow: hidden; }
.main.fullscreen { border-radius: 0; }

See examples/shadertoy-server/ for complete implementation.

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

Testing

Using basic-host

Test MCP Apps locally with the basic-host example:

# 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

Configure SERVERS with a JSON array of your server URLs (default: http://localhost:3001/mcp).

Debug with sendLog

Send debug logs to the host application (rather than just the iframe's dev console):

await app.sendLog({ level: "info", data: "Debug message" });
await app.sendLog({ level: "error", data: { error: err.message } });