1
1
import { Transport } from "../shared/transport.js" ;
2
- import { isJSONRPCNotification , JSONRPCMessage , JSONRPCMessageSchema } from "../types.js" ;
2
+ import { isJSONRPCNotification , isJSONRPCRequest , isJSONRPCResponse , JSONRPCMessage , JSONRPCMessageSchema } from "../types.js" ;
3
3
import { auth , AuthResult , OAuthClientProvider , UnauthorizedError } from "./auth.js" ;
4
4
import { EventSourceParserStream } from "eventsource-parser/stream" ;
5
5
@@ -28,6 +28,14 @@ export interface StartSSEOptions {
28
28
* The ID of the last received event, used for resuming a disconnected stream
29
29
*/
30
30
lastEventId ?: string ;
31
+ /**
32
+ * The callback function that is invoked when the last event ID changes
33
+ */
34
+ onLastEventIdUpdate ?: ( event : string ) => void
35
+ /**
36
+ * When reconnecting to a long-running SSE stream, we need to make sure that message id matches
37
+ */
38
+ replayMessageId ?: string | number ;
31
39
}
32
40
33
41
/**
@@ -200,7 +208,7 @@ export class StreamableHTTPClientTransport implements Transport {
200
208
) ;
201
209
}
202
210
203
- this . _handleSseStream ( response . body ) ;
211
+ this . _handleSseStream ( response . body , options ) ;
204
212
} catch ( error ) {
205
213
this . onerror ?.( error as Error ) ;
206
214
throw error ;
@@ -231,7 +239,7 @@ export class StreamableHTTPClientTransport implements Transport {
231
239
* @param lastEventId The ID of the last received event for resumability
232
240
* @param attemptCount Current reconnection attempt count for this specific stream
233
241
*/
234
- private _scheduleReconnection ( lastEventId : string , attemptCount = 0 ) : void {
242
+ private _scheduleReconnection ( options : StartSSEOptions , attemptCount = 0 ) : void {
235
243
// Use provided options or default options
236
244
const maxRetries = this . _reconnectionOptions . maxRetries ;
237
245
@@ -247,18 +255,19 @@ export class StreamableHTTPClientTransport implements Transport {
247
255
// Schedule the reconnection
248
256
setTimeout ( ( ) => {
249
257
// Use the last event ID to resume where we left off
250
- this . _startOrAuthSse ( { lastEventId } ) . catch ( error => {
258
+ this . _startOrAuthSse ( options ) . catch ( error => {
251
259
this . onerror ?.( new Error ( `Failed to reconnect SSE stream: ${ error instanceof Error ? error . message : String ( error ) } ` ) ) ;
252
260
// Schedule another attempt if this one failed, incrementing the attempt counter
253
- this . _scheduleReconnection ( lastEventId , attemptCount + 1 ) ;
261
+ this . _scheduleReconnection ( options , attemptCount + 1 ) ;
254
262
} ) ;
255
263
} , delay ) ;
256
264
}
257
265
258
- private _handleSseStream ( stream : ReadableStream < Uint8Array > | null , onLastEventIdUpdate ?: ( event : string ) => void ) : void {
266
+ private _handleSseStream ( stream : ReadableStream < Uint8Array > | null , options : StartSSEOptions ) : void {
259
267
if ( ! stream ) {
260
268
return ;
261
269
}
270
+ const { onLastEventIdUpdate, replayMessageId } = options ;
262
271
263
272
let lastEventId : string | undefined ;
264
273
const processStream = async ( ) => {
@@ -287,6 +296,9 @@ export class StreamableHTTPClientTransport implements Transport {
287
296
if ( ! event . event || event . event === "message" ) {
288
297
try {
289
298
const message = JSONRPCMessageSchema . parse ( JSON . parse ( event . data ) ) ;
299
+ if ( replayMessageId !== undefined && isJSONRPCResponse ( message ) ) {
300
+ message . id = replayMessageId ;
301
+ }
290
302
this . onmessage ?.( message ) ;
291
303
} catch ( error ) {
292
304
this . onerror ?.( error as Error ) ;
@@ -302,7 +314,7 @@ export class StreamableHTTPClientTransport implements Transport {
302
314
// Use the exponential backoff reconnection strategy
303
315
if ( lastEventId !== undefined ) {
304
316
try {
305
- this . _scheduleReconnection ( lastEventId , 0 ) ;
317
+ this . _scheduleReconnection ( options , 0 ) ;
306
318
}
307
319
catch ( error ) {
308
320
this . onerror ?.( new Error ( `Failed to reconnect: ${ error instanceof Error ? error . message : String ( error ) } ` ) ) ;
@@ -352,8 +364,9 @@ export class StreamableHTTPClientTransport implements Transport {
352
364
const lastEventId = options ?. resumptionToken
353
365
const onLastEventIdUpdate = options ?. onresumptiontoken ;
354
366
if ( lastEventId ) {
367
+
355
368
// If we have at last event ID, we need to reconnect the SSE stream
356
- this . _startOrAuthSse ( { lastEventId } ) . catch ( err => this . onerror ?.( err ) ) ;
369
+ this . _startOrAuthSse ( { lastEventId, replayMessageId : isJSONRPCRequest ( message ) ? message . id : undefined } ) . catch ( err => this . onerror ?.( err ) ) ;
357
370
return ;
358
371
}
359
372
@@ -418,7 +431,7 @@ export class StreamableHTTPClientTransport implements Transport {
418
431
// Handle SSE stream responses for requests
419
432
// We use the same handler as standalone streams, which now supports
420
433
// reconnection with the last event ID
421
- this . _handleSseStream ( response . body , onLastEventIdUpdate ) ;
434
+ this . _handleSseStream ( response . body , { onLastEventIdUpdate } ) ;
422
435
} else if ( contentType ?. includes ( "application/json" ) ) {
423
436
// For non-streaming servers, we might get direct JSON responses
424
437
const data = await response . json ( ) ;
0 commit comments