Skip to content
Generative UIMCP app frameMenu

MCP app frame

Hosts an MCP App (a ui:// resource, text/html;profile=mcp-app) in a sandboxed iframe through the official app bridge: loading, errors, resize to content, chatcn tokens as host styles, full screen, picture in picture, and tool input and output passthrough.

A seat picker View runs offline in the sandbox, themed by chatcn tokens, fed by tool input and result.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/mcp-app-frame.json

This item depends on @modelcontextprotocol/ext-apps and @modelcontextprotocol/client 2.x. Install it only if you host MCP Apps; nothing else in chatcn needs them.

Usage

tsx
import { McpAppFrame } from "@/components/ui/chatcn/mcp-app-frame"

// The tool's _meta.ui.resourceUri points at a ui:// resource. Read it from the server first.
const resource = await mcp.readResource({ uri: tool._meta.ui.resourceUri })

<McpAppFrame
  resource={{ uri, html: resource.contents[0].text, meta: resource.contents[0]._meta?.ui }}
  appName="United"
  toolName={part.toolName}
  toolInput={part.input}
  toolResult={part.output}
  onMessage={(text) => send(text)}
  onCallTool={(params) => mcp.callTool(params)}
/>

The demo's View is a single HTML string with a hand-written bridge, so it runs offline. Real Views usually use the App class from @modelcontextprotocol/ext-apps; either way the frame speaks the same protocol.

Examples

Loading and errors

While the resource is read, the frame holds its height with a static skeleton and says Starting after 300 ms. A failed read and a page that never sends ui/initialize both explain what went wrong and offer Reload app.

Reading the resource, a failed read and a page that never initializes.

Theme bridge

chatcnHostStyles(element) resolves our tokens where the frame sits, light or dark, and sends them as the host style variables Views read with useHostStyles: --color-background-primary from the card surface, --color-text-secondary from muted text, --color-ring-primary from the accent, radii from our scale, and bold capped at 600. When the theme flips, the View gets a host context update and repaints.

Security

  • The iframe runs with allow-scripts allow-forms and popups only, never allow-same-origin, so the View cannot read the page, its cookies or its storage.
  • A Content-Security-Policy is injected from the resource's _meta.ui.csp: network, frames and base URIs are limited to the declared HTTPS domains; everything else is blocked.
  • Device permissions map to the iframe allow attribute from _meta.ui.permissions. Links open only for http and https.

Props

PropTypeNotes
resource{ uri, html, meta? }Undefined while it is being read
appName, icon, toolNameHeader attribution. The icon is your server's mark
toolInputPartial, toolInput, toolResult, toolCancelledSent to the View in protocol order once it is ready
displayMode, defaultDisplayMode, onDisplayModeChange"inline" | "fullscreen" | "pip"
displayModesMcpAppDisplayMode[]Modes you offer. PiP becomes full screen on small touch screens
initialHeight, maxHeightnumberBefore the View reports its size, and the inline cap
onMessage(text, params) => voidui/message from the View
onOpenLink, onCallTool, onUpdateModelContext, onCloseBridge requests
error, onRetry, timeoutMsResource errors and the initialize timeout

useChatcnMcpApp is exported from @/hooks/use-chatcn-genui-mcp-app for your own frame UI, with chatcnHostStyles and withCsp.

Keyboard

KeyAction
EscapeLeave full screen or picture in picture; focus returns where it was
TabStays inside the frame in full screen

Accessibility

  • The iframe is titled with the app name. Full screen is a modal dialog; inline and floating frames are labelled regions.
  • While the app floats or fills the screen, its place in the answer says so and offers Show it here.

Motion

  • The iframe never moves in the DOM, because moving it would reload the app. Display modes restyle it: full screen enters with opacity and scale in 220 ms, picture in picture in 180 ms from its corner. Keyboard-triggered changes are instant.
  • Inline height follows the content over 200 ms; under reduced motion it jumps.