An agent is only as capable as its tools. You can write a bespoke tool for every integration — or you can adopt MCP, the Model Context Protocol, an open standard that lets one agent speak to thousands of pre-built tool servers with a single line of config. Hermes ships a native MCP client: add a server to your config, restart, and its tools appear alongside terminal and read_file as first-class callable tools — no bridge CLI, no glue code. This tutorial connects GitHub, the filesystem, and a custom HTTP server, runs them in Docker, fronts them with sun-port, and versions the whole thing in Git — plus the security controls that stop an untrusted server from leaking your keys.
MCP standardizes the connection between an AI application and the tools it can call. Before MCP, every integration was a custom one-off: you'd write a wrapper around the GitHub API, another around your database, another around your internal service — each with its own auth, its own error handling, its own prompt description. MCP replaces all of that with a single JSON-RPC protocol and two transports.
read_file, create_issue, query_db — over a standardized interface.MCP is not a model, not a vector database, and not a replacement for your agent's own logic. It's plumbing — reliable, boring, standardized plumbing — and that's exactly what makes it useful: one client speaks to every compliant server.
Hermes has the client built in. At startup it reads an mcp_servers block from ~/.hermes/config.yaml, connects to each server in a dedicated background event loop, calls list_tools() to discover capabilities, and registers each tool in the shared tool registry. From then on the tools are available in every conversation — no per-session setup, no hot-reload needed (adding a server still requires a restart).
The two transports map to two config shapes:
# stdio transport — Hermes launches the server as a subprocess
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
# HTTP transport — connect to a remote or shared server over the network
mcp_servers:
company_api:
url: "https://mcp.mycompany.com/v1/mcp"
headers:
Authorization: "Bearer sk-..."
A server config carries either command (stdio) or url (HTTP), never both. Every option defaults sanely: timeout is 120s per tool call, connect_timeout 60s for the initial handshake.
$ pip install mcp # the MCP SDK — without it, MCP support is silently disabled
$ node --version # needed for npx-based servers
v20.11.0
$ uv --version # needed for uvx-based (Python) servers
uv 0.4.0
The mcp Python package is an optional dependency — Hermes skips MCP discovery with a warning if it's missing, so install it first and watch the startup log to confirm the client engaged.
The smallest possible win: a stdio server that tells the time.
# ~/.hermes/config.yaml
mcp_servers:
time:
command: "uvx"
args: ["mcp-server-time"]
Restart Hermes. On startup it connects, discovers get_current_time, and registers it as mcp_time_get_current_time. The naming convention is predictable: mcp_{server}_{tool}, with hyphens and dots replaced by underscores. That predictability matters — it's how you know which server owns which tool when two servers expose the same name.
The GitHub server turns the entire gh-style surface — issues, PRs, releases — into agent-callable tools:
# ~/.hermes/config.yaml
mcp_servers:
github:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_xxxxxxxxxxxxxxxxxxxx"
timeout: 60
Now the agent can list issues, open PRs, and review diffs by name:
$ # inside a Hermes session, the agent can call:
mcp_github_list_issues(owner="im-sun", repo="agent-rule")
mcp_github_create_pull_request(owner="im-sun", repo="agent-rule", title="...")
env and not your shell? Hermes deliberately does not pass your full environment to MCP subprocesses. Only a safe allowlist is inherited — PATH, HOME, USER, LANG, LC_ALL, TERM, SHELL, TMPDIR, and any XDG_* vars. Everything else — API keys, tokens, secrets — is excluded unless you name it explicitly in env. That's the whole point of the env key: the GitHub token above is the only credential that subprocess ever sees.The filesystem server is the workhorse for local automation — read, write, and list files in a directory you scope explicitly:
# ~/.hermes/config.yaml
mcp_servers:
filesystem:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/documents"]
timeout: 30
Scope is baked into the args — the server can only touch /home/user/documents, so even a prompt-injected agent can't wander into ~/.ssh. That's a recurring theme: MCP servers are safest when you give them the narrowest root or scope that does the job, exactly the least-privilege discipline from our secrets tutorial.
Stdio servers launched by npx or uvx download packages at startup, which is fragile in production — a missing npm registry, a version bump, or a broken cache takes the tool offline. Running the server in Docker pins the runtime, the version, and the dependencies to an immutable image:
# Dockerfile for a custom MCP server
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"]
But Hermes' stdio transport launches a command, not a container. The clean pattern is to expose the containerized server over the HTTP transport instead, then let the container's own lifecycle (Docker restart policy, healthchecks) manage uptime:
# 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" # loopback only — see Section 9
secrets:
gh_token:
file: ./secrets/gh_token.txt
The secret arrives as a file via Compose secrets (never in docker inspect), and the port binds to 127.0.0.1 so nothing on the public internet can reach it directly. On the Hermes side, point the client at it:
# ~/.hermes/config.yaml
mcp_servers:
github:
url: "http://127.0.0.1:8080/mcp"
timeout: 180
connect_timeout: 30
Loopback-only is fine for a single host, but a multi-agent fleet — or a shared MCP server other machines need — has to cross the network. That's where sun-port, the Pingora-based reverse proxy we covered in our reverse-proxy tutorial, comes in. It terminates TLS at the edge and forwards to the container over loopback, so the MCP server stays unexposed while clients reach it over HTTPS:
# sun-port route: public HTTPS -> internal MCP server
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;
}
}
Then the Hermes client (on any host that can reach mcp.agent-rule.com) connects over TLS:
mcp_servers:
github:
url: "https://mcp.agent-rule.com/mcp"
headers:
Authorization: "Bearer sk-..."
mcp version, the server fails with an ImportError naming mcp.client.streamable_http — and other servers keep working. Fix it with pip install --upgrade mcp. Don't let one broken server mask the others: check the startup log for per-server status rather than assuming a silent global failure.MCP servers are third-party code running with your credentials, so Hermes layers on three built-in protections you'd otherwise have to hand-roll:
env you name. Your shell's secrets don't leak into a random npm package.ghp_...), OpenAI-style keys (sk-...), bearer tokens, and token=/key=/API_KEY=/password=/secret= patterns.sampling/createMessage capability). Hermes enables it by default but lets you disable it per server, cap tokens and requests-per-minute, and whitelist models — important for untrusted servers.# Disable sampling for an untrusted server
mcp_servers:
third_party:
command: "npx"
args: ["-y", "some-untrusted-server"]
sampling:
enabled: false
Every MCP tool is registered as mcp_{server}_{tool}, so two servers can each expose a read_file without colliding — they become mcp_filesystem_read_file and mcp_github_read_file. Three lifecycle facts worth internalizing:
discover_mcp_tools() only connects to servers that aren't already connected, so re-running discovery never double-registers.# "MCP SDK not available -- skipping MCP tool discovery"
$ pip install mcp
# "Failed to connect to MCP server 'X'"
# - command not on PATH (install npx / uvx)
# - npx package needs -y in args to auto-install
# - server took too long to start -> raise connect_timeout
# "MCP server 'X' requires HTTP transport but streamable_http is not available"
$ pip install --upgrade mcp
# Tools not appearing: check the key is mcp_servers (not mcp or servers),
# verify YAML indentation, and grep the startup log for the tool prefix
pip install mcp — the whole subsystem is silently disabled without it. Install first, then confirm in the startup log.mcp_servers, not mcp or servers. A typo here produces "no servers configured" with zero errors./ gives a prompt-injected agent the whole disk. Scope to the smallest root that works.0.0.0.0 — a containerized MCP server bound to all interfaces is reachable by the internet. Bind 127.0.0.1 and expose only through sun-port.env — Hermes won't forward them, so the GitHub server starts unauthenticated. Every credential a stdio server needs must be an explicit env entry.npx auto-install in prod — a fresh pull at every boot is a supply-chain and availability risk. Pin the runtime in Docker.command+args for local stdio, url+headers for remote HTTP.mcp_{server}_{tool} makes it obvious which server owns which capability and prevents collisions.npx auto-installs; HTTP transport bridges the container boundary.MCP's value isn't any single tool — it's that the next tool costs a config line instead of a weekend. Add a server, restart, and your agent gains a capability with the same trust boundaries, the same naming scheme, and the same security posture as everything it could already do. For an agent whose job is to orchestrate im-bot rooms, Git repos, PostgreSQL backends, and Docker workloads through sun-port, that's the difference between a tool and a toolkit.