[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80778":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":15,"stars7d":16,"stars30d":17,"stars90d":15,"forks30d":15,"starsTrendScore":15,"compositeScore":18,"rankGlobal":10,"rankLanguage":10,"license":19,"archived":20,"fork":20,"defaultBranch":21,"hasWiki":22,"hasPages":20,"topics":23,"createdAt":10,"pushedAt":10,"updatedAt":24,"readmeContent":25,"aiSummary":26,"trendingCount":15,"starSnapshotCount":15,"syncStatus":16,"lastSyncTime":27,"discoverSource":28},80778,"hermes-mcp","mlennie\u002Fhermes-mcp","mlennie"," MCP server that lets any llm (i.e. Claude Desktop) delegate tasks to a local Hermes Agent — cron jobs, web search, email, documents, and more.","",null,"Python",44,9,1,0,2,4,44.4,"Apache License 2.0",false,"main",true,[],"2026-06-12 04:01:30","# hermes-mcp\n\n> 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](https:\u002F\u002Fgithub.com\u002Fhermes-agent\u002Fhermes-agent)** running on your own hardware. (See [Client compatibility](#client-compatibility) for the current matrix.)\n\nUse 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\u002FSlack messaging — your client calls Hermes through this bridge.\n\n```\n┌──────────────────────────────────────────────────────┐\n│ MCP client                                           │\n│ (Claude Desktop \u002F Claude.ai \u002F Codex CLI \u002F Cursor \u002F…) │\n└────────────────────────┬─────────────────────────────┘\n                         │ HTTPS + OAuth 2.1\n                         ▼\n                ┌──────────────────────┐\n                │ cloudflared tunnel   │  (public HTTPS edge)\n                └──────────┬───────────┘\n                           │\n                           ▼ localhost:8765\n                ┌──────────────────────┐\n                │ hermes-mcp           │  (FastMCP, Streamable HTTP)\n                │ - OAuth 2.1 + PKCE   │\n                │ - HTTP -> gateway    │\n                └──────────┬───────────┘\n                           │ HTTP \u002Fv1\u002Fchat\u002Fcompletions\n                           ▼ localhost:8642\n                ┌──────────────────────┐\n                │ hermes-gateway       │  (the running Hermes brain;\n                │                      │   same agent loop Telegram uses)\n                └──────────────────────┘\n```\n\n## Quickstart\n\nThese steps assume you already have **Hermes Agent** installed and working on a Linux\u002FWSL machine, with the gateway listening on `127.0.0.1:8642`.\n\n```bash\n# 1. Install\npipx install hermes-mcp\n\n# 2. Mint OAuth client credentials\nhermes-mcp mint-client                  # prints OAUTH_CLIENT_ID + OAUTH_CLIENT_SECRET\n\n# 3. Start a quick tunnel (testing only — URL changes on restart)\ncloudflared tunnel --url http:\u002F\u002F127.0.0.1:8765\n# prints: https:\u002F\u002Frandom-words-here.trycloudflare.com\n\n# 4. Export env vars (using the URL from step 3)\nexport OAUTH_CLIENT_ID=\u003Cfrom step 2>\nexport OAUTH_CLIENT_SECRET=\u003Cfrom step 2>\nexport OAUTH_ISSUER_URL=https:\u002F\u002Frandom-words-here.trycloudflare.com\nexport MCP_ALLOWED_HOSTS=random-words-here.trycloudflare.com\nexport HERMES_API_KEY=\u003Cthe API_SERVER_KEY from ~\u002F.hermes\u002F.env>\n\n# 5. Verify everything is wired up\nhermes-mcp doctor\n\n# 6. Run\nhermes-mcp serve\n```\n\nThen connect from your MCP client of choice — see [Client compatibility](#client-compatibility) below for per-client config snippets (Claude Desktop, Codex CLI, Cursor).\n\nOnce 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.\n\nTry asking: *\"Use Hermes to schedule a daily cron job that emails me a summary of my inbox at 8am.\"*\n\n---\n\n## Configuration\n\nAll settings via environment variables. See [`.env.example`](.env.example) for the canonical list.\n\n| Variable | Required | Default | Purpose |\n|---|---|---|---|\n| `OAUTH_CLIENT_ID` | **yes** | — | Static OAuth 2.1 client ID. Generate with `hermes-mcp mint-client`. |\n| `OAUTH_CLIENT_SECRET` | **yes** | — | Static OAuth 2.1 client secret (≥32 chars). Generate with `hermes-mcp mint-client`. |\n| `OAUTH_ISSUER_URL` | **yes** | — | Public HTTPS URL where the server is reachable (your tunnel hostname). |\n| `HERMES_API_KEY` | **yes** | — | Bearer token for the local Hermes gateway's OpenAI-compatible API (the `API_SERVER_KEY` from `~\u002F.hermes\u002F.env`). |\n| `HERMES_API_URL` | no | `http:\u002F\u002F127.0.0.1:8642` | Base URL of the running Hermes gateway. |\n| `HERMES_MODEL` | no | `hermes-agent` | Model identifier sent to `\u002Fv1\u002Fchat\u002Fcompletions`. |\n| `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. |\n| `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. |\n| `BIND_PORT` | no | `8765` | Port. |\n| `HERMES_REQUEST_TIMEOUT_SECONDS` | no | `300` | Max wall-clock per `hermes_ask` call. |\n| `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). |\n| `MCP_BEARER_TOKEN` | no | (unset) | Optional static bearer token (32+ chars). When set, the server accepts `Authorization: Bearer \u003Ctoken>` directly at `\u002Fmcp`, 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`. |\n| `LOG_LEVEL` | no | `INFO` | `DEBUG` enables prompt-body logging. |\n\n## Client compatibility\n\nhermes-mcp speaks plain **Streamable HTTP** and supports two auth paths so different MCP clients can connect:\n\n- **OAuth 2.1 (PKCE-only public client).** Static `OAUTH_CLIENT_ID` + auto-approve `\u002Fauthorize`. PKCE is the dynamic per-exchange secret; `client_secret` is accepted in the form but no longer enforced (clients that send one still work, clients that omit it work too). DCR is disabled.\n- **Static bearer token.** Set `MCP_BEARER_TOKEN` and the server accepts `Authorization: Bearer \u003Ctoken>` directly at `\u002Fmcp`, bypassing OAuth entirely. Necessary for clients whose UI has no OAuth field.\n\nPick whichever the client's UI supports — both auth paths coexist on the same server instance.\n\n### Tested ✅\n\n| Client | Auth | How to connect |\n|---|---|---|\n| **Claude Desktop \u002F 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.) |\n| **OpenAI Codex desktop** | Bearer | Settings → MCP → Connect to a custom MCP → Streamable HTTP → URL = your tunnel + `\u002Fmcp`, **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. |\n| **Cursor** | Bearer | Settings → MCP → Add custom MCP → paste the JSON below into `~\u002F.cursor\u002Fmcp.json`. No OAuth flow, no extra config. |\n\n#### Cursor — exact `~\u002F.cursor\u002Fmcp.json` snippet\n\n```json\n{\n  \"mcpServers\": {\n    \"hermes\": {\n      \"url\": \"https:\u002F\u002Fhermes.claude-hermes-mcp.com\u002Fmcp\",\n      \"headers\": {\n        \"Authorization\": \"Bearer \u003Cyour-MCP_BEARER_TOKEN>\"\n      }\n    }\n  }\n}\n```\n\nReplace `\u003Cyour-MCP_BEARER_TOKEN>` with the token printed by `hermes-mcp mint-bearer-token`. The token sits in your laptop's `~\u002F.cursor\u002Fmcp.json` (file-mode protected by your OS); if you'd rather not have it there in cleartext, use `\"Authorization\": \"Bearer ${env:HERMES_MCP_BEARER}\"` and set the env var on your OS instead. Save the file and Cursor picks it up automatically — no restart needed.\n\n### Untested (likely workable via bearer)\n\nThese 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](https:\u002F\u002Fgithub.com\u002Fmlennie\u002Fhermes-mcp\u002Fissues) with your config so we can promote it.\n\n| Client | Likely auth | Where to set it |\n|---|---|---|\n| **Continue (VSCode)** | Bearer via `requestOptions.headers` | `continue` config: `requestOptions.headers.Authorization = \"Bearer ...\"` |\n| **OpenAI Codex CLI** | OAuth (PKCE) | `~\u002F.codex\u002Fconfig.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. |\n\n### Adding a new client whose custom URI scheme isn't in the default\n\nIf your client uses OAuth AND a redirect scheme not in the default `claude,claudeai,cursor`, add it to `OAUTH_ALLOWED_REDIRECT_SCHEMES`:\n\n```bash\nexport OAUTH_ALLOWED_REDIRECT_SCHEMES=claude,claudeai,cursor,vscode\n```\n\nThen restart `hermes-mcp` and complete the OAuth handshake from the new client.\n\n## What MCP clients see\n\nThe MCP server exposes four tools:\n\n### `hermes_ask(prompt, session_id?, toolsets?, async_mode?)`\n\nDelegates a task to Hermes. Use it for anything the calling LLM cannot do directly:\n\n- Scheduling cron jobs \u002F recurring tasks\n- Browser-driven web search and scraping\n- Sending email\n- Creating, saving, or editing local documents\n- Anything that should persist after this chat ends (Hermes memory, skills)\n- Sending WhatsApp \u002F Slack messages via Hermes's messaging gateway\n\nPass 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.\n\nThe `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.\n\n#### Async mode for long-running tasks\n\nMost MCP clients enforce a per-tool-call timeout: Claude.ai \u002F 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:\n\n```jsonc\n\u002F\u002F hermes_ask(prompt=\"...\", async_mode=true) returns immediately:\n{\"job_id\": \"8a3f...e21\", \"status\": \"pending\"}\n```\n\nThen 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.\n\n**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:\n\n| Use **async** when ANY is true | Use **sync** only when ALL are true |\n|---|---|\n| 3+ distinct external actions (multi-folder, multi-issue, multi-email) | Exactly one external action, or none |\n| Browser-driven work (scraping, research) | Confident response in \u003C30s |\n| Drive trees, doc generation, multi-recipient outreach | No Telegram-approval-gated tools likely to fire |\n| Multi-step agentic work (Hermes will chain tools) | |\n| Estimated >30 seconds | |\n| Telegram approval buttons may appear | |\n| **You're not sure** — pick async | |\n\nFalse 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.\n\nIf you want to force the choice, just say so in your prompt: *\"use `async_mode=true` for this\"*.\n\n### `hermes_check(job_id)`\n\nReturns a JSON string with the current status of an async job:\n\n```jsonc\n{\"job_id\": \"8a3f...e21\", \"status\": \"completed\", \"created_at\": 1747...,\n \"finished_at\": 1747..., \"prompt_chars\": 12303, \"session_id\": \"...\",\n \"result\": \"...\"}\n{\"job_id\": \"8a3f...e21\", \"status\": \"failed\",    \"error\":  \"...\", ...}\n{\"job_id\": \"8a3f...e21\", \"status\": \"cancelled\", ...}\n{\"job_id\": \"8a3f...e21\", \"status\": \"running\",   ...}\n{\"job_id\": \"8a3f...e21\", \"status\": \"pending\",   ...}\n{\"job_id\": \"\u003Cyour-input>\", \"status\": \"unknown\"} \u002F\u002F never issued by this server, reaped after 24h, or wiped by hermes_reset\n```\n\n`created_at` and `finished_at` are epoch seconds — the calling LLM can subtract them to show \"running for N minutes\" in chat.\n\n### `hermes_cancel(job_id)`\n\nReleases an in-flight async job. **Critical caveat: this does NOT stop the gateway from running.**\n\nPython 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*.\n\nCancel when you want to **release the result**, not undo the work. If the work needs to be undone, ask Hermes to undo it explicitly.\n\nReturns the same JSON shape as `hermes_check`. Cancelling an already-terminal job is a no-op and returns the current status unchanged.\n\n### `hermes_reset()`\n\nWipes 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`.\n\n```jsonc\n\u002F\u002F hermes_reset() returns:\n{\"cleared\": 4, \"by_status\": {\"running\": 1, \"pending\": 3}}\n{\"cleared\": 0, \"by_status\": {}}   \u002F\u002F empty store\n```\n\nSame 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.\n\n**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.\n\nExpired 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.\n\n## Network exposure: `cloudflared`\n\nRecommended. Free, open-source, no bandwidth cap that matters at personal scale.\n\nThere are two flavors. Use the **quick tunnel** to test today; use the **named tunnel** for any setup you want to leave running.\n\n### Quick tunnel (for testing)\n\nThrowaway URL, no Cloudflare account needed, dies on `cloudflared` restart. Perfect for the first end-to-end test.\n\n```bash\n# 1. Install cloudflared\nsudo apt install cloudflared        # or download from cloudflare.com\n\n# 2. Run a quick tunnel pointed at the local bridge\ncloudflared tunnel --url http:\u002F\u002F127.0.0.1:8765\n```\n\n`cloudflared` prints a URL like `https:\u002F\u002Frandom-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):\n\n```\nConnector URL:  https:\u002F\u002Frandom-words-here.trycloudflare.com\u002Fmcp\nClient ID:      \u003Cfrom `hermes-mcp mint-client`>\nClient Secret:  \u003Cfrom `hermes-mcp mint-client`>\n```\n\nSet `OAUTH_ISSUER_URL` to `https:\u002F\u002Frandom-words-here.trycloudflare.com` and add the hostname to `MCP_ALLOWED_HOSTS` so MCP's DNS-rebinding check accepts it.\n\n⚠ 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.\n\n### Named tunnel (for keeping it)\n\nStable hostname on a Cloudflare-managed domain. Survives reboots.\n\n**Prerequisite:** a domain on Cloudflare DNS. Easiest is registering one through [Cloudflare Registrar](https:\u002F\u002Fdash.cloudflare.com\u002F?to=\u002F:account\u002Fdomains\u002Fregister) (~$10\u002Fyr, 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\u002FWorkspace 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.\n\n```bash\n# 1. Authorize this machine on your Cloudflare account (interactive: opens a URL)\ncloudflared tunnel login\n\n# 2. Create the tunnel — pick any name, e.g. \"hermes\"\ncloudflared tunnel create hermes\n\n# 3. Route a DNS hostname to it (requires the domain be on Cloudflare DNS)\ncloudflared tunnel route dns hermes hermes.your-domain.example\n\n# 4. Configure ~\u002F.cloudflared\u002Fconfig.yml\ncat > ~\u002F.cloudflared\u002Fconfig.yml \u003C\u003CEOF\ntunnel: \u003CUUID-from-step-2>\ncredentials-file: $HOME\u002F.cloudflared\u002F\u003CUUID-from-step-2>.json\ningress:\n  - hostname: hermes.your-domain.example\n    service: http:\u002F\u002F127.0.0.1:8765\n  - service: http_status:404\nEOF\n\n# 5. Test it\ncloudflared tunnel run hermes\n# In another terminal:\n#   curl -sS https:\u002F\u002Fhermes.your-domain.example\u002F.well-known\u002Foauth-authorization-server\n#   ⇒ should print the OAuth metadata JSON\n```\n\nYour stable URL is now `https:\u002F\u002Fhermes.your-domain.example`. Update your MCP client to point at `\u003CURL>\u002Fmcp`, set `OAUTH_ISSUER_URL` to the URL, and add `hermes.your-domain.example` to `MCP_ALLOWED_HOSTS`.\n\nRun cloudflared as a systemd user service — see [`deploy\u002Fcloudflared.service`](deploy\u002Fcloudflared.service):\n\n```bash\nmkdir -p ~\u002F.config\u002Fsystemd\u002Fuser\ncp deploy\u002Fcloudflared.service ~\u002F.config\u002Fsystemd\u002Fuser\u002Fcloudflared.service\nsystemctl --user daemon-reload\nsystemctl --user enable --now cloudflared.service\njournalctl --user -u cloudflared -f\n```\n\n## Alternative tunnel: `ngrok`\n\nEqually valid; pick this if you already have an ngrok account and don't want to set up Cloudflare DNS.\n\n```bash\nngrok config add-authtoken \u003Cyour-token>\n\n# Free tier includes one stable static domain\nngrok http 8765 --domain=your-name.ngrok-free.app\n```\n\nA systemd unit is provided in [`deploy\u002Fngrok.service`](deploy\u002Fngrok.service).\n\n## Adding the connector in Claude\n\n**Claude Desktop:** Settings → Connectors → Add custom connector → paste `\u003Ctunnel-url>\u002Fmcp` → paste your `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET`. Claude completes the OAuth 2.1 authorization-code flow with PKCE automatically.\n\n**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.\n\n> Screenshots are coming once we cut a v0.1.0 release. PRs welcome.\n\n## Running as a service on the mini-PC\n\nInstall [`deploy\u002Fhermes-mcp.service`](deploy\u002Fhermes-mcp.service) as a **systemd user unit** so it shares the lifecycle of your other personal services (e.g. `hermes-gateway`, `mcp-proxy`):\n\n```bash\n# 1. Install hermes-mcp on a stable path\npipx install hermes-mcp\n\n# 2. Set up the env file (mode 0600)\nmkdir -p ~\u002F.config\u002Fhermes-mcp\ninstall -m 0600 .env.example ~\u002F.config\u002Fhermes-mcp\u002Fenv\n$EDITOR ~\u002F.config\u002Fhermes-mcp\u002Fenv       # fill in OAUTH_*, HERMES_API_KEY, etc.\n\n# 3. Install the unit\nmkdir -p ~\u002F.config\u002Fsystemd\u002Fuser\ncp deploy\u002Fhermes-mcp.service ~\u002F.config\u002Fsystemd\u002Fuser\u002F\n\n# 4. Make sure user services start at boot, not just login\nloginctl enable-linger \"$USER\"\n\n# 5. Enable + start\nsystemctl --user daemon-reload\nsystemctl --user enable --now hermes-mcp\njournalctl --user -u hermes-mcp -f\n```\n\nRestart after editing the env file: `systemctl --user restart hermes-mcp`.\n\n### What survives a reboot\n\n| Concern | Behavior |\n|---|---|\n| `hermes-mcp` process | Starts at boot. ✅ |\n| `cloudflared` tunnel | Starts at boot. ✅ |\n| Tunnel URL | Stable (named tunnel). ✅ |\n| OAuth `client_id` \u002F `client_secret` | Read from `~\u002F.config\u002Fhermes-mcp\u002Fenv` at startup. ✅ |\n| `MCP_BEARER_TOKEN` (if set) | Read from `~\u002F.config\u002Fhermes-mcp\u002Fenv` at startup. ✅ |\n| Live OAuth access \u002F refresh tokens | **Stored in memory only — lost on every restart.** ❌ |\n\n**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.\n\n**The fix is one click**, not re-entering credentials:\n\n> Claude Desktop → **Settings → Connectors** → click your `hermes-mcp` connector → **Disconnect** → **Reconnect**.\n\nClaude does the OAuth flow against the bridge using the saved `client_id` \u002F `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).\n\nThis is a known limitation of the in-memory token store. Persisting tokens to disk is on the roadmap.\n\n## Security\n\n**This bridge lets a remote LLM run actions on your machine via Hermes.** Treat it accordingly. Full threat model in [THREAT_MODEL.md](THREAT_MODEL.md). In short:\n\n- **Do not run Hermes with `--yolo`.** Keep approval hooks on.\n- **Scope `platform_toolsets.api_server`** in your Hermes config to the minimum toolset your use case needs (see [What Claude sees](#hermes_askprompt-session_id-toolsets)).\n- **`MCP_BEARER_TOKEN` (if set) and `HERMES_API_KEY` are the long-lived credentials.** The bearer token, if configured, is the only gate behind the tunnel URL for clients on that path — a leak is equivalent to remote action execution on your host. `HERMES_API_KEY` lets an attacker bypass the bridge and call the gateway directly. Rotate (`hermes-mcp mint-bearer-token` for the bearer; edit `API_SERVER_KEY` in `~\u002F.hermes\u002F.env` for the gateway) if exposed.\n- **`OAUTH_CLIENT_SECRET` is *not* a security gate.** Despite the name, it's accepted at `\u002Ftoken` for backward compatibility (Claude's UI requires a value to send) but not enforced server-side. PKCE — specifically the mandatory `code_verifier` check at every authorization-code exchange — is what protects token issuance. Leaking `OAUTH_CLIENT_SECRET` does not on its own let an attacker mint access tokens. Treat it as you would a username, not a password.\n- **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.\n\nCode-side mitigations baked in:\n\n- OAuth 2.1 with **mandatory PKCE-S256** (server enforces `code_verifier` on every authorization-code exchange; mismatch returns `invalid_grant`). Server registered as a **public client** (`token_endpoint_auth_method=none`), so `client_secret` is not part of the auth contract — PKCE is the dynamic per-exchange secret.\n- Static bearer-token path (`MCP_BEARER_TOKEN`) compared in constant time via `hmac.compare_digest`. First use logs a single INFO-level audit line per process.\n- Authorization codes are single-use with atomic pop-on-exchange; refresh tokens rotate atomically and approximate RFC 6819 reuse detection.\n- `redirect_uri` scheme allowlist on `\u002Fauthorize` (https, http-on-localhost, claude, claudeai, cursor; configurable via `OAUTH_ALLOWED_REDIRECT_SCHEMES`) prevents the bridge becoming an open redirector to `javascript:` \u002F `data:` URIs.\n- 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.\n- DNS-rebinding protection via `MCP_ALLOWED_HOSTS` enforced at the transport layer.\n- 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.\n- Bind defaults to `127.0.0.1`; non-loopback `BIND_HOST` triggers a startup warning.\n- **No telemetry, ever.** Your prompts go Claude → tunnel edge → bridge → gateway. Nothing else.\n\n## Common pitfalls\n\n- **`hermes-mcp doctor` reports \"hermes gateway unreachable\"** → the gateway isn't running. `systemctl --user status hermes-gateway` will tell you why.\n- **`doctor` reports \"rejected the API key (401)\"** → `HERMES_API_KEY` doesn't match `API_SERVER_KEY` in `~\u002F.hermes\u002F.env`. Update one or the other and restart.\n- **Connector stuck on \"Verifying\"** → most often it's a wrong `client_id` or `OAUTH_ISSUER_URL` not matching the URL you pasted into Claude (they must be the same hostname). The `client_secret` value doesn't matter to the server but Claude won't submit the form without one — paste anything ≥1 char.\n- **\"Invalid Host header\" \u002F 421** → your tunnel hostname isn't in `MCP_ALLOWED_HOSTS`. Add it (comma-separated) and restart.\n- **Cloudflared 502** → `hermes-mcp` isn't running. `journalctl --user -u hermes-mcp` will tell you why.\n- **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`\u002F`client_secret` are saved, so Claude re-auths in a few seconds. See [What survives a reboot](#what-survives-a-reboot).\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).\n\n## License\n\nApache-2.0. See [LICENSE](LICENSE).\n\n## Status\n\nThis is an unofficial bridge. It is not affiliated with or endorsed by the Hermes Agent project, and not affiliated with Anthropic.\n","hermes-mcp 是一个MCP服务器，允许诸如Claude Desktop等语言模型将任务委托给本地运行的Hermes Agent执行，包括定时任务、网页搜索、邮件处理、文档创建等功能。该项目使用Python编写，支持OAuth 2.1认证和静态bearer token方式连接客户端，并通过cloudflared隧道提供公网HTTPS边缘服务。它适用于需要将日常聊天与自动化任务结合的场景，如个人或企业内部的自动化流程管理，特别适合已经使用Claude或其他兼容MCP客户端的用户来扩展其功能。","2026-06-11 04:02:18","CREATED_QUERY"]