MCP Servers

A collection of Model Context Protocol servers, templates, tools and more.

OAuth 2.1 / OIDC bouncer for MCP servers via Traefik plugin + sidecar

Created 5/12/2026
Updated about 5 hours ago
Repository documentation and setup instructions

MCPBouncer

Drop OAuth 2.1 onto any MCP server with a Traefik label.

What it does

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-Sub and X-Mcp-Scopes headers.
  • 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).

How it works

┌─ External Client ─┐
│   (MCP client)    │
└────────┬──────────┘
         │ HTTP request
         ↓
    ┌─────────────────────────────────┐
    │  Traefik (port 443/80)          │
    │  ┌─────────────────────────────┐│
    │  │ MCPBouncer Plugin (Yaegi)    ││
    │  │ Intercepts OAuth paths      ││
    │  │ Validates JWT in-process    ││
    │  └────────┬──────────┬──────────┘│
    └───────────┼──────────┼───────────┘
                │          │
        OAuth paths   Regular MCP paths
                │          │
                ↓          ↓
        ┌──────────────────────────┐
        │   Sidecar (internal)     │
        │ Port: 8080 (Docker net)  │
        │ Handles all OAuth flows  │
        │ Manages local JWT issuer │
        │ Stores refresh tokens    │
        └────────┬─────────────────┘
                 │
                 ↓ OIDC discovery
            ┌─────────────────┐
            │  Upstream IdP   │
            │ (Google/Zitadel)│
            └─────────────────┘

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-Sub and X-Mcp-Scopes.
  • Returns 401 with WWW-Authenticate header 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.

Quick start

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 --build

Test discovery:

curl -i https://mcp.localhost/wiki/anything

Expected 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.

Labels reference

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) |

See docs/labels.md for extended examples and notes.

Sidecar environment variables

| 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 32

Security notes

PKCE 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.

Status

Early stage. Targets the MCP Authorization spec rev. 2025-06-18.

License

MIT

Quick Setup
Installation guide for this server

Installation Command (package not published)

git clone https://github.com/Sipioteo/MCPBouncer
Manual Installation: Please check the README for detailed setup instructions and any additional dependencies required.

Cursor configuration (mcp.json)

{ "mcpServers": { "sipioteo-mcpbouncer": { "command": "git", "args": [ "clone", "https://github.com/Sipioteo/MCPBouncer" ] } } }