litellm - ✅(Solved) Fix [Bug]: MCP OAuth 3LO fails when `token_url` is auto-discovered because falls back to 2LO incorrectly [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#23221Fetched 2026-04-08 00:37:59
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
labeled ×3commented ×1cross-referenced ×1

Error Message

This makes needs_user_oauth_token = False, so the server is not skipped during startup tool pre-loading. LiteLLM then attempts a client_credentials grant against GitHub's token endpoint — which GitHub does not support (only authorization code). GitHub returns an empty or non-JSON error body, causing json.JSONDecodeError: Expecting value: line 1 column 1 (char 0). The tool list is silently dropped to [].

Root Cause

During startup, _load_mcp_servers_from_config runs _descovery_metadata() against the MCP server URL whenever auth_type == oauth2. For GitHub's MCP endpoint, this follows the WWW-Authenticate challenge and auto-discovers the token URL (https://github.com/login/oauth/token), storing it in resolved_token_url.

The relevant code in mcp_server_manager.py:

resolved_token_url = server_config.get("token_url") or (
    mcp_oauth_metadata.token_url if mcp_oauth_metadata else None
)

Since client_id, client_secret, and now an auto-discovered token_url are all set, MCPServer.has_client_credentials evaluates to True:

@property
def has_client_credentials(self) -> bool:
    return bool(self.client_id and self.client_secret and self.token_url)

This makes needs_user_oauth_token = False, so the server is not skipped during startup tool pre-loading. LiteLLM then attempts a client_credentials grant against GitHub's token endpoint — which GitHub does not support (only authorization code). GitHub returns an empty or non-JSON error body, causing json.JSONDecodeError: Expecting value: line 1 column 1 (char 0). The tool list is silently dropped to [].

The core issue is that has_client_credentials cannot distinguish between:

  • 2LO intent: client_id + client_secret + explicit token_url in config → use client_credentials grant ✅
  • 3LO intent: client_id + client_secret in config (no token_url) → token_url auto-discovered and incorrectly triggers M2M ❌

Fix Action

Fix / Workaround

Current Workaround

I'm patching mcp_server_manager.py at Docker build time. The fix prevents the auto-discovered token_url from being stored when client_id + client_secret are explicitly set in config but token_url is not — which is the unambiguous signal that the user intends 3LO, not 2LO:

Alternatively, a cleaner longer-term fix would be to add an explicit oauth_flow: authorization_code | client_credentials field to the MCP server config, removing the ambiguity entirely. But the above patch is minimal and backward-compatible — it only changes behavior when a user has client_id + client_secret without an explicit token_url, which is the documented 3LO setup.

PR fix notes

PR #23255: Fixing mcp issue

Description (problem / solution / changelog)

Relevant issues

https://github.com/BerriAI/litellm/issues/23221

When client_id + client_secret are configured without an explicit token_url (the documented 3LO setup), OAuth metadata discovery was populating token_url, causing has_client_credentials to return True and incorrectly triggering the client_credentials grant. Tools appeared connected but returned empty.

Fix: skip auto-discovery of token_url when PKCE credentials are present but token_url is absent from config — the unambiguous signal of 3LO intent.

Changed files

  • litellm/proxy/_experimental/mcp_server/mcp_server_manager.py (modified, +19/-3)
  • tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py (modified, +148/-3)

Code Example

Authentication successful. Connected to <server>.

---

Fetching OAuth2 client_credentials token for MCP server <server_id>
Failed to get tools from server github_mcp: Expecting value: line 1 column 1 (char 0)

---

mcp_servers:
  github_mcp:
    url: "https://api.githubcopilot.com/mcp"
    transport: "http"
    auth_type: oauth2
    client_id: os.environ/GITHUB_OAUTH_CLIENT_ID
    client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET
    allowed_tools: ["list_issues", "list_tags", "list_branches"]
    allow_all_keys: true

---

/mcp
Authentication successful. Connected to github_mcp.

---

resolved_token_url = server_config.get("token_url") or (
    mcp_oauth_metadata.token_url if mcp_oauth_metadata else None
)

---

@property
def has_client_credentials(self) -> bool:
    return bool(self.client_id and self.client_secret and self.token_url)

---

# Only use auto-discovered token_url for M2M when no PKCE credentials
# (client_id + client_secret) are explicitly set in config.
_explicit_token_url = server_config.get("token_url")
_has_pkce_creds = bool(
    server_config.get("client_id") and server_config.get("client_secret")
)
resolved_token_url = _explicit_token_url or (
    (mcp_oauth_metadata.token_url if mcp_oauth_metadata else None)
    if not _has_pkce_creds
    else None
)

---

# Before (line ~293):
resolved_token_url = server_config.get("token_url") or (
    mcp_oauth_metadata.token_url if mcp_oauth_metadata else None
)

# After:
_explicit_token_url = server_config.get("token_url")
_has_pkce_creds = bool(
    server_config.get("client_id") and server_config.get("client_secret")
)
resolved_token_url = _explicit_token_url or (
    (mcp_oauth_metadata.token_url if mcp_oauth_metadata else None)
    if not _has_pkce_creds
    else None
)
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

When an MCP server is configured with auth_type: oauth2 and only client_id and client_secret for an interactive OAuth 3LO flow (i.e., no token_url explicitly provided), LiteLLM incorrectly treats the grant type as 2LO instead of 3LO when the token_url is auto-discovered.

At startup, LiteLLM performs MCP server discovery and attempts to pre load tools. During this process it follows the WWW-Authenticate challenge returned by the MCP endpoint and auto discovers the OAuth token_url. Because client_id, client_secret, and the newly discovered token_url are all present, LiteLLM incorrectly assumes the intended grant type is 2LO instead of 3LO.

It was observed that the interactive OAuth flow itself still works. Clients such as Claude Code can authenticate successfully and display a message like:

Authentication successful. Connected to <server>.

However, when the model later attempts to list or invoke tools from that MCP server, LiteLLM returns an empty tool set because startup discovery already failed.

With debug logging enabled (LITELLM_LOG=DEBUG), the logs show LiteLLM Proxy attempting the incorrect grant type and then failing while parsing the response:

Fetching OAuth2 client_credentials token for MCP server <server_id>
Failed to get tools from server github_mcp: Expecting value: line 1 column 1 (char 0)

As a result, the MCP server appears connected and authenticated, but no tools are available to the model.

Environment

  • LiteLLM version: v1.81.12-stable.2 (Docker image ghcr.io/berriai/litellm-database:main-v1.81.12-stable.2)
  • MCP server: GitHub Copilot MCP — https://api.githubcopilot.com/mcp
  • Auth flow intended: Interactive OAuth 3LO, using a pre-registered GitHub OAuth App's client_id and client_secret
  • Client: Claude Code CLI (claude-cli/2.1.72)
  • Deploy: EKS

Steps to Reproduce

  1. Configure a GitHub MCP server in config.yaml using the documented interactive OAuth setup (no token_url):
mcp_servers:
  github_mcp:
    url: "https://api.githubcopilot.com/mcp"
    transport: "http"
    auth_type: oauth2
    client_id: os.environ/GITHUB_OAUTH_CLIENT_ID
    client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET
    allowed_tools: ["list_issues", "list_tags", "list_branches"]
    allow_all_keys: true
  1. Start the LiteLLM proxy.
  2. In Claude Code, add the MCP server and authenticate via the 3LO flow:
    /mcp
    ⎿  Authentication successful. Connected to github_mcp.
  3. Ask the model to use a GitHub tool (e.g. list issues).
  4. Observe that the model reports no tools available from the server.
  5. Check LiteLLM Proxy logs — you'll see the client_credentials grant attempt and the JSON parse failure.

Root Cause

During startup, _load_mcp_servers_from_config runs _descovery_metadata() against the MCP server URL whenever auth_type == oauth2. For GitHub's MCP endpoint, this follows the WWW-Authenticate challenge and auto-discovers the token URL (https://github.com/login/oauth/token), storing it in resolved_token_url.

The relevant code in mcp_server_manager.py:

resolved_token_url = server_config.get("token_url") or (
    mcp_oauth_metadata.token_url if mcp_oauth_metadata else None
)

Since client_id, client_secret, and now an auto-discovered token_url are all set, MCPServer.has_client_credentials evaluates to True:

@property
def has_client_credentials(self) -> bool:
    return bool(self.client_id and self.client_secret and self.token_url)

This makes needs_user_oauth_token = False, so the server is not skipped during startup tool pre-loading. LiteLLM then attempts a client_credentials grant against GitHub's token endpoint — which GitHub does not support (only authorization code). GitHub returns an empty or non-JSON error body, causing json.JSONDecodeError: Expecting value: line 1 column 1 (char 0). The tool list is silently dropped to [].

The core issue is that has_client_credentials cannot distinguish between:

  • 2LO intent: client_id + client_secret + explicit token_url in config → use client_credentials grant ✅
  • 3LO intent: client_id + client_secret in config (no token_url) → token_url auto-discovered and incorrectly triggers M2M ❌

Current Workaround

I'm patching mcp_server_manager.py at Docker build time. The fix prevents the auto-discovered token_url from being stored when client_id + client_secret are explicitly set in config but token_url is not — which is the unambiguous signal that the user intends 3LO, not 2LO:

# Only use auto-discovered token_url for M2M when no PKCE credentials
# (client_id + client_secret) are explicitly set in config.
_explicit_token_url = server_config.get("token_url")
_has_pkce_creds = bool(
    server_config.get("client_id") and server_config.get("client_secret")
)
resolved_token_url = _explicit_token_url or (
    (mcp_oauth_metadata.token_url if mcp_oauth_metadata else None)
    if not _has_pkce_creds
    else None
)

This keeps has_client_credentials = False for PKCE-intended servers, which correctly sets needs_user_oauth_token = True and skips M2M tool pre-loading at startup, letting the per-user token flow work as intended.

Proposed Fix

Change to _load_mcp_servers_from_config in mcp_server_manager.py:

# Before (line ~293):
resolved_token_url = server_config.get("token_url") or (
    mcp_oauth_metadata.token_url if mcp_oauth_metadata else None
)

# After:
_explicit_token_url = server_config.get("token_url")
_has_pkce_creds = bool(
    server_config.get("client_id") and server_config.get("client_secret")
)
resolved_token_url = _explicit_token_url or (
    (mcp_oauth_metadata.token_url if mcp_oauth_metadata else None)
    if not _has_pkce_creds
    else None
)

Alternatively, a cleaner longer-term fix would be to add an explicit oauth_flow: authorization_code | client_credentials field to the MCP server config, removing the ambiguity entirely. But the above patch is minimal and backward-compatible — it only changes behavior when a user has client_id + client_secret without an explicit token_url, which is the documented 3LO setup.

What part of LiteLLM is this about?

Proxy

What LiteLLM version are you on ?

v1.81.12

extent analysis

Fix Plan

To resolve the issue, you need to modify the _load_mcp_servers_from_config function in mcp_server_manager.py. The goal is to correctly distinguish between 2LO and 3LO intents based on the presence of client_id, client_secret, and an explicit token_url in the config.

Here are the steps:

  • Open mcp_server_manager.py.
  • Locate the _load_mcp_servers_from_config function.
  • Replace the existing logic for determining resolved_token_url with the following code:
_explicit_token_url = server_config.get("token_url")
_has_pkce_creds = bool(
    server_config.get("client_id") and server_config.get("client_secret")
)
resolved_token_url = _explicit_token_url or (
    (mcp_oauth_metadata.token_url if mcp_oauth_metadata else None)
    if not _has_pkce_creds
    else None
)

This change ensures that when client_id and client_secret are provided without an explicit token_url, the auto-discovered token_url is not used, thus preventing the incorrect assumption of a 2LO flow.

Verification

After applying the fix:

  1. Restart the LiteLLM proxy.
  2. Attempt to connect to the MCP server using the 3LO flow through Claude Code.
  3. Verify that the model can successfully list and invoke tools from the MCP server.
  4. Check the LiteLLM Proxy logs to ensure that the client_credentials grant attempt is not made and that there are no JSON parse failures.

Extra Tips

  • Consider adding an explicit oauth_flow field to the MCP server config for a cleaner, long-term solution.
  • Ensure that the client_id and client_secret are correctly set in the environment variables (GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET) to avoid any authentication issues.
  • Monitor the logs and user feedback to catch any potential regressions or issues related to the OAuth flow.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING