Skip to content

fix(mcp): resolve OAuth2 root endpoints returning "MCP server not found"#20784

Merged
ishaan-jaff merged 1 commit intoBerriAI:mainfrom
michelligabriele:fix/mcp-oauth2-root-endpoint-resolution
Feb 10, 2026
Merged

fix(mcp): resolve OAuth2 root endpoints returning "MCP server not found"#20784
ishaan-jaff merged 1 commit intoBerriAI:mainfrom
michelligabriele:fix/mcp-oauth2-root-endpoint-resolution

Conversation

@michelligabriele
Copy link
Collaborator

When MCP SDK hits root-level /register, /authorize, /token without server name prefix, auto-resolve to the single configured OAuth2 server. Also fix WWW-Authenticate header to use correct public URL behind reverse proxy.

Relevant issues

Follow-up to #20602 — separate bug reported by the same user.

Pre-Submission checklist

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

Type

🐛 Bug Fix

Changes

Problem

After connecting an MCP client to a LiteLLM proxy behind a reverse proxy (e.g. https://llm.example.com/mcp), the OAuth2 flow fails with {"detail":"MCP server not found"} at the /authorize endpoint. The customer's URL showed: /authorize?client_id=dummy_client — the MCP SDK was hitting root-level OAuth endpoints without the server name prefix.

Root Cause (3 issues chained together)

  1. server.pyWWW-Authenticate header used str(request.base_url) (internal URL like http://0.0.0.0:4000) instead of get_request_base_url(request) (public URL). Behind a reverse proxy, the MCP SDK can't follow the internal URL and falls back to root-level OAuth discovery.

  2. discoverable_endpoints.py /register — Root /register (no server name prefix) immediately returned client_id: "dummy_client" without trying to resolve the actual server.

  3. discoverable_endpoints.py /authorizelookup_name = mcp_server_name or client_id resolved to "dummy_client" which doesn't match any configured server → 404.

Fix

  • Added _resolve_oauth2_server_for_root_endpoints() helper that auto-resolves to the single configured OAuth2 server when root-level endpoints are hit without a server name prefix. Returns None when 0 or 2+ OAuth2 servers exist (ambiguous case).
  • Applied the resolver to /register, /authorize, /token endpoints and both _build_oauth_authorization_server_response() and _build_oauth_protected_resource_response().
  • Fixed WWW-Authenticate header in server.py to use get_request_base_url(request) for correct public URL behind reverse proxy.

E2E Verification (via ngrok → LiteLLM proxy → Atlassian MCP)

  • Test 1: Root discovery (/.well-known/oauth-authorization-server) now returns endpoints with /atlassian_mcp/ prefix
  • Test 2: Root /register returns client_id: "atlassian_mcp" instead of "dummy_client"
  • Test 3: Root /authorize?client_id=dummy_client returns 307 redirect to Atlassian instead of 404
Screenshot 2026-02-09 alle 22 55 48

Tests added

5 new tests in tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py:

  • test_authorize_root_resolves_single_oauth2_server
  • test_authorize_root_fails_with_multiple_oauth2_servers
  • test_token_root_resolves_single_oauth2_server
  • test_register_root_resolves_single_oauth2_server
  • test_discovery_root_includes_server_name_prefix

When MCP SDK hits root-level /register, /authorize, /token without
server name prefix, auto-resolve to the single configured OAuth2
server. Also fix WWW-Authenticate header to use correct public URL
behind reverse proxy.
@vercel
Copy link

vercel bot commented Feb 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Feb 9, 2026 9:58pm

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 9, 2026

Greptile Overview

Greptile Summary

Fixed OAuth2 flow failing with "MCP server not found" when MCP SDK hits root-level endpoints (/register, /authorize, /token) without server name prefix. Added _resolve_oauth2_server_for_root_endpoints() helper that auto-resolves to the single configured OAuth2 server when exactly 1 exists. Also fixed WWW-Authenticate header in server.py to use public URL (get_request_base_url()) instead of internal URL (str(request.base_url)) for correct reverse proxy support.

Key changes:

  • Added resolver applied to /register, /authorize, /token endpoints and both discovery response builders
  • Fixed 3 chained issues: incorrect WWW-Authenticate URL, root /register returning "dummy_client", and /authorize lookup failing on "dummy_client"
  • Added 5 comprehensive tests covering single-server resolution, multi-server ambiguity, and discovery endpoint behavior
  • Fixed 2 existing tests to clear registry and prevent resolver interference

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The fix is well-scoped, addresses a specific bug with clear root cause analysis, includes comprehensive test coverage (5 new tests), and only touches MCP OAuth flow code without affecting critical request paths. The resolver logic is defensive (returns None when ambiguous) and all changes are backwards-compatible.
  • No files require special attention

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py Added _resolve_oauth2_server_for_root_endpoints() helper to auto-resolve single OAuth2 server for root endpoints; applied resolver to /register, /authorize, /token and discovery endpoints
litellm/proxy/_experimental/mcp_server/server.py Fixed WWW-Authenticate header to use get_request_base_url() instead of str(request.base_url) for correct public URL behind reverse proxy
tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py Added 5 new tests for root-level OAuth endpoint resolution; cleared global registry in existing tests to prevent resolver interference

Sequence Diagram

sequenceDiagram
    participant Client as MCP SDK Client
    participant Proxy as LiteLLM Proxy
    participant Resolver as _resolve_oauth2_server_for_root_endpoints()
    participant Provider as OAuth2 Provider

    Note over Client,Proxy: Root-level OAuth flow (no server name prefix)
    
    Client->>Proxy: GET /.well-known/oauth-authorization-server
    Proxy->>Resolver: Check for single OAuth2 server
    Resolver-->>Proxy: Return server (if exactly 1 exists)
    Proxy-->>Client: Return endpoints with /server_name/ prefix
    
    Client->>Proxy: POST /register (no server name)
    Proxy->>Resolver: Check for single OAuth2 server
    Resolver-->>Proxy: Return resolved server
    Proxy-->>Client: Return client_id: "server_name"
    
    Client->>Proxy: GET /authorize?client_id=dummy_client
    Proxy->>Resolver: Check for single OAuth2 server
    Resolver-->>Proxy: Return resolved server
    Proxy->>Provider: Redirect with actual client_id
    Provider-->>Client: 307 Redirect to OAuth provider
    
    Client->>Provider: User authorizes
    Provider-->>Proxy: Callback with auth code
    Proxy-->>Client: 302 Redirect with code
    
    Client->>Proxy: POST /token (no server name)
    Proxy->>Resolver: Check for single OAuth2 server
    Resolver-->>Proxy: Return resolved server
    Proxy->>Provider: Exchange code for token
    Provider-->>Proxy: Return access_token
    Proxy-->>Client: Return access_token
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Member

@ishaan-jaff ishaan-jaff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@ishaan-jaff ishaan-jaff merged commit 9697104 into BerriAI:main Feb 10, 2026
7 of 8 checks passed
@hossainbuet
Copy link

@michelligabriele thanks for this. I believe this will fix my issue, can you please confirm ? cc: @jquinter
#19977

krrishdholakia pushed a commit that referenced this pull request Feb 10, 2026
…logging_payload is missing (#20851)

* fix: Preserved nullable object fields by carrying schema properties

* Fix: _convert_schema_types

* Fix all mypy issues

* Add alert about email notifications

* fixing tests

* extending timeout for long running tests

* Text changes

* [Feat] MCP Oauth2 Fixes - Add support for MCP M2M Oauth2 support (#20788)

* add has_client_credentials

* MCPOAuth2TokenCache

* init MCP Oauth2 constants

* MCPOAuth2TokenCache

* resolve_mcp_auth

* test fixes

* docs fix

* address greptile review: min TTL, env-configurable constants, tests, docs

- Fix zero-TTL edge case: floor at MCP_OAUTH2_TOKEN_CACHE_MIN_TTL (10s)
- Make all MCP OAuth2 constants env-configurable via os.getenv()
- Move test file to follow 1:1 mapping convention (test_oauth2_token_cache.py)
- Add MCP OAuth doc page (mcp_oauth.md) with M2M and PKCE sections
- Update FAQ in mcp.md to reflect M2M support
- Add E2E test script and config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix mypy lint

* fix oauth2

* remove old files

* docs fix

* address greptile comments

* fix: atomic lock creation + validate JSON response shape

- Use dict.setdefault() for atomic per-server lock creation
- Add isinstance(body, dict) check before accessing token response fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace asserts with proper guards, wrap HTTP errors with context

- Replace `assert` statements with `if/raise ValueError` (asserts can be
  disabled with python -O in production)
- Wrap `httpx.HTTPStatusError` to provide a clear error message with
  server_id and status code
- Add tests for HTTP error and non-dict JSON response error paths
- Remove unused imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* [UI] M2M OAuth2 UI Flow  (#20794)

* add has_client_credentials

* MCPOAuth2TokenCache

* init MCP Oauth2 constants

* MCPOAuth2TokenCache

* resolve_mcp_auth

* test fixes

* docs fix

* address greptile review: min TTL, env-configurable constants, tests, docs

- Fix zero-TTL edge case: floor at MCP_OAUTH2_TOKEN_CACHE_MIN_TTL (10s)
- Make all MCP OAuth2 constants env-configurable via os.getenv()
- Move test file to follow 1:1 mapping convention (test_oauth2_token_cache.py)
- Add MCP OAuth doc page (mcp_oauth.md) with M2M and PKCE sections
- Update FAQ in mcp.md to reflect M2M support
- Add E2E test script and config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix mypy lint

* fix oauth2

* ui feat fixes

* test M2M

* test fix

* ui feats

* ui fixes

* ui fix client ID

* fix: backend endpoints

* docs fix

* fixes greptile

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* [Fix] prevent shared backend model key from being polluted by per-deployment custom pricing (#20679)

* bug: custom price override for models

* added associated test

* fix(mcp): resolve OAuth2 root endpoints returning "MCP server not found" (#20784)

When MCP SDK hits root-level /register, /authorize, /token without
server name prefix, auto-resolve to the single configured OAuth2
server. Also fix WWW-Authenticate header to use correct public URL
behind reverse proxy.

* Add support for langchain_aws via litellm passthrough

* fix(proxy): return early instead of raising ValueError when standard_logging_payload is missing

The `_PROXY_VirtualKeyModelMaxBudgetLimiter.async_log_success_event` hook
raises `ValueError` when `standard_logging_payload` is `None`.  This breaks
non-standard call types (e.g. vLLM `/classify`) that do not populate the
payload, and the resulting exception disrupts downstream success callbacks
like Langfuse.

Return early with a debug log instead, matching the existing pattern used
for missing `user_api_key_model_max_budget`.

Fixes #18986

---------

Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: yuneng-jiang <yuneng.jiang@gmail.com>
Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shivam Rawat <161387515+shivamrawat1@users.noreply.github.com>
Co-authored-by: michelligabriele <gabriele.michelli@icloud.com>
Sameerlite added a commit that referenced this pull request Feb 11, 2026
…logging_payload is missing (#20851)

* fix: Preserved nullable object fields by carrying schema properties

* Fix: _convert_schema_types

* Fix all mypy issues

* Add alert about email notifications

* fixing tests

* extending timeout for long running tests

* Text changes

* [Feat] MCP Oauth2 Fixes - Add support for MCP M2M Oauth2 support (#20788)

* add has_client_credentials

* MCPOAuth2TokenCache

* init MCP Oauth2 constants

* MCPOAuth2TokenCache

* resolve_mcp_auth

* test fixes

* docs fix

* address greptile review: min TTL, env-configurable constants, tests, docs

- Fix zero-TTL edge case: floor at MCP_OAUTH2_TOKEN_CACHE_MIN_TTL (10s)
- Make all MCP OAuth2 constants env-configurable via os.getenv()
- Move test file to follow 1:1 mapping convention (test_oauth2_token_cache.py)
- Add MCP OAuth doc page (mcp_oauth.md) with M2M and PKCE sections
- Update FAQ in mcp.md to reflect M2M support
- Add E2E test script and config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix mypy lint

* fix oauth2

* remove old files

* docs fix

* address greptile comments

* fix: atomic lock creation + validate JSON response shape

- Use dict.setdefault() for atomic per-server lock creation
- Add isinstance(body, dict) check before accessing token response fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace asserts with proper guards, wrap HTTP errors with context

- Replace `assert` statements with `if/raise ValueError` (asserts can be
  disabled with python -O in production)
- Wrap `httpx.HTTPStatusError` to provide a clear error message with
  server_id and status code
- Add tests for HTTP error and non-dict JSON response error paths
- Remove unused imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* [UI] M2M OAuth2 UI Flow  (#20794)

* add has_client_credentials

* MCPOAuth2TokenCache

* init MCP Oauth2 constants

* MCPOAuth2TokenCache

* resolve_mcp_auth

* test fixes

* docs fix

* address greptile review: min TTL, env-configurable constants, tests, docs

- Fix zero-TTL edge case: floor at MCP_OAUTH2_TOKEN_CACHE_MIN_TTL (10s)
- Make all MCP OAuth2 constants env-configurable via os.getenv()
- Move test file to follow 1:1 mapping convention (test_oauth2_token_cache.py)
- Add MCP OAuth doc page (mcp_oauth.md) with M2M and PKCE sections
- Update FAQ in mcp.md to reflect M2M support
- Add E2E test script and config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix mypy lint

* fix oauth2

* ui feat fixes

* test M2M

* test fix

* ui feats

* ui fixes

* ui fix client ID

* fix: backend endpoints

* docs fix

* fixes greptile

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* [Fix] prevent shared backend model key from being polluted by per-deployment custom pricing (#20679)

* bug: custom price override for models

* added associated test

* fix(mcp): resolve OAuth2 root endpoints returning "MCP server not found" (#20784)

When MCP SDK hits root-level /register, /authorize, /token without
server name prefix, auto-resolve to the single configured OAuth2
server. Also fix WWW-Authenticate header to use correct public URL
behind reverse proxy.

* Add support for langchain_aws via litellm passthrough

* fix(proxy): return early instead of raising ValueError when standard_logging_payload is missing

The `_PROXY_VirtualKeyModelMaxBudgetLimiter.async_log_success_event` hook
raises `ValueError` when `standard_logging_payload` is `None`.  This breaks
non-standard call types (e.g. vLLM `/classify`) that do not populate the
payload, and the resulting exception disrupts downstream success callbacks
like Langfuse.

Return early with a debug log instead, matching the existing pattern used
for missing `user_api_key_model_max_budget`.

Fixes #18986

---------

Co-authored-by: Sameer Kankute <sameer@berri.ai>
Co-authored-by: yuneng-jiang <yuneng.jiang@gmail.com>
Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shivam Rawat <161387515+shivamrawat1@users.noreply.github.com>
Co-authored-by: michelligabriele <gabriele.michelli@icloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants