---
title: "MCP OAuth"
url: "/docs/mcp_oauth"
canonical_url: "https://docs.litellm.ai/docs/mcp_oauth"
type: "docs"
last_updated: "2026-10-10"
summary: "LiteLLM supports several OAuth 2.0 patterns for MCP servers. Every authtype"
related:
  - "/docs/mcp_per_user_auth"
  - "/docs/mcp_oauth_passthrough"
---
# MCP OAuth

> Index of all LiteLLM docs: https://docs.litellm.ai/llms.txt


LiteLLM supports several OAuth 2.0 patterns for MCP servers. Every `auth_type: oauth2` server in `config.yaml` must declare its flow via `oauth2_flow`; the passthrough modes are their own `auth_type` values, documented in [MCP OAuth Passthrough](./mcp_oauth_passthrough.md):

| Flow | `oauth2_flow` | Use Case | How It Works |
|------|---------------|----------|--------------|
| **Interactive (PKCE)** | `authorization_code` | User-facing apps (Claude Code, Cursor) | Browser-based consent, per-user tokens |
| **Machine-to-Machine (M2M)** | `client_credentials` | Backend services, CI/CD, automated agents | `client_credentials` grant, proxy-managed tokens |
| **On-Behalf-Of (OBO)** | n/a (uses `auth_type: oauth2_token_exchange`) | User-context tool calls to protected MCP servers | LiteLLM exchanges the caller token for a scoped MCP token. See [MCP OBO Auth](./mcp_obo_auth.md). |
| **Passthrough (transparent)** | n/a (uses `auth_type: true_passthrough`) | Client already holds the upstream token; LiteLLM adds no auth of its own | Forwards the client's `Authorization` verbatim, no LiteLLM admission. [See MCP OAuth Passthrough](./mcp_oauth_passthrough.md) |
| **Delegated upstream OAuth** | n/a (uses `auth_type: oauth_delegate`) | LiteLLM admits the caller; the upstream owns tool authorization | LiteLLM admission plus a separate forwarded upstream bearer, keeps spend and rate limits. [See MCP OAuth Passthrough](./mcp_oauth_passthrough.md) |

Per-user tokens from the interactive flow are stored under the calling key's `user_id`. For how that interacts with service-account keys, and for the non-OAuth per-user options (per-request headers, BYOK keys, per-user env vars), see [Per-User and Per-Key Upstream Credentials](./mcp_per_user_auth.md).

## Interactive OAuth (PKCE)

For user-facing MCP clients (Claude Code, Cursor), LiteLLM supports the full OAuth 2.0 authorization code flow with PKCE.

### Setup

```yaml title="config.yaml" showLineNumbers
mcp_servers:
  github_mcp:
    url: "https://api.githubcopilot.com/mcp"
    auth_type: oauth2
    oauth2_flow: authorization_code
    client_id: os.environ/GITHUB_OAUTH_CLIENT_ID
    client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET
```

[**See Claude Code Tutorial**](/docs/tutorials/claude_responses_api)

### How It Works

```mermaid
sequenceDiagram
    participant Browser as User-Agent (Browser)
    participant Client as Client
    participant LiteLLM as LiteLLM Proxy
    participant MCP as MCP Server (Resource Server)
    participant Auth as Authorization Server

    Note over Client,LiteLLM: Step 1 – Resource discovery
    Client->>LiteLLM: GET /.well-known/oauth-protected-resource/{mcp_server_name}/mcp
    LiteLLM->>Client: Return resource metadata

    Note over Client,LiteLLM: Step 2 – Authorization server discovery
    Client->>LiteLLM: GET /.well-known/oauth-authorization-server/{mcp_server_name}
    LiteLLM->>Client: Return authorization server metadata

    Note over Client,Auth: Step 3 – Dynamic client registration
    Client->>LiteLLM: POST /{mcp_server_name}/register
    LiteLLM->>Auth: Forward registration request
    Auth->>LiteLLM: Issue client credentials
    LiteLLM->>Client: Return client credentials

    Note over Client,Browser: Step 4 – User authorization (PKCE)
    Client->>Browser: Open authorization URL + code_challenge + resource
    Browser->>Auth: Authorization request
    Note over Auth: User authorizes
    Auth->>Browser: Redirect with authorization code
    Browser->>LiteLLM: Callback to LiteLLM with code
    LiteLLM->>Browser: Redirect back with authorization code
    Browser->>Client: Callback with authorization code

    Note over Client,Auth: Step 5 – Token exchange
    Client->>LiteLLM: Token request + code_verifier + resource
    LiteLLM->>Auth: Forward token request
    Auth->>LiteLLM: Access (and refresh) token
    LiteLLM->>Client: Return tokens

    Note over Client,MCP: Step 6 – Authenticated MCP call
    Client->>LiteLLM: MCP request with access token + LiteLLM API key
    LiteLLM->>MCP: MCP request with Bearer token
    MCP-->>LiteLLM: MCP response
    LiteLLM-->>Client: Return MCP response
```

**Participants**

- **Client** -- The MCP-capable AI agent (e.g., Claude Code, Cursor, or another IDE/agent) that initiates OAuth discovery, authorization, and tool invocations on behalf of the user.
- **LiteLLM Proxy** -- Mediates all OAuth discovery, registration, token exchange, and MCP traffic while protecting stored credentials.
- **Authorization Server** -- Issues OAuth 2.0 tokens via dynamic client registration, PKCE authorization, and token endpoints.
- **MCP Server (Resource Server)** -- The protected MCP endpoint that receives LiteLLM's authenticated JSON-RPC requests.
- **User-Agent (Browser)** -- Temporarily involved so the end user can grant consent during the authorization step.

**Flow Steps**

1. **Resource Discovery**: The client fetches MCP resource metadata from LiteLLM's `.well-known/oauth-protected-resource` endpoint to understand scopes and capabilities.
2. **Authorization Server Discovery**: The client retrieves the OAuth server metadata (token endpoint, authorization endpoint, supported PKCE methods) through LiteLLM's `.well-known/oauth-authorization-server` endpoint.
3. **Dynamic Client Registration**: The client registers through LiteLLM, which forwards the request to the authorization server (RFC 7591). If the provider doesn't support dynamic registration, you can pre-store `client_id`/`client_secret` in LiteLLM (e.g., GitHub MCP) and the flow proceeds the same way.
4. **User Authorization**: The client launches a browser session (with code challenge and resource hints). The user approves access, the authorization server sends the code through LiteLLM back to the client.
5. **Token Exchange**: The client calls LiteLLM with the authorization code, code verifier, and resource. LiteLLM exchanges them with the authorization server and returns the issued access/refresh tokens.
6. **MCP Invocation**: With a valid token, the client sends the MCP JSON-RPC request (plus LiteLLM API key) to LiteLLM, which forwards it to the MCP server and relays the tool response.

See the official [MCP Authorization Flow](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-flow-steps) for additional reference.

### Redirect URLs for static OAuth clients {#static-client-redirect-urls}

Use a static OAuth client when the upstream identity provider (IdP) requires an application to be registered in advance. Configure the application's `client_id` and `client_secret` in the LiteLLM MCP server entry. This supports providers that do not offer Dynamic Client Registration (RFC 7591).

The authorization flow uses two callback URLs:

| Callback | Purpose | Configuration |
|----------|---------|---------------|
| LiteLLM callback | Receives the authorization response from the upstream IdP. | Register `<proxy origin>/callback` in the IdP application's **Redirect URI** or **Callback URL** field. |
| MCP client callback | Returns the authorization response from LiteLLM to the MCP client. | The client supplies this URL as `redirect_uri` to `/{mcp_server_name}/authorize`. Configure additional trusted callbacks in LiteLLM when required by the validation rules below. |

For example, a proxy at `https://llm.example.com` uses `https://llm.example.com/callback` as its IdP callback. A desktop client's local callback, such as `http://localhost:33418/callback`, is supplied to LiteLLM and does not need to be registered with the upstream IdP.

#### Configure the upstream OAuth application

Set `PROXY_BASE_URL` to the proxy's public origin and register the corresponding `/callback` URL with the IdP:

```bash
export PROXY_BASE_URL=https://llm.example.com
# IdP callback URL: https://llm.example.com/callback
```

LiteLLM resolves its public origin from `PROXY_BASE_URL`, trusted `X-Forwarded-*` headers, or the incoming request URL, in that order. See [Reverse proxy and ingress configuration](#reverse-proxy-and-ingress-configuration) for header trust requirements.

Add the application's credentials and OAuth endpoints to the MCP server configuration:

```yaml title="config.yaml" showLineNumbers
mcp_servers:
  jira_mcp:
    url: https://mcp.example.com/mcp
    auth_type: oauth2
    oauth2_flow: authorization_code
    client_id: os.environ/JIRA_OAUTH_CLIENT_ID
    client_secret: os.environ/JIRA_OAUTH_CLIENT_SECRET
    authorization_url: https://idp.example.com/oauth2/authorize
    token_url: https://idp.example.com/oauth2/token
    scopes:
      - read:jira-work
```

Replace the example server URL, OAuth endpoints, and scopes with the values for your provider. `authorization_url` and `token_url` are optional when the upstream MCP server publishes OAuth metadata that LiteLLM can discover. Explicitly configured endpoints take precedence over conflicting discovered endpoints.

When the upstream server publishes no OAuth metadata at all (Microsoft Graph behind [ms-365-mcp-server](./mcp_servers/microsoft_365.md), for example), add `per_server_oauth_discovery: true` so LiteLLM publishes the discovery documents for `/{mcp_server_name}/mcp` itself, with its own `/{mcp_server_name}/authorize` and `/{mcp_server_name}/token` endpoints fronting the provider URLs above. It is accepted only with `auth_type: oauth2`, `oauth2_flow: authorization_code`, and no `delegate_auth_to_upstream`.

For static clients, LiteLLM handles `POST /{mcp_server_name}/register` locally. It returns the MCP server name as `client_id`, `dummy` as `client_secret`, and the client's submitted `redirect_uris`. LiteLLM uses the configured upstream credentials for authorization and token exchange.

#### Identify the MCP client's callback URL

Obtain the callback URL from the client's `redirect_uris` field in `POST /{mcp_server_name}/register` or its `redirect_uri` parameter in `GET /{mcp_server_name}/authorize`. Callback paths and ports can vary by client version and deployment.

For an origin mismatch, the `HTTP 400` response includes the submitted URL in `detail.redirect_uri`. The proxy also logs the rejected value as `MCP OAuth: rejecting redirect_uri '<value>'` at warning level.

The following examples summarize common callback configurations:

| Client or callback type | Callback example | LiteLLM configuration |
|-------------------------|------------------|-----------------------|
| Cursor | `cursor://anysphere.cursor-mcp/oauth/callback` | Included in the built-in trusted callbacks. |
| Desktop or command-line client using a loopback listener | `http://localhost:33418/callback` | Loopback callbacks are accepted on any port. |
| VS Code for the Web | `https://vscode.dev/redirect` or `https://insiders.vscode.dev/redirect` | Add `vscode.dev,insiders.vscode.dev` to `MCP_TRUSTED_REDIRECT_ORIGINS`. |
| Web application on a separate origin | `https://app.example.com/oauth/callback` | Add `app.example.com` to `MCP_TRUSTED_REDIRECT_ORIGINS`. |
| Native client using a custom URI scheme | `myclient://auth/callback` | Add the callback URI to `MCP_TRUSTED_NATIVE_REDIRECT_URIS`. |
| LiteLLM Admin UI | `<proxy origin>/ui/mcp/oauth/callback` | Accepted when the proxy's resolved public origin matches the UI origin. |

For clients that require additional trusted callbacks, set the applicable environment variable in the proxy deployment:

```bash
# Trusted HTTPS client hosts, with optional ports or wildcard subdomains.
export MCP_TRUSTED_REDIRECT_ORIGINS='app.example.com,*.tools.example.com'

# Trusted native client callback URIs.
export MCP_TRUSTED_NATIVE_REDIRECT_URIS='myclient://auth/callback'
```

`MCP_TRUSTED_REDIRECT_ORIGINS` accepts a comma-separated list of hosts or `host:port` entries. `MCP_TRUSTED_NATIVE_REDIRECT_URIS` accepts a comma-separated list of native callback URIs. Include the callback path; `myclient://auth/callback` and `myclient://auth/callback/` are distinct entries.

#### Redirect URI validation

For per-server static OAuth, `/{mcp_server_name}/authorize` validates the callback against these rules:

| Callback type | Requirements |
|---------------|--------------|
| Trusted native callback | Matches the built-in Cursor callback or an entry in `MCP_TRUSTED_NATIVE_REDIRECT_URIS`. |
| Loopback | Uses HTTP or HTTPS with `localhost`, an address in `127.0.0.0/8`, or `::1`. Any port and path are accepted. |
| Same origin | Uses the same scheme, host, and port as the proxy's resolved public origin. Default ports are normalized. |
| Additional trusted origin | Uses HTTPS and a host or `host:port` listed in `MCP_TRUSTED_REDIRECT_ORIGINS`. A wildcard such as `*.tools.example.com` matches subdomains, including `a.tools.example.com`, but excludes `tools.example.com` itself. |

Callback URLs must include a host and must not contain a fragment (`#...`), embedded credentials (`user:pass@host`), or a backslash in the host. Custom URI schemes require a trusted native callback entry. Native callback URIs must not contain a query string. HTTP and HTTPS callbacks may include a query string, which LiteLLM preserves when redirecting to the client.

#### Troubleshoot callback errors

When LiteLLM rejects a per-server callback, it returns `HTTP 400` with `detail.error` set to `invalid_request`. `detail.error_description` identifies the validation failure. Some responses also include `detail.hint` with configuration guidance; origin mismatch responses include `detail.redirect_uri`.

| Error or symptom | Resolution |
|------------------|------------|
| The upstream IdP reports a redirect URI mismatch. | Verify that the IdP application's registered callback is `<proxy origin>/callback` and that LiteLLM resolves the expected public origin. |
| LiteLLM rejects a callback on the proxy's public origin. | Set `PROXY_BASE_URL` or configure trusted forwarded headers. See [Reverse proxy and ingress configuration](#reverse-proxy-and-ingress-configuration). |
| LiteLLM rejects an HTTPS callback on a separate origin. | Add the approved client host, including its port when applicable, to `MCP_TRUSTED_REDIRECT_ORIGINS`. |
| LiteLLM rejects a custom URI scheme. | Add the client's callback URI to `MCP_TRUSTED_NATIVE_REDIRECT_URIS`. |
| LiteLLM reports that the callback contains a URL fragment. | Configure the client to use a callback URL without a fragment. |

For example, an origin mismatch response includes these fields:

```json title="Origin mismatch response (selected fields)"
{
  "detail": {
    "error": "invalid_request",
    "error_description": "redirect_uri origin (https://app.example.com) does not match the proxy origin. host/port: redirect_uri 'app.example.com' does not match the proxy origin",
    "redirect_uri": "https://app.example.com/oauth/callback"
  }
}
```

#### Gateway Dynamic Client Registration

The aggregate `/mcp` endpoint uses gateway-level registration through `POST /register`, `GET /authorize`, and `POST /token`. Each registration accepts one to four `redirect_uris`, with a maximum of 256 characters per URI.

For this flow, the `redirect_uri` supplied to `/authorize` must exactly match a registered value. An unregistered value returns `HTTP 400` with the following top-level JSON fields:

```json
{
  "error": "invalid_request",
  "error_description": "redirect_uri is not registered for this client"
}
```

Per-server static registration returns placeholder credentials and does not store a client-specific callback allowlist. Its authorization endpoint applies the per-server validation rules described above.

#### Verify the configuration

Use the following requests to verify discovery, static registration, and the authorization redirect. The examples use a proxy listening on `http://localhost:4000` with `PROXY_BASE_URL=https://llm.example.com` and the `jira_mcp` configuration above.

Retrieve the authorization server metadata:

```bash
curl -sS http://localhost:4000/.well-known/oauth-authorization-server/jira_mcp | jq .issuer
# "https://llm.example.com/jira_mcp"
```

The issuer's origin is `https://llm.example.com`, so the IdP callback URL is `https://llm.example.com/callback`.

Verify the static registration response:

```bash
curl -sS -X POST http://localhost:4000/jira_mcp/register \
  -H 'Content-Type: application/json' \
  -d '{"client_name":"my-mcp-client","redirect_uris":["http://localhost:33418/callback"]}'
# {"client_id":"jira_mcp","client_secret":"dummy","redirect_uris":["http://localhost:33418/callback"]}
```

Request authorization with the loopback callback:

```bash
curl -sS -o /dev/null -D - --get http://localhost:4000/jira_mcp/authorize \
  --data-urlencode 'response_type=code' \
  --data-urlencode 'client_id=jira_mcp' \
  --data-urlencode 'redirect_uri=http://localhost:33418/callback' \
  --data-urlencode 'state=example-state' \
  --data-urlencode 'code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM' \
  --data-urlencode 'code_challenge_method=S256'
```

The expected response is `HTTP 307 Temporary Redirect`. Its `Location` header points to the upstream authorization endpoint and includes the configured upstream `client_id` and the URL-encoded `redirect_uri=https://llm.example.com/callback`.

To verify an additional trusted origin, repeat the authorization request with `redirect_uri=https://app.example.com/oauth/callback`. Without a matching `MCP_TRUSTED_REDIRECT_ORIGINS` entry, the expected response is `HTTP 400`. Set `MCP_TRUSTED_REDIRECT_ORIGINS=app.example.com`, restart the proxy, and repeat the request; the expected response is `HTTP 307`.

### Reverse proxy and ingress configuration {#reverse-proxy-and-ingress-configuration}

If LiteLLM runs behind a TLS-terminating ingress (Kubernetes, ALB, nginx, Cloudflare, etc.), the proxy needs to know its public origin so the OAuth `authorize` endpoint can compare the browser-supplied `redirect_uri` (e.g. `https://llm.example.com/ui/mcp/oauth/callback`) against its own scheme + host + port. If the proxy resolves to its internal address (`http://<pod-ip>:4000`) the same-origin check fails and the **Connect** button on the MCP server page returns `400 Bad Request` with `{"detail":"invalid_request"}`.

The simplest and recommended fix is to set `PROXY_BASE_URL` to the exact origin users see in the address bar:

```bash
PROXY_BASE_URL=https://llm.example.com
```

Rules for the value:

- Full origin only: scheme + host (+ port if non-default).
- No trailing slash, no path component.
- Must match the address bar exactly. `https://llm.example.com` and `https://llm.example.com:443` are accepted as the same origin (the default port is normalized away), but `https://llm.example.com` will not match a browser running against `https://llm.example.com:8443`.

When `PROXY_BASE_URL` is set, LiteLLM uses it directly and skips the `X-Forwarded-*` trust path described below.

#### Origin resolution order

For MCP OAuth endpoints, LiteLLM resolves the proxy's public origin in this order:

1. **`PROXY_BASE_URL` env var** — used verbatim if set to a valid `http(s)` URL. Invalid values are ignored with a warning.
2. **`X-Forwarded-Proto` / `X-Forwarded-Host` / `X-Forwarded-Port`** — only honored when **both** [`use_x_forwarded_for`](./proxy/config_settings#general_settings---reference) is `true` **and** the request peer's IP falls inside [`mcp_trusted_proxy_ranges`](./proxy/config_settings#general_settings---reference). If `use_x_forwarded_for` is enabled without `mcp_trusted_proxy_ranges`, the headers are not trusted (there is no way to distinguish a trusted reverse proxy from a direct attacker).
3. **`request.base_url`** — the literal URL FastAPI sees on the request. For ingressed deployments this is typically `http://<internal-host>:4000` and will not match the browser origin.

If you cannot or do not want to set `PROXY_BASE_URL`, configure the X-Forwarded path explicitly:

```yaml title="config.yaml" showLineNumbers
general_settings:
  use_x_forwarded_for: true
  mcp_trusted_proxy_ranges:
    - "10.0.0.0/8"      # your ingress / load-balancer CIDR(s)
```

and verify your ingress sends `X-Forwarded-Proto`, `X-Forwarded-Host`, and (if non-default) `X-Forwarded-Port`. See [MCP OAuth troubleshooting](./mcp_troubleshoot#mcp-oauth-invalid-request) for the diagnostic curl.

#### Allowing additional first-party redirect_uri origins {#allowing-additional-first-party-redirect_uri-origins}

If a first-party OAuth client lives on a sister domain (for example, an internal web app on `app.example.com` registering against the MCP proxy on `llm.example.com`), set `MCP_TRUSTED_REDIRECT_ORIGINS` to allowlist its origin in addition to the proxy's own:

```bash
MCP_TRUSTED_REDIRECT_ORIGINS=app.example.com,*.tools.example.com
```

- Comma-separated list of `host` or `host:port` entries.
- HTTPS only. The allowlist path rejects any non-`https` `redirect_uri`.
- A `*.suffix` entry matches any strictly-deeper subdomain of `suffix` (`*.tools.example.com` matches `a.tools.example.com` but not `tools.example.com`).
- Loopback (`localhost`, `127.0.0.0/8`, `::1`) is always accepted regardless of this setting.

This is for first-party OAuth clients you control. For the standard ingress case, prefer `PROXY_BASE_URL`.

#### Why the same-origin check exists

The MCP proxy's `/v1/mcp/server/oauth/<server_id>/authorize` endpoint validates that the caller's `redirect_uri` shares scheme + host + port with the proxy's own public origin (or with one of the loopback / allowlisted entries above). The check exists to stop an attacker from phishing a logged-in admin into a link that bounces an authorization code, for an upstream OAuth-protected MCP server such as GitHub or Slack, through an attacker-controlled host. Same-origin (plus an explicit ops allowlist) is the threat-model-safe equivalent of the loopback-only rule used for native MCP clients.

`PROXY_BASE_URL` is the right escape hatch for ingressed deployments because the operator is declaring the proxy's true public origin out of band, rather than asking the proxy to infer it from headers an attacker might be able to set. The check itself is not relaxed.

## Machine-to-Machine (M2M) Auth

LiteLLM automatically fetches, caches, and refreshes OAuth2 tokens using the `client_credentials` grant. No manual token management required.

### Setup

You can configure M2M OAuth via the LiteLLM UI or `config.yaml`.

### UI Setup

Navigate to the **MCP Servers** page and click **+ Add New MCP Server**.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/d1f1e89c-a789-4975-8846-b15d9821984a/ascreenshot_630800e00a2e4b598baabfc25efbabd3_text_export.jpeg)

Enter a name for your server and select **HTTP** as the transport type.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/2008c9d6-6093-4121-beab-1e52c71376aa/ascreenshot_516ffd6c7b524465a253a56048c3d228_text_export.jpeg)

Paste the MCP server URL.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/b0ee8b7d-6de8-492b-8962-287987feec29/ascreenshot_b3efca82078a4c6bb1453c58161909f9_text_export.jpeg)

Under **Authentication**, select **OAuth**.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/e1597814-ff8e-40b9-9d7b-864dcdbe0910/ascreenshot_2097612712264d8f9e553f7ca9175fb0_text_export.jpeg)

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/f6ea5694-f28a-4bc3-9c9a-bb79f199bd65/ascreenshot_9be839f55b1b4f96bfe24030ba2c7f8d_text_export.jpeg)

Choose **Machine-to-Machine (M2M)** as the OAuth flow type. This is for server-to-server authentication using the `client_credentials` grant, with no browser interaction.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/9853310c-1d86-4628-bad1-7a391eca0e4d/ascreenshot_f302a286fa264fdd8d56db53b8f9395c_text_export.jpeg)

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/df64dc65-ef86-475d-adaf-12e227d5e873/ascreenshot_9e2f41d43a76435f918a00b52ffcc639_text_export.jpeg)

Fill in the **Client ID** and **Client Secret** provided by your OAuth provider.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/0de5a7bd-9898-4fc7-8843-b23dd5aac47f/ascreenshot_b9087aaa81a14b5b9c199929efc4a563_text_export.jpeg)

Enter the **Token URL**, the endpoint LiteLLM will call to fetch access tokens using `client_credentials`.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/0aea70f1-558c-4dca-91bc-1175fe1ddc89/ascreenshot_b3fcf8a1287e4e2d9a3d67c4a29f7bff_text_export.jpeg)

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/e842ef09-1fd7-47a6-909b-252d389f0abc/ascreenshot_2a87dad3624847e7ac370591d1d1aedd_text_export.jpeg)

Scroll down and review the server URL and all fields, then click **Create MCP Server**.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/0857712b-4b53-40f8-8c1f-a4c72edaa644/ascreenshot_47be3fcd5de64ed391f70c1fb74a8bfc_text_export.jpeg)

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/9d961765-955f-4905-a3dc-1a446aa3b2cc/ascreenshot_43fd39d014224564bc6b35aced1fb6d3_text_export.jpeg)

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/3825d5fa-8fd1-4e71-b090-77ff0259c3f6/ascreenshot_2509a7ebd9bf421eb0e82f2553566745_text_export.jpeg)

Once created, open the server and navigate to the **MCP Tools** tab to verify that LiteLLM can connect and list available tools.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/8107e27b-5072-4675-8fd6-89b47692b1bd/ascreenshot_f774bc76138f430d808fb4482ebfcdca_text_export.jpeg)

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/ce94bb7b-c81b-4396-9939-178efb2cdfce/ascreenshot_28b838ab6ae34c76858454555c4c1d79_text_export.jpeg)

Select a tool (e.g. **echo**) to test it. Fill in the required parameters and click **Call Tool**.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/c459c1d3-ec29-4211-9c28-37fbe7783bbc/ascreenshot_e9b138b3c2cc4440bb1a6f42ac7ae861_text_export.jpeg)

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/5438ac60-e0ac-4a79-bf6f-5594f160d3b5/ascreenshot_9133a17d26204c46bce497e74685c483_text_export.jpeg)

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/a8f6821b-3982-4b4d-9b25-70c8aff5ac31/ascreenshot_28d474d0e62545a482cff6128527883a_text_export.jpeg)

LiteLLM automatically fetches an OAuth token behind the scenes and calls the tool. The result confirms the M2M OAuth flow is working end-to-end.

![](https://colony-recorder.s3.amazonaws.com/files/2026-02-10/c3924549-a949-48d1-ac67-ab4c30475859/ascreenshot_8f6eca9d717f45478d50a881bd244bb3_text_export.jpeg)

### Config.yaml Setup

```yaml title="config.yaml" showLineNumbers
mcp_servers:
  my_mcp_server:
    url: "https://my-mcp-server.com/mcp"
    auth_type: oauth2
    oauth2_flow: client_credentials
    client_id: os.environ/MCP_CLIENT_ID
    client_secret: os.environ/MCP_CLIENT_SECRET
    token_url: "https://auth.example.com/oauth/token"
    scopes: ["mcp:read", "mcp:write"]  # optional
```

### Sending the token on a different header

By default the token LiteLLM resolves goes out as `Authorization: Bearer <token>`, which is what
almost every MCP server expects. Some deployments put the MCP server behind an API gateway that
reads its own credential from a private header, and the server behind the gateway still wants its
own bearer on `Authorization`. That needs two credentials on the same request.

Set `upstream_token_header` to name the header the resolved token should use. Anything you configure
under `static_headers` is then left alone, so a second credential reaches the server behind the
gateway untouched.

```yaml title="config.yaml" showLineNumbers
mcp_servers:
  my_mcp_server:
    url: "https://my-mcp-server.com/mcp"
    auth_type: oauth2
    oauth2_flow: client_credentials
    client_id: os.environ/MCP_CLIENT_ID
    client_secret: os.environ/MCP_CLIENT_SECRET
    token_url: "https://auth.example.com/oauth/token"
    upstream_token_header: "esb-oauth"
    static_headers:
      Authorization: "Bearer os.environ/UPSTREAM_MCP_TOKEN"
```

Each request to the MCP server then carries both:

```
esb-oauth: Bearer <the token LiteLLM minted>
Authorization: Bearer <the token you configured>
```

Leaving `upstream_token_header` unset keeps the default, so existing servers are unaffected. The
value must be a valid HTTP header name; the proxy refuses to start on a malformed one, and the
management API rejects it with a 400.

In the UI the same setting is the **Token Header** field in the OAuth section of the MCP server
form, and it applies to the interactive flow and the token-exchange modes as well as M2M.

### How It Works

1. On first MCP request, LiteLLM POSTs to `token_url` with `grant_type=client_credentials`
2. The access token is cached in-memory with TTL = `expires_in - 60s`
3. Subsequent requests reuse the cached token
4. When the token expires, LiteLLM fetches a new one automatically

```mermaid
sequenceDiagram
    participant Client as Client
    participant LiteLLM as LiteLLM Proxy
    participant Auth as Authorization Server
    participant MCP as MCP Server

    Client->>LiteLLM: MCP request + LiteLLM API key
    LiteLLM->>Auth: POST /oauth/token (client_credentials)
    Auth->>LiteLLM: access_token (expires_in: 3600)
    LiteLLM->>MCP: MCP request + Bearer token
    MCP-->>LiteLLM: MCP response
    LiteLLM-->>Client: MCP response

    Note over LiteLLM: Token cached for subsequent requests
    Client->>LiteLLM: Next MCP request
    LiteLLM->>MCP: MCP request + cached Bearer token
    MCP-->>LiteLLM: MCP response
    LiteLLM-->>Client: MCP response
```

### Test with Mock Server

Use [BerriAI/mock-oauth2-mcp-server](https://github.com/BerriAI/mock-oauth2-mcp-server) to test locally:

```bash title="Terminal 1 - Start mock server" showLineNumbers
uv add fastapi uvicorn
python mock_oauth2_mcp_server.py  # starts on :8765
```

```yaml title="config.yaml" showLineNumbers
mcp_servers:
  test_oauth2:
    url: "http://localhost:8765/mcp"
    auth_type: oauth2
    oauth2_flow: client_credentials
    client_id: "test-client"
    client_secret: "test-secret"
    token_url: "http://localhost:8765/oauth/token"
```

```bash title="Terminal 2 - Start proxy and test" showLineNumbers
litellm --config config.yaml --port 4000

# See MCP REST API guide for full examples (server_id, tool naming, common errors)
# https://docs.litellm.ai/docs/mcp_rest_api

curl http://localhost:4000/mcp-rest/tools/list \
  -H "Authorization: Bearer $LITELLM_API_KEY"

curl http://localhost:4000/mcp-rest/tools/call \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $LITELLM_API_KEY" \
  -d '{
    "server_id": "test_oauth2",
    "name": "echo",
    "arguments": {"message": "hello"}
  }'
```

### Config Reference

| Field | Required | Description |
|-------|----------|-------------|
| `auth_type` | Yes | Must be `oauth2`. For RFC 8693 On-Behalf-Of, use `oauth2_token_exchange` instead — see [MCP OBO Auth](./mcp_obo_auth.md). |
| `oauth2_flow` | Yes | Flow selector. One of `"client_credentials"` (M2M) or `"authorization_code"` (interactive PKCE, including `delegate_auth_to_upstream`). Required for every `auth_type: oauth2` server in `config.yaml`; the proxy refuses to start if it is missing or invalid. Servers created through the UI get it from the OAuth flow type selector. Only legacy database rows created before this field existed fall back to inference from field shape at request time; config entries are never inferred. |
| `client_id` | Yes for M2M, optional for interactive | OAuth2 client ID. Required for `client_credentials`. For interactive flows, can be obtained via Dynamic Client Registration (RFC 7591) at `POST /{server_name}/register` if the upstream supports it. Supports `os.environ/VAR_NAME`. |
| `client_secret` | Yes for M2M, optional for interactive | OAuth2 client secret. Same applicability as `client_id`. Supports `os.environ/VAR_NAME`. |
| `token_url` | Yes for M2M, optional for interactive | Token endpoint URL. LiteLLM POSTs to this for `client_credentials` and for the authorization-code exchange. |
| `authorization_url` | Interactive only | Upstream authorization endpoint. When present, LiteLLM treats the server as interactive PKCE and proxies `GET /{server_name}/authorize` to this URL. |
| `registration_url` | Optional | Upstream Dynamic Client Registration endpoint (RFC 7591). When present, `POST /{server_name}/register` proxies through to this URL. |
| `scopes` | No | List of scopes to request. For M2M, joined into the `scope` parameter on the token request. For interactive, forwarded on the authorize request. |
| `token_validation` | No | Dict of key-value rules checked against the OAuth token response after the `/token` exchange. Any rule mismatch fails the exchange with `token_validation_failed`. Useful for asserting a tenant claim like `{"team.enterprise_id": "T12345"}`. |
| `token_storage_ttl_seconds` | No | Override the TTL for the per-user token cache (interactive flow). If unset, LiteLLM uses `expires_in - buffer` from the token response. |

## Debugging OAuth

When the LiteLLM proxy is hosted remotely and you cannot access server logs, enable **debug headers** to get masked authentication diagnostics in the HTTP response.

### Enable Debug Mode

Add the `x-litellm-mcp-debug: true` header to your MCP client request.

**Claude Code:**

```bash
claude mcp add --transport http litellm_proxy http://proxy.example.com/atlassian_mcp/mcp \
  --header "x-litellm-api-key: Bearer sk-..." \
  --header "x-litellm-mcp-debug: true"
```

**curl:**

```bash
curl -X POST http://localhost:4000/atlassian_mcp/mcp \
  -H "Content-Type: application/json" \
  -H "x-litellm-api-key: Bearer sk-..." \
  -H "x-litellm-mcp-debug: true" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

### Reading the Debug Response Headers

The response includes these headers (all sensitive values are masked):

| Header | Description |
|--------|-------------|
| `x-mcp-debug-inbound-auth` | Which inbound auth headers were present. |
| `x-mcp-debug-oauth2-token` | The OAuth2 token (masked). Shows `SAME_AS_LITELLM_KEY` if the LiteLLM key is leaking. |
| `x-mcp-debug-auth-resolution` | Which auth method was used: `oauth2-passthrough`, `m2m-client-credentials`, `per-request-header`, `static-token`, or `no-auth`. |
| `x-mcp-debug-outbound-url` | The upstream MCP server URL. |
| `x-mcp-debug-server-auth-type` | The `auth_type` configured on the server. |

**Example, healthy OAuth2 passthrough:**

```
x-mcp-debug-inbound-auth: x-litellm-api-key=Bearer****1234; authorization=Bearer****ef01
x-mcp-debug-oauth2-token: Bearer****ef01
x-mcp-debug-auth-resolution: oauth2-passthrough
x-mcp-debug-outbound-url: https://mcp.atlassian.com/v1/mcp
x-mcp-debug-server-auth-type: oauth2
```

**Example, LiteLLM key leaking (misconfigured):**

```
x-mcp-debug-inbound-auth: authorization=Bearer****1234
x-mcp-debug-oauth2-token: Bearer****1234 (SAME_AS_LITELLM_KEY - likely misconfigured)
x-mcp-debug-auth-resolution: oauth2-passthrough
x-mcp-debug-outbound-url: https://mcp.atlassian.com/v1/mcp
x-mcp-debug-server-auth-type: oauth2
```

### Common Issues

#### LiteLLM API key leaking to the MCP server

**Symptom:** `x-mcp-debug-oauth2-token` shows `SAME_AS_LITELLM_KEY`.

The `Authorization` header carries the LiteLLM API key instead of an OAuth2 token. The OAuth2 flow never ran because the client already had an `Authorization` header set.

**Fix:** Move the LiteLLM key to `x-litellm-api-key`:

```bash
# WRONG — blocks OAuth2 discovery
claude mcp add --transport http my_server http://proxy/server/mcp \
    --header "Authorization: Bearer sk-..."

# CORRECT — LiteLLM key in dedicated header, Authorization free for OAuth2
claude mcp add --transport http my_server http://proxy/server/mcp \
    --header "x-litellm-api-key: Bearer sk-..."
```

#### No OAuth2 token present

**Symptom:** `x-mcp-debug-oauth2-token` shows `(none)` and `x-mcp-debug-auth-resolution` shows `no-auth`.

Check that:
1. The `Authorization` header is NOT set as a static header in the client config.
2. The MCP server in LiteLLM config has `auth_type: oauth2`.
3. The `.well-known/oauth-protected-resource` endpoint returns valid metadata.

#### M2M token used instead of user token

**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.

## Passthrough and Delegated Upstream OAuth

For servers where the client already authenticates directly against the upstream's own OAuth issuer, LiteLLM can forward the client's upstream token instead of managing tokens itself. The transparent `auth_type: true_passthrough` mode, the admission-gated `auth_type: oauth_delegate` mode, and the legacy `delegate_auth_to_upstream` flag are covered in [MCP OAuth Passthrough](./mcp_oauth_passthrough.md). That page also documents the `dcr_bridge` flag for OAuth-only clients such as OpenCode, Claude Code, Cursor, and Claude Desktop, where the gateway hosts registration and sign-in so the client can connect with a single OAuth flow.

## Related pages

- [MCP Per-User and Per-Key Upstream Credentials](https://docs.litellm.ai/docs/mcp_per_user_auth.md)
- [MCP OAuth Passthrough](https://docs.litellm.ai/docs/mcp_oauth_passthrough.md)
