Agent skill
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 View", "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.
Install this agent skill to your Project
npx add-skill https://github.com/tldraw/tldraw/tree/main/apps/mcp-app/.claude/skills/create-mcp-app
SKILL.md
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 } })
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
convert-web-app
This skill should be used when the user asks to "add MCP App support to my web app", "turn my web app into a hybrid MCP App", "make my web page work as an MCP App too", "wrap my existing UI as an MCP App", "convert iframe embed to MCP App", "turn my SPA into an MCP App", or needs to add MCP App support to an existing web application while keeping it working standalone. Provides guidance for analyzing existing web apps and creating a hybrid web + MCP App with server-side tool and resource registration.
add-app-to-server
This skill should be used when the user asks to "add an app to my MCP server", "add UI to my MCP server", "add a view to my MCP tool", "enrich MCP tools with UI", "add interactive UI to existing server", "add MCP Apps to my server", or needs to add interactive UI capabilities to an existing MCP server that already has tools. Provides guidance for analyzing existing tools and adding MCP Apps UI resources.
migrate-oai-app
This skill should be used when the user asks to "migrate from OpenAI Apps SDK", "convert OpenAI App to MCP", "port from window.openai", "migrate from skybridge", "convert openai/outputTemplate", or needs guidance on converting OpenAI Apps SDK applications to MCP Apps SDK. Provides step-by-step migration guidance with API mapping tables.
pr-walkthrough
Create a narrated video walkthrough of a pull request with code slides and audio narration. Use when asked to create a PR walkthrough, PR video, or walkthrough video.
write-docs
Writing SDK documentation for tldraw. Use when creating new documentation articles, updating existing docs, or when documentation writing guidance is needed. Applies to docs in apps/docs/content/.
write-e2e-tests
Writing Playwright E2E tests for tldraw. Use when creating browser tests, testing UI interactions, or adding E2E coverage in apps/examples/e2e or apps/dotcom/client/e2e.
Didn't find tool you were looking for?