mcp-cli/docs/MCP_APPS.md at main · IBM/mcp-cli

MCP Apps (SEP-1865)

MCP Apps are interactive HTML UIs served by MCP servers and rendered in the user's browser via sandboxed iframes. When a tool has a _meta.ui annotation, mcp-cli launches a local web server that bridges the browser and the MCP backend.

Prerequisites

# Install the apps extra (adds websockets dependency)
pip install "mcp-cli[apps]"

Quick Start

# Connect to a server that provides app-enabled tools
mcp-cli --server view_demo

# Ask for something visual
> Show me the sales data as a chart

# The browser opens automatically with an interactive chart app
# Tool results are pushed to the app in real-time via WebSocket

Use /tools in chat mode to see which tools have app UIs (shown in the APP column).

How It Works

Tool Detection

MCP servers annotate tools with _meta.ui metadata indicating they have an associated UI:

{
  "name": "show_chart",
  "description": "Display data as an interactive chart",
  "_meta": {
    "ui": {
      "resourceUri": "ui://view-demo/chart",
      "mediaType": "text/html"
    }
  }
}

When mcp-cli detects _meta.ui on a tool result, it automatically:

  1. Fetches the HTML UI resource from the MCP server (via resources/read or HTTP)
  2. Starts a local HTTP + WebSocket server on an available port (starting from 9470)
  3. Opens the user's default browser
  4. Pushes the tool result to the app once the app signals it's ready

Architecture

Browser                    Python Backend                MCP Server
+-----------------+       +------------------+       +--------------+
|  Host Page (JS)  |--WS--|  AppBridge        |--MCP--|  Tool Server |
|  +-------------+ |      |  (bridge.py)      |       |              |
|  | App iframe  | |      +------------------+       +--------------+
|  | (sandboxed) | |              |
|  +-------------+ |      +------------------+
|   postMessage    |      |  AppHostServer   |
+-----------------+       |  (host.py)        |
                          +------------------+

Components:

  • host.py (AppHostServer) — Manages app lifecycle: port allocation, HTTP serving (host page + app HTML), WebSocket server, browser launch
  • host_page.py — JavaScript host page template; bridges iframe postMessage and WebSocket, handles ui/initialize, display modes, reconnection with exponential backoff
  • bridge.py (AppBridge) — JSON-RPC protocol handler: proxies tools/call and resources/read to MCP servers, manages message queue, formats tool results per MCP spec
  • models.py — Pydantic models: AppInfo, AppState (PENDING -> INITIALIZING -> READY -> CLOSED), HostContext

App Lifecycle

PENDING -> INITIALIZING -> READY -> CLOSED
  1. PENDING: App info created, port allocated
  2. INITIALIZING: WebSocket connected, host page loaded, waiting for app to initialize
  3. READY: App sent ui/notifications/initialized, tool results can be pushed
  4. CLOSED: App teardown or browser tab closed

Protocol Messages

Browser -> Python (inbound):

Method Description
tools/call Proxy a tool call to the MCP server
resources/read Proxy a resource read to the MCP server
ui/message App sends a message to the conversation
ui/update-model-context App updates its model context
ui/notifications/initialized App signals it's ready to receive data
ui/notifications/teardown App is shutting down

Python -> Browser (outbound):

Method Description
ui/notifications/tool-result Push tool result data to the app
ui/notifications/tool-input Push tool input arguments to the app

Security Model

Iframe Sandbox

Apps run in a sandboxed iframe with the following permissions:

allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox

Content Security Policy

Server-supplied CSP domains are validated against a strict regex (^[a-zA-Z0-9\-.:/*]+$) before being included in CSP directives. This prevents CSP injection attacks.

XSS Prevention

  • Tool names are html.escape()d before template injection into the host page
  • All user-supplied content is sanitized at template boundaries

URL Scheme Validation

The ui/open-link handler only allows http:// and https:// schemes, blocking javascript: and other dangerous schemes.

Tool Name Validation

The bridge rejects tool names not matching ^[a-zA-Z0-9_\-./]+$ per the MCP spec.

Safe JSON Serialization

_safe_json_dumps() falls back to _to_serializable() on TypeError/ValueError, with circular reference protection via a visited-object set.

Session Reliability

Deferred Tool Result Delivery

Initial tool results are stored on the bridge and pushed only after the app sends ui/notifications/initialized. This prevents race conditions where postMessage is silently dropped before the app sets up its message listener.

Message Queue

When the WebSocket is disconnected, notifications are queued in a deque(maxlen=50). On reconnect, drain_pending() flushes queued messages.

Reconnection

The host page uses exponential backoff for WebSocket reconnection (1s, 2s, 4s, ... capped at 30s). On successful reconnect, the backoff resets and the bridge state returns to INITIALIZING.

Duplicate Prevention

Calling launch_app() for an already-running tool closes the previous instance before launching a new one. If an app is already running, new tool results are pushed to the existing bridge.

Initialization Timeout

A configurable JavaScript timeout (default 30s) shows "App initialization timed out" in the host page status bar if the app never sends ui/notifications/initialized.

Spec Compliance

  • ui/initialize response includes protocol version, host capabilities (with sandbox details), host info, and host context
  • ui/resource-teardown sent to iframe on beforeunload
  • ui/notifications/host-context-changed sent after display mode changes
  • structuredContent recovered from JSON text blocks when CTP transport normalization discards it

Configuration

Default values in src/mcp_cli/config/defaults.py:

Setting Default Description
DEFAULT_APP_HOST_PORT_START 9470 Starting port for local app servers
DEFAULT_APP_AUTO_OPEN_BROWSER True Auto-open browser on app launch
DEFAULT_APP_MAX_CONCURRENT 10 Maximum concurrent MCP Apps
DEFAULT_APP_TOOL_TIMEOUT 120.0s Tool call timeout from an app
DEFAULT_APP_INIT_TIMEOUT 30s Initialization timeout (JS-side)

Known Limitations

  • Map and video rendering quality depends on the server-side app JavaScript, not mcp-cli
  • ui/notifications/tool-input-partial (streaming argument assembly) is not yet implemented
  • HTTPS/TLS for remote deployment is not yet implemented
  • CTP transport's _normalize_mcp_response discards structuredContent — recovered via text block extraction

Examples

See examples/apps/ for working demos:

# Full end-to-end demo (requires: pip install mcp-cli[apps])
python examples/apps/apps_demo.py

# _meta.ui pipeline — shows how metadata survives the tool pipeline
python examples/apps/meta_pipeline_demo.py

# Bridge protocol — demonstrates JSON-RPC message routing
python examples/apps/bridge_protocol_demo.py