Skip to main content

本地架设单个 MCP 服务器供多客户端共享

· 8 min read
Kyle
CTO of the Ph.D. Creative Station

同时运行多个 AI Agent 实例(如 Hermes 的多个 profile)时,每个实例各自启一套 MCP server 子进程,很快就会出现工具连接间歇性失败。本文从根因分析出发,对比社区主流 Gateway 方案,最终选用 sparfenyuk/mcp-proxy 在本地架设一个共享 MCP 后端,并给出完整的安装、配置与验证流程。

1. 问题与根因

1.1. 现象

同时启动多个 Hermes 实例(如 general_researchercyber_hacker 及默认实例),MCP 工具间歇性连接失败。

1.2. 根因

每个 Hermes 实例在启动时独立执行 discover_mcp_tools(),读取自身 config.yaml 中的 mcp_servers 段,为每个 MCP server 启动独立的 stdio 子进程。Hermes 没有任何跨实例的 MCP 协调机制——这是设计现状,不是 bug。

实测数据(3 个 Hermes 实例 × 6 个 MCP server):

导致间歇性失败的三个直接原因:

  1. Obsidian / Zotero MCP:多个客户端同时访问同一个本地 API(Obsidian vault、Zotero library),可能触发限流或文件锁冲突
  2. npm 缓存锁:多个 npx exec 同时拉取相同包时互相踩文件锁
  3. 资源耗尽:60 个进程竞争 CPU / 内存,MCP 后台 event loop 超时

同期 Hermes 的 crash log 中可见多个 mcp-event-loop 线程全部卡在 work_queue.get()——这是 MCP 连接超时 / 挂起的直接证据。

1.3. Hermes 自身相关已知问题(均已修复)

GitHub Issue问题状态
#15275TUI session 内 tui_gateway.entryslash_worker 各 spawn 一份 MCP✅ 已修(改为单例)
#34966reload 时旧 MCP 进程不杀✅ 已修(改用 process-group kill)
#5344OAuth 多 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-gatewayRust/servers/{name}/mcp 独立路径JSON新(2026.03)
jkandasa/mcp-bridgeGo/mcp 统一聚合YAML
MikkoParkkola/mcp-gatewayRust/mcp 聚合 + /mcp/{backend} 直接YAML高(v2.19, 预编译二进制)
sparfenyuk/mcp-proxyPython/servers/{name}/mcp + /servers/{name}/sseJSON
filhocf/mcp-proxyPython/servers/{name}/sseJSON高(企业级功能)
mcprouterGo按 tool name 路由YAML
supergatewayNode单 server 单端口CLI args高(需配合 systemd)

2.3. 筛选过程

第一轮过滤:排除 supergateway——一个 server 一个端口,不解决多 server 聚合问题。

第二轮(决赛圈)sparfenyuk/mcp-proxy vs MikkoParkkola/mcp-gateway

维度sparfenyuk/mcp-proxyMikkoParkkola/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.jsonYAML(gateway.yaml
多实例并发--stateless 模式,每次请求独立 stdio 会话默认共享会话,重型 backend 可能多路复用冲突

2.4. 最终选择

选择理由:

  1. Hermes 兼容——直接后端端点同时提供 /mcp(Streamable HTTP)和 /sse(SSE)两种协议,Hermes 默认 Streamable HTTP 直连即可
  2. 每个 backend 独立端点——/servers/{name}/mcp,对应 Hermes 中一条 url: 配置,tool name 完全不变
  3. --stateless 模式——每次请求创建独立 stdio 会话,避免重型 MCP server(paper-search、scansci-pdf)的多路复用冲突
  4. pip 安装——无需编译,Python 生态成熟
  5. 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"
}
}
}
}

zoteroZOTERO_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 超时失联