OAuth 2.1 reverse proxy for MCP servers
8.8K
OAuth 2.1 reverse proxy for MCP servers. Implements RFC 9728 Protected Resource Metadata and JWT validation, delegating authentication to an external authorization server.
Read the blog post: I couldn't find an OAuth 2.1 proxy for MCP servers, so I built one
mcp-gate sits in front of any HTTP MCP server and adds the resource-server side of the MCP Authorization specification (2026-07-28), which Claude.ai custom connectors require:
/.well-known/oauth-protected-resource — RFC 9728 metadata pointing clients to the authorization server (GET and HEAD). When RESOURCE_URI has a path, the document is also served at the RFC 9728 §3.1 path-inserted URL, e.g. /.well-known/oauth-protected-resource/mcp./healthz — Readiness check for container orchestration; returns 200 once signing keys are loaded./* — Validates the Bearer JWT against the provider's JWKS, then reverse-proxies to the upstream MCP server.Prometheus metrics are served on a separate listener (METRICS_ADDR, default :9090) at /metrics.
exp, iss, aud (string or array), and sub are required; 30-second clock-skew leeway.files satisfies a required files:read (never the reverse). The scope claim is accepted as a space-delimited string or a JSON array.WWW-Authenticate on 401/403, carrying realm, resource_metadata, and scope.Authorization and Cookie headers are stripped before proxying, and an access_token query parameter is removed and logged. The upstream authenticates with its own credentials.SSE_IDLE_TIMEOUT) for silent streams.ALLOWED_ORIGINS) for deployments exposed to DNS rebinding. It is off by default.MCP headers (MCP-Protocol-Version, Mcp-Method, Mcp-Name, Mcp-Param-*) are forwarded verbatim. mcp-gate records them in metrics and logs but never acts on them.
Claude.ai → Reverse Proxy → mcp-gate (JWT validation) → MCP Server → Backend
↕
Authorization Server (OAuth 2.1 / OIDC)
export LISTEN_ADDR=0.0.0.0:8080
export UPSTREAM_URL=http://mcp-server:8000
export RESOURCE_URI=https://mcp.example.com
export AUTHORIZATION_SERVER=https://auth.example.com/application/o/mcp/
export JWKS_URI=https://auth.example.com/application/o/mcp/jwks/
export EXPECTED_ISSUER=https://auth.example.com/application/o/mcp/
export EXPECTED_AUDIENCE=your-client-id
go run ./cmd/mcp-gate
docker run -d --name mcp-gate -p 8080:8080 -p 9090:9090 \
--env-file mcp-gate.env \
cpremus/mcp-gate:0.20
Images are published to Docker Hub and GHCR (ghcr.io/c-premus/mcp-gate) on each release, for linux/amd64. Tags are X.Y.Z, X.Y, and latest, with no v prefix (for example 0.20.2). Pin a version rather than tracking latest.
A Compose example is in docker-compose.example.yml.
See the Setup Guide for step-by-step instructions on:
All configuration is via environment variables. There are no config files. See the Setup Guide for the full list.
mcp-gate validates JWTs statelessly and is safe to run as multiple replicas behind a load balancer. The per-IP rate limiter defaults to in-memory state, which means the configured RPS holds per replica. Set REDIS_ADDR=host:port to back the limiter with Redis so the configured RPS is enforced globally across replicas. REDIS_USERNAME, REDIS_PASSWORD, and REDIS_DB are read separately so Vault can inject a password as a single secret. Redis errors fail open (the request passes through and a counter is incremented) so a Redis hiccup never blackholes user traffic.
Every request is logged as structured JSON (method, path, status, duration_ms, client_ip, user_agent). mcp_method, mcp_name, and mcp_name_encoded are added when the client sends the corresponding MCP headers.
Metrics use the mcpgate_ prefix. mcpgate_info{version="…"} reports the running build. Its version label keeps the release tag's v prefix (v0.20.2), which the image tag drops.
docs/grafana/ contains a provisionable dashboard and alert rules. Both expect a Prometheus job named mcp-gate and a service target label on each gate. See Monitoring in the Setup Guide.
MIT
Content type
Image
Digest
sha256:5afbee7c0…
Size
8.7 MB
Last updated
about 18 hours ago
docker pull cpremus/mcp-gate