Skip to content

Commit 2736dfb

Browse files
committed
Move logic from Auth Server Metadata handler to PRM handler
1 parent cf4cc3f commit 2736dfb

File tree

2 files changed

+46
-56
lines changed

2 files changed

+46
-56
lines changed

src/mcp/client/auth.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,23 @@ async def _handle_protected_resource_response(self, response: httpx.Response) ->
273273
self.context.protected_resource_metadata = metadata
274274
if metadata.authorization_servers:
275275
self.context.auth_server_url = str(metadata.authorization_servers[0])
276+
277+
# Only set scope if client_metadata.scope is None
278+
# Per MCP spec, priority order:
279+
# 1. Use scope from WWW-Authenticate header (if provided)
280+
# 2. Use all scopes from PRM scopes_supported (if available)
281+
# 3. Omit scope parameter if neither is available
282+
if self.context.client_metadata.scope is None:
283+
if self.context.www_authenticate_scope is not None:
284+
# Priority 1: WWW-Authenticate header scope
285+
self.context.client_metadata.scope = self.context.www_authenticate_scope
286+
elif self.context.protected_resource_metadata.scopes_supported is not None:
287+
# Priority 2: PRM scopes_supported
288+
self.context.client_metadata.scope = " ".join(
289+
self.context.protected_resource_metadata.scopes_supported
290+
)
291+
# Priority 3: Omit scope parameter
292+
276293
except ValidationError:
277294
pass
278295

@@ -504,23 +521,6 @@ async def _handle_oauth_metadata_response(self, response: httpx.Response) -> Non
504521
metadata = OAuthMetadata.model_validate_json(content)
505522
self.context.oauth_metadata = metadata
506523

507-
# Only set scope if client_metadata.scope is None
508-
# Per MCP spec, priority order:
509-
# 1. Use scope from WWW-Authenticate header (if provided)
510-
# 2. Use all scopes from PRM scopes_supported (if available)
511-
# 3. Omit scope parameter if neither is available
512-
if self.context.client_metadata.scope is None:
513-
if self.context.www_authenticate_scope is not None:
514-
# Priority 1: WWW-Authenticate header scope
515-
self.context.client_metadata.scope = self.context.www_authenticate_scope
516-
elif (
517-
self.context.protected_resource_metadata is not None
518-
and self.context.protected_resource_metadata.scopes_supported is not None
519-
):
520-
# Priority 2: PRM scopes_supported
521-
self.context.client_metadata.scope = " ".join(self.context.protected_resource_metadata.scopes_supported)
522-
# Priority 3: Omit scope parameter
523-
524524
async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
525525
"""HTTPX auth flow integration."""
526526
async with self.context.lock:

tests/client/test_auth.py

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -102,22 +102,28 @@ def oauth_metadata_response():
102102

103103

104104
@pytest.fixture
105-
def prm_metadata():
106-
"""PRM metadata with scopes."""
107-
return ProtectedResourceMetadata(
108-
resource=AnyHttpUrl("https://api.example.com/v1/mcp"),
109-
authorization_servers=[AnyHttpUrl("https://auth.example.com")],
110-
scopes_supported=["resource:read", "resource:write"],
105+
def prm_metadata_response():
106+
"""PRM metadata response with scopes."""
107+
return httpx.Response(
108+
200,
109+
content=(
110+
b'{"resource": "https://api.example.com/v1/mcp", '
111+
b'"authorization_servers": ["https://auth.example.com"], '
112+
b'"scopes_supported": ["resource:read", "resource:write"]}'
113+
),
111114
)
112115

113116

114117
@pytest.fixture
115-
def prm_metadata_without_scopes():
116-
"""PRM metadata without scopes."""
117-
return ProtectedResourceMetadata(
118-
resource=AnyHttpUrl("https://api.example.com/v1/mcp"),
119-
authorization_servers=[AnyHttpUrl("https://auth.example.com")],
120-
scopes_supported=None,
118+
def prm_metadata_without_scopes_response():
119+
"""PRM metadata response without scopes."""
120+
return httpx.Response(
121+
200,
122+
content=(
123+
b'{"resource": "https://api.example.com/v1/mcp", '
124+
b'"authorization_servers": ["https://auth.example.com"], '
125+
b'"scopes_supported": null}'
126+
),
121127
)
122128

123129

@@ -437,20 +443,16 @@ async def test_handle_metadata_response_success(self, oauth_provider: OAuthClien
437443
async def test_prioritize_www_auth_scope_over_prm(
438444
self,
439445
oauth_provider_without_scope: OAuthClientProvider,
440-
oauth_metadata_response: httpx.Response,
441-
prm_metadata: ProtectedResourceMetadata,
446+
prm_metadata_response: httpx.Response,
442447
):
443448
"""Test that WWW-Authenticate scope is prioritized over PRM scopes."""
444449
provider = oauth_provider_without_scope
445450

446-
# Set up PRM metadata with scopes
447-
provider.context.protected_resource_metadata = prm_metadata
448-
449451
# Set WWW-Authenticate scope (priority 1)
450452
provider.context.www_authenticate_scope = "special:scope from:www-authenticate"
451453

452-
# Process the OAuth metadata
453-
await provider._handle_oauth_metadata_response(oauth_metadata_response)
454+
# Process the PRM metadata
455+
await provider._handle_protected_resource_response(prm_metadata_response)
454456

455457
# Verify that WWW-Authenticate scope is used (not PRM scopes)
456458
assert provider.context.client_metadata.scope == "special:scope from:www-authenticate"
@@ -459,17 +461,13 @@ async def test_prioritize_www_auth_scope_over_prm(
459461
async def test_prioritize_prm_scopes_when_no_www_auth_scope(
460462
self,
461463
oauth_provider_without_scope: OAuthClientProvider,
462-
oauth_metadata_response: httpx.Response,
463-
prm_metadata: ProtectedResourceMetadata,
464+
prm_metadata_response: httpx.Response,
464465
):
465466
"""Test that PRM scopes are prioritized when WWW-Authenticate header has no scopes."""
466467
provider = oauth_provider_without_scope
467468

468-
# Set up PRM metadata with specific scopes
469-
provider.context.protected_resource_metadata = prm_metadata
470-
471-
# Process the OAuth metadata (no WWW-Authenticate scope)
472-
await provider._handle_oauth_metadata_response(oauth_metadata_response)
469+
# Process the PRM metadata (no WWW-Authenticate scope)
470+
await provider._handle_protected_resource_response(prm_metadata_response)
473471

474472
# Verify that PRM scopes are used
475473
assert provider.context.client_metadata.scope == "resource:read resource:write"
@@ -478,17 +476,13 @@ async def test_prioritize_prm_scopes_when_no_www_auth_scope(
478476
async def test_omit_scope_when_no_prm_scopes_or_www_auth(
479477
self,
480478
oauth_provider_without_scope: OAuthClientProvider,
481-
oauth_metadata_response: httpx.Response,
482-
prm_metadata_without_scopes: ProtectedResourceMetadata,
479+
prm_metadata_without_scopes_response: httpx.Response,
483480
):
484481
"""Test that scope is omitted when PRM has no scopes and WWW-Authenticate doesn't specify scope."""
485482
provider = oauth_provider_without_scope
486483

487-
# Set up PRM metadata without scopes
488-
provider.context.protected_resource_metadata = prm_metadata_without_scopes
489-
490-
# Process the OAuth metadata (no WWW-Authenticate scope set)
491-
await provider._handle_oauth_metadata_response(oauth_metadata_response)
484+
# Process the PRM metadata (no WWW-Authenticate scope set)
485+
await provider._handle_protected_resource_response(prm_metadata_without_scopes_response)
492486

493487
# Verify that scope is omitted
494488
assert provider.context.client_metadata.scope is None
@@ -497,20 +491,16 @@ async def test_omit_scope_when_no_prm_scopes_or_www_auth(
497491
async def test_preserve_existing_client_scope(
498492
self,
499493
oauth_provider: OAuthClientProvider,
500-
oauth_metadata_response: httpx.Response,
501-
prm_metadata: ProtectedResourceMetadata,
494+
prm_metadata_response: httpx.Response,
502495
):
503496
"""Test that existing client scope is preserved regardless of metadata."""
504497
provider = oauth_provider
505498

506499
# Set WWW-Authenticate scope
507500
provider.context.www_authenticate_scope = "special:scope from:www-authenticate"
508501

509-
# Set up PRM metadata with scopes
510-
provider.context.protected_resource_metadata = prm_metadata
511-
512502
# Process the OAuth metadata
513-
await provider._handle_oauth_metadata_response(oauth_metadata_response)
503+
await provider._handle_protected_resource_response(prm_metadata_response)
514504

515505
# Verify that predefined scope is preserved
516506
assert provider.context.client_metadata.scope == "read write"

0 commit comments

Comments
 (0)