mcp-auth-adapter
v1.0.0
Published
OAuth/OIDC authentication adapter for MCP clients
Maintainers
Readme
MCP Auth Adapter
An OAuth/OIDC authentication adapter for Model Context Protocol (MCP) clients. It sits in front of any OAuth 2.0 / OIDC upstream IdP that serves standard discovery metadata -- such as Keycloak, Auth0, Okta, Azure AD (Entra ID), Google Identity, or any provider serving standard OAuth 2.0 / OIDC discovery metadata -- and provides functionality required by the MCP Authorization specification for the most common MCP clients (Claude Code/Desktop, Cursor IDE, ChatGPT, Gemini CLI, VS Code, ...) and their known problematic behaviours.
MCP servers announce this adapter as their authorization server. MCP clients discover it via .well-known and interact with its endpoints. Authentication itself, token issuing, and token exchanges are all performed by the upstream IdP -- this adapter is only a very thin, transparent, stateless facade.
Features
- Well-known discovery (
/.well-known/openid-configuration,/.well-known/oauth-authorization-server) -- filtered, MCP-focused view of the upstream IdP metadata with injected adapter endpoints and tailored configurations. - Open Dynamic Client Registration (
POST /register, optional) -- returns a pre-configured fixedclient_idfor all registering MCP clients per RFC 7591. - Authorization proxy (
GET /authorize, optional) -- intercepts authorization requests, applies configurable scope filtering and/or CIMDclient_idsubstitution, and redirects to the upstream IdP. - CIMD adapter (
GET /authorize+POST /token, EXPERIMENTAL, optional) -- accepts Client ID Metadata Document styleclient_idURLs, validates metadata documents, and maps them to pre-configured fixed upstream IdP client_ids. See CIMD Adapter.
Container Image
Pre-built container images are published to GitHub Container Registry on every release. This is the recommended way to deploy in production -- no Node.js installation required.
Prerequisites: Docker or Podman
Pull and run
Podman is used in examples, but you can use docker command instead:
podman run -d --name mcp-auth-adapter \
-p 3000:3000 \
-e MCP_BASE_URL=https://mcp-auth.example.com \
-e MCP_UPSTREAM_SSO_URL=https://sso.example.com/auth/realms/external \
-e MCP_PROXY_DCR_CLIENT_ID=mcp-client \
ghcr.io/velias/mcp-auth-adapter:latestOr use an env file for all configuration (see Configuration below):
podman run -d -p 3000:3000 --env-file .env ghcr.io/velias/mcp-auth-adapter:latestAvailable tags
Each release vX.Y.Z produces the following image tags:
X.Y.Z-- exact version (recommended for production)X.Y-- latest patch within a minor versionX-- latest minor within a major versionlatest-- most recent release
To build the image locally from source, see CONTRIBUTING.md.
Build from Source
Prerequisites: Node.js >= 18.x (uses native fetch), npm
npm install
npm run build
# Create .env from the template and edit it
cp .env.example .env
npm startConfiguration
Environment variables are used. All variables are prefixed with MCP_. A .env file in the project root is loaded automatically, explicit environment variables take precedence.
| Variable | Required | Default | Description |
|---|---|---|---|
| | | | Core |
| MCP_BASE_URL | Yes | -- | Public base URL of this adapter. Used as issuer (RFC 8414 §3.3) and to construct endpoint URLs. Must be http or https; trailing slashes are stripped automatically. Must exactly match what MCP servers advertise in their Protected Resource Metadata authorization_servers array. |
| MCP_UPSTREAM_SSO_URL | Yes | -- | Base URL (issuer) of the upstream IdP. Must be http or https; trailing slashes are stripped automatically. Works with any OAuth 2.0 / OIDC provider. Discovery is attempted via /.well-known/openid-configuration, then /.well-known/oauth-authorization-server (RFC 8414); on failure, fallback endpoints are derived using Keycloak URL conventions (see below). |
| MCP_PORT | No | 3000 | Port this app listens on. |
| MCP_SHUTDOWN_TIMEOUT_SECONDS | No | 30 | Maximum seconds to wait for in-flight requests to drain after SIGTERM/SIGINT before force-exiting. |
| | | | Dynamic Client Registration |
| MCP_PROXY_DCR_CLIENT_ID | No | -- | Fixed client_id returned by POST /register. Setting this enables the DCR proxy. Must be pre-registered at the upstream IdP as a public client. If omitted, the upstream IdP's registration endpoint is announced directly. |
| | | | Scope filtering (auto-enables /authorize proxy) |
| MCP_PROXY_AUTH_SCOPES_REMOVED | No | -- | Comma-separated scopes to strip from /authorize requests (e.g. offline_access). Ignored if MCP_PROXY_AUTH_SCOPES_PRESERVED is also set. |
| MCP_PROXY_AUTH_SCOPES_PRESERVED | No | -- | Comma-separated scopes to keep in /authorize requests; all others are stripped. Takes precedence over MCP_PROXY_AUTH_SCOPES_REMOVED. |
| | | | Well-known discovery |
| MCP_WELL_KNOWN_SCOPES_SUPPORTED | No | -- | Comma-separated scopes to announce in scopes_supported. If empty, the field is omitted. Note: some MCP clients request all announced scopes -- this controls announced scopes, not forwarded scopes. |
| MCP_WELL_KNOWN_REFRESH_MINUTES | No | 60 | How often (in minutes) to re-fetch the upstream well-known document. |
| | | | CIMD adapter (EXPERIMENTAL, auto-enables /authorize proxy + /token proxy) |
| MCP_PROXY_CIMD_MAP | No | -- | JSON object mapping CIMD URLs to upstream IdP client_ids. Format: {"<cimd_url>":"<upstream_client_id>", ...}. N:1 mapping supported. CIMD auto-enables when this is non-empty or MCP_PROXY_CIMD_DEFAULT_CLIENT_ID is set. |
| MCP_PROXY_CIMD_DEFAULT_CLIENT_ID | No | -- | Fallback upstream client_id for CIMD URLs not in the map. If unset, unknown CIMD URLs are rejected with 403 (strict allowlist). |
| MCP_PROXY_CIMD_CACHE_MINUTES | No | 30 | Cache TTL (in minutes) for validated CIMD metadata documents. |
| | | | Observability |
| MCP_METRICS_ENABLED | No | true | Enable Prometheus metrics endpoint (GET /metrics) and request instrumentation. Set to false to disable (zero overhead). |
| MCP_DEBUG | No | false | Emit structured debug logs for every request. |
Known MCP Client Behaviors
MCP clients interact with OAuth/OIDC in ways that can cause issues with upstream IdPs not specifically designed for MCP. This adapter addresses the most common known problems.
Clients request all announced scopes
Many MCP clients (notably Claude Code, Claude Desktop, Cursor IDE) read scopes_supported from the well-known document and include all of them in the /authorize request. When an upstream IdP announces dozens of scopes (e.g. Keycloak exposes internal scopes like profile, email, roles, web-origins, etc.), the authorization request balloons with scopes the MCP server doesn't need — confusing users on the consent screen or causing outright rejection by the upstream IdP if some scopes require pre-approval.
Mitigation 1 — control what's announced:
# Only announce scopes your MCP servers actually need
MCP_WELL_KNOWN_SCOPES_SUPPORTED=openid,api.read,api.writeThis replaces the upstream scopes_supported in discovery, so greedy clients only see (and request) what you intend.
Mitigation 2 — filter scopes at the authorize proxy:
Even if you cannot control what's announced (e.g. you need scopes_supported to reflect the full upstream list for other consumers), the authorize proxy can strip unwanted scopes before forwarding to the upstream IdP:
# Remove specific problematic scopes from authorize requests
MCP_PROXY_AUTH_SCOPES_REMOVED=roles,web-origins,microprofile-jwt
# Or use an allowlist — only these scopes reach the upstream IdP
MCP_PROXY_AUTH_SCOPES_PRESERVED=openid,api.read,api.writeThis catches scopes regardless of whether the client added them from the discovery document or hardcoded them.
Clients always request offline_access scope
Some MCP clients (e.g. Claude Code, Cursor IDE) unconditionally add offline_access to every authorization request to be sure they obtain refresh tokens, as some IdPs provide it only under this scope. This may be problematic when this scope has different consequence in your IdP:
- The upstream IdP requires explicit admin consent or client-level configuration to issue offline tokens
- The IdP rejects the entire authorization request when
offline_accessis not an allowed scope for the client - Organization policy restricts long-lived refresh tokens for security reasons
Mitigation — strip the scope at the proxy:
# Remove offline_access before forwarding to the upstream IdP
MCP_PROXY_AUTH_SCOPES_REMOVED=offline_accessOr use the allowlist approach to be more restrictive:
# Only forward these specific scopes, drop everything else
MCP_PROXY_AUTH_SCOPES_PRESERVED=openid,api.read,api.writeCombining both controls
For a typical deployment where greedy clients and offline_access are both issues:
# Announce only relevant scopes (controls what clients ask for)
MCP_WELL_KNOWN_SCOPES_SUPPORTED=openid,api.read,api.write
# Strip offline_access even if a client adds it explicitly
MCP_PROXY_AUTH_SCOPES_REMOVED=offline_accessMCP_WELL_KNOWN_SCOPES_SUPPORTED controls the demand side (what clients see and request), while MCP_PROXY_AUTH_SCOPES_REMOVED / MCP_PROXY_AUTH_SCOPES_PRESERVED controls the supply side (what actually reaches the upstream IdP). Using both provides defense in depth.
Open DCR and its Security Limitations
MCP Clients need a way to get client_id necessary to login through the upstream IdP.
You can use Open DCR functionality of this adapter if your IdP does not provide it, or if you do not want to use it.
The Open DCR endpoint returns a fixed public client_id (token_endpoint_auth_method: none) to be used by MCP Clients.
But as many MCP Clients are local apps, any local application can obtain this client_id and start an OAuth flow.
IdP do not know who is asking for the client_id. Two emerging standards address this:
- DCR with Software Statement Assertion (SSA) -- cryptographically proves client identity via signed JWTs (RFC 7591 §2.3). No major MCP client currently includes Software Statements in DCR requests.
- Client ID Metadata Documents (CIMD) -- the
client_idis an HTTPS URL pointing to a metadata document. Default mechanism in the MCP Auth Spec (2025-11-25), not yet universally adopted. This adapter includes experimental CIMD support -- see CIMD Adapter.
Until "DCR with SSA" or CIMD is widely supported, user consent during login at the upstream IdP is the last line of defense. This is an accepted limitation of the MCP auth ecosystem.
CIMD Adapter (EXPERIMENTAL)
Status: Based on
draft-ietf-oauth-client-id-metadata-document-01(March 2026), an IETF Internet-Draft not yet at RFC status. This implementation may change as the spec evolves.
When configured, the adapter bridges MCP clients using CIMD-style client_id (HTTPS URLs) to upstream IdPs that don't support CIMD natively:
- Validates CIMD URL syntax per the spec (Section 3)
- Checks if the client is allowed in the configuration (map lookup + optional default) -- rejects unknown clients before any I/O
- Fetches and validates the CIMD metadata document (with SSRF protections and caching)
- Validates
redirect_uriagainst the document'sredirect_uris(exact match per RFC 9700) - Substitutes the CIMD
client_idwith a pre-registered upstream IdP client_id - Forwards the request to the upstream IdP
Configuration example:
MCP_PROXY_CIMD_MAP='{"https://cursor.com/.well-known/oauth-client.json":"cursor-sso-client","https://claude.ai/.well-known/oauth-client.json":"claude-sso-client"}'
MCP_PROXY_CIMD_DEFAULT_CLIENT_ID=generic-mcp-clientWhen CIMD is enabled, the well-known document is modified to:
- Advertise
client_id_metadata_document_supported: true - Rewrite
token_endpointto this adapter's/tokenproxy - Ensure
token_endpoint_auth_methods_supportedincludes"none"
Upstream IdP client registration: Each upstream client_id in MCP_PROXY_CIMD_MAP must be pre-registered at the upstream IdP as a public client (token_endpoint_auth_method: none). Redirect URI patterns must match what the corresponding MCP clients use (typically http://127.0.0.1:* or http://localhost:*).
Why configure separate upstream clients per MCP client? While a single default upstream client_id works, configuring dedicated upstream clients per CIMD URL enables distinct user consent screens at the upstream IdP. The consent screen can display the specific application name (e.g. "Cursor IDE" vs "Claude Code"), giving users visibility into which MCP client is requesting access. This is the primary security benefit of per-client mapping -- users can make informed consent decisions and administrators can revoke access per MCP client independently.
CIMD Security Considerations
- Token
azpmismatch: Issued tokens contain the upstream client_id in theazpclaim, not the CIMD URL the MCP client sent. This works only if MCP client validatesazpagainst its ownclient_id. If a future client does, tokens would appear invalid -- an inherent limitation of client_id substitution that requires native IdP CIMD support to resolve. - SSRF protection: DNS resolution checks (rejects private/loopback/link-local IPs including IPv6-mapped IPv4), no redirect following, 5KB response size limit, 5-second timeout.
- DNS rebinding caveat: A TOCTOU gap exists between the DNS check and the actual fetch connection. The cache mitigates this by limiting repeated fetches.
- Cache isolation: Configured (mapped) clients are pinned in cache and cannot be evicted by an attacker flooding unknown CIMD URLs. Unpinned cache is capped at 1000 entries.
- Allowlist-first: When
MCP_PROXY_CIMD_DEFAULT_CLIENT_IDis not set, only mapped CIMD URLs are allowed; unknown URLs are rejected without any outbound fetch. - Token proxy: Relays token requests to the upstream IdP with
client_idsubstitution, body size limits, timeouts, response size limits, and header whitelisting.
Token Issuer Validation
This adapter rewrites issuer in well-known metadata to its own MCP_BASE_URL (RFC 8414 §3.3), but tokens are issued by the upstream IdP -- their iss claim contains the upstream IdP URL.
MCP servers and clients must not validate the access token JWT iss claim against this adapter's discovery issuer.
Two correct approaches:
- Skip
issvalidation (recommended) -- JWKS signature verification is sufficient. A valid signature against the adapter'sjwks_uri(which points to the upstream IdP's JWKS) cryptographically proves the token's origin. - Validate
issagainst the upstream IdP URL -- configure the MCP server withMCP_UPSTREAM_SSO_URL, notMCP_BASE_URL.
This separation exists because the adapter is an lightweight authorization metadata facade, not a token issuer. It controls discovery, client registration, and authorization redirects, but all token operations remain at the upstream IdP.
All the major MCP clients we tested today are OK.
Deployment Notes
Upstream IdP Client Registration
Every client_id used by this adapter (both the DCR client and each CIMD-mapped client) must be pre-registered at the upstream IdP with the following settings:
| Setting | Value | Reason |
|---|---|---|
| Client type | Public | MCP clients cannot hold secrets (token_endpoint_auth_method: none) |
| Consent | Enabled (required) | User consent is the primary security control -- it lets users see which application is requesting access and decide whether to grant it |
| Standard flow | Enabled | Authorization code flow is the only flow used by MCP clients |
| Valid redirect URIs | See below | Must cover all MCP clients that will use this client_id |
| Allowed scopes | | Must cover all the scopes required by MCP servers using this MCP authentication adapter, mainly the one requiring pre-approval |
Redirect URI patterns to cover the most common MCP clients -- non-authoritative hints, please verify at the deployment time:
| Pattern | MCP Clients |
|---|---|
| http://localhost:* | Claude Code, Claude Desktop, Gemini CLI, Codex CLI, Codex App, Goose, Windsurf, Zed, Warp (CLI agents), Amazon Q CLI, MCP Inspector |
| http://127.0.0.1:* | VS Code |
| https://claude.ai/api/mcp/auth_callback | Claude.ai (web) |
| https://claude.com/api/mcp/auth_callback | Claude.com (web) |
| https://chatgpt.com/connector_platform_oauth_redirect | ChatGPT (web) |
| https://chatgpt.com/connector/oauth/* | ChatGPT (web) |
| cursor://anysphere.cursor-mcp/* | Cursor IDE |
| https://insiders.vscode.dev/* | VS Code Insiders (web) |
| https://vscode.dev/* | VS Code (web) |
| warp://mcp/* | Warp |
| vscode://saoudrizwan.claude-dev/* | Cline |
Note: ephemeral ports are typically used on localhost/127.0.0.1, so your IdP has to allow any port here. And any possible path also.
For the DCR client (MCP_PROXY_DCR_CLIENT_ID), configure all patterns above to support all MCP clients with a single shared client.
For CIMD-mapped clients (MCP_PROXY_CIMD_MAP), you can be more restrictive -- each upstream client only needs the redirect URI patterns for the specific MCP client it maps to. This is one of the advantages of per-client mapping: tighter redirect URI scoping alongside distinct consent screens.
TLS
RFC 7591 §5 requires TLS for the DCR registration endpoint. In production, TLS should be terminated at the reverse proxy / load balancer in front of this application.
Caching
Well-known endpoints return Cache-Control: public, max-age=<seconds> (half of MCP_WELL_KNOWN_REFRESH_MINUTES). DCR returns Cache-Control: no-store. CDNs (e.g. Akamai) must honor origin cache headers to ensure clients receive up-to-date discovery documents.
Rate Limiting
RFC 7591 §3 recommends rate limiting for open DCR endpoints. This adapter does not implement app-level rate limiting -- it should be handled by an external WAF or reverse proxy (e.g. Akamai, Cloudflare, nginx).
CORS
This adapter intentionally does not set CORS headers. All endpoints are designed for server-to-server or redirect-based flows (well-known discovery, DCR, authorize redirects, token proxy) -- none require browser XMLHttpRequest/fetch access from a different origin. The absence of CORS headers also provides a CSRF defense layer for the DCR endpoint.
This means browser-based MCP clients (single-page apps that call these endpoints directly via JavaScript) will not work out of the box. If your deployment requires browser-based access, you have two options:
- Reverse proxy -- configure CORS at the reverse proxy layer (e.g. nginx, Akamai, Cloudflare). Recommended for production deployments that already have a reverse proxy.
- Built-in CORS (not yet implemented) -- a config option to enable CORS directly in the adapter for simpler deployments, development, and testing. Create feature request please if you need it.
Access Logging
This adapter does not produce HTTP access logs (per-request log lines with method, path, status, latency). Application-level logging covers lifecycle events, errors, and debug detail (when MCP_DEBUG=true), and the /metrics endpoint provides aggregate request counts and latency histograms -- but individual request traces are not logged.
For per-request access logs, rely on the reverse proxy or load balancer in front of this adapter (e.g. nginx, HAProxy, Envoy, Istio sidecar, or cloud load balancer access logs). This is the standard pattern for microservices and avoids duplicating logging that the infrastructure layer already provides.
Internal Endpoints
/health/* and /metrics are unauthenticated operational endpoints intended for cluster-internal use only (Kubernetes probes, Prometheus scrape via ServiceMonitor). They should not be exposed to untrusted networks -- keep them behind the cluster-internal Service, not on public Routes/Ingress. A reverse proxy or Ingress should block these paths from external access.
/metricsexposes operational data (request counts, latencies, error rates, upstream health status). No sensitive data (tokens, client_ids, user data) is included -- labels contain only HTTP method, route pattern, and status code.Cache-Control: no-storeis set./health/readyreveals shutdown state, which could be useful for reconnaissance./metricsreturns Prometheus text exposition format, compatible with:- OpenShift built-in monitoring (ServiceMonitor)
- Standalone Prometheus
- OpenTelemetry Collectors via the Prometheus receiver -- for OTel-based pipelines, no application-side OTLP push is needed; the OTel Collector scrapes
/metricsand forwards to any backend.
Kubernetes annotations for Prometheus auto-discovery:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "3000"
prometheus.io/path: "/metrics"ServiceMonitor for OpenShift / Prometheus Operator:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: mcp-auth-adapter
spec:
selector:
matchLabels:
app: mcp-auth-adapter
endpoints:
- port: http
path: /metricsUpstream IdP Compatibility
On first startup (and after every MCP_UPSTREAM_SSO_URL change), review the adapter's log output for warnings prefixed with Upstream IdP compatibility:. These indicate the upstream IdP may not fully support MCP authorization requirements -- for example, missing authorization_endpoint, missing token_endpoint, or missing PKCE support (code_challenge_methods_supported without S256). The adapter injects safe defaults where possible, but these warnings should be investigated to ensure the upstream IdP is correctly configured for MCP flows.
Health Probes
| Endpoint | Purpose | Response |
|---|---|---|
| GET /health/live | Liveness -- process is running, HTTP listener responsive | 200 always |
| GET /health/ready | Readiness -- application initialized, ready to serve | 200 normally, 503 during graceful shutdown |
Both are mounted before body-parsing middleware. Neither checks upstream IdP availability -- the adapter is functional even with fallback defaults.
Graceful Shutdown
On SIGTERM or SIGINT the adapter:
- Marks itself not-ready (
/health/readyreturns503) so the load balancer stops sending new traffic. - Stops accepting new connections.
- Drains in-flight requests until complete (or
MCP_SHUTDOWN_TIMEOUT_SECONDSelapses, default 30 s, then force-exits). - Clears the periodic well-known refresh timer.
Logging
The adapter emits structured logs to stdout (info, debug) and stderr (warn, error) in a machine-parseable key=value format:
ts=2025-06-01T12:00:00.000Z level=info msg="MCP Auth Adapter started" port=3000 baseUrl=http://localhost:3000| Level | Output | When |
|---|---|---|
| info | stdout | Startup, upstream refresh success, shutdown lifecycle |
| warn | stderr | Upstream fetch failures (fallback kept), config conflicts, IdP compatibility issues |
| error | stderr | Unhandled request errors, upstream request failures, fatal startup errors |
| debug | stdout | Per-request details (method, path, IP, user-agent), discovery fetch attempts — only when MCP_DEBUG=true |
All levels except debug are always active. Set MCP_DEBUG=true to enable verbose per-request logging — useful for development and troubleshooting but noisy for production.
No log aggregation agent or format is assumed — the structured key=value lines are compatible with most log collectors (Fluentd, Promtail, Vector, CloudWatch, etc.) and can be parsed with simple regex or key=value splitters.
Metrics / Observability
The adapter exposes a GET /metrics endpoint in Prometheus text exposition format when MCP_METRICS_ENABLED=true (default). Set MCP_METRICS_ENABLED=false to disable entirely -- no endpoint, no middleware, no-op instrumentation stubs, zero overhead.
Exposed metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
| mcp_auth_http_requests_total | counter | method, path, status | Total HTTP requests to functional endpoints |
| mcp_auth_http_request_duration_seconds | histogram | method, path | Request duration (buckets: 5ms, 10ms, 50ms, 100ms, 500ms, 1s, 5s) |
| mcp_auth_upstream_refresh_total | counter | result | Upstream well-known refresh attempts (success / error) |
| mcp_auth_upstream_refresh_duration_seconds | gauge | -- | Last upstream refresh duration |
| mcp_auth_upstream_refresh_last_success_timestamp | gauge | -- | Unix timestamp of last successful refresh |
| mcp_auth_cimd_cache_operations_total | counter | result | CIMD cache lookups (hit / miss); only when CIMD is enabled |
| mcp_auth_cimd_cache_evictions_total | counter | -- | CIMD cache evictions |
| mcp_auth_cimd_cache_size | gauge | -- | Current CIMD cache entry count |
| mcp_auth_token_proxy_upstream_duration_seconds | histogram | -- | Token proxy upstream request duration; only when CIMD is enabled |
| mcp_auth_token_proxy_upstream_status_total | counter | status | Token proxy upstream response status codes |
| process_uptime_seconds | gauge | -- | Process uptime |
| process_resident_memory_bytes | gauge | -- | Resident memory size |
| process_heap_used_bytes | gauge | -- | V8 heap used |
| nodejs_eventloop_lag_seconds | gauge | -- | Event loop lag (mean) |
Instrumentation scope
Only functional endpoints are instrumented: /.well-known/*, /register, /authorize, /token. Health probes (/health/*), the /metrics endpoint itself, and unmatched paths are not tracked -- no noise from operational endpoints or probe traffic.
Memory footprint
When enabled, the metrics subsystem uses approximately 20--50 KB of memory. All label values come from a small, fixed set (HTTP method, known route patterns, status codes), so there is no unbounded cardinality growth. When disabled (MCP_METRICS_ENABLED=false), the no-op stubs consume effectively zero memory.
Development
See CONTRIBUTING.md for development setup, testing, linting, and code style guidelines.
Upstream Well-Known Handling
The adapter fetches the upstream IdP's discovery document at startup (trying OIDC and RFC 8414 paths) but only exposes a strict whitelist of fields relevant to MCP. See Well-Known Field Filtering for details.
- Discovery fallback chain: The adapter tries
/.well-known/openid-configurationfirst, then/.well-known/oauth-authorization-server(RFC 8414). If both fail, endpoints are derived fromMCP_UPSTREAM_SSO_URLusing Keycloak URL conventions (e.g.{issuer}/protocol/openid-connect/auth). This last-resort fallback is Keycloak-specific -- for other IdPs the derived URLs will be incorrect. Capability fields default to safe minimums (e.g.code_challenge_methods_supported: ["S256"]). - Flow-level defaults: When the upstream provides
authorization_endpointandtoken_endpointbut omits flow fields, the adapter injects:response_types_supported: ["code"],grant_types_supported: ["authorization_code"],code_challenge_methods_supported: ["S256"]. Existing upstream values are never overridden. - Periodic refresh: Re-fetches at the configured interval (default: 60 min). On success, the new document is used immediately. On failure, the previous document is kept.
- Compatibility validation: At startup and on each periodic refresh, the adapter validates the upstream document and logs
Upstream IdP compatibility:warnings for:- Missing
authorization_endpointortoken_endpoint(MCP authorization flow will not work). - Missing
code_challenge_methods_supported(the adapter will advertise["S256"]but if the upstream doesn't actually support PKCE, token exchange will fail). code_challenge_methods_supportedpresent but withoutS256(MCP requires PKCE with S256).
- Missing
Well-Known Field Filtering
The adapter only exposes a strict whitelist of upstream fields. New upstream fields are not automatically included -- they must be added to UPSTREAM_WHITELIST_FIELDS in src/routes/well-known.ts.
Included fields
issuer, authorization_endpoint, token_endpoint, jwks_uri, registration_endpoint, scopes_supported, response_types_supported, response_modes_supported, grant_types_supported, token_endpoint_auth_methods_supported, token_endpoint_auth_signing_alg_values_supported, code_challenge_methods_supported, id_token_signing_alg_values_supported, subject_types_supported, claims_supported, introspection_endpoint, userinfo_endpoint, revocation_endpoint, authorization_response_iss_parameter_supported
Adapted fields
| Field | Condition | Adaptation |
|---|---|---|
| issuer | Always | Replaced with MCP_BASE_URL per RFC 8414 §3.3 |
| registration_endpoint | MCP_PROXY_DCR_CLIENT_ID set | Replaced with {MCP_BASE_URL}/register |
| authorization_endpoint | Scope filtering or CIMD configured | Replaced with {MCP_BASE_URL}/authorize |
| token_endpoint_auth_methods_supported | DCR or CIMD enabled | "none" injected if not already present |
| token_endpoint | CIMD enabled | Rewritten to {MCP_BASE_URL}/token |
| client_id_metadata_document_supported | CIMD enabled | Set to true |
| scopes_supported | MCP_WELL_KNOWN_SCOPES_SUPPORTED set | Replaced with configured value; omitted if empty |
| response_types_supported | Upstream omits + auth flow present | Defaults to ["code"] |
| grant_types_supported | Upstream omits + auth flow present | Defaults to ["authorization_code"] |
| code_challenge_methods_supported | Upstream omits + auth flow present | Defaults to ["S256"] |
| authorization_response_iss_parameter_supported | Auth proxy enabled | Removed (upstream issuer won't match rewritten issuer) |
Excluded fields (by category)
| Category | Fields | Reason |
|---|---|---|
| OIDC session / logout | end_session_endpoint, check_session_iframe, frontchannel_logout_*, backchannel_logout_* | MCP clients manage token lifecycles via expiration and revocation, not OIDC logout. |
| CIBA | backchannel_authentication_endpoint, backchannel_authentication_request_signing_alg_*, backchannel_token_delivery_modes_supported | Not part of MCP flows. |
| Device flow | device_authorization_endpoint | Not a standard MCP flow. |
| PAR | pushed_authorization_request_endpoint, require_pushed_authorization_requests | Not standard in MCP. |
| JAR / JARM | request_object_*, request_parameter_supported, request_uri_parameter_supported, require_request_uri_registration, authorization_signing_alg_*, authorization_encryption_* | Not used by MCP clients. |
| mTLS | mtls_endpoint_aliases, tls_client_certificate_bound_access_tokens | Not typical for MCP client flows. |
| Encryption | id_token_encryption_*, userinfo_signing_alg_*, userinfo_encryption_* | Not consumed by MCP clients. |
| Server-side auth | introspection_endpoint_auth_*, revocation_endpoint_auth_* | Server-side concern, not relevant to client discovery. |
| Misc | claim_types_supported, claims_parameter_supported, acr_values_supported, prompt_values_supported | Not needed for standard MCP flows. |
Note: This list is informative only, anything not included in UPSTREAM_WHITELIST_FIELDS is automatically excluded.
Security review
An OWASP Top 10 security review was performed on 2026-05-15. No critical issues were found. If you discover a security vulnerability, please report it responsibly via GitHub Security Advisories.
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines. All PRs must reference a GitHub issue, and new features should be discussed in an issue before implementation.
License
This project is licensed under the Apache License 2.0.
Disclaimer
This project is an independent, community-driven effort. It is not affiliated with, endorsed by, or connected to Anthropic or the Model Context Protocol project. "Model Context Protocol" and "MCP" may be trademarks of their respective owners. Use of these names is solely for descriptive purposes to indicate compatibility.
