diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index 5eaf80641f3..19db31d54d2 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -748,7 +748,7 @@ async def fetch_upstream_oauth_protected_resource( async_client = get_async_httpx_client(llm_provider=httpxSpecialProvider.Oauth2Check) - last_error: Optional[Exception] = None + network_errors: list[Exception] = [] for candidate in candidates: try: response = await async_client.get( @@ -756,13 +756,13 @@ async def fetch_upstream_oauth_protected_resource( headers={"Accept": "application/json"}, ) except Exception as exc: # network / connect errors - last_error = exc + if is_network_error(exc): + network_errors.append(exc) continue if response.status_code == 200: try: payload = response.json() - except Exception as exc: - last_error = exc + except Exception: continue if isinstance(payload, dict): _OAUTH_METADATA_CACHE[cache_key] = ( @@ -771,10 +771,8 @@ async def fetch_upstream_oauth_protected_resource( ) return payload - if last_error is not None and all( - is_network_error(last_error) for _ in candidates - ): - raise last_error + if len(network_errors) == len(candidates): + raise network_errors[-1] return None diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_oauth_passthrough.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_oauth_passthrough.py index 32a5fb1f6bc..b105ede65e5 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_oauth_passthrough.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_oauth_passthrough.py @@ -258,6 +258,37 @@ async def test_oauth_protected_resource_passthrough_network_error_returns_502(): assert exc_info.value.status_code == 502 +@pytest.mark.asyncio +async def test_fetch_upstream_metadata_returns_none_when_not_all_candidates_network_fail(): + passthrough_server = MCPServer( + server_id="passthrough-partial-network", + name="jet_knowledge_qa", + server_name="jet_knowledge_qa", + alias="jet_knowledge_qa", + url="https://upstream.example.com/mcp", + transport=MCPTransport.http, + auth_type=MCPAuth.none, + extra_headers=["Authorization"], + ) + + not_found_response = MagicMock() + not_found_response.status_code = 404 + mock_client = MagicMock() + mock_client.get = AsyncMock( + side_effect=[not_found_response, httpx.ConnectError("path fallback failed")] + ) + + with patch.object( + discoverable_endpoints, "get_async_httpx_client", return_value=mock_client + ): + result = await discoverable_endpoints.fetch_upstream_oauth_protected_resource( + passthrough_server + ) + + assert result is None + assert mock_client.get.await_count == 2 + + @pytest.mark.asyncio async def test_oauth_protected_resource_gateway_managed_unchanged(): """Regression guard: OAuth2 servers still advertise the gateway as AS."""