diff --git a/litellm/proxy/gateway/mcp/outbound_credentials/types.py b/litellm/proxy/gateway/mcp/outbound_credentials/types.py index 3ae9972264e..dc4a7ef6c79 100644 --- a/litellm/proxy/gateway/mcp/outbound_credentials/types.py +++ b/litellm/proxy/gateway/mcp/outbound_credentials/types.py @@ -138,15 +138,22 @@ ApiKeyScheme = Literal["bearer", "apikey", "basic", "token", "raw"] class AuthorizationCodeConfig(BaseModel): - """Per-user 3LO; the gateway is the OAuth client and stores the user's token.""" + """Per-user 3LO; the gateway is the OAuth client and stores the user's token. + + Endpoints are discovered (RFC 9728 -> RFC 8414) and the client is registered via DCR + (RFC 7591), so the common case carries none of the fields below; they are optional manual + overrides for IdPs without discovery / DCR. The discovered endpoints and the DCR-registered + client are persisted by the AS surface, not here; `resolve()` reads the per-user token from + the `TokenStore`. + """ model_config = ConfigDict(frozen=True) kind: Literal[AuthSpecKind.authorization_code] = AuthSpecKind.authorization_code - client_id: str - client_secret: SecretStr - authorization_url: str - token_url: str scopes: tuple[str, ...] = () + client_id: str | None = None + client_secret: SecretStr | None = None + authorization_url: str | None = None + token_url: str | None = None class ClientCredentialsConfig(BaseModel): diff --git a/tests/mcp_tests/gateway/test_resolver.py b/tests/mcp_tests/gateway/test_resolver.py index e9bc8d0b68f..eecff64103d 100644 --- a/tests/mcp_tests/gateway/test_resolver.py +++ b/tests/mcp_tests/gateway/test_resolver.py @@ -186,9 +186,16 @@ def test_auth_spec_kind_is_derived_from_config(): def test_discriminated_union_rejects_config_missing_required_fields(): - # authorization_code requires client_id/secret/urls; an empty body must fail at construction. + # client_credentials requires client_id/secret/token_url; an empty body must fail. with pytest.raises(ValidationError): - _spec({"kind": "authorization_code"}) + _spec({"kind": "client_credentials"}) + + +def test_authorization_code_config_allows_discovery_defaults(): + # Discovery + DCR: endpoints and client creds are obtained at runtime, so an empty + # authorization_code config is valid - the fields are optional manual overrides. + spec = _spec({"kind": "authorization_code"}) + assert isinstance(spec.config, AuthorizationCodeConfig) def test_discriminated_union_picks_the_variant_by_kind():