@@ -44,6 +44,8 @@ public class StdioClientTransport implements McpClientTransport {
4444
4545 private static final Logger logger = LoggerFactory .getLogger (StdioClientTransport .class );
4646
47+ private static final int DEFAULT_INPUT_MAX_SIZE = 16 * 1024 * 1024 ; // 16MB
48+
4749 // @formatter:off
4850 private static final Set <Integer > EXIT_SUCCESS_CODES = Set .of (
4951 0 , // success
@@ -76,6 +78,8 @@ public class StdioClientTransport implements McpClientTransport {
7678
7779 private final Sinks .Many <String > errorSink ;
7880
81+ private final int inputMaxSize ;
82+
7983 private volatile boolean isClosing = false ;
8084
8185 // visible for tests
@@ -87,8 +91,21 @@ public class StdioClientTransport implements McpClientTransport {
8791 * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization
8892 */
8993 public StdioClientTransport (ServerParameters params , McpJsonMapper jsonMapper ) {
94+ this (params , jsonMapper , DEFAULT_INPUT_MAX_SIZE );
95+ }
96+
97+ /**
98+ * Creates a new StdioClientTransport with the specified parameters and JsonMapper.
99+ * @param params The parameters for configuring the server process
100+ * @param jsonMapper The JsonMapper to use for JSON serialization/deserialization
101+ * @param inputMaxSize The maximum number of characters read for a single inbound
102+ * message. A peer that sends a longer message (or never terminates a line) has its
103+ * message rejected instead of forcing the transport to buffer it in memory.
104+ */
105+ public StdioClientTransport (ServerParameters params , McpJsonMapper jsonMapper , int inputMaxSize ) {
90106 Assert .notNull (params , "The params can not be null" );
91107 Assert .notNull (jsonMapper , "The JsonMapper can not be null" );
108+ Assert .isTrue (inputMaxSize > 0 , "inputMaxSize must be positive" );
92109
93110 this .inboundSink = Sinks .many ().unicast ().onBackpressureBuffer ();
94111 this .outboundSink = Sinks .many ().unicast ().onBackpressureBuffer ();
@@ -97,6 +114,8 @@ public StdioClientTransport(ServerParameters params, McpJsonMapper jsonMapper) {
97114
98115 this .jsonMapper = jsonMapper ;
99116
117+ this .inputMaxSize = inputMaxSize ;
118+
100119 this .errorSink = Sinks .many ().unicast ().onBackpressureBuffer ();
101120
102121 // Start threads
@@ -260,7 +279,7 @@ private void startInboundProcessing() {
260279 this .inboundScheduler .schedule (() -> {
261280 try (BufferedReader processReader = new BufferedReader (new InputStreamReader (process .getInputStream ()))) {
262281 String line ;
263- while (!isClosing && (line = processReader . readLine ()) != null ) {
282+ while (!isClosing && (line = readLine (processReader , inputMaxSize )) != null ) {
264283 try {
265284 JSONRPCMessage message = McpSchema .deserializeJsonRpcMessage (this .jsonMapper , line );
266285 if (!this .inboundSink .tryEmitNext (message ).isSuccess ()) {
@@ -278,6 +297,11 @@ private void startInboundProcessing() {
278297 }
279298 }
280299 }
300+ catch (MaxSizeExceededException e ) {
301+ if (!isClosing ) {
302+ logger .error ("Inbound message exceeds the maximum allowed size" , e );
303+ }
304+ }
281305 catch (IOException e ) {
282306 if (!isClosing ) {
283307 logger .error ("Error reading from input stream" , e );
@@ -290,6 +314,37 @@ private void startInboundProcessing() {
290314 });
291315 }
292316
317+ /**
318+ * Reads a single line, mirroring {@link BufferedReader#readLine()}, but aborting once
319+ * more than {@code maxSize} characters have been read without encountering a line
320+ * terminator. This bounds how much memory a single inbound message can occupy.
321+ */
322+ static String readLine (BufferedReader reader , int maxSize ) throws IOException , MaxSizeExceededException {
323+ StringBuilder sb = new StringBuilder ();
324+ int c ;
325+ while ((c = reader .read ()) != -1 ) {
326+ if (c == '\n' ) {
327+ return sb .toString ();
328+ }
329+ if (c == '\r' ) {
330+ // Consume an optional trailing '\n' so that "\r\n" is treated as a
331+ // single terminator, mirroring BufferedReader#readLine().
332+ reader .mark (1 );
333+ int next = reader .read ();
334+ if (next != '\n' && next != -1 ) {
335+ reader .reset ();
336+ }
337+ return sb .toString ();
338+ }
339+ if (sb .length () >= maxSize ) {
340+ throw new MaxSizeExceededException (
341+ "Inbound message exceeds the maximum allowed size of " + maxSize + " characters" );
342+ }
343+ sb .append ((char ) c );
344+ }
345+ return sb .isEmpty () ? null : sb .toString ();
346+ }
347+
293348 /**
294349 * Starts the outbound processing thread that writes JSON-RPC messages to the
295350 * process's output stream. Messages are serialized to JSON and written with a newline
0 commit comments