You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We use mcp.shared.auth.ProtectedResourceMetadata.resource for canonical resource URLs exposed at /.well-known/oauth-protected-resource. When AnyHttpUrl normalizes a URL whose original path is /, it serializes the value with a trailing slash. This breaks RFC 9728 §3.1 expectations and causes OAuth protected-resource validation to fail in strict clients — tokens are rejected and the client falls back into an infinite OAuth authorize/token loop.
Symptom
Any MCP server exposing a resource with streamable_http_path="/" ends up producing a resource value with a trailing slash in the protected-resource metadata. Clients that validate strict against the canonical form (e.g. Claude Desktop) silently reject the token exchange and reinitiate the OAuth flow indefinitely.
Observed server logs:
POST / → 401 (no token, expected)
GET /.well-known/oauth-protected-resource → 200 (resource = "https://host/")
GET /authorize → 302
POST /token → 200 (token issued)
POST / → never sent with Authorization: Bearer
GET /authorize → 302 (new flow)
POST /token → 200
... loops forever
Suggested fixes (options)
Add a custom validator on ProtectedResourceMetadata.resource that strips the trailing slash when the original path is exactly /.
Change the field type from AnyHttpUrl to str with explicit canonical-form validation.
Use a stricter URL normalization option in pydantic if one exists for this case.
Workaround in consumer repo
We patched it on our side by setting streamable_http_path="/mcp" so the URL has a non-trivial path, which AnyHttpUrl preserves without appending a slash. The consumer commit is 27c8e0c.
But that is a workaround for the symptom — the underlying contract violation in the protected-resource metadata is still present for any consumer that uses streamable_http_path="/".
Happy to help
Happy to open a follow-up PR if you prefer the custom-validator route — let me know which option you would prefer to merge.
Nice catch @viejunoacien — this is more impactful than it looks at first glance. The AnyHttpUrl trailing slash isn't just a cosmetic issue; it causes silent token rejection with no user-visible error, which is the worst kind of OAuth failure.
We've hit this exact pattern across multiple business API connectors at corpusiq.io (36 APIs including Stripe, HubSpot, QuickBooks, NetSuite). When we set streamable_http_path="/" on a connector:
The server logs show exactly the pattern from your reproduction: POST /token → 200, but POST / (with Bearer) never arrives because the client abandons the session after resource URL validation fails.
One additional consideration for the fix: the resource field in ProtectedResourceMetadata is what OAuth clients use as the canonical resource identifier. If we strip the trailing slash in the metadata response, we need to ensure the server's actual endpoint handling also works without it. Otherwise we trade one mismatch for another.
The PR #2888 approach looks clean — custom validator on the resource field preserves the canonical form while keeping AnyHttpUrl for URL validation elsewhere. Happy to help review or test against our multi-API setup if that would be useful.
This is the same root cause as #1265, so I'm going to track it there rather than in two places. For what it's worth the SDK side is fixed in v2.0.0: #2925 set url_preserve_empty_path=True on ProtectedResourceMetadata (and the other OAuth metadata models) and #2987 did the same for AuthSettings, so a resource_server_url given as a path-less string is now served without the synthetic trailing slash — essentially your option 3. On 1.x, mounting at a non-root path as you did remains the workaround. Feel free to reopen if you still see the trailing slash on 2.x with a string resource_server_url.
+1 on the client pain: pathless// resources serialized with a trailing slash fail strict PRM/RFC 9728 identity checks even when Inspector looks fine. We're seeing the same "works in Inspector / breaks in real clients" gap across OAuth PRM, trailing-slash, and needs-auth discovery.
Activity
Ben-Home commentedon Jun 19, 2026
Nice catch @viejunoacien — this is more impactful than it looks at first glance. The
AnyHttpUrltrailing slash isn't just a cosmetic issue; it causes silent token rejection with no user-visible error, which is the worst kind of OAuth failure.We've hit this exact pattern across multiple business API connectors at corpusiq.io (36 APIs including Stripe, HubSpot, QuickBooks, NetSuite). When we set
streamable_http_path="/"on a connector:/.well-known/oauth-protected-resource→ gets"resource": "https://host/"https://host(no trailing slash, per RFC 9728 §3.1)aud: "https://host"Authorization: Bearer <token>tohttps://host/aud→ mismatch → 401 → infinite re-auth loopThe server logs show exactly the pattern from your reproduction: POST /token → 200, but POST / (with Bearer) never arrives because the client abandons the session after resource URL validation fails.
One additional consideration for the fix: the
resourcefield inProtectedResourceMetadatais what OAuth clients use as the canonical resource identifier. If we strip the trailing slash in the metadata response, we need to ensure the server's actual endpoint handling also works without it. Otherwise we trade one mismatch for another.The PR #2888 approach looks clean — custom validator on the
resourcefield preserves the canonical form while keepingAnyHttpUrlfor URL validation elsewhere. Happy to help review or test against our multi-API setup if that would be useful.maxisbey commentedon Aug 14, 2026
This is the same root cause as #1265, so I'm going to track it there rather than in two places. For what it's worth the SDK side is fixed in v2.0.0: #2925 set
url_preserve_empty_path=TrueonProtectedResourceMetadata(and the other OAuth metadata models) and #2987 did the same forAuthSettings, so aresource_server_urlgiven as a path-less string is now served without the synthetic trailing slash — essentially your option 3. On 1.x, mounting at a non-root path as you did remains the workaround. Feel free to reopen if you still see the trailing slash on 2.x with a stringresource_server_url.AI Disclaimer
slowe89 commentedon Sep 10, 2026
+1 on the client pain: pathless/
/resources serialized with a trailing slash fail strict PRM/RFC 9728 identity checks even when Inspector looks fine. We're seeing the same "works in Inspector / breaks in real clients" gap across OAuth PRM, trailing-slash, and needs-auth discovery.Cataloguing those seams here if useful: https://compatlab-waitlist.vercel.app (permission-only).