OAuth client: authorization URL is built with a second `?` when the advertised `authorization_endpoint` already carries a query (RFC 6749 §3

当 OAuth 授权服务器的 RFC 8414 元数据里 authorization_endpoint 本身已经带查询参数(例如 ?tenant=acme 、 ?prompt=select_account 、 ?p= 、 ?subdomain=xxx )时,MCP Python SD

快速结论:当 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 侧问题。

解决步骤

  1. 先确认你的授权服务器 authorization_endpoint 是否带 query。可在获取 RFC 8414 元数据后打印该字段,或在浏览器授权跳转时观察是否出现两个 ?。
  2. 检查当前安装的 MCP Python SDK 版本及源码中 oauth2.py 的授权 URL 构造逻辑,确认是否为 f"{auth_endpoint}?{urlencode(auth_params)}"。
  3. 确认是否为已修复版本。Issue 提出的修复方案是新增模块级 helper,用 urlsplit 拆分端点、parse_qsl(parts.query, keep_blank_values=True) 保留原有参数、再 urlencode 合并新参数、最后 urlunsplit 组装;普通端点输出与原来逐字节一致,带 query 的端点会把已有参数放在前面、新参数放在后面并以 & 连接。
  4. 如当前版本未包含修复,可优先尝试升级到包含该合并逻辑的 SDK 版本;若暂时无法升级,可参考 Issue 中用户的做法,在应用侧对 OAuth 客户端进行子类覆写,自行合并 query,作为临时规避。
  5. 若你走 v1 路径,还需同时确认 build_protected_resource_metadata_discovery_urls 的 Priority 2 分支是否保留 query;该处缺陷与授权 URL 拼接属于同类问题,但修复点不同,需分别处理。
  6. 若你准备自行打补丁或提 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 重命名不再干净适用)。

GamsGo AI

AI 工具推荐

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

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

了解 GamsGo AI

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

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 28926

发表回复

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