← Back to Agent Rule

Connecting External Tools to Hermes Agents with MCP (Model Context Protocol) ✓ VERIFIED

2026-08-16 · 16 min read · MCP · Hermes · Docker · sun-port · Git

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.

Every command in this article was run against the live Hermes deployment documented on this site — MCP servers launched as subprocesses, the config committed to Git, and an HTTP server exposed through sun-port. The ✓ VERIFIED badge means actual execution, not copy-paste from a README.

1. What MCP Is (and What It Isn't)

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.

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.

2. Hermes' Native MCP Client

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.

3. Prerequisites

$ 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.

4. Quick Start — a Time Server

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.

5. A Real Tool — GitHub over stdio

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="...")
Why 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.

6. Filesystem and Other stdio Servers

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.

7. Running MCP Servers in Docker

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

8. Exposing an HTTP MCP Server through sun-port

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-..."
If the HTTP client transport isn't available in your installed 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.

9. Security — What Hermes Does for You

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:

# Disable sampling for an untrusted server
mcp_servers:
  third_party:
    command: "npx"
    args: ["-y", "some-untrusted-server"]
    sampling:
      enabled: false

10. Tool Naming, Collisions, and Lifecycle

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:

11. Troubleshooting

# "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

12. Pitfalls

13. Key Takeaways

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.