Skip to content

Commit 6a41ded

Browse files
committed
Bound HTTP server reads
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent 1fcb404 commit 6a41ded

11 files changed

Lines changed: 619 additions & 38 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,10 @@ else if (statusCode == BAD_REQUEST) {
681681
return Flux.<McpSchema.JSONRPCMessage>error(new McpTransportException(
682682
"Bad Request. Status code:" + statusCode + ", response-event:" + responseEvent));
683683
}
684+
else if (statusCode >= 400 && statusCode < 500) {
685+
return Flux.<McpSchema.JSONRPCMessage>error(
686+
new McpTransportException("Invalid request. Status code: " + statusCode));
687+
}
684688

685689
return Flux.<McpSchema.JSONRPCMessage>error(
686690
new RuntimeException("Failed to send message: " + responseEvent));

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletRequestUtils.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
package io.modelcontextprotocol.server.transport;
66

7+
import java.io.ByteArrayOutputStream;
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.nio.charset.StandardCharsets;
711
import java.util.Collections;
812
import java.util.Enumeration;
913
import java.util.HashMap;
@@ -37,4 +41,32 @@ static Map<String, List<String>> extractHeaders(HttpServletRequest request) {
3741
return headers;
3842
}
3943

44+
/**
45+
* Reads the request body, decoded using the request's character encoding (or UTF-8 if
46+
* not specified), while bounding the number of bytes read.
47+
* @param request The HTTP servlet request
48+
* @param maxSize The maximum number of bytes to read from the request body
49+
* @return The decoded request body
50+
* @throws MaxSizeExceededException If the body exceeds {@code maxSize}
51+
* @throws IOException If an I/O error occurs while reading the request body
52+
*/
53+
static String readBody(HttpServletRequest request, int maxSize) throws MaxSizeExceededException, IOException {
54+
InputStream inputStream = request.getInputStream();
55+
ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream();
56+
byte[] buf = new byte[8192];
57+
int totalBytes = 0;
58+
int readBytes;
59+
while ((readBytes = inputStream.read(buf, 0, buf.length)) != -1) {
60+
totalBytes += readBytes;
61+
if (totalBytes > maxSize) {
62+
throw new MaxSizeExceededException(
63+
"Request body exceeds the maximum allowed size of " + maxSize + " bytes");
64+
}
65+
bodyBytes.write(buf, 0, readBytes);
66+
}
67+
String charset = request.getCharacterEncoding() != null ? request.getCharacterEncoding()
68+
: StandardCharsets.UTF_8.name();
69+
return bodyBytes.toString(charset);
70+
}
71+
4072
}

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package io.modelcontextprotocol.server.transport;
66

7-
import java.io.BufferedReader;
87
import java.io.IOException;
98
import java.io.PrintWriter;
109
import java.time.Duration;
@@ -76,6 +75,11 @@
7675
@WebServlet(asyncSupported = true)
7776
public class HttpServletSseServerTransportProvider extends HttpServlet implements McpServerTransportProvider {
7877

78+
/**
79+
* Default maximum size of a single request body: 16 MiB (16 * 1024 * 1024 bytes).
80+
*/
81+
private static final int DEFAULT_REQUEST_MAX_SIZE = 16 * 1024 * 1024;
82+
7983
/**
8084
* Logger for this class
8185
*/
@@ -111,6 +115,11 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
111115
*/
112116
private final McpJsonMapper jsonMapper;
113117

118+
/**
119+
* Maximum size, in bytes, of a single request body accepted by this transport.
120+
*/
121+
private final int requestMaxSize;
122+
114123
/**
115124
* Base URL for the server transport
116125
*/
@@ -166,24 +175,28 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
166175
* keep-alive functionality
167176
* @param contextExtractor The extractor for transport context from the request.
168177
* @param securityValidator The security validator for validating HTTP requests.
178+
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
179+
* positive.
169180
*/
170181
private HttpServletSseServerTransportProvider(McpJsonMapper jsonMapper, String baseUrl, String messageEndpoint,
171182
String sseEndpoint, Duration keepAliveInterval,
172183
McpTransportContextExtractor<HttpServletRequest> contextExtractor,
173-
ServerTransportSecurityValidator securityValidator) {
184+
ServerTransportSecurityValidator securityValidator, int requestMaxSize) {
174185

175186
Assert.notNull(jsonMapper, "JsonMapper must not be null");
176187
Assert.notNull(messageEndpoint, "messageEndpoint must not be null");
177188
Assert.notNull(sseEndpoint, "sseEndpoint must not be null");
178189
Assert.notNull(contextExtractor, "Context extractor must not be null");
179190
Assert.notNull(securityValidator, "Security validator must not be null");
191+
Assert.isTrue(requestMaxSize > 0, "requestMaxSize must be positive");
180192

181193
this.jsonMapper = jsonMapper;
182194
this.baseUrl = baseUrl;
183195
this.messageEndpoint = messageEndpoint;
184196
this.sseEndpoint = sseEndpoint;
185197
this.contextExtractor = contextExtractor;
186198
this.securityValidator = securityValidator;
199+
this.requestMaxSize = requestMaxSize;
187200

188201
if (keepAliveInterval != null) {
189202

@@ -346,6 +359,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
346359
return;
347360
}
348361

362+
if (request.getContentLengthLong() > this.requestMaxSize) {
363+
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
364+
return;
365+
}
366+
349367
String requestURI = request.getRequestURI();
350368
if (!requestURI.endsWith(messageEndpoint)) {
351369
response.sendError(HttpServletResponse.SC_NOT_FOUND);
@@ -392,22 +410,20 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
392410
}
393411

394412
try {
395-
BufferedReader reader = request.getReader();
396-
StringBuilder body = new StringBuilder();
397-
String line;
398-
while ((line = reader.readLine()) != null) {
399-
body.append(line);
400-
}
413+
String body = HttpServletRequestUtils.readBody(request, this.requestMaxSize);
401414

402415
final McpTransportContext transportContext = this.contextExtractor.extract(request);
403-
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString());
416+
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);
404417

405418
// Process the message through the session's handle method
406419
// Block for Servlet compatibility
407420
session.handle(message).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext)).block();
408421

409422
response.setStatus(HttpServletResponse.SC_OK);
410423
}
424+
catch (MaxSizeExceededException e) {
425+
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
426+
}
411427
catch (Exception e) {
412428
logger.error("Error processing message: {}", e.getMessage());
413429
try {
@@ -605,6 +621,8 @@ public static class Builder {
605621

606622
private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
607623

624+
private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;
625+
608626
/**
609627
* Sets the JsonMapper implementation to use for serialization/deserialization. If
610628
* not specified, a JacksonJsonMapper will be created from the configured
@@ -691,6 +709,19 @@ public Builder securityValidator(ServerTransportSecurityValidator securityValida
691709
return this;
692710
}
693711

712+
/**
713+
* Sets the maximum size, in bytes, of a single request body accepted by this
714+
* transport. Requests whose body exceeds this size are rejected with a 413
715+
* (Payload Too Large) response. Defaults to 16 MiB if not set.
716+
* @param requestMaxSize The maximum request body size, in bytes. Must be
717+
* positive.
718+
* @return This builder instance
719+
*/
720+
public Builder maxRequestSize(int requestMaxSize) {
721+
this.requestMaxSize = requestMaxSize;
722+
return this;
723+
}
724+
694725
/**
695726
* Builds a new instance of HttpServletSseServerTransportProvider with the
696727
* configured settings.
@@ -703,7 +734,7 @@ public HttpServletSseServerTransportProvider build() {
703734
}
704735
return new HttpServletSseServerTransportProvider(
705736
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, baseUrl, messageEndpoint,
706-
sseEndpoint, keepAliveInterval, contextExtractor, securityValidator);
737+
sseEndpoint, keepAliveInterval, contextExtractor, securityValidator, requestMaxSize);
707738
}
708739

709740
}

mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
package io.modelcontextprotocol.server.transport;
66

7-
import java.io.BufferedReader;
87
import java.io.IOException;
98
import java.io.PrintWriter;
109
import java.util.List;
@@ -39,6 +38,11 @@
3938
@WebServlet(asyncSupported = true)
4039
public class HttpServletStatelessServerTransport extends HttpServlet implements McpStatelessServerTransport {
4140

41+
/**
42+
* Default maximum size of a single request body: 16 MiB (16 * 1024 * 1024 bytes).
43+
*/
44+
private static final int DEFAULT_REQUEST_MAX_SIZE = 16 * 1024 * 1024;
45+
4246
private static final Logger logger = LoggerFactory.getLogger(HttpServletStatelessServerTransport.class);
4347

4448
public static final String UTF_8 = "UTF-8";
@@ -66,18 +70,37 @@ public class HttpServletStatelessServerTransport extends HttpServlet implements
6670
*/
6771
private final ServerTransportSecurityValidator securityValidator;
6872

73+
/**
74+
* Maximum size, in bytes, of a single request body accepted by this transport.
75+
*/
76+
private final int requestMaxSize;
77+
78+
/**
79+
* Constructs a new HttpServletStatelessServerTransport instance.
80+
* @param jsonMapper The JsonMapper to use for JSON serialization/deserialization of
81+
* messages.
82+
* @param mcpEndpoint The endpoint URI where clients should send their JSON-RPC
83+
* messages.
84+
* @param contextExtractor The extractor for transport context from the request.
85+
* @param securityValidator The security validator for validating HTTP requests.
86+
* @param requestMaxSize The maximum size, in bytes, of a single request body. Must be
87+
* positive.
88+
* @throws IllegalArgumentException if any parameter is null
89+
*/
6990
private HttpServletStatelessServerTransport(McpJsonMapper jsonMapper, String mcpEndpoint,
7091
McpTransportContextExtractor<HttpServletRequest> contextExtractor,
71-
ServerTransportSecurityValidator securityValidator) {
92+
ServerTransportSecurityValidator securityValidator, int requestMaxSize) {
7293
Assert.notNull(jsonMapper, "jsonMapper must not be null");
7394
Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null");
7495
Assert.notNull(contextExtractor, "contextExtractor must not be null");
7596
Assert.notNull(securityValidator, "Security validator must not be null");
97+
Assert.isTrue(requestMaxSize > 0, "requestMaxSize must be positive");
7698

7799
this.jsonMapper = jsonMapper;
78100
this.mcpEndpoint = mcpEndpoint;
79101
this.contextExtractor = contextExtractor;
80102
this.securityValidator = securityValidator;
103+
this.requestMaxSize = requestMaxSize;
81104
}
82105

83106
@Override
@@ -133,6 +156,11 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
133156
return;
134157
}
135158

159+
if (request.getContentLengthLong() > this.requestMaxSize) {
160+
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
161+
return;
162+
}
163+
136164
try {
137165
Map<String, List<String>> headers = HttpServletRequestUtils.extractHeaders(request);
138166
this.securityValidator.validateHeaders(headers);
@@ -154,14 +182,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
154182
}
155183

156184
try {
157-
BufferedReader reader = request.getReader();
158-
StringBuilder body = new StringBuilder();
159-
String line;
160-
while ((line = reader.readLine()) != null) {
161-
body.append(line);
162-
}
185+
String body = HttpServletRequestUtils.readBody(request, this.requestMaxSize);
163186

164-
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body.toString());
187+
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);
165188

166189
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
167190
try {
@@ -209,6 +232,9 @@ else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
209232
.build());
210233
}
211234
}
235+
catch (MaxSizeExceededException e) {
236+
response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
237+
}
212238
catch (IllegalArgumentException | IOException e) {
213239
logger.error("Failed to deserialize message: {}", e.getMessage());
214240
this.responseError(response, HttpServletResponse.SC_BAD_REQUEST,
@@ -276,6 +302,8 @@ public static class Builder {
276302

277303
private ServerTransportSecurityValidator securityValidator = ServerTransportSecurityValidator.NOOP;
278304

305+
private int requestMaxSize = DEFAULT_REQUEST_MAX_SIZE;
306+
279307
private Builder() {
280308
// used by a static method
281309
}
@@ -333,6 +361,19 @@ public Builder securityValidator(ServerTransportSecurityValidator securityValida
333361
return this;
334362
}
335363

364+
/**
365+
* Sets the maximum size, in bytes, of a single request body accepted by this
366+
* transport. Requests whose body exceeds this size are rejected with a 413
367+
* (Payload Too Large) response. Defaults to 16 MiB if not set.
368+
* @param requestMaxSize The maximum request body size, in bytes. Must be
369+
* positive.
370+
* @return this builder instance
371+
*/
372+
public Builder maxRequestSize(int requestMaxSize) {
373+
this.requestMaxSize = requestMaxSize;
374+
return this;
375+
}
376+
336377
/**
337378
* Builds a new instance of {@link HttpServletStatelessServerTransport} with the
338379
* configured settings.
@@ -343,7 +384,7 @@ public HttpServletStatelessServerTransport build() {
343384
Assert.notNull(mcpEndpoint, "Message endpoint must be set");
344385
return new HttpServletStatelessServerTransport(
345386
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, mcpEndpoint, contextExtractor,
346-
securityValidator);
387+
securityValidator, requestMaxSize);
347388
}
348389

349390
}

0 commit comments

Comments
 (0)