diff --git a/litellm/proxy/gateway/mcp/_spike_exhaustiveness.py b/litellm/proxy/gateway/mcp/_spike_exhaustiveness.py index 623d3e5946f..e6b4e623021 100644 --- a/litellm/proxy/gateway/mcp/_spike_exhaustiveness.py +++ b/litellm/proxy/gateway/mcp/_spike_exhaustiveness.py @@ -63,6 +63,8 @@ def http_status(err: CredError) -> int: return 500 case "precondition_required": return 412 + case "not_implemented": + return 501 assert_never(err.tag) diff --git a/litellm/proxy/gateway/mcp/outbound_credentials/resolver.py b/litellm/proxy/gateway/mcp/outbound_credentials/resolver.py index 53434665a37..8b4624b1a6b 100644 --- a/litellm/proxy/gateway/mcp/outbound_credentials/resolver.py +++ b/litellm/proxy/gateway/mcp/outbound_credentials/resolver.py @@ -205,5 +205,5 @@ def _bearer(token: StoredToken) -> StaticHeaderAuth: def _todo(kind: AuthSpecKind) -> Result[httpx.Auth, CredError]: return Error( - CredError.of_misconfigured(f"{kind.value}: resolver arm not implemented yet") + CredError.of_not_implemented(f"{kind.value}: resolver arm not implemented yet") ) diff --git a/litellm/proxy/gateway/mcp/outbound_credentials/types.py b/litellm/proxy/gateway/mcp/outbound_credentials/types.py index e8b44cd5fa0..3ae9972264e 100644 --- a/litellm/proxy/gateway/mcp/outbound_credentials/types.py +++ b/litellm/proxy/gateway/mcp/outbound_credentials/types.py @@ -68,6 +68,7 @@ class CredError: "upstream_unavailable", "unsupported_mode", "precondition_required", + "not_implemented", ] = tag() unauthorized: str = ( @@ -85,6 +86,9 @@ class CredError: precondition_required: str = ( case() ) # a required per-user value (e.g. an env var) has not been provided -> 412 + not_implemented: str = ( + case() + ) # the declared mode's resolver arm is not built yet -> 501 (not operator error) @staticmethod def of_unauthorized(detail: str) -> CredError: @@ -106,6 +110,10 @@ class CredError: def of_precondition_required(detail: str) -> CredError: return CredError(precondition_required=detail) + @staticmethod + def of_not_implemented(detail: str) -> CredError: + return CredError(not_implemented=detail) + @property def summary(self) -> str: # Exhaustiveness: every Literal tag has an arm; the trailing assert_never typechecks @@ -121,6 +129,8 @@ class CredError: return self.unsupported_mode case "precondition_required": return f"precondition required: {self.precondition_required}" + case "not_implemented": + return f"not implemented: {self.not_implemented}" assert_never(self.tag) diff --git a/tests/mcp_tests/gateway/test_resolver.py b/tests/mcp_tests/gateway/test_resolver.py index c9decbb0ac9..f2600dc6d04 100644 --- a/tests/mcp_tests/gateway/test_resolver.py +++ b/tests/mcp_tests/gateway/test_resolver.py @@ -192,10 +192,11 @@ async def test_api_key_per_user_isolated_by_subject(): assert result.error.tag == "unauthorized" -def test_missing_status_maps_byok_401_distinct_from_env_var_412(): - # The two per-user sources surface different HTTP statuses at the edge. +def test_crederror_maps_to_distinct_http_statuses(): + # Each failure class surfaces its own HTTP status at the edge. assert http_status(CredError.of_unauthorized("byok missing")) == 401 assert http_status(CredError.of_precondition_required("env var missing")) == 412 + assert http_status(CredError.of_not_implemented("stub arm")) == 501 async def test_passthrough_forwards_the_inbound_token(): @@ -242,9 +243,10 @@ async def test_self_contained_arms_never_read_the_inbound_token(): ], ) async def test_unimplemented_arms_fail_closed(config: dict): + # Stub arms signal not_implemented (-> 501), not misconfigured (-> 500 operator error). result = await PROVIDER.resolve(SUBJECT, _spec(config)) assert isinstance(result, Error) - assert result.error.tag == "misconfigured" + assert result.error.tag == "not_implemented" async def test_authorization_code_returns_a_valid_stored_token():