44
55package io .modelcontextprotocol .client .transport ;
66
7- import java .io .IOException ;
8- import java .net .URI ;
9- import java .net .http .HttpClient ;
10- import java .net .http .HttpRequest ;
11- import java .net .http .HttpResponse ;
12- import java .net .http .HttpResponse .BodyHandler ;
13- import java .time .Duration ;
14- import java .util .List ;
15- import java .util .Optional ;
16- import java .util .concurrent .CompletionException ;
17- import java .util .concurrent .atomic .AtomicReference ;
18- import java .util .function .Consumer ;
19- import java .util .function .Function ;
20-
21- import org .reactivestreams .Publisher ;
22- import org .slf4j .Logger ;
23- import org .slf4j .LoggerFactory ;
24-
25- import io .modelcontextprotocol .json .TypeRef ;
26- import io .modelcontextprotocol .json .McpJsonMapper ;
27-
7+ import io .modelcontextprotocol .client .transport .ResponseSubscribers .ResponseEvent ;
288import io .modelcontextprotocol .client .transport .customizer .McpAsyncHttpClientRequestCustomizer ;
299import io .modelcontextprotocol .client .transport .customizer .McpSyncHttpClientRequestCustomizer ;
30- import io .modelcontextprotocol .client .transport .ResponseSubscribers .ResponseEvent ;
3110import io .modelcontextprotocol .common .McpTransportContext ;
11+ import io .modelcontextprotocol .json .McpJsonMapper ;
12+ import io .modelcontextprotocol .json .TypeRef ;
3213import io .modelcontextprotocol .spec .DefaultMcpTransportSession ;
3314import io .modelcontextprotocol .spec .DefaultMcpTransportStream ;
3415import io .modelcontextprotocol .spec .HttpHeaders ;
4122import io .modelcontextprotocol .spec .ProtocolVersions ;
4223import io .modelcontextprotocol .util .Assert ;
4324import io .modelcontextprotocol .util .Utils ;
25+ import org .reactivestreams .Publisher ;
26+ import org .slf4j .Logger ;
27+ import org .slf4j .LoggerFactory ;
4428import reactor .core .Disposable ;
4529import reactor .core .publisher .Flux ;
4630import reactor .core .publisher .FluxSink ;
4731import reactor .core .publisher .Mono ;
4832import reactor .util .function .Tuple2 ;
4933import reactor .util .function .Tuples ;
5034
35+ import java .io .IOException ;
36+ import java .net .URI ;
37+ import java .net .http .HttpClient ;
38+ import java .net .http .HttpRequest ;
39+ import java .net .http .HttpResponse ;
40+ import java .net .http .HttpResponse .BodyHandler ;
41+ import java .time .Duration ;
42+ import java .util .List ;
43+ import java .util .Optional ;
44+ import java .util .concurrent .CompletionException ;
45+ import java .util .concurrent .atomic .AtomicReference ;
46+ import java .util .function .Consumer ;
47+ import java .util .function .Function ;
48+
5149/**
5250 * An implementation of the Streamable HTTP protocol as defined by the
5351 * <code>2025-03-26</code> version of the MCP specification.
@@ -87,7 +85,9 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
8785 */
8886 private final HttpClient httpClient ;
8987
90- /** HTTP request builder for building requests to send messages to the server */
88+ /**
89+ * HTTP request builder for building requests to send messages to the server
90+ */
9191 private final HttpRequest .Builder requestBuilder ;
9292
9393 /**
@@ -124,9 +124,12 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
124124
125125 private final AtomicReference <Consumer <Throwable >> exceptionHandler = new AtomicReference <>();
126126
127+ private final AtomicReference <Consumer <Void >> connectionClosedHandler = new AtomicReference <>();
128+
127129 private HttpClientStreamableHttpTransport (McpJsonMapper jsonMapper , HttpClient httpClient ,
128130 HttpRequest .Builder requestBuilder , String baseUri , String endpoint , boolean resumableStreams ,
129- boolean openConnectionOnStartup , McpAsyncHttpClientRequestCustomizer httpRequestCustomizer ) {
131+ boolean openConnectionOnStartup , McpAsyncHttpClientRequestCustomizer httpRequestCustomizer ,
132+ Consumer <Void > connectionClosedHandler ) {
130133 this .jsonMapper = jsonMapper ;
131134 this .httpClient = httpClient ;
132135 this .requestBuilder = requestBuilder ;
@@ -136,6 +139,7 @@ private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient h
136139 this .openConnectionOnStartup = openConnectionOnStartup ;
137140 this .activeSession .set (createTransportSession ());
138141 this .httpRequestCustomizer = httpRequestCustomizer ;
142+ this .connectionClosedHandler .set (connectionClosedHandler );
139143 }
140144
141145 @ Override
@@ -193,6 +197,12 @@ public void setExceptionHandler(Consumer<Throwable> handler) {
193197 this .exceptionHandler .set (handler );
194198 }
195199
200+ @ Override
201+ public void setConnectionClosedHandler (Consumer <Void > closedHandler ) {
202+ logger .debug ("Connection closed handler registered" );
203+ this .connectionClosedHandler .set (closedHandler );
204+ }
205+
196206 private void handleException (Throwable t ) {
197207 logger .debug ("Handling exception for session {}" , sessionIdOrPlaceholder (this .activeSession .get ()), t );
198208 if (t instanceof McpTransportSessionNotFoundException ) {
@@ -206,6 +216,14 @@ private void handleException(Throwable t) {
206216 }
207217 }
208218
219+ private void handleConnectionClosed () {
220+ logger .debug ("Handling connection closed for session {}" , sessionIdOrPlaceholder (this .activeSession .get ()));
221+ Consumer <Void > handler = this .connectionClosedHandler .get ();
222+ if (handler != null ) {
223+ handler .accept (null );
224+ }
225+ }
226+
209227 @ Override
210228 public Mono <Void > closeGracefully () {
211229 return Mono .defer (() -> {
@@ -356,6 +374,7 @@ else if (statusCode == BAD_REQUEST) {
356374 if (ref != null ) {
357375 transportSession .removeConnection (ref );
358376 }
377+ this .handleConnectionClosed ();
359378 }))
360379 .contextWrite (ctx )
361380 .subscribe ();
@@ -615,6 +634,8 @@ public static class Builder {
615634
616635 private Duration connectTimeout = Duration .ofSeconds (10 );
617636
637+ private Consumer <Void > connectionClosedHandler = null ;
638+
618639 /**
619640 * Creates a new builder with the specified base URI.
620641 * @param baseUri the base URI of the MCP server
@@ -763,6 +784,17 @@ public Builder connectTimeout(Duration connectTimeout) {
763784 return this ;
764785 }
765786
787+ /**
788+ * Set the connection closed handler.
789+ * @param connectionClosedHandler the connection closed handler
790+ * @return this builder
791+ */
792+ public Builder connectionClosedHandler (Consumer <Void > connectionClosedHandler ) {
793+ Assert .notNull (connectionClosedHandler , "connectionClosedHandler must not be null" );
794+ this .connectionClosedHandler = connectionClosedHandler ;
795+ return this ;
796+ }
797+
766798 /**
767799 * Construct a fresh instance of {@link HttpClientStreamableHttpTransport} using
768800 * the current builder configuration.
@@ -772,7 +804,7 @@ public HttpClientStreamableHttpTransport build() {
772804 HttpClient httpClient = this .clientBuilder .connectTimeout (this .connectTimeout ).build ();
773805 return new HttpClientStreamableHttpTransport (jsonMapper == null ? McpJsonMapper .getDefault () : jsonMapper ,
774806 httpClient , requestBuilder , baseUri , endpoint , resumableStreams , openConnectionOnStartup ,
775- httpRequestCustomizer );
807+ httpRequestCustomizer , connectionClosedHandler );
776808 }
777809
778810 }
0 commit comments