Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions docs/mcp_oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,74 @@ Check that:
**Symptom:** `x-mcp-debug-auth-resolution` shows `m2m-client-credentials`.

The server has `client_id`/`client_secret`/`token_url` configured so LiteLLM is fetching a machine-to-machine token instead of using the per-user OAuth2 token. To use per-user tokens, remove the client credentials from the server config.


## Delegate Auth to Upstream (PKCE Passthrough)

For OAuth2 MCP servers where the client (Claude Code, Cursor, ChatGPT, etc.) already authenticates directly against the upstream server's own OAuth issuer, you can opt the route into **upstream-delegated auth**: LiteLLM stops checking its own API key / SSO and lets the client's PKCE flow run end-to-end with the upstream MCP server.

Use this when the upstream server is the source of truth for who can access it and you don't want LiteLLM to gate the route a second time.

### Setup

```yaml title="config.yaml" showLineNumbers
mcp_servers:
notion_mcp:
url: "https://mcp.notion.com/mcp"
auth_type: oauth2
delegate_auth_to_upstream: true
```

That's the entire change. The flag is honored **only** when `auth_type: oauth2`; setting it on any other auth type is silently ignored.

### How It Works

1. Client sends an MCP request to LiteLLM with no `x-litellm-api-key` (and optionally no `Authorization` header).
2. LiteLLM detects that every target server in the request is `auth_type: oauth2` AND has `delegate_auth_to_upstream: true`, and skips its own API-key/SSO check.
3. LiteLLM also skips its pre-emptive 401, so the upstream MCP server's own `401` + `WWW-Authenticate` flows back to the client.
4. The client completes PKCE directly with the upstream OAuth issuer.
5. The client retries with `Authorization: Bearer <upstream-token>`. LiteLLM forwards it untouched.

```mermaid
sequenceDiagram
participant Client
participant LiteLLM as LiteLLM Proxy
participant MCP as Upstream MCP Server
participant Auth as Upstream OAuth Server

Client->>LiteLLM: MCP request (no LiteLLM key)
LiteLLM->>MCP: Forward request (no Authorization)
MCP-->>LiteLLM: 401 + WWW-Authenticate
LiteLLM-->>Client: 401 + WWW-Authenticate (passthrough)

Note over Client,Auth: Client runs PKCE directly with upstream
Client->>Auth: Authorize + token exchange (PKCE)
Auth-->>Client: access_token

Client->>LiteLLM: MCP request + Bearer access_token
LiteLLM->>MCP: Forward request + Bearer access_token
MCP-->>LiteLLM: MCP response
LiteLLM-->>Client: MCP response
```

### Fail-Closed Behavior

The bypass only fires when **every** target the request resolves to opts in. It fails closed and runs normal LiteLLM auth in any of these cases:

- The server's `auth_type` is anything other than `oauth2`.
- `delegate_auth_to_upstream` is not explicitly `true`.
- The request targets multiple servers (`x-mcp-servers: a,b`) and any one of them is not delegated.
- The target server cannot be resolved from the URL path or `x-mcp-servers` header.

### Security Trade-offs

- This flag turns the MCP route into an **unauthenticated** ingress at the LiteLLM layer. Spend tracking, per-key rate limits, and any guardrails that depend on `user_api_key_auth.user_id` will not run for these requests.
- LiteLLM cannot tell who the caller is — that's the entire point — so per-user auditing must come from the upstream MCP server's own logs.
- Only enable this on servers whose upstream OAuth issuer you trust to enforce access control.

### Config Reference

| Field | Required | Description |
|-------|----------|-------------|
| `auth_type` | Yes | Must be `oauth2`. The flag is ignored otherwise. |
| `delegate_auth_to_upstream` | Yes | Set to `true` to opt this server into PKCE passthrough. |