首页
/ LocalAI 测试 MCP Apps(交互式工具 UI):从 Docker 测试服务器到浏览器端 iframe 桥接的完整实战指南

LocalAI 测试 MCP Apps(交互式工具 UI):从 Docker 测试服务器到浏览器端 iframe 桥接的完整实战指南

2026-09-05 14:58:36作者:薛曦旖Francesca

MCP Apps 是 Model Context Protocol 的扩展:工具通过 _meta.ui.resourceUri 声明一个交互式 HTML 界面,当 LLM 调用该工具时,宿主(Host)会在聊天中以沙箱 iframe 内联渲染这个应用。本文以 LocalAI 仓库中的测试手册 .agents/testing-mcp-apps.md 为骨架展开:你将学会用一行 Docker 命令起一个带 React 时钟 UI 的 MCP 测试服务器,用 JSON-RPC 请求验证协议正确性,再在 LocalAI 的 Chat 页面接入该服务器、观察双向通信日志,并理解 LocalAI 前端 MCPAppFrameuseMCPClientuseChat 三个核心文件的实现原理与 CORS 代理的安全边界。

一、MCP Apps 是什么:协议层的关键声明

普通 MCP 工具只返回文本或图片;MCP Apps 扩展让工具声明一个 UI 资源:

  • 工具定义中携带 _meta.ui.resourceUri,指向一段 HTML 资源;
  • 应用通过 postMessage(JSON-RPC 格式)与宿主双向通信:可以调用服务端工具、向聊天发送消息、更新模型上下文;
  • 整个 MCP App 协议在浏览器端完成,LocalAI 服务端无需任何改动。

在 LocalAI 中,判定“一个工具是否带 App UI”依赖官方包 @modelcontextprotocol/ext-apps 提供的 getToolUiResourceUriisToolVisibilityAppOnly 两个辅助函数,见 useMCPClient.js 第 5 行的导入。

二、快速上手:在 Docker 中运行 MCP Apps 测试服务器

文档推荐用 @modelcontextprotocol/server-basic-react 这个现成的测试包:它暴露一个 get-time 工具,带一个交互式 React 时钟 UI。由于该包要求 Node >= 20,文档直接在 Docker 中运行(映射 3001 端口):

docker run -d --name mcp-app-test -p 3001:3001 node:22-slim \
  sh -c 'npx -y @modelcontextprotocol/server-basic-react'

等待约 10 秒后按顺序验证:

# 1. 确认进程启动
docker logs mcp-app-test
# 预期输出: "MCP server listening on http://localhost:3001/mcp"

# 2. 验证 MCP 协议握手(initialize)
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"}}}'

# 3. 列出工具,应看到 get-time 带 _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":{}}'

tools/list 的响应中应包含:

{
  "name": "get-time",
  "_meta": {
    "ui": { "resourceUri": "ui://get-time/mcp-app.html" }
  }
}

这个 resourceUri 正是后续 LocalAI 前端通过 readResource 拉取 HTML 的地址。@modelcontextprotocol/ext-apps 仓库中还有更多示例服务器(均支持 stdio 与 HTTP 两种传输,不带 --stdio 参数时默认以 HTTP 模式监听 3001 端口),server-basic-react 只是最简单的时钟示例。

三、在 LocalAI UI 中接入并触发 MCP App

前置条件:LocalAI 正在运行(例如 http://localhost:8080),且 React UI 已构建:

cd core/http/react-ui && npm install && npm run build

操作步骤:

  1. 浏览器打开 LocalAI 的 Chat 页面;
  2. 点击聊天头部工具栏中的 "Client MCP"
  3. 新增一个客户端 MCP 服务器:
    • URLhttp://localhost:3001/mcp
    • Use CORS proxy:启用(默认开启)——文档给出的理由是浏览器无法直接跨域访问 localhost:3001,由 LocalAI 的 /api/cors-proxy 代理转发;
  4. 服务器连接后应发现 get-time 工具;
  5. 选择模型,发送消息 "What time is it?"
  6. LLM 应调用 get-time 工具;
  7. 工具结果以独立聊天消息的形式在 iframe 中渲染出交互式 React 时钟(而不是折叠进 activity 分组)。

关于 CORS 代理的重要限制:从源码看,代理端点 cors_proxy.go 内置了 SSRF 防护:localhost*.local、云 metadata 域名会被直接拒绝(第 49-53 行),所有解析出的 IP 必须通过 utils.IsPublicIP 判定为公网地址(第 59-63 行),否则返回 403。因此对于纯本地回环的测试服务器,启用 CORS proxy 实际上可能无法连通——此时应关闭 CORS proxy 让浏览器直连,或把测试服务器部署到可公网访问的地址再走代理。这一点值得在实际测试时留意。

代理路由的注册位置在 routes/localai.go 第 492-494 行,同时注册了 GET/POST 与 OPTIONS 预检,前端侧的默认代理路径配置在 config.jscorsProxy: '/api/cors-proxy')。

四、逐项验证清单(What to Verify)

文档给出了完整的验收清单,逐项对应具体的 UI 行为:

  • [ ] get-time 出现在已连接工具列表中且未被过滤(它对 LLM 可调用);
  • [ ] iframe 以独立聊天消息形式渲染,带拼图(puzzle-piece)图标;
  • [ ] 应用可加载且可交互(时钟走动、按钮可用);
  • [ ] 不出现 "Reconnect to MCP server" 遮罩层(表示连接存活);
  • [ ] 控制台日志能看到双向通信(见下一节);
  • [ ] 应用渲染后 LLM 继续生成包含时间的文本回复;
  • [ ] 非 UI 工具仍按纯文本结果正常工作;
  • [ ] 刷新页面后 HTML 以静态形式展示,直到重新连接才出现重连遮罩。

其中"独立消息而非折叠分组"的行为由 Chat.jsx 中的 ActivityGroup 组件实现:它把带 appUI 字段的 tool_result 项与普通项分离(第 112-114 行),前者单独渲染 MCPAppFrame(第 166-180 行)。"重连遮罩"则来自 MCPAppFrame.jsx 第 97-101 行:当 mcpClient 为空(例如刷新页面后连接丢失)时,iframe 保留静态 HTML 并叠加提示层。

五、控制台日志模式:健康的双向通信长什么样

浏览器控制台中,一次健康的 MCP App 会话呈现如下 JSON-RPC 序列:

Parsed message { jsonrpc: "2.0", id: N, result: {...} }     // Bridge 初始化
get-time result: { content: [...] }                          // 收到工具结果
Calling get-time tool...                                     // 应用调用工具
Sending message { method: "tools/call", ... }                // 应用 -> 宿主 -> 服务器
Parsed message { jsonrpc: "2.0", id: N, result: {...} }     // 服务器响应
Sending message text to Host: ...                            // 应用发送消息
Sending message { method: "ui/message", ... }                // 消息通知
Message accepted                                             // 宿主确认

以下告警可安全忽略:

  • Source map error: ... about:srcdoc——devtools 找不到 srcdoc iframe 的 sourcemap;
  • Ignoring message from unknown source——iframe 导航产生的重复 postMessage;
  • notifications/cancelled——应用清理上一次请求。

六、源码级原理:LocalAI 如何渲染 MCP App

6.1 工具结果的 UI 探测:useMCPClient

useMCPClient.js 暴露了四个 App UI 专用辅助方法(第 179-212 行):

方法 作用
hasAppUI(toolName) getToolUiResourceUri 判断工具是否声明了 UI 资源
getAppResource(toolName) 通过 client.readResource({ uri }) 拉取 UI HTML,返回 { html, meta },其中 meta 即工具上的 _meta.ui
getClientForTool(toolName) 返回该工具所属 MCP 服务器的 Client 实例,供 iframe 桥接复用
getToolDefinition(toolName) 返回完整工具定义(含 _meta

在 Chat 页面,handleSend 把这些能力组装进 getToolAppUI 回调(Chat.jsx 第 889-901 行):先 hasAppUI 判定,再取资源,最终返回包含 htmlmetatoolNametoolInputtoolDefinition 与归一化后的 toolResult 的对象。

6.2 Agentic 循环中的 appUI 挂载:useChat

useChat.js 第 766-791 行实现了客户端工具执行循环:每个 tool_calloptions.executeTool 执行后,若提供 options.getToolAppUI 则调用它获取 UI 资源,并把 appUI 字段附加到 tool_result 消息上。注意 Chat.jsx 第 888 行将 maxToolTurns 设为 10,即单轮对话内最多 10 次客户端工具往返。

6.3 iframe 与桥接:MCPAppFrame

MCPAppFrame.jsx 是渲染层核心,关键实现:

  • 传输层new PostMessageTransport(iframe.contentWindow, iframe.contentWindow) 包裹宿主与 srcdoc iframe 之间的 window.postMessage(第 18 行);
  • 桥接层AppBridge(来自 @modelcontextprotocol/ext-apps/app-bridge,第 19-24 行)自动把应用侧的 tools/callresources/readresources/list 经由宿主的 MCP Client 转发到服务器,宿主自报身份为 { name: 'LocalAI', version: '1.0.0' },上下文声明 displayMode: 'inline'
  • 初始化时序bridge.oninitialized 中把之前收到的 toolInputtoolResult 补发给应用;若工具结果晚于桥接建立到达,useEffect(第 60-64 行)会再次 sendToolResult
  • 沙箱安全:iframe 使用 sandbox="allow-scripts allow-forms"(第 89 行),不含 allow-same-origin——应用运行在不透明源(opaque origin)下,无法访问宿主 Cookie、DOM 或 localStorage;若资源声明了 permissions,还会经 buildAllowAttribute 生成 allow 属性(第 81-82 行);
  • 尺寸自适应onsizechange 回调把 iframe 高度限制在 600px 以内(第 31-33 行);外链请求经 onopenlinkwindow.open(url, '_blank', 'noopener,noreferrer') 打开(第 35-38 行);
  • 卸载清理:组件卸载时仅 bridge.close() 关闭本地传输,刻意不发送 teardownResource(第 66-77 行注释)——否则会销毁服务端状态,导致流式结束、ActivityGroup 接管 StreamingActivity 组件重挂载时出现 "Connection closed" 错误。

6.4 工具可见性过滤

标记为 _meta.ui.visibility: "app-only" 的工具会从 LLM 可见的工具列表中过滤掉,但仍可被应用 iframe 调用——对应的判定函数 isToolVisibilityAppOnly 同样导入自 @modelcontextprotocol/ext-apps/app-bridgeuseMCPClient.js 第 5 行)。这与验证清单第一条相互印证:get-time 未标记 app-only,所以它既对 LLM 可见、也可被应用调用。

七、关键文件索引与清理

与 MCP Apps 实现直接相关的文件:

测试结束后的清理只需一行:

docker rm -f mcp-app-test

八、小结

LocalAI 对 MCP Apps 的支持是一套纯浏览器端方案:PostMessageTransport 提供宿主与 srcdoc iframe 间的 JSON-RPC 通道,AppBridge 自动转发工具调用与资源读取,沙箱属性保证应用隔离。测试路径非常短——Docker 一行命令起 get-time 时钟服务器、curl 验证 initializetools/list、UI 中接入 Client MCP 并发送 "What time is it?"——随后用文档给出的日志模式与验证清单逐项核对即可确认双向通信、独立消息渲染与重连行为均符合预期。需要特别留意的是 CORS 代理对私网/回环地址的拒绝策略:本地回环测试服务器应直连,代理路径面向公网服务器。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384