mcp-oauth2-proxy
v0.1.12
Published
Local stdio MCP proxy to OAuth2-protected MCP servers (client_credentials, authorization_code+PKCE).
Downloads
1,561
Maintainers
Readme
mcp-oauth2-proxy
A local stdio MCP server that proxies to a remote, OAuth2-protected HTTP MCP server. Drop it into Claude Desktop, Cursor, VS Code Copilot, or any other MCP client — log in once in your browser — done.
MCP client ─stdio (JSON-RPC)─▶ mcp-oauth2-proxy ─HTTP+SSE + Bearer─▶ upstream MCP server
│
└─ OAuth2 token endpoint (IdP)Documentation
This README is a quick start. Full documentation lives in the wiki.
| Page | What's there | | ---- | ------------ | | Getting Started | Install, first run, wiring into a client. | | Configuration | Every field and environment variable. | | OAuth2 Grants and Tokens | Grants, interactive login, token + cache lifecycle. | | Discovery | RFC 9728 / RFC 8414 endpoint discovery. | | Security | Threat model and built-in defenses. | | Remote Hosts (SSH Port Forwarding) | Logging in when the proxy runs remotely. | | Troubleshooting | Common problems and a FAQ. | | Architecture · Bridge · OAuth2 internals | How it works inside. | | Contributing and Releases | Dev setup, tests, release process. |
Contents
- Features
- How it works
- Requirements
- Install
- Quick start
- Wire it into an MCP client
- Configuration
- Security
- Development
- Out of scope
- License
Features
- Interactive
authorization_code+ PKCE flow with a built-in local browser callback listener — no manual code copy/paste. - Refresh-token cache on disk (AES‑256‑GCM,
0600), so the browser only opens once per machine. client_credentialsgrant for headless / service-to-service use.- RFC 9728 + RFC 8414 discovery of token and authorization endpoints from the upstream — usually zero OAuth config required.
- Proactive token refresh with skew, in-flight de-duplication, and a 401 → invalidate → retry loop.
- Streamable-HTTP upstream support: single-shot JSON, SSE
text/event-streamresponses, and the optional long-lived server-notification channel. HonorsMcp-Session-Id. - Stderr-only logging (pino) with redaction of tokens, secrets, and
Authorizationheaders — stdout stays a clean JSON-RPC channel.
How it works
- Discovery. Optionally fetch RFC 9728/8414 metadata to fill in
tokenUrl,authorizationUrl, andscope. - Token manager. Wrap the configured
Grantwith caching, refresh-skew, in-flight dedup, and 401 invalidation. - Prefetch. Call
getToken()once at startup so the interactive browser flow (if needed) happens before the first MCP message arrives. - Bridge. For each stdin JSON-RPC line, POST to
upstream.urlwith aBearertoken; single-shot JSON responses become one stdout line, SSE responses one line per event. A401triggersinvalidate()and a single retry. - Server stream. After
initialize, optionally hold open aGET text/event-streamchannel for server-initiated notifications.
For the full internals, see the Architecture, Bridge Internals, and OAuth2 Internals wiki pages.
Requirements
- Node.js 20+
- An OAuth2-protected MCP server speaking the Streamable HTTP MCP transport.
- An OAuth2 client registered with your IdP. For the interactive flow,
register
http://127.0.0.1:53682/callbackas a redirect URI (or whatever you setOAUTH2_CALLBACK_PORTto).
Install
You don't need to install anything — MCP clients can launch the proxy
directly via npx:
npx -y mcp-oauth2-proxyFor development against a local checkout:
git clone https://github.com/ChengleiYuan/mcp-oauth2-proxy.git
cd mcp-oauth2-proxy
npm install
npm run buildQuick start
Interactive login (recommended for end users)
UPSTREAM_URL=https://mcp.example.com/mcp \
OAUTH2_GRANT=authorization_code \
OAUTH2_CLIENT_ID=<your-client-id> \
npx -y mcp-oauth2-proxyOn first launch the proxy discovers the OAuth endpoints, opens your browser
for a PKCE login, captures the code on http://127.0.0.1:53682/callback,
caches the refresh token (encrypted) under your OS config dir, and starts the
bridge. Subsequent launches reuse the cached token silently. Details:
OAuth2 Grants and Tokens.
Headless / service account
UPSTREAM_URL=https://mcp.example.com/mcp \
OAUTH2_GRANT=client_credentials \
OAUTH2_TOKEN_URL=https://idp.example.com/oauth2/token \
OAUTH2_CLIENT_ID=my-service \
OAUTH2_CLIENT_SECRET='…' \
OAUTH2_SCOPE='mcp:read mcp:write' \
npx -y mcp-oauth2-proxyWire it into an MCP client
{
"mcpServers": {
"remote-oauth2-mcp": {
"command": "npx",
"args": ["-y", "mcp-oauth2-proxy"],
"env": {
"UPSTREAM_URL": "https://mcp.example.com/mcp",
"OAUTH2_GRANT": "authorization_code",
"OAUTH2_CLIENT_ID": "<your-client-id>"
}
}
}
}On Windows hosts that don't resolve .cmd shims (so command: "npx"
fails to start), use the explicit form:
{
"command": "cmd",
"args": ["/c", "npx", "-y", "mcp-oauth2-proxy"]
}Paths in the env block must be absolute and use forward slashes on
every platform.
Configuration
The proxy can be configured by a JSON file (MCP_PROXY_CONFIG), by
environment variables, or a mix — env vars override file values, then the
merged result is validated. Minimal example file:
{
"upstream": { "url": "https://mcp.example.com/mcp" },
"oauth2": {
"grant": "authorization_code",
"clientId": "my-client",
"scope": "mcp:read mcp:write"
}
}See config.example.json for a fuller sample.
Most-used environment variables:
| Env var | Maps to |
| --------------------- | ------------------- |
| UPSTREAM_URL | upstream.url |
| OAUTH2_GRANT | oauth2.grant |
| OAUTH2_CLIENT_ID | oauth2.clientId |
| OAUTH2_CLIENT_SECRET| oauth2.clientSecret |
| OAUTH2_TOKEN_URL | oauth2.tokenUrl |
| OAUTH2_SCOPE | oauth2.scope |
| LOG_LEVEL | log.level |
Full reference: every field, default, and environment variable is documented in the Configuration wiki page. Endpoint auto-discovery is covered in Discovery. Running the proxy on a remote machine? See Remote Hosts (SSH Port Forwarding). Hitting a snag? See Troubleshooting.
Security
- Tokens are acquired by the proxy itself; the upstream
Authorizationheader is always set by the proxy, never passed through from the client. - Access tokens live in memory only. Refresh tokens are cached encrypted
(AES‑256‑GCM, key file mode
0600) — honest obfuscation against casual disk reads, not protection against a process running as the same OS user. - Cleartext
http://to non-loopback hosts is rejected at startup (override withALLOW_INSECURE_HTTP=true);https://and loopbackhttp://are always allowed. The interactive callback listener validates theHostheader to defeat DNS-rebinding. - All logs go to stderr with tokens, secrets, and
Authorizationheaders redacted; stdout is reserved for JSON-RPC.
Full threat model and defenses: Security.
Development
npm install
npm run build # compile TS to dist/
npm run dev # tsx watch
npm test # vitest (unit + integration)The integration test spins up a mock OAuth2 token endpoint and a mock MCP
upstream in-process and drives the real bridge through PassThrough
streams — no network required. Project layout, the full test strategy, and
the automated release process are documented in
Contributing and Releases.
Out of scope
- Multiple upstream MCP servers per process
- OS-keychain-backed refresh-token storage (DPAPI / Keychain / libsecret)
- mTLS / JWT-bearer / device-code / ROPC grants
- HTTP / SSE inbound transport (this is a stdio MCP server)
