From 22b900918e8ea3ef1217c9795fc15dee073c8259 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Thu, 11 Dec 2025 11:13:26 +0800 Subject: [PATCH 1/3] refactor(transport): make fields final and specify charset in stream readers Make scheduler fields final in client and server transports for better immutability. Explicitly specify UTF-8 charset when creating InputStreamReader instances to ensure consistent character encoding. --- .../client/transport/StdioClientTransport.java | 13 +++++++------ .../transport/StdioServerTransportProvider.java | 15 ++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java index 1b4eaca97..163a8b46a 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/client/transport/StdioClientTransport.java @@ -48,16 +48,16 @@ public class StdioClientTransport implements McpClientTransport { /** The server process being communicated with */ private Process process; - private McpJsonMapper jsonMapper; + private final McpJsonMapper jsonMapper; /** Scheduler for handling inbound messages from the server process */ - private Scheduler inboundScheduler; + private final Scheduler inboundScheduler; /** Scheduler for handling outbound messages to the server process */ - private Scheduler outboundScheduler; + private final Scheduler outboundScheduler; /** Scheduler for handling error messages from the server process */ - private Scheduler errorScheduler; + private final Scheduler errorScheduler; /** Parameters for configuring and starting the server process */ private final ServerParameters params; @@ -180,7 +180,7 @@ public void awaitForExit() { private void startErrorProcessing() { this.errorScheduler.schedule(() -> { try (BufferedReader processErrorReader = new BufferedReader( - new InputStreamReader(process.getErrorStream()))) { + new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { String line; while (!isClosing && (line = processErrorReader.readLine()) != null) { try { @@ -246,7 +246,8 @@ public Mono sendMessage(JSONRPCMessage message) { */ private void startInboundProcessing() { this.inboundScheduler.schedule(() -> { - try (BufferedReader processReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + try (BufferedReader processReader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { String line; while (!isClosing && (line = processReader.readLine()) != null) { try { diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java index 68be62931..549bd07b5 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java @@ -10,7 +10,6 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; -import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; @@ -22,7 +21,6 @@ import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; -import io.modelcontextprotocol.spec.ProtocolVersions; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.json.McpJsonMapper; import org.slf4j.Logger; @@ -82,11 +80,6 @@ public StdioServerTransportProvider(McpJsonMapper jsonMapper, InputStream inputS this.outputStream = outputStream; } - @Override - public List protocolVersions() { - return List.of(ProtocolVersions.MCP_2024_11_05); - } - @Override public void setSessionFactory(McpServerSession.Factory sessionFactory) { // Create a single session for the stdio connection @@ -124,10 +117,10 @@ private class StdioMcpSessionTransport implements McpServerTransport { private final AtomicBoolean isStarted = new AtomicBoolean(false); /** Scheduler for handling inbound messages */ - private Scheduler inboundScheduler; + private final Scheduler inboundScheduler; /** Scheduler for handling outbound messages */ - private Scheduler outboundScheduler; + private final Scheduler outboundScheduler; private final Sinks.One outboundReady = Sinks.one(); @@ -198,9 +191,9 @@ private void startInboundProcessing() { if (isStarted.compareAndSet(false, true)) { this.inboundScheduler.schedule(() -> { inboundReady.tryEmitValue(null); - BufferedReader reader = null; + BufferedReader reader; try { - reader = new BufferedReader(new InputStreamReader(inputStream)); + reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); while (!isClosing.get()) { try { String line = reader.readLine(); From 497d6f1ce173c7b9981bec1999ca08cd93bf1a89 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 4 Mar 2026 00:42:25 +0800 Subject: [PATCH 2/3] Align with the latest version of the class StdioServerTransportProvider(#717) --- .../transport/StdioServerTransportProvider.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java index 352b02a10..66cc304d6 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java @@ -10,6 +10,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; @@ -21,6 +22,7 @@ import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; +import io.modelcontextprotocol.spec.ProtocolVersions; import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.json.McpJsonMapper; import org.slf4j.Logger; @@ -80,6 +82,11 @@ public StdioServerTransportProvider(McpJsonMapper jsonMapper, InputStream inputS this.outputStream = outputStream; } + @Override + public List protocolVersions() { + return List.of(ProtocolVersions.MCP_2024_11_05); + } + @Override public void setSessionFactory(McpServerSession.Factory sessionFactory) { // Create a single session for the stdio connection @@ -131,10 +138,10 @@ private class StdioMcpSessionTransport implements McpServerTransport { private final AtomicBoolean isStarted = new AtomicBoolean(false); /** Scheduler for handling inbound messages */ - private final Scheduler inboundScheduler; + private Scheduler inboundScheduler; /** Scheduler for handling outbound messages */ - private final Scheduler outboundScheduler; + private Scheduler outboundScheduler; private final Sinks.One outboundReady = Sinks.one(); @@ -205,7 +212,7 @@ private void startInboundProcessing() { if (isStarted.compareAndSet(false, true)) { this.inboundScheduler.schedule(() -> { inboundReady.tryEmitValue(null); - BufferedReader reader; + BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); while (!isClosing.get()) { From 1ce9a1f14951c836a605e3996f02a79c78f2e1d1 Mon Sep 17 00:00:00 2001 From: codeboyzhou Date: Wed, 4 Mar 2026 01:46:51 +0800 Subject: [PATCH 3/3] test(transport): add tests for utf-8 handling in stdio transport Add test cases to verify proper UTF-8 character handling in StdioClientTransport when system default charset is not UTF-8. Includes a test echo server subprocess to simulate non-UTF-8 environment. --- .../transport/StdioClientTransportTests.java | 128 ++++++++++++++++++ .../transport/StdioUtf8TestEchoServer.java | 51 +++++++ 2 files changed, 179 insertions(+) create mode 100644 mcp-test/src/test/java/io/modelcontextprotocol/client/transport/StdioClientTransportTests.java create mode 100644 mcp-test/src/test/java/io/modelcontextprotocol/client/transport/StdioUtf8TestEchoServer.java diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/StdioClientTransportTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/StdioClientTransportTests.java new file mode 100644 index 000000000..c7ebefe2d --- /dev/null +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/StdioClientTransportTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol.client.transport; + +import io.modelcontextprotocol.json.McpJsonDefaults; +import io.modelcontextprotocol.spec.McpSchema; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import reactor.test.StepVerifier; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StdioClientTransport}. + * + * @author Christian Tzolov + */ +@Timeout(30) +class StdioClientTransportTests { + + static final String FILE_SEPARATOR = FileSystems.getDefault().getSeparator(); + + @Test + void shouldHandleUtf8MessagesWithNonUtf8DefaultCharset() throws Exception { + String utf8Content = "한글 漢字 café 🎉"; + + String javaHome = System.getProperty("java.home"); + String classpath = System.getProperty("java.class.path"); + String javaExecutable = javaHome + FILE_SEPARATOR + "bin" + FILE_SEPARATOR + "java"; + + ServerParameters params = ServerParameters.builder(javaExecutable) + .args("-Dfile.encoding=ISO-8859-1", "-cp", classpath, StdioUtf8TestEchoServer.class.getName()) + .build(); + + StdioClientTransport transport = new StdioClientTransport(params, McpJsonDefaults.getMapper()); + + AtomicReference receivedMessage = new AtomicReference<>(); + CountDownLatch messageLatch = new CountDownLatch(1); + + StepVerifier.create(transport.connect(message -> { + return message.doOnNext(msg -> { + receivedMessage.set(msg); + messageLatch.countDown(); + }); + })).verifyComplete(); + + McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, "echo", 1, + Map.of("message", utf8Content)); + + StepVerifier.create(transport.sendMessage(request)).verifyComplete(); + + assertThat(messageLatch.await(10, TimeUnit.SECONDS)).isTrue(); + + assertThat(receivedMessage.get()).isNotNull(); + assertThat(receivedMessage.get()).isInstanceOf(McpSchema.JSONRPCResponse.class); + McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) receivedMessage.get(); + assertThat(response.result()).isEqualTo(utf8Content); + + transport.closeGracefully().block(); + } + + @Test + void shouldHandleUtf8ErrorMessagesWithNonUtf8DefaultCharset() throws Exception { + String utf8ErrorContent = "错误: 한글 漢字 🎉"; + + String javaHome = System.getProperty("java.home"); + String classpath = System.getProperty("java.class.path"); + String javaExecutable = javaHome + FILE_SEPARATOR + "bin" + FILE_SEPARATOR + "java"; + + ProcessBuilder pb = new ProcessBuilder(javaExecutable, "-Dfile.encoding=ISO-8859-1", "-cp", classpath, + StdioUtf8TestEchoServer.class.getName()); + pb.redirectErrorStream(false); + + Process process = pb.start(); + + try { + process.getOutputStream() + .write(("{\"jsonrpc\":\"2.0\",\"method\":\"echo\",\"params\":{\"message\":\"test\"},\"id\":1}\n") + .getBytes(StandardCharsets.UTF_8)); + process.getOutputStream().flush(); + + Thread errorThread = getErrorThread(process, utf8ErrorContent); + + process.waitFor(10, TimeUnit.SECONDS); + errorThread.join(1000); + } + finally { + process.destroyForcibly(); + process.waitFor(10, TimeUnit.SECONDS); + } + } + + private static @NonNull Thread getErrorThread(Process process, String utf8ErrorContent) { + AtomicReference errorContent = new AtomicReference<>(); + CountDownLatch errorLatch = new CountDownLatch(1); + + Thread errorThread = new Thread(() -> { + try (BufferedReader errorReader = new BufferedReader( + new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = errorReader.readLine()) != null) { + if (line.contains(utf8ErrorContent)) { + errorContent.set(line); + errorLatch.countDown(); + break; + } + } + } + catch (Exception ignored) { + } + }); + errorThread.start(); + return errorThread; + } + +} diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/StdioUtf8TestEchoServer.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/StdioUtf8TestEchoServer.java new file mode 100644 index 000000000..baf179840 --- /dev/null +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/transport/StdioUtf8TestEchoServer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol.client.transport; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Minimal STDIO echo server for testing UTF-8 encoding behavior in StdioClientTransport. + * + *

+ * This class is spawned as a subprocess with {@code -Dfile.encoding=ISO-8859-1} to + * simulate a non-UTF-8 default charset environment. It reads JSON-RPC messages from stdin + * and echoes the {@code params.message} value back to stdout, allowing the parent test to + * verify that multi-byte UTF-8 characters are preserved. + * + * @see StdioClientTransportTests#shouldHandleUtf8MessagesWithNonUtf8DefaultCharset + */ +public class StdioUtf8TestEchoServer { + + public static void main(String[] args) throws Exception { + CountDownLatch latch = new CountDownLatch(1); + StringBuilder receivedMessage = new StringBuilder(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("\"echo\"")) { + int start = line.indexOf("\"message\":\"") + "\"message\":\"".length(); + int end = line.indexOf("\"", start); + if (start > 0 && end > start) { + receivedMessage.append(line, start, end); + } + String response = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"" + receivedMessage + "\"}\n"; + System.out.write(response.getBytes(StandardCharsets.UTF_8)); + System.out.flush(); + latch.countDown(); + break; + } + } + } + + latch.await(5, TimeUnit.SECONDS); + } + +}