OAuth 2.1 reverse proxy for MCP servers
10K+
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. typ must be an access-token type (at+jwt, application/at+jwt, or JWT unless STRICT_TOKEN_TYP=true); tokens with a crit header or a cnf (sender-constrained) claim are refused.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; a malformed Authorization header (duplicated, or Bearer with no token) gets 400 invalid_request.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:3f9e7a757…
Size
9.1 MB
Last updated
about 4 hours ago
docker pull cpremus/mcp-gate