@@ -106,6 +106,10 @@ class OAuthContext:
106106 # State
107107 lock : anyio .Lock = field (default_factory = anyio .Lock )
108108
109+ # Discovery state for fallback support
110+ discovery_base_url : str | None = None
111+ discovery_pathname : str | None = None
112+
109113 def get_authorization_base_url (self , server_url : str ) -> str :
110114 """Extract base URL by removing path component."""
111115 parsed = urlparse (server_url )
@@ -197,26 +201,53 @@ async def _handle_protected_resource_response(self, response: httpx.Response) ->
197201 except ValidationError :
198202 pass
199203
204+ def _build_well_known_path (self , pathname : str ) -> str :
205+ """Construct well-known path for OAuth metadata discovery."""
206+ well_known_path = f"/.well-known/oauth-authorization-server{ pathname } "
207+ if pathname .endswith ("/" ):
208+ # Strip trailing slash from pathname to avoid double slashes
209+ well_known_path = well_known_path [:- 1 ]
210+ return well_known_path
211+
212+ def _should_attempt_fallback (self , response_status : int , pathname : str ) -> bool :
213+ """Determine if fallback to root discovery should be attempted."""
214+ return response_status == 404 and pathname != "/"
215+
216+ async def _try_metadata_discovery (self , url : str ) -> httpx .Request :
217+ """Build metadata discovery request for a specific URL."""
218+ return httpx .Request ("GET" , url , headers = {MCP_PROTOCOL_VERSION : LATEST_PROTOCOL_VERSION })
219+
200220 async def _discover_oauth_metadata (self ) -> httpx .Request :
201- """Build OAuth metadata discovery request."""
221+ """Build OAuth metadata discovery request with fallback support ."""
202222 if self .context .auth_server_url :
203223 auth_server_url = self .context .auth_server_url
204224 else :
205225 auth_server_url = self .context .server_url
206226
207- # Per RFC 8414, preserve the path component when constructing discovery URL
227+ # Per RFC 8414, try path-aware discovery first
208228 parsed = urlparse (auth_server_url )
209- well_known_path = f"/.well-known/oauth-authorization-server{ parsed .path } "
210- if parsed .path .endswith ("/" ):
211- # Strip trailing slash from pathname
212- well_known_path = well_known_path [:- 1 ]
213-
229+ well_known_path = self ._build_well_known_path (parsed .path )
214230 base_url = f"{ parsed .scheme } ://{ parsed .netloc } "
215231 url = urljoin (base_url , well_known_path )
216- return httpx .Request ("GET" , url , headers = {MCP_PROTOCOL_VERSION : LATEST_PROTOCOL_VERSION })
217232
218- async def _handle_oauth_metadata_response (self , response : httpx .Response ) -> None :
219- """Handle OAuth metadata response."""
233+ # Store fallback info for use in response handler
234+ self .context .discovery_base_url = base_url
235+ self .context .discovery_pathname = parsed .path
236+
237+ return await self ._try_metadata_discovery (url )
238+
239+ async def _discover_oauth_metadata_fallback (self ) -> httpx .Request :
240+ """Build fallback OAuth metadata discovery request for legacy servers."""
241+ base_url = getattr (self .context , "discovery_base_url" , "" )
242+ if not base_url :
243+ raise OAuthFlowError ("No base URL available for fallback discovery" )
244+
245+ # Fallback to root discovery for legacy servers
246+ url = urljoin (base_url , "/.well-known/oauth-authorization-server" )
247+ return await self ._try_metadata_discovery (url )
248+
249+ async def _handle_oauth_metadata_response (self , response : httpx .Response , is_fallback : bool = False ) -> bool :
250+ """Handle OAuth metadata response. Returns True if handled successfully."""
220251 if response .status_code == 200 :
221252 try :
222253 content = await response .aread ()
@@ -225,9 +256,18 @@ async def _handle_oauth_metadata_response(self, response: httpx.Response) -> Non
225256 # Apply default scope if none specified
226257 if self .context .client_metadata .scope is None and metadata .scopes_supported is not None :
227258 self .context .client_metadata .scope = " " .join (metadata .scopes_supported )
259+ return True
228260 except ValidationError :
229261 pass
230262
263+ # Check if we should attempt fallback (404 on path-aware discovery)
264+ if not is_fallback and self ._should_attempt_fallback (
265+ response .status_code , getattr (self .context , "discovery_pathname" , "/" )
266+ ):
267+ return False # Signal that fallback should be attempted
268+
269+ return True # Signal no fallback needed (either success or non-404 error)
270+
231271 async def _register_client (self ) -> httpx .Request | None :
232272 """Build registration request or skip if already registered."""
233273 if self .context .client_info :
@@ -426,10 +466,16 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
426466 discovery_response = yield discovery_request
427467 await self ._handle_protected_resource_response (discovery_response )
428468
429- # Step 2: Discover OAuth metadata
469+ # Step 2: Discover OAuth metadata (with fallback for legacy servers)
430470 oauth_request = await self ._discover_oauth_metadata ()
431471 oauth_response = yield oauth_request
432- await self ._handle_oauth_metadata_response (oauth_response )
472+ handled = await self ._handle_oauth_metadata_response (oauth_response , is_fallback = False )
473+
474+ # If path-aware discovery failed with 404, try fallback to root
475+ if not handled :
476+ fallback_request = await self ._discover_oauth_metadata_fallback ()
477+ fallback_response = yield fallback_request
478+ await self ._handle_oauth_metadata_response (fallback_response , is_fallback = True )
433479
434480 # Step 3: Register client if needed
435481 registration_request = await self ._register_client ()
@@ -472,10 +518,16 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
472518 discovery_response = yield discovery_request
473519 await self ._handle_protected_resource_response (discovery_response )
474520
475- # Step 2: Discover OAuth metadata
521+ # Step 2: Discover OAuth metadata (with fallback for legacy servers)
476522 oauth_request = await self ._discover_oauth_metadata ()
477523 oauth_response = yield oauth_request
478- await self ._handle_oauth_metadata_response (oauth_response )
524+ handled = await self ._handle_oauth_metadata_response (oauth_response , is_fallback = False )
525+
526+ # If path-aware discovery failed with 404, try fallback to root
527+ if not handled :
528+ fallback_request = await self ._discover_oauth_metadata_fallback ()
529+ fallback_response = yield fallback_request
530+ await self ._handle_oauth_metadata_response (fallback_response , is_fallback = True )
479531
480532 # Step 3: Register client if needed
481533 registration_request = await self ._register_client ()
0 commit comments