Summary
POST /revoke answers 400 invalid_request to a public client (token_endpoint_auth_method: none) that sends only token and client_id, which is what RFC 7009 allows for a client without credentials.
Cause
In mcp/server/auth/handlers/revoke.py the form model is
class RevocationRequest(BaseModel):
token: str
token_type_hint: Literal["access_token", "refresh_token"] | None = None
client_id: str
client_secret: str | None
client_secret: str | None has no default, so pydantic treats the field as required (nullable, but it must be present). A public client omits it, RevocationRequest.model_validate(dict(form_data)) fails and the handler returns 400 before the provider's revoke_token is called. ClientAuthenticator already handles the secret on its own (it reads it from the form or the Basic header and demands it only for a client registered with one), so the model does not need the field at all, or it needs = None.
Reproduction
Register a client with token_endpoint_auth_method: "none", obtain tokens, then POST /revoke with token=<refresh token>&client_id=<id>. Expected 200, actual 400 {"error": "invalid_request", ...}. Claude Code registers this way and hit it (mcp 2.2.0).
Suggested fix
client_secret: str | None = None (or drop the field).
Summary
POST /revokeanswers400 invalid_requestto a public client (token_endpoint_auth_method: none) that sends onlytokenandclient_id, which is what RFC 7009 allows for a client without credentials.Cause
In
mcp/server/auth/handlers/revoke.pythe form model isclient_secret: str | Nonehas no default, so pydantic treats the field as required (nullable, but it must be present). A public client omits it,RevocationRequest.model_validate(dict(form_data))fails and the handler returns400before the provider'srevoke_tokenis called.ClientAuthenticatoralready handles the secret on its own (it reads it from the form or the Basic header and demands it only for a client registered with one), so the model does not need the field at all, or it needs= None.Reproduction
Register a client with
token_endpoint_auth_method: "none", obtain tokens, thenPOST /revokewithtoken=<refresh token>&client_id=<id>. Expected200, actual400 {"error": "invalid_request", ...}. Claude Code registers this way and hit it (mcp 2.2.0).Suggested fix
client_secret: str | None = None(or drop the field).