Skip to main content

MCP OAuth

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:

Flowoauth2_flowUse CaseHow It Works
Interactive (PKCE)authorization_codeUser-facing apps (Claude Code, Cursor)Browser-based consent, per-user tokens
Machine-to-Machine (M2M)client_credentialsBackend services, CI/CD, automated agentsclient_credentials grant, proxy-managed tokens
On-Behalf-Of (OBO)n/a (uses auth_type: oauth2_token_exchange)User-context tool calls to protected MCP serversLiteLLM exchanges the caller token for a scoped MCP token. See MCP OBO Auth.
Passthrough (transparent)n/a (uses auth_type: true_passthrough)Client already holds the upstream token; LiteLLM adds no auth of its ownForwards the client's Authorization verbatim, no LiteLLM admission. See MCP OAuth Passthrough
Delegated upstream OAuthn/a (uses auth_type: oauth_delegate)LiteLLM admits the caller; the upstream owns tool authorizationLiteLLM admission plus a separate forwarded upstream bearer, keeps spend and rate limits. See MCP OAuth Passthrough

Interactive OAuth (PKCE)​

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

Setup​

config.yaml
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

How It Works​

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 for additional reference.

Redirect URLs for static OAuth clients​

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:

CallbackPurposeConfiguration
LiteLLM callbackReceives the authorization response from the upstream IdP.Register <proxy origin>/callback in the IdP application's Redirect URI or Callback URL field.
MCP client callbackReturns 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:

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 for header trust requirements.

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

config.yaml
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.

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 typeCallback exampleLiteLLM configuration
Cursorcursor://anysphere.cursor-mcp/oauth/callbackIncluded in the built-in trusted callbacks.
Desktop or command-line client using a loopback listenerhttp://localhost:33418/callbackLoopback callbacks are accepted on any port.
VS Code for the Webhttps://vscode.dev/redirect or https://insiders.vscode.dev/redirectAdd vscode.dev,insiders.vscode.dev to MCP_TRUSTED_REDIRECT_ORIGINS.
Web application on a separate originhttps://app.example.com/oauth/callbackAdd app.example.com to MCP_TRUSTED_REDIRECT_ORIGINS.
Native client using a custom URI schememyclient://auth/callbackAdd the callback URI to MCP_TRUSTED_NATIVE_REDIRECT_URIS.
LiteLLM Admin UI<proxy origin>/ui/mcp/oauth/callbackAccepted 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:

# 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 typeRequirements
Trusted native callbackMatches the built-in Cursor callback or an entry in MCP_TRUSTED_NATIVE_REDIRECT_URIS.
LoopbackUses HTTP or HTTPS with localhost, an address in 127.0.0.0/8, or ::1. Any port and path are accepted.
Same originUses the same scheme, host, and port as the proxy's resolved public origin. Default ports are normalized.
Additional trusted originUses 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 symptomResolution
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.
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:

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:

{
"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:

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:

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:

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​

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:

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 is true and the request peer's IP falls inside mcp_trusted_proxy_ranges. 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:

config.yaml
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 for the diagnostic curl.

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:

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.

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

Paste the MCP server URL.

Under Authentication, select OAuth.

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.

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

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

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

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

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

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.

Config.yaml Setup​

config.yaml
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.

config.yaml
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

Test with Mock Server​

Use BerriAI/mock-oauth2-mcp-server to test locally:

Terminal 1 - Start mock server
uv add fastapi uvicorn
python mock_oauth2_mcp_server.py # starts on :8765
config.yaml
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"
Terminal 2 - Start proxy and test
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​

FieldRequiredDescription
auth_typeYesMust be oauth2. For RFC 8693 On-Behalf-Of, use oauth2_token_exchange instead — see MCP OBO Auth.
oauth2_flowYesFlow 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_idYes for M2M, optional for interactiveOAuth2 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_secretYes for M2M, optional for interactiveOAuth2 client secret. Same applicability as client_id. Supports os.environ/VAR_NAME.
token_urlYes for M2M, optional for interactiveToken endpoint URL. LiteLLM POSTs to this for client_credentials and for the authorization-code exchange.
authorization_urlInteractive onlyUpstream authorization endpoint. When present, LiteLLM treats the server as interactive PKCE and proxies GET /{server_name}/authorize to this URL.
registration_urlOptionalUpstream Dynamic Client Registration endpoint (RFC 7591). When present, POST /{server_name}/register proxies through to this URL.
scopesNoList of scopes to request. For M2M, joined into the scope parameter on the token request. For interactive, forwarded on the authorize request.
token_validationNoDict 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_secondsNoOverride 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:

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:

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

HeaderDescription
x-mcp-debug-inbound-authWhich inbound auth headers were present.
x-mcp-debug-oauth2-tokenThe OAuth2 token (masked). Shows SAME_AS_LITELLM_KEY if the LiteLLM key is leaking.
x-mcp-debug-auth-resolutionWhich auth method was used: oauth2-passthrough, m2m-client-credentials, per-request-header, static-token, or no-auth.
x-mcp-debug-outbound-urlThe upstream MCP server URL.
x-mcp-debug-server-auth-typeThe 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:

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