Skip to main content

MCP bridge that lets any MCP client (Claude, Codex, Cursor, ...) delegate tasks to a local Hermes Agent

Project description

hermes-mcp

An MCP server that lets Claude Desktop and Claude.ai (web + mobile) — and any other MCP client that supports static OAuth credentials — delegate tasks to a local Hermes Agent running on your own hardware. (See Client compatibility for the current matrix; Codex CLI and Cursor are tracked for future support pending Dynamic Client Registration.)

Use Claude (or another supported MCP client) as your daily chat. When you ask for something Hermes is built for — scheduling cron jobs, browser automation, email, document creation, persistent skills, WhatsApp/Slack messaging — your client calls Hermes through this bridge.

┌──────────────────────────────────────────────────────┐
│ MCP client                                           │
│ (Claude Desktop / Claude.ai / Codex CLI / Cursor /…) │
└────────────────────────┬─────────────────────────────┘
                         │ HTTPS + OAuth 2.1
                         ▼
                ┌──────────────────────┐
                │ cloudflared tunnel   │  (public HTTPS edge)
                └──────────┬───────────┘
                           │
                           ▼ localhost:8765
                ┌──────────────────────┐
                │ hermes-mcp           │  (FastMCP, Streamable HTTP)
                │ - OAuth 2.1 + PKCE   │
                │ - HTTP -> gateway    │
                └──────────┬───────────┘
                           │ HTTP /v1/chat/completions
                           ▼ localhost:8642
                ┌──────────────────────┐
                │ hermes-gateway       │  (the running Hermes brain;
                │                      │   same agent loop Telegram uses)
                └──────────────────────┘

Quickstart

These steps assume you already have Hermes Agent installed and working on a Linux/WSL machine, with the gateway listening on 127.0.0.1:8642.

# 1. Install
pipx install hermes-mcp

# 2. Mint OAuth client credentials
hermes-mcp mint-client                  # prints OAUTH_CLIENT_ID + OAUTH_CLIENT_SECRET

# 3. Start a quick tunnel (testing only — URL changes on restart)
cloudflared tunnel --url http://127.0.0.1:8765
# prints: https://random-words-here.trycloudflare.com

# 4. Export env vars (using the URL from step 3)
export OAUTH_CLIENT_ID=<from step 2>
export OAUTH_CLIENT_SECRET=<from step 2>
export OAUTH_ISSUER_URL=https://random-words-here.trycloudflare.com
export MCP_ALLOWED_HOSTS=random-words-here.trycloudflare.com
export HERMES_API_KEY=<the API_SERVER_KEY from ~/.hermes/.env>

# 5. Verify everything is wired up
hermes-mcp doctor

# 6. Run
hermes-mcp serve

Then connect from your MCP client of choice — see Client compatibility below for per-client config snippets (Claude Desktop, Codex CLI, Cursor).

Once you've confirmed it works end-to-end, follow the named tunnel and systemd sections to make it permanent.

Try asking: "Use Hermes to schedule a daily cron job that emails me a summary of my inbox at 8am."


Configuration

All settings via environment variables. See .env.example for the canonical list.

Variable Required Default Purpose
OAUTH_CLIENT_ID yes Static OAuth 2.1 client ID. Generate with hermes-mcp mint-client.
OAUTH_CLIENT_SECRET yes Static OAuth 2.1 client secret (≥32 chars). Generate with hermes-mcp mint-client.
OAUTH_ISSUER_URL yes Public HTTPS URL where the server is reachable (your tunnel hostname).
HERMES_API_KEY yes Bearer token for the local Hermes gateway's OpenAI-compatible API (the API_SERVER_KEY from ~/.hermes/.env).
HERMES_API_URL no http://127.0.0.1:8642 Base URL of the running Hermes gateway.
HERMES_MODEL no hermes-agent Model identifier sent to /v1/chat/completions.
MCP_ALLOWED_HOSTS no (localhost only) Comma-separated additional Host header values to accept (typically your public tunnel hostname). MCP uses this for DNS-rebinding protection.
BIND_HOST no 127.0.0.1 Bind address. The tunnel reaches it on localhost. Do not bind 0.0.0.0 unless you understand the implications.
BIND_PORT no 8765 Port.
HERMES_REQUEST_TIMEOUT_SECONDS no 300 Max wall-clock per hermes_ask call.
OAUTH_ALLOWED_REDIRECT_SCHEMES no claude,claudeai,cursor Comma-separated OAuth redirect-URI custom schemes to accept. https and http-on-localhost always allowed. Extend to add support for new MCP clients (e.g. add vscode for Continue).
LOG_LEVEL no INFO DEBUG enables prompt-body logging.

Client compatibility

hermes-mcp speaks plain Streamable HTTP + OAuth 2.1, but the OAuth provider is configured for static, pre-shared credentials (single OAUTH_CLIENT_ID / OAUTH_CLIENT_SECRET pair, DCR disabled). A client can connect only if it supports pasting in static OAuth client credentials. The MCP ecosystem is mid-transition: some clients still support this, others have moved to Dynamic Client Registration only.

Tested ✅

Client How to connect
Claude Desktop / Claude.ai (web + mobile) Settings → Connectors → Add custom connector → paste the server URL + your OAUTH_CLIENT_ID + your OAUTH_CLIENT_SECRET.

Known incompatible (DCR-only) ❌

Client Status Tracking
OpenAI Codex CLI Empirically confirmed incompatible — codex mcp add / codex mcp login auto-attempts Dynamic Client Registration and fails with Registration failed: Dynamic client registration not supported. No documented way to pre-configure a static client_id / client_secret. Add DCR support — coming in a future release.

Untested (likely DCR-only, unconfirmed)

These clients use OAuth + custom URI schemes per the MCP spec convention, which strongly suggests they also rely on DCR. We haven't tested them. The scheme allowlist defaults already include their custom URI schemes, so the moment DCR support lands they'll likely work.

Client Custom redirect scheme Docs
Cursor cursor://anysphere.cursor-mcp/oauth/callback (already in default allowlist) ~/.cursor/mcp.json
Continue (VSCode) vscode:// (add to OAUTH_ALLOWED_REDIRECT_SCHEMES) Continue MCP docs

If you successfully connect one of these (or another client not listed), please open an issue — we'll promote it to "Tested" with your specific config notes.

Adding a new client whose custom URI scheme isn't in the default

If your client supports static OAuth credentials AND uses a redirect scheme not in the default claude,claudeai,cursor, add it to OAUTH_ALLOWED_REDIRECT_SCHEMES:

export OAUTH_ALLOWED_REDIRECT_SCHEMES=claude,claudeai,cursor,vscode

Then restart hermes-mcp and complete the OAuth handshake from the new client.

What MCP clients see

The MCP server exposes four tools:

hermes_ask(prompt, session_id?, toolsets?, async_mode?)

Delegates a task to Hermes. Use it for anything the calling LLM cannot do directly:

  • Scheduling cron jobs / recurring tasks
  • Browser-driven web search and scraping
  • Sending email
  • Creating, saving, or editing local documents
  • Anything that should persist after this chat ends (Hermes memory, skills)
  • Sending WhatsApp / Slack messages via Hermes's messaging gateway

Pass the same session_id across calls within one chat to let Hermes build on previous steps (draft → refine → save). It is forwarded as the X-Hermes-Session-Id header so Hermes threads the call into an existing session.

The toolsets argument is accepted for backward compatibility but is currently ignored — toolset selection now lives in your Hermes config (platform_toolsets.api_server). Set it there to match the Telegram surface (typically [hermes-telegram]) so MCP clients get the same tools the Telegram path does.

Async mode for long-running tasks

Most MCP clients enforce a per-tool-call timeout: Claude.ai / Claude Desktop is roughly two minutes; Codex CLI, Cursor, and others differ. If Hermes is going to take longer than the client's limit, the call fails with a tool-execution error and any side effects already started (emails sent, files created) keep running but aren't reported back. Async mode sidesteps this:

// hermes_ask(prompt="...", async_mode=true) returns immediately:
{"job_id": "8a3f...e21", "status": "pending"}

Then poll hermes_check(job_id) until status is completed, failed, or cancelled. Hermes keeps running in the background regardless of whether you poll. Jobs are stored in-memory for ~24 hours and lost on a server restart.

When to use async — the calling LLM reads heuristics from the tool description and should pick the right mode on its own, but the rules of thumb are:

Use async when ANY is true Use sync only when ALL are true
3+ distinct external actions (multi-folder, multi-issue, multi-email) Exactly one external action, or none
Browser-driven work (scraping, research) Confident response in <30s
Drive trees, doc generation, multi-recipient outreach No Telegram-approval-gated tools likely to fire
Multi-step agentic work (Hermes will chain tools)
Estimated >30 seconds
Telegram approval buttons may appear
You're not sure — pick async

False async costs you a polling loop. False sync costs you the whole task hitting the 2-minute cliff. The asymmetry strongly favors async when in doubt.

If you want to force the choice, just say so in your prompt: "use async_mode=true for this".

hermes_check(job_id)

Returns a JSON string with the current status of an async job:

{"job_id": "8a3f...e21", "status": "completed", "created_at": 1747...,
 "finished_at": 1747..., "prompt_chars": 12303, "session_id": "...",
 "result": "..."}
{"job_id": "8a3f...e21", "status": "failed",    "error":  "...", ...}
{"job_id": "8a3f...e21", "status": "cancelled", ...}
{"job_id": "8a3f...e21", "status": "running",   ...}
{"job_id": "8a3f...e21", "status": "pending",   ...}
{"job_id": "<your-input>", "status": "unknown"} // never issued by this server, reaped after 24h, or wiped by hermes_reset

created_at and finished_at are epoch seconds — the calling LLM can subtract them to show "running for N minutes" in chat.

hermes_cancel(job_id)

Releases an in-flight async job. Critical caveat: this does NOT stop the gateway from running.

Python threads can't be safely killed mid-httpx.post, so cancellation is bookkeeping only: subsequent hermes_check calls return status: cancelled, but the Hermes worker keeps running until it finishes or hits its 300-second timeout. Anything Hermes does in the meantime — emails sent, Drive files created, Linear issues opened — happens anyway.

Cancel when you want to release the result, not undo the work. If the work needs to be undone, ask Hermes to undo it explicitly.

Returns the same JSON shape as hermes_check. Cancelling an already-terminal job is a no-op and returns the current status unchanged.

hermes_reset()

Wipes every job from the in-memory store in a single call. Use this to recover from a cluttered or stuck queue without restarting the server process. After it returns, every prior job_id becomes unknown on hermes_check and hermes_cancel.

// hermes_reset() returns:
{"cleared": 4, "by_status": {"running": 1, "pending": 3}}
{"cleared": 0, "by_status": {}}   // empty store

Same caveat as hermes_cancel, but applied to everything at once: it does not stop in-flight worker threads or gateway calls. Workers whose jobs are wiped run to completion and their side effects happen anyway; their eventual mark_completed becomes a safe no-op when the job id is gone.

The job store is shared across all MCP callers. If multiple client sessions (Claude, Codex, Cursor, ...) or a background Hermes-agent workflow are pointed at the same MCP bridge, resetting wipes their jobs too. Treat it as a global operation and confirm with the user before calling it if other work might be in flight.

Expired terminal jobs (older than the 24h TTL) are reaped lazily before counting, so the by_status map reflects only what was actually live in the store at call time.

Network exposure: cloudflared

Recommended. Free, open-source, no bandwidth cap that matters at personal scale.

There are two flavors. Use the quick tunnel to test today; use the named tunnel for any setup you want to leave running.

Quick tunnel (for testing)

Throwaway URL, no Cloudflare account needed, dies on cloudflared restart. Perfect for the first end-to-end test.

# 1. Install cloudflared
sudo apt install cloudflared        # or download from cloudflare.com

# 2. Run a quick tunnel pointed at the local bridge
cloudflared tunnel --url http://127.0.0.1:8765

cloudflared prints a URL like https://random-words-here.trycloudflare.com. That's your tunnel for as long as the process runs. Use it as the server URL in your MCP client (see Client compatibility above):

Connector URL:  https://random-words-here.trycloudflare.com/mcp
Client ID:      <from `hermes-mcp mint-client`>
Client Secret:  <from `hermes-mcp mint-client`>

Set OAUTH_ISSUER_URL to https://random-words-here.trycloudflare.com and add the hostname to MCP_ALLOWED_HOSTS so MCP's DNS-rebinding check accepts it.

⚠ Quick tunnels are ephemeral. The hostname changes every restart — your client's connector breaks every time. Move to a named tunnel as soon as you're past the smoke test.

Named tunnel (for keeping it)

Stable hostname on a Cloudflare-managed domain. Survives reboots.

Prerequisite: a domain on Cloudflare DNS. Easiest is registering one through Cloudflare Registrar (~$10/yr, sold at cost). If you already have a domain elsewhere, change its nameservers at the registrar to the two Cloudflare gives you, wait for the zone to go Active, then continue. Don't put your primary domain on Cloudflare DNS without first auditing email/Workspace records — you'll need to verify Cloudflare's auto-import covers MX, SPF, DKIM, and DMARC before changing nameservers. Buying a separate cheap domain just for the tunnel is the boring safe move.

# 1. Authorize this machine on your Cloudflare account (interactive: opens a URL)
cloudflared tunnel login

# 2. Create the tunnel — pick any name, e.g. "hermes"
cloudflared tunnel create hermes

# 3. Route a DNS hostname to it (requires the domain be on Cloudflare DNS)
cloudflared tunnel route dns hermes hermes.your-domain.example

# 4. Configure ~/.cloudflared/config.yml
cat > ~/.cloudflared/config.yml <<EOF
tunnel: <UUID-from-step-2>
credentials-file: $HOME/.cloudflared/<UUID-from-step-2>.json
ingress:
  - hostname: hermes.your-domain.example
    service: http://127.0.0.1:8765
  - service: http_status:404
EOF

# 5. Test it
cloudflared tunnel run hermes
# In another terminal:
#   curl -sS https://hermes.your-domain.example/.well-known/oauth-authorization-server
#   ⇒ should print the OAuth metadata JSON

Your stable URL is now https://hermes.your-domain.example. Update your MCP client to point at <URL>/mcp, set OAUTH_ISSUER_URL to the URL, and add hermes.your-domain.example to MCP_ALLOWED_HOSTS.

Run cloudflared as a systemd user service — see deploy/cloudflared.service:

mkdir -p ~/.config/systemd/user
cp deploy/cloudflared.service ~/.config/systemd/user/cloudflared.service
systemctl --user daemon-reload
systemctl --user enable --now cloudflared.service
journalctl --user -u cloudflared -f

Alternative tunnel: ngrok

Equally valid; pick this if you already have an ngrok account and don't want to set up Cloudflare DNS.

ngrok config add-authtoken <your-token>

# Free tier includes one stable static domain
ngrok http 8765 --domain=your-name.ngrok-free.app

A systemd unit is provided in deploy/ngrok.service.

Adding the connector in Claude

Claude Desktop: Settings → Connectors → Add custom connector → paste <tunnel-url>/mcp → paste your OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET. Claude completes the OAuth 2.1 authorization-code flow with PKCE automatically.

Claude mobile app: same flow under Settings → Connectors. The connector you add is per-account, so it works on both Desktop and mobile from one configuration.

Screenshots are coming once we cut a v0.1.0 release. PRs welcome.

Running as a service on the mini-PC

Install deploy/hermes-mcp.service as a systemd user unit so it shares the lifecycle of your other personal services (e.g. hermes-gateway, mcp-proxy):

# 1. Install hermes-mcp on a stable path
pipx install hermes-mcp

# 2. Set up the env file (mode 0600)
mkdir -p ~/.config/hermes-mcp
install -m 0600 .env.example ~/.config/hermes-mcp/env
$EDITOR ~/.config/hermes-mcp/env       # fill in OAUTH_*, HERMES_API_KEY, etc.

# 3. Install the unit
mkdir -p ~/.config/systemd/user
cp deploy/hermes-mcp.service ~/.config/systemd/user/

# 4. Make sure user services start at boot, not just login
loginctl enable-linger "$USER"

# 5. Enable + start
systemctl --user daemon-reload
systemctl --user enable --now hermes-mcp
journalctl --user -u hermes-mcp -f

Restart after editing the env file: systemctl --user restart hermes-mcp.

What survives a reboot

Concern Behavior
hermes-mcp process Starts at boot. ✅
cloudflared tunnel Starts at boot. ✅
Tunnel URL Stable (named tunnel). ✅
OAuth client_id / client_secret Read from ~/.config/hermes-mcp/env at startup. ✅
Live OAuth access / refresh tokens Stored in memory only — lost on every restart.

Practical impact: the host can reboot freely; the bridge comes back up on the same URL. But Claude Desktop is holding access and refresh tokens that are now invalid (the in-memory store they were minted from is gone). On the next call, Claude usually reports "Error occurred during tool execution" rather than transparently re-running OAuth.

The fix is one click, not re-entering credentials:

Claude Desktop → Settings → Connectors → click your hermes-mcp connector → DisconnectReconnect.

Claude does the OAuth flow against the bridge using the saved client_id / client_secret and you're back online. Same goes for any time you systemctl --user restart hermes-mcp (e.g. after editing the env file or upgrading the package).

This is a known limitation of the in-memory token store. Persisting tokens to disk is on the roadmap.

Security

This bridge lets a remote LLM run actions on your machine via Hermes. Treat it accordingly. Full threat model in THREAT_MODEL.md. In short:

  • Do not run Hermes with --yolo. Keep approval hooks on.
  • Scope platform_toolsets.api_server in your Hermes config to the minimum toolset your use case needs (see What Claude sees).
  • The OAuth client_secret and HERMES_API_KEY are credentials. A leaked client_secret lets an attacker mint access tokens against your bridge; a leaked HERMES_API_KEY lets them bypass the bridge and call the gateway directly. Rotate (hermes-mcp mint-client for OAuth; edit API_SERVER_KEY in ~/.hermes/.env for the gateway) if exposed.
  • Prompt injection is real. A malicious prompt slipping into Claude's context (via a webpage, a file you pasted) can craft tool calls. Hermes's own approval hooks are your last line of defense — keep them on.

Code-side mitigations baked in:

  • OAuth 2.1 with mandatory PKCE-S256. client_secret comparison via hmac.compare_digest. Authorization codes are single-use with atomic pop-on-exchange; refresh tokens rotate atomically and approximate RFC 6819 reuse detection.
  • redirect_uri scheme allowlist on /authorize (https, http-on-localhost, claude, claudeai) prevents the bridge becoming an open redirector to javascript: / data: URIs.
  • Access tokens are 256-bit secrets.token_urlsafe, expire after 1 hour, live only in memory (no on-disk persistence). Refresh tokens 30d, also in memory.
  • DNS-rebinding protection via MCP_ALLOWED_HOSTS enforced at the transport layer.
  • Prompt bodies and gateway response bodies logged only at DEBUG. INFO logs are endpoint + length + session_id + duration only. The OAuth state parameter is sanitized before logging.
  • Bind defaults to 127.0.0.1; non-loopback BIND_HOST triggers a startup warning.
  • No telemetry, ever. Your prompts go Claude → tunnel edge → bridge → gateway. Nothing else.

Common pitfalls

  • hermes-mcp doctor reports "hermes gateway unreachable" → the gateway isn't running. systemctl --user status hermes-gateway will tell you why.
  • doctor reports "rejected the API key (401)"HERMES_API_KEY doesn't match API_SERVER_KEY in ~/.hermes/.env. Update one or the other and restart.
  • Connector stuck on "Verifying" → 9 times out of 10 it's a wrong client_id or client_secret, or OAUTH_ISSUER_URL doesn't match the URL you pasted into Claude. They must be the same hostname.
  • "Invalid Host header" / 421 → your tunnel hostname isn't in MCP_ALLOWED_HOSTS. Add it (comma-separated) and restart.
  • Cloudflared 502hermes-mcp isn't running. journalctl --user -u hermes-mcp will tell you why.
  • After a reboot or systemctl --user restart hermes-mcp, Claude says "Error occurred during tool execution" → expected. OAuth tokens are in-memory; restarting the bridge invalidates them. Fix: in Claude Desktop, Settings → Connectors → your hermes-mcp connector → Disconnect → Reconnect. The client_id/client_secret are saved, so Claude re-auths in a few seconds. See What survives a reboot.

Contributing

See CONTRIBUTING.md and CODE_OF_CONDUCT.md.

License

Apache-2.0. See LICENSE.

Status

This is an unofficial bridge. It is not affiliated with or endorsed by the Hermes Agent project, and not affiliated with Anthropic.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hermes_mcp-0.4.0.tar.gz (67.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hermes_mcp-0.4.0-py3-none-any.whl (35.1 kB view details)

Uploaded Python 3

File details

Details for the file hermes_mcp-0.4.0.tar.gz.

File metadata

  • Download URL: hermes_mcp-0.4.0.tar.gz
  • Upload date:
  • Size: 67.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for hermes_mcp-0.4.0.tar.gz
Algorithm Hash digest
SHA256 373b0028cee7898423f4a264f9e935fd9130325d68e36edbe029c7340443c931
MD5 511faf74148eb8e3b9fc0c898fd5c121
BLAKE2b-256 07205d9ed6051f14ccf27a15fe4a2c0be5386043522a1fbecd69334b952cbf37

See more details on using hashes here.

Provenance

The following attestation bundles were made for hermes_mcp-0.4.0.tar.gz:

Publisher: release.yml on mlennie/hermes-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hermes_mcp-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: hermes_mcp-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 35.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for hermes_mcp-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 573dfa9ff0413f6c93b8b664d55308b5be0c4930c192e159954852c716004e59
MD5 6e2a5c7ebe5393dde0d4b5b149f86142
BLAKE2b-256 125b2e6a7803fca959a881a570cdd8fd20848bf741a5d7767bcddcabd96a6e93

See more details on using hashes here.

Provenance

The following attestation bundles were made for hermes_mcp-0.4.0-py3-none-any.whl:

Publisher: release.yml on mlennie/hermes-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page