Skip to content

Commit 05f37ad

Browse files
authored
Use explicit UTF-8 charset in StdioServerTransportProvider (#826)
Also add a test simulating a different locale in an isolated process. Resolves #295
1 parent 7f68ea5 commit 05f37ad

File tree

3 files changed

+121
-1
lines changed

3 files changed

+121
-1
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ private void startInboundProcessing() {
214214
inboundReady.tryEmitValue(null);
215215
BufferedReader reader = null;
216216
try {
217-
reader = new BufferedReader(new InputStreamReader(inputStream));
217+
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
218218
while (!isClosing.get()) {
219219
try {
220220
String line = reader.readLine();

mcp-test/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportProviderTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
package io.modelcontextprotocol.server.transport;
66

7+
import java.io.BufferedReader;
78
import java.io.ByteArrayInputStream;
89
import java.io.ByteArrayOutputStream;
910
import java.io.InputStream;
11+
import java.io.InputStreamReader;
1012
import java.io.PrintStream;
1113
import java.nio.charset.StandardCharsets;
1214
import java.util.Map;
@@ -135,6 +137,42 @@ void shouldHandleIncomingMessages() throws Exception {
135137
}).verifyComplete();
136138
}
137139

140+
@Test
141+
void shouldHandleUtf8MessagesWithNonUtf8DefaultCharset() throws Exception {
142+
String utf8Content = "한글 漢字 café 🎉";
143+
String jsonMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"test\"," + "\"params\":{\"message\":\"" + utf8Content
144+
+ "\"},\"id\":1}\n";
145+
146+
// Start a subprocess with non-UTF-8 default charset
147+
String javaHome = System.getProperty("java.home");
148+
String classpath = System.getProperty("java.class.path");
149+
ProcessBuilder pb = new ProcessBuilder(javaHome + "/bin/java", "-Dfile.encoding=ISO-8859-1", "-cp", classpath,
150+
StdioUtf8TestServer.class.getName());
151+
pb.redirectErrorStream(false);
152+
Process process = pb.start();
153+
154+
try {
155+
// Write UTF-8 encoded JSON-RPC message to the subprocess stdin
156+
process.getOutputStream().write(jsonMessage.getBytes(StandardCharsets.UTF_8));
157+
process.getOutputStream().flush();
158+
process.getOutputStream().close();
159+
160+
// Read the echoed message from subprocess stdout
161+
String result;
162+
try (BufferedReader reader = new BufferedReader(
163+
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
164+
result = reader.readLine();
165+
}
166+
167+
// Verify that multi-byte UTF-8 characters survived the round trip
168+
assertThat(result).isEqualTo(utf8Content);
169+
}
170+
finally {
171+
process.destroyForcibly();
172+
process.waitFor(10, TimeUnit.SECONDS);
173+
}
174+
}
175+
138176
@Test
139177
void shouldNotifyClients() {
140178
// Set session factory
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2024-2024 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.server.transport;
6+
7+
import java.io.OutputStream;
8+
import java.io.PrintStream;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.Map;
11+
import java.util.concurrent.CountDownLatch;
12+
import java.util.concurrent.TimeUnit;
13+
14+
import io.modelcontextprotocol.json.McpJsonDefaults;
15+
import io.modelcontextprotocol.spec.McpSchema;
16+
import io.modelcontextprotocol.spec.McpServerSession;
17+
import reactor.core.publisher.Mono;
18+
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
22+
23+
/**
24+
* Minimal STDIO server process for testing UTF-8 encoding behavior.
25+
*
26+
* <p>
27+
* This class is spawned as a subprocess with {@code -Dfile.encoding=ISO-8859-1} to
28+
* simulate a non-UTF-8 default charset environment. It uses
29+
* {@link StdioServerTransportProvider} to read a JSON-RPC message from stdin and echoes
30+
* the received {@code params.message} value back to stdout, allowing the parent test to
31+
* verify that multi-byte UTF-8 characters are preserved regardless of the JVM default
32+
* charset.
33+
*
34+
* @see StdioServerTransportProviderTests#shouldHandleUtf8MessagesWithNonUtf8DefaultCharset
35+
*/
36+
public class StdioUtf8TestServer {
37+
38+
@SuppressWarnings("unchecked")
39+
public static void main(String[] args) throws Exception {
40+
// Capture the original stdout for echoing the result later
41+
PrintStream originalOut = System.out;
42+
43+
// Redirect System.out to stderr so that logger output does not
44+
// interfere with the test result written to stdout
45+
System.setOut(new PrintStream(System.err, true));
46+
47+
CountDownLatch messageLatch = new CountDownLatch(1);
48+
StringBuilder receivedMessage = new StringBuilder();
49+
50+
StdioServerTransportProvider transportProvider = new StdioServerTransportProvider(McpJsonDefaults.getMapper(),
51+
System.in, OutputStream.nullOutputStream());
52+
53+
McpServerSession.Factory sessionFactory = transport -> {
54+
McpServerSession session = mock(McpServerSession.class);
55+
when(session.handle(any())).thenAnswer(invocation -> {
56+
McpSchema.JSONRPCMessage msg = invocation.getArgument(0);
57+
if (msg instanceof McpSchema.JSONRPCRequest request) {
58+
Map<String, Object> params = (Map<String, Object>) request.params();
59+
receivedMessage.append(params.get("message"));
60+
}
61+
messageLatch.countDown();
62+
return Mono.empty();
63+
});
64+
when(session.closeGracefully()).thenReturn(Mono.empty());
65+
return session;
66+
};
67+
68+
// Start processing stdin
69+
transportProvider.setSessionFactory(sessionFactory);
70+
71+
// Wait for the message to be processed
72+
if (messageLatch.await(10, TimeUnit.SECONDS)) {
73+
// Write the received message to the original stdout in UTF-8
74+
originalOut.write(receivedMessage.toString().getBytes(StandardCharsets.UTF_8));
75+
originalOut.write('\n');
76+
originalOut.flush();
77+
}
78+
79+
transportProvider.closeGracefully().block(java.time.Duration.ofSeconds(5));
80+
}
81+
82+
}

0 commit comments

Comments
 (0)