Skip to content

Commit 3f7f2f1

Browse files
committed
fix(mcp): standardize MCP tool name formatting and improve error handling
- Add prefixedToolName utility method to ensure consistent tool name formatting - E nforce alphanumeric, underscore, and hyphen characters only in tool names - Limit tool names to 64 characters maximum - Use original tool name in actual calls while using formatted names in definitions - Add error handling for tool call responses in SyncMcpToolCallback - Update tests to reflect the changes Signed-off-by: Christian Tzolov <[email protected]>
1 parent ded9fac commit 3f7f2f1

File tree

4 files changed

+38
-9
lines changed

4 files changed

+38
-9
lines changed

mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public AsyncMcpToolCallback(McpAsyncClient mcpClient, Tool tool) {
8686
@Override
8787
public ToolDefinition getToolDefinition() {
8888
return ToolDefinition.builder()
89-
.name(this.asyncMcpClient.getClientInfo().name() + "-" + this.tool.name())
89+
.name(McpToolUtils.prefixedToolName(this.asyncMcpClient.getClientInfo().name(), this.tool.name()))
9090
.description(this.tool.description())
9191
.inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema()))
9292
.build();
@@ -107,7 +107,9 @@ public ToolDefinition getToolDefinition() {
107107
@Override
108108
public String call(String functionInput) {
109109
Map<String, Object> arguments = ModelOptionsUtils.jsonToMap(functionInput);
110-
return this.asyncMcpClient.callTool(new CallToolRequest(this.getToolDefinition().name(), arguments))
110+
// Note that we use the original tool name here, not the adapted one from
111+
// getToolDefinition
112+
return this.asyncMcpClient.callTool(new CallToolRequest(this.tool.name(), arguments))
111113
.map(response -> ModelOptionsUtils.toJsonString(response.content()))
112114
.block();
113115
}

mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java

+20
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,26 @@ public final class McpToolUtils {
5656
private McpToolUtils() {
5757
}
5858

59+
public static String prefixedToolName(String prefix, String toolName) {
60+
61+
String input = prefix + "-" + toolName;
62+
63+
if (input == null || input.isEmpty()) {
64+
throw new IllegalArgumentException("Input string cannot be null or empty");
65+
}
66+
67+
// Replace any character that isn't alphanumeric, underscore, or hyphen with
68+
// concatenation
69+
String formatted = input.replaceAll("[^a-zA-Z0-9_-]", "");
70+
71+
// If the string is longer than 64 characters, keep the last 64 characters
72+
if (formatted.length() > 64) {
73+
formatted = formatted.substring(formatted.length() - 64);
74+
}
75+
76+
return formatted;
77+
}
78+
5979
/**
6080
* Converts a list of Spring AI tool callbacks to MCP synchronous tool registrations.
6181
* <p>

mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.java

+10-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.ai.mcp;
1818

1919
import java.util.Map;
20-
import java.util.UUID;
2120

2221
import io.modelcontextprotocol.client.McpSyncClient;
2322
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
@@ -42,7 +41,9 @@
4241
* <li>Manages JSON serialization/deserialization of tool inputs and outputs</li>
4342
* </ul>
4443
* <p>
45-
* Example usage: <pre>{@code
44+
* Example usage:
45+
*
46+
* <pre>{@code
4647
* McpSyncClient mcpClient = // obtain MCP client
4748
* Tool mcpTool = // obtain MCP tool definition
4849
* ToolCallback callback = new McpToolCallback(mcpClient, mcpTool);
@@ -88,7 +89,7 @@ public SyncMcpToolCallback(McpSyncClient mcpClient, Tool tool) {
8889
@Override
8990
public ToolDefinition getToolDefinition() {
9091
return ToolDefinition.builder()
91-
.name(mcpClient.getClientInfo().name() + "-" + this.tool.name())
92+
.name(McpToolUtils.prefixedToolName(this.mcpClient.getClientInfo().name(), this.tool.name()))
9293
.description(this.tool.description())
9394
.inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema()))
9495
.build();
@@ -109,8 +110,12 @@ public ToolDefinition getToolDefinition() {
109110
@Override
110111
public String call(String functionInput) {
111112
Map<String, Object> arguments = ModelOptionsUtils.jsonToMap(functionInput);
112-
CallToolResult response = this.mcpClient
113-
.callTool(new CallToolRequest(this.getToolDefinition().name(), arguments));
113+
// Note that we use the original tool name here, not the adapted one from
114+
// getToolDefinition
115+
CallToolResult response = this.mcpClient.callTool(new CallToolRequest(this.tool.name(), arguments));
116+
if (response.isError()) {
117+
throw new IllegalStateException("Error calling tool: " + response.content());
118+
}
114119
return ModelOptionsUtils.toJsonString(response.content());
115120
}
116121

mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ void getToolDefinitionShouldReturnCorrectDefinition() {
6363
@Test
6464
void callShouldHandleJsonInputAndOutput() {
6565

66-
when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient", "1.0.0"));
66+
// when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient",
67+
// "1.0.0"));
6768

6869
when(tool.name()).thenReturn("testTool");
6970
CallToolResult callResult = mock(CallToolResult.class);
@@ -79,7 +80,8 @@ void callShouldHandleJsonInputAndOutput() {
7980

8081
@Test
8182
void callShoulIngroeToolContext() {
82-
when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient", "1.0.0"));
83+
// when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient",
84+
// "1.0.0"));
8385

8486
when(tool.name()).thenReturn("testTool");
8587
CallToolResult callResult = mock(CallToolResult.class);

0 commit comments

Comments
 (0)