@@ -88,6 +88,11 @@ public class HttpClientSseClientTransport implements McpClientTransport {
8888 /** Default SSE endpoint path */
8989 private static final String DEFAULT_SSE_ENDPOINT = "/sse" ;
9090
91+ /**
92+ * Default maximum number of bytes read for a single inbound message.
93+ */
94+ private static final int DEFAULT_MAX_RESPONSE_SIZE = 16 * 1024 * 1024 ; // 16MiB
95+
9196 /** Base URI for the MCP server */
9297 private final URI baseUri ;
9398
@@ -128,6 +133,12 @@ public class HttpClientSseClientTransport implements McpClientTransport {
128133 */
129134 private final SseMessageEndpointValidator messageEndpointValidator ;
130135
136+ /**
137+ * Maximum number of bytes read for a single inbound message, whether it arrives on
138+ * the SSE stream or as the response to a posted message.
139+ */
140+ private final int maxResponseSize ;
141+
131142 /**
132143 * Creates a new transport instance with custom HTTP client builder, object mapper,
133144 * and headers.
@@ -139,25 +150,29 @@ public class HttpClientSseClientTransport implements McpClientTransport {
139150 * @param httpRequestCustomizer customizer for the requestBuilder before executing
140151 * requests
141152 * @param messageEndpointValidator validator for the message endpoint
153+ * @param maxResponseSize the maximum number of bytes read for a single inbound
154+ * message
142155 * @throws IllegalArgumentException if objectMapper, clientBuilder, or headers is null
143156 */
144157 HttpClientSseClientTransport (HttpClient httpClient , HttpRequest .Builder requestBuilder , String baseUri ,
145158 String sseEndpoint , McpJsonMapper jsonMapper , McpAsyncHttpClientRequestCustomizer httpRequestCustomizer ,
146- SseMessageEndpointValidator messageEndpointValidator ) {
159+ SseMessageEndpointValidator messageEndpointValidator , int maxResponseSize ) {
147160 Assert .notNull (jsonMapper , "jsonMapper must not be null" );
148161 Assert .hasText (baseUri , "baseUri must not be empty" );
149162 Assert .hasText (sseEndpoint , "sseEndpoint must not be empty" );
150163 Assert .notNull (httpClient , "httpClient must not be null" );
151164 Assert .notNull (requestBuilder , "requestBuilder must not be null" );
152165 Assert .notNull (httpRequestCustomizer , "httpRequestCustomizer must not be null" );
153166 Assert .notNull (messageEndpointValidator , "messageEndpointValidator must not be null" );
167+ Assert .isTrue (maxResponseSize > 0 , "maxResponseSize must be positive" );
154168 this .baseUri = URI .create (baseUri );
155169 this .sseEndpoint = sseEndpoint ;
156170 this .jsonMapper = jsonMapper ;
157171 this .httpClient = httpClient ;
158172 this .requestBuilder = requestBuilder ;
159173 this .httpRequestCustomizer = httpRequestCustomizer ;
160174 this .messageEndpointValidator = messageEndpointValidator ;
175+ this .maxResponseSize = maxResponseSize ;
161176 }
162177
163178 @ Override
@@ -195,6 +210,8 @@ public static class Builder {
195210
196211 private SseMessageEndpointValidator messageEndpointValidator = new DefaultSseMessageEndpointValidator ();
197212
213+ private int maxResponseSize = DEFAULT_MAX_RESPONSE_SIZE ;
214+
198215 /**
199216 * Creates a new builder instance.
200217 */
@@ -326,6 +343,26 @@ public Builder messageEndpointValidator(SseMessageEndpointValidator messageEndpo
326343 return this ;
327344 }
328345
346+ /**
347+ * Sets the maximum number of bytes read for a single inbound message, whether it
348+ * arrives on the SSE stream or as the response to a posted message. A peer that
349+ * sends a larger message (or never terminates one) has its stream aborted instead
350+ * of forcing the transport to buffer it in memory. Defaults to 16MiB.
351+ *
352+ * <p>
353+ * The bound applies per message, not to the stream as a whole: a long-lived SSE
354+ * stream may deliver any number of messages, each up to this size. SSE field
355+ * framing is allowed a small amount of headroom on top of this size, so a message
356+ * of exactly this many bytes is still accepted.
357+ * @param maxResponseSize the maximum inbound message size, in bytes
358+ * @return this builder
359+ */
360+ public Builder maxResponseSize (int maxResponseSize ) {
361+ Assert .isTrue (maxResponseSize > 0 , "maxResponseSize must be positive" );
362+ this .maxResponseSize = maxResponseSize ;
363+ return this ;
364+ }
365+
329366 /**
330367 * Builds a new {@link HttpClientSseClientTransport} instance.
331368 * @return a new transport instance
@@ -334,7 +371,7 @@ public HttpClientSseClientTransport build() {
334371 HttpClient httpClient = this .clientBuilder .connectTimeout (this .connectTimeout ).build ();
335372 return new HttpClientSseClientTransport (httpClient , requestBuilder , baseUri , sseEndpoint ,
336373 jsonMapper == null ? McpJsonDefaults .getMapper () : jsonMapper , httpRequestCustomizer ,
337- messageEndpointValidator );
374+ messageEndpointValidator , maxResponseSize );
338375 }
339376
340377 }
@@ -353,13 +390,15 @@ public Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> h
353390 var transportContext = ctx .getOrDefault (McpTransportContext .KEY , McpTransportContext .EMPTY );
354391 return Mono .from (this .httpRequestCustomizer .customize (builder , "GET" , uri , null , transportContext ));
355392 }).flatMap (requestBuilder -> Mono .create (sink -> {
356- Disposable connection = Flux .<ResponseEvent >create (sseSink -> this .httpClient
357- .sendAsync (requestBuilder .build (),
358- responseInfo -> ResponseSubscribers .sseToBodySubscriber (responseInfo , sseSink ))
359- .exceptionallyCompose (e -> {
360- sseSink .error (e );
361- return CompletableFuture .failedFuture (e );
362- }))
393+ Disposable connection = Flux .<ResponseEvent >create (
394+ sseSink -> this .httpClient
395+ .sendAsync (requestBuilder .build (),
396+ responseInfo -> ResponseSubscribers .sseToBodySubscriber (responseInfo , sseSink ,
397+ this .maxResponseSize ))
398+ .exceptionallyCompose (e -> {
399+ sseSink .error (e );
400+ return CompletableFuture .failedFuture (e );
401+ }))
363402 .map (responseEvent -> (ResponseSubscribers .SseResponseEvent ) responseEvent )
364403 .flatMap (responseEvent -> {
365404 if (isClosing ) {
@@ -490,7 +529,8 @@ private Mono<HttpResponse<String>> sendHttpPost(final String endpoint, final Str
490529 return Mono .from (this .httpRequestCustomizer .customize (builder , "POST" , requestUri , body , transportContext ));
491530 }).flatMap (customizedBuilder -> {
492531 var request = customizedBuilder .build ();
493- return Mono .fromFuture (httpClient .sendAsync (request , HttpResponse .BodyHandlers .ofString ()));
532+ return Mono .fromFuture (
533+ httpClient .sendAsync (request , ResponseSubscribers .boundedStringBodyHandler (this .maxResponseSize )));
494534 });
495535 }
496536
0 commit comments