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:
- Fetches the HTML UI resource from the MCP server (via
resources/reador HTTP) - Starts a local HTTP + WebSocket server on an available port (starting from 9470)
- Opens the user's default browser
- 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 launchhost_page.py— JavaScript host page template; bridges iframepostMessageand WebSocket, handlesui/initialize, display modes, reconnection with exponential backoffbridge.py(AppBridge) — JSON-RPC protocol handler: proxiestools/callandresources/readto MCP servers, manages message queue, formats tool results per MCP specmodels.py— Pydantic models:AppInfo,AppState(PENDING -> INITIALIZING -> READY -> CLOSED),HostContext
App Lifecycle
PENDING -> INITIALIZING -> READY -> CLOSED
- PENDING: App info created, port allocated
- INITIALIZING: WebSocket connected, host page loaded, waiting for app to initialize
- READY: App sent
ui/notifications/initialized, tool results can be pushed - 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/initializeresponse includes protocol version, host capabilities (with sandbox details), host info, and host contextui/resource-teardownsent to iframe onbeforeunloadui/notifications/host-context-changedsent after display mode changesstructuredContentrecovered 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_responsediscardsstructuredContent— 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