RevocationRequest requires client_secret, so public clients get 400 from /revoke

公共客户端( token_endpoint_auth_method: "none" )调用 MCP Python SDK 的 POST /revoke 时只提交 token 和 client_id ,会收到 400 invalid_request 。优先排查 mcp/server/auth/hand

快速结论:公共客户端(token_endpoint_auth_method: "none")调用 MCP Python SDK 的 POST /revoke 时只提交 token 和 client_id,会收到 400 invalid_request。优先排查 mcp/server/auth/handlers/revoke.py 中请求模型的字段定义:client_secret 缺少默认值,被 Pydantic 当成必填字段。

适用环境:MCP Python SDK;Issue 报告者在 mcp 2.2.0 下通过 Claude Code 触发,指定标签为 v1、v2。其余 Python 版本、操作系统等环境信息 Issue 未明确给出。

最快修复方案:暂无确认的一步修复方案。Issue 已被维护者标记为 #3508 的重复项并关闭,未记录合并后的修复验证。可优先尝试 Issue 中建议的改法:把 RevocationRequest.client_secret 改为 client_secret: str | None = None,或直接删除该字段,由 ClientAuthenticator 统一完成客户端凭据校验。

注意事项:该改法来自 Issue 讨论中的建议,不是已验证的发布修复;升级 SDK 前请先确认目标版本是否包含对应改动。改模型后应回归测试机密客户端的两种认证方式(表单 secret 与 Basic 头),避免把认证校验职责分散到请求模型里。

问题场景

使用 MCP Python SDK 搭建带 OAuth 认证的 MCP 服务端时,按 token_endpoint_auth_method: "none" 注册公共客户端,获取 token 后调用 POST /revoke 撤销 refresh token。请求体只带 token 和 client_id,符合 RFC 7009 对无凭据客户端的要求,但服务端在进入 provider 的 revoke_token 之前就返回了 400。Issue 报告者是通过 Claude Code 触发该问题的。

报错原文

POST /revoke → 400 invalid_request
{"error": "invalid_request", ...}

# 根因代码(mcp/server/auth/handlers/revoke.py)
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 虽标注为可空类型,但没有写 = None,Pydantic 会将其视为必填字段:值可以为 null,但键必须出现。公共客户端按 RFC 7009 不提交 client_secret,RevocationRequest.model_validate(dict(form_data)) 校验失败,handler 在认证与撤销逻辑执行前直接返回 400。

讨论中还指出两点:其一,ClientAuthenticator 本身已负责读取表单或 Basic 头中的 secret,且只对注册了 secret 的客户端强制要求,请求模型不必重复这个校验;其二,同样的“Optional 但无默认值”写法如果出现在 token、register 等其他 OAuth 表单模型中,可能以相同方式导致问题。

环境排查

  • 确认 MCP Python SDK 版本(Issue 中为 mcp 2.2.0),检查所用版本是否已包含 #3508 对应修复。
  • 确认客户端注册时 token_endpoint_auth_method 的实际值是否为 "none"。
  • 抓取 POST /revoke 请求体,确认是否只包含 token 与 client_id。
  • 检查 mcp/server/auth/handlers/revoke.py 中 RevocationRequest 的字段定义。
  • Issue 未提供 Python、操作系统等版本信息,无需作为排查前提。

解决步骤

  1. 打开 mcp/server/auth/handlers/revoke.py,定位 RevocationRequest 模型定义。
  2. 将 client_secret: str | None 改为 client_secret: str | None = None;如果确认 ClientAuthenticator 已完全覆盖该字段的用途,也可以直接删除 client_secret 字段。
  3. 不要在新的模型层额外强制校验 secret,认证职责保留在 ClientAuthenticator,确保公共客户端和机密客户端两条路径都不受影响。
  4. 根据讨论建议,同类改动可一并检查 /revoke 的“未知或已失效 token 也应返回 200”行为,以及 revoke、token、register 三个模型是否存在相同的无默认值 Optional 字段。
  5. 补回归测试,覆盖:公共客户端 refresh token 无 secret 返回 200;公共客户端带匹配 token_type_hint 的 access token 返回 200;公共客户端未知 token 返回 200;机密客户端表单 secret 返回 200;机密客户端 Basic 头返回 200;机密客户端 secret 错误或缺失返回 400 或 401;重复撤销两次均返回 200。

验证方法

以公共客户端重新执行 POST /revoke,请求体只含 token=<refresh token>&client_id=<id>,确认返回 200 而非 400,并且 provider 的 revoke_token 被实际调用。同时按上面的测试矩阵验证机密客户端两种认证方式仍正常。

参考来源

modelcontextprotocol/python-sdk #3648

Issue 讨论中提及的重复项:#3508(未提供链接,可提示用户在仓库中自行检索)。

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 31562

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注