* Revert "fix: Add timeout-based wait for model deletion completion (#8756)"
This reverts commit 9e1b0d0c82.
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat: add mcp prompts and resources
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat(ui): add client-side MCP
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat(ui): allow to authenticate MCP servers
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat(ui): add MCP Apps
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* chore: update AGENTS
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* chore: allow to collapse navbar, save state in storage
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* feat(ui): add MCP button also to home page
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
* fix(chat): populate string content
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
---------
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
5.7 KiB
Testing MCP Apps (Interactive Tool UIs)
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
Quick Start: Run a Test MCP App Server
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" }
}
}
Testing in LocalAI's UI
- Make sure LocalAI is running (e.g.
http://localhost:8080) - Build the React UI:
cd core/http/react-ui && npm install && npm run build - Open the Chat page in your browser
- Click "Client MCP" in the chat header
- Add a new client MCP server:
- URL:
http://localhost:3001/mcp - Use CORS proxy: enabled (default) — required because the browser can't hit
localhost:3001directly due to CORS; LocalAI's proxy at/api/cors-proxyhandles it
- URL:
- The server should connect and discover the
get-timetool - Select a model and send: "What time is it?"
- The LLM should call the
get-timetool - The tool result should render the interactive React clock app in an iframe as a standalone chat message (not inside the collapsed activity group)
What to Verify
- Tool appears in the connected tools list (not filtered —
get-timeis callable by the LLM) - The iframe renders as a standalone chat message with a puzzle-piece icon
- The app loads and is interactive (clock UI, buttons work)
- No "Reconnect to MCP server" overlay (connection is live)
- Console logs show bidirectional communication:
tools/callmessages from app to host (app calling server tools)ui/messagenotifications (app sending messages)
- After the app renders, the LLM continues and produces a text response with the time
- Non-UI tools continue to work normally (text-only results)
- Page reload shows the HTML statically with a reconnect overlay until you reconnect
Console Log Patterns
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 requests
Architecture Notes
- No server-side changes needed — the MCP App protocol runs entirely in the browser
PostMessageTransportwrapswindow.postMessagebetween host andsrcdociframeAppBridge(from@modelcontextprotocol/ext-apps) auto-forwardstools/call,resources/read,resources/listfrom the app to the MCP server via the host'sClient- The iframe uses
sandbox="allow-scripts allow-forms"(noallow-same-origin) — opaque origin, no access to host cookies/DOM/localStorage - App-only tools (
_meta.ui.visibility: "app-only") are filtered from the LLM's tool list but remain callable by the app iframe
Key Files
core/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, attachesappUIto tool_result messagescore/http/react-ui/src/pages/Chat.jsx— renders MCPAppFrame as standalone chat messages
Other Test Servers
The @modelcontextprotocol/ext-apps repo has many example servers:
@modelcontextprotocol/server-basic-react— simple clock (React)- More examples at https://github.com/modelcontextprotocol/ext-apps/tree/main/examples
All examples support both stdio and HTTP transport. Run without --stdio for HTTP mode on port 3001.
Cleanup
docker rm -f mcp-app-test