MCP Apps, announced January 2026, extend the Model Context Protocol with a mechanism for servers to return interactive HTML interfaces that render directly inside the AI client. Instead of getting a text summary of a chart, you get the chart itself — interactive, scrollable, drillable — embedded in the same conversation window.
The extension is intentionally minimal. It doesn’t redesign the protocol. It adds two things: a convention for declaring that a tool has an associated UI, and a resource type (ui://) for serving that UI. Everything else — JSON-RPC communication, tool invocation, streaming — stays the same.
The Two Primitives
MCP Apps extend the protocol through two additions to existing primitives.
Tools with UI metadata. A standard MCP tool declaration gets an additional _meta.ui.resourceUri field in its schema. This field points to a ui:// resource that the host can preload before the tool runs. When the AI client sees this field, it knows to fetch and render the UI resource alongside the tool’s text output.
registerAppTool(server, "visualize-data", { title: "Visualize Data", description: "Returns an interactive chart visualization", inputSchema: { type: "object", properties: { data: { type: "array" } } }, _meta: { ui: { resourceUri: "ui://charts/interactive" } }}, async (args) => { return { content: [{ type: "text", text: JSON.stringify(args.data) }] };});The tool handler itself returns the data as a standard text content block. The UI layer handles rendering — the tool doesn’t know or care whether it’s running inside a host with App support.
App resources. A ui:// resource contains the full HTML/JavaScript bundle that the host renders. It’s served by the server on demand, registered using registerAppResource from the @modelcontextprotocol/ext-apps SDK. The host fetches this once (or caches it), then renders it in a sandboxed iframe.
const resourceUri = "ui://dashboard/main.html";
registerAppResource(server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, async () => { const html = await fs.readFile("./dist/dashboard.html", "utf-8"); return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }; });As of the January 2026 release, only text/html content is supported. External URL iframes, remote DOM access, and native widgets are deferred to future iterations.
How the Host Renders the App
When a user invokes a tool that declares a ui:// resource, the host:
- Fetches the HTML bundle from the server via the MCP resource protocol
- Renders it in a sandboxed iframe within the conversation view
- Passes the tool’s result data to the iframe via
postMessage - Establishes a JSON-RPC channel over
postMessagefor bidirectional communication
The iframe is sandboxed — no access to the parent window’s DOM, no access to host cookies, no ability to navigate the parent page, no script execution in the parent context. The app runs in an isolated environment with only what the MCP channel provides.
The Client SDK
Inside the iframe, the @modelcontextprotocol/ext-apps client SDK manages communication with the host:
import { App } from "@modelcontextprotocol/ext-apps";
const app = new App({ name: "Dashboard App", version: "1.0.0" });
// Receives data from the tool resultapp.ontoolresult = (result) => { const data = JSON.parse(result.content?.find(c => c.type === "text")?.text); renderChart(data);};
// Calls server tools from UI interactions (drill-down, filter changes)async function drillDown(segment) { const result = await app.callServerTool({ name: "get-segment-details", arguments: { segment } }); updateVisualization(result);}
// Updates the AI's context with what the user did in the UIapp.updateModelContext({ action: "filtered", dimension: "country", value: "FR" });
app.connect();Three SDK methods do the heavy lifting:
app.ontoolresult— receives the initial data payload when the tool executesapp.callServerTool()— initiates additional tool calls from the UI (drill-down, filter, export)app.updateModelContext()— tells the AI what the user just did in the interface, so follow-up conversation is context-aware
The last one is what makes MCP Apps genuinely different from embedding a chart library. The AI knows that you filtered to France and clicked the “revenue by device” breakdown. Your next question — “why did mobile revenue drop?” — arrives with that context already in place.
Preloading and Latency
The _meta.ui.resourceUri in the tool declaration serves a specific purpose: it lets hosts preload the UI bundle before the tool executes. The host fetches the HTML resource during initialization rather than waiting for a tool invocation. This means the iframe renders immediately when the tool completes, rather than adding a separate round trip.
In practice, each tool call still involves round-trip latency. If your visualization depends on a subsequent get-segment-details call, that’s another round trip. For exploratory analysis this is acceptable; for production dashboards expecting sub-second response, it’s a real constraint.
Security Model
MCP Apps inherit the MCP security model and add app-specific constraints.
The sandboxed iframe is the primary isolation boundary. Standard iframe sandbox restrictions apply: no allow-same-origin, no allow-top-navigation, restricted allow-scripts. The app cannot escape its iframe to access the parent window or other iframes in the host.
Pre-declared templates — the ui:// resources registered at server startup — enable security review before execution. Unlike dynamically generated HTML, a ui:// resource is declared in advance, allowing host operators to audit what UI code runs. Hosts can require explicit user approval for UI-initiated tool calls (calls made via app.callServerTool()), creating a consent checkpoint between user actions and server-side operations.
The broader MCP ecosystem has real security challenges that MCP Apps inherit. Equixly’s research found command injection flaws in 43% of analyzed MCP servers. Docker’s guidance recommends containerizing every MCP server with CPU/memory caps and read-only filesystems. Maintaining an allowlist of approved servers and auditing message logs should be standard practice for any production MCP deployment.
Bundling Requirements
The ui:// resource must be a single self-contained HTML file. No external CDN references, no lazy-loaded modules, no separate stylesheet links — everything has to be inlined. Vite with vite-plugin-singlefile is the standard build tool for this constraint:
npm create vite@latest my-app -- --template reactnpm add -D vite-plugin-singlefile# Add to vite.config.ts: plugins: [viteSingleFile()]npm run build# dist/index.html is your deployable bundleThe MCP Apps SDK ships starter templates for React, Vue, Svelte, Preact, Solid, and vanilla JavaScript. Any of these work — the host doesn’t care which framework generated the HTML, only that the bundle is self-contained and connects via the App SDK.
What This Enables
The practical impact for data workflows: you go from six context switches (write SQL → execute → export CSV → import to BI → build viz → share link) to zero. The visualization lives in the conversation. You ask a question, see a chart, ask a follow-up, and the AI knows what you’re looking at.
The limitation is the flip side: MCP Apps are for exploration, not production. There’s no persistent dashboard URL, no shared view, no scheduled refresh. See MCP Apps vs Traditional BI for where this fits in your overall analytics stack.