mcp-oauth-gateway
Supports authenticating against Auth0 authorization servers, including passing Auth0-specific token parameters such as the API identifier (audience) that the access token must be addressed to.
Supports authenticating against Google's OAuth 2.1 authorization server, including Google-specific authorize parameters (access_type=offline, prompt=consent) needed for it to issue a refresh token.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-oauth-gatewaylog in to https://mcp.example.com/mcp and print my client config"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-oauth-gateway
OAuth 2.1 for MCP clients that only know how to send a static Authorization header.
Many MCP clients can't authenticate against an MCP server that implements the spec's
authorization flow. They send one fixed header, never discover the authorization server, and
don't react to a 401 challenge — so the server is simply unreachable:
Error POSTing to endpoint: {"error":{"code":-32001,"message":"Bearer token required"}}
Server status: needs-auth
SDK auth failed: Dynamic Client Registration rejected (HTTP 400):
{"error":"invalid_client_metadata","error_description":"dynamic client registration is disabled..."}Hand-pasting a long-lived JWT into the client's config "works", and then goes stale: the token expires, ends up in plaintext in several config files, and every client needs its own copy.
This gateway sits on loopback and holds the credential for the client:
static header OAuth 2.1, refreshed automatically
client ───────────────▶ 127.0.0.1:33419/mcp ─────────────────────▶ your MCP server
│
└── performs RFC 9728 discovery, PKCE, and silent refresh;
keeps tokens in the OS config dir (0600), not in your configThe client keeps talking to a loopback URL with a stable local token that is not the OAuth credential, so nothing in the client's config ever changes when the OAuth token rotates.
Zero dependencies. Node ≥ 20 only (it uses the built-in fetch, node:http and node:test).
What it does on the wire
POST <mcp-url>→ reads the401and itsWWW-Authenticatechallenge.Fetches the Protected Resource Metadata (RFC 9728) the challenge points at.
Fetches the authorization server's metadata (RFC 8414, falling back to
openid-configuration).Registers a client dynamically (RFC 7591) if the server allows it; otherwise it tells you exactly what to configure instead.
Opens the browser for an authorization code + PKCE (S256) flow, with
resource=<mcp-url>(RFC 8707) so the token's audience is bound to your server.Stores the access and refresh token, and renews silently before expiry.
Nothing is vendor-specific: any MCP server that publishes protected-resource metadata and any OAuth 2.1 authorization server with PKCE works.
Related MCP server: Uno MCP Stdio
Quick start
No clone needed — it is published on npm:
# 1. Authorize once (opens a browser; stores tokens outside any repo)
npx mcp-oauth-gateway login --url https://your-mcp-host/mcp
# 2. Run the gateway
npx mcp-oauth-gateway serve --url https://your-mcp-host/mcp --port 33419
# 3. Print ready-to-paste client configuration
npx mcp-oauth-gateway print-config --url https://your-mcp-host/mcpOr from a checkout (git clone https://github.com/zmhhaha/mcp-oauth-gateway), replacing
npx mcp-oauth-gateway with node bin/mcp-oauth-gateway.mjs.
If your authorization server does not offer dynamic client registration, add --client-id <id>
to login. The error message tells you the redirect URI to register.
Confidential clients and server-specific parameters
The default flow is a public client: PKCE, no secret. That is the right shape for a loopback
CLI and what most MCP servers accept. If your authorization server issues a client secret, pass it
by environment variable so it stays out of ps and your shell history:
export MCP_CLIENT_SECRET=… # from your secret store
npx mcp-oauth-gateway login --url https://your-mcp-host/mcp \
--client-id <id> --client-secret-env MCP_CLIENT_SECRETThe secret is stored beside the tokens (mode 600, outside any repo) and a plain serve picks it up
from there, so no flags have to be repeated. --token-auth-method client_secret_post switches away
from HTTP Basic for servers that want the secret in the body. A secret handed out by dynamic
registration is stored the same way.
Some servers require parameters the MCP authorization specification never mentions. Both flags repeat, and both are remembered for later refreshes:
# Google: without access_type=offline the server issues no refresh token at all
--authorize-param access_type=offline --authorize-param prompt=consent
# Auth0: the API identifier the access token must be addressed to
--token-param audience=https://api.exampleHeadless hosts: containers, CI, servers
The flow above opens a browser and receives the redirect on http://localhost:<port>/oauth/callback,
which cannot work on a host with no browser. Use the RFC 8628 device flow instead — no browser,
no listener, no port forwarding, nothing outside the process is needed to complete it:
npx mcp-oauth-gateway login --device --url https://your-mcp-host/mcp --client-id <id>It prints a short code and a URL. Open that URL on any device where you are already signed in to the authorization server — a phone is fine — and enter the code. The command polls until you approve, then stores exactly what the browser flow would have stored (refresh token included), so later restarts renew silently.
The authorization server must advertise device_authorization_endpoint and must have the device
flow enabled for your client. For Casdoor that means the application needs both the Device Code grant type and a Device login signin method; 4.11.0's UI can set only the first, so see
Casdoor notes.
Pointing a client at it
DSH (DeepSeek Harness)
DSH's MCP client has no OAuth at all, so this is the intended use case.
This repository is the DSH bundle — its package.json declares dsh.bundle.patch — so the
Plugins panel can install it straight from this repository's URL or from a local checkout.
(DSH has no browsable marketplace: it installs from a package name on npm, a Git repository URL,
a tarball, or a local path, and otherwise only lists DSH's own official plugins. This package is
published on npm, so the package-name route works too.)
See dsh/README.md for the two values print-config gives you, and the caveats.
Any client that accepts custom headers
{
"mcpServers": {
"your-server": {
"type": "http",
"url": "http://127.0.0.1:33419/mcp",
"headers": { "x-mcp-gateway-token": "<from print-config>" }
}
}
}Good old curl
curl -sS -X POST http://127.0.0.1:33419/mcp \
-H "x-mcp-gateway-token: <from print-config>" \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'Clients that do implement OAuth (Claude Code, Codex with its experimental Rust client) should keep using their own flow — this gateway is for the ones that can't.
Commands
Command | What it does |
| Interactive authorization; stores access + refresh token |
| Runs the loopback gateway until interrupted |
| Shows the stored credential — never the tokens themselves |
| Forces a refresh now (handy from a scheduled task) |
| Prints client configuration for DSH, JSON clients and curl |
| Deletes the stored credential |
Option | Meaning |
| Skip dynamic registration and use this client id |
| Loopback port (default |
| Host inside the redirect URI (default |
| Space/comma separated scopes (default |
| Issuer override for servers without an RFC 9728 challenge |
| State directory (default: the OS config dir) |
| Don't try to open a browser; print the URL instead |
Security and limitations
Loopback only. It binds
127.0.0.1and::1(both, becauselocalhostresolves to either — a mismatch here breaks the browser redirect). It refuses any request without its token.The local token is not the OAuth credential. It is stable and can live in client config files; the OAuth tokens stay in the gateway's state file (mode
0600under the OS config dir, never in a client's config).Same-user processes can read that state file. The gateway's token is readable by anything running as you — the same trust boundary as any local dev tool. Do not run it on a shared machine as a shared user.
One upstream per process, one login at a time, in-memory state. Run several gateways (one port each) for several MCP servers.
Keep it running. Tokens refresh only while the gateway runs. A refresh token has its own lifetime on the authorization server, so a gateway that stays down past it needs a fresh
login— starting it on demand works, but leaving it running is what makes the setup hands-off.No retry on a mid-flight
401. Expiry is handled proactively (including readingexpfrom the JWT when the server omitsexpires_in); a token that the server rejects anyway means re-login.chmod 0600is POSIX-only. On Windows the ACL of your own profile directory is the boundary.The gateway does not verify tokens or signatures — that is the MCP server's job. It only obtains and forwards them.
Troubleshooting
Symptom | Cause | Fix |
| The authorization server has DCR disabled (many do) | Register an application manually and pass |
| Same, and there is nothing to register against | Same |
| The token's audience is the | Accept both in the server's audience allow-list |
Login page loads, then "redirect URI mismatch" | The redirect URI is not registered | Register |
The authorization server says something like "Failed to sign in" | The URL was opened in a context that has no session there — an embedded preview/popup window, or a different browser. (Some clients open links in a webview, which also cannot finish a provider sign-in.) | Open it in the browser where you are signed in. |
Browser says success, client still unauthenticated | The client got a token but it is not bound to this server (RFC 8707 | Same as the |
Works, then stops after some days | The refresh token was revoked, or the server rotated it before Casdoor supported that | Re-run |
| Would be a bug in this gateway | Please report it |
Development
node --test # 32 tests, no network requiredThe suite covers the RFC 7636 PKCE vector, challenge and metadata parsing, the manual-client error paths, token-store semantics, the loopback proxy (including a test that proves SSE frames are streamed and not buffered), and the transport selection per upstream scheme.
Releasing
Two traps that cost real time on the first publish, both silent:
Never write a
binpath with a leading./."mcp-oauth-gateway": "./bin/x.mjs"makes npm consider the entry invalid and drop the wholebinfield from the published package —npm publishonly warns, andnpxthen fails with no obvious cause. Use"bin/x.mjs".npm pkg fixcorrects it.npm 11 has staged publishing, and a bypass-2FA token is no longer the recommended route.
npm publishmay leave the version unpublished while the registry reserves the name with a0.0.0-stageplaceholder; the metadata for a brand-new package also takes a minute to appear, so an immediatenpm viewornpx <pkg>@<version>can reportETARGETeven though the publish succeeded. Check the version endpoint (registry.npmjs.org/<pkg>/<version>) before concluding anything, and prefernpm stage publish+npm stage approveover a bypass token (npm's own guidance).npm stage listreadsGET /-/stage.
Notes on specific authorization servers
Casdoor — DCR behaviour,
aud = resource, redirect matching, and the configuration cache that quietly reverts database edits.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
Managed MCP gateway with OAuth, per-agent permissions, shared accounts and activity logs.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables Claude.ai to connect to a Hermes MCP server via OAuth 2.1 authorization code flow with PKCE, acting as a reverse proxy and single-user authorization gateway.-
- AlicenseAqualityCmaintenanceLocal stdio proxy for Uno MCP Gateway that enables MCP clients without OAuth support to securely connect to authenticated remote servers.832 PyPI1MIT
- AlicenseNot gradedqualityDmaintenanceActs as a secure OAuth 2.0/2.1 proxy gateway for MCP servers, enabling integration with Claude and ChatGPT platforms.8 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables deploying a remote MCP server with OAuth 2.1 resource-server authorization, protecting tools behind validated bearer tokens and RFC 9728 discovery.MIT