Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ private void startInboundProcessing() {
inboundReady.tryEmitValue(null);
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream));
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
while (!isClosing.get()) {
try {
String line = reader.readLine();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

package io.modelcontextprotocol.server.transport;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
Expand Down Expand Up @@ -135,6 +137,42 @@ void shouldHandleIncomingMessages() throws Exception {
}).verifyComplete();
}

@Test
void shouldHandleUtf8MessagesWithNonUtf8DefaultCharset() throws Exception {
String utf8Content = "한글 漢字 café 🎉";
String jsonMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"test\"," + "\"params\":{\"message\":\"" + utf8Content
+ "\"},\"id\":1}\n";

// Start a subprocess with non-UTF-8 default charset
String javaHome = System.getProperty("java.home");
String classpath = System.getProperty("java.class.path");
ProcessBuilder pb = new ProcessBuilder(javaHome + "/bin/java", "-Dfile.encoding=ISO-8859-1", "-cp", classpath,
StdioUtf8TestServer.class.getName());
pb.redirectErrorStream(false);
Process process = pb.start();

try {
// Write UTF-8 encoded JSON-RPC message to the subprocess stdin
process.getOutputStream().write(jsonMessage.getBytes(StandardCharsets.UTF_8));
process.getOutputStream().flush();
process.getOutputStream().close();

// Read the echoed message from subprocess stdout
String result;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
result = reader.readLine();
}

// Verify that multi-byte UTF-8 characters survived the round trip
assertThat(result).isEqualTo(utf8Content);
}
finally {
process.destroyForcibly();
process.waitFor(10, TimeUnit.SECONDS);
}
}

@Test
void shouldNotifyClients() {
// Set session factory
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2024-2024 the original author or authors.
*/

package io.modelcontextprotocol.server.transport;

import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
import reactor.core.publisher.Mono;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
* Minimal STDIO server process for testing UTF-8 encoding behavior.
*
* <p>
* This class is spawned as a subprocess with {@code -Dfile.encoding=ISO-8859-1} to
* simulate a non-UTF-8 default charset environment. It uses
* {@link StdioServerTransportProvider} to read a JSON-RPC message from stdin and echoes
* the received {@code params.message} value back to stdout, allowing the parent test to
* verify that multi-byte UTF-8 characters are preserved regardless of the JVM default
* charset.
*
* @see StdioServerTransportProviderTests#shouldHandleUtf8MessagesWithNonUtf8DefaultCharset
*/
public class StdioUtf8TestServer {

@SuppressWarnings("unchecked")
public static void main(String[] args) throws Exception {
// Capture the original stdout for echoing the result later
PrintStream originalOut = System.out;

// Redirect System.out to stderr so that logger output does not
// interfere with the test result written to stdout
System.setOut(new PrintStream(System.err, true));

CountDownLatch messageLatch = new CountDownLatch(1);
StringBuilder receivedMessage = new StringBuilder();

StdioServerTransportProvider transportProvider = new StdioServerTransportProvider(McpJsonDefaults.getMapper(),
System.in, OutputStream.nullOutputStream());

McpServerSession.Factory sessionFactory = transport -> {
McpServerSession session = mock(McpServerSession.class);
when(session.handle(any())).thenAnswer(invocation -> {
McpSchema.JSONRPCMessage msg = invocation.getArgument(0);
if (msg instanceof McpSchema.JSONRPCRequest request) {
Map<String, Object> params = (Map<String, Object>) request.params();
receivedMessage.append(params.get("message"));
}
messageLatch.countDown();
return Mono.empty();
});
when(session.closeGracefully()).thenReturn(Mono.empty());
return session;
};

// Start processing stdin
transportProvider.setSessionFactory(sessionFactory);

// Wait for the message to be processed
if (messageLatch.await(10, TimeUnit.SECONDS)) {
// Write the received message to the original stdout in UTF-8
originalOut.write(receivedMessage.toString().getBytes(StandardCharsets.UTF_8));
originalOut.write('\n');
originalOut.flush();
}

transportProvider.closeGracefully().block(java.time.Duration.ofSeconds(5));
}

}