Skip to content

Commit 2e5dbd3

Browse files
committed
Bound HTTP client reads
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent 8c0fdd1 commit 2e5dbd3

9 files changed

Lines changed: 1302 additions & 138 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)