Skip to main content
The MCP server supports OAuth 2.1 authorization, enabling integration with identity providers like Keycloak, Auth0, and Okta.
OAuth is disabled by default. Existing MCP configurations continue to work unchanged.

Overview

When OAuth is enabled, every HTTP request to the MCP server must include a valid JWT bearer token in the Authorization header. The server validates the token using configured JWKS providers and enforces scope requirements at multiple levels. All scope enforcement happens at the HTTP transport level per the MCP specification, returning 403 Forbidden responses with WWW-Authenticate headers that enable step-up authorization.

Quick Start

Configuration Options

JWKS Configuration

The oauth.jwks array configures one or more JWKS providers for JWT verification. The MCP OAuth config uses the same JWKSConfiguration format as the router’s top-level authentication configuration, though it is configured independently. You can point the MCP JWKS to the same JWKS URL as your router authentication, or to a different identity provider entirely. For the full JWKS configuration reference including all options (refresh_unknown_kid, allowed_use, etc.), see Router Authentication.

Remote JWKS URL

Symmetric Secret

For development or testing, you can use a shared symmetric secret instead of a remote JWKS endpoint:

Scope Enforcement

Scopes are enforced at multiple levels, all at the HTTP transport layer per the MCP specification. Each level is additive — a request must satisfy all applicable scope gates.

Initialize Scopes (HTTP Level)

The scopes.initialize list defines scopes required for every HTTP request to the MCP server. These are checked before the JSON-RPC payload is parsed, serving as the baseline authorization to establish an MCP connection.

Method-Level Scopes

Additional scopes can be required for specific MCP methods:
When both initialize and method-level scopes are configured, the token must contain all of them. For example, calling tools/call requires both the initialize scopes and the tools_call scopes.
Scopes in the JWT must be provided as a space-separated string in the scope claim (per OAuth 2.0 convention). Array-format scope claims are not supported.

Built-in Tool Scopes

The MCP server provides three built-in tools: execute_graphql, get_operation_info, and get_schema. Each can have its own scope requirements, configured independently from tools_call:
Built-in tool scopes are additive to tools_call — the token must satisfy both. If tools_call is empty, only the built-in tool scope is checked. Scopes for execute_graphql are only relevant when enable_arbitrary_operations: true, and scopes for get_schema are only relevant when expose_schema: true. When the corresponding feature is disabled, the tool is not registered and its scopes are excluded from the scopes_supported metadata.

Per-Tool Scopes (from @requiresScopes)

When your GraphQL schema uses the @requiresScopes directive on fields, the MCP server automatically extracts scope requirements for each tool at startup. If a tool’s underlying GraphQL operation touches fields that require specific scopes, those scopes are enforced when the tool is called. For example, if your schema defines:
And you have an operation getTopSecretFacts.graphql that queries topSecretFacts, calling that tool will require either read:fact OR read:all in addition to any initialize and tools_call scopes. The @requiresScopes directive uses OR-of-AND semantics. When an operation touches multiple fields with @requiresScopes, the MCP server computes the combined scope requirement using the Cartesian product rules. For a full explanation of how scopes combine, see the @requiresScopes directive documentation. Per-tool scopes are computed at startup (and on config reload), so they are enforced at the HTTP level with zero runtime overhead per request.

Runtime Scopes (execute_graphql)

When enable_arbitrary_operations is enabled, the execute_graphql tool allows AI models to craft custom GraphQL queries. Since the server cannot know which fields will be queried ahead of time, scope checking happens at request time by parsing the GraphQL query and extracting @requiresScopes requirements for the fields it references. This runtime check uses the same OR-of-AND semantics and smart challenge selection as per-tool scopes. If the token lacks required scopes, the server returns a 403 Forbidden with an appropriate scope challenge before the query is executed.
If the GraphQL query cannot be parsed, the request is passed through to the GraphQL engine, which handles the error. Scope checking is best-effort and does not block malformed queries.

Scope Enforcement Summary

get_operation_info and Scope Discovery

The get_operation_info tool includes scope requirements in its response, allowing AI models to discover what scopes a tool needs before calling it:

Token Upgrade Flow

Tokens can be upgraded on the same MCP session without reconnecting:
  1. Client connects with a token that has mcp:connect scope
  2. Client calls tools/call and receives 403 Forbidden with insufficient_scope
  3. Client obtains a new token with additional scopes from the authorization server
  4. Client retries with the new token on the same session (same Mcp-Session-Id)

Scope Challenge Behavior

When the server returns a 403 Forbidden response, the WWW-Authenticate header includes a scope parameter per RFC 6750 telling the client which scopes to request. For per-tool and runtime challenges where multiple scope groups can satisfy the requirement (OR-of-AND), the server selects the best group — the one requiring the fewest additional scopes based on what the token already has:
  1. For each AND-group, count how many scopes the token is missing
  2. Pick the group with the fewest missing scopes (ties go to the first group)
For example, suppose a tool requires (read:employee AND read:private AND read:fact) OR (read:all) and a client presents a token with scopes read:employee read:private: Both groups have 1 missing scope. The server picks the first, returning the complete AND-group (not just the missing scopes):
The client can then request the additional read:fact scope from the authorization server and retry.

scope_challenge_include_token_scopes

Some MCP client SDKs do not correctly accumulate scopes when performing step-up authorization — they request only the scopes from the WWW-Authenticate challenge, discarding the scopes they already had. This causes a loop where gaining a new scope loses a previous one. This is a known issue in the MCP TypeScript SDK. To work around this, set scope_challenge_include_token_scopes: true to include the token’s existing scopes alongside the required scopes in the challenge.
Setting scope_challenge_include_token_scopes: true reveals the token’s existing scopes in the WWW-Authenticate response header. If this is a concern, leave it as false and ensure your MCP clients correctly accumulate scopes when requesting new tokens.

HTTP Error Responses

401 Unauthorized

Returned when the token is missing, invalid, expired, or signature verification fails.
The scope parameter contains the initialize scopes (minimum scopes needed to connect). The resource_metadata URL points to the RFC 9728 metadata endpoint for OAuth discovery.

403 Forbidden

Returned when the token is valid but lacks required scopes. The exact scope parameter depends on which level of enforcement rejected the request: Method-level rejection (e.g., missing tools_call scopes):
Per-tool rejection (e.g., calling a tool that requires read:fact):
The scope parameter always contains only the scopes needed for the specific operation that failed (unless scope_challenge_include_token_scopes is enabled).
Per the MCP specification, HTTP-level authentication failures return only HTTP status codes and headers — no JSON-RPC response body is included.

RFC 9728 Protected Resource Metadata

When OAuth is enabled and authorization_server_url is configured, the MCP server exposes a public (unauthenticated) metadata endpoint at:
This follows the RFC 9728 path-aware format. MCP clients use this endpoint to automatically discover the authorization server and all supported scopes. Example response:
The scopes_supported field is automatically computed as the union of:
  • All configured static scopes (initialize, tools_list, tools_call)
  • All scopes extracted from @requiresScopes directives on fields used by registered operations
This means MCP clients can request all supported scopes upfront during the initial OAuth authorization, avoiding step-up challenges entirely. The authorization server controls which scopes are actually granted based on the client’s permissions and the user’s consent.
Well-behaved MCP clients should read scopes_supported from the metadata endpoint and request all supported scopes during the initial authorization. This avoids the step-up flow entirely when the authorization server grants all requested scopes. The step-up flow with 403 challenges serves as a fallback when the client doesn’t have all scopes upfront.

Startup Validation

The router performs startup validation when OAuth is enabled:
  • If oauth.jwks is empty, the router exits with a fatal error to prevent starting an unprotected endpoint
  • If server.base_url is empty, the router exits with a fatal error because it is required for RFC 9728 metadata discovery

Full Configuration Example