快速结论:在使用 MCP Python SDK 搭建授权服务器、开启 identity_assertion_enabled=True 并接入企业托管授权(ID-JAG)时,凡是使用 CIMD(Client ID Metadata Document)作为 client_id、不带 client_secret 的公共客户端,都会在 jwt-bearer 换令牌这一步被 TokenHandler 直接拒绝,报错为 Server TokenHandler rejects the jwt-bearer (ID-JAG) grant from public CIMD clients, which the enterprise-managed authorization extension permits。优先排查 TokenHandler 是否仍按“有无 client_secret”判断客户端认证,而不是按解析出的 token_endpoint_auth_method 判断。
适用环境:Issue 已确认的范围是 MCP Python SDK 的授权服务器端(mcp/server/auth/handlers/token.py、ClientAuthenticator),标签为 v2;触发条件是开启 identity_assertion_enabled=True,并配置了 ext-auth 企业托管授权扩展。Issue 未提供 Python、操作系统、依赖等具体版本,不要臆补。
最快修复方案:暂无确认的一步修复方案。Issue 中确认的修复方向是:改为根据解析后的 token_endpoint_auth_method 决定是否要求客户端认证,对 none 的 CIMD 客户端跳过客户端认证,并把安全边界放到 ID-JAG 校验上。对应 PR 为 #3606,但讨论中尚未确认其已合并落地,可优先关注该 PR 或自行按此思路修补。
注意事项:放开“无密钥”客户端不等于放弃校验。按讨论,none 路径必须继续校验 ID-JAG(iss、aud 须为 AS token endpoint、exp 含时钟偏差、断言中的 client_id 必须与请求携带的 client_id 一致),否则公共客户端可能兑换签发给其他客户端的断言。另外对 CIMD 客户端而言,授权服务器需先解析客户端文档才能知道其认证方式,因此认证方式查询必须放在客户端解析之后。是否属于 CIMD 客户端,讨论中倾向由 provider 钩子判断,而非简单检查 client_id 是否为 URL 形状。
问题场景
用户基于 MCP Python SDK 实现授权服务器,启用 identity_assertion_enabled=True,并按 modelcontextprotocol/ext-auth 的企业托管授权扩展支持 ID-JAG 流程。客户端以 CIMD 文档 URL 作为 client_id,声明 token_endpoint_auth_method="none",在 grant_types 中含 urn:ietf:params:oauth:grant-type:jwt-bearer,且不能持有 client_secret。该客户端携带 grant_type、assertion、client_id、scope、resource 发起换令牌请求时,在 provider 看到断言之前就被 TokenHandler 拒绝,返回 400 unauthorized_client。
报错原文
Server TokenHandler rejects the jwt-bearer (ID-JAG) grant from public CIMD clients, which the enterprise-managed authorization extension permits
error: unauthorized_client
error_description: The JWT bearer grant requires a confidential client
原因分析
最可能的原因是 TokenHandler 中的守卫把“客户端认证”和“grant 处理”混在一起:只要 client_info.client_secret 为空就拒绝 jwt-bearer 请求,而不看客户端声明的 token_endpoint_auth_method。
而 CIMD 规范本身禁止 client_secret_*,CIMD 客户端只能使用 none 或 private_key_jwt;同时 ClientAuthenticator 未实现 private_key_jwt,于是任何 CIMD 客户端都无法完成 ID-JAG 交换。按扩展规范第 5 节,CIMD 客户端在换取访问令牌这一步的客户端认证是可选的,请求示例本身也不带客户端认证,因此当前检查比规范更严格。
讨论中提出的修正思路是:先解析 grant_type,对 jwt-bearer 按 client_id 解析客户端(先查预注册,否则拉取 CIMD 文档);再按解析出的认证方式分别处理——client_secret_basic/client_secret_post 保持要求密钥,private_key_jwt 校验客户端断言(需补 ClientAuthenticator 分支),none 则完全跳过客户端认证(与 #1842/#1846 相关)。
环境排查
- 确认 MCP Python SDK 版本(本 Issue 属
v2分支范围)。 - 确认授权服务器已设置
identity_assertion_enabled=True。 - 确认客户端
client_id为 CIMD 文档 URL(https 且带路径),且token_endpoint_auth_method="none"、grant_types含 jwt-bearer。 - 检查
mcp/server/auth/handlers/token.py中是否仍有if not client_info.client_secret形式的拒绝逻辑。 - 确认
ClientAuthenticator对应版本是否已实现none与private_key_jwt分支。 - 确认所用 provider 是否提供(或需要新增)识别 CIMD 客户端的钩子,例如
is_metadata_document_client之类由 discussion 提出的方案,具体名称以实际版本为准。
解决步骤
- 先复现:用 CIMD 客户端(https
client_id、token_endpoint_auth_method="none"、grant_types含 jwt-bearer)提交带合法 ID-JAG 且无client_secret的 jwt-bearer 请求,确认返回 400unauthorized_client,且错误发生在断言校验之前。 - 定位
TokenHandler中基于client_secret是否存在的拒绝分支,这是讨论中唯一被指出的阻断点。 - 调整处理顺序:先解析
grant_type,对 jwt-bearer 按client_id解析客户端;CIMD 客户端需先拉取文档,再读取其token_endpoint_auth_method。 - 按认证方式分支处理:
client_secret_*维持现行要求;private_key_jwt校验客户端断言(需补实现);none跳过客户端认证,但保留预注册客户端的原有行为。 - 在
none路径补齐 ID-JAG 校验:iss、aud指向 AS token endpoint、exp含时钟偏差、断言client_id声明与请求client_id一致。 - 判断客户端是否为 CIMD 客户端时,优先由 provider 决定(讨论倾向钩子方式),不要仅凭
client_id的 URL 形状判断。 - 若不愿自行改代码,可优先关注并试用 Issue #3606 的修复分支;该 PR 在讨论中尚被 bot 关闭、待维护者指派,合并状态未确认。
验证方法
用同一 CIMD 客户端重放请求:请求应不再返回 unauthorized_client,而是进入断言校验并成功换得令牌;同时验证以下反向场景仍被拒绝——断言 client_id 与请求 client_id 不一致、aud 非本 AS token endpoint、断言过期,以及预注册的机密客户端缺少密钥或密钥错误。若仍被拒,确认实际运行代码已包含修复而非旧版本。
参考来源
modelcontextprotocol/python-sdk #3598
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。


