@@ -82,6 +82,11 @@ public class HttpClientSseClientTransport implements McpClientTransport {
8282 /** Default SSE endpoint path */
8383 private static final String DEFAULT_SSE_ENDPOINT = "/sse" ;
8484
85+ /**
86+ * Default maximum number of bytes read for a single inbound message.
87+ */
88+ private static final int DEFAULT_MAX_RESPONSE_SIZE = 16 * 1024 * 1024 ; // 16MiB
89+
8590 /** Base URI for the MCP server */
8691 private final URI baseUri ;
8792
@@ -122,6 +127,12 @@ public class HttpClientSseClientTransport implements McpClientTransport {
122127 */
123128 private final SseMessageEndpointValidator messageEndpointValidator ;
124129
130+ /**
131+ * Maximum number of bytes read for a single inbound message, whether it arrives on
132+ * the SSE stream or as the response to a posted message.
133+ */
134+ private final int maxResponseSize ;
135+
125136 /**
126137 * Creates a new transport instance with custom HTTP client builder, object mapper,
127138 * and headers.
@@ -133,25 +144,29 @@ public class HttpClientSseClientTransport implements McpClientTransport {
133144 * @param httpRequestCustomizer customizer for the requestBuilder before executing
134145 * requests
135146 * @param messageEndpointValidator validator for the message endpoint
147+ * @param maxResponseSize the maximum number of bytes read for a single inbound
148+ * message
136149 * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null
137150 */
138151 HttpClientSseClientTransport (HttpClient httpClient , HttpRequest .Builder requestBuilder , String baseUri ,
139152 String sseEndpoint , McpJsonMapper jsonMapper , McpAsyncHttpClientRequestCustomizer httpRequestCustomizer ,
140- SseMessageEndpointValidator messageEndpointValidator ) {
153+ SseMessageEndpointValidator messageEndpointValidator , int maxResponseSize ) {
141154 Assert .notNull (jsonMapper , "jsonMapper must not be null" );
142155 Assert .hasText (baseUri , "baseUri must not be empty" );
143156 Assert .hasText (sseEndpoint , "sseEndpoint must not be empty" );
144157 Assert .notNull (httpClient , "httpClient must not be null" );
145158 Assert .notNull (requestBuilder , "requestBuilder must not be null" );
146159 Assert .notNull (httpRequestCustomizer , "httpRequestCustomizer must not be null" );
147160 Assert .notNull (messageEndpointValidator , "messageEndpointValidator must not be null" );
161+ Assert .isTrue (maxResponseSize > 0 , "maxResponseSize must be positive" );
148162 this .baseUri = URI .create (baseUri );
149163 this .sseEndpoint = sseEndpoint ;
150164 this .jsonMapper = jsonMapper ;
151165 this .httpClient = httpClient ;
152166 this .requestBuilder = requestBuilder ;
153167 this .httpRequestCustomizer = httpRequestCustomizer ;
154168 this .messageEndpointValidator = messageEndpointValidator ;
169+ this .maxResponseSize = maxResponseSize ;
155170 }
156171
157172 @ Override
@@ -189,6 +204,8 @@ public static class Builder {
189204
190205 private SseMessageEndpointValidator messageEndpointValidator = new DefaultSseMessageEndpointValidator ();
191206
207+ private int maxResponseSize = DEFAULT_MAX_RESPONSE_SIZE ;
208+
192209 /**
193210 * Creates a new builder instance.
194211 */
@@ -344,6 +361,26 @@ public Builder messageEndpointValidator(SseMessageEndpointValidator messageEndpo
344361 return this ;
345362 }
346363
364+ /**
365+ * Sets the maximum number of bytes read for a single inbound message, whether it
366+ * arrives on the SSE stream or as the response to a posted message. A peer that
367+ * sends a larger message (or never terminates one) has its stream aborted instead
368+ * of forcing the transport to buffer it in memory. Defaults to 16MiB.
369+ *
370+ * <p>
371+ * The bound applies per message, not to the stream as a whole: a long-lived SSE
372+ * stream may deliver any number of messages, each up to this size. SSE field
373+ * framing is allowed a small amount of headroom on top of this size, so a message
374+ * of exactly this many bytes is still accepted.
375+ * @param maxResponseSize the maximum inbound message size, in bytes
376+ * @return this builder
377+ */
378+ public Builder maxResponseSize (int maxResponseSize ) {
379+ Assert .isTrue (maxResponseSize > 0 , "maxResponseSize must be positive" );
380+ this .maxResponseSize = maxResponseSize ;
381+ return this ;
382+ }
383+
347384 /**
348385 * Builds a new {@link HttpClientSseClientTransport} instance.
349386 * @return a new transport instance
@@ -352,7 +389,7 @@ public HttpClientSseClientTransport build() {
352389 HttpClient httpClient = this .clientBuilder .connectTimeout (this .connectTimeout ).build ();
353390 return new HttpClientSseClientTransport (httpClient , requestBuilder , baseUri , sseEndpoint ,
354391 jsonMapper == null ? McpJsonDefaults .getMapper () : jsonMapper , httpRequestCustomizer ,
355- messageEndpointValidator );
392+ messageEndpointValidator , maxResponseSize );
356393 }
357394
358395 }
@@ -371,13 +408,15 @@ public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> h
371408 var transportContext = ctx .getOrDefault (McpTransportContext .KEY , McpTransportContext .EMPTY );
372409 return Mono .from (this .httpRequestCustomizer .customize (builder , "GET" , uri , null , transportContext ));
373410 }).flatMap (requestBuilder -> Mono .create (sink -> {
374- Disposable connection = Flux .<ResponseEvent >create (sseSink -> this .httpClient
375- .sendAsync (requestBuilder .build (),
376- responseInfo -> ResponseSubscribers .sseToBodySubscriber (responseInfo , sseSink ))
377- .exceptionallyCompose (e -> {
378- sseSink .error (e );
379- return CompletableFuture .failedFuture (e );
380- }))
411+ Disposable connection = Flux .<ResponseEvent >create (
412+ sseSink -> this .httpClient
413+ .sendAsync (requestBuilder .build (),
414+ responseInfo -> ResponseSubscribers .sseToBodySubscriber (responseInfo , sseSink ,
415+ this .maxResponseSize ))
416+ .exceptionallyCompose (e -> {
417+ sseSink .error (e );
418+ return CompletableFuture .failedFuture (e );
419+ }))
381420 .map (responseEvent -> (ResponseSubscribers .SseResponseEvent ) responseEvent )
382421 .flatMap (responseEvent -> {
383422 if (isClosing ) {
@@ -508,7 +547,8 @@ private Mono<HttpResponse<String>> sendHttpPost(final String endpoint, final Str
508547 return Mono .from (this .httpRequestCustomizer .customize (builder , "POST" , requestUri , body , transportContext ));
509548 }).flatMap (customizedBuilder -> {
510549 var request = customizedBuilder .build ();
511- return Mono .fromFuture (httpClient .sendAsync (request , HttpResponse .BodyHandlers .ofString ()));
550+ return Mono .fromFuture (
551+ httpClient .sendAsync (request , ResponseSubscribers .boundedStringBodyHandler (this .maxResponseSize )));
512552 });
513553 }
514554
0 commit comments