Skip to main content
Real MCP clients (Claude Desktop, Cursor, ChatGPT connectors and similar) do not always follow the strict shape the authorization server enforces by default. AuthorizationServerConfig.ClientCompat relaxes individual checks, one switch at a time. Every switch defaults to false, which is today’s strict behavior.

Flags

OptionalResource

When resource is absent at /oauth/authorize, /oauth/par, the token endpoint (authorization code and refresh) or the jwt-bearer grant, the request defaults to the sole configured resource. With two or more configured resources an absent resource is still invalid_target. A resource that is present but unknown is always invalid_target. Client credentials, token exchange and the device flow are unchanged (device already defaulted). The client_credentials grant also honors it: an absent resource defaults to the sole configured resource and the token aud is the configured identifier. Token exchange (RFC 8693) already defaulted an absent resource to the subject token’s aud; that is unchanged. Tradeoff: low. The audience can only be a resource you configured, and with a single resource there is no ambiguity about the audience.

DropUnknownScopes

A requested scope is kept only if it is in the resource catalog (when the catalog is non-empty) and in the client’s registered scope set (when the client registered one). Everything else is dropped, and the granted scope is returned in the token response scope field and the scope claim. If scopes were requested and none remain, the request fails with invalid_scope. An entirely empty scope request is rejected exactly as before. On the refresh grant, scopes outside the original grant are dropped the same way, so a refresh can never widen. Tradeoff: medium. A client can no longer learn it asked for something wrong from an error, and it may receive fewer scopes than it expected. It can never receive more than the catalog and its registration allow. Clients must read the returned scope rather than assume what they asked for.

LoopbackAnyPort

RFC 8252 section 7.3: when a registered redirect URI is http on 127.0.0.1 or [::1], the requested URI matches if scheme, host, path and query are identical and only the port differs. Applied at /authorize, PAR and /token (the token request may carry a different port than the code was issued for, as long as both are the same loopback URI up to the port). 127.0.0.1 and [::1] do not match each other. Userinfo, fragments and invalid ports never match. localhost is excluded unless LoopbackAllowLocalhost is also set. Tradeoff: higher. Any process on the user’s machine can listen on a port and receive the redirect. PKCE (always required) is what prevents that process from redeeming the code, so do not enable this for clients that cannot use PKCE S256 (this server requires it for everyone). Only affects clients that registered a loopback URI.

PathAudience

A requested resource matches a configured resource if it is equal modulo a trailing slash, or if the configured identifier is a path prefix of the request on a / boundary (https://api.example.com/mcp/sse matches https://api.example.com/mcp; https://api.example.com/mcp-admin does not). Queries, fragments, userinfo, dot segments and percent-encoded dots or slashes are refused. When several configured resources match, the longest wins. The issued token aud is always the configured canonical identifier, never the requested sub-path. It applies at every grant that takes a resource: authorization code, refresh, client_credentials, token exchange (the resource is compared to the subject token aud by canonical identifier, and audience may name a sub-path) and ID-JAG redemption at the jwt-bearer grant (the resource claim in the ID-JAG and the presented resource match when both resolve to the same configured resource). ID-JAG issuance is unchanged: it accepts any absolute resource URL because the target need not be one of this server’s resources. Tradeoff: low to medium. A client can obtain a token for the whole configured resource while naming a sub-path, so do not rely on the sub-path to narrow authorization. For a single-resource MCP server with public (CIMD or DCR) clients:
Leave LoopbackAllowLocalhost off unless a client you must support registers http://localhost/.... Pair this with RequireState: true and CIMD TrustPolicy: AllowHTTPSHosts(...) for the clients you intend to serve. If you only serve clients that behave, enable none of these.

CIMD client metadata

token_endpoint_auth_method in a client metadata document is checked against an allowlist: none (the default when omitted) and private_key_jwt. client_secret_basic, client_secret_post, client_secret_jwt and any other value are rejected because a published document has no place for a secret. private_key_jwt requires exactly one of jwks (a key set with at least one key) or jwks_uri (an absolute https URL). Unknown top-level fields are ignored, so vendor extensions do not break resolution. CIMDConfig.HonorCacheControl (default false) lets the publisher shorten how long a fetched document is cached with Cache-Control: max-age, no-cache or no-store. The effective freshness is always between cimd.MinCacheTTL (10 seconds) and CIMDConfig.CacheTTL; a publisher can never extend caching past CacheTTL. A redirect URI with a bare custom scheme such as cursor:// is refused by default (RFC 8252 section 7.1 requires a reverse-DNS scheme). List the scheme to accept it:
Entries must be lowercase and may not be http, https, javascript, data, file (or vbscript, blob, about, ftp); theauth.New fails otherwise. Fragments and userinfo are still refused and matching stays exact. Tradeoff: medium. Any app on the user’s device can register the same private-use scheme and receive the code; PKCE S256 (always required here) is what stops it redeeming the code. Prefer a narrow TrustPolicy, and note the listed scheme applies to every document the policy admits.

Authentication context and step-up

Independent of ClientCompat: the authorization server records how and when the user authenticated and emits it, so a resource server using mcpresource.RequireStepUp (RFC 9470) can be satisfied by tokens from this server.
  • auth_time is the later of sign-in and the latest POST /auth/step-up of the session that authorized. amr comes from AuthorizationServerConfig.SessionAMR, which defaults to ["pwd"] because sessions do not record their sign-in method. Supply it (for example from your own login records) to emit otp, hwk or mfa.
  • acr maps the method set through AuthorizationServerConfig.ACRValues (keys are the distinct amr values sorted and joined with +, such as "otp+pwd"). Unmapped sets use the default: urn:theauth:acr:1fa for one method, urn:theauth:acr:mfa for mfa or two or more methods. Configured values are advertised as acr_values_supported.
  • The claims appear in JWT and opaque access tokens and in introspection responses. They are stored on the authorization code and the refresh token family and copied on every refresh; a refresh never advances auth_time. Grants without a user session (client_credentials, token exchange, jwt-bearer) carry none of them.
  • /oauth/authorize honors max_age (seconds; 0 always re-authenticates), acr_values (a session satisfies it when its acr equals one listed value) and prompt=none, also through PAR and JAR. When a requirement is unmet the user is redirected to LoginURL with next, reauth=1 and the max_age and acr_values hints; with prompt=none the client receives login_required instead. A requirement with no authentication context fails closed.
Limit: there is no in-band step-up flow at /authorize. The login page you mount at LoginURL must create a fresh session or step the existing one up (POST /auth/step-up) and send the user back to next; if it returns without doing so the user is redirected again. A step-up with TOTP does not by itself change amr unless SessionAMR reports it.

Test vectors

internal/as/compat_test.go holds table-driven vectors for resource resolution, scope narrowing and redirect matching. They are plain data so the TypeScript library can reuse them. Client shapes under internal/as/testdata/clientcompat are representative of public client documentation, not captured traffic. The TypeScript package has the same switches under lenientClients. See MCP client compatibility for the TypeScript names and the few places the two differ.
Last modified on October 13, 2026