记一次 MCP 工具失踪案的排查与修复
一、现象:同一个 MCP 服务,CLI 有工具,Web UI 没有
事情是这样的——我的 Hermes Agent 配置了一个 FNS MCP 服务器,用于操作 Obsidian 笔记系统:
# ~/.hermes/config.yaml
mcp_servers:
fns:
url: https://xxx.xxx.xxx/api/mcp
headers:
Authorization: Bearer xxx
用 hermes mcp test fns 测试一切正常:
✓ Connected (98ms)
✓ Tools discovered: 24
用 CLI 启动的 Hermes 会话也能正常使用 mcp_fns_note_list、mcp_fns_note_get 等 24 个工具。但是——通过 hermes-web-ui(一个独立的第三方开源项目,v0.5.30)创建的会话里,一个 mcp_ 开头的工具都看不到。
更诡异的是,在 Web UI 里输入 /reload-mcp 命令,不仅没生效,还陷入了”死循环”——每次输入都只返回一个 status,MCP 工具纹丝不动。
二、排查:逐层剥开洋葱
第一层:是不是 MCP 服务本身挂了?
$ hermes mcp test fns
✓ Connected (100ms)
✓ Tools discovered: 24
不是。FNS 服务活得好好的。
第二层:是不是配置路径不对?
检查进程架构:
CLI 会话:
hermes (独立进程) → 启动时调用 discover_mcp_tools() ✅
Web UI 会话:
hermes-web-ui (Node.js)
→ hermes_bridge.py (PID 1173747)
→ AIAgent → ???
bridge 进程确实读取了同一个 config.yaml,工具集配置中也包含了 fns。那问题出在哪?
第三层:/reload-mcp 为什么失效?
翻了 hermes-web-ui 的服务端源码,发现了关键代码:
// 斜杠命令白名单
var commandMap = {
usage, status, abort, queue,
clear, title, compress, steer, destroy
};
// 命令解析 —— 不在白名单的统统降级为 status!
function parseCommand(input) {
// ...
return commandMap[cmd]
? { name: cmd, ... }
: { name: "status", ... }; // ← 这里!
}
/reload-mcp 不在白名单里,被 Web UI 拦截后直接 fallback 成 /status 了,根本没传到 bridge! 这就是”死循环”的真相——每次 /reload-mcp 都被当成 /status 处理,MCP 从未真正 reload。
第四层:那为什么新会话也没 MCP 工具?
即使绕过命令系统,新建的会话也没有 MCP 工具。这说明问题更深层。
对比各入口点的初始化流程:
| 入口 | 调用 discover_mcp_tools() | MCP 工具 |
|---|---|---|
hermes chat(CLI) | ✅ hermes_cli/main.py | ✅ |
hermes gateway run | ✅ gateway/run.py | ✅ |
hermes_bridge.py(Web UI) | ❌ 根本没调用! | ❌ |
看 bridge 代码中 AIAgent 的创建:
# hermes_bridge.py(修复前)
def _create_agent(self, session_id, ...):
from run_agent import AIAgent
# ... 各种配置 ...
agent = AIAgent(
enabled_toolsets=_load_enabled_toolsets(), # 包含了 "fns"
# ...
)
_load_enabled_toolsets() 确实把 fns 纳入了工具集列表,但 discover_mcp_tools() 从未被调用!工具集列表里虽然有 fns,但 MCP 工具的实际发现和注册(连接服务器、获取工具列表、注册到 tool registry)这一步被跳过了。
这其实是 Hermes 的一个已知模式——cron scheduler 曾经有过一模一样的问题,专门写了个测试用例来防止回归:
# tests/cron/test_scheduler_mcp_init.py
"""
The fix inserts discover_mcp_tools() before the AIAgent(...) call,
matching the initialization that CLI and gateway already perform.
"""
三、修复:两行代码的事
在 hermes_bridge.py 的 AIAgent 创建之前,加入 MCP 工具发现:
# hermes_bridge.py(修复后)
def _create_agent(self, session_id, ...):
from run_agent import AIAgent
# Discover & register MCP server tools before creating the agent.
# Without this, tools from configured MCP servers (e.g. fns)
# are never injected into bridge sessions.
from tools.mcp_tool import discover_mcp_tools
discover_mcp_tools() # ← 就这一行!
# ... 继续创建 agent ...
agent = AIAgent(...)
验证修复效果:
>>> from tools.mcp_tool import discover_mcp_tools
>>> result = discover_mcp_tools()
>>> len(result)
24
>>> result[:3]
['mcp_fns_file_delete', 'mcp_fns_file_get_info', 'mcp_fns_file_list']
重启 hermes-web-ui 后,新建会话成功注入了全部 24+1 个 MCP 工具 ✅。
四、反思:这次排查教会我的几件事
1. 斜杠命令的”中间人拦截”
hermes-web-ui 作为独立项目,在自己的服务端实现了一套斜杠命令处理。它维护了一个命令白名单,不在白名单中的命令不会被透传。这意味着 Hermes 原生的 /reload-mcp、/reload、/rollback 等命令在 Web UI 中都是无效的。
💡 教训:当发现某个命令在 CLI 有效而 Web UI 无效时,先检查 Web UI 的命令白名单。
2. MCP 工具发现不是自动的
discover_mcp_tools() 不是模块导入时的副作用(model_tools.py 明确注释说”移除了模块级副作用”),而是需要每个入口点显式调用。
💡 教训:新增 Hermes 入口点(bridge、adapter、plugin)时,务必在 AIAgent 创建前调用
discover_mcp_tools()。
3. 排查时先把链路画出来
这次排查的关键转折点,是把 CLI 和 Web UI 的初始化链路并排对比。一旦看清了”CLI 调了但 bridge 没调”,根因就呼之欲出了。
CLI: main.py → discover_mcp_tools() → AIAgent ✅
Bridge: bridge.py → (跳过) → AIAgent ❌
4. 开源项目的版本信息很重要
这次问题的环境是 hermes-web-ui v0.5.30,这是一个独立于 Hermes Agent 本身的开源项目。不同版本可能有不同的命令白名单和初始化逻辑。记录版本号对排查和后续升级都至关重要。
总结
| 项目 | 说明 |
|---|---|
| 环境 | hermes-web-ui v0.5.30 + Hermes Agent + FNS MCP |
| 现象 | Web UI 会话缺少 MCP 工具,CLI 正常 |
| 根因 1 | Web UI 命令白名单不含 /reload-mcp,命令被拦截 |
| 根因 2 | hermes_bridge.py 从未调用 discover_mcp_tools() |
| 修复 | 在 bridge 创建 AIAgent 前加入 discover_mcp_tools() |
| 文件 | hermes-web-ui/dist/server/agent-bridge/hermes_bridge.py |
如果你也在用 hermes-web-ui 搭配 MCP 服务器,检查一下你的 bridge 代码是否也有这个问题——如果 Web UI 里看不到 MCP 工具但 CLI 里正常,十有八九是同一个原因。
本文基于 hermes-web-ui v0.5.30,修复代码已在实际环境中验证通过。