1717import java .util .Optional ;
1818import java .util .concurrent .CompletionException ;
1919import java .util .concurrent .Flow ;
20- import java .util .concurrent .atomic .AtomicBoolean ;
2120import java .util .concurrent .atomic .AtomicReference ;
2221import java .util .function .Consumer ;
2322import java .util .function .Function ;
@@ -269,6 +268,15 @@ private void handleException(Throwable t) {
269268 }
270269 }
271270
271+ private void handleExceptionSafely (Throwable t ) {
272+ try {
273+ handleException (t );
274+ }
275+ catch (Exception e ) {
276+ logger .error ("Error handling exception {}" , t .getMessage (), e );
277+ }
278+ }
279+
272280 @ Override
273281 public Mono <Void > closeGracefully () {
274282 return Mono .defer (() -> {
@@ -282,38 +290,33 @@ public Mono<Void> closeGracefully() {
282290 });
283291 }
284292
285- private Flux <McpSchema .JSONRPCMessage > consumeSseStream (Flow .Publisher <List <ByteBuffer >> body ,
286- McpTransportStream <Disposable > existingStream , Runnable onFirstMessage ) {
293+ /**
294+ * Every successfully processed event yields exactly one element, empty when it
295+ * carries no message, so that callers can tell when the first one has arrived.
296+ */
297+ private Flux <Optional <McpSchema .JSONRPCMessage >> consumeSseStream (Flow .Publisher <List <ByteBuffer >> body ,
298+ McpTransportStream <Disposable > existingStream ) {
287299 Flux <String > lines = ResponseBodyHandlers .decodeLines (body , this .maxResponseSize );
288300 return ResponseBodyHandlers .decodeSseResponse (lines , this .maxResponseSize ).flatMap (sseEvent -> {
289301 if (!isMessageEvent (sseEvent .event ())) {
290302 logger .debug ("Received SSE event with type: {}" , sseEvent );
291- if (onFirstMessage != null ) {
292- onFirstMessage .run ();
293- }
294- return Flux .empty ();
303+ return Flux .just (Optional .empty ());
295304 }
296305 String data = sseEvent .data ();
297306 if (data == null || data .isBlank ()) {
298307 logger .debug ("Skipping SSE event with empty data (stream primer)" );
299- if (onFirstMessage != null ) {
300- onFirstMessage .run ();
301- }
302- return Flux .empty ();
308+ return Flux .just (Optional .empty ());
303309 }
304310 try {
305311 McpSchema .JSONRPCMessage message = McpSchema .deserializeJsonRpcMessage (this .jsonMapper , data );
306312 Tuple2 <Optional <String >, Iterable <McpSchema .JSONRPCMessage >> idWithMessages = Tuples
307313 .of (Optional .ofNullable (sseEvent .id ()), List .of (message ));
308314 McpTransportStream <Disposable > sessionStream = existingStream != null ? existingStream
309315 : new DefaultMcpTransportStream <>(this .resumableStreams , this ::reconnect );
310- if (onFirstMessage != null ) {
311- onFirstMessage .run ();
312- }
313- return Flux .from (sessionStream .consumeSseStream (Flux .just (idWithMessages )));
316+ return Flux .from (sessionStream .consumeSseStream (Flux .just (idWithMessages ))).map (Optional ::of );
314317 }
315318 catch (IOException e ) {
316- return Flux .<McpSchema .JSONRPCMessage >error (
319+ return Flux .<Optional < McpSchema .JSONRPCMessage > >error (
317320 new McpTransportException ("Error parsing JSON-RPC message: " + data , e ));
318321 }
319322 });
@@ -395,10 +398,11 @@ private Mono<Disposable> reconnect(McpTransportStream<Disposable> stream) {
395398 "Unrecognized server error when connecting to SSE stream, status code: " + statusCode );
396399 }
397400 logger .debug ("SSE connection established successfully" );
398- return consumeSseStream (httpResponse .body (), stream , null );
401+ return consumeSseStream (httpResponse .body (), stream );
399402 });
400403 })
401- .retryWhen (authorizationErrorRetrySpec ())
404+ .retryWhen (authorizationErrorRetrySpec ()).<McpSchema
405+ .JSONRPCMessage >handle ((message , messages ) -> message .ifPresent (messages ::next ))
402406 .flatMap (jsonrpcMessage -> requestHandler .apply (Mono .just (jsonrpcMessage )))
403407 .onErrorComplete (t -> {
404408 if (t instanceof CompletionException ) {
@@ -480,15 +484,6 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
480484
481485 final AtomicReference <Disposable > disposableRef = new AtomicReference <>();
482486
483- // Once sendMessage() has completed, a later failure can no longer be reported
484- // through its sink: signalling it there would only have Reactor drop it.
485- final AtomicBoolean delivered = new AtomicBoolean ();
486- final Runnable markDelivered = () -> {
487- if (delivered .compareAndSet (false , true )) {
488- deliveredSink .success ();
489- }
490- };
491-
492487 Disposable connection = Mono .deferContextual (ctx -> {
493488 HttpRequest .Builder requestBuilder = this .requestBuilder .copy ();
494489
@@ -543,29 +538,32 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
543538
544539 if (contentType .isBlank () || "0" .equals (contentLength ) || statusCode == 202 ) {
545540 logger .debug ("No body returned for POST in session {}" , sessionRepresentation );
546- markDelivered .run ();
547- return ResponseBodyHandlers .drain (httpResponse .body (), this .maxResponseSize );
541+ return ResponseBodyHandlers .<Optional <McpSchema .JSONRPCMessage >>drain (httpResponse .body (),
542+ this .maxResponseSize )
543+ .startWith (Optional .empty ());
548544 }
549545 else if (contentType .contains (TEXT_EVENT_STREAM )) {
550- return consumeSseStream (httpResponse .body (), null , markDelivered );
546+ return consumeSseStream (httpResponse .body (), null );
551547 }
552548 else if (contentType .contains (APPLICATION_JSON )) {
553- return ResponseBodyHandlers .decodeAggregateResponse (httpResponse .body (), this .maxResponseSize )
554- .flatMapMany (data -> {
555- markDelivered .run ();
556- if (sentMessage instanceof McpSchema .JSONRPCNotification ) {
557- logger .warn ("Notification: {} received non-compliant response: {}" , sentMessage ,
558- Utils .hasText (data ) ? data : "[empty]" );
559- return Flux .empty ();
560- }
561- try {
562- return Flux .just (McpSchema .deserializeJsonRpcMessage (jsonMapper , data ));
563- }
564- catch (IOException e ) {
565- return Flux .<McpSchema .JSONRPCMessage >error (new McpTransportException (
566- "Error deserializing JSON-RPC message: " + data , e ));
567- }
568- });
549+ return ResponseBodyHandlers .decodeAggregateResponse (httpResponse .body (),
550+ this .maxResponseSize ).<Optional <McpSchema .JSONRPCMessage >>handle ((data , messages ) -> {
551+ if (sentMessage instanceof McpSchema .JSONRPCNotification ) {
552+ logger .warn ("Notification: {} received non-compliant response: {}" , sentMessage ,
553+ Utils .hasText (data ) ? data : "[empty]" );
554+ messages .next (Optional .empty ());
555+ return ;
556+ }
557+ try {
558+ messages
559+ .next (Optional .of (McpSchema .deserializeJsonRpcMessage (jsonMapper , data )));
560+ }
561+ catch (IOException e ) {
562+ messages .error (new McpTransportException (
563+ "Error deserializing JSON-RPC message: " + data , e ));
564+ }
565+ })
566+ .flux ();
569567 }
570568
571569 logger .warn ("Unknown media type {} returned for POST in session {}" , contentType ,
@@ -575,32 +573,41 @@ else if (contentType.contains(APPLICATION_JSON)) {
575573 });
576574 })
577575 .retryWhen (authorizationErrorRetrySpec ())
578- .flatMap (jsonRpcMessage -> requestHandler .apply (Mono .just (jsonRpcMessage )))
579576 .onErrorMap (CompletionException .class , t -> t .getCause ())
577+ // sendMessage() is resolved by the first signal only: any later failure
578+ // is
579+ // merely handled below, as sendMessage() has already completed by then.
580+ // An exchange ending without any event still means the server accepted
581+ // the message, so completion resolves it successfully too.
582+ .switchOnFirst ((first , messages ) -> {
583+ if (first .isOnError ()) {
584+ // Handled before failing sendMessage(), so that a session the
585+ // server does not recognise is already invalidated by the time
586+ // the caller learns about it. Consumed here so that it is not
587+ // handled a second time below.
588+ handleExceptionSafely (first .getThrowable ());
589+ deliveredSink .error (first .getThrowable ());
590+ return Flux .empty ();
591+ }
592+ deliveredSink .success ();
593+ return messages ;
594+ }).<McpSchema
595+ .JSONRPCMessage >handle ((message , messages ) -> message .ifPresent (messages ::next ))
596+ .flatMap (jsonRpcMessage -> requestHandler .apply (Mono .just (jsonRpcMessage )))
580597 .doFinally (s -> {
581598 Disposable ref = disposableRef .getAndSet (null );
582599 if (ref != null ) {
583600 transportSession .removeConnection (ref );
584601 }
585602 })
586603 .onErrorComplete (t -> {
587- // handle the error first
588- try {
589- this .handleException (t );
590- }
591- catch (Exception e ) {
592- logger .error ("Error handling exception {}" , t .getMessage (), e );
593- }
594- // inform the caller of sendMessage, unless it has already completed
595- if (delivered .compareAndSet (false , true )) {
596- deliveredSink .error (t );
597- }
604+ handleExceptionSafely (t );
598605 return true ;
599606 })
600- // An exchange can end without anything having signalled delivery, e.g.
601- // an SSE response closed before its first event. The server accepted
602- // the message all the same, so sendMessage() must not be left pending .
603- .doOnComplete ( markDelivered )
607+ // Closing the session before the first signal cancels the exchange:
608+ // complete sendMessage() instead of leaving it pending. A no-op once it
609+ // has resolved .
610+ .doOnCancel ( deliveredSink :: success )
604611 .contextWrite (deliveredSink .contextView ())
605612 .subscribe ();
606613
0 commit comments