# MCP app frame

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.

- Registry name: `mcp-app-frame` (`@candor/mcp-app-frame`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/mcp-app-frame

## Install

```bash
npx candor-ui add mcp-app-frame
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/mcp-app-frame`.
It brings in: button, locale, use-copy, utils.
npm packages: @modelcontextprotocol/ext-apps@^2.0.4, lucide-react@^1.67.0.

## Example

```tsx
"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>

Props:

- `html: string`: The ui:// resource's HTML (mime text/html;profile=mcp-app).
- `resourceMeta?: McpUiResourceMeta | undefined`: The resource's `_meta.ui`: csp domains, permissions, prefersBorder.
- `toolInput?: Record<string, unknown> | undefined`: The tool call's arguments, sent once the view is ready.
- `toolResult?: McpToolResult | undefined`: The tool's result, sent after the input when it arrives.
- `toolCancelled?: string | undefined`: Set when the tool call was cancelled; the reason is sent to the view.
- `sandboxProxyUrl?: string | undefined`: Your hosted sandbox proxy page (see mcpAppSandboxProxyHtml), on another origin.
- `callableTools?: readonly 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.
- `title?: string | undefined`: Names the frame for assistive tech; "Tool app" by default.
- `maxHeight?: number | undefined` (default `720`): The tallest the frame grows to, in pixels.
- `hostOrigin?: string | undefined`: This app's origin; defaults to window.location.origin.
- `className?: string | undefined`

## Functions

### toolUiResourceUri

```ts
toolUiResourceUri(tool: ToolLike)
```

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

### appCallableTools

```ts
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

```ts
sanitizeMcpAppCsp(csp: McpUiResourceCsp | undefined)
```

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

### buildMcpAppCsp

```ts
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

```ts
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

```ts
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

```ts
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

```ts
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

```ts
candorHostStyles(el: Element)
```

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

## Strings (`mcpAppFrame`)

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

- `title`: Tool app
- `locked`: This app can’t be shown safely here.
- `noProxy`: No 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.
- `invalidProxy`: The sandbox proxy address isn’t a valid URL.
- `notHttps`: The sandbox proxy must be served over https.
- `noOrigin`: This app’s own origin isn’t known yet, so the sandbox can’t be checked.
- `sameOrigin`: The 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.
- `linkRequest`: The app asked to open a link. It wasn’t opened; copy it if you trust it.
- `copyLink`: Copy link
- `copied`: Copied
- `copyFailed`: Couldn’t copy
- `dismiss`: Dismiss

## Types

### McpToolResult

A tool result as MCP sends it.

```ts
McpUiToolResultNotification["params"]
```

### McpAppMessage

What a view asks to add to the conversation.

```ts
McpUiMessageRequest["params"]
```

### McpAppBridgeOptions

```ts
{ /** 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. */ 
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
