Fix UTF-8 encoding for non-ASCII tool names in HTTP client transports

Both HttpClientSseClientTransport and HttpClientStreamableHttpTransport
set Content-Type to 'application/json' without specifying the charset.
While Java's BodyPublishers.ofString() uses UTF-8 by default, the
missing charset in the header can cause the server to interpret the
request body using a different encoding (e.g., ISO-8859-1), corrupting
non-ASCII characters such as Chinese tool names.

Explicitly set Content-Type to 'application/json; charset=utf-8' in
POST requests on both client transports.

Fixes #260

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Daniel Garnier-Moiroux <git@garnier.wf>
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
RameshReddy Adutla 2026-03-04 20:59:04 +00:00 • committed by Daniel Garnier-Moiroux
commit 8fd9903e62
3 changed files with 62 additions and 2 deletions

View file

@ -445,7 +445,7 @@ public class HttpClientSseClientTransport implements McpClientTransport {
return Mono.deferContextual(ctx -> {
var builder = this.requestBuilder.copy()
.uri(requestUri)
.header(HttpHeaders.CONTENT_TYPE, "application/json")
.header(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
.header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION)
.POST(HttpRequest.BodyPublishers.ofString(body));
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);

View file

@ -102,6 +102,8 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
private static final String APPLICATION_JSON = "application/json";
private static final String APPLICATION_JSON_UTF8 = "application/json; charset=utf-8";
private static final String TEXT_EVENT_STREAM = "text/event-stream";
public static int NOT_FOUND = 404;
@ -477,7 +479,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
var builder = requestBuilder.uri(uri)
.header(HttpHeaders.ACCEPT, APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_UTF8)
.header(HttpHeaders.CACHE_CONTROL, "no-cache")
.header(HttpHeaders.PROTOCOL_VERSION,
ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,

View file

@ -23,6 +23,7 @@ import java.util.stream.Collectors;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonDefaults;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
@ -47,6 +48,7 @@ import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.util.McpJsonMapperUtils;
import io.modelcontextprotocol.util.Utils;
import net.javacrumbs.jsonunit.core.Option;
import org.junit.jupiter.params.ParameterizedTest;
@ -914,6 +916,62 @@ public abstract class AbstractMcpClientServerIntegrationTests {
}
}
@ParameterizedTest(name = "{0} : {displayName} ")
@MethodSource("clientsForTesting")
void testToolWithNonAsciiCharacters(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
String inputSchema = """
{
"type": "object",
"properties": {
"username": { "type": "string" }
},
"required": ["username"]
}
""";
McpServerFeatures.SyncToolSpecification nonAsciiTool = McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("greeter")
.description("打招呼")
.inputSchema(McpJsonDefaults.getMapper(), inputSchema)
.build())
.callHandler((exchange, request) -> {
String username = (String) request.arguments().get("username");
return McpSchema.CallToolResult.builder()
.addContent(new McpSchema.TextContent("Hello " + username))
.build();
})
.build();
var mcpServer = prepareSyncServerBuilder().capabilities(ServerCapabilities.builder().tools(true).build())
.tools(nonAsciiTool)
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
var tools = mcpClient.listTools().tools();
assertThat(tools).hasSize(1);
assertThat(tools.get(0).name()).isEqualTo("greeter");
assertThat(tools.get(0).description()).isEqualTo("打招呼");
CallToolResult response = mcpClient
.callTool(new McpSchema.CallToolRequest("greeter", Map.of("username", "测试用户")));
assertThat(response).isNotNull();
assertThat(response.isError()).isFalse();
assertThat(response.content()).hasSize(1);
assertThat(((McpSchema.TextContent) response.content().get(0)).text()).isEqualTo("Hello 测试用户");
}
finally {
mcpServer.closeGracefully();
}
}
@ParameterizedTest(name = "{0} : {displayName} ")
@MethodSource("clientsForTesting")
void testToolListChangeHandlingSuccess(String clientType) {