Add UI to MCP Server
Enrich an existing MCP server's tools with interactive UIs using the MCP Apps SDK (@modelcontextprotocol/ext-apps).
How It Works
Existing tools get paired with HTML resources that render inline in the host's conversation. The tool continues to work for text-only clients — UI is an enhancement, not a replacement. Each tool that benefits from UI gets linked to a resource via _meta.ui.resourceUri, and the host renders that resource in a sandboxed iframe when the tool is called.
Getting Reference Code
Clone the SDK repository for working examples and API documentation:
API Reference (Source Files)
Read JSDoc documentation directly from /tmp/mcp-ext-apps/src/:
Key Examples (Mixed Tool Patterns)
These examples demonstrate servers with both App-enhanced and plain tools — the exact pattern you're adding:
Framework Templates
Learn and adapt from /tmp/mcp-ext-apps/examples/basic-server-{framework}/:
Step 1: Analyze Existing Tools
Before writing any code, analyze the server's existing tools and determine which ones benefit from UI.
- Read the server source and list all registered tools
- For each tool, assess whether it would benefit from UI (returns data that could be visualized, involves user interaction, etc.) vs. is fine as text-only (simple lookups, utility functions)
- Identify tools that could become app-only helpers (data the UI needs to poll/fetch but the model doesn't need to call directly)
- Present the analysis to the user and confirm which tools to enhance
Decision Framework
Step 2: Add Dependencies
Plus framework-specific dependencies if needed (e.g., react, react-dom, @vitejs/plugin-react for React), and @modelcontextprotocol/node / @modelcontextprotocol/express if the server uses HTTP transports.
ext-apps 2.x requires the split base MCP SDK packages at ^2.0.0
(@modelcontextprotocol/core comes in transitively). Existing servers that
still import @modelcontextprotocol/sdk v1 must migrate those imports and
handler schemas before adding the App integration:
Step 3: Set Up the Build Pipeline
Vite Configuration
Create vite.config.ts with vite-plugin-singlefile to bundle the UI into a single HTML file:
HTML Entry Point
Create mcp-app.html (or one per distinct UI if tools need different views):
Build Scripts
Add build scripts to package.json. The UI must be built before the server code bundles it:
Step 4: Convert Tools to App Tools
Transform plain MCP tools into App tools with UI.
Before (plain MCP tool):
After (App tool with UI):
Key guidance:
- Always keep the
contentarray with a text fallback for text-only clients - Add
structuredContentfor data the UI needs to render - Link the tool to its resource via
_meta.ui.resourceUri - Leave tools that don't benefit from UI unchanged — they stay as plain tools
Step 5: Register Resources
Register the HTML resource so the host can fetch it:
If multiple tools share the same UI, they can reference the same resourceUri and the same resource registration.
Step 6: Build the UI
Handler Registration
Register ALL handlers BEFORE calling app.connect():
Host Styling
Use host CSS variables for theme integration:
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 the full list.
For React apps, use the useApp and useHostStyles hooks instead — see basic-server-react/ for the pattern.
Optional Enhancements
App-Only Helper Tools
Tools the UI calls but the model doesn't need to invoke directly (polling, pagination, chunk loading):
The UI calls these via
app.callServerTool({ name: "poll-data", arguments: {} }).
CSP Configuration
If the UI needs to load external resources (fonts, APIs, CDNs), declare the domains:
Streaming Partial Input
For large tool inputs, show progress during LLM generation:
Graceful Degradation with getUiCapability()
Conditionally register App tools only when the client supports UI, falling back to text-only tools:
Fullscreen Mode
Allow the UI to expand to fullscreen:
Common Mistakes to Avoid
- Forgetting text
contentfallback — Always includecontentarray with text for non-UI hosts - Registering handlers after
connect()— Register ALL handlers BEFORE callingapp.connect() - Missing
vite-plugin-singlefile— Without it, assets won't load in the sandboxed iframe - Forgetting resource registration — The tool references a
resourceUrithat must have a matching resource - Hardcoding styles — Use host CSS variables (
var(--color-*)) for theme integration - Not handling safe area insets — Always apply
ctx.safeAreaInsetsinonhostcontextchanged
Testing
Using basic-host
Test the enhanced server with the basic-host example:
Configure SERVERS with a JSON array of your server URLs (default: http://localhost:3001/mcp).
Verify
- Plain tools still work and return text output
- App tools render their UI in the iframe
ontoolinputhandler fires with tool argumentsontoolresulthandler fires with tool result- Host styling (theme, fonts, colors) applies correctly


