Install
$ agentstack add mcp-mlennie-hermes-mcp ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
hermes-mcp
> An MCP server that lets Claude Desktop, Claude.ai (web + mobile), OpenAI Codex desktop, Cursor, and any other MCP client (via OAuth 2.1 or static bearer token) delegate tasks to a local Hermes Agent running on your own hardware. (See [Client compatibility](#client-compatibility) for the current matrix.)
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=
export OAUTH_CLIENT_SECRET=
export OAUTH_ISSUER_URL=https://random-words-here.trycloudflare.com
export MCP_ALLOWED_HOSTS=random-words-here.trycloudflare.com
export HERMES_API_KEY=
# 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](#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](#named-tunnel-for-keeping-it) and [systemd](#running-as-a-service-on-the-mini-pc) 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](.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). | | MCP_BEARER_TOKEN | no | (unset) | Optional static bearer token (32+ chars). When set, the server accepts Authorization: Bearer directly at /mcp, in addition to OAuth. Necessary for MCP clients whose UI has no OAuth flow (Codex desktop's custom-MCP form, Cursor's headers block). Generate with hermes-mcp mint-bearer-token. | | LOG_LEVEL | no | INFO | DEBUG enables prompt-body logging. |
Client compatibility
hermes-mcp speaks plain Streamable HTTP and supports two auth paths so different MCP clients can connect:
- OAuth 2.1 (PKCE-only public client). Static
OAUTH_CLIENT_ID+ auto-approve/authorize. PKCE is the dynamic per-exchange secret;client_secretis accepted in the form but no longer enforced (clients that send one still work, clients that omit it work too). DCR is disabled. - Static bearer token. Set
MCP_BEARER_TOKENand the server acceptsAuthorization: Bearerdirectly at/mcp, bypassing OAuth entirely. Necessary for clients whose UI has no OAuth field.
Pick whichever the client's UI supports — both auth paths coexist on the same server instance.
Tested ✅
| Client | Auth | How to connect | |---|---|---| | Claude Desktop / Claude.ai (web + mobile) | OAuth | Settings → Connectors → Add custom connector → paste the server URL + your OAUTH_CLIENT_ID + your OAUTH_CLIENT_SECRET. (The secret is accepted but no longer enforced server-side; the field is still required by Claude's UI, so set it.) | | OpenAI Codex desktop | Bearer | Settings → MCP → Connect to a custom MCP → Streamable HTTP → URL = your tunnel + /mcp, Bearer token env var = name of an OS env var on your laptop that holds your MCP_BEARER_TOKEN value. Restart Codex desktop after setting the env var so it's inherited. Skips OAuth entirely. | | Cursor | Bearer | Settings → MCP → Add custom MCP → paste the JSON below into ~/.cursor/mcp.json. No OAuth flow, no extra config. |
Cursor — exact ~/.cursor/mcp.json snippet
{
"mcpServers": {
"hermes": {
"url": "https://hermes.claude-hermes-mcp.com/mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}
Replace ` with the token printed by hermes-mcp mint-bearer-token. The token sits in your laptop's ~/.cursor/mcp.json (file-mode protected by your OS); if you'd rather not have it there in cleartext, use "Authorization": "Bearer ${env:HERMESMCPBEARER}"` and set the env var on your OS instead. Save the file and Cursor picks it up automatically — no restart needed.
Untested (likely workable via bearer)
These clients all support setting custom request headers, which should be enough to drive the bearer-token path. We haven't tested them end-to-end — if you connect one, please open an issue with your config so we can promote it.
| Client | Likely auth | Where to set it | |---|---|---| | Continue (VSCode) | Bearer via requestOptions.headers | continue config: requestOptions.headers.Authorization = "Bearer ..." | | OpenAI Codex CLI | OAuth (PKCE) | ~/.codex/config.toml → [mcp_servers.hermes.oauth] client_id = "...". Caveat: Codex CLI's OAuth flow uses a localhost callback on whichever machine codex runs on; if you SSH from a laptop into the mini-PC where hermes-mcp lives, the laptop browser can't reach that callback. Easiest workaround is to use the desktop app (above) instead. |
Adding a new client whose custom URI scheme isn't in the default
If your client uses OAuth AND 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 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": "", "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](#client-compatibility) above):
Connector URL: https://random-words-here.trycloudflare.com/mcp
Client ID:
Client Secret:
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 b
…
## Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [mlennie](https://github.com/mlennie)
- **Source:** [mlennie/hermes-mcp](https://github.com/mlennie/hermes-mcp)
- **License:** Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.