@@ -325,9 +325,14 @@ async def test_oauth_discovery_legacy_fallback_when_no_prm(self):
325325 # When auth_server_url is None (PRM failed), we use server_url and only try root
326326 discovery_urls = build_oauth_authorization_server_metadata_discovery_urls (None , "https://mcp.linear.app/sse" )
327327
328- # Should only try the root URL (legacy behavior)
328+ # Current behavior: try path-aware variants first, then root fallbacks
329329 assert discovery_urls == [
330+ "https://mcp.linear.app/.well-known/oauth-authorization-server/sse" ,
331+ "https://mcp.linear.app/.well-known/openid-configuration/sse" ,
332+ "https://mcp.linear.app/sse/.well-known/oauth-authorization-server" ,
333+ "https://mcp.linear.app/sse/.well-known/openid-configuration" ,
330334 "https://mcp.linear.app/.well-known/oauth-authorization-server" ,
335+ "https://mcp.linear.app/.well-known/openid-configuration" ,
331336 ]
332337
333338 @pytest .mark .anyio
@@ -337,11 +342,14 @@ async def test_oauth_discovery_path_aware_when_auth_server_has_path(self):
337342 "https://auth.example.com/tenant1" , "https://api.example.com/mcp"
338343 )
339344
340- # Should try path-based URLs only (no root URLs)
345+ # Current behavior: path-aware variants (including symmetric OAuth path-local), then root fallbacks
341346 assert discovery_urls == [
342347 "https://auth.example.com/.well-known/oauth-authorization-server/tenant1" ,
343348 "https://auth.example.com/.well-known/openid-configuration/tenant1" ,
349+ "https://auth.example.com/tenant1/.well-known/oauth-authorization-server" ,
344350 "https://auth.example.com/tenant1/.well-known/openid-configuration" ,
351+ "https://auth.example.com/.well-known/oauth-authorization-server" ,
352+ "https://auth.example.com/.well-known/openid-configuration" ,
345353 ]
346354
347355 @pytest .mark .anyio
@@ -351,7 +359,7 @@ async def test_oauth_discovery_root_when_auth_server_has_no_path(self):
351359 "https://auth.example.com" , "https://api.example.com/mcp"
352360 )
353361
354- # Should try root URLs only
362+ # Should try root URLs only (no path-aware when no path)
355363 assert discovery_urls == [
356364 "https://auth.example.com/.well-known/oauth-authorization-server" ,
357365 "https://auth.example.com/.well-known/openid-configuration" ,
@@ -364,7 +372,7 @@ async def test_oauth_discovery_root_when_auth_server_has_only_slash(self):
364372 "https://auth.example.com/" , "https://api.example.com/mcp"
365373 )
366374
367- # Should try root URLs only
375+ # Should try root URLs only (trailing slash treated as root)
368376 assert discovery_urls == [
369377 "https://auth.example.com/.well-known/oauth-authorization-server" ,
370378 "https://auth.example.com/.well-known/openid-configuration" ,
@@ -383,7 +391,10 @@ async def test_oauth_discovery_fallback_order(self, oauth_provider: OAuthClientP
383391 assert discovery_urls == [
384392 "https://api.example.com/.well-known/oauth-authorization-server/v1/mcp" ,
385393 "https://api.example.com/.well-known/openid-configuration/v1/mcp" ,
394+ "https://api.example.com/v1/mcp/.well-known/oauth-authorization-server" ,
386395 "https://api.example.com/v1/mcp/.well-known/openid-configuration" ,
396+ "https://api.example.com/.well-known/oauth-authorization-server" ,
397+ "https://api.example.com/.well-known/openid-configuration" ,
387398 ]
388399
389400 @pytest .mark .anyio
@@ -459,9 +470,12 @@ async def test_oauth_discovery_fallback_conditions(self, oauth_provider: OAuthCl
459470 request = oauth_metadata_request_2 ,
460471 )
461472
462- # Next request should be OIDC path-appended URL
473+ # Next request should be OAuth path-local URL (symmetric), then OIDC path-local
463474 oauth_metadata_request_3 = await auth_flow .asend (oauth_metadata_response_2 )
464- assert str (oauth_metadata_request_3 .url ) == "https://auth.example.com/v1/mcp/.well-known/openid-configuration"
475+ assert (
476+ str (oauth_metadata_request_3 .url )
477+ == "https://auth.example.com/v1/mcp/.well-known/oauth-authorization-server"
478+ )
465479 assert oauth_metadata_request_3 .method == "GET"
466480
467481 # Send a 500 response
@@ -471,12 +485,12 @@ async def test_oauth_discovery_fallback_conditions(self, oauth_provider: OAuthCl
471485 request = oauth_metadata_request_3 ,
472486 )
473487
474- # Mock the authorization process to minimize unnecessary state in this test
488+ # Mock the authorization process to minimize unnecessary state before authorization triggers
475489 oauth_provider ._perform_authorization_code_grant = mock .AsyncMock (
476490 return_value = ("test_auth_code" , "test_code_verifier" )
477491 )
478492
479- # All path-based URLs failed, flow continues with default endpoints
493+ # All path-based URLs failed; flow continues with default endpoints
480494 # Next request should be token exchange using MCP server base URL (fallback when OAuth metadata not found)
481495 token_request = await auth_flow .asend (oauth_metadata_response_3 )
482496 assert str (token_request .url ) == "https://api.example.com/token"
@@ -505,6 +519,46 @@ async def test_oauth_discovery_fallback_conditions(self, oauth_provider: OAuthCl
505519 except StopAsyncIteration :
506520 pass # Expected - generator should complete
507521
522+ @pytest .mark .anyio
523+ async def test_oauth_discovery_path_aware_issuer_with_origin_only_metadata (self ):
524+ """Servers may publish metadata only at the origin even when issuer has a path;
525+ ensure we include origin fallbacks."""
526+ # Path-aware issuer URL
527+ discovery_urls = build_oauth_authorization_server_metadata_discovery_urls (
528+ "https://auth.example.com/tenant1" , "https://api.example.com/v1/mcp"
529+ )
530+
531+ # Must include origin-based fallbacks at the end, after path-aware variants
532+ assert discovery_urls [- 2 :] == [
533+ "https://auth.example.com/.well-known/oauth-authorization-server" ,
534+ "https://auth.example.com/.well-known/openid-configuration" ,
535+ ]
536+
537+ @pytest .mark .anyio
538+ async def test_oauth_discovery_direct_metadata_url_precedence (self ):
539+ """If a direct metadata URL is provided, it should be tried first before derived well-known locations."""
540+ # Simulate PRM providing a direct OIDC configuration URL
541+ direct_metadata_url = "https://auth.example.com/.well-known/openid-configuration"
542+ discovery_urls = build_oauth_authorization_server_metadata_discovery_urls (
543+ direct_metadata_url , "https://api.example.com/v1/mcp"
544+ )
545+
546+ # First entry should be the provided URL exactly
547+ assert discovery_urls [0 ] == direct_metadata_url
548+
549+ @pytest .mark .anyio
550+ async def test_oauth_discovery_oidc_only_metadata (self ):
551+ """Some servers expose only OIDC metadata; ensure OIDC paths are included in order and allow fallback."""
552+ discovery_urls = build_oauth_authorization_server_metadata_discovery_urls (
553+ "https://auth.example.com/tenant1" , "https://api.example.com/v1/mcp"
554+ )
555+
556+ # Ensure OIDC path-aware and path-local are present early
557+ assert "https://auth.example.com/.well-known/openid-configuration/tenant1" in discovery_urls [:3 ]
558+ assert "https://auth.example.com/tenant1/.well-known/openid-configuration" in discovery_urls [:5 ]
559+
560+ # No flow needed here; presence in discovery list is sufficient
561+
508562 @pytest .mark .anyio
509563 async def test_handle_metadata_response_success (self , oauth_provider : OAuthClientProvider ):
510564 """Test successful metadata response handling."""
@@ -1311,9 +1365,9 @@ async def callback_handler() -> tuple[str, str | None]:
13111365 # PRM returns 404 again - all PRM URLs failed
13121366 prm_response_2 = httpx .Response (404 , request = prm_request_2 )
13131367
1314- # Should fall back to root OAuth discovery (March 2025 spec behavior)
1368+ # Current behavior: fall back to path-aware OAuth discovery first using server path
13151369 oauth_metadata_request = await auth_flow .asend (prm_response_2 )
1316- assert str (oauth_metadata_request .url ) == "https://mcp.linear.app/.well-known/oauth-authorization-server"
1370+ assert str (oauth_metadata_request .url ) == "https://mcp.linear.app/.well-known/oauth-authorization-server/sse "
13171371 assert oauth_metadata_request .method == "GET"
13181372
13191373 # Send successful OAuth metadata response
@@ -1419,9 +1473,11 @@ async def callback_handler() -> tuple[str, str | None]:
14191473 # Also returns 404 - all PRM URLs failed
14201474 prm_response_3 = httpx .Response (404 , request = prm_request_3 )
14211475
1422- # Should fall back to root OAuth discovery
1476+ # Current behavior: fall back to path-aware OAuth discovery based on server path
14231477 oauth_metadata_request = await auth_flow .asend (prm_response_3 )
1424- assert str (oauth_metadata_request .url ) == "https://api.example.com/.well-known/oauth-authorization-server"
1478+ assert (
1479+ str (oauth_metadata_request .url ) == "https://api.example.com/.well-known/oauth-authorization-server/v1/mcp"
1480+ )
14251481
14261482 # Complete the flow
14271483 oauth_metadata_response = httpx .Response (
0 commit comments