@@ -49,7 +49,6 @@ export class StreamableHTTPClientTransport implements Transport {
49
49
private _requestInit ?: RequestInit ;
50
50
private _authProvider ?: OAuthClientProvider ;
51
51
private _sessionId ?: string ;
52
- private _lastEventId ?: string ;
53
52
54
53
onclose ?: ( ) => void ;
55
54
onerror ?: ( error : Error ) => void ;
@@ -102,16 +101,16 @@ export class StreamableHTTPClientTransport implements Transport {
102
101
) ;
103
102
}
104
103
105
- private async _startOrAuthStandaloneSSE ( ) : Promise < void > {
104
+ private async _startOrAuthStandaloneSSE ( lastEventId ?: string ) : Promise < void > {
106
105
try {
107
106
// Try to open an initial SSE stream with GET to listen for server messages
108
107
// This is optional according to the spec - server may not support it
109
108
const headers = await this . _commonHeaders ( ) ;
110
109
headers . set ( "Accept" , "text/event-stream" ) ;
111
110
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 ) ;
115
114
}
116
115
117
116
const response = await fetch ( this . _url , {
@@ -150,31 +149,61 @@ export class StreamableHTTPClientTransport implements Transport {
150
149
return ;
151
150
}
152
151
152
+ let lastEventId : string | undefined ;
153
+
153
154
const processStream = async ( ) => {
154
155
// Create a pipeline: binary stream -> text decoder -> SSE parser
155
156
const eventStream = stream
156
157
. pipeThrough ( new TextDecoderStream ( ) )
157
158
. pipeThrough ( new EventSourceParserStream ( ) ) ;
158
159
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
+ }
172
176
}
173
177
}
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
+ }
174
192
}
175
193
} ;
176
194
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
+ } ) ;
178
207
}
179
208
180
209
async start ( ) {
@@ -252,7 +281,7 @@ export class StreamableHTTPClientTransport implements Transport {
252
281
// if the accepted notification is initialized, we start the SSE stream
253
282
// if it's supported by the server
254
283
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
256
285
this . _startOrAuthStandaloneSSE ( ) . catch ( err => this . onerror ?.( err ) ) ;
257
286
}
258
287
return ;
@@ -268,6 +297,9 @@ export class StreamableHTTPClientTransport implements Transport {
268
297
269
298
if ( hasRequests ) {
270
299
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
271
303
this . _handleSseStream ( response . body ) ;
272
304
} else if ( contentType ?. includes ( "application/json" ) ) {
273
305
// For non-streaming servers, we might get direct JSON responses
0 commit comments