fix(ai): follow RFC 9728 protected-resource metadata in MCP OAuth discovery - #837
Closed
yurekami wants to merge 1 commit into
Closed
Conversation
…covery discover() probed only the two authorization-server documents at the MCP endpoint's own origin, so a server whose authorization server lives on another host could never be logged into. Its doc comment already claimed to try protected-resource metadata; nothing fetched it. Consult the RFC 9728 protected-resource document first (path-suffixed, then bare root), resolve each advertised issuer with both the RFC 8414 path-insertion form and the OpenID Connect Discovery path-append form, then fall back to today's two origin-rooted probes. refreshToken() inherits this through its existing discover() call. Also split fetchJson's transport failure from its JSON-parse failure: a sign-in page served at a well-known URI now reports as non-JSON instead of surfacing a bare SyntaxError. fixes PrimeIntellect-ai#766
Contributor
|
Thank you for the report and proposed work. This root cause is now covered by maintainer-owned stacked PR #1164, authored independently from We did not inspect or reuse this PR's diff, branch, commits, implementation code, or tests; its public description/comments were used only as a bug report. To keep one review surface, this PR is superseded by #1164 and is being closed. The complete review stack is #1158–#1165. It is being left unmerged for human review after CI and review-bot findings are cleared. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #766.
The defect
discover()probed only two documents, both at the MCP endpoint's own origin:The doc comment names protected-resource metadata;
oauth-protected-resourceappears nowhere in the tree atb9a4461. RFC 9728 makes that document the resource's own statement of which authorization server to use, and it routinely names a different host, so/mcp loginwas impossible for those servers.The change
/.well-known/oauth-protected-resource/mcp/v1), then bare root. Both are needed — see ironclad below.https://host/.well-known/oauth-authorization-server/oidc) and the OpenID Connect Discovery path-append form.fetchJson's transport failure from its JSON-parse failure.refreshToken()inherits this through its existingdiscover()call; no call site changed. Point 4 is not cosmetic: Ashby returns HTTP 200 with an HTML sign-in page at/.well-known/oauth-authorization-server, so today's failure is a bareSyntaxError: Unexpected token '<'with no indication of which document was bad.Evidence
I ran the real
createMcpOAuthProvider().login()against production servers withfetchrestricted toGET /.well-known/requests, so discovery ran end to end and the DCR/token requests were blocked. Same harness on both sides.Before, at
b9a4461:oauth-authorization-serveropenid-configurationhttps://ai.todoist.net/mcphttps://mcp.ashbyhq.com/mcp/v1SyntaxErrorhttps://mcp.box.comhttps://drivemcp.googleapis.com/mcp/v1https://mcp.na1.ironcladapp.com/mcphttps://mcp.shippo.comhttps://mcp.linear.app/mcpAfter:
https://ai.todoist.net/mcp…/oauth-protected-resource/mcphttps://todoist.com(DCR available)https://mcp.ashbyhq.com/mcp/v1…/oauth-protected-resource/mcp/v1https://mcp-auth.ashbyhq.com/oidc(DCR available)https://mcp.box.com…/oauth-protected-resourcehttps://api.box.com/(no DCR)https://drivemcp.googleapis.com/mcp/v1…/oauth-protected-resource/mcp/v1https://accounts.google.com/(no DCR)https://mcp.na1.ironcladapp.com/mcphttps://ironcladapp.com(no DCR)https://mcp.shippo.com…/oauth-protected-resourcehttps://goshippo.com(DCR available)https://mcp.linear.app/mcp…/oauth-protected-resource/mcphttps://mcp.linear.app(unchanged)Ashby, Box and Google exercise the three edge cases the helpers exist for: an issuer carrying a path, an issuer advertised with a trailing slash, and a path-suffixed document where the bare root 404s. Ironclad is the reverse — its suffixed document 404s and only the bare root answers.
Three of these discover successfully but advertise no
registration_endpoint, so they still need a pre-registered client id. That is the separate gap the reporter notes; this PR does not address it.Tests
Five cases added to
packages/ai/test/mcp-oauth.test.ts(10 pass, 5 pre-existing unchanged). I mutation-checked them — each mutant is caught by exactly one test, and each passes at HEAD only when it should:falls back to the bare protected-resource document…resolves the authorization server from protected-resource metadataresolves an issuer that carries a pathfetchJsonnon-JSON splitreports a non-JSON metadata document distinctly…The precedence test serves a valid authorization-server document at the endpoint origin too, so it pins that the protected-resource document wins rather than merely that the origin probe failed.
npm run checkis clean.Verification
oauth-protected-resourceabsence checked againstmain@b9a4461.b9a4461through the same harness against live servers.npm run check;packages/ai/test/mcp-oauth.test.ts10/10.authorization_endpoint,token_endpoint, andS256among itscode_challenge_methods_supported— not that the subsequent DCR + PKCE exchange succeeds.Deliberately out of scope
WWW-Authenticateresource_metadata(step 1 of the proposal). Needs an extra unauthenticated probe and reaches intomcp-manager.ts. Every server above resolves without it.McpCredentialsshape.issuermatches the advertised one (RFC 9728 SHOULD). Not added because a strict comparison would reject servers that work: Box advertiseshttps://api.box.com/and its document reportshttps://api.box.com, and Google advertiseshttps://accounts.google.com/against a reportedhttps://accounts.google.com. Happy to add it with normalization if you want it here rather than separately.One note on the fan-out: every advertised issuer is tried in order rather than only
authorization_servers[0], with no cap. Say the word if you would rather bound it.Note
Fix MCP OAuth discovery to follow RFC 9728 protected-resource metadata for cross-host authorization servers
discoverfunction inpackages/ai/src/mcp/oauth.tsnow fetches RFC 9728 protected-resource metadata from the MCP endpoint first, reads listedauthorization_serversissuers, and builds candidate discovery URLs using both RFC 8414 and OIDC discovery patterns before falling back to the endpoint's own origin.fetchJsonis updated to throw a distinct error when a well-known endpoint returns non-JSON content, and includes response body text for non-OK responses.Macroscope summarized b1c38eb.