快速结论:当 OAuth 授权服务器的 RFC 8414 元数据里 authorization_endpoint 本身已经带查询参数(例如 ?tenant=acme、?prompt=select_account、?p=<policy>、?subdomain=xxx)时,MCP Python SDK 的客户端会再拼一个 ?,导致授权服务器把后续参数解析进原有参数值里,授权流程在同意页直接失败。优先确认你的 authorization_endpoint 是否带 query,并升级到包含修复的 SDK 版本。
适用环境:MCP Python SDK mcp 2.2.0、1.30.0,以及当时 main(提交 6affe5c,src/mcp/client/auth/oauth2.py:427);Python 3.14.7。该问题在 v1 与 v2 代码路径上均存在。TypeScript SDK 不受影响。
最快修复方案:暂无确认的一步修复方案。Issue 中提出的修复是使用 urlsplit/parse_qsl/urlunsplit 将授权参数合并进已有 query,而不是用 f-string 直接拼接;该修复已确认在 main 上复现并给出补丁计划与测试,但原 Issue 最终以重复问题(#2776)关闭,未在本文中记录已合并发布的修复版本。可优先尝试升级 SDK,或确认所用版本是否已包含该合并逻辑。
注意事项:在修复落地前,用户通常会通过子类覆写 _perform_authorization/授权 URL 构造逻辑来规避,这属于临时 workaround,不是官方方案。v1 中还涉及另一处相关代码 mcp.client.auth.utils.build_protected_resource_metadata_discovery_urls,其 Priority 2 分支只保留 path 而丢弃 query,需要一并核对。不要在没有验证的情况下自行假定某个版本已修复,建议以实际安装版本的源码行为为准。
问题场景
在使用 MCP Python SDK 的 OAuth 客户端连接需要 OAuth 授权的 MCP 服务器时,客户端会从服务器公告的 RFC 8414 元数据读取 authorization_endpoint,然后把授权参数拼到该 URL 后面跳转浏览器。
当授权服务器的 authorization_endpoint 本身带有查询参数时触发此问题。Issue 中提到的真实场景包括:
- Azure AD B2C 风格的租户/策略选择器
?p=<policy>; - 多租户同意应用上的环境/层级标签,例如 Nevermined 公告
https://nevermined.app/oauth/authorize?network=sandbox|live; - Salesforce 授权服务器返回的
?prompt=select_account(生产环境命中); - Datadog 自定义域名 OAuth 要求通过
?subdomain=<SUBDOMAIN>传递子域。
触发位置为客户端执行授权流程时的授权 URL 构造环节,最终表现为浏览器跳转地址里出现两个 ?。
报错原文
authorization_url = f"{auth_endpoint}?{urlencode(auth_params)}" # src/mcp/client/auth/oauth2.py:427 on main
advertised: https://auth.example.com/authorize?tenant=acme
sent: https://auth.example.com/authorize?tenant=acme?response_type=code&client_id=…&redirect_uri=…&state=…&code_challenge=…
# 服务器实际收到:
tenant = "acme?response_type=code"
并且没有 response_type —— a hard failure at the consent page
Issue 正文强调该行为违反 RFC 6749 §3.1:授权端点的 URI “MAY include an application/x-www-form-urlencoded formatted query component, which MUST be retained when adding additional query parameters”。
原因分析
原因比较明确:OAuthClientProvider._perform_authorization 在拼接授权 URL 时,未检查 auth_endpoint 是否已经含有 query component,直接用 f"{auth_endpoint}?{urlencode(auth_params)}" 追加。当端点本身带 query 时就会产生第二个 ?,而按照 URL 解析规则,第一个 ? 之后的全部内容被视为 query,因此第二个 ? 会被当作参数值的一部分,例如 tenant = "acme?response_type=code",同时真正的 response_type 参数丢失。
对比之下,TypeScript SDK 的 client/auth.js 使用 new URL(endpoint) + searchParams.set(...) 构造 URL,能保留已有 query,所以不受影响。这也从侧面印证问题出在 Python SDK 的字符串拼接方式上。
此外,评论中用户报告 v1 的 mcp.client.auth.utils:build_protected_resource_metadata_discovery_urls 在 Priority 2 分支中只保留 path、丢弃 query,属于同一类 URL 处理缺陷,需单独确认。
环境排查
- MCP Python SDK 版本:
mcp是否为2.2.0、1.30.0或当时的main;确认是否已包含授权 URL 合并 query 的修复。 - Python 版本:Issue 中确认环境为 Python 3.14.7,实际排查时记录你所在环境的 Python 版本。
- 授权服务器元数据:检查
authorization_endpoint是否带 query,例如?tenant=、?p=、?prompt=、?subdomain=、?network=。 - SDK 代码位置:v2/v1 检查
src/mcp/client/auth/oauth2.py中授权 URL 拼接处是否仍为 f-string 形式(main上为:427)。 - v1 额外检查:
mcp.client.auth.utils.build_protected_resource_metadata_discovery_urls的 Priority 2 分支是否使用urljoin而丢失 query。 - 客户端类型:若使用 TypeScript SDK 则不受影响,可用来对照验证是否为 Python SDK 侧问题。
解决步骤
- 先确认你的授权服务器
authorization_endpoint是否带 query。可在获取 RFC 8414 元数据后打印该字段,或在浏览器授权跳转时观察是否出现两个?。 - 检查当前安装的 MCP Python SDK 版本及源码中
oauth2.py的授权 URL 构造逻辑,确认是否为f"{auth_endpoint}?{urlencode(auth_params)}"。 - 确认是否为已修复版本。Issue 提出的修复方案是新增模块级 helper,用
urlsplit拆分端点、parse_qsl(parts.query, keep_blank_values=True)保留原有参数、再urlencode合并新参数、最后urlunsplit组装;普通端点输出与原来逐字节一致,带 query 的端点会把已有参数放在前面、新参数放在后面并以&连接。 - 如当前版本未包含修复,可优先尝试升级到包含该合并逻辑的 SDK 版本;若暂时无法升级,可参考 Issue 中用户的做法,在应用侧对 OAuth 客户端进行子类覆写,自行合并 query,作为临时规避。
- 若你走 v1 路径,还需同时确认
build_protected_resource_metadata_discovery_urls的 Priority 2 分支是否保留 query;该处缺陷与授权 URL 拼接属于同类问题,但修复点不同,需分别处理。 - 若你准备自行打补丁或提 PR,Issue 中给出的补丁计划包括:helper 单元测试(普通端点不变、已有 query 保留、空值保留)以及一个驱动
_perform_authorization_code_grant的流程测试,断言捕获到的跳转 URL 保留tenant=acme且response_type=code只出现在单个?之后。
验证方法
用带 query 的 authorization_endpoint(例如 https://auth.example.com/authorize?tenant=acme)驱动一次完整授权流程,检查浏览器实际跳转的 URL:
- 只出现一个
?; - 原有参数
tenant=acme仍在,且值没有被后续参数污染; response_type=code、client_id、redirect_uri、state、code_challenge等参数以&正常连接在原有 query 之后;- 授权服务器同意页能正常识别
response_type,不再出现tenant = "acme?response_type=code"这类解析结果。
如果是通过单元测试或流程测试验证,可断言捕获到的 redirect URL 同时满足“保留 tenant=acme”和“response_type=code 位于单个 ? 之后”这两个条件。
参考来源
modelcontextprotocol/python-sdk #3505
相关重复问题:modelcontextprotocol/python-sdk #2776(原 Issue 以重复问题关闭);关联 PR:#2779(已停滞,因 httpx2 重命名不再干净适用)。
AI 工具推荐
想把多个 AI 模型放在一个入口?
GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。
推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。
这个方案解决了吗?
可以继续搜索完整报错,或查看同一工具的其他排查指南。


