Saltar al contenido principal

MCP Apps: Interactive UIs Inside a Tool Call

Avanzado

For its entire first year MCP was a text protocol: a tool call returned JSON or Markdown and the client rendered whatever it liked. MCP Apps — the first official extension, finalized January 26, 2026 and folded into the 2026-07-28 core spec — adds a UI channel. A server can now ship a chunk of HTML, the client renders it in a sandboxed iframe, and the iframe talks back over JSON-RPC 2.0 on postMessage. Every write still passes through the same audit path as any other tool call. Think interactive report + confirm-before-execute, not "run a full webapp inside Claude."

What you'll learn
  • What MCP Apps actually is — and the four things it deliberately is NOT
  • The wire shape: capability negotiation, ui:// resources, the tool _meta.ui link, and the postMessage bridge
  • The security model — sandbox, CSP, Permission Policy, host approval — and where it still leaves you exposed
  • When to reach for an App instead of a plain tool result (rare-but-worth-it patterns)
  • How this composes with the rest of the 2026-07-28 stateless spec you already speak

The one-paragraph version

The server declares one or more UI resources at ui://<server>/<name> URIs with MIME type text/html;profile=mcp-app. A tool advertises "I have a UI" by putting _meta.ui.resourceUri on its schema. On a tools/call the server can return the usual text/data plus a reference to the UI resource; the client fetches the HTML with resources/read, drops it into a sandboxed <iframe>, and now the iframe and the host chat over postMessage using JSON-RPC 2.0. UI-initiated tool calls still require the same user approval as any other tool call — the App can render, it can propose, it cannot silently execute.

What MCP Apps is NOT

Watch out
  • Not a full webapp runtime. The iframe defaults to no network (`connect-src 'none'`), no top-level navigation, and no third-party scripts. If your idea needs to load React from a CDN and call your own API, this isn't it.
  • Not a way to bypass tool-call approval. UI actions that mutate anything still travel as normal JSON-RPC tool calls the host can log, throttle, and require explicit user consent for.
  • Not sanitization. The sandbox limits what the UI can DO, not what it can trick a user into typing. Treat every App like third-party code — allowlist which servers can render UI at all.
  • Not persistent. There is no session; when the tool call ends, the iframe goes away. Any state you need across calls lives in a server-side handle you return, exactly like the rest of the stateless spec.

The four moving parts

Guided walkthrough1 of 4
  1. The client advertises the extension in its per-request _meta.capabilities under the reverse-DNS namespace io.modelcontextprotocol/ui, listing the MIME types it can render. A server that sees no such capability just skips the UI channel and returns plain results — the whole extension is opt-in on both sides.

Wire shapes you'll actually type

Capability advertisement on a client request:

{
"jsonrpc": "2.0",
"method": "tools/call",
"params": { "name": "get_weather", "arguments": { "location": "Milan" } },
"_meta": {
"capabilities": {
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
}

The tool schema, on the server side:

{
"name": "get_weather",
"description": "Get current weather and a 7-day forecast for a location.",
"inputSchema": {
"type": "object",
"properties": { "location": { "type": "string" } },
"required": ["location"]
},
"_meta": {
"ui": {
"resourceUri": "ui://weather-server/dashboard-template",
"visibility": ["model", "app"]
}
}
}

The UI resource declared next to it:

{
"uri": "ui://weather-server/dashboard-template",
"mimeType": "text/html;profile=mcp-app",
"_meta": {
"ui": {
"connectDomains": [],
"permissions": []
}
}
}

And the first message the iframe sends back:

UI → host: JSON-RPC 2.0 over postMessage

window.parent.postMessage(
{
  jsonrpc: "2.0",
  id: 1,
  method: "ui/initialize",
  params: {
    toolName: "get_weather",
    toolArguments: { location: "Milan" },
    toolResult: /* whatever the server returned alongside the UI */
  }
},
"*"
);

Everything after that — asking the host to call another tool, subscribing to a resource, sending a UI-side event — is more JSON-RPC 2.0 on the same channel, structured identically to the wire protocol you already speak on the server side. That's the whole point: UI developers can use the standard @modelcontextprotocol/sdk instead of learning a bespoke shim.

Security model in one screen

What you'll learn
  • Sandbox: content renders in a sandboxed <iframe>. Default: no top-level navigation, no forms to arbitrary origins, no popups, no plugins.
  • CSP by default: connect-src 'none'. The UI cannot fetch() anywhere. To allow specific origins, the server declares them in the resource's _meta.ui.connectDomains and the host builds the CSP header from that list.
  • Permission Policy: the resource's _meta.ui.permissions map into the iframe's allow attribute — camera, microphone, geolocation, clipboard-write. Nothing is granted implicitly.
  • Every UI-initiated write is a normal tool call. The host validates it, may require user approval, and logs it in the same audit stream as a model-initiated call.
  • Templates are prefetchable and hashable. A host that pins to a specific hash catches a swap-under-you attack; a host that doesn't gets whatever the server serves that day.

When to actually reach for an App

The bar is high on purpose — every App is client surface area for you and attack surface for the user. Reach for one only when a UI beats a chat turn by a clear margin:

  • Data-grid confirms. "Here are 47 rows I'm about to update — uncheck the ones you don't want." A chat rendering of that is either enormous or dishonest; a grid with checkboxes is honest and fast.
  • Chart-driven approval. "Here's the query plan / cost projection / trace waterfall. Approve or reject." Charts are cheap in HTML and terrible as ASCII.
  • Structured pickers where the shape of the input is not text. Date-range with a mini calendar, tree-select over a filesystem or an org chart, map picker with a bounding box.
  • In-place editors for the small step where a diff view + accept/reject beats regenerating the whole answer.

If your idea is "let me embed a whole dashboard," it doesn't belong here — link out. If your idea is "let me run arbitrary user code in the sandbox," it definitely doesn't belong here.

The 2025-11-25 compatibility contract

MCP Apps rides on the extension framework introduced in the 2026-07-28 core spec, but the extension itself has been Final since 2026-01-26. Practical implication: a client that only speaks the older 2025-11-25 revision plus this extension can already render Apps; a stateless 2026-07-28 client picks it up as one entry in its per-request capability map. Because the extension is opt-in on both sides, a server that adds it never breaks an old client — it just skips the UI branch. This is the shape every future MCP extension will take, so understanding this handshake pays down debt for the ones to come.

Where MCP Apps lands on the AILmanac map

  • MCP 2026-07-28: The Stateless Spec — the core protocol Apps rides on; especially the Extensions framework section (SEP-2133).
  • MCP & Connecting to Tools — the API-side connector. It's the client that will render the iframe; today's connector abstracts the transport, so Apps flows through unchanged.
  • MCP in Claude Code — where Claude Code's MCP support is going; UI rendering is a client capability, not a server capability, so this is the surface that decides which Apps ever render for you.
  • Securing MCP Servers — the hardening pattern for the servers on the other end of that iframe.

Quick check

Check yourself

0/4
  1. How does an MCP Apps iframe talk back to the host?
  2. By default, what network access does an MCP Apps iframe have?
  3. A tool wants to render a UI. What is the ONE piece of metadata that turns a plain tool into a UI-carrying one?
  4. A UI inside the iframe wants to mutate something on the server. What happens?

Vocabulary

MCP Apps terminology
Pulsa Intro o Espacio para girar la tarjeta. Usa las flechas izquierda y derecha para moverte entre las tarjetas.Término mostrado.
1 / 9

Sources & further reading

Next