What happened?
While testing the v2 auth/protected-resource-metadata path, I noticed query-bearing resource identifiers are treated as if the query is not part of the resource.
For a resource server URL like:
https://api.example.com/mcp?tenant=a
three SDK paths currently drop or ignore ?tenant=a:
mcp.server.auth.routes.build_resource_metadata_url() returns:
https://api.example.com/.well-known/oauth-protected-resource/mcp
mcp.client.auth.utils.build_protected_resource_metadata_discovery_urls(None, resource) also tries:
https://api.example.com/.well-known/oauth-protected-resource/mcp
https://api.example.com/.well-known/oauth-protected-resource
mcp.shared.auth_utils.check_resource_allowed() treats different query components as matching, so the client accepts protected resource metadata for ?tenant=b when the server URL was ?tenant=a.
This is latent for path-only deployments, but it matters for query-routed or multi-tenant resource identifiers.
What did you expect?
RFC 9728 derives the protected-resource metadata URL by inserting /.well-known/oauth-protected-resource before the protected resource path and/or query. If the resource identifier includes a query component, the derived metadata URL and resource validation should not silently collapse it with a different query.
For https://api.example.com/mcp?tenant=a, I expected the path-specific metadata URL to be:
https://api.example.com/.well-known/oauth-protected-resource/mcp?tenant=a
And a PRM document whose resource is https://api.example.com/mcp?tenant=b should not validate for a client configured with https://api.example.com/mcp?tenant=a.
Code to reproduce
import anyio
from pydantic import AnyHttpUrl, AnyUrl
from mcp.client.auth import OAuthClientProvider
from mcp.client.auth.utils import build_protected_resource_metadata_discovery_urls
from mcp.server.auth.routes import build_resource_metadata_url
from mcp.shared.auth import OAuthClientMetadata, ProtectedResourceMetadata
from mcp.shared.auth_utils import check_resource_allowed
from tests.interaction.auth._harness import InMemoryTokenStorage
async def main() -> None:
resource = "https://api.example.com/mcp?tenant=a"
print(build_resource_metadata_url(AnyHttpUrl(resource)))
print(build_protected_resource_metadata_discovery_urls(None, resource))
print(check_resource_allowed(
"https://api.example.com/mcp?tenant=a",
"https://api.example.com/mcp?tenant=b",
))
provider = OAuthClientProvider(
server_url=resource,
client_metadata=OAuthClientMetadata(
client_name="probe",
client_uri=AnyHttpUrl("https://example.com"),
redirect_uris=[AnyUrl("http://localhost:3030/callback")],
),
storage=InMemoryTokenStorage(),
)
prm = ProtectedResourceMetadata(
resource=AnyHttpUrl("https://api.example.com/mcp?tenant=b"),
authorization_servers=[AnyHttpUrl("https://auth.example.com")],
)
await provider._validate_resource_match(prm)
print("accepted mismatched query")
anyio.run(main)
Current output:
https://api.example.com/.well-known/oauth-protected-resource/mcp
['https://api.example.com/.well-known/oauth-protected-resource/mcp', 'https://api.example.com/.well-known/oauth-protected-resource']
True
accepted mismatched query
SDK version
Current main branch, v2 development line.
Area
Auth
AI-assisted (Claude/Codex) for navigation and review; change authored and understood by me.
What happened?
While testing the v2 auth/protected-resource-metadata path, I noticed query-bearing resource identifiers are treated as if the query is not part of the resource.
For a resource server URL like:
three SDK paths currently drop or ignore
?tenant=a:mcp.server.auth.routes.build_resource_metadata_url()returns:mcp.client.auth.utils.build_protected_resource_metadata_discovery_urls(None, resource)also tries:mcp.shared.auth_utils.check_resource_allowed()treats different query components as matching, so the client accepts protected resource metadata for?tenant=bwhen the server URL was?tenant=a.This is latent for path-only deployments, but it matters for query-routed or multi-tenant resource identifiers.
What did you expect?
RFC 9728 derives the protected-resource metadata URL by inserting
/.well-known/oauth-protected-resourcebefore the protected resource path and/or query. If the resource identifier includes a query component, the derived metadata URL and resource validation should not silently collapse it with a different query.For
https://api.example.com/mcp?tenant=a, I expected the path-specific metadata URL to be:And a PRM document whose
resourceishttps://api.example.com/mcp?tenant=bshould not validate for a client configured withhttps://api.example.com/mcp?tenant=a.Code to reproduce
Current output:
SDK version
Current
mainbranch, v2 development line.Area
Auth
AI-assisted (Claude/Codex) for navigation and review; change authored and understood by me.