Use explicit UTF-8 charset in StdioServerTransportProvider (#826)
Also add a test simulating a different locale in an isolated process. Resolves #295
This commit is contained in:
parent
5a06a37b04
commit
5b9c2fb414
3 changed files with 121 additions and 1 deletions
|
|
@ -200,7 +200,7 @@ public class StdioServerTransportProvider implements McpServerTransportProvider
|
||||||
inboundReady.tryEmitValue(null);
|
inboundReady.tryEmitValue(null);
|
||||||
BufferedReader reader = null;
|
BufferedReader reader = null;
|
||||||
try {
|
try {
|
||||||
reader = new BufferedReader(new InputStreamReader(inputStream));
|
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||||
while (!isClosing.get()) {
|
while (!isClosing.get()) {
|
||||||
try {
|
try {
|
||||||
String line = reader.readLine();
|
String line = reader.readLine();
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol.server.transport;
|
package io.modelcontextprotocol.server.transport;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
@ -135,6 +137,42 @@ class StdioServerTransportProviderTests {
|
||||||
}).verifyComplete();
|
}).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
|
@Test
|
||||||
void shouldNotifyClients() {
|
void shouldNotifyClients() {
|
||||||
// Set session factory
|
// Set session factory
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue