@@ -225,6 +225,126 @@ describe("OAuth Authorization", () => {
225225 } ) ;
226226 } ) ;
227227
228+ it ( "falls back to root discovery when path-aware discovery returns 404" , async ( ) => {
229+ // First call (path-aware) returns 404
230+ mockFetch . mockResolvedValueOnce ( {
231+ ok : false ,
232+ status : 404 ,
233+ } ) ;
234+
235+ // Second call (root fallback) succeeds
236+ mockFetch . mockResolvedValueOnce ( {
237+ ok : true ,
238+ status : 200 ,
239+ json : async ( ) => validMetadata ,
240+ } ) ;
241+
242+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com/path/name" ) ;
243+ expect ( metadata ) . toEqual ( validMetadata ) ;
244+
245+ const calls = mockFetch . mock . calls ;
246+ expect ( calls . length ) . toBe ( 2 ) ;
247+
248+ // First call should be path-aware
249+ const [ firstUrl , firstOptions ] = calls [ 0 ] ;
250+ expect ( firstUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/path/name" ) ;
251+ expect ( firstOptions . headers ) . toEqual ( {
252+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
253+ } ) ;
254+
255+ // Second call should be root fallback
256+ const [ secondUrl , secondOptions ] = calls [ 1 ] ;
257+ expect ( secondUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
258+ expect ( secondOptions . headers ) . toEqual ( {
259+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
260+ } ) ;
261+ } ) ;
262+
263+ it ( "returns undefined when both path-aware and root discovery return 404" , async ( ) => {
264+ // First call (path-aware) returns 404
265+ mockFetch . mockResolvedValueOnce ( {
266+ ok : false ,
267+ status : 404 ,
268+ } ) ;
269+
270+ // Second call (root fallback) also returns 404
271+ mockFetch . mockResolvedValueOnce ( {
272+ ok : false ,
273+ status : 404 ,
274+ } ) ;
275+
276+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com/path/name" ) ;
277+ expect ( metadata ) . toBeUndefined ( ) ;
278+
279+ const calls = mockFetch . mock . calls ;
280+ expect ( calls . length ) . toBe ( 2 ) ;
281+ } ) ;
282+
283+ it ( "does not fallback when the original URL is already at root path" , async ( ) => {
284+ // First call (path-aware for root) returns 404
285+ mockFetch . mockResolvedValueOnce ( {
286+ ok : false ,
287+ status : 404 ,
288+ } ) ;
289+
290+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com/" ) ;
291+ expect ( metadata ) . toBeUndefined ( ) ;
292+
293+ const calls = mockFetch . mock . calls ;
294+ expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
295+
296+ const [ url ] = calls [ 0 ] ;
297+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
298+ } ) ;
299+
300+ it ( "does not fallback when the original URL has no path" , async ( ) => {
301+ // First call (path-aware for no path) returns 404
302+ mockFetch . mockResolvedValueOnce ( {
303+ ok : false ,
304+ status : 404 ,
305+ } ) ;
306+
307+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com" ) ;
308+ expect ( metadata ) . toBeUndefined ( ) ;
309+
310+ const calls = mockFetch . mock . calls ;
311+ expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
312+
313+ const [ url ] = calls [ 0 ] ;
314+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
315+ } ) ;
316+
317+ it ( "falls back when path-aware discovery encounters CORS error" , async ( ) => {
318+ // First call (path-aware) fails with TypeError (CORS)
319+ mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
320+
321+ // Retry path-aware without headers (simulating CORS retry)
322+ mockFetch . mockResolvedValueOnce ( {
323+ ok : false ,
324+ status : 404 ,
325+ } ) ;
326+
327+ // Second call (root fallback) succeeds
328+ mockFetch . mockResolvedValueOnce ( {
329+ ok : true ,
330+ status : 200 ,
331+ json : async ( ) => validMetadata ,
332+ } ) ;
333+
334+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com/deep/path" ) ;
335+ expect ( metadata ) . toEqual ( validMetadata ) ;
336+
337+ const calls = mockFetch . mock . calls ;
338+ expect ( calls . length ) . toBe ( 3 ) ;
339+
340+ // Final call should be root fallback
341+ const [ lastUrl , lastOptions ] = calls [ 2 ] ;
342+ expect ( lastUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
343+ expect ( lastOptions . headers ) . toEqual ( {
344+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
345+ } ) ;
346+ } ) ;
347+
228348 it ( "returns metadata when first fetch fails but second without MCP header succeeds" , async ( ) => {
229349 // Set up a counter to control behavior
230350 let callCount = 0 ;
0 commit comments