From a66e091cd7c4f3acd0e6f5066d44ebb4d28432f9 Mon Sep 17 00:00:00 2001 From: Daniel Meismer Date: Tue, 18 Aug 2026 17:05:40 -0400 Subject: [PATCH 1/5] fix(mcp): complete DCR bridge OAuth challenges --- .../mcp_server/auth/user_api_key_auth_mcp.py | 54 +++++--- .../auth/test_user_api_key_auth_mcp.py | 124 ++++++++++++++---- 2 files changed, 138 insertions(+), 40 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index 7d85f3c4908..882ded78e2f 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -1,6 +1,7 @@ import re from collections.abc import Sequence from datetime import datetime, timezone +from types import MappingProxyType from typing import TYPE_CHECKING, Final, cast from fastapi import HTTPException @@ -13,6 +14,7 @@ import litellm from litellm._logging import verbose_logger from litellm.proxy._experimental.mcp_server.oauth_utils import ( get_passthrough_resource_metadata_url, + get_passthrough_www_authenticate, get_request_base_url, well_known_root_suffix, ) @@ -437,24 +439,23 @@ class MCPRequestHandler: path=request_route, mcp_servers=mcp_servers, client_ip=IPAddressUtils.get_mcp_client_ip(request), + ) or ( + MCPRequestHandler._single_dcr_bridge_delegate_target( + path=request_route, + mcp_servers=mcp_servers, + client_ip=IPAddressUtils.get_mcp_client_ip(request), + ) + is not None + and not oauth2_headers ): validated_user_api_key_auth = UserAPIKeyAuth() elif ( - ( - bridge_delegate_target := MCPRequestHandler._single_dcr_bridge_delegate_target( - path=request_route, - mcp_servers=mcp_servers, - client_ip=IPAddressUtils.get_mcp_client_ip(request), - ) + bridge_delegate_target := MCPRequestHandler._single_dcr_bridge_delegate_target( + path=request_route, + mcp_servers=mcp_servers, + client_ip=IPAddressUtils.get_mcp_client_ip(request), ) - is not None - and oauth2_headers - and is_bridge_envelope_shaped(oauth2_headers["Authorization"]) - ): - # A single DCR-bridge oauth_delegate target carrying an envelope-shaped - # Authorization: open the envelope, admit under its recovered identity, and - # inject the inner upstream token for egress. A non-envelope bearer on the same - # server is NOT admitted here — it falls through to the oauth2 arm, which 401s. + ) is not None and oauth2_headers: validated_user_api_key_auth, mcp_server_auth_headers = await MCPRequestHandler._admit_dcr_bridge_delegate( server=bridge_delegate_target, authorization_value=oauth2_headers["Authorization"], @@ -740,7 +741,10 @@ class MCPRequestHandler: if len(target_names) != 1: return None server: Final = global_mcp_server_manager.get_mcp_server_by_name(target_names[0], client_ip=client_ip) - if server is None or not server.is_oauth_delegate or not server.is_dcr_bridge: + # Both flags are security-sensitive opt-ins. Require literal booleans so + # partially populated objects and truthy proxy values cannot enable bridge + # admission accidentally. + if server is None or server.is_oauth_delegate is not True or server.is_dcr_bridge is not True: return None # Egress resolves the injected per-server token only by alias / server_name; a server with # neither cannot receive the forwarded token, so fail closed rather than admit-and-drop. @@ -798,7 +802,25 @@ class MCPRequestHandler: new_headers: Final = {**(mcp_server_auth_headers or {}), **injected} return admitted, new_headers case BridgeEnvelopeInvalid() | NotBridgeEnvelope(): - raise HTTPException(status_code=401, detail="Invalid or expired credential") + resource_name: Final = server.alias or server.server_name + if resource_name is None: + raise HTTPException( + status_code=500, + detail="Server misconfigured: MCP server has no routable name", + ) + raise HTTPException( + status_code=401, + detail="Invalid or expired credential", + headers=MappingProxyType( + { + "www-authenticate": get_passthrough_www_authenticate( + scope=request.scope, + server_name=resource_name, + invalid_token=True, + ) + } + ), + ) case _: assert_never(result) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py index 697c9b018ec..0fa840e4dd3 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py @@ -5097,13 +5097,10 @@ class TestMCPDcrBridgeDelegateAdmission: """Admission-side arm for a DCR-bridge ``oauth_delegate`` client that authenticates with a single envelope bearer (LIT-4338). - The arm fires only for a single ``is_dcr_bridge`` ``is_oauth_delegate`` target carrying an - envelope-shaped Authorization. It opens the litellm-signed envelope, reloads the live key - record the sealed ``key_hash`` references so the caller is admitted under the key's current - authorization context (team/org/object-permission) and revocation state, and injects the inner - upstream token under the server's per-server auth-header key so egress forwards it. A key that - is missing, blocked, or expired fails closed with a 401. Everything else must stay on its - existing admission path. + A credential-free request reaches the named MCP handler so it can issue the initial OAuth + challenge. Every bearer on that same route enters envelope resolution. A valid envelope opens + under its live authorization context, while invalid envelopes and non-envelope bearers receive + a named ``invalid_token`` challenge. Everything else stays on its existing admission path. """ _MASTER_KEY = "sk-bridge-master-key-for-envelope-derivation" @@ -5272,6 +5269,57 @@ class TestMCPDcrBridgeDelegateAdmission: request.body = mock_body return request + async def test_bridge_target_requires_literal_boolean_opt_ins(self): + """Truthy proxy values must not opt an unresolved server into bridge admission.""" + for delegate_value, bridge_value in ((MagicMock(), True), (True, MagicMock())): + server = MagicMock() + server.is_oauth_delegate = delegate_value + server.is_dcr_bridge = bridge_value + server.server_name = "bridge_delegate_server" + server.alias = None + + with patch( + "litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager" + ) as mock_mgr: + mock_mgr.get_mcp_server_by_name.return_value = server + assert ( + MCPRequestHandler._single_dcr_bridge_delegate_target( + path="/mcp/bridge_delegate_server", + mcp_servers=None, + client_ip=None, + ) + is None + ) + + async def test_credential_free_named_bridge_request_reaches_mcp_handler(self): + scope = { + "type": "http", + "method": "POST", + "path": "/mcp/bridge_delegate_server", + "headers": [], + } + + with ( + patch( + "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", + new_callable=AsyncMock, + ) as mock_auth, + patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, + ): + mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server() + ( + auth_result, + _mcp_auth_header, + _mcp_servers, + mcp_server_auth_headers, + _oauth2_headers, + _raw_headers, + ) = await MCPRequestHandler.process_mcp_request(scope) + + mock_auth.assert_not_called() + assert auth_result == UserAPIKeyAuth() + assert mcp_server_auth_headers == {} + async def test_valid_envelope_reloads_live_key_and_admits_its_authorization_context(self): """A valid envelope admits under the LIVE key record the sealed key_hash references, not a blank identity: the reload is keyed by that exact hash, and the admitted auth carries the @@ -5887,8 +5935,7 @@ class TestMCPDcrBridgeDelegateAdmission: mock_auth.assert_called_once() async def test_expired_envelope_fails_closed_401(self): - """An envelope whose exp is in the past must fail closed with a 401, never fall through to - anonymous admission.""" + """An expired envelope fails closed and tells the client where to reauthorize.""" expired = self._mint_bridge_envelope( expires_in=60, minted_at=datetime.now(timezone.utc) - timedelta(hours=2), @@ -5897,7 +5944,10 @@ class TestMCPDcrBridgeDelegateAdmission: "type": "http", "method": "POST", "path": "/mcp/bridge_delegate_server", - "headers": [(b"authorization", f"Bearer {expired}".encode("latin-1"))], + "headers": [ + (b"host", b"testserver"), + (b"authorization", f"Bearer {expired}".encode("latin-1")), + ], } with ( @@ -5914,6 +5964,12 @@ class TestMCPDcrBridgeDelegateAdmission: assert exc_info.value.status_code == 401 mock_auth.assert_not_called() + assert exc_info.value.headers == { + "www-authenticate": ( + 'Bearer error="invalid_token", ' + 'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/bridge_delegate_server"' + ) + } async def test_envelope_minted_for_a_different_server_fails_closed_401(self): """An envelope sealed for another server_id must be rejected when presented to this server, @@ -5924,7 +5980,10 @@ class TestMCPDcrBridgeDelegateAdmission: "type": "http", "method": "POST", "path": "/mcp/bridge_delegate_server", - "headers": [(b"authorization", f"Bearer {wrong_server}".encode("latin-1"))], + "headers": [ + (b"host", b"testserver"), + (b"authorization", f"Bearer {wrong_server}".encode("latin-1")), + ], } with ( @@ -5941,6 +6000,12 @@ class TestMCPDcrBridgeDelegateAdmission: assert exc_info.value.status_code == 401 mock_auth.assert_not_called() + assert exc_info.value.headers == { + "www-authenticate": ( + 'Bearer error="invalid_token", ' + 'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/bridge_delegate_server"' + ) + } async def test_envelope_under_wrong_master_key_fails_closed_401(self): """An envelope-shaped bearer whose signature does not verify under the proxy's derived keys @@ -5950,7 +6015,10 @@ class TestMCPDcrBridgeDelegateAdmission: "type": "http", "method": "POST", "path": "/mcp/bridge_delegate_server", - "headers": [(b"authorization", f"Bearer {foreign}".encode("latin-1"))], + "headers": [ + (b"host", b"testserver"), + (b"authorization", f"Bearer {foreign}".encode("latin-1")), + ], } with ( @@ -5967,26 +6035,29 @@ class TestMCPDcrBridgeDelegateAdmission: assert exc_info.value.status_code == 401 mock_auth.assert_not_called() + assert exc_info.value.headers == { + "www-authenticate": ( + 'Bearer error="invalid_token", ' + 'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/bridge_delegate_server"' + ) + } - async def test_non_envelope_bearer_on_bridge_server_falls_through_to_oauth2_arm(self): - """A plain (non-envelope) bearer on the same bridge server must NOT be admitted by the - envelope arm: it falls through to the oauth2 arm, which validates it as a LiteLLM key and - 401s here. Proves the arm is gated on envelope shape, not merely on the target being a - bridge server.""" + async def test_non_envelope_bearer_on_bridge_server_returns_named_challenge(self): + """A raw provider bearer cannot authorize a bridge route and triggers reauthorization.""" scope = { "type": "http", "method": "POST", "path": "/mcp/bridge_delegate_server", - "headers": [(b"authorization", b"Bearer plain-upstream-bearer-not-an-envelope")], + "headers": [ + (b"host", b"testserver"), + (b"authorization", b"Bearer plain-upstream-bearer-not-an-envelope"), + ], } - async def mock_user_api_key_auth_fails(api_key, request): - raise HTTPException(status_code=401, detail="Invalid API key") - with ( patch( "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", - side_effect=mock_user_api_key_auth_fails, + new_callable=AsyncMock, ) as mock_auth, patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY), @@ -5996,8 +6067,13 @@ class TestMCPDcrBridgeDelegateAdmission: await MCPRequestHandler.process_mcp_request(scope) assert exc_info.value.status_code == 401 - # The envelope arm was skipped, so the oauth2 arm ran and validated the bearer. - mock_auth.assert_called_once() + mock_auth.assert_not_called() + assert exc_info.value.headers == { + "www-authenticate": ( + 'Bearer error="invalid_token", ' + 'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/bridge_delegate_server"' + ) + } async def test_explicit_litellm_key_wins_over_envelope_arm(self): """An explicit x-litellm-api-key is always a LiteLLM credential and its arm precedes the From da036ad0f0da3fa60cfc56e7d3697f184e2bb443 Mon Sep 17 00:00:00 2001 From: Daniel Meismer Date: Tue, 25 Aug 2026 22:29:42 -0400 Subject: [PATCH 2/5] fix(mcp): harden DCR bridge admission Preserve standard Authorization key validation while preventing client MCP credentials from receiving anonymous bridge admission. Generated with AI Co-Authored-By: Codex --- .../mcp_server/auth/user_api_key_auth_mcp.py | 75 +++++++++++----- .../auth/test_user_api_key_auth_mcp.py | 90 ++++++++++++++++++- 2 files changed, 144 insertions(+), 21 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index 882ded78e2f..4d999919f2f 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -447,6 +447,8 @@ class MCPRequestHandler: ) is not None and not oauth2_headers + and not mcp_server_auth_headers + and not mcp_auth_header ): validated_user_api_key_auth = UserAPIKeyAuth() elif ( @@ -456,9 +458,13 @@ class MCPRequestHandler: client_ip=IPAddressUtils.get_mcp_client_ip(request), ) ) is not None and oauth2_headers: - validated_user_api_key_auth, mcp_server_auth_headers = await MCPRequestHandler._admit_dcr_bridge_delegate( + ( + validated_user_api_key_auth, + mcp_server_auth_headers, + ) = await MCPRequestHandler._admit_dcr_bridge_authorization( server=bridge_delegate_target, authorization_value=oauth2_headers["Authorization"], + litellm_api_key=litellm_api_key, mcp_server_auth_headers=mcp_server_auth_headers, request=request, route=request_route, @@ -802,28 +808,57 @@ class MCPRequestHandler: new_headers: Final = {**(mcp_server_auth_headers or {}), **injected} return admitted, new_headers case BridgeEnvelopeInvalid() | NotBridgeEnvelope(): - resource_name: Final = server.alias or server.server_name - if resource_name is None: - raise HTTPException( - status_code=500, - detail="Server misconfigured: MCP server has no routable name", - ) - raise HTTPException( - status_code=401, - detail="Invalid or expired credential", - headers=MappingProxyType( - { - "www-authenticate": get_passthrough_www_authenticate( - scope=request.scope, - server_name=resource_name, - invalid_token=True, - ) - } - ), - ) + raise MCPRequestHandler._dcr_bridge_invalid_token_challenge(server=server, request=request) case _: assert_never(result) + @staticmethod + async def _admit_dcr_bridge_authorization( + server: MCPServer, + authorization_value: str, + litellm_api_key: str, + mcp_server_auth_headers: dict[str, dict[str, str]] | None, + request: Request, + route: str, + ) -> tuple[UserAPIKeyAuth, dict[str, dict[str, str]] | None]: + if is_bridge_envelope_shaped(authorization_value): + return await MCPRequestHandler._admit_dcr_bridge_delegate( + server=server, + authorization_value=authorization_value, + mcp_server_auth_headers=mcp_server_auth_headers, + request=request, + route=route, + ) + try: + admitted: Final = await user_api_key_auth(api_key=litellm_api_key, request=request) + except (HTTPException, ProxyException) as exc: + if not _is_litellm_auth_admission_error(exc): + raise + raise MCPRequestHandler._dcr_bridge_invalid_token_challenge(server=server, request=request) from exc + return admitted, mcp_server_auth_headers + + @staticmethod + def _dcr_bridge_invalid_token_challenge(server: MCPServer, request: Request) -> HTTPException: + resource_name: Final = server.alias or server.server_name + if resource_name is None: + raise HTTPException( + status_code=500, + detail="Server misconfigured: MCP server has no routable name", + ) + return HTTPException( + status_code=401, + detail="Invalid or expired credential", + headers=MappingProxyType( + { + "www-authenticate": get_passthrough_www_authenticate( + scope=request.scope, + server_name=resource_name, + invalid_token=True, + ) + } + ), + ) + @staticmethod async def _admit_gateway_session( authorization_value: str, diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py index 0fa840e4dd3..52badaba5ac 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py @@ -5320,6 +5320,37 @@ class TestMCPDcrBridgeDelegateAdmission: assert auth_result == UserAPIKeyAuth() assert mcp_server_auth_headers == {} + @pytest.mark.parametrize( + "headers", + ( + [(b"x-mcp-auth", b"Bearer upstream-token")], + [(b"x-mcp-bridge_delegate_server-authorization", b"Bearer upstream-token")], + ), + ids=("deprecated-mcp-auth", "per-server-auth"), + ) + async def test_client_mcp_credentials_do_not_receive_keyless_bridge_admission(self, headers): + scope = { + "type": "http", + "method": "POST", + "path": "/mcp/bridge_delegate_server", + "headers": headers, + } + + with ( + patch( + "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", + new_callable=AsyncMock, + side_effect=HTTPException(status_code=401, detail="Invalid key"), + ) as mock_auth, + patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, + ): + mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server() + with pytest.raises(HTTPException) as exc_info: + await MCPRequestHandler.process_mcp_request(scope) + + assert exc_info.value.status_code == 401 + mock_auth.assert_awaited_once() + async def test_valid_envelope_reloads_live_key_and_admits_its_authorization_context(self): """A valid envelope admits under the LIVE key record the sealed key_hash references, not a blank identity: the reload is keyed by that exact hash, and the admitted auth carries the @@ -6058,6 +6089,7 @@ class TestMCPDcrBridgeDelegateAdmission: patch( "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", new_callable=AsyncMock, + side_effect=HTTPException(status_code=401, detail="Invalid key"), ) as mock_auth, patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY), @@ -6067,7 +6099,7 @@ class TestMCPDcrBridgeDelegateAdmission: await MCPRequestHandler.process_mcp_request(scope) assert exc_info.value.status_code == 401 - mock_auth.assert_not_called() + mock_auth.assert_awaited_once() assert exc_info.value.headers == { "www-authenticate": ( 'Bearer error="invalid_token", ' @@ -6075,6 +6107,62 @@ class TestMCPDcrBridgeDelegateAdmission: ) } + async def test_valid_litellm_authorization_key_uses_standard_admission(self): + scope = { + "type": "http", + "method": "POST", + "path": "/mcp/bridge_delegate_server", + "headers": [(b"authorization", b"Bearer sk-valid-litellm-key")], + } + admitted = UserAPIKeyAuth(api_key="hashed-key", user_id="litellm-key-user") + + with ( + patch( + "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", + new_callable=AsyncMock, + return_value=admitted, + ) as mock_auth, + patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, + patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY), + ): + mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server() + ( + auth_result, + _mcp_auth, + _servers, + mcp_server_auth_headers, + _oauth, + _raw, + ) = await MCPRequestHandler.process_mcp_request(scope) + + assert auth_result is admitted + assert mcp_server_auth_headers == {} + assert mock_auth.await_args.kwargs["api_key"] == "Bearer sk-valid-litellm-key" + + async def test_non_401_litellm_key_failure_is_not_converted_to_oauth_challenge(self): + scope = { + "type": "http", + "method": "POST", + "path": "/mcp/bridge_delegate_server", + "headers": [(b"authorization", b"Bearer sk-blocked-litellm-key")], + } + + with ( + patch( + "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", + new_callable=AsyncMock, + side_effect=HTTPException(status_code=403, detail="Key blocked"), + ), + patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, + patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY), + ): + mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server() + with pytest.raises(HTTPException) as exc_info: + await MCPRequestHandler.process_mcp_request(scope) + + assert exc_info.value.status_code == 403 + assert not exc_info.value.headers + async def test_explicit_litellm_key_wins_over_envelope_arm(self): """An explicit x-litellm-api-key is always a LiteLLM credential and its arm precedes the envelope arm: user_api_key_auth validates the key and NO inner token is injected, even From 1a2a24ecc695ad277e34dc4b19b1db0cdcf93e27 Mon Sep 17 00:00:00 2001 From: Daniel Meismer Date: Tue, 25 Aug 2026 22:40:01 -0400 Subject: [PATCH 3/5] chore(mcp): document mutable bridge header shape Generated with AI Co-Authored-By: Codex --- .../_experimental/mcp_server/auth/user_api_key_auth_mcp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index 4d999919f2f..8bd0d4efae8 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -817,10 +817,10 @@ class MCPRequestHandler: server: MCPServer, authorization_value: str, litellm_api_key: str, - mcp_server_auth_headers: dict[str, dict[str, str]] | None, + mcp_server_auth_headers: dict[str, dict[str, str]] | None, # mutable-ok: existing MCP sink shape request: Request, route: str, - ) -> tuple[UserAPIKeyAuth, dict[str, dict[str, str]] | None]: + ) -> tuple[UserAPIKeyAuth, dict[str, dict[str, str]] | None]: # mutable-ok: existing MCP sink shape if is_bridge_envelope_shaped(authorization_value): return await MCPRequestHandler._admit_dcr_bridge_delegate( server=server, From c74a8df52df939e1e6735c69815687a9aa2ef4e0 Mon Sep 17 00:00:00 2001 From: Daniel Meismer Date: Tue, 25 Aug 2026 22:48:22 -0400 Subject: [PATCH 4/5] chore(mcp): satisfy test quality lint Document the intentional internal seams used by the DCR bridge admission tests and normalize import ordering.\n\nGenerated with AI\n\nCo-Authored-By: Codex --- .../auth/test_user_api_key_auth_mcp.py | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py index 52badaba5ac..cb3133502f7 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py @@ -7,8 +7,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import HTTPException from fastapi.testclient import TestClient - - from starlette.datastructures import Headers from litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp import ( @@ -5135,6 +5133,8 @@ class TestMCPDcrBridgeDelegateAdmission: minted_at=None, master_key=None, ): + from pydantic import SecretStr + from litellm.proxy._experimental.mcp_server.outbound_credentials.bridge_credentials import ( envelope_keys_from_master_key, ) @@ -5145,7 +5145,6 @@ class TestMCPDcrBridgeDelegateAdmission: mint_envelope, user_identity, ) - from pydantic import SecretStr identity = ( user_identity(server_id=server_id, user_id=user_id) @@ -5278,7 +5277,7 @@ class TestMCPDcrBridgeDelegateAdmission: server.server_name = "bridge_delegate_server" server.alias = None - with patch( + with patch( # test-quality-ok: isolate the MCP registry when testing target selection "litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager" ) as mock_mgr: mock_mgr.get_mcp_server_by_name.return_value = server @@ -5300,11 +5299,13 @@ class TestMCPDcrBridgeDelegateAdmission: } with ( - patch( + patch( # test-quality-ok: observe the auth boundary while testing admission orchestration "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", new_callable=AsyncMock, ) as mock_auth, - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, + patch( # test-quality-ok: isolate the MCP registry used by request admission + "litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager" + ) as mock_mgr, ): mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server() ( @@ -5337,12 +5338,14 @@ class TestMCPDcrBridgeDelegateAdmission: } with ( - patch( + patch( # test-quality-ok: force credential rejection through request admission "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", new_callable=AsyncMock, side_effect=HTTPException(status_code=401, detail="Invalid key"), ) as mock_auth, - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, + patch( # test-quality-ok: isolate the MCP registry used by request admission + "litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager" + ) as mock_mgr, ): mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server() with pytest.raises(HTTPException) as exc_info: @@ -6117,13 +6120,17 @@ class TestMCPDcrBridgeDelegateAdmission: admitted = UserAPIKeyAuth(api_key="hashed-key", user_id="litellm-key-user") with ( - patch( + patch( # test-quality-ok: supply standard key admission through the auth boundary "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", new_callable=AsyncMock, return_value=admitted, ) as mock_auth, - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, - patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY), + patch( # test-quality-ok: isolate the MCP registry used by request admission + "litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager" + ) as mock_mgr, + patch( # test-quality-ok: configure key classification for the orchestration test + "litellm.proxy.proxy_server.master_key", self._MASTER_KEY + ), ): mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server() ( @@ -6148,13 +6155,17 @@ class TestMCPDcrBridgeDelegateAdmission: } with ( - patch( + patch( # test-quality-ok: force a non-401 auth result through request admission "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", new_callable=AsyncMock, side_effect=HTTPException(status_code=403, detail="Key blocked"), ), - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, - patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY), + patch( # test-quality-ok: isolate the MCP registry used by request admission + "litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager" + ) as mock_mgr, + patch( # test-quality-ok: configure key classification for the orchestration test + "litellm.proxy.proxy_server.master_key", self._MASTER_KEY + ), ): mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server() with pytest.raises(HTTPException) as exc_info: @@ -6644,8 +6655,8 @@ class TestGatewaySessionAdmission: ) from litellm.proxy._experimental.mcp_server.outbound_credentials.session_token import ( SessionPrincipal, - mint_session_token, mint_session_refresh_token, + mint_session_token, ) keys = session_keys_from_master_key(self._MASTER_KEY) @@ -7111,8 +7122,8 @@ class TestUserSubjectTeamUnion: def _manager_with(self, server_ids, allow_all=()): from litellm.proxy._experimental.mcp_server.mcp_server_manager import MCPServerManager - from litellm.types.mcp_server.mcp_server_manager import MCPServer from litellm.types.mcp import MCPTransport + from litellm.types.mcp_server.mcp_server_manager import MCPServer manager = MCPServerManager() for sid in server_ids: From 764048750ecbddd9e4baefa8483d66d02089d37b Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:52:28 -0700 Subject: [PATCH 5/5] fix(mcp): name invalid_token challenges by the caller's requested spelling --- .../mcp_server/auth/user_api_key_auth_mcp.py | 46 +++++++++++++------ .../auth/test_user_api_key_auth_mcp.py | 46 +++++++++++++++++++ 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index 8bd0d4efae8..281a555dc5c 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -1,5 +1,6 @@ import re from collections.abc import Sequence +from dataclasses import dataclass from datetime import datetime, timezone from types import MappingProxyType from typing import TYPE_CHECKING, Final, cast @@ -300,6 +301,16 @@ def _admission_failure_fallback( raise exc +@dataclass(frozen=True, slots=True) +class DcrBridgeTarget: + """The single DCR-bridge server a request targets, paired with the exact name the caller + used to reach it (alias or server_name, whichever they typed), which is the spelling an + ``invalid_token`` challenge must echo back.""" + + requested_name: str + server: MCPServer + + class MCPRequestHandler: """ Class to handle MCP request processing, including: @@ -462,7 +473,8 @@ class MCPRequestHandler: validated_user_api_key_auth, mcp_server_auth_headers, ) = await MCPRequestHandler._admit_dcr_bridge_authorization( - server=bridge_delegate_target, + server=bridge_delegate_target.server, + requested_name=bridge_delegate_target.requested_name, authorization_value=oauth2_headers["Authorization"], litellm_api_key=litellm_api_key, mcp_server_auth_headers=mcp_server_auth_headers, @@ -730,10 +742,10 @@ class MCPRequestHandler: @staticmethod def _single_dcr_bridge_delegate_target( path: str, mcp_servers: list[str] | None, client_ip: str | None - ) -> MCPServer | None: + ) -> DcrBridgeTarget | None: """The one DCR-bridge ``oauth_delegate`` server this request targets, or ``None``. - Returns the server only when EXACTLY ONE target resolves and it is both + Returns the target only when EXACTLY ONE name resolves and its server is both ``is_oauth_delegate`` and ``is_dcr_bridge``. Fails closed (``None``) on a multi-target request, an unresolved target, or a non-matching server, so the envelope admission arm never fires for an aggregate scope or a server that did not @@ -756,11 +768,12 @@ class MCPRequestHandler: # neither cannot receive the forwarded token, so fail closed rather than admit-and-drop. if not (server.server_name or server.alias): return None - return server + return DcrBridgeTarget(requested_name=target_names[0], server=server) @staticmethod async def _admit_dcr_bridge_delegate( server: MCPServer, + requested_name: str, authorization_value: str, mcp_server_auth_headers: dict[str, dict[str, str]] | None, request: Request, @@ -808,13 +821,16 @@ class MCPRequestHandler: new_headers: Final = {**(mcp_server_auth_headers or {}), **injected} return admitted, new_headers case BridgeEnvelopeInvalid() | NotBridgeEnvelope(): - raise MCPRequestHandler._dcr_bridge_invalid_token_challenge(server=server, request=request) + raise MCPRequestHandler._dcr_bridge_invalid_token_challenge( + requested_name=requested_name, request=request + ) case _: assert_never(result) @staticmethod async def _admit_dcr_bridge_authorization( server: MCPServer, + requested_name: str, authorization_value: str, litellm_api_key: str, mcp_server_auth_headers: dict[str, dict[str, str]] | None, # mutable-ok: existing MCP sink shape @@ -824,6 +840,7 @@ class MCPRequestHandler: if is_bridge_envelope_shaped(authorization_value): return await MCPRequestHandler._admit_dcr_bridge_delegate( server=server, + requested_name=requested_name, authorization_value=authorization_value, mcp_server_auth_headers=mcp_server_auth_headers, request=request, @@ -834,17 +851,18 @@ class MCPRequestHandler: except (HTTPException, ProxyException) as exc: if not _is_litellm_auth_admission_error(exc): raise - raise MCPRequestHandler._dcr_bridge_invalid_token_challenge(server=server, request=request) from exc + raise MCPRequestHandler._dcr_bridge_invalid_token_challenge( + requested_name=requested_name, request=request + ) from exc return admitted, mcp_server_auth_headers @staticmethod - def _dcr_bridge_invalid_token_challenge(server: MCPServer, request: Request) -> HTTPException: - resource_name: Final = server.alias or server.server_name - if resource_name is None: - raise HTTPException( - status_code=500, - detail="Server misconfigured: MCP server has no routable name", - ) + def _dcr_bridge_invalid_token_challenge(requested_name: str, request: Request) -> HTTPException: + """The RFC 6750 ``invalid_token`` challenge for a failed bridge admission. + + Named by the exact spelling the caller requested, matching the per-server well-known + document and the other challenge emitters, so ``resource_metadata`` always points at the + resource the client actually asked for even when alias and server_name differ.""" return HTTPException( status_code=401, detail="Invalid or expired credential", @@ -852,7 +870,7 @@ class MCPRequestHandler: { "www-authenticate": get_passthrough_www_authenticate( scope=request.scope, - server_name=resource_name, + server_name=requested_name, invalid_token=True, ) } diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py index cb3133502f7..99e3e8d7413 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py @@ -5891,6 +5891,7 @@ class TestMCPDcrBridgeDelegateAdmission: ): _auth, new_headers = await MCPRequestHandler._admit_dcr_bridge_delegate( server=self._bridge_delegate_server(server_name="bridge_name", alias="bridge_alias"), + requested_name="bridge_name", authorization_value=f"Bearer {envelope}", mcp_server_auth_headers=attacker_forwarded, request=self._mcp_request(), @@ -5927,6 +5928,7 @@ class TestMCPDcrBridgeDelegateAdmission: ): _auth, new_headers = await MCPRequestHandler._admit_dcr_bridge_delegate( server=server, + requested_name="bridge_delegate_server", authorization_value=f"Bearer {envelope}", mcp_server_auth_headers=None, request=self._mcp_request(), @@ -6076,6 +6078,47 @@ class TestMCPDcrBridgeDelegateAdmission: ) } + @pytest.mark.parametrize("requested_name", ["bridge_name", "bridge_alias"]) + async def test_invalid_envelope_challenge_names_the_requested_spelling(self, requested_name): + """A server reachable under both its server_name and a distinct alias must challenge with + metadata for the exact spelling the caller used, matching the per-server well-known + document, so the client rediscovers against the resource it actually asked for.""" + foreign = self._mint_bridge_envelope(master_key="a-different-master-key-entirely") + scope = { + "type": "http", + "method": "POST", + "path": f"/mcp/{requested_name}", + "headers": [ + (b"host", b"testserver"), + (b"authorization", f"Bearer {foreign}".encode("latin-1")), + ], + } + + with ( + patch( # test-quality-ok: prove standard admission is never consulted for an envelope bearer + "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp.user_api_key_auth", + new_callable=AsyncMock, + ) as mock_auth, + patch( # test-quality-ok: isolate the MCP registry, same seam as the sibling challenge tests + "litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager" + ) as mock_mgr, + patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY), # test-quality-ok: envelope keys derive from the proxy master_key module global + ): + mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server( + server_name="bridge_name", alias="bridge_alias" + ) + with pytest.raises(HTTPException) as exc_info: + await MCPRequestHandler.process_mcp_request(scope) + + assert exc_info.value.status_code == 401 + mock_auth.assert_not_called() + assert exc_info.value.headers == { + "www-authenticate": ( + 'Bearer error="invalid_token", ' + f'resource_metadata="http://testserver/.well-known/oauth-protected-resource/mcp/{requested_name}"' + ) + } + async def test_non_envelope_bearer_on_bridge_server_returns_named_challenge(self): """A raw provider bearer cannot authorize a bridge route and triggers reauthorization.""" scope = { @@ -6301,6 +6344,7 @@ class TestMCPDcrBridgeDelegateAdmission: ): auth_result, new_headers = await MCPRequestHandler._admit_dcr_bridge_delegate( server=self._bridge_delegate_server(), + requested_name="bridge_delegate_server", authorization_value=f"Bearer {envelope}", mcp_server_auth_headers=existing, request=self._mcp_request(), @@ -6325,6 +6369,7 @@ class TestMCPDcrBridgeDelegateAdmission: with pytest.raises(HTTPException) as exc_info: await MCPRequestHandler._admit_dcr_bridge_delegate( server=self._bridge_delegate_server(), + requested_name="bridge_delegate_server", authorization_value=f"Bearer {envelope}", mcp_server_auth_headers=None, request=self._mcp_request(), @@ -6343,6 +6388,7 @@ class TestMCPDcrBridgeDelegateAdmission: with pytest.raises(HTTPException) as exc_info: await MCPRequestHandler._admit_dcr_bridge_delegate( server=self._bridge_delegate_server(), + requested_name="bridge_delegate_server", authorization_value=f"Bearer {envelope}", mcp_server_auth_headers=None, request=self._mcp_request(),