Repository navigation
fix(mcp): serve /mcp-scoped protected resource metadata and CORS preflight on token/register (#3807) - #5622
fix(mcp): serve /mcp-scoped protected resource metadata and CORS preflight on token/register (#3807)#5622springfall2008 wants to merge 2 commits into
Conversation
…light on token/register (#3807) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
springfall2008
left a comment
There was a problem hiding this comment.
Automated comment from the triage bot.
Reviewed at head 882212d (parent is current main d17f0e0, so nothing stale). No correctness bugs found: the route block moved into add_routes() is verbatim-identical to main's inline registration (same handlers, same order, catch-all still last), and the six new routes match the paths that 404'd in the reporter's log. 1 finding inline (apps/predbat/web_mcp.py:121, low) and 1 in the body, both non-blocking.
What I checked beyond the diff:
- The new test's expectations hold against the aiohttp 3.14.3 the coverage venv installs:
UrlDispatcher.resolveis async (soasyncio.run(app.router.resolve(request))is valid), and an unmatched method/path falls through the resource-index walk to the{tail:.*}catch-all — which reproduces the pre-fixDefault route for /oauth/token - 404the log showed, and makes the red-green claim plausible. All six new routes are pinned as routed to their intended handler rather thandefault_route; with the fix reverted each of those six assertions fails. - The test is registered in the module's runner (
run_web_mcp_tests), the module is in TEST_REGISTRY and not marked slow, sorun_all --quickexercises it. "preflight" already appears on main (web_mcp.py:277), so the cspell dictionary is not a problem for the new wording. - The #4799 deferral — keeping
resource: <base>on the /mcp-scoped metadata document — holds against the code:html_mcp_postvalidates the JWT audience againsthttp://{host}(web_mcp.py:917) and both token handlers copy the request'sresourceparameter into the token'saud, so a client that took<base>/mcpfrom the metadata would be issued a token this server then rejects. Right call to settle identity with #4799.
docs/components.md:164 — the docs guidance asked for in the issue is not addressed
gcoan's collaborator comment in #3807 asks for the PR plus "any guidance to the docs for others". The "How to configure your MCP client (mcp)" section of docs/components.md documents only the manual Authorization: Bearer-header VSCode config — exactly the setup that predates this issue — and says nothing about OAuth-based clients like the claude.ai custom connector, which is the audience this issue is about. The PR body's reason for skipping docs ("the fix needs no user configuration") answers whether a config item changed, but not the ask: users hitting the claude.ai handshake now succeed by luck of a fixed route table with no page telling them it should work, or what works and what is still limited. A short note in that section (OAuth-capable clients should now connect; which endpoints the server serves at /.well-known/...; the resource/audience identity caveat pointing at #4799) — or a comment on the issue saying the guidance will land with the #4799 work — would satisfy it.
…source caveat (#3807) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Automated reply from the triage bot. On the review-body finding (
|
This is an automated draft PR generated from issue #3807 — a maintainer should review it before merging.
Fixes #3807
Summary
claude.ai's OAuth handshake against the MCP server failed after authorization had already succeeded. Two probes were falling through to the catch-all
default_route404:/.well-known/oauth-protected-resource/mcpOPTIONSrequests on/oauth/token,/token,/oauth/registerand/registerThis PR adds those six routes, as diagnosed in the issue by @hamiltongross. The scoped path reuses
oauth_protected_resource_metadata, and the preflight requests reusehandle_options.So that the routes can be tested without binding a port, route registration moves out of
start()into a newadd_routes(app)method. The existing routes are unchanged: same paths, same handlers, same order, with the catch-all still last.Testing
test_mcp_oauth_routesintest_web_mcp.py. It resolves each path through the real aiohttp router viamake_mocked_request, checks that it reaches the intended handler (notdefault_route), and checks the scoped metadata body and the CORS headers on the preflight response.git stash push -u -- apps/predbat/web_mcp.py), it fails withAttributeError: ... no attribute 'add_routes'. That only proves the helper is missing.add_routeskept but the six new route lines removed, the assertions themselves fail. All six probes route todefault_route, and the token preflight returns 404. These are the same 404s as in the reporter's log.tools/triage_test.sh web_mcppasses with the fix../run_pre_commitpasses, including the quick suite it runs ("All tests passed, 4 slow tests skipped").Notes
The scoped metadata still advertises
resource: <base>, not<base>/mcp. This is deliberate. RFC 9728 and the stricter clients expect the path-scoped document to name<base>/mcp(see MCP OAuth token exchange fails with Codex: required form parameters not detected #4799, where Codex reportsexpected <base>/mcp, received <base>). I didn't change it, for two reasons:html_mcp_postvalidates the token audience againsthttp://{host}(plus the localhost default). A client that took<base>/mcpfrom the metadata and sent it as the RFC 8707resourcewould get a token with that audience, and the token would then be rejected.Settling the resource and audience identity belongs with MCP OAuth token exchange fails with Codex: required form parameters not detected #4799.
/oauth/authorizeand/authorizestill have noOPTIONShandler. The evidence doesn't point at them: the authorize POST is a form navigation, which doesn't trigger a preflight. I left them out to keep the change to the routes the log showed failing./mcpand/also get noOPTIONShandler, deliberately. A preflight route alone wouldn't let a browser-based client work: the MCP handlers,oauth_tokenandoauth_registersend noAccess-Control-Allow-Originon their actual responses, so the browser would pass the preflight and then block the response. Supporting browser clients needs CORS headers on every MCP/OAuth response, probably through a middleware. No log shows a client failing that way (the reporter's claude.ai POSTs to/mcpshow noOPTIONS /mcp), so that is left for a follow-up.Docs: a new "OAuth-based MCP clients (mcp)" section in
docs/components.mdlists the discovery and OAuth endpoints, says that approval usesmcp_secret, notes the internet-exposure risk of cloud-hosted clients, and records theresourcecaveat tracked in MCP OAuth token exchange fails with Codex: required form parameters not detected #4799. This answers the docs request in the issue.Blast radius: GitNexus
impactonPredbatMCPServer.startis LOW with no static callers (the component lifecycle calls it).detect_changesshows onlystartand the test module touched, with no affected processes.🤖 Generated with Claude Code