MCP.Market

mcp-oauth-remote-gateway skill

by NousResearch·NousResearch/hermes-agent·263k stars·MIT

Manual OAuth for remote MCP servers on headless gateways.

A100/100content scan

Is the mcp-oauth-remote-gateway skill safe?

Clean: nothing in its files matched our rules. We read 3 files in the folder on 2026-10-25.

No findings.

Install the mcp-oauth-remote-gateway skill

A skill is a folder. Copy it into your agent's skills folder and the agent loads it when the task matches its description.

git clone --depth 1 https://github.com/NousResearch/hermes-agent.git /tmp/hermes-agent
mkdir -p ~/.claude/skills
cp -r /tmp/hermes-agent/optional-skills/mcp/mcp-oauth-remote-gateway ~/.claude/skills/mcp-oauth-remote-gateway
available in every project

In the Claude apps, zip the folder and upload it from the Skills settings. The folder on GitHub

The instructions your agent would load

SKILL.md as published, without the frontmatter. Read it on GitHub

MCP OAuth on a Remote Hermes Gateway

Overview

Hermes' built-in MCP OAuth client runs a one-shot HTTP listener on 127.0.0.1: inside the Hermes process and registers that loopback address as the OAuth redirect_uri. That works perfectly for a local CLI on the user's own machine. It breaks completely when Hermes runs as a remote gateway (container, VPS, messaging bot), because the user's browser resolves 127.0.0.1 to the user's own laptop, not the remote container — so the authorization code never reaches Hermes.

This skill does the OAuth dance by hand and writes the resulting tokens into the exact files Hermes' token storage expects, so a subsequent /reload-mcp finds cached tokens and skips the browser flow entirely.

When to Use

Use this skill when all of the following are true:

  1. The user wants to add a remote HTTP MCP server that requires OAuth (not a static Bearer token).
  2. Hermes is running as a remote gateway (container, VPS, Docker, managed service) — NOT a local CLI on the user's laptop.
  3. The server supports OAuth 2.1 with PKCE and RFC 7591 Dynamic Client Registration (most modern MCP servers do — Better Stack, Linear, Cloudflare, Datadog, etc.). If it doesn't support DCR (GitHub is the notable exception), this skill does not apply — use a pre-registered OAuth App or a Personal Access Token instead.

Do NOT use this for:

  • Local CLI Hermes — just set auth: oauth in mcp_servers. and /reload-mcp. The built-in flow opens a browser and captures the callback on localhost. Works perfectly.
  • Servers that accept a static Bearer token (API key) — always prefer headers.Authorization: "Bearer " when the user is willing. Simpler, no refresh dance.
  • GitHub Copilot MCP (api.githubcopilot.com/mcp/) — GitHub does not expose DCR. Use a PAT or a pre-registered OAuth App (see pitfall 12).

Why the Built-in OAuth Flow Fails on a Remote Gateway

Hermes' native MCP OAuth client (tools/mcp_oauth.py):

  1. Picks a free local port P.
  2. Registers a dynamic OAuth client with the AS, sending redirect_uri = http://127.0.0.1:P/callback.
  3. Starts an HTTP server on 127.0.0.1:P inside the Hermes process.
  4. Prints the authorize URL and waits for the code at its local endpoint.

When Hermes runs remotely, the 127.0.0.1 in the redirect_uri is the remote container's loopback, not the user's. After authorizing, the user's browser 302s to http://127.0.0.1:P/callback?code=..., which resolves to the user's own laptop and fails to connect. The callback never reaches the Hermes process, the flow times out, and /reload-mcp returns "No MCP tools available" with no detail.

Symptoms to recognize: [xdg-open] processes under the hermes user, an empty or missing tokens directory ($HERMESHOME/mcp-tokens/), and a reload that responds without any "Added/Reconnected: X" line in changedetail.

Cheap First Fallbacks: the Built-in Flow's Own Escape Hatches

Before any manual token surgery, check whether the built-in flow's fallbacks already cover the deployment. When Hermes detects a remote session it prints two options alongside the authorize URL (tools/mcp_oauth.py):

listener. The user authorizes, the browser fails to connect to 127.0.0.1:, and they paste the full address-bar URL (?code=...&state=...) back at the prompt. Works for SSH'd-in CLI sessions.

  1. Paste-back — on an interactive TTY, a stdin reader races the HTTP

makes the redirect reach the remote listener normally.

  1. SSH port-forward — ssh -N -L :127.0.0.1: @

Both require an interactive terminal to the Hermes host. The rest of this skill is for when there is NO interactive TTY — Hermes running purely as a messaging gateway/bot where /reload-mcp triggers the flow with nobody at a prompt.

Preferred Front Door: the Hermes Dashboard (try this BEFORE manual token surgery)

A remote Hermes gateway often also runs the dashboard web UI as a SEPARATE process (e.g. hermes dashboard --host 0.0.0.0 --port ; check with ps aux | grep 'hermes dashboard'). It exposes a connector/MCP console — endpoints like /api/mcp/servers, /api/mcp/status, and /connectors (all login-gated; a cookieless curl returning 401/302 confirms they exist).

Why the dashboard solves the core problem: when the user drives OAuth from the dashboard in their own browser, the redirect lands in a context the dashboard can capture — sidestepping the 127.0.0.1-callback failure that breaks the CLI/manual flow. So the correct escalation order for "add or re-auth an OAuth MCP server on a remote gateway" is:

  1. Dashboard, in the user's browser — the intended front door. Add servers, run OAuth, reload, all authenticated as the user. No copy-paste-callback dance, no hand-writing token files.
  2. Manual token surgery (the rest of this skill) — the FALLBACK for when there's no browser session to the dashboard (pure-chat/headless context).

Finding the dashboard's PUBLIC URL. The dashboard binds internally to 0.0.0.0:, but the user needs the externally-reachable URL. Most deploy platforms inject it into the environment — grep for it rather than making the user hunt:

env | grep -iE "HERMES_DASHBOARD_PUBLIC_URL|RAILWAY_PUBLIC_DOMAIN|RAILWAY_STATIC_URL|RAILWAY_SERVICE_.*_URL|PUBLIC_URL|BASE_URL|DOMAIN" \
  | sed -E 's/(TOKEN|SECRET|KEY|PASSWORD)=.*/\1=***REDACTED***/I'

HERMESDASHBOARDPUBLICURL is authoritative when present. On Railway also check RAILWAYPUBLICDOMAIN / RAILWAYSTATICURL (the .up.railway.app host) and RAILWAYSERVICEURL vars, which sometimes carry a friendlier custom domain. Hand the user the full https:// URL and point them at the Connectors/MCP section. ALWAYS pipe through the sed redaction above — these env greps sit next to TOKEN/SECRET vars.

What the dashboard does NOT fix (still host-side / shell): stdio servers that need shell auth state (a CLI login command whose credentials may not persist across restarts) and anything reading credentials from $HERMES_HOME/.env. Those are out of the dashboard's scope regardless.

The Workaround

Do the OAuth dance manually, then write the resulting tokens into the exact files Hermes' HermesTokenStorage would have written, so on /reload-mcp Hermes finds cached tokens and skips the browser flow entirely.

Run the shell commands below through the terminal tool on the gateway host and do the Python steps (PKCE generation, token exchange, file writes) via execute_code or a terminal python3 invocation — file writes must happen in the SAME code block as the token exchange (see pitfall 16).

1. Confirm it's a remote gateway

env | grep -iE "HERMES|RAILWAY|CONTAINER"
echo "$DISPLAY $WAYLAND_DISPLAY $SSH_CLIENT"

No display + a remote indicator = remote gateway. tools/mcpoauth.py::canopenbrowser() uses these same env vars, so if Hermes' own auto-detect says "headless", the built-in flow won't work.

2. Find HERMES_HOME and the config path

HERMES_HOME=$(python3 -c 'from hermes_constants import get_hermes_home; print(get_hermes_home())')
echo "config: $HERMES_HOME/config.yaml"
echo "tokens: $HERMES_HOME/mcp-tokens/"

3. Discover OAuth metadata from the MCP server

MCP servers advertise their OAuth setup via RFC 9728 (OAuth 2.0 Protected Resource Metadata). The WWW-Authenticate header on a 401 tells you where to look:

curl -sI https://mcp.example.com | grep -i www-authenticate
# → Bearer realm="mcp", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

Not every server returns WWW-Authenticate. Some return a bare {"errors":["Unauthorized"]} 401 with no auth-discovery hint. When that happens, probe well-known paths directly:

for p in \
  /.well-known/oauth-protected-resource \
  /.well-known/oauth-authorization-server \
  /.well-known/openid-configuration ; do
  echo "=== $p ==="
  curl -s -A "python-httpx/0.27" "https://mcp.example.com$p" | head -c 400; echo
done

Fetch the resource metadata to get authorizationservers, then fetch the AS's /.well-known/oauth-authorization-server to get authorizationendpoint, tokenendpoint, and registrationendpoint.

Pitfall: many servers sit behind Cloudflare and 403 bare urllib user agents. Always set User-Agent: python-httpx/0.27 (or similar) on requests in this flow.

4. Dynamic Client Registration (RFC 7591)

POST to the registration_endpoint with:

{
  "client_name": "Hermes Agent (manual OAuth)",
  "redirect_uris": ["http://127.0.0.1:8765/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "<scopes_from_resource_metadata>"
}

Omit scope entirely if the AS's scopessupported is empty — see step 5 pitfall. Use port 8765 (or any port — nothing will listen). tokenendpointauthmethod: none marks this as a public PKCE client. Save the returned client_id.

5. Build the authorize URL with PKCE

Generate:

  • codeverifier: secrets.tokenurlsafe(64)[:128]
  • codechallenge: base64url(sha256(codeverifier)) (no padding)
  • state: secrets.token_urlsafe(24)

Query params: responsetype=code, clientid, redirecturi, codechallenge, codechallengemethod=S256, state, plus resource= (RFC 8707 — many servers require this to bind the token to the specific MCP resource). Include scope= ONLY if the AS metadata's scopessupported is a non-empty array AND/OR the resource metadata declares specific scopes. If scopessupported: [], omit the scope parameter — the server grants its full default set on its own. Fabricating scope strings against an empty scopessupported can cause invalidscope errors on some ASes.

Stash codeverifier and state to disk** (e.g. ~/.hermes/cache/scratch/.mcp-oauth-work/.json, 0600 perms). You need them for step 7, possibly across multiple chat turns.

6. Give the user the authorize URL

Open this URL in your browser:
<authorize_url>

After approving, your browser will try to load http://127.0.0.1:8765/callback
and fail to connect — THAT'S EXPECTED. Just copy the entire URL from the
address bar (it will contain ?code=...&state=...) and paste it back here.

7. Exchange the code for tokens

When the user pastes the callback URL:

  1. Parse code and state from the query string.
  2. Verify state matches the stashed value (CSRF check — do not skip).
  3. POST application/x-www-form-urlencoded to the token_endpoint:
  • granttype=authorizationcode
  • code=
  • redirect_uri=
  • client_id=
  • code_verifier=
  • resource= (if the AS required it in step 5, include here too)
  1. Response contains accesstoken, refreshtoken, tokentype, expiresin, scope.

More skills from NousResearch/hermes-agent

  • A1passwordSet up op CLI, sign in, and read or inject secrets.
  • A3-statement-modelBuild integrated IS/BS/CF financial workbooks in Excel.
  • AaccelerateRun PyTorch training across GPUs with minimal changes.
  • Cactual-setupSet up Actual Computer (actual.inc) inference in Hermes.
  • Aadversarial-ux-testRoleplay a hostile user to find and triage UX pain points.
  • Aagent-merge-conflict-arbiterNeutral arbiter for merge conflicts between two agents.
  • AagentmailUse when an agent needs AgentMail CLI email inboxes.
  • Aai-presenter-videoMake a verified AI presenter video from script + image.
  • AairtableAirtable REST API via curl. Records CRUD, filters, upserts.
  • AanatomyExplain an idea as an interactive isometric machine figure.
  • Cantigravity-cliOperate the Antigravity CLI (agy): plugins, auth, sandbox.
  • Aapple-notesManage Apple Notes via memo CLI: create, search, edit.

All agent skills → · MCP servers