@@ -16,6 +16,24 @@ let toolDiscoveryAborts = 0;
1616let blockTokenExchange = false ;
1717let tokenExchangeSignals : ( AbortSignal | null | undefined ) [ ] = [ ] ;
1818let tokenExchangeAborts = 0 ;
19+ let lastTransportAuth : ( ( ) => Promise < void > ) | undefined ;
20+ let tokenRefreshSignals : ( AbortSignal | null | undefined ) [ ] = [ ] ;
21+ let tokenRefreshAborts = 0 ;
22+
23+ function hangUntilAbort (
24+ signal : AbortSignal | null | undefined ,
25+ onAbort : ( ) => void ,
26+ fallback : string ,
27+ ) : Promise < void > {
28+ return new Promise ( ( _resolve , reject ) => {
29+ const fail = ( ) : void => {
30+ onAbort ( ) ;
31+ reject ( signal ?. reason ?? new Error ( fallback ) ) ;
32+ } ;
33+ if ( signal ?. aborted === true ) fail ( ) ;
34+ else signal ?. addEventListener ( "abort" , fail , { once : true } ) ;
35+ } ) ;
36+ }
1937
2038const authProvider = { resetAuthorization : async ( ) => undefined } ;
2139
@@ -45,6 +63,11 @@ await withMockedModule(
4563 }
4664 return { tools : [ ] } ;
4765 }
66+ async callTool ( ) : Promise < { content : [ ] } > {
67+ if ( lastTransportAuth === undefined ) throw new Error ( "no live HTTP transport" ) ;
68+ await lastTransportAuth ( ) ;
69+ return { content : [ ] } ;
70+ }
4871 async close ( ) : Promise < void > {
4972 clientCloses += 1 ;
5073 }
@@ -62,21 +85,32 @@ await withMockedModule(
6285 private readonly options ?: { requestInit ?: RequestInit } ,
6386 ) {
6487 transportOptions . push ( options ) ;
88+ lastTransportAuth = ( ) => this . auth ( ) ;
6589 }
6690 async finishAuth ( ) : Promise < void > {
6791 const signal = this . options ?. requestInit ?. signal ;
6892 tokenExchangeSignals . push ( signal ) ;
6993 if ( blockTokenExchange ) {
70- await new Promise < void > ( ( _resolve , reject ) => {
71- const onAbort = ( ) : void => {
94+ await hangUntilAbort (
95+ signal ,
96+ ( ) => {
7297 tokenExchangeAborts += 1 ;
73- reject ( signal ?. reason ?? new Error ( "token exchange aborted" ) ) ;
74- } ;
75- if ( signal ?. aborted === true ) onAbort ( ) ;
76- else signal ?. addEventListener ( "abort" , onAbort , { once : true } ) ;
77- } ) ;
98+ } ,
99+ "token exchange aborted" ,
100+ ) ;
78101 }
79102 }
103+ async auth ( ) : Promise < void > {
104+ const signal = this . options ?. requestInit ?. signal ;
105+ tokenRefreshSignals . push ( signal ) ;
106+ await hangUntilAbort (
107+ signal ,
108+ ( ) => {
109+ tokenRefreshAborts += 1 ;
110+ } ,
111+ "token refresh aborted" ,
112+ ) ;
113+ }
80114 get sessionId ( ) : string | undefined {
81115 return undefined ;
82116 }
@@ -133,6 +167,9 @@ describe("HTTP MCP auth policy", () => {
133167 blockTokenExchange = false ;
134168 tokenExchangeSignals = [ ] ;
135169 tokenExchangeAborts = 0 ;
170+ lastTransportAuth = undefined ;
171+ tokenRefreshSignals = [ ] ;
172+ tokenRefreshAborts = 0 ;
136173 } ) ;
137174
138175 test ( "built-in anonymous Exa treats 401 as a normal failure without OAuth machinery" , async ( ) => {
@@ -212,6 +249,24 @@ describe("HTTP MCP auth policy", () => {
212249 expect ( callbackCloses ) . toBe ( 1 ) ;
213250 } ) ;
214251
252+ test ( "aborts a live-transport auth refresh that ignores the transport abort controller" , async ( ) => {
253+ const abort = new AbortController ( ) ;
254+ const result = await connectMCPServer (
255+ { name : "linear" , type : "http" , url : "https://mcp.linear.app/mcp" } ,
256+ { signal : abort . signal } ,
257+ ) ;
258+ expect ( result . ok ) . toBe ( true ) ;
259+ if ( ! result . ok ) return ;
260+ expect ( transportOptions ) . toEqual ( [ { authProvider, requestInit : { signal : abort . signal } } ] ) ;
261+
262+ const call = result . client . call ( "ping" , { } , abort . signal ) ;
263+ while ( tokenRefreshSignals . length === 0 ) await Promise . resolve ( ) ;
264+ expect ( tokenRefreshSignals [ 0 ] ) . toBe ( abort . signal ) ;
265+ abort . abort ( new Error ( "toolset disposed" ) ) ;
266+ await expect ( call ) . rejects . toThrow ( "toolset disposed" ) ;
267+ expect ( tokenRefreshAborts ) . toBe ( 1 ) ;
268+ } ) ;
269+
215270 test ( "ordinary HTTP creates endpoint-scoped OAuth and passes it to transport" , async ( ) => {
216271 const result = await connectMCPServer ( {
217272 name : "exa" ,
0 commit comments