@@ -49,7 +49,6 @@ export class StreamableHTTPClientTransport implements Transport {
4949 private _requestInit ?: RequestInit ;
5050 private _authProvider ?: OAuthClientProvider ;
5151 private _sessionId ?: string ;
52- private _lastEventId ?: string ;
5352
5453 onclose ?: ( ) => void ;
5554 onerror ?: ( error : Error ) => void ;
@@ -102,16 +101,16 @@ export class StreamableHTTPClientTransport implements Transport {
102101 ) ;
103102 }
104103
105- private async _startOrAuthStandaloneSSE ( ) : Promise < void > {
104+ private async _startOrAuthStandaloneSSE ( lastEventId ?: string ) : Promise < void > {
106105 try {
107106 // Try to open an initial SSE stream with GET to listen for server messages
108107 // This is optional according to the spec - server may not support it
109108 const headers = await this . _commonHeaders ( ) ;
110109 headers . set ( "Accept" , "text/event-stream" ) ;
111110
112- // Include Last-Event-ID header for resumable streams
113- if ( this . _lastEventId ) {
114- headers . set ( "last-event-id" , this . _lastEventId ) ;
111+ // Include Last-Event-ID header for resumable streams if provided
112+ if ( lastEventId ) {
113+ headers . set ( "last-event-id" , lastEventId ) ;
115114 }
116115
117116 const response = await fetch ( this . _url , {
@@ -150,31 +149,61 @@ export class StreamableHTTPClientTransport implements Transport {
150149 return ;
151150 }
152151
152+ let lastEventId : string | undefined ;
153+
153154 const processStream = async ( ) => {
154155 // Create a pipeline: binary stream -> text decoder -> SSE parser
155156 const eventStream = stream
156157 . pipeThrough ( new TextDecoderStream ( ) )
157158 . pipeThrough ( new EventSourceParserStream ( ) ) ;
158159
159- for await ( const event of eventStream ) {
160- // Update last event ID if provided
161- if ( event . id ) {
162- this . _lastEventId = event . id ;
163- }
164- // Handle message events (default event type is undefined per docs)
165- // or explicit 'message' event type
166- if ( ! event . event || event . event === "message" ) {
167- try {
168- const message = JSONRPCMessageSchema . parse ( JSON . parse ( event . data ) ) ;
169- this . onmessage ?.( message ) ;
170- } catch ( error ) {
171- this . onerror ?.( error as Error ) ;
160+ try {
161+ for await ( const event of eventStream ) {
162+ // Update last event ID if provided
163+ if ( event . id ) {
164+ lastEventId = event . id ;
165+ }
166+
167+ // Handle message events (default event type is undefined per docs)
168+ // or explicit 'message' event type
169+ if ( ! event . event || event . event === "message" ) {
170+ try {
171+ const message = JSONRPCMessageSchema . parse ( JSON . parse ( event . data ) ) ;
172+ this . onmessage ?.( message ) ;
173+ } catch ( error ) {
174+ this . onerror ?.( error as Error ) ;
175+ }
172176 }
173177 }
178+ } catch ( error ) {
179+ // Handle stream errors - likely a network disconnect
180+ this . onerror ?.( new Error ( `SSE stream disconnected: ${ error instanceof Error ? error . message : String ( error ) } ` ) ) ;
181+
182+ // Attempt to reconnect if the stream disconnects unexpectedly
183+ // Wait a short time before reconnecting to avoid rapid reconnection loops
184+ if ( this . _abortController && ! this . _abortController . signal . aborted ) {
185+ setTimeout ( ( ) => {
186+ // Use the last event ID to resume where we left off
187+ this . _startOrAuthStandaloneSSE ( lastEventId ) . catch ( reconnectError => {
188+ this . onerror ?.( new Error ( `Failed to reconnect SSE stream: ${ reconnectError instanceof Error ? reconnectError . message : String ( reconnectError ) } ` ) ) ;
189+ } ) ;
190+ } , 1000 ) ; // 1 second delay before reconnection attempt
191+ }
174192 }
175193 } ;
176194
177- processStream ( ) . catch ( err => this . onerror ?.( err ) ) ;
195+ processStream ( ) . catch ( err => {
196+ this . onerror ?.( err ) ;
197+
198+ // Try to reconnect on unexpected errors
199+ if ( this . _abortController && ! this . _abortController . signal . aborted ) {
200+ setTimeout ( ( ) => {
201+ this . _startOrAuthStandaloneSSE ( lastEventId ) . catch ( reconnectError => {
202+ this . onerror ?.( new Error ( `Failed to reconnect SSE stream: ${ reconnectError instanceof Error ? reconnectError . message : String ( reconnectError ) } ` ) ) ;
203+ } ) ;
204+ } , 1000 ) ;
205+ }
206+ } ) ;
178207 }
179208
180209 async start ( ) {
@@ -252,7 +281,7 @@ export class StreamableHTTPClientTransport implements Transport {
252281 // if the accepted notification is initialized, we start the SSE stream
253282 // if it's supported by the server
254283 if ( isJSONRPCNotification ( message ) && message . method === "notifications/initialized" ) {
255- // We don't need to handle 405 here anymore as it's handled in _startOrAuthStandaloneSSE
284+ // Start without a lastEventId since this is a fresh connection
256285 this . _startOrAuthStandaloneSSE ( ) . catch ( err => this . onerror ?.( err ) ) ;
257286 }
258287 return ;
@@ -268,6 +297,9 @@ export class StreamableHTTPClientTransport implements Transport {
268297
269298 if ( hasRequests ) {
270299 if ( contentType ?. includes ( "text/event-stream" ) ) {
300+ // Handle SSE stream responses for requests
301+ // We use the same handler as standalone streams, which now supports
302+ // reconnection with the last event ID
271303 this . _handleSseStream ( response . body ) ;
272304 } else if ( contentType ?. includes ( "application/json" ) ) {
273305 // For non-streaming servers, we might get direct JSON responses
0 commit comments