Add a Claude Code plugin (`plugins/mcp-apps/`) for distribution via the plugin marketplace. The plugin provides the "Create MCP App" skill that guides the agent through building MCP Apps with interactive UIs. The skill includes: - Instructions to clone SDK repo for reference templates - Framework selection guidance (React, Vanilla JS, Vue, Svelte, etc.) - Tool visibility configuration (`_meta.ui.visibility`) - Host styling integration patterns - Testing workflow using `basic-host` - Common mistakes to avoid Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
7.8 KiB
| name | description |
|---|---|
| Create MCP App | This skill should be used when the user asks to "create an MCP App", "add a UI to an MCP tool", "build an interactive MCP widget", "scaffold an MCP App", or needs guidance on MCP Apps SDK patterns, UI-resource registration, MCP App lifecycle, or host integration. Provides comprehensive guidance for building MCP Apps with interactive UIs. |
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 source:
| 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/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); // Handles theme, styles, fonts automatically
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`;
}
};
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
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 } });