Skip to content

fix(mcp): serve /mcp-scoped protected resource metadata and CORS preflight on token/register (#3807) - #5622

Open
springfall2008 wants to merge 2 commits into
mainfrom
fix/mcp-oauth-scoped-metadata-cors-3807
Open

springfall2008 wants to merge 2 commits into
mainfrom
fix/mcp-oauth-scoped-metadata-cors-3807

Conversation

@springfall2008

@springfall2008 springfall2008 commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

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_route 404:

  • the RFC 9728 path-aware metadata request /.well-known/oauth-protected-resource/mcp
  • the CORS preflight OPTIONS requests on /oauth/token, /token, /oauth/register and /register

This PR adds those six routes, as diagnosed in the issue by @hamiltongross. The scoped path reuses oauth_protected_resource_metadata, and the preflight requests reuse handle_options.

So that the routes can be tested without binding a port, route registration moves out of start() into a new add_routes(app) method. The existing routes are unchanged: same paths, same handlers, same order, with the catch-all still last.

Testing

  • New test test_mcp_oauth_routes in test_web_mcp.py. It resolves each path through the real aiohttp router via make_mocked_request, checks that it reaches the intended handler (not default_route), and checks the scoped metadata body and the CORS headers on the preflight response.
  • Fails without the fix, passes with it. I ran it red two ways:
    1. With the whole source change stashed (git stash push -u -- apps/predbat/web_mcp.py), it fails with AttributeError: ... no attribute 'add_routes'. That only proves the helper is missing.
    2. With add_routes kept but the six new route lines removed, the assertions themselves fail. All six probes route to default_route, and the token preflight returns 404. These are the same 404s as in the reporter's log.
  • tools/triage_test.sh web_mcp passes with the fix.
  • ./run_pre_commit passes, 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 reports expected <base>/mcp, received <base>). I didn't change it, for two reasons:

    • html_mcp_post validates the token audience against http://{host} (plus the localhost default). A client that took <base>/mcp from the metadata and sent it as the RFC 8707 resource would get a token with that audience, and the token would then be rejected.
    • The reporter says claude.ai connects with the base identity as-is.

    Settling the resource and audience identity belongs with MCP OAuth token exchange fails with Codex: required form parameters not detected #4799.

  • /oauth/authorize and /authorize still have no OPTIONS handler. 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.

  • /mcp and / also get no OPTIONS handler, deliberately. A preflight route alone wouldn't let a browser-based client work: the MCP handlers, oauth_token and oauth_register send no Access-Control-Allow-Origin on 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 /mcp show no OPTIONS /mcp), so that is left for a follow-up.

  • Docs: a new "OAuth-based MCP clients (mcp)" section in docs/components.md lists the discovery and OAuth endpoints, says that approval uses mcp_secret, notes the internet-exposure risk of cloud-hosted clients, and records the resource caveat 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 impact on PredbatMCPServer.start is LOW with no static callers (the component lifecycle calls it). detect_changes shows only start and the test module touched, with no affected processes.

🤖 Generated with Claude Code

…light on token/register (#3807)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@springfall2008 springfall2008 self-assigned this Oct 18, 2026
@springfall2008 springfall2008 added BOT_REVIEW Trigger an autotriage BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push labels Oct 18, 2026

@springfall2008 springfall2008 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.resolve is async (so asyncio.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-fix Default route for /oauth/token - 404 the log showed, and makes the red-green claim plausible. All six new routes are pinned as routed to their intended handler rather than default_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, so run_all --quick exercises 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_post validates the JWT audience against http://{host} (web_mcp.py:917) and both token handlers copy the request's resource parameter into the token's aud, so a client that took <base>/mcp from 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.

Comment thread apps/predbat/web_mcp.py
@springfall2008 springfall2008 removed the BOT_REVIEW Trigger an autotriage label Oct 18, 2026
…source caveat (#3807)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@springfall2008

Copy link
Copy Markdown
Owner Author

Automated reply from the triage bot.

On the review-body finding (docs/components.md, no docs guidance for OAuth clients): I've added it in 08f6f40. The new section is "OAuth-based MCP clients (mcp)", placed after the VSCode bearer-header example. It covers:

  • that OAuth clients such as a claude.ai custom connector need only the server URL;
  • the discovery documents served under /.well-known/..., including both /mcp-scoped variants, plus the register/authorize/token endpoints, all taken from add_routes();
  • that approving the client means entering mcp_secret on the authorize page (the password field in oauth_authorize's login form);
  • that a cloud-hosted client connects from the internet, with a pointer back to the existing security note about plain-http exposure;
  • the resource/audience caveat: the metadata advertises the base URL, not <base>/mcp, and stricter clients are tracked in MCP OAuth token exchange fails with Codex: required form parameters not detected #4799.

./run_pre_commit passes (all hooks including cspell and markdownlint, plus the quick suite: "All tests passed, 4 slow tests skipped"), as does tools/triage_test.sh web_mcp. I've updated the PR body's Notes to match.

@springfall2008 springfall2008 removed the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Oct 18, 2026
@chalfontchubby
chalfontchubby marked this pull request as ready for review October 20, 2026 09:11

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failed to connect claude.ai to Predbat MCP Server (via cloudflare tunnel)

1 participant