Skip to content
Material 3 Expressivev1.3.0-rc.1
MCP Apps guide

Hosting and theming MCP Apps

Embed apps, pass tool results, configure capabilities and sandbox policy, and share host styles.

McpAppFrame embeds a UI resource from a connected MCP server. It owns the iframe and SDK bridge, derives host context from the surrounding Material provider, and delivers the tool call after the app initializes.

Connect and render

Advertise the extension when constructing your MCP client:

import { Client } from '@modelcontextprotocol/client'
import { mcpAppsClientCapabilities } from '@language-lit/material3-expressive-mcp-apps'

const client = new Client({ name: 'my-host', version: '1.0.0' }, {
  capabilities: mcpAppsClientCapabilities,
})
// Await client.connect(yourTransport) before rendering the frame.

The resource hook handles loading and errors, including UI metadata published in resources/list rather than the read result:

import { Material3Provider } from '@language-lit/material3-expressive'
import { McpAppFrame, useMcpAppResource } from '@language-lit/material3-expressive-mcp-apps'
import type { Client, CallToolResult } from '@modelcontextprotocol/client'

export function ToolPanel({ client, uri, result }: {
  client: Client; uri: string; result: CallToolResult
}) {
  const { resource, loading, error } = useMcpAppResource(client, uri)
  if (error) return <p role="alert">{error.message}</p>
  if (loading || !resource) return <p>Loading app…</p>
  return <Material3Provider>
    <McpAppFrame client={client} resource={resource} title="Tool result"
      sandboxUrl={sandboxUrl}
      onAuthorizeToolCall={async ({ name, arguments: args }) =>
        name === 'refresh_result' && await mayRefresh(args)}
      toolResult={result} maxHeight={600}
      availableDisplayModes={['inline', 'fullscreen', 'pip']} />
  </Material3Provider>
}

Pass toolInfo={{ id, tool }} and toolInput={arguments} when available. toolCancelled={{ reason }} delivers cancellation. Values are sent after initialization and again when their references change. Use fresh objects for new notifications. Pass null as client only when no server access is needed.

Enable host actions deliberately

PropHost behavior
onMessageReceives a request to add a user turn; return false to refuse
onUpdateModelContextReceives context intended for the model
onDownloadFileHandles download requests; return false to refuse
onOpenLinkHandles URLs; return false to refuse
onLogReceives app logs
onBridgeExposes the SDK bridge for advanced protocol operations
onStatusChange, onErrorReport lifecycle and failures
onTeardownRequestApp asked to close; host decides when to unmount

Message, context-update and download capabilities are advertised only when their callbacks exist. Without a custom link handler, HTTP(S) links open in a new tab with noopener; other schemes are refused. The demo records link requests locally. The SDK proxies server tools and resources through your connected client. App-originated tool calls are denied unless onAuthorizeToolCall returns true. Authenticate and authorize the same call again on the MCP server; the browser callback is not a backend security boundary.

Theme and style projection

The frame reads live Material CSS tokens from its surrounding provider and projects them into MCP style variables for colors, typography, radii and shadows. styleVariables overrides this projection. fonts accepts font-face CSS; collectFontFaceCss reads accessible same-origin stylesheets. Cross-origin font requests from an opaque iframe need suitable CORS and CSP permissions. The preview uses local fallback fonts and needs no font fetch inside the app.

Material has no success/warning roles, so protocol success/warning map to tertiary/secondary. Monospace uses a system stack; regular borders use 1px; semibold maps to Material medium. These are adaptation choices, not additional Material tokens.

McpAppProvider follows host light/dark mode, exposes host variables on its root, applies supplied fonts, and pads safe areas. A host's custom palette is not reconstructed from those protocol variables into a complete Material theme. Pass a shared theme explicitly when you control both sides and need identical custom colors. colorMode can override the host. The provider's document-theme and host-style effects can be disabled through their props.

Layout and lifecycle

Inline height follows app size notifications, bounded by minHeight and maxHeight. Fullscreen and pip reposition the surface around the same iframe, preserving app state. Control the mode with displayMode and onDisplayModeChange, or use defaultDisplayMode. Apps request a mode through useDisplayMode; only host-offered modes are granted.

Fullscreen is CSS layout, not a modal dialog or the browser Fullscreen API. The frame has exit controls. Escape in host controls returns inline; keyboard events inside an iframe do not bubble to its parent, so an app should implement its own keyboard exit if required. There is no modal focus-trap claim.

Unmounting closes the bridge. Changing the resource or handshake capability presence reconnects it. Callback identity changes do not reconnect. A custom transport is read at connection creation, not switched during an active session.

Resource trust and sandbox policy

Browser embedding requires sandboxUrl, an HTTP(S) proxy on a different origin from the host. The proxy performs the SDK sandbox handshake, serves the app in an inner iframe, and enforces resource CSP and permissions. Validate resource policy requests against host policy before granting them.

The companion repository includes a separately built service under deploy/sandbox. Its launch URL carries a signed, short-lived ticket issued by an authenticated host backend. Configure the service with its exact public origin, exact allowed host origins, and a server-side secret of at least 32 random bytes. Never put that secret in browser code. The service uses in-memory, one-use views, so deploy one instance or enable sticky routing.

See Getting started for server registration and the official protocol for sandbox responsibilities. The companion does not certify arbitrary HTML as safe or replace the host's authorization layer.