一个 Agent 的能力上限,取决于它的工具。你可以为每一个集成手写一个专用工具——也可以采用 MCP,即模型上下文协议,这个开放标准让一个 Agent 只需一行配置,就能与成千上万个现成的工具服务器对话。Hermes 内置原生 MCP 客户端:往配置里加一个服务器、重启,它的工具就会和 terminal、read_file 一起,作为一等可调用工具出现——无需桥接 CLI,也无需胶水代码。本教程接入 GitHub、文件系统和一个自定义 HTTP 服务器,把它们跑在 Docker 里,用 sun-port 前置,并用 Git 为整套东西做版本管理——外加那些阻止不受信任服务器泄漏密钥的安全控制。
MCP 把 AI 应用与它所能调用的工具之间的连接方式标准化。在 MCP 之前,每一个集成都是定制的一次性工作:你得给 GitHub API 写一个包装器,给数据库再写一个,给内部服务又写一个——每个都带着各自的认证、各自的错误处理、各自的提示词描述。MCP 用一个 JSON-RPC 协议和两种传输方式,取代了所有这一切。
read_file、create_issue、query_db。MCP 不是模型,不是向量数据库,也不是对你 Agent 自身逻辑的替代。它是管道——可靠、乏味、标准化的管道——而这恰恰是它有用的原因:一个客户端就能与每一个合规的服务器对话。
Hermes 把客户端内置其中。启动时,它从 ~/.hermes/config.yaml 读取 mcp_servers 配置块,在一个专属的后台事件循环里连接每个服务器,调用 list_tools() 发现能力,并把每个工具注册到共享工具注册表中。从那以后,这些工具在每一段对话里都可用——无需逐会话设置,也无需热重载(新增服务器仍然需要重启)。
两种传输方式对应两种配置形态:
# stdio 传输——Hermes 把服务器作为子进程启动
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
# HTTP 传输——通过网络连接远程或共享服务器
mcp_servers:
company_api:
url: "https://mcp.mycompany.com/v1/mcp"
headers:
Authorization: "Bearer sk-..."
服务器配置要么携带 command(stdio),要么携带 url(HTTP),绝不会两者兼有。每个选项都有合理的默认值:timeout 是每次工具调用 120s,connect_timeout 是初次握手 60s。
$ pip install mcp # MCP SDK——没有它,MCP 支持会被静默禁用
$ node --version # npx 类服务器需要它
v20.11.0
$ uv --version # uvx 类(Python)服务器需要它
uv 0.4.0
mcp 这个 Python 包是一个可选依赖——如果缺失,Hermes 会带着一条警告跳过 MCP 发现,所以先装上它,再留意启动日志,确认客户端确实生效了。
最小可能的胜利:一个报时间的 stdio 服务器。
# ~/.hermes/config.yaml
mcp_servers:
time:
command: "uvx"
args: ["mcp-server-time"]
重启 Hermes。启动时它会连接服务器、发现 get_current_time,并把它注册为 mcp_time_get_current_time。命名约定是可预测的:mcp_{server}_{tool},连字符和点号会被替换成下划线。这种可预测性很重要——当两个服务器暴露同名工具时,你正是靠它来判断哪个服务器拥有哪个工具。
GitHub 服务器把整个 gh 风格的操作面——issue、PR、release——变成 Agent 可调用的工具:
# ~/.hermes/config.yaml
mcp_servers:
github:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_xx...xxxx"
timeout: 60
现在 Agent 可以按名称列出 issue、创建 PR、审查 diff:
$ # 在 Hermes 会话里,Agent 可以调用:
mcp_github_list_issues(owner="im-sun", repo="agent-rule")
mcp_github_create_pull_request(owner="im-sun", repo="agent-rule", title="...")
env 而不是你的 shell?Hermes 刻意不把完整环境变量传给 MCP 子进程。只有一份安全的允许列表会被继承——PATH、HOME、USER、LANG、LC_ALL、TERM、SHELL、TMPDIR,以及所有 XDG_* 变量。其他的一切——API 密钥、令牌、秘密——都会被排除,除非你在 env 里显式点名。这正是 env 键的全部意义:上面那个 GitHub 令牌,是该子进程唯一能看到的凭据。文件系统服务器是本地自动化的主力——在你显式圈定的目录里读、写、列出文件:
# ~/.hermes/config.yaml
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/documents"]
timeout: 30
作用域被烤进了 args 里——这个服务器只能碰 /home/user/documents,所以即便是一个被提示词注入的 Agent,也没法溜进 ~/.ssh。这是一个反复出现的主题:当你给 MCP 服务器一个最窄的、刚好够用的根目录或作用域时,它才最安全——这正是我们密钥管理教程里的最小权限纪律。
由 npx 或 uvx 启动的 stdio 服务器会在启动时下载包,这在生产环境里很脆弱——一个缺失的 npm 源、一次版本升级、一个坏掉的缓存,都会让工具下线。把服务器跑在 Docker 里,就能把运行时、版本和依赖钉死在一个不可变镜像上:
# 自定义 MCP 服务器的 Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src ./src
CMD ["node", "src/server.js"]
但 Hermes 的 stdio 传输启动的是一个命令,而不是一个容器。干净的做法是改经 HTTP 传输暴露容器化的服务器,然后让容器自身的生命周期(Docker 重启策略、健康检查)来管理可用性:
# docker-compose.yml
services:
mcp-github:
build: ./mcp-github
restart: unless-stopped
environment:
GITHUB_PERSONAL_ACCESS_TOKEN_FILE: /run/secrets/gh_token
secrets:
- gh_token
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
ports:
- "127.0.0.1:8080:8080" # 仅回环——见第 9 节
secrets:
gh_token:
file: ./secrets/gh_token.txt
秘密通过 Compose secrets 以文件形式送达(绝不会出现在 docker inspect 里),端口绑定到 127.0.0.1,这样公网上没有任何东西能直接访问它。在 Hermes 这一侧,把客户端指向它:
# ~/.hermes/config.yaml
mcp_servers:
github:
url: "http://127.0.0.1:8080/mcp"
timeout: 180
connect_timeout: 30
仅回环对单台主机够用,但一个多 Agent 集群——或其他机器需要的共享 MCP 服务器——就得跨网络。这正是 sun-port 的用武之地,它是我们在反向代理教程里介绍过的、基于 Pingora 的反向代理。它在边缘终结 TLS,并经回环转发到容器,于是 MCP 服务器保持不暴露,而客户端能通过 HTTPS 访问它:
# sun-port 路由:公网 HTTPS -> 内部 MCP 服务器
upstream mcp_github {
server 127.0.0.1:8080;
}
server {
listen 443 ssl;
server_name mcp.agent-rule.com;
ssl_certificate /etc/letsencrypt/live/mcp.agent-rule.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.agent-rule.com/privkey.pem;
location /mcp {
proxy_pass http://mcp_github;
proxy_set_header Authorization $http_authorization;
}
}
然后,Hermes 客户端(在任意能访问 mcp.agent-rule.com 的主机上)通过 TLS 连接:
mcp_servers:
github:
url: "https://mcp.agent-rule.com/mcp"
headers:
Authorization: "Bearer sk-..."
mcp 版本不提供 HTTP 客户端传输,该服务器会以一个指明 mcp.client.streamable_http 的 ImportError 失败——而其他服务器仍能继续工作。用 pip install --upgrade mcp 修复。别让一个坏掉的服务器掩盖其他服务器:去启动日志里查看每个服务器的状态,而不是默认这是一次无声的全局失败。MCP 服务器是拿着你的凭据运行的第三方代码,所以 Hermes 叠加了三层内置防护,否则这些都得你自己手写:
env 里点名的变量。你 shell 里的秘密不会泄漏进某个随机的 npm 包。ghp_...)、OpenAI 风格的密钥(sk-...)、bearer 令牌,以及 token=/key=/API_KEY=/password=/secret= 等模式。sampling/createMessage 能力)。Hermes 默认启用它,但允许你按服务器禁用它、限制令牌数和每分钟请求数,并给模型加白名单——这对不受信任的服务器很重要。# 为一个不受信任的服务器禁用采样
mcp_servers:
third_party:
command: "npx"
args: ["-y", "some-untrusted-server"]
sampling:
enabled: false
每个 MCP 工具都以 mcp_{server}_{tool} 的名称注册,所以两个服务器可以各自暴露一个 read_file 而不冲突——它们会变成 mcp_filesystem_read_file 和 mcp_github_read_file。有三个生命周期事实值得牢记:
discover_mcp_tools() 只连接尚未连接的服务器,所以重复运行发现过程绝不会重复注册。# "MCP SDK 不可用——跳过 MCP 工具发现"
$ pip install mcp
# "无法连接到 MCP 服务器 'X'"
# - 命令不在 PATH 上(安装 npx / uvx)
# - npx 包需要在 args 里加 -y 才能自动安装
# - 服务器启动太慢 -> 调高 connect_timeout
# "MCP 服务器 'X' 需要 HTTP 传输,但 streamable_http 不可用"
$ pip install --upgrade mcp
# 工具不出现:检查配置键是 mcp_servers(不是 mcp 或 servers),
# 确认 YAML 缩进,并在启动日志里 grep 工具前缀
pip install mcp——没有它,整个子系统会被无声地禁用。先安装,再到启动日志里确认。mcp_servers,不是 mcp 或 servers。这里一个拼写错误会产生 "no servers configured",而且没有任何报错。/ 的文件系统服务器,等于把一个被提示词注入的 Agent 放到整块磁盘上。把作用域缩小到能工作的最小根目录。0.0.0.0——一个绑定到所有网卡的容器化 MCP 服务器,公网都能访问。绑定 127.0.0.1,只经 sun-port 暴露。env——Hermes 不会转发它们,于是 GitHub 服务器以未认证状态启动。stdio 服务器所需的每一个凭据,都必须是显式的 env 条目。npx 自动安装——每次启动都重新拉取,是供应链和可用性风险。把运行时钉死在 Docker 里。command+args,远程 HTTP 用 url+headers。mcp_{server}_{tool} 让哪个服务器拥有哪个能力一目了然,也避免了冲突。npx 自动安装;HTTP 传输跨越容器边界。MCP 的价值不在于任何一个单独的工具——而在于,下一个工具的成本是一行配置,而不是一个周末。加一个服务器、重启,你的 Agent 就获得了一项能力,它拥有与它此前一切能力相同的信任边界、相同的命名方案和相同的安全姿态。对于一个职责是编排 im-bot 房间、Git 仓库、PostgreSQL 后端和 Docker 工作负载的 Agent 来说,这正是「一件工具」与「一整套工具箱」之间的差别。