mcp-oauth-remote-gateway

Manual OAuth for remote MCP servers on headless gateways.

  • MCP
  • OAuth
  • PKCE
  • Remote-Deployment

Declared platforms: linux · macos

Install
npx skills add 'https://github.com/NousResearch/hermes-agent/tree/main/optional-skills/mcp/mcp-oauth-remote-gateway'
Download bundle ↓
main · 24fd22bScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

references/stripe-mcp-oauth-revocation.md

references/stripe-mcp-oauth-revocation.mdBrowse 3 files
View on GitHub
← Back to SKILL.md

Stripe MCP (mcp.stripe.com) — recurring OAuth session revocation, fix with a restricted key

A worked example of pitfall 9/10 in SKILL.md: a provider whose OAuth session dies on a recurring basis, where the durable fix is to drop OAuth for a static API key.

Symptom (the "dies after a while" complaint)

Stripe MCP works for a few days then goes "not connected." Auto-refresh is healthy in between (the access token is 1h-lived and rotates fine), so it LOOKS like a refresh-token expiry or a max-session cap — it is neither. Roughly weekly, Stripe revokes the entire OAuth grant server-side. The next grant_type=refresh_token POST returns:

HTTP 400  {"error":"invalid_grant","error_description":"Invalid refresh token"}

The whole grant is dead — not just the short-lived access token — so auto-refresh cannot recover it. It requires a fresh interactive browser consent flow, which a headless remote gateway cannot drive. Don't be fooled by a green smoke-test at any given moment: the failure is intermittent revocation, not a permanently broken token.

Why the three usual hypotheses are all wrong

Per Stripe's OAuth docs (https://docs.stripe.com/stripe-apps/api-authentication/oauth):

  • Access tokens expire in 1 hour.
  • Refresh tokens expire after 1 year, and are rolled on every exchange — so as long as you refresh at least once a year they never naturally expire.
  • Hermes auto-refreshes independently of whether you call Stripe tools, so "not using the tools enough" is irrelevant.

So a recurring death cannot be refresh-token expiry (1yr) or "max OAuth session length" (no clean documented cap). It is server-side session revocation. Do NOT loop on refresh.

The durable fix: drop OAuth, use a restricted API key as a Bearer token

Stripe's MCP docs (https://docs.stripe.com/mcp) are explicit that for non-interactive / agent use, OAuth is the wrong tool — mcp.stripe.com accepts a static restricted key (rk_live_...) as a Bearer token. A restricted key has no session, no refresh, no expiry — it works until revoked, ending the re-auth cycle entirely.

config.yaml change (no token files needed — delete the OAuth dance for this server):

mcp_servers:
  stripe:
    url: https://mcp.stripe.com
    headers:
      Authorization: *** rk_live_..."      # restricted key from Dashboard
      # Stripe-Account: "acct_xxx"            # only for Connect platform / connected-account calls

Generate the key in Stripe Dashboard → Developers → API keys → Restricted keys. Grant least-privilege scopes for what the bot actually does:

  • account reads: read on Charges, Customers, Subscriptions, Coupons/Promotion codes
  • refunds / writes: add the corresponding write scopes Then /reload-mcp (full restart only if the breaker is tripped, per pitfall 7).

Decision rule

For ANY unattended remote-gateway MCP server that keeps getting its session revoked (invalid_grant on refresh, or -32002 "Session expired" after a successful refresh), and whose provider offers a static API key — prefer the static key over OAuth. OAuth's refresh dance is for interactive clients; it is a liability for a headless gateway. Stripe (restricted key) and Linear (Personal API key) both fit this rule.

Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 341.
10. **A successful refresh that STILL yields a rejected token = server-side SESSION revocation; only a fresh authorization_code flow fixes it.** Distinct from pitfall 9. The stored token file can look healthy (`expires_at` well out, refresh_token present), yet a live `initialize` POST returns `401 invalid_token` with a JSON-RPC body like `{"error":{"code":-32002,"message":"Session expired. Please re-authenticate."}}`. The `grant_type=refresh_token` POST may **succeed** (HTTP 200, new access_token) — yet the brand-new token gets the SAME `-32002`. The provider revoked the underlying MCP *session* server-side; the OAuth refresh chain re-mints credentials but cannot re-establish a revoked session. Decision rule when an OAuth MCP reports "not connected": (1) smoke-test the stored access_token with a manual `initialize` POST; (2) if `401 invalid_token`, attempt a refresh and smoke-test the NEW token; (3a) new token works → write it + restart to clear the breaker; (3b) new token STILL gets `-32002`/"Session expired" → stop, this is session revocation, hand the user the authorize URL for a full re-auth. `scripts/diagnose-oauth-mcp.py` automates steps 1–2 and prints which branch you're in. For an unattended gateway whose session keeps getting revoked, prefer a static Personal API key. See `references/stripe-mcp-oauth-revocation.md` for a worked example of a provider that revokes weekly.
SKILL.mdView in source ↗
Source excerpt starting at line 365.
- `scripts/diagnose-oauth-mcp.py` — re-runnable, read-only-by-default diagnostic. Given a server name, it smoke-tests the stored access_token, attempts a refresh, smoke-tests the new token, and prints exactly which recovery branch you're in (`TOKEN_OK` = breaker/restart, `REFRESH_FIXED` = persist+restart, `SESSION_REVOKED` = full re-auth, `REFRESH_DEAD` = full re-auth/API key). Pass `--write` to persist a working refreshed token atomically. Never prints secret values. **Run this FIRST when an OAuth MCP server reports "not connected"** — it encodes the pitfall 7/9/10 decision tree.- `references/stripe-mcp-oauth-revocation.md` — a worked example (Stripe) of a provider that revokes its OAuth session on a recurring basis, and the durable fix: switch to a static restricted API key.