MCP app frame

Pro

Shows an MCP App: a tool's own UI, isolated in a sandbox on another origin. Without a usable sandboxProxyUrl it shows a locked notice saying why, and never falls back to running the HTML on this app's origin. The view gets the tool input once it's initialized, then the result (or a cancellation), plus Candor's theme as host styles. Links it asks to open go to onOpenLink or are shown as text to copy; tool calls go through only for callableTools; file downloads and device permissions are not granted.

Markdown

Preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/mcp-app-frame

Brings in: button, locale, use-copy, utils · npm: @modelcontextprotocol/ext-apps@^2.0.4, lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

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

// Without a sandbox proxy on another origin, the frame stays locked and says why.
// Host the page from mcpAppSandboxProxyHtml(yourOrigin) at, say, https://sandbox.example.com/proxy.html.
export default function Example() {
  return (
    <McpAppFrame
      className="max-w-lg"
      html="<!doctype html><p>Tool UI</p>"
      toolInput={{ city: "Oslo" }}
      sandboxProxyUrl={undefined}
    />
  )
}

<McpAppFrame>

PropTypeDefaultDescription
htmlrequiredstring—The ui:// resource's HTML (mime text/html;profile=mcp-app).
resourceMetaMcpUiResourceMeta | undefined—The resource's `_meta.ui`: csp domains, permissions, prefersBorder.
toolInputRecord<string, unknown> | undefined—The tool call's arguments, sent once the view is ready.
toolResultMcpToolResult | undefined—The tool's result, sent after the input when it arrives.
toolCancelledstring | undefined—Set when the tool call was cancelled; the reason is sent to the view.
sandboxProxyUrlstring | undefined—Your hosted sandbox proxy page (see mcpAppSandboxProxyHtml), on another origin.
callableToolsreadonly string[] | undefined—Tool names the view may call through you (see appCallableTools). Default none.
onCallTool((name: string, args: Record<string, unknown>) => Promise<McpToolResult>) | undefined—Runs a tool the view calls, if it's in callableTools; called with its name and arguments. Without it, calls are refused.
onMessage((message: McpAppMessage) => void | Promise<void>) | undefined—Called when the view asks to add a message to the conversation. Without it, the request is refused.
onOpenLink((url: string) => void | Promise<void>) | undefined—Handle a link the view asks to open. Default: show it for the person to copy.
theme"light" | "dark" | undefined—Defaults to the page's Candor theme.
titlestring | undefined—Names the frame for assistive tech; "Tool app" by default.
maxHeightnumber | undefined720The tallest the frame grows to, in pixels.
hostOriginstring | undefined—This app's origin; defaults to window.location.origin.
classNamestring | undefined—Extra classes for the outer element.

Functions

toolUiResourceUri(tool: ToolLike)

The ui:// resource a tool declares for its UI, or undefined. Reads both the current and the deprecated key.

appCallableTools(tools: readonly ToolLike[])

The tools a view may call through the host: only those whose _meta.ui.visibility lists "app". A tool that doesn't say is not callable from a view here (default deny), even though the spec's default visibility includes "app"; list it in callableTools yourself if you want it.

sanitizeMcpAppCsp(csp: McpUiResourceCsp | undefined)

The resource's declared CSP domains with anything that isn't a plain source removed.

buildMcpAppCsp(csp?: McpUiResourceCsp)

The Content-Security-Policy for a view: the spec's restrictive default, widened only by the domains the resource declares (after sanitizing).

injectMcpAppCsp(html: string, policy: string)

Puts the policy as the very first thing in the document (after a doctype), so no script in the HTML runs before it.

mcpAppSandboxProxyHtml(hostOrigin: string)

The sandbox proxy page. Host it on a different origin from your app (a separate subdomain such as sandbox.example.com is enough) and pass its URL as sandboxProxyUrl. Serve it as text/html with only Content-Security-Policy: frame-ancestors <your app's origin> — the inner frame inherits the page's policy, so don't add other directives there; the view's own policy is injected into its HTML. The page only accepts messages from hostOrigin, runs one resource, and relays JSON-RPC both ways.

checkSandboxProxy(url: string | undefined, hostOrigin: string | undefined, t: McpAppFrameTranslate = english)

Whether a proxy URL can be used from this origin: it must be https (http only for localhost) and on a different origin. Returns the proxy's origin, or a sentence saying why not (in English unless you pass McpAppFrame's t).

createMcpAppBridge(o: McpAppBridgeOptions)

Makes the host side of the bridge with Candor's policies wired in: the view gets its HTML (with the CSP injected) once the proxy is ready; tool calls go through only for callableTools; links and messages go to your handlers; file downloads are refused. Connect it to a SandboxTransport.

candorHostStyles(el: Element)

Candor's theme tokens as MCP Apps style variables, read from an element.

Strings

The fixed words, in English by default. Change any of them under mcpAppFrame in <LocaleProvider strings={{ … }}>; the table is exported as mcpAppFrameStrings.

KeyEnglish
titleTool app
lockedThis app can’t be shown safely here.
noProxyNo sandbox proxy is set up. MCP Apps run HTML from a tool’s server, so this app only shows them from a sandbox page on a separate origin.
invalidProxyThe sandbox proxy address isn’t a valid URL.
notHttpsThe sandbox proxy must be served over https.
noOriginThis app’s own origin isn’t known yet, so the sandbox can’t be checked.
sameOriginThe sandbox proxy is on this app’s own origin, so the tool’s HTML wouldn’t be isolated from this app. Host it on a separate origin.
linkRequestThe app asked to open a link. It wasn’t opened; copy it if you trust it.
copyLinkCopy link
copiedCopied
copyFailedCouldn’t copy
dismissDismiss

Types

McpToolResult
A tool result as MCP sends it.
McpUiToolResultNotification["params"]
McpAppMessage
What a view asks to add to the conversation.
McpUiMessageRequest["params"]
McpAppBridgeOptions
{ /** The view's HTML (the resource's text). */ html: string resourceMeta?: McpUiResourceMeta | undefined /** Tools the view may call; anything else is refused. */ callableTools?: readonly string[] | undefined onCallTool?: ((name: string, args: Record<string, unknown>) => Promise<McpToolResult>) | undefined /** The view asks to add a message to the conversation. Without a handler it's refused. */