.agents/testing-mcp-apps.md
MCP Apps is an extension to MCP where tools declare interactive HTML UIs via _meta.ui.resourceUri. When the LLM calls such a tool, the UI renders the app in a sandboxed iframe inline in the chat. The app communicates bidirectionally with the host via postMessage (JSON-RPC) and can call server tools, send messages, and update model context.
Spec: https://modelcontextprotocol.io/extensions/apps/overview
The @modelcontextprotocol/server-basic-react npm package is a ready-to-use test server that exposes a get-time tool with an interactive React clock UI. It requires Node >= 20, so run it in Docker:
docker run -d --name mcp-app-test -p 3001:3001 node:22-slim \
sh -c 'npx -y @modelcontextprotocol/server-basic-react'
Wait ~10 seconds for it to start, then verify:
# Check it's running
docker logs mcp-app-test
# Expected: "MCP server listening on http://localhost:3001/mcp"
# Verify MCP protocol works
curl -s -X POST http://localhost:3001/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}'
# List tools — should show get-time with _meta.ui.resourceUri
curl -s -X POST http://localhost:3001/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
The tools/list response should contain:
{
"name": "get-time",
"_meta": {
"ui": { "resourceUri": "ui://get-time/mcp-app.html" }
}
}
http://localhost:8080)cd core/http/react-ui && npm install && npm run buildhttp://localhost:3001/mcplocalhost:3001 directly due to CORS; LocalAI's proxy at /api/cors-proxy handles itget-time toolget-time toolget-time is callable by the LLM)tools/call messages from app to host (app calling server tools)ui/message notifications (app sending messages)Healthy bidirectional communication looks like:
Parsed message { jsonrpc: "2.0", id: N, result: {...} } // Bridge init
get-time result: { content: [...] } // Tool result received
Calling get-time tool... // App calls tool
Sending message { method: "tools/call", ... } // App -> host -> server
Parsed message { jsonrpc: "2.0", id: N, result: {...} } // Server response
Sending message text to Host: ... // App sends message
Sending message { method: "ui/message", ... } // Message notification
Message accepted // Host acknowledged
Benign warnings to ignore:
Source map error: ... about:srcdoc — browser devtools can't find source maps for srcdoc iframesIgnoring message from unknown source — duplicate postMessage from iframe navigationnotifications/cancelled — app cleaning up previous requestsPostMessageTransport wraps window.postMessage between host and srcdoc iframeAppBridge (from @modelcontextprotocol/ext-apps) auto-forwards tools/call, resources/read, resources/list from the app to the MCP server via the host's Clientsandbox="allow-scripts allow-forms" (no allow-same-origin) — opaque origin, no access to host cookies/DOM/localStorage_meta.ui.visibility: "app-only") are filtered from the LLM's tool list but remain callable by the app iframecore/http/react-ui/src/components/MCPAppFrame.jsx — iframe + AppBridge componentcore/http/react-ui/src/hooks/useMCPClient.js — MCP client hook with app UI helpers (hasAppUI, getAppResource, getClientForTool, getToolDefinition)core/http/react-ui/src/hooks/useChat.js — agentic loop, attaches appUI to tool_result messagescore/http/react-ui/src/pages/Chat.jsx — renders MCPAppFrame as standalone chat messagesThe @modelcontextprotocol/ext-apps repo has many example servers:
@modelcontextprotocol/server-basic-react — simple clock (React)All examples support both stdio and HTTP transport. Run without --stdio for HTTP mode on port 3001.
docker rm -f mcp-app-test