Skip to content

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:

json
{
  "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:

json
{
  "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.

js
// 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:

json
{"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.

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