[v2] RFC 9728 PRM URLs and resource matching drop query components
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 74/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- python
- Domain
- authentication
Research direction
Start with mcp.server.auth.routes.build_resource_metadata_url(), mcp.client.auth.utils.build_protected_resource_metadata_discovery_urls(), and mcp.shared.auth_utils.check_resource_allowed(), then trace OAuthClientProvider._validate_resource_match(). Reproduce the query-bearing cases shown in the issue and add focused auth tests; done means metadata URLs preserve the query and different query components no longer match.
Written by the indexing model from the issue text.
Description
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=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-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.
- Dominant language
- Python
- Stars
- 24.3k
- Forks
- 4k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 30
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from modelcontextprotocol/python-sdk
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
modelcontextprotocol/python-sdk#3566 ·
-
v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
modelcontextprotocol/python-sdk#3546 · 5 comments ·
-
v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
modelcontextprotocol/python-sdk#3545 · 1 comment ·
-
v1 v2
Difficulty 1/5 Under an hour Newbie friendliness 91/100
modelcontextprotocol/python-sdk#3508 · 2 comments ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
modelcontextprotocol/python-sdk#3504 ·
All issues in modelcontextprotocol/python-sdk
Similar issues
-
essnmx good first issue
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
[Feature] 奇物选择添加优先级 Open
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
syfoud/Simulated_Scepter#174 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Giskard-AI/giskard-oss#2840 · 1 comment ·
-
A claim comment carrying the issue number is silently declined while the workflow reports success Openarea: repo bug perceived difficulty: 2
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
yeti-platform/yeti#1380 ·