@@ -347,6 +347,35 @@ describe("OAuth Authorization", () => {
347347 const [ url ] = calls [ 0 ] ;
348348 expect ( url . toString ( ) ) . toBe ( "https://custom.example.com/metadata" ) ;
349349 } ) ;
350+
351+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
352+ const validMetadata = {
353+ resource : "https://resource.example.com" ,
354+ authorization_servers : [ "https://auth.example.com" ] ,
355+ } ;
356+
357+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
358+ ok : true ,
359+ status : 200 ,
360+ json : async ( ) => validMetadata ,
361+ } ) ;
362+
363+ const metadata = await discoverOAuthProtectedResourceMetadata (
364+ "https://resource.example.com" ,
365+ undefined ,
366+ customFetch
367+ ) ;
368+
369+ expect ( metadata ) . toEqual ( validMetadata ) ;
370+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
371+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
372+
373+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
374+ expect ( url . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
375+ expect ( options . headers ) . toEqual ( {
376+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
377+ } ) ;
378+ } ) ;
350379 } ) ;
351380
352381 describe ( "discoverOAuthMetadata" , ( ) => {
@@ -695,6 +724,39 @@ describe("OAuth Authorization", () => {
695724 discoverOAuthMetadata ( "https://auth.example.com" )
696725 ) . rejects . toThrow ( ) ;
697726 } ) ;
727+
728+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
729+ const validMetadata = {
730+ issuer : "https://auth.example.com" ,
731+ authorization_endpoint : "https://auth.example.com/authorize" ,
732+ token_endpoint : "https://auth.example.com/token" ,
733+ registration_endpoint : "https://auth.example.com/register" ,
734+ response_types_supported : [ "code" ] ,
735+ code_challenge_methods_supported : [ "S256" ] ,
736+ } ;
737+
738+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
739+ ok : true ,
740+ status : 200 ,
741+ json : async ( ) => validMetadata ,
742+ } ) ;
743+
744+ const metadata = await discoverOAuthMetadata (
745+ "https://auth.example.com" ,
746+ { } ,
747+ customFetch
748+ ) ;
749+
750+ expect ( metadata ) . toEqual ( validMetadata ) ;
751+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
752+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
753+
754+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
755+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
756+ expect ( options . headers ) . toEqual ( {
757+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
758+ } ) ;
759+ } ) ;
698760 } ) ;
699761
700762 describe ( "startAuthorization" , ( ) => {
@@ -993,6 +1055,46 @@ describe("OAuth Authorization", () => {
9931055 } )
9941056 ) . rejects . toThrow ( "Token exchange failed" ) ;
9951057 } ) ;
1058+
1059+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
1060+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
1061+ ok : true ,
1062+ status : 200 ,
1063+ json : async ( ) => validTokens ,
1064+ } ) ;
1065+
1066+ const tokens = await exchangeAuthorization ( "https://auth.example.com" , {
1067+ clientInformation : validClientInfo ,
1068+ authorizationCode : "code123" ,
1069+ codeVerifier : "verifier123" ,
1070+ redirectUri : "http://localhost:3000/callback" ,
1071+ resource : new URL ( "https://api.example.com/mcp-server" ) ,
1072+ fetchFn : customFetch ,
1073+ } ) ;
1074+
1075+ expect ( tokens ) . toEqual ( validTokens ) ;
1076+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
1077+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
1078+
1079+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
1080+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/token" ) ;
1081+ expect ( options ) . toEqual (
1082+ expect . objectContaining ( {
1083+ method : "POST" ,
1084+ headers : expect . any ( Headers ) ,
1085+ body : expect . any ( URLSearchParams ) ,
1086+ } )
1087+ ) ;
1088+
1089+ const body = options . body as URLSearchParams ;
1090+ expect ( body . get ( "grant_type" ) ) . toBe ( "authorization_code" ) ;
1091+ expect ( body . get ( "code" ) ) . toBe ( "code123" ) ;
1092+ expect ( body . get ( "code_verifier" ) ) . toBe ( "verifier123" ) ;
1093+ expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
1094+ expect ( body . get ( "client_secret" ) ) . toBe ( "secret123" ) ;
1095+ expect ( body . get ( "redirect_uri" ) ) . toBe ( "http://localhost:3000/callback" ) ;
1096+ expect ( body . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
1097+ } ) ;
9961098 } ) ;
9971099
9981100 describe ( "refreshAuthorization" , ( ) => {
@@ -1900,6 +2002,68 @@ describe("OAuth Authorization", () => {
19002002 // Second call should be to AS metadata with the path from authorization server
19012003 expect ( calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/oauth" ) ;
19022004 } ) ;
2005+
2006+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
2007+ const customFetch = jest . fn ( ) ;
2008+
2009+ // Mock PRM discovery
2010+ customFetch . mockResolvedValueOnce ( {
2011+ ok : true ,
2012+ status : 200 ,
2013+ json : async ( ) => ( {
2014+ resource : "https://resource.example.com" ,
2015+ authorization_servers : [ "https://auth.example.com" ] ,
2016+ } ) ,
2017+ } ) ;
2018+
2019+ // Mock AS metadata discovery
2020+ customFetch . mockResolvedValueOnce ( {
2021+ ok : true ,
2022+ status : 200 ,
2023+ json : async ( ) => ( {
2024+ issuer : "https://auth.example.com" ,
2025+ authorization_endpoint : "https://auth.example.com/authorize" ,
2026+ token_endpoint : "https://auth.example.com/token" ,
2027+ registration_endpoint : "https://auth.example.com/register" ,
2028+ response_types_supported : [ "code" ] ,
2029+ code_challenge_methods_supported : [ "S256" ] ,
2030+ } ) ,
2031+ } ) ;
2032+
2033+ const mockProvider : OAuthClientProvider = {
2034+ get redirectUrl ( ) { return "http://localhost:3000/callback" ; } ,
2035+ get clientMetadata ( ) {
2036+ return {
2037+ client_name : "Test Client" ,
2038+ redirect_uris : [ "http://localhost:3000/callback" ] ,
2039+ } ;
2040+ } ,
2041+ clientInformation : jest . fn ( ) . mockResolvedValue ( {
2042+ client_id : "client123" ,
2043+ client_secret : "secret123" ,
2044+ } ) ,
2045+ tokens : jest . fn ( ) . mockResolvedValue ( undefined ) ,
2046+ saveTokens : jest . fn ( ) ,
2047+ redirectToAuthorization : jest . fn ( ) ,
2048+ saveCodeVerifier : jest . fn ( ) ,
2049+ codeVerifier : jest . fn ( ) . mockResolvedValue ( "verifier123" ) ,
2050+ } ;
2051+
2052+ const result = await auth ( mockProvider , {
2053+ serverUrl : "https://resource.example.com" ,
2054+ fetchFn : customFetch ,
2055+ } ) ;
2056+
2057+ expect ( result ) . toBe ( "REDIRECT" ) ;
2058+ expect ( customFetch ) . toHaveBeenCalledTimes ( 2 ) ;
2059+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
2060+
2061+ // Verify custom fetch was called for PRM discovery
2062+ expect ( customFetch . mock . calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
2063+
2064+ // Verify custom fetch was called for AS metadata discovery
2065+ expect ( customFetch . mock . calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
2066+ } ) ;
19032067 } ) ;
19042068
19052069 describe ( "exchangeAuthorization with multiple client authentication methods" , ( ) => {
0 commit comments