Skip to content

Commit 09ef708

Browse files
committed
fix: prioritize PRM endpoint scopes over OAuth metadata scopes
1 parent 8463dc7 commit 09ef708

File tree

2 files changed

+24
-13
lines changed

2 files changed

+24
-13
lines changed

src/mcp/client/auth.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -478,12 +478,14 @@ async def _handle_oauth_metadata_response(self, response: httpx.Response) -> Non
478478
content = await response.aread()
479479
metadata = OAuthMetadata.model_validate_json(content)
480480
self.context.oauth_metadata = metadata
481-
481+
482482
# Only set scope if client_metadata.scope is None
483483
if self.context.client_metadata.scope is None:
484484
# Priority 1: Use PRM's scopes_supported if available
485-
if (self.context.protected_resource_metadata is not None and
486-
self.context.protected_resource_metadata.scopes_supported is not None):
485+
if (
486+
self.context.protected_resource_metadata is not None
487+
and self.context.protected_resource_metadata.scopes_supported is not None
488+
):
487489
self.context.client_metadata.scope = " ".join(self.context.protected_resource_metadata.scopes_supported)
488490
# Priority 2: Fall back to OAuth metadata scopes if available
489491
elif metadata.scopes_supported is not None:

tests/client/test_auth.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,7 @@ async def callback_handler() -> tuple[str, str | None]:
962962

963963

964964
@pytest.fixture
965-
def client_metadata_no_scope():
965+
def client_metadata_no_scope() -> OAuthClientMetadata:
966966
"""Client metadata without a predefined scope."""
967967
return OAuthClientMetadata(
968968
client_name="Test Client",
@@ -974,8 +974,11 @@ def client_metadata_no_scope():
974974

975975

976976
@pytest.fixture
977-
def oauth_provider_without_scope(client_metadata_no_scope, mock_storage):
977+
def oauth_provider_without_scope(
978+
client_metadata_no_scope: OAuthClientMetadata, mock_storage: MockTokenStorage
979+
) -> OAuthClientProvider:
978980
"""Create OAuth provider without predefined scope."""
981+
979982
async def redirect_handler(url: str) -> None:
980983
pass
981984

@@ -995,7 +998,7 @@ class TestScopeHandlingPriority:
995998
"""Test OAuth scope handling priority between PRM and auth metadata."""
996999

9971000
@pytest.mark.anyio
998-
async def test_prioritize_prm_scopes_over_oauth_metadata(self, oauth_provider_without_scope):
1001+
async def test_prioritize_prm_scopes_over_oauth_metadata(self, oauth_provider_without_scope: OAuthClientProvider):
9991002
"""Test that PRM scopes are prioritized over auth server metadata scopes."""
10001003
provider = oauth_provider_without_scope
10011004

@@ -1023,9 +1026,11 @@ async def test_prioritize_prm_scopes_over_oauth_metadata(self, oauth_provider_wi
10231026

10241027
# Verify that PRM scopes are used (not OAuth metadata scopes)
10251028
assert provider.context.client_metadata.scope == "resource:read resource:write"
1026-
1029+
10271030
@pytest.mark.anyio
1028-
async def test_fallback_to_oauth_metadata_scopes_when_no_prm_scopes(self, oauth_provider_without_scope):
1031+
async def test_fallback_to_oauth_metadata_scopes_when_no_prm_scopes(
1032+
self, oauth_provider_without_scope: OAuthClientProvider
1033+
):
10291034
"""Test fallback to OAuth metadata scopes when PRM has no scopes."""
10301035
provider = oauth_provider_without_scope
10311036

@@ -1055,7 +1060,9 @@ async def test_fallback_to_oauth_metadata_scopes_when_no_prm_scopes(self, oauth_
10551060
assert provider.context.client_metadata.scope == "read write admin"
10561061

10571062
@pytest.mark.anyio
1058-
async def test_fallback_to_oauth_metadata_scopes_when_no_prm(self, oauth_provider_without_scope):
1063+
async def test_fallback_to_oauth_metadata_scopes_when_no_prm(
1064+
self, oauth_provider_without_scope: OAuthClientProvider
1065+
):
10591066
"""Test fallback to OAuth metadata scopes when no PRM is available."""
10601067
provider = oauth_provider_without_scope
10611068

@@ -1080,7 +1087,7 @@ async def test_fallback_to_oauth_metadata_scopes_when_no_prm(self, oauth_provide
10801087
assert provider.context.client_metadata.scope == "read write admin"
10811088

10821089
@pytest.mark.anyio
1083-
async def test_no_scope_changes_when_both_missing(self, oauth_provider_without_scope):
1090+
async def test_no_scope_changes_when_both_missing(self, oauth_provider_without_scope: OAuthClientProvider):
10841091
"""Test that no scope changes occur when both PRM and OAuth metadata lack scopes."""
10851092
provider = oauth_provider_without_scope
10861093

@@ -1110,12 +1117,14 @@ async def test_no_scope_changes_when_both_missing(self, oauth_provider_without_s
11101117
assert provider.context.client_metadata.scope is None
11111118

11121119
@pytest.mark.anyio
1113-
async def test_preserve_existing_client_scope(self, client_metadata_no_scope, mock_storage):
1120+
async def test_preserve_existing_client_scope(
1121+
self, client_metadata_no_scope: OAuthClientMetadata, mock_storage: MockTokenStorage
1122+
):
11141123
"""Test that existing client scope is preserved regardless of metadata."""
11151124
# Create client with predefined scope
11161125
client_metadata = client_metadata_no_scope
11171126
client_metadata.scope = "predefined:scope"
1118-
1127+
11191128
# Create provider
11201129
async def redirect_handler(url: str) -> None:
11211130
pass
@@ -1154,4 +1163,4 @@ async def callback_handler() -> tuple[str, str | None]:
11541163
await provider._handle_oauth_metadata_response(oauth_metadata_response)
11551164

11561165
# Verify that predefined scope is preserved
1157-
assert provider.context.client_metadata.scope == "predefined:scope"
1166+
assert provider.context.client_metadata.scope == "predefined:scope"

0 commit comments

Comments
 (0)