This skill provides guidance for building MCP Apps, the official MCP extension (io.modelcontextprotocol/ui) for rendering interactive HTML UIs inside MCP hosts. This skill should be used when the user asks to "create an MCP App", "add UI to MCP tool", "build interactive MCP", "MCP App server", "ui:// resource", "sandboxed iframe MCP", "interactive chat UI", "embed UI in chat", "MCP tool with interface", or needs to build interactive HTML applications that render inside Claude Desktop, ChatGPT, or VS Code Copilot.
From bopen-toolsnpx claudepluginhub b-open-io/claude-plugins --plugin bopen-toolsThis skill uses the workspace's default tool permissions.
references/build-guide.mdreferences/client-matrix.mdreferences/draft-spec-details.mdreferences/host-integration.mdreferences/patterns.mdreferences/protocol.mdreferences/security.mdSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides agentic engineering workflows: eval-first loops, 15-min task decomposition, model routing (Haiku/Sonnet/Opus), AI code reviews, and cost tracking.
MCP Apps is the first official MCP extension (spec 2026-01-26, co-authored by Anthropic and OpenAI). It enables interactive HTML UIs rendered in sandboxed iframes inside MCP hosts. Extension ID: io.modelcontextprotocol/ui. npm package: @modelcontextprotocol/ext-apps.
MCP Apps bridge the gap between LLM tool calls and rich visual interfaces — the model sees text, users see interactive UIs.
The fastest path is the official create-mcp-app skill from the ext-apps repo:
npx skills add modelcontextprotocol/ext-apps --skill create-mcp-app
Then ask the agent: "Create an MCP App that displays a color picker." For manual setup, see references/build-guide.md.
Three layers:
ui:// resources. Tools declare a _meta.ui.resourceUri pointing to the UI. Resources serve HTML via RESOURCE_MIME_TYPE.App class from @modelcontextprotocol/ext-apps to communicate with the Host.The View is intentionally thin. All tool calls go through the Host proxy — the View never reaches the network or the MCP server directly.
Install the package:
npm install @modelcontextprotocol/ext-apps
Register tools and resources using the helper functions:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
registerAppTool,
registerAppResource,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { readFile } from "fs/promises";
const server = new McpServer({ name: "my-app", version: "1.0.0" });
// Register a tool that exposes a UI
registerAppTool(
server,
"my-tool",
{
description: "Does something useful",
inputSchema: { type: "object", properties: {} },
_meta: {
ui: { resourceUri: "ui://myapp/index.html" },
},
},
async (args) => ({
content: [{ type: "text", text: "Model sees this text" }],
structuredContent: { data: "UI gets this rich data" },
})
);
// Register the HTML resource
registerAppResource(
server,
"ui://myapp/index.html",
"ui://myapp/index.html",
{ mimeType: RESOURCE_MIME_TYPE },
async () => ({
contents: [
{
uri: "ui://myapp/index.html",
mimeType: RESOURCE_MIME_TYPE,
text: await readFile("dist/index.html", "utf-8"),
},
],
})
);
ui:// resources use the MIME type text/html;profile=mcp-app. They must be predeclared in the server manifest — dynamic resource generation is not permitted (security requirement for pre-scanning).
The View is the HTML app. Install the client package:
npm install @modelcontextprotocol/ext-apps
import { App } from "@modelcontextprotocol/ext-apps";
const app = new App({ name: "My App", version: "1.0.0" });
// CRITICAL: Set handlers BEFORE calling connect()
app.ontoolresult = (result) => {
// result.structuredContent has rich data for the UI
// result.content has text (what model sees)
renderData(result.structuredContent ?? result.content);
};
app.onhostcontextchanged = (ctx) => {
// Apply host theme, locale, timezone
applyTheme(ctx.theme);
};
// Connect after handlers are set
await app.connect();
Set ontoolresult before or immediately after connect(). The initial tool result is buffered, so either order works, but setting handlers first is safer to avoid race conditions.
io.modelcontextprotocol/ui in experimental capabilities.ui/initialize. Server responds with supported UI version.ui/notifications/tool-input to View → Tool executes → Host forwards ui/notifications/tool-result to View.app.callServerTool(). Host proxies them. Results flow back via ontoolresult.ui/notifications/resource-teardown when the iframe is destroyed.Control which audience sees each tool:
_meta: {
ui: {
resourceUri: "ui://myapp/index.html",
visibility: ["app"], // UI-only, hidden from the model
}
}
| Visibility | Default | Behavior |
|---|---|---|
["model", "app"] | Yes | Both model and UI can call the tool |
["app"] | No | UI-only tool, hidden from LLM |
["model"] | No | LLM-only, View cannot call it |
Use ["app"] for tools that only make sense as UI interactions (pagination, sorting, drill-down).
MCP App Views must be compiled to a single self-contained HTML file. Use Vite with vite-plugin-singlefile:
bun add -d vite vite-plugin-singlefile
// vite.config.ts
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
export default defineConfig({
plugins: [viteSingleFile()],
build: {
rollupOptions: { input: "src/views/mcp-app.html" },
outDir: "dist",
emptyOutDir: false,
},
});
Any framework works: React, Vue, Svelte, Preact, Solid, or vanilla JS/HTML. The View is just HTML — no special runtime.
Views render inside srcdoc iframes. This means:
import { App } from "@modelcontextprotocol/ext-apps" cannot resolve without a bundler<script src=""> tags fail — external script tags don't work in srcdoc iframesbun add leaflet), import them in your view TS file, and let Vite bundle themTool results MUST include _meta.viewUUID for the host to create a UI instance:
return {
content: [{ type: "text", text: "Summary for the model" }],
structuredContent: { data: richData },
_meta: { viewUUID: randomUUID() },
};
The Host provides context via app.onhostcontextchanged:
interface HostContext {
theme: "light" | "dark" | "system";
locale: string; // e.g. "en-US"
timezone: string; // e.g. "America/New_York"
displayMode: "inline" | "fullscreen" | "pip";
containerDimensions: { width: number; height: number };
platform: "desktop" | "web" | "mobile";
}
CSS variables provided by the Host sandbox:
:root {
--color-background-primary: /* host bg */;
--color-text-primary: /* host text */;
--color-border: /* host border */;
--color-accent: /* host accent */;
}
Always include default values — not all hosts provide all CSS variables:
body {
background: var(--color-background-primary, #ffffff);
color: var(--color-text-primary, #000000);
}
| Mode | Use Case |
|---|---|
inline | Default. Embedded in the chat thread. Good for results, cards, small visualizations. |
fullscreen | Editors, dashboards, complex tools. Occupies the full panel. |
pip | Picture-in-picture. Persistent widget that survives scrolling (calendars, timers, music players). |
Declare the preferred display mode in _meta.ui:
_meta: {
ui: {
resourceUri: "ui://myapp/index.html",
displayMode: "fullscreen",
}
}
Tools degrade gracefully on hosts without UI support. Always populate both content (text for the model) and structuredContent (rich data for the View):
async (args) => ({
content: [
{ type: "text", text: `Found ${results.length} items: ${summary}` }
],
structuredContent: { items: results, total: results.length },
})
Non-UI hosts display content. UI hosts pass structuredContent to the View. This is the key design principle: MCP Apps are an enhancement, not a replacement.
Declare the extension in the server capabilities:
const server = new McpServer({
name: "my-app",
version: "1.0.0",
capabilities: {
experimental: {
"io.modelcontextprotocol/ui": { version: "0.1" },
},
},
});
Hosts that do not support MCP Apps ignore this capability and fall back to standard tool behavior.
Detailed protocol and integration documentation:
references/protocol.md — JSON-RPC methods, capability negotiation, message schemasreferences/security.md — Sandbox model, CSP, permissions, audit loggingreferences/patterns.md — App-only tools, streaming, multi-tool calls, state managementreferences/host-integration.md — AppBridge, @mcp-ui/client, AppRenderer, AppFramereferences/client-matrix.md — Host support (points to canonical source at modelcontextprotocol.io)references/build-guide.md — Complete project setup, configuration files, testing with Claude and basic-hostreferences/draft-spec-details.md — Draft spec additions: new CSS variables (70+), container dimensions, sandbox proxy, device capabilities, Vercel deployment