Repro
from pydantic import AnyHttpUrl
str(AnyHttpUrl("https://example.com"))
# -> 'https://example.com/'
Context
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.
Repro
Context
We use
mcp.shared.auth.ProtectedResourceMetadata.resourcefor canonical resource URLs exposed at/.well-known/oauth-protected-resource. WhenAnyHttpUrlnormalizes a URL whose original path is/, it serializes the value with a trailing slash. This breaks RFC 9728 §3.1 expectations and causes OAuthprotected-resourcevalidation to fail in strict clients — tokens are rejected and the client falls back into an infinite OAuthauthorize/tokenloop.Symptom
Any MCP server exposing a resource with
streamable_http_path="/"ends up producing aresourcevalue with a trailing slash in theprotected-resourcemetadata. 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:
Suggested fixes (options)
ProtectedResourceMetadata.resourcethat strips the trailing slash when the original path is exactly/.AnyHttpUrltostrwith explicit canonical-form validation.Workaround in consumer repo
We patched it on our side by setting
streamable_http_path="/mcp"so the URL has a non-trivial path, whichAnyHttpUrlpreserves without appending a slash. The consumer commit is 27c8e0c.But that is a workaround for the symptom — the underlying contract violation in the
protected-resourcemetadata is still present for any consumer that usesstreamable_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.