Skip to content

Commit cd8a19e

Browse files
committed
Add subprocess-based test to verify UTF-8 encoding fix
Spawns a child JVM with -Dfile.encoding=ISO-8859-1 to simulate a non-UTF-8 default charset environment. The test sends a JSON-RPC message containing multi-byte characters (Korean, Chinese, accented Latin, emoji) through StdioServerTransportProvider and verifies they are preserved. This test fails without the explicit UTF-8 charset fix and passes with it.
1 parent c123636 commit cd8a19e

File tree

2 files changed

+120
-2
lines changed

2 files changed

+120
-2
lines changed

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

Lines changed: 38 additions & 2 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;
@@ -15,13 +17,11 @@
1517
import java.util.concurrent.atomic.AtomicReference;
1618

1719
import io.modelcontextprotocol.json.McpJsonDefaults;
18-
import io.modelcontextprotocol.spec.McpError;
1920
import io.modelcontextprotocol.spec.McpSchema;
2021
import io.modelcontextprotocol.spec.McpServerSession;
2122
import io.modelcontextprotocol.spec.McpServerTransport;
2223
import org.junit.jupiter.api.AfterEach;
2324
import org.junit.jupiter.api.BeforeEach;
24-
import org.junit.jupiter.api.Disabled;
2525
import org.junit.jupiter.api.Test;
2626
import reactor.core.publisher.Mono;
2727
import reactor.test.StepVerifier;
@@ -135,6 +135,42 @@ void shouldHandleIncomingMessages() throws Exception {
135135
}).verifyComplete();
136136
}
137137

138+
@Test
139+
void shouldHandleUtf8MessagesWithNonUtf8DefaultCharset() throws Exception {
140+
String utf8Content = "한글 漢字 café 🎉";
141+
String jsonMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"test\"," + "\"params\":{\"message\":\"" + utf8Content
142+
+ "\"},\"id\":1}\n";
143+
144+
// Start a subprocess with non-UTF-8 default charset
145+
String javaHome = System.getProperty("java.home");
146+
String classpath = System.getProperty("java.class.path");
147+
ProcessBuilder pb = new ProcessBuilder(javaHome + "/bin/java", "-Dfile.encoding=ISO-8859-1", "-cp", classpath,
148+
StdioUtf8TestServer.class.getName());
149+
pb.redirectErrorStream(false);
150+
Process process = pb.start();
151+
152+
try {
153+
// Write UTF-8 encoded JSON-RPC message to the subprocess stdin
154+
process.getOutputStream().write(jsonMessage.getBytes(StandardCharsets.UTF_8));
155+
process.getOutputStream().flush();
156+
process.getOutputStream().close();
157+
158+
// Read the echoed message from subprocess stdout
159+
String result;
160+
try (BufferedReader reader = new BufferedReader(
161+
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
162+
result = reader.readLine();
163+
}
164+
165+
// Verify that multi-byte UTF-8 characters survived the round trip
166+
assertThat(result).isEqualTo(utf8Content);
167+
}
168+
finally {
169+
process.destroyForcibly();
170+
process.waitFor(10, TimeUnit.SECONDS);
171+
}
172+
}
173+
138174
@Test
139175
void shouldNotifyClients() {
140176
// 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)