Preserve query components in PRM resource URLs
Summary
Fixes query-bearing protected-resource identifiers in the OAuth protected resource metadata path.
When a server URL includes a query component, for example https://api.example.com/mcp?tenant=a, the SDK now:
- preserves the query when deriving the RFC 9728 protected resource metadata URL;
- preserves the query in client fallback PRM discovery URLs;
- treats different query components as different resource identifiers during PRM resource validation.
Closes #3065.
Why
RFC 9728's well-known URL construction inserts /.well-known/oauth-protected-resource before the resource path and/or query. Dropping the query collapses query-routed resource identifiers, so a deployment using ?tenant=a and ?tenant=b can publish or accept the wrong protected resource metadata.
Tests
uv run --frozen pytest tests/client/test_auth.py::TestSEP985Discovery::test_prm_discovery_preserves_server_url_query_component tests/client/test_auth.py::test_validate_resource_rejects_mismatched_query_component tests/server/auth/test_protected_resource.py::test_metadata_url_construction_preserves_query_component tests/shared/test_auth_utils.py::test_check_resource_allowed_rejects_different_queries -quv run --frozen pytest tests/client/test_auth.py tests/shared/test_auth_utils.py tests/server/auth/test_protected_resource.py tests/interaction/auth/test_discovery.py -quv run --frozen ruff format src/mcp/server/auth/routes.py src/mcp/client/auth/utils.py src/mcp/shared/auth_utils.py tests/server/auth/test_protected_resource.py tests/client/test_auth.py tests/shared/test_auth_utils.pyuv run --frozen ruff check src/mcp/server/auth/routes.py src/mcp/client/auth/utils.py src/mcp/shared/auth_utils.py tests/server/auth/test_protected_resource.py tests/client/test_auth.py tests/shared/test_auth_utils.pyuv run --frozen pyright src/mcp/server/auth/routes.py src/mcp/client/auth/utils.py src/mcp/shared/auth_utils.py tests/server/auth/test_protected_resource.py tests/client/test_auth.py tests/shared/test_auth_utils.py./scripts/test— 5293 passed, 9 skipped, 1 xfailed; coverage 100%; strict-no-cover clean
Note: full uv run --frozen pyright currently reports two errors in unrelated tests/transports/stdio/test_lifecycle.py around os.waitid typing on this machine. The changed files pass scoped pyright as listed above.
AI-assisted (Claude/Codex) for navigation and review; change authored and understood by me.