Preserve query components in PRM resource URLs

Closed
MA
15d ago
+102-12

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 -q
  • uv 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 -q
  • uv 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.py
  • uv 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.py
  • uv 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.