diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index d3648a06f..999e8d13f 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -387,6 +387,10 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) } try { + // Servlet containers (e.g. Tomcat, Undertow) default to ISO-8859-1 when no + // charset is specified in the Content-Type header. JSON is always UTF-8 per + // RFC 8259, so we must explicitly set the encoding before reading the body. + request.setCharacterEncoding(UTF_8); BufferedReader reader = request.getReader(); StringBuilder body = new StringBuilder(); String line; diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java index 047aeebe8..0f95d8b43 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java @@ -154,6 +154,10 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) } try { + // Servlet containers (e.g. Tomcat, Undertow) default to ISO-8859-1 when no + // charset is specified in the Content-Type header. JSON is always UTF-8 per + // RFC 8259, so we must explicitly set the encoding before reading the body. + request.setCharacterEncoding(UTF_8); BufferedReader reader = request.getReader(); StringBuilder body = new StringBuilder(); String line; diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index 95edb63a0..325be51b0 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -429,6 +429,10 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) McpTransportContext transportContext = this.contextExtractor.extract(request); try { + // Servlet containers (e.g. Tomcat, Undertow) default to ISO-8859-1 when no + // charset is specified in the Content-Type header. JSON is always UTF-8 per + // RFC 8259, so we must explicitly set the encoding before reading the body. + request.setCharacterEncoding(UTF_8); BufferedReader reader = request.getReader(); StringBuilder body = new StringBuilder(); String line; diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java index e5d55c39d..5f6574402 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java @@ -818,6 +818,44 @@ void testToolCallSuccess(String clientType) { } } + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testToolCallWithUnicodeArguments(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + + // String containing multi-byte UTF-8 characters: em dash, Chinese, emoji + String unicodeInput = "Test \u2014 em dash, \u5929\u6c14\u9884\u62a5, \ud83d\ude00"; + + McpServerFeatures.SyncToolSpecification echoTool = McpServerFeatures.SyncToolSpecification.builder() + .tool(Tool.builder().name("echo").description("Echoes input").inputSchema(EMPTY_JSON_SCHEMA).build()) + .callHandler((exchange, request) -> { + String text = (String) request.arguments().get("text"); + return CallToolResult.builder().addContent(new TextContent(text)).build(); + }) + .build(); + + var mcpServer = prepareSyncServerBuilder().capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(echoTool) + .build(); + + try (var mcpClient = clientBuilder.build()) { + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + CallToolResult response = mcpClient + .callTool(new McpSchema.CallToolRequest("echo", Map.of("text", unicodeInput))); + + assertThat(response).isNotNull(); + assertThat(response.content()).hasSize(1); + assertThat(response.content().get(0)).isInstanceOf(TextContent.class); + assertThat(((TextContent) response.content().get(0)).text()).isEqualTo(unicodeInput); + } + finally { + mcpServer.closeGracefully(); + } + } + @ParameterizedTest(name = "{0} : {displayName} ") @MethodSource("clientsForTesting") void testThrowingToolCallIsCaughtBeforeTimeout(String clientType) { diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java index 491c2d4ed..599772630 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -639,6 +639,44 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception { mcpServer.close(); } + @ParameterizedTest(name = "{0} : {displayName} ") + @ValueSource(strings = { "httpclient" }) + void testToolCallWithUnicodeArguments(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + + // String containing multi-byte UTF-8 characters: em dash, Chinese, emoji + String unicodeInput = "Test \u2014 em dash, \u5929\u6c14\u9884\u62a5, \ud83d\ude00"; + + var echoTool = new McpStatelessServerFeatures.SyncToolSpecification( + Tool.builder().name("echo").description("Echoes input").inputSchema(EMPTY_JSON_SCHEMA).build(), + (transportContext, request) -> { + String text = (String) request.arguments().get("text"); + return CallToolResult.builder().content(List.of(new TextContent(text))).build(); + }); + + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(echoTool) + .build(); + + try (var mcpClient = clientBuilder.build()) { + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + CallToolResult response = mcpClient + .callTool(new McpSchema.CallToolRequest("echo", Map.of("text", unicodeInput))); + + assertThat(response).isNotNull(); + assertThat(response.content()).hasSize(1); + assertThat(response.content().get(0)).isInstanceOf(TextContent.class); + assertThat(((TextContent) response.content().get(0)).text()).isEqualTo(unicodeInput); + } + finally { + mcpServer.close(); + } + } + private double evaluateExpression(String expression) { // Simple expression evaluator for testing return switch (expression) {