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:
parent
5e77762eeb
commit
8fd9903e62
3 changed files with 62 additions and 2 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue