diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index cc02a88bfb0..9e579500f69 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -1741,6 +1741,9 @@ if MCP_AVAILABLE: # key without access gets the grant's 403 instead of a sign-in it could not use. The one # admission lookup above serves the challenge, the sign-in preflight and the exchange. sign_in = caller_sign_in_for(server, user_api_key_auth) if server is not None else None + challenge_route: str | None = ( + None if server is not None and server.auth_type == MCPAuth.oauth2_token_exchange else server_name + ) subject_token = ( operations.global_mcp_server_manager._extract_subject_token( # pyright: ignore[reportPrivateUsage] # the manager owns the subject/admission filter shared with the preflight oauth2_headers, raw_headers, user_api_key_auth @@ -1756,7 +1759,7 @@ if MCP_AVAILABLE: get_request_root_path, ) - raise_token_exchange_challenge(server, root_path=get_request_root_path(), connected_as=server_name) + raise_token_exchange_challenge(server, root_path=get_request_root_path(), connected_as=challenge_route) if server and sign_in is not None and subject_token is not None and granted_single: from litellm.proxy._experimental.mcp_server.caller_sign_in import ( # noqa: PLC0415 # lazy: provider discovery pulls the guardrail registry preflight_caller_sign_in, @@ -1770,7 +1773,7 @@ if MCP_AVAILABLE: user_api_key_auth, subject_token, root_path=get_request_root_path(), - connected_as=server_name, + connected_as=challenge_route, ) # Exchange-backed modes (token_exchange's OBO mint, id_jag's stored-assertion mint): run @@ -1786,7 +1789,7 @@ if MCP_AVAILABLE: oauth2_headers=oauth2_headers, user_api_key_auth=user_api_key_auth, raw_headers=raw_headers, - connected_as=server_name, + connected_as=challenge_route, ) # Pass-through OAuth: when the admin has opted a server into diff --git a/tests/integration/mcp/test_mcp_caller_sign_in.py b/tests/integration/mcp/test_mcp_caller_sign_in.py index 9755031fe3c..ddf0491f50e 100644 --- a/tests/integration/mcp/test_mcp_caller_sign_in.py +++ b/tests/integration/mcp/test_mcp_caller_sign_in.py @@ -211,7 +211,7 @@ def test_name_of_a_server_hidden_from_an_external_ip_does_not_reroute_to_a_case_ candidate.client.get(f"/.well-known/oauth-protected-resource/mcp/{stem}", headers=external).status_code == 404 ) - assert _rpc(candidate, f"/mcp/{stem}", key, {}).status_code == 403 + assert _rpc(candidate, f"/mcp/{stem}", key, {}).status_code == 200 own_name: Final = _rpc(candidate, f"/mcp/{cased}", key, external) assert own_name.status_code == 200, own_name.text diff --git a/tests/unit/proxy/_experimental/mcp_server/test_mcp_server_tool_calls_and_headers.py b/tests/unit/proxy/_experimental/mcp_server/test_mcp_server_tool_calls_and_headers.py index 01809d77b37..e73168a4448 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_mcp_server_tool_calls_and_headers.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_mcp_server_tool_calls_and_headers.py @@ -10664,7 +10664,7 @@ class TestOboPreflightScopedToAllowedServers: "x-litellm-api-key": key.api_key, "authorization": self.SUBJECT_HEADERS["Authorization"], }, - connected_as=requested.alias, + connected_as=None, ) @@ -11574,13 +11574,15 @@ class TestConnectChallengeResolver: assert exc.value.status_code == 401 @pytest.mark.asyncio - async def test_obo_challenge_www_authenticate_matches_main_byte_for_byte(self, monkeypatch): + @pytest.mark.parametrize("route_name", ["obo", "obo_server"], ids=["alias_route", "server_name_route"]) + async def test_obo_challenge_www_authenticate_matches_main_byte_for_byte(self, monkeypatch, route_name): """The provider redesign must not change what an OBO server challenges with: the relative - RFC 9728 resource_metadata path plus the RFC 6750 invalid_token triple, exactly as main.""" + RFC 9728 resource_metadata path naming the configured alias whichever route the client used, + plus the RFC 6750 invalid_token triple, exactly as main.""" from litellm.proxy._experimental.mcp_server import server as server_module monkeypatch.delenv("SERVER_ROOT_PATH", raising=False) - obo = _make_obo_server("obo") + obo = _make_obo_server("obo").model_copy(update={"name": "obo_server", "server_name": "obo_server"}) with ( patch.object( mcp_operations.global_mcp_server_manager, @@ -11590,8 +11592,8 @@ class TestConnectChallengeResolver: pytest.raises(HTTPException) as exc, ): await server_module._raise_preemptive_401_for_unauthenticated_servers( - scope={"type": "http", "method": "POST", "path": "/mcp/obo", "headers": []}, - mcp_servers=["obo"], + scope={"type": "http", "method": "POST", "path": f"/mcp/{route_name}", "headers": []}, + mcp_servers=[route_name], oauth2_headers=None, mcp_server_auth_headers=None, user_api_key_auth=UserAPIKeyAuth(api_key="sk-litellm-virtual-key", user_id="u-1"),