Drop OAuth 2.1 onto any MCP server with a Traefik label.
MCPBouncer adds full OAuth 2.1 and OIDC support (DCR, PKCE, JWKS, key rotation) to MCP servers that lack native authentication—without modifying the server image.
- Transparent to the MCP image. Configuration via Traefik labels only. The server behind the proxy sees authenticated requests with
X-Mcp-SubandX-Mcp-Scopesheaders. - Multi-tenant per image. A single sidecar instance serves multiple MCP servers with different OAuth providers (e.g.,
/wiki→ Google,/world→ Zitadel). - Tiny footprint. Sidecar is ~10 MB, stdlib-only Yaegi plugin with no external dependencies beyond the Go standard library.
- Standards-conformant. Implements MCP Authorization spec (rev. 2025-06-18), RFC 8414 (Authorization Server metadata), RFC 7591 (Dynamic Client Registration), RFC 8707 (Resource Indicators).
Source for the diagram:
docs/assets/architecture.mmd(Mermaid). Rendered to SVG so it works on Docker Hub and other Markdown viewers that don't support Mermaid natively.
Plugin (in Traefik):
- Intercepts requests under each MCP's PathPrefix.
- Routes OAuth endpoints (
.well-known/*,/oauth/*) to the sidecar. - Validates JWT locally with cached JWKS from the sidecar.
- Forwards authenticated requests to the MCP server with
X-Mcp-SubandX-Mcp-Scopes. - Returns 401 with
WWW-Authenticateheader on missing/invalid token.
Sidecar (internal Docker network):
- Never exposed externally. Binds only to internal Docker network.
- Handles OAuth 2.1 flows: discovery, DCR, authorization, token exchange.
- Acts as a local Authorization Server, issuing JWT signed with its own Ed25519 keypair.
- Federates to upstream IdP (Google, Zitadel, etc.) for actual user authentication.
- Encrypts upstream refresh tokens at rest using AES-GCM.
- Rotates signing keys automatically with configurable overlap.
Clone and prepare:
git clone https://github.com/Sipioteo/MCPBouncer
cd MCPBouncer/deploy
cp docker-compose.example.yml docker-compose.yml
# Edit docker-compose.yml and traefik.example.yml with your IdP credentials
# (GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, etc.)Launch the stack:
docker compose up --buildTest discovery:
curl -i https://mcp.localhost/wiki/anythingExpected response:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.localhost/wiki/.well-known/oauth-protected-resource"
Content-Type: application/json
{"error":"unauthorized"}
The client can now fetch .well-known/oauth-protected-resource to discover the OAuth server and begin DCR.
For local development with source plugin, use traefik.local.yml and bind-mount the plugin directory.
Attach these labels to your MCP container in docker-compose.yml:
| Label | Type | Required | Description |
|---|---|---|---|
traefik.enable |
bool | Yes | Must be true |
traefik.http.routers.<name>.rule |
string | Yes | Path rule, e.g. Host(...) && PathPrefix(/wiki) |
traefik.http.routers.<name>.middlewares |
string | Yes | Reference to middleware, e.g. mcpb-wiki@docker |
traefik.http.middlewares.<name>.plugin.mcpbouncer.providerIssuer |
string | Yes | OIDC issuer URL (e.g., https://accounts.google.com) |
traefik.http.middlewares.<name>.plugin.mcpbouncer.clientID |
string | Yes | OAuth client ID from upstream IdP |
traefik.http.middlewares.<name>.plugin.mcpbouncer.clientSecret |
string | Yes | OAuth client secret from upstream IdP |
traefik.http.middlewares.<name>.plugin.mcpbouncer.resource |
string | Yes | Resource name (e.g., wiki). Used as JWT aud claim. |
traefik.http.middlewares.<name>.plugin.mcpbouncer.scopes |
string | No | Space-separated OAuth scopes (default: openid) |
traefik.http.middlewares.<name>.plugin.mcpbouncer.sidecarURL |
string | Yes | Internal sidecar URL (e.g., http://bouncer:8080) |
traefik.http.middlewares.<name>.plugin.mcpbouncer.audience |
string | No | JWT aud claim (default: same as resource) |
traefik.http.middlewares.<name>.plugin.mcpbouncer.jwksCacheTTLSeconds |
int | No | JWKS cache TTL in seconds (default: 300) |
traefik.http.middlewares.<name>.plugin.mcpbouncer.requiredScopes |
string | No | Space-separated scopes required for access (checked before forwarding to MCP) |
traefik.http.middlewares.<name>.plugin.mcpbouncer.pathPrefix |
string | No | Stable base path under the host used for publicBase (the JWT iss and OAuth metadata URLs). Set "" for one MCP per host, /wiki for a subpath; default * derives from the request URL (legacy) |
See docs/labels.md for extended examples and notes.
| Variable | Default | Description |
|---|---|---|
BOUNCER_DB_PATH |
/data/bouncer.db |
SQLite database path (must be writable) |
BOUNCER_LISTEN_ADDR |
:8080 |
Bind address (typically :8080 for internal Docker network) |
BOUNCER_ENCRYPTION_KEY |
(required) | 32-byte base64-encoded key for AES-GCM encryption of sensitive fields |
BOUNCER_KEY_ROTATION_DAYS |
30 |
Days between signing key rotations |
BOUNCER_KEY_OVERLAP_HOURS |
24 |
Hours that old and new keys coexist during rotation |
BOUNCER_ACCESS_TOKEN_TTL |
1 (hour) |
Access token TTL in hours |
BOUNCER_REFRESH_TOKEN_TTL |
30 (days) |
Refresh token TTL in days |
BOUNCER_LOG_LEVEL |
info |
Log level (debug or info) |
Generate a random 32-byte base64 key:
openssl rand -base64 32PKCE is mandatory. All OAuth flows require PKCE with S256 challenge method. code_challenge cannot be omitted.
Refresh tokens are encrypted at rest with the key specified in BOUNCER_ENCRYPTION_KEY using AES-GCM. The upstream refresh token is never exposed to clients.
Sidecar is never exposed externally. It binds only to an internal Docker network (bouncer_internal in examples). There is no Traefik routing to the sidecar. Verify in your deployment that the sidecar port (:8080) is not accessible from outside the Docker network.
JWT algorithm validation. Only Ed25519 (EdDSA) and RS256 are accepted. alg=none is rejected outright.
Audience claim is enforced. Every JWT includes an aud claim matching the resource name. A token issued for /wiki will not validate for /world.
Issuer is exact-match. The iss claim in every JWT must exactly match the public base URL (derived from request Host and PathPrefix). No wildcard or domain-level acceptance.
Early stage. Targets the MCP Authorization spec rev. 2025-06-18.
MIT
