@@ -750,7 +750,7 @@ class TestProtectedResourceMetadata:
750750
751751 @pytest .mark .anyio
752752 async def test_resource_param_included_with_recent_protocol_version (self , oauth_provider : OAuthClientProvider ):
753- """Test resource parameter is included for protocol version >= 2025-06-18."""
753+ """Test resource parameter is included in initial token requests for protocol version >= 2025-06-18."""
754754 # Set protocol version to 2025-06-18
755755 oauth_provider .context .protocol_version = "2025-06-18"
756756 oauth_provider .context .client_info = OAuthClientInformationFull (
@@ -767,15 +767,16 @@ async def test_resource_param_included_with_recent_protocol_version(self, oauth_
767767 expected_resource = quote (oauth_provider .context .get_resource_url (), safe = "" )
768768 assert f"resource={ expected_resource } " in content
769769
770- # Test in refresh token
770+ # Refresh requests never include the resource parameter: some providers
771+ # (e.g. Entra ID v2.0) reject RFC 8707 resource values on refresh_token grants.
771772 oauth_provider .context .current_tokens = OAuthToken (
772773 access_token = "test_access" ,
773774 token_type = "Bearer" ,
774775 refresh_token = "test_refresh" ,
775776 )
776777 refresh_request = await oauth_provider ._refresh_token ()
777778 refresh_content = refresh_request .content .decode ()
778- assert "resource=" in refresh_content
779+ assert "resource=" not in refresh_content
779780
780781 @pytest .mark .anyio
781782 async def test_resource_param_excluded_with_old_protocol_version (self , oauth_provider : OAuthClientProvider ):
@@ -805,7 +806,7 @@ async def test_resource_param_excluded_with_old_protocol_version(self, oauth_pro
805806
806807 @pytest .mark .anyio
807808 async def test_resource_param_included_with_protected_resource_metadata (self , oauth_provider : OAuthClientProvider ):
808- """Test resource parameter is always included when protected resource metadata exists."""
809+ """Test resource parameter is included in initial token requests when protected resource metadata exists."""
809810 # Set old protocol version but with protected resource metadata
810811 oauth_provider .context .protocol_version = "2025-03-26"
811812 oauth_provider .context .protected_resource_metadata = ProtectedResourceMetadata (
@@ -823,6 +824,16 @@ async def test_resource_param_included_with_protected_resource_metadata(self, oa
823824 content = request .content .decode ()
824825 assert "resource=" in content
825826
827+ # Even with PRM present, refresh requests omit the resource parameter
828+ oauth_provider .context .current_tokens = OAuthToken (
829+ access_token = "test_access" ,
830+ token_type = "Bearer" ,
831+ refresh_token = "test_refresh" ,
832+ )
833+ refresh_request = await oauth_provider ._refresh_token ()
834+ refresh_content = refresh_request .content .decode ()
835+ assert "resource=" not in refresh_content
836+
826837
827838@pytest .mark .parametrize (
828839 ("protocol_version" , "expected" ),
@@ -972,6 +983,47 @@ async def test_get_resource_url_uses_canonical_when_prm_mismatches(
972983 assert provider .context .get_resource_url () == snapshot ("https://api.example.com/v1/mcp" )
973984
974985
986+ @pytest .mark .anyio
987+ async def test_get_resource_url_removes_root_prm_trailing_slash (
988+ client_metadata : OAuthClientMetadata , mock_storage : MockTokenStorage
989+ ) -> None :
990+ """Bare-domain PRM resources should not pick up the trailing slash AnyHttpUrl adds."""
991+ provider = OAuthClientProvider (
992+ server_url = "https://api.example.com" ,
993+ client_metadata = client_metadata ,
994+ storage = mock_storage ,
995+ )
996+ provider ._initialized = True
997+
998+ # AnyHttpUrl normalizes "https://api.example.com" to "https://api.example.com/"
999+ provider .context .protected_resource_metadata = ProtectedResourceMetadata (
1000+ resource = AnyHttpUrl ("https://api.example.com" ),
1001+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
1002+ )
1003+
1004+ assert provider .context .get_resource_url () == snapshot ("https://api.example.com" )
1005+
1006+
1007+ @pytest .mark .anyio
1008+ async def test_get_resource_url_preserves_non_root_trailing_slash (
1009+ client_metadata : OAuthClientMetadata , mock_storage : MockTokenStorage
1010+ ) -> None :
1011+ """RFC 9728 requires exact-string identity, so intentional trailing slashes on deeper paths stay."""
1012+ provider = OAuthClientProvider (
1013+ server_url = "https://api.example.com/v1/mcp/" ,
1014+ client_metadata = client_metadata ,
1015+ storage = mock_storage ,
1016+ )
1017+ provider ._initialized = True
1018+
1019+ provider .context .protected_resource_metadata = ProtectedResourceMetadata (
1020+ resource = AnyHttpUrl ("https://api.example.com/v1/mcp/" ),
1021+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
1022+ )
1023+
1024+ assert provider .context .get_resource_url () == snapshot ("https://api.example.com/v1/mcp/" )
1025+
1026+
9751027class TestRegistrationResponse :
9761028 """Test client registration response handling."""
9771029
0 commit comments