88from pydantic import AnyHttpUrl
99from starlette .applications import Starlette
1010
11- from mcp .server .auth .routes import create_protected_resource_routes
11+ from mcp .server .auth .routes import build_resource_metadata_url , create_protected_resource_routes
1212
1313
1414@pytest .fixture
@@ -36,10 +36,11 @@ async def test_client(test_app: Starlette):
3636
3737
3838@pytest .mark .anyio
39- async def test_metadata_endpoint (test_client : httpx .AsyncClient ):
40- """Test the OAuth 2.0 Protected Resource metadata endpoint."""
39+ async def test_metadata_endpoint_with_path (test_client : httpx .AsyncClient ):
40+ """Test the OAuth 2.0 Protected Resource metadata endpoint for path-based resource ."""
4141
42- response = await test_client .get ("/.well-known/oauth-protected-resource" )
42+ # For resource with path "/resource", metadata should be accessible at the path-aware location
43+ response = await test_client .get ("/.well-known/oauth-protected-resource/resource" )
4344 assert response .json () == snapshot (
4445 {
4546 "resource" : "https://example.com/resource" ,
@@ -50,3 +51,148 @@ async def test_metadata_endpoint(test_client: httpx.AsyncClient):
5051 "bearer_methods_supported" : ["header" ],
5152 }
5253 )
54+
55+
56+ @pytest .mark .anyio
57+ async def test_metadata_endpoint_root_path_returns_404 (test_client : httpx .AsyncClient ):
58+ """Test that root path returns 404 for path-based resource."""
59+
60+ # Root path should return 404 for path-based resources
61+ response = await test_client .get ("/.well-known/oauth-protected-resource" )
62+ assert response .status_code == 404
63+
64+
65+ @pytest .fixture
66+ def root_resource_app ():
67+ """Fixture to create protected resource routes for root-level resource."""
68+
69+ # Create routes for a resource without path component
70+ protected_resource_routes = create_protected_resource_routes (
71+ resource_url = AnyHttpUrl ("https://example.com" ),
72+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
73+ scopes_supported = ["read" ],
74+ resource_name = "Root Resource" ,
75+ )
76+
77+ app = Starlette (routes = protected_resource_routes )
78+ return app
79+
80+
81+ @pytest .fixture
82+ async def root_resource_client (root_resource_app : Starlette ):
83+ """Fixture to create an HTTP client for the root resource app."""
84+ async with httpx .AsyncClient (
85+ transport = httpx .ASGITransport (app = root_resource_app ), base_url = "https://mcptest.com"
86+ ) as client :
87+ yield client
88+
89+
90+ @pytest .mark .anyio
91+ async def test_metadata_endpoint_without_path (root_resource_client : httpx .AsyncClient ):
92+ """Test metadata endpoint for root-level resource."""
93+
94+ # For root resource, metadata should be at standard location
95+ response = await root_resource_client .get ("/.well-known/oauth-protected-resource" )
96+ assert response .status_code == 200
97+ assert response .json () == snapshot (
98+ {
99+ "resource" : "https://example.com/" ,
100+ "authorization_servers" : ["https://auth.example.com/" ],
101+ "scopes_supported" : ["read" ],
102+ "resource_name" : "Root Resource" ,
103+ "bearer_methods_supported" : ["header" ],
104+ }
105+ )
106+
107+
108+ class TestMetadataUrlConstruction :
109+ """Test URL construction utility function."""
110+
111+ def test_url_without_path (self ):
112+ """Test URL construction for resource without path component."""
113+ resource_url = AnyHttpUrl ("https://example.com" )
114+ result = build_resource_metadata_url (resource_url )
115+ assert str (result ) == "https://example.com/.well-known/oauth-protected-resource"
116+
117+ def test_url_with_path_component (self ):
118+ """Test URL construction for resource with path component."""
119+ resource_url = AnyHttpUrl ("https://example.com/mcp" )
120+ result = build_resource_metadata_url (resource_url )
121+ assert str (result ) == "https://example.com/.well-known/oauth-protected-resource/mcp"
122+
123+ def test_url_with_trailing_slash_only (self ):
124+ """Test URL construction for resource with trailing slash only."""
125+ resource_url = AnyHttpUrl ("https://example.com/" )
126+ result = build_resource_metadata_url (resource_url )
127+ # Trailing slash should be treated as empty path
128+ assert str (result ) == "https://example.com/.well-known/oauth-protected-resource"
129+
130+ @pytest .mark .parametrize (
131+ "resource_url,expected_url" ,
132+ [
133+ ("https://example.com" , "https://example.com/.well-known/oauth-protected-resource" ),
134+ ("https://example.com/" , "https://example.com/.well-known/oauth-protected-resource" ),
135+ ("https://example.com/mcp" , "https://example.com/.well-known/oauth-protected-resource/mcp" ),
136+ ("http://localhost:8001/mcp" , "http://localhost:8001/.well-known/oauth-protected-resource/mcp" ),
137+ ],
138+ )
139+ def test_various_resource_configurations (self , resource_url : str , expected_url : str ):
140+ """Test URL construction with various resource configurations."""
141+ result = build_resource_metadata_url (AnyHttpUrl (resource_url ))
142+ assert str (result ) == expected_url
143+
144+
145+ class TestRouteConsistency :
146+ """Test consistency between URL generation and route registration."""
147+
148+ def test_route_path_matches_metadata_url (self ):
149+ """Test that route path matches the generated metadata URL."""
150+ resource_url = AnyHttpUrl ("https://example.com/mcp" )
151+
152+ # Generate metadata URL
153+ metadata_url = build_resource_metadata_url (resource_url )
154+
155+ # Create routes
156+ routes = create_protected_resource_routes (
157+ resource_url = resource_url ,
158+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
159+ )
160+
161+ # Extract path from metadata URL
162+ from urllib .parse import urlparse
163+
164+ metadata_path = urlparse (str (metadata_url )).path
165+
166+ # Verify consistency
167+ assert len (routes ) == 1
168+ assert routes [0 ].path == metadata_path
169+
170+ @pytest .mark .parametrize (
171+ "resource_url,expected_path" ,
172+ [
173+ ("https://example.com" , "/.well-known/oauth-protected-resource" ),
174+ ("https://example.com/" , "/.well-known/oauth-protected-resource" ),
175+ ("https://example.com/mcp" , "/.well-known/oauth-protected-resource/mcp" ),
176+ ],
177+ )
178+ def test_consistent_paths_for_various_resources (self , resource_url : str , expected_path : str ):
179+ """Test that URL generation and route creation are consistent."""
180+ resource_url_obj = AnyHttpUrl (resource_url )
181+
182+ # Test URL generation
183+ metadata_url = build_resource_metadata_url (resource_url_obj )
184+ from urllib .parse import urlparse
185+
186+ url_path = urlparse (str (metadata_url )).path
187+
188+ # Test route creation
189+ routes = create_protected_resource_routes (
190+ resource_url = resource_url_obj ,
191+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
192+ )
193+ route_path = routes [0 ].path
194+
195+ # Both should match expected path
196+ assert url_path == expected_path
197+ assert route_path == expected_path
198+ assert url_path == route_path
0 commit comments