@@ -2,7 +2,7 @@ import type { JSONRPCMessage, JSONRPCRequest } from '@modelcontextprotocol/core'
22import { OAuthError , OAuthErrorCode , SdkError , SdkErrorCode } from '@modelcontextprotocol/core' ;
33import type { Mock , Mocked } from 'vitest' ;
44
5- import type { OAuthClientProvider } from '../../src/client/auth.js' ;
5+ import type { AuthProvider , OAuthClientProvider } from '../../src/client/auth.js' ;
66import { UnauthorizedError } from '../../src/client/auth.js' ;
77import type { ReconnectionScheduler , StartSSEOptions , StreamableHTTPReconnectionOptions } from '../../src/client/streamableHttp.js' ;
88import { StreamableHTTPClientTransport } from '../../src/client/streamableHttp.js' ;
@@ -12,6 +12,20 @@ describe('StreamableHTTPClientTransport', () => {
1212 let mockAuthProvider : Mocked < OAuthClientProvider > ;
1313
1414 beforeEach ( ( ) => {
15+ const sessionStorageStore = new Map < string , string > ( ) ;
16+ Object . defineProperty ( globalThis , 'sessionStorage' , {
17+ configurable : true ,
18+ value : {
19+ getItem : ( key : string ) => sessionStorageStore . get ( key ) ?? null ,
20+ setItem : ( key : string , value : string ) => {
21+ sessionStorageStore . set ( key , value ) ;
22+ } ,
23+ removeItem : ( key : string ) => {
24+ sessionStorageStore . delete ( key ) ;
25+ }
26+ }
27+ } ) ;
28+
1529 mockAuthProvider = {
1630 get redirectUrl ( ) {
1731 return 'http://localhost/callback' ;
@@ -34,6 +48,7 @@ describe('StreamableHTTPClientTransport', () => {
3448 afterEach ( async ( ) => {
3549 await transport . close ( ) . catch ( ( ) => { } ) ;
3650 vi . clearAllMocks ( ) ;
51+ delete ( globalThis as { sessionStorage ?: Storage } ) . sessionStorage ;
3752 } ) ;
3853
3954 it ( 'should send JSON-RPC messages via POST' , async ( ) => {
@@ -1606,6 +1621,80 @@ describe('StreamableHTTPClientTransport', () => {
16061621 // Global fetch should never have been called
16071622 expect ( globalThis . fetch ) . not . toHaveBeenCalled ( ) ;
16081623 } ) ;
1624+
1625+ it ( 'persists interactive auth metadata across transport recreation before finishAuth' , async ( ) => {
1626+ const customFetch = vi
1627+ . fn ( )
1628+ // First transport send -> 401 with auth metadata
1629+ . mockResolvedValueOnce (
1630+ new Response ( null , {
1631+ status : 401 ,
1632+ headers : {
1633+ 'WWW-Authenticate' :
1634+ 'Bearer resource_metadata="http://localhost:1234/.well-known/oauth-protected-resource", scope="calendar.read"'
1635+ }
1636+ } )
1637+ )
1638+ // Second transport finishAuth -> resource metadata discovery
1639+ . mockResolvedValueOnce (
1640+ Response . json ( {
1641+ authorization_servers : [ 'http://localhost:1234' ] ,
1642+ resource : 'http://localhost:1234/mcp'
1643+ } )
1644+ )
1645+ // auth server metadata discovery
1646+ . mockResolvedValueOnce (
1647+ Response . json ( {
1648+ issuer : 'http://localhost:1234' ,
1649+ authorization_endpoint : 'http://localhost:1234/authorize' ,
1650+ token_endpoint : 'http://localhost:1234/token' ,
1651+ response_types_supported : [ 'code' ] ,
1652+ code_challenge_methods_supported : [ 'S256' ]
1653+ } )
1654+ )
1655+ // authorization code exchange
1656+ . mockResolvedValueOnce (
1657+ Response . json ( {
1658+ access_token : 'new-access-token' ,
1659+ refresh_token : 'new-refresh-token' ,
1660+ token_type : 'Bearer' ,
1661+ expires_in : 3600
1662+ } )
1663+ ) ;
1664+
1665+ const firstAuthProvider : AuthProvider = {
1666+ token : vi . fn ( async ( ) => undefined )
1667+ } ;
1668+
1669+ const firstTransport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) , {
1670+ authProvider : firstAuthProvider ,
1671+ fetch : customFetch
1672+ } ) ;
1673+
1674+ await firstTransport . start ( ) ;
1675+
1676+ await expect ( firstTransport . send ( { jsonrpc : '2.0' , method : 'ping' , params : { } , id : '1' } as JSONRPCMessage ) ) . rejects . toThrow (
1677+ UnauthorizedError
1678+ ) ;
1679+
1680+ await firstTransport . close ( ) ;
1681+
1682+ const secondTransport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) , {
1683+ authProvider : mockAuthProvider ,
1684+ fetch : customFetch
1685+ } ) ;
1686+
1687+ await secondTransport . finishAuth ( 'auth-code-after-redirect' ) ;
1688+
1689+ expect ( mockAuthProvider . saveTokens ) . toHaveBeenCalledWith ( {
1690+ access_token : 'new-access-token' ,
1691+ token_type : 'Bearer' ,
1692+ expires_in : 3600 ,
1693+ refresh_token : 'new-refresh-token'
1694+ } ) ;
1695+
1696+ await secondTransport . close ( ) ;
1697+ } ) ;
16091698 } ) ;
16101699
16111700 describe ( 'SSE retry field handling' , ( ) => {
0 commit comments