@@ -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" , ( ) => {
@@ -619,6 +648,39 @@ describe("OAuth Authorization", () => {
619648 discoverOAuthMetadata ( "https://auth.example.com" )
620649 ) . rejects . toThrow ( ) ;
621650 } ) ;
651+
652+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
653+ const validMetadata = {
654+ issuer : "https://auth.example.com" ,
655+ authorization_endpoint : "https://auth.example.com/authorize" ,
656+ token_endpoint : "https://auth.example.com/token" ,
657+ registration_endpoint : "https://auth.example.com/register" ,
658+ response_types_supported : [ "code" ] ,
659+ code_challenge_methods_supported : [ "S256" ] ,
660+ } ;
661+
662+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
663+ ok : true ,
664+ status : 200 ,
665+ json : async ( ) => validMetadata ,
666+ } ) ;
667+
668+ const metadata = await discoverOAuthMetadata (
669+ "https://auth.example.com" ,
670+ { } ,
671+ customFetch
672+ ) ;
673+
674+ expect ( metadata ) . toEqual ( validMetadata ) ;
675+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
676+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
677+
678+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
679+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
680+ expect ( options . headers ) . toEqual ( {
681+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
682+ } ) ;
683+ } ) ;
622684 } ) ;
623685
624686 describe ( "startAuthorization" , ( ) => {
@@ -917,6 +979,46 @@ describe("OAuth Authorization", () => {
917979 } )
918980 ) . rejects . toThrow ( "Token exchange failed" ) ;
919981 } ) ;
982+
983+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
984+ const customFetch = jest . fn ( ) . mockResolvedValue ( {
985+ ok : true ,
986+ status : 200 ,
987+ json : async ( ) => validTokens ,
988+ } ) ;
989+
990+ const tokens = await exchangeAuthorization ( "https://auth.example.com" , {
991+ clientInformation : validClientInfo ,
992+ authorizationCode : "code123" ,
993+ codeVerifier : "verifier123" ,
994+ redirectUri : "http://localhost:3000/callback" ,
995+ resource : new URL ( "https://api.example.com/mcp-server" ) ,
996+ fetchFn : customFetch ,
997+ } ) ;
998+
999+ expect ( tokens ) . toEqual ( validTokens ) ;
1000+ expect ( customFetch ) . toHaveBeenCalledTimes ( 1 ) ;
1001+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
1002+
1003+ const [ url , options ] = customFetch . mock . calls [ 0 ] ;
1004+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/token" ) ;
1005+ expect ( options ) . toEqual (
1006+ expect . objectContaining ( {
1007+ method : "POST" ,
1008+ headers : expect . any ( Headers ) ,
1009+ body : expect . any ( URLSearchParams ) ,
1010+ } )
1011+ ) ;
1012+
1013+ const body = options . body as URLSearchParams ;
1014+ expect ( body . get ( "grant_type" ) ) . toBe ( "authorization_code" ) ;
1015+ expect ( body . get ( "code" ) ) . toBe ( "code123" ) ;
1016+ expect ( body . get ( "code_verifier" ) ) . toBe ( "verifier123" ) ;
1017+ expect ( body . get ( "client_id" ) ) . toBe ( "client123" ) ;
1018+ expect ( body . get ( "client_secret" ) ) . toBe ( "secret123" ) ;
1019+ expect ( body . get ( "redirect_uri" ) ) . toBe ( "http://localhost:3000/callback" ) ;
1020+ expect ( body . get ( "resource" ) ) . toBe ( "https://api.example.com/mcp-server" ) ;
1021+ } ) ;
9201022 } ) ;
9211023
9221024 describe ( "refreshAuthorization" , ( ) => {
@@ -1824,6 +1926,68 @@ describe("OAuth Authorization", () => {
18241926 // Second call should be to AS metadata with the path from authorization server
18251927 expect ( calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/oauth" ) ;
18261928 } ) ;
1929+
1930+ it ( "supports overriding the fetch function used for requests" , async ( ) => {
1931+ const customFetch = jest . fn ( ) ;
1932+
1933+ // Mock PRM discovery
1934+ customFetch . mockResolvedValueOnce ( {
1935+ ok : true ,
1936+ status : 200 ,
1937+ json : async ( ) => ( {
1938+ resource : "https://resource.example.com" ,
1939+ authorization_servers : [ "https://auth.example.com" ] ,
1940+ } ) ,
1941+ } ) ;
1942+
1943+ // Mock AS metadata discovery
1944+ customFetch . mockResolvedValueOnce ( {
1945+ ok : true ,
1946+ status : 200 ,
1947+ json : async ( ) => ( {
1948+ issuer : "https://auth.example.com" ,
1949+ authorization_endpoint : "https://auth.example.com/authorize" ,
1950+ token_endpoint : "https://auth.example.com/token" ,
1951+ registration_endpoint : "https://auth.example.com/register" ,
1952+ response_types_supported : [ "code" ] ,
1953+ code_challenge_methods_supported : [ "S256" ] ,
1954+ } ) ,
1955+ } ) ;
1956+
1957+ const mockProvider : OAuthClientProvider = {
1958+ get redirectUrl ( ) { return "http://localhost:3000/callback" ; } ,
1959+ get clientMetadata ( ) {
1960+ return {
1961+ client_name : "Test Client" ,
1962+ redirect_uris : [ "http://localhost:3000/callback" ] ,
1963+ } ;
1964+ } ,
1965+ clientInformation : jest . fn ( ) . mockResolvedValue ( {
1966+ client_id : "client123" ,
1967+ client_secret : "secret123" ,
1968+ } ) ,
1969+ tokens : jest . fn ( ) . mockResolvedValue ( undefined ) ,
1970+ saveTokens : jest . fn ( ) ,
1971+ redirectToAuthorization : jest . fn ( ) ,
1972+ saveCodeVerifier : jest . fn ( ) ,
1973+ codeVerifier : jest . fn ( ) . mockResolvedValue ( "verifier123" ) ,
1974+ } ;
1975+
1976+ const result = await auth ( mockProvider , {
1977+ serverUrl : "https://resource.example.com" ,
1978+ fetchFn : customFetch ,
1979+ } ) ;
1980+
1981+ expect ( result ) . toBe ( "REDIRECT" ) ;
1982+ expect ( customFetch ) . toHaveBeenCalledTimes ( 2 ) ;
1983+ expect ( mockFetch ) . not . toHaveBeenCalled ( ) ;
1984+
1985+ // Verify custom fetch was called for PRM discovery
1986+ expect ( customFetch . mock . calls [ 0 ] [ 0 ] . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
1987+
1988+ // Verify custom fetch was called for AS metadata discovery
1989+ expect ( customFetch . mock . calls [ 1 ] [ 0 ] . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
1990+ } ) ;
18271991 } ) ;
18281992
18291993 describe ( "exchangeAuthorization with multiple client authentication methods" , ( ) => {
0 commit comments