Repository navigation
fix(mcp): preserve registration URL from header discovery - #12205
Conversation
wenshao
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
Not explored to full depth (tool budget reached): "agent 1a": could not execute packages/core/src/mcp/oauth-provider.test.ts in this worktree — vitest's globalSetup reports packages/core/dist/index.js missing, and cd pa…; "agent 1a": could not verify by mutation that the added test goes red with oauth-provider.ts:748-749 reverted, for the same reason (and I deliberately did not mutate the sh…; "agent 6a": running packages/core/src/mcp/oauth-provider.test.ts — vitest aborts on the unit-test build-prerequisites guard because this shared worktree has no packages/….
中文说明
已审查。 建议见行内评论。
未探索到全部深度(达到工具调用预算):"agent 1a":could not execute packages/core/src/mcp/oauth-provider.test.ts in this worktree — vitest's globalSetup reports packages/core/dist/index.js missing, and cd pa…;"agent 1a":could not verify by mutation that the added test goes red with oauth-provider.ts:748-749 reverted, for the same reason (and I deliberately did not mutate the sh…;"agent 6a":running packages/core/src/mcp/oauth-provider.test.ts — vitest aborts on the unit-test build-prerequisites guard because this shared worktree has no packages/…。
— DeepSeek/deepseek-v4.1-flash@2d473174 via Qwen Code /review (v0.24.4)
| registrationUrl: | ||
| discoveredConfig.registrationUrl ?? config.registrationUrl, |
There was a problem hiding this comment.
[Suggestion] R1-1: The fallback added here is missing from the sibling discovery merge, so an explicitly configured registrationUrl is still discarded on that path.
This change gives the WWW-Authenticate merge the fallback it needed. The structurally identical standard-discovery merge at oauth-provider.ts:774 was left as registrationUrl: discoveredConfig.registrationUrl,, so it still overwrites a configured URL with undefined whenever the discovered authorization-server metadata omits registration_endpoint — the field is optional in RFC 8414, and metadataToOAuthConfig assigns undefined when it is absent.
Trigger: a config that pins registrationUrl (no authorizationUrl, no clientId) against a server whose HEAD probe does not answer 401/307 with a usable www-authenticate header, so the standard-discovery branch at line 766 runs instead of the branch this PR fixes. The flow then re-derives a registration URL from the origin of config.authorizationUrl, finds no registration_endpoint there either, and ends at No client ID provided and dynamic registration not supported — even though the user configured a working registration endpoint. The same config now succeeds on the WWW-Authenticate path, so the outcome depends on which discovery path the server's response selects.
Witness — two-arm probe on that config:
PR arm THREW: No client ID provided and dynamic registration not supported
fetch: HEAD /v2/mcp -> GET oauth-protected-resource
-> GET oauth-authorization-server/tenant
-> GET oauth-authorization-server (no POST at all)
line 774 fixed RESOLVED
fetch: POST /tenant/dcr/register -> POST /tenant/token
// oauth-provider.ts:774
registrationUrl: discoveredConfig.registrationUrl ?? config.registrationUrl,registrationUrl is live user-facing configuration rather than an internal field: declared at packages/core/src/mcp/oauth-provider.ts:61, forwarded from settings at packages/cli/src/nonInteractive/control/controllers/systemController.ts:451, and hashed into the MCP transport-pool fingerprint at packages/core/src/tools/mcp-pool-key.ts:88 — so the value this preserves must be the value that fingerprint records.
Please also pin it: a test driving the standard-discovery path (no 401 challenge, mocked metadata without registration_endpoint) with a config carrying registrationUrl, asserting the registration POST lands on the configured URL. Revert line 774 to the bare assignment and that assertion should go red.
中文说明
[Suggestion] 这里新增的兜底写法漏掉了同级的另一个发现分支,显式配置的 registrationUrl 在那条路径上仍会被丢弃。
本次改动给 WWW-Authenticate 分支补上了它需要的兜底。但 25 行之下结构完全相同的标准发现分支(oauth-provider.ts:774)仍是 registrationUrl: discoveredConfig.registrationUrl,:当发现到的授权服务器元数据缺少 registration_endpoint(该字段在 RFC 8414 中是可选的,缺失时 metadataToOAuthConfig 会写入 undefined)时,这个显式 key 会用 undefined 覆盖 ...config 展开进来的值,用户显式配置的注册地址依然丢失。
触发条件:配置固定了 registrationUrl(没有 authorizationUrl、没有 clientId),而服务端的 HEAD 探测没有返回带可用 www-authenticate 头的 401/307,于是走的是 766 行的标准发现分支,而不是本 PR 修复的分支。随后代码从 config.authorizationUrl 的 origin 重新推导注册地址,那里同样没有 registration_endpoint,最终抛出 No client ID provided and dynamic registration not supported —— 尽管用户已经配置了可用的注册端点。同样的配置在 WWW-Authenticate 路径上修复后能够成功,因此结果取决于服务端响应走的是哪条发现路径。
证据(同配置的双分支探针):
PR 分支 THREW: No client ID provided and dynamic registration not supported
请求:HEAD /v2/mcp -> GET oauth-protected-resource
-> GET oauth-authorization-server/tenant
-> GET oauth-authorization-server (完全没有 POST)
774 行修复后 RESOLVED
请求:POST /tenant/dcr/register -> POST /tenant/token
// oauth-provider.ts:774
registrationUrl: discoveredConfig.registrationUrl ?? config.registrationUrl,registrationUrl 是对外的用户配置而非内部字段:声明于 packages/core/src/mcp/oauth-provider.ts:61,由 packages/cli/src/nonInteractive/control/controllers/systemController.ts:451 从设置透传,并参与 MCP 传输池指纹计算(packages/core/src/tools/mcp-pool-key.ts:88)—— 因此这里保留的值必须与指纹所记录的值一致。
另外请补上回归测试:走标准发现路径(无 401 挑战,mock 元数据不含 registration_endpoint),配置携带 registrationUrl,断言注册 POST 打到配置的 URL。把 774 行改回裸赋值后该断言应变为红色。
— DeepSeek/deepseek-v4.1-flash@2d473174 via Qwen Code /review (v0.24.4)
| const configWithoutClientAndAuthorizationUrl: MCPOAuthConfig = { | ||
| ...mockConfig, | ||
| }; |
There was a problem hiding this comment.
[Suggestion] R1-2: The ?? config.registrationUrl fallback this PR adds is not pinned by any test.
The new test builds its config by spreading mockConfig (which carries no registrationUrl) and deleting clientId/authorizationUrl/tokenUrl, so config.registrationUrl is undefined throughout and only the left operand of the added ?? is ever evaluated. The only test in the file that sets a configured registrationUrl keeps its authorizationUrl from mockConfig, so it never reaches either discovery merge.
Trigger: remove ?? config.registrationUrl from oauth-provider.ts:748-749 and run the suite — it stays green — while the behaviour that clause guards silently regresses: a pinned registration URL is lost whenever discovery advertises none, reproducing the failure described on the line above.
Witness — mutation on the unmodified PR:
mutant (fallback removed) packages/core Test Files 1 passed (1) | Tests 28 passed (28)
scenario probe THREW: No client ID provided and dynamic registration not supported
unmutated (lines restored) scenario probe RESOLVED
The clause is load-bearing and unwitnessed, so please add the missing case — same five-step mock chain, but with a config that carries the endpoint and mocked authorization-server metadata that omits registration_endpoint:
const configWithRegistrationUrl = {
...mockConfig,
registrationUrl: 'https://auth.example.com/pinned/register',
};
delete configWithRegistrationUrl.clientId;
delete configWithRegistrationUrl.authorizationUrl;
// authServerMetadata below omits registration_endpoint
// ...
// expect(mockFetch).toHaveBeenCalledWith(
// 'https://auth.example.com/pinned/register',
// expect.objectContaining({ method: 'POST' }),
// );mockConfig declares no registrationUrl, and metadataToOAuthConfig writes the key even when it is undefined, so the new case has to set the field on the config and omit registration_endpoint from the mocked metadata to actually reach the fallback.
中文说明
[Suggestion] 本 PR 新增的 ?? config.registrationUrl 兜底没有任何测试固定住它。
新测试用 mockConfig(不含 registrationUrl)展开后再删除 clientId/authorizationUrl/tokenUrl,因此 config.registrationUrl 始终为 undefined,新增 ?? 的右操作数从未被求值。文件中唯一设置了 registrationUrl 的测试保留了 mockConfig 里的 authorizationUrl,根本走不到任何一个发现分支的合并处。
触发条件:把 oauth-provider.ts:748-749 中的 ?? config.registrationUrl 删除后跑测试 —— 依然全绿 —— 但该子句守护的行为会静默退化:当发现结果不提供注册地址时,用户固定的注册地址会被丢弃,重现上一行描述的那个失败。
证据(在未修改的 PR 上做变异):
变异(删除兜底) packages/core Test Files 1 passed (1) | Tests 28 passed (28)
场景探针 THREW: No client ID provided and dynamic registration not supported
未变异(恢复该行) 场景探针 RESOLVED
该子句是承重的却无人固定,因此请补上缺失的用例 —— 沿用同一套五步 mock 链,但配置携带该端点,且 mock 的授权服务器元数据省略 registration_endpoint:
const configWithRegistrationUrl = {
...mockConfig,
registrationUrl: 'https://auth.example.com/pinned/register',
};
delete configWithRegistrationUrl.clientId;
delete configWithRegistrationUrl.authorizationUrl;
// 下面的 authServerMetadata 省略 registration_endpoint
// ...
// expect(mockFetch).toHaveBeenCalledWith(
// 'https://auth.example.com/pinned/register',
// expect.objectContaining({ method: 'POST' }),
// );mockConfig 没有声明 registrationUrl,而 metadataToOAuthConfig 即使值为 undefined 也会写入该 key,所以新用例必须在配置上显式设置该字段,并让 mock 元数据省略 registration_endpoint,才能真正走到兜底分支。
— DeepSeek/deepseek-v4.1-flash@2d473174 via Qwen Code /review (v0.24.4)
|
@qwen-code /triage |
Real-environment verification — PR #12205 @
|
| BASE | PR | |
|---|---|---|
| Outcome | No client ID provided and dynamic registration not supported (issue text, verbatim) |
POST auth.atlassian.com/<tenant>/dcr/register → 201, real client_id; TUI shows auth.atlassian.com/authorize?…&resource=https://mcp.atlassian.com/v2/mcp and waits for login |
| Requests | HEAD 401 → resource metadata 200 → tenant AS 200 (advertises DCR) → origin AS 200 (no DCR) → stops, no POST | HEAD 401 → resource metadata 200 → tenant AS 200 → POST 201 |
2. Local Atlassian-shaped server — full flow through the TUI
All runs go through /mcp → Authenticate in the real TUI. Each row is one server/config shape; outcomes come from the server ledger and the final TUI state.
| # | Scenario | BASE | PR |
|---|---|---|---|
| S1 | header discovery; tenant advertises DCR; nothing pinned (the issue) | ❌ issue error | ✅ POST tenant DCR → token → initialize with Bearer → connected, 2 tools |
| S2 | standard discovery (no WWW-Authenticate); registrationUrl pinned; nothing advertised (R1-1) |
❌ same error | ✅ POST pinned URL → connected |
| S3 | header discovery; URL pinned and tenant advertises one | ✅ POST pinned URL | ✅ POST tenant URL (pin ignored, see note B) |
| S4 | header discovery; URL pinned; tenant advertises nothing | ✅ POST pinned URL | ✅ POST pinned URL |
3. Tests and mutation (macOS, Node 24)
oauth-provider.test.ts: 29/29. The 12 MCP suites (src/mcp/*,mcp-client,mcp-client-manager): 457/457. CI on the head is green (Test ubuntu, Lint & Static, no-AK integration).
Mutant on oauth-provider.ts |
PR's tests |
|---|---|
M2: remove the added registrationUrl key from the header branch (= main) |
28/29, header test red ✅ |
M3: remove ?? config.registrationUrl from the standard branch (= main) |
28/29, standard-discovery test red ✅ |
| M4: M2 + M3 (= main) | 27/29 ✅ |
M1: remove ?? config.registrationUrl from the header branch |
29/29 green — survives |
M5: config-first (config.registrationUrl ?? discovered…) in both branches |
29/29 green (precedence isn't covered by any test) |
Notes for the author (non-blocking)
A. R1-2 is only half closed: the header-branch fallback still has no test. The new standard-discovery test covers the ?? in the standard branch; nothing covers the one in the header branch. That clause matters. With M1 applied, S4 (URL pinned, header discovery, tenant advertises nothing) regresses against main: BASE and PR both connect via the pinned URL, but the M1 bundle fails with the issue's error. Suggested test: suggested-header-fallback-test.diff. It is a copy of your first new test with three edits: the config pins registrationUrl, the tenant metadata omits registration_endpoint, and the assertion expects a POST to the pinned URL. It passes on the PR, fails on M1 (29/30), and M2–M4 still fail.
B. On the header path, precedence changes compared with main. On main, the header-discovery merge kept a pinned registrationUrl through ...config, so S3 posts to the pinned URL. With this PR the discovered endpoint wins, and the pin is silently ignored whenever the server advertises its own endpoint. The description says "Breaking changes: none", but this is a narrow behaviour change for anyone who pinned a URL on a header-discovery server. oauth.registrationUrl has no documented precedence. The intent may be "explicit config wins", the way setting authorizationUrl skips discovery entirely. If so, a one-token change does it: config.registrationUrl ?? discoveredConfig.registrationUrl in both merges. I ran that arm on S1–S4: tenant, pinned, pinned (same as main), pinned. All four connect, and your two tests still pass (M5). Whichever order you choose, please add a test for it. This is the maintainers' call and not a blocker.
A pre-existing gap outside this diff: startup auto-OAuth (suggest a follow-up issue)
In an interactive session, attemptAutomaticMcpOAuth → handleAutomaticOAuth (mcp-client.ts:1165) runs at startup, before the user opens /mcp. It has two problems. It hands the resource-metadata URL to discoverOAuthConfig() as if it were a server base URL, so the metadata document itself is never fetched. It also rebuilds the config without registrationUrl. BASE and PR behave identically here:
-
Real Atlassian. This path does not hit the issue's error. It finds the legacy AS at the MCP origin (
mcp.atlassian.com/.well-known/oauth-authorization-server), registers atmcp.atlassian.com/v1/register(201), and opens the browser atmcp.atlassian.com/v1/authorize. That is not the tenant AS the resource advertises. I didn't log in, so whether that token works on/v2/mcpis untested. This refines the sandbox report's S4: how this path fails depends on the server's shape, and for the actual Atlassian deployment it detours to the wrong AS. -
Local server (no AS at the MCP origin): discovery fails and the status bar shows
1 MCP offline. Manual Authenticate then works on the PR arm. -
Candidate fix, 2 edits:
candidate-auto-path.diff.- Try
discoverOAuthFromWWWAuthenticate(wwwAuthenticate)first, falling back to the current lookup. - Forward
registrationUrl ?? mcpServerConfig.oauth?.registrationUrl.
On real Atlassian, startup becomes HEAD → resource metadata → tenant AS → tenant DCR 201 →
auth.atlassian.com/authorize: 4 requests instead of 9. On the local server it connects at startup with no manual step, for both S1 and S2 (pinned URL).tsc --noEmitis clean andmcp-client/mcp-client-managerpass 256/256. - Try
-
The second rebuilt-config site cited by the sandbox report (around
mcp-client.ts:2251, insideconnectToMcpServer) has no production importer and appears 0 times in the CLI bundle. Only the:1165site is live.
I'm not suggesting this for this PR: it's a separate code path and needs its own tests.
Other
- fix(core): preserve MCP OAuth registration URL during discovery #12233 produces the byte-identical
oauth-provider.ts(blobc92942063d5); only the tests differ. Whichever one lands, the other can be closed. - Not covered: completing an Atlassian login/consent (no account used); Windows/Linux (the author tested Windows; CI ubuntu is green); the OpenTUI renderer's
/mcppath (it makes the sameauthenticate()call, but I didn't drive it).
Evidence, candidate diffs and the fake server are on wenshao/qwen-code@aa35e41c (branch assets-pr12205).
中文版
真实环境验证 — PR #12205 @ 7bab1efc(macOS,真实 CLI + 真实 Atlassian)
结论:✅ 建议合并。 在真实 TUI 里对真实的 https://mcp.atlassian.com/v2/mcp 执行 /mcp → Authenticate,现在会在 tenant 路径上完成动态客户端注册:Atlassian 返回 201 并签发 client ID。base 构建则报出与 #12165 完全相同的错误。此前评审的 R1-1 已修复。另有两处不阻断合并的小问题留给作者:缺一个测试(R1-2 只关了一半),以及一个优先级取舍。启动时自动 OAuth 路径有一个既有缺口,不在本 diff 范围内,建议另开 issue。
本报告是对沙箱验证的补充。沙箱验证在 dist 层对 loopback 服务做了 A/B,并注明未覆盖真实 Atlassian/浏览器;下面只写新增内容。
装置
- 两臂。
PR= 从7bab1efc构建的 CLI bundle;BASE= 同一份构建,只回退改动的两个registrationUrl表达式。把 chunk 文件名里的哈希归一化后,两个 bundle 只有一个 chunk 不同,且该 chunk 内只差这两处表达式。merge-base 为790bd83c;此后 main 没有改动oauth-provider.ts、oauth-utils.ts、mcp-client.ts。 - 运行方式。 真实
qwenTUI 跑在 tmux 中,QWEN_HOME/QWEN_RUNTIME_DIR隔离。配置为{"httpUrl": …, "oauth": {"enabled": true}},不带clientId。BROWSER指向一个记录 URL 的桩;只有本地服务那一面,桩还会跟随 URL 完成回调。另用--import预加载记录每一次全局fetch。 - 目标。 (a) 真实 Atlassian,停在登录之前,未使用任何 Atlassian 账号。(b) 复刻 Atlassian 形态的本地服务(
fake-atlassian.mjs):401 +resource_metadata,带路径的授权服务器(AS)公布…/<tenant>/dcr/register,origin 上的 AS 不带registration_endpoint。在这个服务上可以走完整个流程。
1. 真实 Atlassian:/mcp → Authenticate(见图 1)
| BASE | PR | |
|---|---|---|
| 结果 | No client ID provided and dynamic registration not supported(与 issue 原文一致) |
POST auth.atlassian.com/<tenant>/dcr/register → 201,拿到真实 client_id;TUI 给出 auth.atlassian.com/authorize?…&resource=…/v2/mcp 并等待登录 |
| 请求 | HEAD 401 → 受保护资源元数据 200 → tenant AS 200(含 DCR)→ origin AS 200(无 DCR)→ 停止,没有 POST | HEAD 401 → 受保护资源元数据 200 → tenant AS 200 → POST 201 |
2. 本地 Atlassian 形态服务:TUI 全流程(见图 2)
全部通过真实 TUI 的 /mcp → Authenticate 触发。每一行对应一种服务/配置形态;结果取自服务端请求账本和 TUI 最终状态。
| # | 场景 | BASE | PR |
|---|---|---|---|
| S1 | header 发现;tenant 公布 DCR;未钉配置(即 issue) | ❌ issue 报错 | ✅ POST 到 tenant DCR → token → 带 Bearer 的 initialize → connected,2 个工具 |
| S2 | 标准发现(无 WWW-Authenticate);钉了 registrationUrl;服务端未公布(R1-1) |
❌ 同一报错 | ✅ POST 到钉住的 URL → connected |
| S3 | header 发现;既钉了 URL,tenant 也公布了一个 | ✅ POST 到钉住的 URL | ✅ POST 到 tenant URL(忽略钉住的值,见注 B) |
| S4 | header 发现;钉了 URL;tenant 未公布 | ✅ POST 到钉住的 URL | ✅ POST 到钉住的 URL |
3. 测试与变异(macOS,Node 24)
oauth-provider.test.ts29/29;12 个 MCP 测试套件(src/mcp/*、mcp-client、mcp-client-manager)457/457;head 上的 CI 为绿(Test ubuntu、Lint & Static、no-AK 集成测试)。- M2(去掉 header 分支新增的
registrationUrl键,等同 main)/ M3(去掉标准分支的?? config.registrationUrl,等同 main)/ M4(M2 + M3,等同 main):均被新测试杀死。 - M1(去掉 header 分支的
?? config.registrationUrl)29/29 全绿,存活。 - M5(两处都改成配置优先)29/29 全绿:目前没有测试覆盖优先级。
给作者的建议(非阻断)
A. R1-2 只关了一半:header 分支的回退仍然没有测试。 新增的标准发现测试覆盖了标准分支的 ??,header 分支的那个没有任何测试覆盖。这个子句是承重的:应用 M1 后,S4(钉了 URL、header 发现、tenant 未公布)相对 main 会回归——BASE 和 PR 都用钉住的 URL 连上,M1 的 bundle 却报出 issue 的错误。建议补充的测试:suggested-header-fallback-test.diff。它复制你新增的第一个测试并改三处:配置钉上 registrationUrl,tenant 元数据去掉 registration_endpoint,断言改为 POST 到钉住的 URL。该测试在 PR 上通过、在 M1 上失败(29/30),M2–M4 仍然失败。
B. header 路径上的优先级相对 main 发生了变化。 main 的 header 发现合并通过 ...config 保留了钉住的 registrationUrl,所以 S3 会 POST 到钉住的 URL。本 PR 改为发现值优先:只要服务端公布了自己的端点,钉住的值就会被静默忽略。PR 描述写着「Breaking changes: none」,但对在 header 发现类服务上钉过 URL 的用户,这是一个范围很窄的行为变化。oauth.registrationUrl 的优先级没有文档说明。如果本意是「显式配置优先」(就像设置 authorizationUrl 会直接跳过发现),一处单 token 的改动即可:两处合并都写成 config.registrationUrl ?? discoveredConfig.registrationUrl。这一臂在 S1–S4 上依次是 tenant、钉住、钉住(与 main 相同)、钉住,四个场景都能连上,你的两个测试也仍然通过(M5)。无论选择哪种顺序,都请补一个测试。这由维护者决定,不阻断合并。
diff 之外的既有缺口:启动时自动 OAuth(建议另开 issue;见图 3)
交互模式下,attemptAutomaticMcpOAuth → handleAutomaticOAuth(mcp-client.ts:1165)会在启动时、用户打开 /mcp 之前运行。它有两个问题:把受保护资源元数据的 URL 当成服务器基址交给 discoverOAuthConfig(),因此元数据文档本身从未被请求;重建的配置里也丢掉了 registrationUrl。BASE 与 PR 在这条路径上的行为完全相同:
-
真实 Atlassian。 这条路径不会报 issue 的错误。它在 MCP origin 上找到旧版 AS(
mcp.atlassian.com/.well-known/oauth-authorization-server),在mcp.atlassian.com/v1/register注册(201),并用浏览器打开mcp.atlassian.com/v1/authorize。这不是资源公布的 tenant AS。我没有登录,所以这个 token 能否用于/v2/mcp未验证。这一点是对沙箱报告 S4 的细化:这条路径怎么失败取决于服务形态,对真实的 Atlassian 部署来说是绕到了错误的 AS。 -
本地服务(MCP origin 上没有 AS):发现失败,状态栏显示
1 MCP offline;之后在 PR 臂上手动 Authenticate 可以成功。 -
候选修复,两处改动:
candidate-auto-path.diff。- 先用
discoverOAuthFromWWWAuthenticate(wwwAuthenticate)发现,失败再回退到现有查找; - 传递
registrationUrl ?? mcpServerConfig.oauth?.registrationUrl。
真实 Atlassian 上,启动流程变为 HEAD → 受保护资源元数据 → tenant AS → tenant DCR 201 →
auth.atlassian.com/authorize,请求数由 9 个减为 4 个。本地服务上,S1 和 S2(钉住 URL)都能在启动时直接连上,无需手动操作。tsc --noEmit干净,mcp-client/mcp-client-manager256/256 通过。 - 先用
-
沙箱报告提到的第二处(
mcp-client.ts约 2251 行,位于connectToMcpServer内)没有任何生产代码引用,CLI bundle 中出现次数为 0;只有:1165这一处是活的。
不建议并入本 PR:这是另一条代码路径,需要自己的测试。
其他
- fix(core): preserve MCP OAuth registration URL during discovery #12233 改出的
oauth-provider.ts与本 PR 字节级相同(blobc92942063d5),只有测试不同。无论合入哪个,另一个都可以关闭。 - 未覆盖: 完成 Atlassian 登录/授权(未使用账号);Windows/Linux(作者测了 Windows,CI ubuntu 为绿);OpenTUI 渲染器的
/mcp路径(调用的是同一个authenticate(),但我没有实际驱动)。
证据图、候选 diff 与本地假服务位于 wenshao/qwen-code@aa35e41c(分支 assets-pr12205)。
|
@qwen-code /triage |
…s-p0-p8 Brings in main through e944a23: QwenLM#12709 (workspace-bound Managed sessions admitted without execution), QwenLM#12205 (MCP registration URL from header discovery) and the v0.24.6 release. The one conflict is EmbeddedRuntimeBrokerTest. It takes QwenLM#12709's test that a workspace-bound Session cannot resolve the global runtime workspace: this branch has no Hosted Workspace execution either, so the new fail-closed rule in the embedded Broker's session resolver applies here as it does on main. main's test that the embedded Broker answers 501 for prepare, start and resolve stays out, since this branch's Broker serves those routes. The rule merged into the branch's own EmbeddedRuntimeBroker without the RuntimeBrokerException import that main's copy already had, so the merge adds it.
Main already carries this branch's source fix: QwenLM#12205 landed a byte-identical registrationUrl preservation change to MCPOAuthProvider.authenticate, so oauth-provider.ts needed no resolution and now matches main exactly. The only real collision was in the shared createMockResponse test helper. This branch generalized it to a `headers` map; main added a narrow `wwwAuthenticate` shorthand for the QwenLM#12205 coverage, and QwenLM#13007 then rewrote the surrounding suite. Keep both options so neither side's tests change, in the lookup order the merged body already implements: explicit `headers`, then `contentType`, then `wwwAuthenticate`. Both sides' registration-URL tests are retained. They are overlapping coverage of the same fix rather than a conflict.



What this PR does
Preserve the dynamic client registration endpoint discovered through the
WWW-AuthenticateOAuth flow, while retaining an explicitly configured registration URL when discovery does not provide one.Why it's needed
Some MCP servers advertise a path-scoped authorization server and its dynamic client registration endpoint through protected-resource metadata. The
WWW-Authenticatediscovery branch copied the authorization URL, token URL, and scopes but droppedregistrationUrl, so authentication fell back to origin-only metadata and failed before opening the browser.Reviewer Test Plan
How to verify
Configure an MCP server with no client ID or authorization URL, return a
401response with aWWW-Authenticateresource-metadata challenge, and have the discovered authorization server advertise a path-scopedregistration_endpoint. The flow should POST client registration to that discovered path and continue to the token exchange instead of raisingNo client ID provided and dynamic registration not supported.The regression test covers the complete mocked path: header challenge, protected-resource metadata, path-scoped authorization-server metadata, dynamic registration, token exchange, and OAuth callback.
Evidence (Before & After)
N/A — no user interface change. Before this change the regression test rejects with
No client ID provided and dynamic registration not supported; after this change it passes.Tested on
Environment
Windows 11, Node.js 24.15.0, pnpm 11.24.0, focused Vitest run in
packages/core.Risk & Scope
prepare.AI assistance
Linked Issues
Fixes #12165
中文说明
本 PR 做了什么
保留
WWW-AuthenticateOAuth 流程发现到的动态客户端注册端点;当发现结果没有注册地址时,继续保留用户显式配置的registrationUrl。为什么需要它
部分 MCP 服务会通过受保护资源元数据公布带路径的授权服务器及其动态客户端注册端点。当前的
WWW-Authenticate发现分支复制了授权地址、令牌地址和 scopes,却丢弃了registrationUrl,导致认证流程退回到仅按 origin 查询元数据,并在打开浏览器之前失败。Reviewer Test Plan
如何验证
配置一个没有 client ID 和 authorization URL 的 MCP 服务,使其返回带
WWW-Authenticate资源元数据挑战的401响应,并让发现到的授权服务器公布一个带路径的registration_endpoint。流程应向发现到的路径发起客户端注册 POST 请求,并继续令牌交换,而不是抛出No client ID provided and dynamic registration not supported。回归测试覆盖完整的 mock 流程:header challenge、受保护资源元数据、带路径的授权服务器元数据、动态注册、令牌交换和 OAuth 回调。
证据(Before & After)
N/A — 没有用户界面变化。修复前回归测试以
No client ID provided and dynamic registration not supported拒绝;修复后通过。测试环境
环境
Windows 11、Node.js 24.15.0、pnpm 11.24.0,在
packages/core中运行定向 Vitest。风险与范围
prepare。AI 辅助