Forest
记一次 MCP 工具失踪案的排查与修复

记一次 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_listmcp_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 rungateway/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 正常
根因 1Web UI 命令白名单不含 /reload-mcp,命令被拦截
根因 2hermes_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,修复代码已在实际环境中验证通过。