diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java b/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java index fec8531e9d8..95d1b90399a 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java @@ -86,7 +86,7 @@ public AsyncMcpToolCallback(McpAsyncClient mcpClient, Tool tool) { @Override public ToolDefinition getToolDefinition() { return ToolDefinition.builder() - .name(this.asyncMcpClient.getClientInfo().name() + "-" + this.tool.name()) + .name(McpToolUtils.prefixedToolName(this.asyncMcpClient.getClientInfo().name(), this.tool.name())) .description(this.tool.description()) .inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema())) .build(); @@ -107,7 +107,9 @@ public ToolDefinition getToolDefinition() { @Override public String call(String functionInput) { Map arguments = ModelOptionsUtils.jsonToMap(functionInput); - return this.asyncMcpClient.callTool(new CallToolRequest(this.getToolDefinition().name(), arguments)) + // Note that we use the original tool name here, not the adapted one from + // getToolDefinition + return this.asyncMcpClient.callTool(new CallToolRequest(this.tool.name(), arguments)) .map(response -> ModelOptionsUtils.toJsonString(response.content())) .block(); } diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java index d9b74af42c9..1e051dbb0d3 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java @@ -56,6 +56,26 @@ public final class McpToolUtils { private McpToolUtils() { } + public static String prefixedToolName(String prefix, String toolName) { + + String input = prefix + "-" + toolName; + + if (input == null || input.isEmpty()) { + throw new IllegalArgumentException("Input string cannot be null or empty"); + } + + // Replace any character that isn't alphanumeric, underscore, or hyphen with + // concatenation + String formatted = input.replaceAll("[^a-zA-Z0-9_-]", ""); + + // If the string is longer than 64 characters, keep the last 64 characters + if (formatted.length() > 64) { + formatted = formatted.substring(formatted.length() - 64); + } + + return formatted; + } + /** * Converts a list of Spring AI tool callbacks to MCP synchronous tool registrations. *

diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.java b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.java index 528a0a960bf..1b71e930161 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.java @@ -17,7 +17,6 @@ package org.springframework.ai.mcp; import java.util.Map; -import java.util.UUID; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; @@ -42,7 +41,9 @@ *

  • Manages JSON serialization/deserialization of tool inputs and outputs
  • * *

    - * Example usage:

    {@code
    + * Example usage:
    + *
    + * 
    {@code
      * McpSyncClient mcpClient = // obtain MCP client
      * Tool mcpTool = // obtain MCP tool definition
      * ToolCallback callback = new McpToolCallback(mcpClient, mcpTool);
    @@ -88,7 +89,7 @@ public SyncMcpToolCallback(McpSyncClient mcpClient, Tool tool) {
     	@Override
     	public ToolDefinition getToolDefinition() {
     		return ToolDefinition.builder()
    -			.name(mcpClient.getClientInfo().name() + "-" + this.tool.name())
    +			.name(McpToolUtils.prefixedToolName(this.mcpClient.getClientInfo().name(), this.tool.name()))
     			.description(this.tool.description())
     			.inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema()))
     			.build();
    @@ -109,8 +110,12 @@ public ToolDefinition getToolDefinition() {
     	@Override
     	public String call(String functionInput) {
     		Map arguments = ModelOptionsUtils.jsonToMap(functionInput);
    -		CallToolResult response = this.mcpClient
    -			.callTool(new CallToolRequest(this.getToolDefinition().name(), arguments));
    +		// Note that we use the original tool name here, not the adapted one from
    +		// getToolDefinition
    +		CallToolResult response = this.mcpClient.callTool(new CallToolRequest(this.tool.name(), arguments));
    +		if (response.isError()) {
    +			throw new IllegalStateException("Error calling tool: " + response.content());
    +		}
     		return ModelOptionsUtils.toJsonString(response.content());
     	}
     
    diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java
    index 2a14b3e29bd..747ee6352e9 100644
    --- a/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java
    +++ b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java
    @@ -63,7 +63,8 @@ void getToolDefinitionShouldReturnCorrectDefinition() {
     	@Test
     	void callShouldHandleJsonInputAndOutput() {
     
    -		when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient", "1.0.0"));
    +		// when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient",
    +		// "1.0.0"));
     
     		when(tool.name()).thenReturn("testTool");
     		CallToolResult callResult = mock(CallToolResult.class);
    @@ -79,7 +80,8 @@ void callShouldHandleJsonInputAndOutput() {
     
     	@Test
     	void callShoulIngroeToolContext() {
    -		when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient", "1.0.0"));
    +		// when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient",
    +		// "1.0.0"));
     
     		when(tool.name()).thenReturn("testTool");
     		CallToolResult callResult = mock(CallToolResult.class);