A get_forecast tool returns thirty lines of JSON — temperatures, wind vectors, an hourly array. The model reads it, then does what models do: it flattens the structure into a paragraph of prose. "Tomorrow looks mild, with a chance of rain in the afternoon." Accurate, and yet you have lost every affordance the data actually wanted — the hover-to-inspect chart, the toggle between Celsius and Fahrenheit, the little map. The tool had a shape in mind. The chat squashed it flat.
That gap is what MCP Apps closes. As of January 2026 it is the first official extension to the Model Context Protocol (tracked as SEP-1865), and it standardizes one specific thing: how an MCP server ships an interactive UI alongside a tool, so a host can render it in the conversation instead of narrating the JSON.
Why a standard, and not just "return some HTML"
The idea of returning a UI from a tool isn't new. Two implementations already proved the demand — the community's MCP-UI project and OpenAI's Apps SDK — and they were mutually incompatible. If you built for one, you maintained a separate adapter for the other, and a server had no reliable way to even ask "does this host support UI?" before trying.
MCP Apps is the reconciliation. It was authored jointly by MCP core maintainers at Anthropic and OpenAI together with the MCP-UI creators, and it folds both architectures into one wire format. The payoff is the boring, valuable kind: write the app once, and it renders in Claude, ChatGPT, VS Code, Goose, Postman, and the rest — without per-host branches.
The design decision that makes everything else work is that the UI is a resource, declared separately from the tool result.
A UI resource lives at a ui:// URI with the MIME type text/html;profile=mcp-app. It is the template — the static HTML/JS shell. The tool then links to it through a metadata field, _meta.ui.resourceUri:
{
"tools": [
{
"name": "get_forecast",
"description": "Fetch the weather forecast",
"_meta": { "ui": { "resourceUri": "ui://weather/forecast" } }
}
],
"resources": [
{
"uri": "ui://weather/forecast",
"mimeType": "text/html;profile=mcp-app",
"text": "<!doctype html> ..."
}
]
}
Because the template is declared at registration time — not smuggled into a tool response — the host can fetch it with a normal resources/read, inspect it, and cache it before the tool ever runs. Prefetching improves the perceived speed of the first render, and the review step is a genuine security control: the host sees the exact HTML it's about to execute. Presentation (the template) and data (the tool result) stay on separate tracks, which also means one template can be reused across many calls.
The iframe is a sandbox with a contract
When the tool fires, the host renders that resource inside a sandboxed iframe and wires up a message channel. Two things are worth understanding here.
First, the sandbox is not decorative. The host builds a Content-Security-Policy from the resource's declared metadata, starting from default-src 'none' and opening only the specific external domains the app asked for. An app that never declared a domain cannot phone home to it.
Second, the iframe and the host talk over bidirectional JSON-RPC carried on postMessage — the same RPC style MCP already uses everywhere else, rather than a bespoke iframe-ready hack. The exchange follows a defined lifecycle: the view sends ui/initialize and gets back host context; the host delivers tool arguments and results (including structuredContent); the user interacts; and on the way out the host sends ui/resource-teardown before unmounting. Inside that window the view can issue real requests — tools/call to run another tool, ui/update-model-context to feed something back into the model, ui/open-link to ask for external navigation.
The SDK hides the plumbing. On the client side it is roughly:
import { App } from "@modelcontextprotocol/ext-apps";
const app = new App();
await app.connect();
// host pushes a fresh tool result into the view
app.ontoolresult = ({ structuredContent }) => render(structuredContent);
// user clicks "refresh" inside the iframe → call back to the server
async function refresh(city) {
const { structuredContent } = await app.callServerTool("get_forecast", { city });
render(structuredContent);
}
That callServerTool is the part that makes these apps and not just static cards. The rendered UI is a live client of the same MCP server: a click in the chart can trigger another tool call, the server can push fresh data back in, and the model stays in the loop through the context updates. Data flows both ways.
The trust boundary is the interesting part
The obvious objection to "servers can now inject UI into my chat" is security, and the spec treats it as a first-class concern rather than an afterthought. Three properties are worth internalizing before you ship anything:
UI content runs in a restricted sandbox, every host/app message is loggable JSON-RPC, and UI-initiated tool calls can require explicit user consent.
Those aren't equivalent to "safe." An app is still arbitrary code the server controls, and a tool call kicked off from a button click is a tool call — with whatever side effects that tool has. If you're the host, the review-before-render step and the consent gate are your leverage; use them. If you're the server author, assume the strict CSP is the default and declare exactly the domains you need, because anything you forgot to declare simply won't connect.
What to do with this now
If you already maintain an MCP server, the practical next step is small: pick one tool whose output is genuinely structured — a dashboard, a form, a chart, a multi-step flow — and give it a ui:// template plus a _meta.ui.resourceUri link. Keep the plain-text result too, because hosts that don't support the extension will fall back to it, and that graceful degradation is the point of a standard. Then let the interactive version take over wherever it's supported.
The mental shift is the takeaway: a tool result no longer has to be text the model paraphrases. It can be a surface the user operates directly, backed by the same server, inside the same conversation. The forecast can be a chart again.
Sources: MCP Apps — Bringing UI Capabilities to MCP Clients (MCP Blog, 2026-01-26), MCP Apps overview & specification (apps.extensions.modelcontextprotocol.io), SEP-1865 / ext-apps specification (GitHub)