@@ -271,6 +271,117 @@ describe("OAuth Proxy Routes", () => {
271271
272272 expect ( res . status ) . toBe ( 401 ) ;
273273 } ) ;
274+
275+ test ( "generates synthetic metadata when origin has no metadata but supports OAuth via WWW-Authenticate" , async ( ) => {
276+ mockConnectionStorage ( {
277+ connection_url : "https://mcp.example.com" ,
278+ } ) ;
279+
280+ global . fetch = mock ( ( url : string , options ?: RequestInit ) => {
281+ // First 3 calls: Protected Resource Metadata discovery (returns 404)
282+ if (
283+ ( url as string ) . includes ( "oauth-protected-resource" ) &&
284+ options ?. method === "GET"
285+ ) {
286+ return Promise . resolve (
287+ new Response ( JSON . stringify ( { error : "Not found" } ) , {
288+ status : 404 ,
289+ statusText : "Not Found" ,
290+ } ) ,
291+ ) ;
292+ }
293+
294+ // 4th call: checkOriginSupportsOAuth - returns 401 with WWW-Authenticate
295+ if ( options ?. method === "POST" ) {
296+ return Promise . resolve (
297+ new Response (
298+ JSON . stringify ( {
299+ error : "invalid_token" ,
300+ error_description : "Missing or invalid access token" ,
301+ } ) ,
302+ {
303+ status : 401 ,
304+ headers : {
305+ "WWW-Authenticate" :
306+ 'Bearer realm="OAuth", error="invalid_token"' ,
307+ "Content-Type" : "application/json" ,
308+ } ,
309+ } ,
310+ ) ,
311+ ) ;
312+ }
313+
314+ return Promise . resolve (
315+ new Response ( JSON . stringify ( { error : "Unexpected request" } ) , {
316+ status : 500 ,
317+ } ) ,
318+ ) ;
319+ } ) as unknown as typeof fetch ;
320+
321+ const res = await app . request (
322+ "http://localhost:3000/.well-known/oauth-protected-resource/mcp/conn_123" ,
323+ ) ;
324+
325+ expect ( res . status ) . toBe ( 200 ) ;
326+ const body = ( await res . json ( ) ) as {
327+ resource : string ;
328+ authorization_servers : string [ ] ;
329+ bearer_methods_supported : string [ ] ;
330+ scopes_supported : string [ ] ;
331+ } ;
332+ expect ( body . resource ) . toBe ( "http://localhost:3000/mcp/conn_123" ) ;
333+ expect ( body . authorization_servers ) . toEqual ( [
334+ "http://localhost:3000/oauth-proxy/conn_123" ,
335+ ] ) ;
336+ expect ( body . bearer_methods_supported ) . toEqual ( [ "header" ] ) ;
337+ expect ( body . scopes_supported ) . toEqual ( [ "*" ] ) ;
338+ } ) ;
339+
340+ test ( "returns 404 when origin has no metadata and does not support OAuth" , async ( ) => {
341+ mockConnectionStorage ( {
342+ connection_url : "https://mcp.example.com" ,
343+ } ) ;
344+
345+ global . fetch = mock ( ( url : string , options ?: RequestInit ) => {
346+ // Protected Resource Metadata discovery (returns 404)
347+ if (
348+ ( url as string ) . includes ( "oauth-protected-resource" ) &&
349+ options ?. method === "GET"
350+ ) {
351+ return Promise . resolve (
352+ new Response ( JSON . stringify ( { error : "Not found" } ) , {
353+ status : 404 ,
354+ statusText : "Not Found" ,
355+ } ) ,
356+ ) ;
357+ }
358+
359+ // checkOriginSupportsOAuth - returns 401 WITHOUT WWW-Authenticate (no OAuth support)
360+ if ( options ?. method === "POST" ) {
361+ return Promise . resolve (
362+ new Response ( JSON . stringify ( { error : "Unauthorized" } ) , {
363+ status : 401 ,
364+ headers : {
365+ "Content-Type" : "application/json" ,
366+ } ,
367+ } ) ,
368+ ) ;
369+ }
370+
371+ return Promise . resolve (
372+ new Response ( JSON . stringify ( { error : "Unexpected request" } ) , {
373+ status : 500 ,
374+ } ) ,
375+ ) ;
376+ } ) as unknown as typeof fetch ;
377+
378+ const res = await app . request (
379+ "http://localhost:3000/.well-known/oauth-protected-resource/mcp/conn_123" ,
380+ ) ;
381+
382+ // Should pass through the 404 since origin doesn't support OAuth
383+ expect ( res . status ) . toBe ( 404 ) ;
384+ } ) ;
274385 } ) ;
275386
276387 describe ( "Authorization Server Metadata Proxy" , ( ) => {
@@ -320,23 +431,74 @@ describe("OAuth Proxy Routes", () => {
320431 expect ( res . status ) . toBe ( 404 ) ;
321432 } ) ;
322433
323- test ( "returns 404 when no auth server in protected resource metadata" , async ( ) => {
324- mockConnectionWithAuthServer (
325- { connection_url : "https://origin.example.com/mcp" } ,
326- new Response (
327- JSON . stringify ( {
328- resource : "https://origin.example.com/mcp" ,
329- authorization_servers : [ ] , // Empty
434+ test ( "falls back to origin root when protected resource metadata has empty auth servers" , async ( ) => {
435+ // When Protected Resource Metadata has empty authorization_servers,
436+ // we should fall back to the origin's root and try to fetch auth server metadata from there
437+ ( ContextFactory . create as ReturnType < typeof mock > ) . mockImplementation (
438+ ( ) =>
439+ Promise . resolve ( {
440+ storage : {
441+ connections : {
442+ findById : mock ( ( ) =>
443+ Promise . resolve ( {
444+ connection_url : "https://origin.example.com/mcp" ,
445+ } ) ,
446+ ) ,
447+ } ,
448+ } ,
330449 } ) ,
331- { status : 200 , headers : { "Content-Type" : "application/json" } } ,
332- ) ,
333450 ) ;
334451
452+ global . fetch = mock ( ( url : string ) => {
453+ if ( ( url as string ) . includes ( "oauth-protected-resource" ) ) {
454+ // Return metadata with empty auth servers
455+ return Promise . resolve (
456+ new Response (
457+ JSON . stringify ( {
458+ resource : "https://origin.example.com/mcp" ,
459+ authorization_servers : [ ] , // Empty - should trigger fallback
460+ } ) ,
461+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
462+ ) ,
463+ ) ;
464+ }
465+ // Auth server metadata at origin root
466+ if ( ( url as string ) . includes ( "oauth-authorization-server" ) ) {
467+ return Promise . resolve (
468+ new Response (
469+ JSON . stringify ( {
470+ issuer : "https://origin.example.com" ,
471+ authorization_endpoint : "https://origin.example.com/authorize" ,
472+ token_endpoint : "https://origin.example.com/token" ,
473+ } ) ,
474+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
475+ ) ,
476+ ) ;
477+ }
478+ return Promise . resolve (
479+ new Response ( JSON . stringify ( { error : "Unexpected" } ) , {
480+ status : 500 ,
481+ } ) ,
482+ ) ;
483+ } ) as unknown as typeof fetch ;
484+
335485 const res = await app . request (
336- "/.well-known/oauth-authorization-server/oauth-proxy/conn_123" ,
486+ "http://localhost:3000 /.well-known/oauth-authorization-server/oauth-proxy/conn_123" ,
337487 ) ;
338488
339- expect ( res . status ) . toBe ( 404 ) ;
489+ // Should succeed by falling back to origin root
490+ expect ( res . status ) . toBe ( 200 ) ;
491+ const body = ( await res . json ( ) ) as {
492+ authorization_endpoint : string ;
493+ token_endpoint : string ;
494+ } ;
495+ // URLs should be rewritten to go through our proxy
496+ expect ( body . authorization_endpoint ) . toBe (
497+ "http://localhost:3000/oauth-proxy/conn_123/authorize" ,
498+ ) ;
499+ expect ( body . token_endpoint ) . toBe (
500+ "http://localhost:3000/oauth-proxy/conn_123/token" ,
501+ ) ;
340502 } ) ;
341503
342504 test ( "proxies and rewrites authorization server metadata" , async ( ) => {
0 commit comments