Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-13 | ✗→✓ | ▲ Improved | 69% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 34% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 63% | 0% |
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.
Every MCP App requires two parts linked together:
_meta.ui.resourceUri references the resourceHost calls tool → Server returns result → Host renders resource UI → UI receives result| 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 |
Adding to existing MCP server:
registerAppTool, registerAppResource from SDK_meta.ui.resourceUriCreating new MCP server:
vite-plugin-singlefileClone the SDK repository for working examples and API documentation:
bashgit clone --branch "v$(npm view @modelcontextprotocol/ext-apps version)" --depth 1 https://github.com/modelcontextprotocol/ext-apps.git /tmp/mcp-ext-apps
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:
server.ts with registerAppTool and registerAppResourcevite.config.ts with vite-plugin-singlefilepackage.json with all required dependencies.gitignore excluding node_modules/ and dist/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 |
| 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 |
Use npm install to add dependencies rather than manually writing version numbers:
bashnpm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
Use tsx as a devDependency for running TypeScript server files:
bashnpm install -D tsx
json"scripts": { "serve": "tsx server.ts" }
Note: The SDK examples use bun but generated projects should use tsx for broader compatibility.
Register ALL handlers BEFORE calling app.connect():
typescriptconst 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()
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"] } }
Vanilla JS - Use helper functions:
typescriptimport { 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:
typescriptimport { 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.
Always respect safeAreaInsets:
typescriptapp.onhostcontextchanged = (ctx) => { if (ctx.safeAreaInsets) { const { top, right, bottom, left } = ctx.safeAreaInsets document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px` } }
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
typescriptapp.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):
typescriptapp.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) }
Pause expensive operations (animations, WebGL, polling) when view scrolls out of viewport:
typescriptconst 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'))
Request fullscreen via app.requestDisplayMode(). Check availability in host context:
typescriptlet 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.connect()vite-plugin-singlefile_meta.ui.resourceUrictx.safeAreaInsetscontent array for non-UI hostsontoolinputpartial to show progress during generationTest MCP Apps locally with the basic-host example:
bash# 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).
Send debug logs to the host application (rather than just the iframe's dev console):
typescriptawait app.sendLog({ level: 'info', data: 'Debug message' }) await app.sendLog({ level: 'error', data: { error: err.message } })
Other measured skills in the registry, with their headline benchmark lift.