@@ -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 */
@@ -338,6 +355,26 @@ public Builder messageEndpointValidator(SseMessageEndpointValidator messageEndpo
338355 return this ;
339356 }
340357
358+ /**
359+ * Sets the maximum number of bytes read for a single inbound message, whether it
360+ * arrives on the SSE stream or as the response to a posted message. A peer that
361+ * sends a larger message (or never terminates one) has its stream aborted instead
362+ * of forcing the transport to buffer it in memory. Defaults to 16MiB.
363+ *
364+ * <p>
365+ * The bound applies per message, not to the stream as a whole: a long-lived SSE
366+ * stream may deliver any number of messages, each up to this size. SSE field
367+ * framing is allowed a small amount of headroom on top of this size, so a message
368+ * of exactly this many bytes is still accepted.
369+ * @param maxResponseSize the maximum inbound message size, in bytes
370+ * @return this builder
371+ */
372+ public Builder maxResponseSize (int maxResponseSize ) {
373+ Assert .isTrue (maxResponseSize > 0 , "maxResponseSize must be positive" );
374+ this .maxResponseSize = maxResponseSize ;
375+ return this ;
376+ }
377+
341378 /**
342379 * Builds a new {@link HttpClientSseClientTransport} instance.
343380 * @return a new transport instance
@@ -346,7 +383,7 @@ public HttpClientSseClientTransport build() {
346383 HttpClient httpClient = this .clientBuilder .connectTimeout (this .connectTimeout ).build ();
347384 return new HttpClientSseClientTransport (httpClient , requestBuilder , baseUri , sseEndpoint ,
348385 jsonMapper == null ? McpJsonDefaults .getMapper () : jsonMapper , httpRequestCustomizer ,
349- messageEndpointValidator );
386+ messageEndpointValidator , maxResponseSize );
350387 }
351388
352389 }
@@ -365,13 +402,15 @@ public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> h
365402 var transportContext = ctx .getOrDefault (McpTransportContext .KEY , McpTransportContext .EMPTY );
366403 return Mono .from (this .httpRequestCustomizer .customize (builder , "GET" , uri , null , transportContext ));
367404 }).flatMap (requestBuilder -> Mono .create (sink -> {
368- Disposable connection = Flux .<ResponseEvent >create (sseSink -> this .httpClient
369- .sendAsync (requestBuilder .build (),
370- responseInfo -> ResponseSubscribers .sseToBodySubscriber (responseInfo , sseSink ))
371- .exceptionallyCompose (e -> {
372- sseSink .error (e );
373- return CompletableFuture .failedFuture (e );
374- }))
405+ Disposable connection = Flux .<ResponseEvent >create (
406+ sseSink -> this .httpClient
407+ .sendAsync (requestBuilder .build (),
408+ responseInfo -> ResponseSubscribers .sseToBodySubscriber (responseInfo , sseSink ,
409+ this .maxResponseSize ))
410+ .exceptionallyCompose (e -> {
411+ sseSink .error (e );
412+ return CompletableFuture .failedFuture (e );
413+ }))
375414 .map (responseEvent -> (ResponseSubscribers .SseResponseEvent ) responseEvent )
376415 .flatMap (responseEvent -> {
377416 if (isClosing ) {
@@ -502,7 +541,8 @@ private Mono<HttpResponse<String>> sendHttpPost(final String endpoint, final Str
502541 return Mono .from (this .httpRequestCustomizer .customize (builder , "POST" , requestUri , body , transportContext ));
503542 }).flatMap (customizedBuilder -> {
504543 var request = customizedBuilder .build ();
505- return Mono .fromFuture (httpClient .sendAsync (request , HttpResponse .BodyHandlers .ofString ()));
544+ return Mono .fromFuture (
545+ httpClient .sendAsync (request , ResponseSubscribers .boundedStringBodyHandler (this .maxResponseSize )));
506546 });
507547 }
508548
0 commit comments