Create MCP App
About
This skill helps developers build MCP Apps with interactive UIs that run in Claude Desktop and other MCP hosts. It provides comprehensive guidance on SDK patterns, UI-resource registration, and the tool+resource architecture. Use it when creating MCP Apps, adding UIs to tools, or scaffolding interactive MCP Views.
Quick Install
Claude Code
Recommended/plugin add https://github.com/majiayu000/claude-skill-registrygit clone https://github.com/majiayu000/claude-skill-registry.git ~/.claude/skills/Create MCP AppCopy and paste this command in Claude Code to install this skill
Documentation
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:
- Tool - Called by the LLM/host, returns data
- Resource - Serves the bundled HTML UI that displays the data
- Link - The tool's
_meta.ui.resourceUrireferences 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,registerAppResourcefrom 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.tswithregisterAppToolandregisterAppResource - Client-side app with all lifecycle handlers
vite.config.tswithvite-plugin-singlefilepackage.jsonwith all required dependencies.gitignoreexcludingnode_modules/anddist/
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
- 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
contentarray for non-UI hosts - Hardcoded styles - Use host CSS variables for theme integration
- No streaming for large inputs - Use
ontoolinputpartialto 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 } });
GitHub Repository
Related Skills
content-collections
MetaThis skill provides a production-tested setup for Content Collections, a TypeScript-first tool that transforms Markdown/MDX files into type-safe data collections with Zod validation. Use it when building blogs, documentation sites, or content-heavy Vite + React applications to ensure type safety and automatic content validation. It covers everything from Vite plugin configuration and MDX compilation to deployment optimization and schema validation.
creating-opencode-plugins
MetaThis skill provides the structure and API specifications for creating OpenCode plugins that hook into 25+ event types like commands, files, and LSP operations. It offers implementation patterns for JavaScript/TypeScript modules that intercept and extend the AI assistant's lifecycle. Use it when you need to build event-driven plugins for monitoring, custom handling, or extending OpenCode's capabilities.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
Algorithmic Art Generation
MetaThis skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.
