## Configuration Options

| Option | Description | Default |
| --- | --- | --- |
| `oauth.enabled` | Enable OAuth 2.1 / JWKS-based authentication for the MCP server | `false` |
| `oauth.authorization_server_url` | Base URL of the OAuth 2.0 authorization server. Advertised via the [RFC 9728 metadata endpoint](https://cosmo-docs.wundergraph.com/router/mcp/oauth/configuration#rfc-9728-protected-resource-metadata) so clients can discover authorization endpoints. | - |
| `oauth.scope_challenge_include_token_scopes` | When `true`, includes the token’s existing scopes in the `scope` parameter of 403 responses. Works around MCP SDK scope accumulation bugs. See [Scope Challenge Behavior](https://cosmo-docs.wundergraph.com/router/mcp/oauth/scopes#scope-challenge-behavior). | `false` |
| `oauth.max_scope_combinations` | Maximum scope combinations computed per operation. Raise for schemas with many overlapping `@requiresScopes`. | `2048` |
| `oauth.scopes.initialize` | Scopes required for **all** HTTP requests (checked before JSON-RPC parsing). This is the baseline scope needed to establish an MCP connection. | `[]` |
| `oauth.scopes.tools_list` | Scopes required for the `tools/list` MCP method. | `[]` |
| `oauth.scopes.tools_call` | Scopes required for the `tools/call` MCP method (any tool invocation). Per-tool and built-in tool scopes are enforced additively. | `[]` |
| `oauth.scopes.execute_graphql` | Scopes required to call the `execute_graphql` built-in tool. Additive to `tools_call`. Only relevant when `enable_arbitrary_operations` is `true`. | `[]` |
| `oauth.scopes.get_operation_info` | Scopes required to call the `get_operation_info` built-in tool. Additive to `tools_call`. | `[]` |
| `oauth.scopes.get_schema` | Scopes required to call the `get_schema` built-in tool. Additive to `tools_call`. Only relevant when `expose_schema` is `true`. | `[]` |
| `oauth.jwks` | List of JWKS providers for JWT verification. Supports remote JWKS URLs or symmetric secrets. | `[]` |

## JWKS Configuration

The `oauth.jwks` array configures one or more JWKS providers for JWT verification.

### Remote JWKS URL

```yaml
oauth:
  jwks:
    - url: 'https://auth.example.com/.well-known/jwks.json'
      audiences:
        - 'https://mcp.example.com'
      algorithms:
        - 'RS256'
        - 'ES256'
      refresh_interval: '1m' # How often to refresh the key set
```

| Field | Description | Default |
| --- | --- | --- |
| `url` | URL of the JWKS endpoint | (required) |
| `audiences` | Allowed JWT `aud` claim values | (any) |
| `algorithms` | Allowed signing algorithms (RS256, ES256, PS256, EdDSA, etc.) | (all) |
| `refresh_interval` | How often to refresh the JWKS key set | `1m` |

### Symmetric Secret

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

```yaml
oauth:
  jwks:
    - secret: 'your-shared-secret'
      symmetric_algorithm: 'HS256' # HS256, HS384, or HS512. For other algorithms, use a remote JWKS URL.
      header_key_id: 'my-key-id'
```

## Environment Variables

| Environment Variable | Configuration Path |
| --- | --- |
| `MCP_OAUTH_ENABLED` | `mcp.oauth.enabled` |
| `MCP_OAUTH_AUTHORIZATION_SERVER_URL` | `mcp.oauth.authorization_server_url` |
| `MCP_OAUTH_SCOPE_CHALLENGE_INCLUDE_TOKEN_SCOPES` | `mcp.oauth.scope_challenge_include_token_scopes` |
| `MCP_OAUTH_MAX_SCOPE_COMBINATIONS` | `mcp.oauth.max_scope_combinations` |

## HTTP Error Responses

### 401 Unauthorized

Returned when the token is missing, invalid, expired, or signature verification fails.

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
                  scope="mcp:connect",
                  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"
```

### 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):

```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                  scope="mcp:tools:execute",
                  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp",
                  error_description="missing required scopes: mcp:tools:execute"
```

**Example response for protected resource metadata:**

```json
{
  "resource": "https://mcp.example.com",
  "authorization_servers": ["https://auth.example.com"],
  "bearer_methods_supported": ["header"],
  "resource_documentation": "https://mcp.example.com/mcp",
  "scopes_supported": [
    "mcp:connect",
    "mcp:tools:execute",
    "mcp:tools:read",
    "read:all",
    "read:employee",
    "read:fact",
    "read:private"
  ]
}
```

## 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](https://datatracker.ietf.org/doc/rfc9728/) metadata discovery.

## Full Configuration Example

```yaml
mcp:
  enabled: true
  server:
    listen_addr: '0.0.0.0:5025'
    base_url: 'https://mcp.example.com'
  graph_name: 'my-graph'
  exclude_mutations: true
  enable_arbitrary_operations: true # Enables execute_graphql tool
  expose_schema: true # Enables get_schema tool
  oauth:
    enabled: true
    authorization_server_url: 'https://auth.example.com'
    scope_challenge_include_token_scopes: false # Set to true for MCP clients with scope accumulation bugs
    scopes:
      initialize:
        - 'mcp:connect'
      tools_list:
        - 'mcp:tools:read'
      tools_call:
        - 'mcp:tools:execute'
      # Built-in tool scopes (additive to tools_call)
      execute_graphql:
        - 'mcp:graphql:execute'
      get_schema:
        - 'mcp:schema:read'
      get_operation_info:
        - 'mcp:ops:read'
    jwks:
      - url: 'https://auth.example.com/.well-known/jwks.json'
        audiences:
          - 'https://mcp.example.com'
        algorithms:
          - 'RS256'
        refresh_interval: '1m'
  storage:
    provider_id: 'mcp'
  session:
    stateless: true

storage_providers:
  file_system:
    - id: 'mcp'
      path: 'operations'
```
