InterviewsVector

Legacy mirror · noindex · attributed reading

MCP Apps

Text-only tool output caps what agents can show. MCP Apps (SEP-1724, official January 26, 2026) let a tool return sandboxed interactive HTML rendered inline in Claude Desktop, C…

Source and authorship: this is an attributed reading mirror from ai-engineering-from-scratch by Rohit Ghumare under the MIT License. InterviewsVector does not claim this lesson text. It is excluded from indexing and from original Academy completion metrics.

View upstream source

MCP Apps — Interactive UI Resources via ui://

Text-only tool output caps what agents can show. MCP Apps (SEP-1724, official January 26, 2026) let a tool return sandboxed interactive HTML rendered inline in Claude Desktop, ChatGPT, Cursor, Goose, and VS Code. Dashboards, forms, maps, 3D scenes, all through one extension. This lesson walks the ui:// resource scheme, the text/html;profile=mcp-app MIME, the iframe-sandbox postMessage protocol, and the security surface that comes with letting a server render HTML.

Type: Build Languages: Python (stdlib, UI resource emitter), HTML (sample app) Prerequisites: Phase 13 · 07 (MCP server), Phase 13 · 10 (resources) Time: ~75 minutes

Learning Objectives

  • Return a ui:// resource from a tool call and set the correct MIME and metadata.
  • Declare a tool's associated UI with _meta.ui.resourceUri, _meta.ui.csp, and _meta.ui.permissions.
  • Implement the iframe sandbox postMessage JSON-RPC for UI-to-host communication.
  • Apply CSP and permissions-policy defaults that defend against UI-originated attacks.

The Problem

A 2025-era visualize_timeline tool can return "Here are 14 notes organized chronologically: ...". That is a paragraph. Users actually want the interactive timeline. Before MCP Apps, the options were: client-specific widget APIs (Claude artifacts, OpenAI Custom GPT HTML), or no UI at all.

MCP Apps (SEP-1724, shipped January 26, 2026) standardize the contract. A tool result contains a resource whose URI is ui://... and whose MIME is text/html;profile=mcp-app. The host renders it in a sandboxed iframe with a limited CSP and no network access unless explicitly granted. The UI inside the iframe posts messages to the host via a tiny postMessage JSON-RPC dialect.

Every compatible client (Claude Desktop, ChatGPT, Goose, VS Code) renders the same ui:// resource the same way. One server, one HTML bundle, universal UI.

The Concept

The ui:// resource scheme

A tool returns:

{ "content": [ {"type": "text", "text": "Here is your notes timeline:"}, {"type": "ui_resource", "uri": "ui://notes/timeline"} ], "_meta": { "ui": { "resourceUri": "ui://notes/timeline", "csp": { "defaultSrc": "'self'", "scriptSrc": "'self' 'unsafe-inline'", "connectSrc": "'self'" }, "permissions": [] } } }

The host then calls resources/read on the ui://notes/timeline URI and gets back:

{ "contents": [{ "uri": "ui://notes/timeline", "mimeType": "text/html;profile=mcp-app", "text": "<!doctype html>..." }] }

Iframe sandbox

The host renders the HTML inside a sandboxed <iframe> with:

  • sandbox="allow-scripts allow-same-origin" (or stricter per server declaration)
  • Server-declared CSP applied via response headers.
  • No cookies, no localStorage from the host's origin.
  • Network access limited to connectSrc in CSP.

postMessage protocol

The iframe communicates with the host via window.postMessage. A tiny JSON-RPC 2.0 dialect:

Always pin targetOrigin to the peer's exact origin, and on the receiving side validate event.origin against an allowlist before processing any payload. Never use "*" for either side of this channel — the body carries tool calls and resource reads.

// iframe to host (pin to host origin) window.parent.postMessage({ jsonrpc: "2.0", id: 1, method: "host.callTool", params: { name: "notes_update", arguments: { id: "note-14", title: "..." } } }, "https://host.example.com"); // host to iframe (pin to iframe origin) iframe.contentWindow.postMessage({ jsonrpc: "2.0", id: 1, result: { content: [...] } }, "https://iframe.example.com"); // receiver on both sides window.addEventListener("message", (event) => { if (event.origin !== "https://expected-peer.example.com") return; // safe to process event.data });

Available host-side methods the UI can call:

  • host.callTool(name, arguments) — invokes a server tool.
  • host.readResource(uri) — reads an MCP resource.
  • host.getPrompt(name, arguments) — fetches a prompt template.
  • host.close() — dismisses the UI.

Every call still goes through the MCP protocol and inherits the server's permissions.

Permissions

The _meta.ui.permissions list requests extra capabilities:

  • camera — access the user's camera (used for scan-a-document UIs).
  • microphone — voice input.
  • geolocation — location.
  • network:* — wider network access than connectSrc alone allows.

Each permission is a prompt the user sees before the UI renders.

Security risks

HTML in an iframe is still HTML. New attack surface:

  • Prompt-injection via UI. A malicious server UI can show text that looks like a system message and tricks the user. Host rendering should visibly distinguish server UI from host UI.
  • Exfiltration via connectSrc. If CSP permits connect-src: *, the UI can send data anywhere. Default should be strict.
  • Clickjacking. The UI overlays host chrome. Hosts must prevent z-index manipulation and enforce opacity rules.
  • Steal focus. UI takes keyboard focus and captures the next message. Hosts must intercept.

Phase 13 · 15 covers these in depth as part of MCP security; this lesson introduces them.

ui/initialize handshake

After the iframe loads, it sends ui/initialize over postMessage:

{"jsonrpc": "2.0", "id": 0, "method": "ui/initialize", "params": {"theme": "dark", "locale": "en-US", "sessionId": "..."}}

Host responds with capabilities and a session token. The UI uses the session token on every subsequent host call.

AppRenderer / AppFrame SDK primitives

The ext-apps SDK exposes two convenience primitives:

  • AppRenderer (server side) — wraps a React / Vue / Solid component and emits a ui:// resource with the right MIME and metadata.
  • AppFrame (client side) — receives the resource, mounts the iframe, and mediates postMessage.

You can use these or hand-roll the HTML and JSON-RPC.

Ecosystem status

MCP Apps shipped January 26, 2026. Client support as of April 2026:

  • Claude Desktop. Full support since January 2026.
  • ChatGPT. Full support via the Apps SDK (same underlying MCP Apps protocol).
  • Cursor. Beta; enable via settings.
  • VS Code. Insider builds only.
  • Goose. Full support.
  • Zed, Windsurf. Roadmapped.

Servers in production: dashboards, map visualizations, data tables, chart builders, sandbox IDE previews.

t3-ui-sandbox

Use It

code/main.py extends the notes server with a visualize_timeline tool that returns a ui://notes/timeline resource, plus a handler for resources/read on that URI which returns a small but complete HTML bundle with an SVG timeline. The HTML is stdlib-templated — no build system. postMessage is sketched in JS comments since stdlib cannot drive a browser.

What to look at:

  • _meta.ui on the tool response carries resourceUri, CSP, permissions.
  • The HTML renders without network access; all data is inlined.
  • JS calls host.callTool via window.parent.postMessage (documented but inert in this stdlib demo).

Ship It

This lesson produces outputs/skill-mcp-apps-spec.md. Given a tool that would benefit from an interactive UI, the skill produces the full MCP Apps contract: ui:// URI, CSP, permissions, postMessage entrypoints, and a security checklist.

Exercises

  1. Run code/main.py and inspect the HTML emitted. Open the HTML directly in a browser; verify the SVG renders. Then sketch the postMessage contract the UI would use to call host.callTool("notes_update", ...).

  2. Tighten the CSP: remove 'unsafe-inline' and use a nonce-based script policy. What changes in the HTML generation code?

  3. Add a second UI resource ui://notes/editor with a form for editing a note in place. When the user submits, the iframe calls host.callTool("notes_update", ...).

  4. Audit the UI's attack surface. Where could a malicious server inject content? What does the iframe sandbox defend against and what does it not?

  5. Read the SEP-1724 spec and identify one capability in the MCP Apps SDK that this toy implementation does not use. (Hint: component-level state sync.)

Key Terms

TermWhat people sayWhat it actually means
MCP Apps"Interactive UI resources"SEP-1724 extension shipped 2026-01-26
ui://"App URI scheme"Resource scheme for UI bundles
text/html;profile=mcp-app"The MIME"Content-type for MCP App HTML
Iframe sandbox"Render container"Browser sandboxing of the UI with CSP and permissions
postMessage JSON-RPC"UI-to-host wire"Tiny JSON-RPC-over-postMessage dialect for host calls
_meta.ui"Tool-UI binding"Metadata linking a tool result to a UI resource
CSP"Content-Security-Policy"Declares allowed sources for scripts, network, styles
AppRenderer"Server SDK primitive"Converts a framework component into a ui:// resource
AppFrame"Client SDK primitive"Iframe mount helper that mediates postMessage
ui/initialize"Handshake"First postMessage from UI to host

Further Reading