本地架设单个 MCP 服务器供多客户端共享
同时运行多个 AI Agent 实例(如 Hermes 的多个 profile)时,每个实例各自启一套 MCP server 子进程,很快就会出现工具连接间歇性失败。本文从根因分析出发,对比社区主流 Gateway 方案,最终选用 sparfenyuk/mcp-proxy 在本地架设一个共享 MCP 后端,并给出完整的安装、配置与验证流程。
1. 问题与根因
1.1. 现象
同时启动多个 Hermes 实例(如 general_researcher、cyber_hacker 及默认实例),MCP 工具间歇性连接失败。
1.2. 根因
每个 Hermes 实例在启动时独立执行 discover_mcp_tools(),读取自身 config.yaml 中的 mcp_servers 段,为每个 MCP server 启动独立的 stdio 子进程。Hermes 没有任何跨实例的 MCP 协调机制——这是设计现状,不是 bug。
实测数据(3 个 Hermes 实例 × 6 个 MCP server):
导致间歇性失败的三个直接原因:
- Obsidian / Zotero MCP:多个客户端同时访问同一个本地 API(Obsidian vault、Zotero library),可能触发限流或文件锁冲突
- npm 缓存锁:多个
npx exec同时拉取相同包时互相踩文件锁 - 资源耗尽:60 个进程竞争 CPU / 内存,MCP 后台 event loop 超时
同期 Hermes 的 crash log 中可见多个 mcp-event-loop 线程全部卡在 work_queue.get()——这是 MCP 连接超时 / 挂起的直接证据。
1.3. Hermes 自身相关已知问题(均已修复)
| GitHub Issue | 问题 | 状态 |
|---|---|---|
| #15275 | TUI session 内 tui_gateway.entry 和 slash_worker 各 spawn 一份 MCP | ✅ 已修(改为单例) |
| #34966 | reload 时旧 MCP 进程不杀 | ✅ 已修(改用 process-group kill) |
| #5344 | OAuth 多 server 端口冲突 | ✅ 已修 |
这些都是同进程内的 bug 修复,不涉及跨实例共享。
1.4. 提炼的技术问题
核心问题:stdio transport 的 MCP server 只能服务一个客户端(每个客户端 spawn 自己的子进程)。多 Hermes 实例场景下,需要一个共享 MCP 后端的方案。
需求约束:
- 所有 Hermes 实例共享同一套 MCP server,避免重复 spawn
- MCP server 的 tool name 尽量不变(减少对现有 workflow 的影响)
- Hermes 配置改动尽量小,与主流 MCP client 配置方式兼容
- 不引入过多运维复杂度
2. 方案调研与比较
2.1. 社区方案全景
MCP 社区对"多客户端共享 MCP server"的解法已经比较成熟。核心思路:将 stdio MCP server 通过 proxy / gateway 暴露为 HTTP 端点,让多个 client 连接同一个后端。
参考:Ilir Ivezaj, "MCP in Production: How I Run 6 Shared Servers for 4 AI Agents" (2026). 核心论断——"stdio has a critical limitation: each client spawns its own server instance. HTTP solves this."
2.2. 候选工具对比
| 工具 | 语言 | 端点模型 | 配置格式 | 成熟度 |
|---|---|---|---|---|
| rai-onl/mcp-gateway | Rust | /servers/{name}/mcp 独立路径 | JSON | 新(2026.03) |
| jkandasa/mcp-bridge | Go | /mcp 统一聚合 | YAML | 高 |
| MikkoParkkola/mcp-gateway | Rust | /mcp 聚合 + /mcp/{backend} 直接 | YAML | 高(v2.19, 预编译二进制) |
| sparfenyuk/mcp-proxy | Python | /servers/{name}/mcp + /servers/{name}/sse | JSON | 高 |
| filhocf/mcp-proxy | Python | /servers/{name}/sse | JSON | 高(企业级功能) |
| mcprouter | Go | 按 tool name 路由 | YAML | 高 |
| supergateway | Node | 单 server 单端口 | CLI args | 高(需配合 systemd) |
2.3. 筛选过程
第一轮过滤:排除 supergateway——一个 server 一个端口,不解决多 server 聚合问题。
第二轮(决赛圈):sparfenyuk/mcp-proxy vs MikkoParkkola/mcp-gateway
| 维度 | sparfenyuk/mcp-proxy | MikkoParkkola/mcp-gateway |
|---|---|---|
| 直接后端端点 | /servers/{name}/mcp(Streamable HTTP)+ /servers/{name}/sse(SSE) | /mcp/{backend}(仅 POST,不支持 GET SSE) |
| Hermes 兼容性 | ✅ Streamable HTTP 原生支持 | ❌ 直接后端模式与 Hermes 的 GET SSE 握手不兼容 |
| 安装 | pip install mcp-proxy(Python) | cargo install 或预编译二进制(Rust) |
| 配置格式 | JSON(servers.json) | YAML(gateway.yaml) |
| 多实例并发 | --stateless 模式,每次请求独立 stdio 会话 | 默认共享会话,重型 backend 可能多路复用冲突 |
2.4. 最终选择
选择理由:
- Hermes 兼容——直接后端端点同时提供
/mcp(Streamable HTTP)和/sse(SSE)两种协议,Hermes 默认 Streamable HTTP 直连即可 - 每个 backend 独立端点——
/servers/{name}/mcp,对应 Hermes 中一条url:配置,tool name 完全不变 --stateless模式——每次请求创建独立 stdio 会话,避免重型 MCP server(paper-search、scansci-pdf)的多路复用冲突- pip 安装——无需编译,Python 生态成熟
- JSON 配置——
named-server格式支持env字段注入环境变量,表达力足够
3. 操作流程
3.1. 安装
pipx install mcp-proxy
# → ~/.local/bin/mcp-proxy
3.2. 配置文件
~/.config/mcp-proxy/servers.json:
{
"mcpServers": {
"arxiv": {
"command": "uvx",
"args": ["arxiv-mcp-server"]
},
"scholar": {
"command": "npx",
"args": ["-y", "scholar-mcp", "--transport=stdio"]
},
"paper-search": {
"command": "uvx",
"args": ["--from", "paper-search-mcp", "python", "-m", "paper_search_mcp.server"],
"env": {
"PAPER_SEARCH_MCP_UNPAYWALL_EMAIL": "your-email@example.com"
}
},
"zotero": {
"command": "npx",
"args": ["-y", "@oscardvs/zoteus"]
},
"scansci-pdf": {
"command": "uvx",
"args": ["--from", "scansci-pdf", "scansci-pdf", "run"],
"env": {
"SCANSCI_PDF_UNPAYWALL_EMAIL": "your-email@example.com"
}
},
"obsidian": {
"command": "npx",
"args": ["-y", "mcp-obsidian-cli"],
"env": {
"OBSIDIAN_CLI_PATH": "~/.local/bin/obsidian"
}
}
}
}
zotero的ZOTERO_API_KEY通过 shell 环境变量传入(Hermes 启动时从.env注入),不在 JSON 中硬编码。
3.3. 启动脚本
~/.config/mcp-proxy/start.sh:
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG="${DIR}/servers.json"
PORT="${MCP_PROXY_PORT:-39401}"
mcp-proxy \
--port "$PORT" \
--stateless \
--pass-environment \
--named-server-config "$CONFIG"
chmod +x ~/.config/mcp-proxy/start.sh
~/.config/mcp-proxy/start.sh
参数说明:
--stateless:每次请求独立 stdio 会话,防止多 Hermes 实例并发冲突--pass-environment:将 shell 环境变量透传给 stdio 子进程(ZOTERO_API_KEY等)MCP_PROXY_PORT环境变量可覆盖端口,默认 39401
3.4. Hermes 配置
每个 profile 的 config.yaml:
mcp_servers:
arxiv:
url: http://127.0.0.1:39401/servers/arxiv/mcp
timeout: 30
scholar:
url: http://127.0.0.1:39401/servers/scholar/mcp
timeout: 60
paper-search:
url: http://127.0.0.1:39401/servers/paper-search/mcp
timeout: 60
zotero:
url: http://127.0.0.1:39401/servers/zotero/mcp
timeout: 30
scansci-pdf:
url: http://127.0.0.1:39401/servers/scansci-pdf/mcp
timeout: 60
obsidian:
url: http://127.0.0.1:39401/servers/obsidian/mcp
timeout: 30
enabled: true
关键变化:
- 每个 backend 仍然是独立的 MCP server,tool name 不变
- Hermes 默认 Streamable HTTP 协议,无需显式指定
transport: sse - 从 6 个 stdio 命令 → 6 条
url:,Hermes 不再 spawn 任何 MCP 子进程
3.5. 验证
# 健康检查
curl -s http://127.0.0.1:39401/status
# 逐个测试
for s in arxiv scholar paper-search zotero scansci-pdf obsidian; do
hermes -p cyber_hacker mcp test $s
done
3.6. 最终文件结构
~/.config/mcp-proxy/
├── servers.json # 6 backend 定义
└── start.sh # 启动脚本(端口 39401,--stateless)
4. 注意事项
- ZOTERO_API_KEY:通过 shell 环境变量传入,不在 JSON 中硬编码
- Obsidian / Zotero 依赖:对应的桌面应用需先启动
- uvx / npx 冷启动:首次请求时包管理器会下载 / 缓存依赖,响应可能慢 5–15 秒
--stateless必要性:paper-search 和 scansci-pdf 是重型 MCP server,不支持单 stdio 会话多路复用。不加--stateless会导致多 Hermes 实例并发时这两个 backend 超时失联
