Spring Framework integration for Model Context Protocol (MCP), providing Spring AI function calling capabilities and Spring-friendly abstractions for MCP clients and MCP servers
The SyncMcpToolCallback class provides a synchronous adapter that bridges MCP tools to Spring AI's ToolCallback interface. It handles blocking tool execution and data conversion between MCP and Spring AI formats.
import org.springframework.ai.mcp.SyncMcpToolCallback;
import org.springframework.ai.mcp.ToolContextToMcpMetaConverter;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.execution.ToolExecutionException;public class SyncMcpToolCallback implements ToolCallback {
// Immutable, thread-safe implementation
}static SyncMcpToolCallback.Builder builder();
class Builder {
Builder mcpClient(McpSyncClient mcpClient); // required
Builder tool(McpSchema.Tool tool); // required
Builder prefixedToolName(String prefixedToolName); // optional
Builder toolContextToMcpMetaConverter(ToolContextToMcpMetaConverter converter); // optional
SyncMcpToolCallback build(); // returns SyncMcpToolCallback
}mcpClient (required): The McpSyncClient instance for tool execution
tool (required): The McpSchema.Tool definition from the MCP server
mcpClient.listTools()prefixedToolName (optional): Custom prefixed name for the tool
toolContextToMcpMetaConverter (optional): Converter for tool context to MCP metadata
ToolContextToMcpMetaConverter.defaultConverter()ToolContextToMcpMetaConverter.noOp() to skip conversionMcpSyncClient mcpClient = // ... initialize MCP client
McpSchema.Tool tool = // ... get tool from MCP server
// Basic usage
SyncMcpToolCallback callback = SyncMcpToolCallback.builder()
.mcpClient(mcpClient)
.tool(tool)
.build();
// With custom prefix
SyncMcpToolCallback callback = SyncMcpToolCallback.builder()
.mcpClient(mcpClient)
.tool(tool)
.prefixedToolName("my_server_list_files")
.build();
// With custom converter
SyncMcpToolCallback callback = SyncMcpToolCallback.builder()
.mcpClient(mcpClient)
.tool(tool)
.toolContextToMcpMetaConverter(ToolContextToMcpMetaConverter.noOp())
.build();
// With all options
SyncMcpToolCallback callback = SyncMcpToolCallback.builder()
.mcpClient(mcpClient)
.tool(tool)
.prefixedToolName("prod_weather_get_forecast")
.toolContextToMcpMetaConverter(customConverter)
.build();@Deprecated
public SyncMcpToolCallback(McpSyncClient mcpClient, McpSchema.Tool tool);Note: This constructor is deprecated since version 1.1.0. Use the builder pattern instead for better flexibility and readability.
ToolDefinition getToolDefinition();Returns the Spring AI ToolDefinition for this MCP tool, including the name, description, and input schema.
ToolDefinition containing:
name(): Tool name (prefixed if specified)description(): Tool description from MCP serverinputSchema(): JSON schema for tool inputs (normalized)SyncMcpToolCallback callback = // ... create callback
ToolDefinition definition = callback.getToolDefinition();
String name = definition.name();
String description = definition.description();
String inputSchema = definition.inputSchema();
System.out.println("Tool: " + name);
System.out.println("Description: " + description);
System.out.println("Schema: " + inputSchema);String getOriginalToolName();Returns the original MCP tool name without any prefix modification. Useful when you need to reference the server-side tool name.
String: Original tool name from MCP server (without prefix)SyncMcpToolCallback callback = // ... create callback with prefix
String originalName = callback.getOriginalToolName();
String prefixedName = callback.getToolDefinition().name();
System.out.println("Original: " + originalName); // "list_files"
System.out.println("Prefixed: " + prefixedName); // "my_server_list_files"String call(String toolCallInput);
String call(String toolCallInput, ToolContext toolContext);Executes the MCP tool with the provided input and optional context. This is a blocking operation that waits for the MCP server to complete the tool execution.
toolCallInput: JSON string containing the tool arguments
"{}""{\"path\": \"/home/user\", \"recursive\": true}"toolContext (optional): Spring AI tool context that can be converted to MCP metadata
toolContextToMcpMetaConverternew ToolContext(Map.of("userId", "user123"))String: JSON string containing the tool execution results from the MCP server
ToolExecutionException: If tool execution fails or the MCP server returns an error
ToolDefinition for contextCallToolRequest with tool name, arguments, and metadatamcpClient.callTool() (blocking call)isError()ToolExecutionExceptionSyncMcpToolCallback callback = // ... create callback
// Simple execution without context
String input = "{\"path\": \"/home/user\", \"recursive\": true}";
String result = callback.call(input);
System.out.println("Result: " + result);
// Execution with context
ToolContext context = new ToolContext(Map.of(
"userId", "user123",
"sessionId", "session456"
));
String resultWithContext = callback.call(input, context);
// Handling null/empty input (automatically becomes "{}")
String emptyResult = callback.call(null); // Same as call("{}")
String emptyResult2 = callback.call(""); // Same as call("{}")
// Error handling
try {
String result = callback.call(input);
} catch (ToolExecutionException e) {
ToolDefinition failedTool = e.getToolDefinition();
System.err.println("Tool " + failedTool.name() + " failed");
System.err.println("Error: " + e.getMessage());
Throwable cause = e.getCause(); // Original exception
}import org.springframework.ai.mcp.SyncMcpToolCallback;
import org.springframework.ai.mcp.ToolContextToMcpMetaConverter;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.execution.ToolExecutionException;
import java.util.Map;
import java.util.List;
// Initialize MCP client
McpSyncClient mcpClient = // ... create your MCP sync client
// Get available tools from the MCP server
McpSchema.ListToolsResult toolsResult = mcpClient.listTools();
List<McpSchema.Tool> tools = toolsResult.tools();
// Create a callback for each tool
List<SyncMcpToolCallback> callbacks = tools.stream()
.map(tool -> SyncMcpToolCallback.builder()
.mcpClient(mcpClient)
.tool(tool)
.prefixedToolName("myserver_" + tool.name())
.toolContextToMcpMetaConverter(ToolContextToMcpMetaConverter.defaultConverter())
.build())
.toList();
// Use a specific callback
SyncMcpToolCallback callback = callbacks.get(0);
// Prepare input
String input = "{\"query\": \"example\"}";
// Execute with context
ToolContext context = new ToolContext(Map.of(
"requestId", "req-123",
"timestamp", System.currentTimeMillis()
));
try {
String result = callback.call(input, context);
System.out.println("Tool: " + callback.getToolDefinition().name());
System.out.println("Result: " + result);
} catch (ToolExecutionException e) {
System.err.println("Execution failed: " + e.getMessage());
}
// Get tool information
ToolDefinition definition = callback.getToolDefinition();
System.out.println("Name: " + definition.name());
System.out.println("Description: " + definition.description());
System.out.println("Original Name: " + callback.getOriginalToolName());import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class McpChatService {
private final ChatClient chatClient;
@Autowired
public McpChatService(ChatModel chatModel, List<McpSyncClient> mcpClients) {
// Create tool callbacks from MCP tools
List<SyncMcpToolCallback> mcpCallbacks = mcpClients.stream()
.flatMap(client -> {
try {
return client.listTools().tools().stream()
.map(tool -> SyncMcpToolCallback.builder()
.mcpClient(client)
.tool(tool)
.build());
} catch (Exception e) {
System.err.println("Failed to list tools: " + e.getMessage());
return java.util.stream.Stream.empty();
}
})
.toList();
// Convert to ToolCallback array
ToolCallback[] toolCallbacks = mcpCallbacks.toArray(new ToolCallback[0]);
// Create ChatClient with MCP tools
this.chatClient = ChatClient.builder(chatModel)
.defaultFunctions(toolCallbacks)
.build();
}
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
public String chatWithContext(String userMessage, Map<String, Object> contextData) {
return chatClient.prompt()
.user(userMessage)
.toolContext(contextData)
.call()
.content();
}
}The call methods throw ToolExecutionException when:
isError() set to trueimport org.springframework.ai.tool.execution.ToolExecutionException;
// Pattern 1: Basic error handling
try {
String result = callback.call(input);
} catch (ToolExecutionException e) {
System.err.println("Tool execution failed: " + e.getMessage());
ToolDefinition failedTool = e.getToolDefinition();
// Handle the error appropriately
}
// Pattern 2: Differentiate error types
try {
String result = callback.call(input);
} catch (ToolExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof java.net.ConnectException) {
System.err.println("Connection failed to MCP server");
} else if (cause instanceof java.net.SocketTimeoutException) {
System.err.println("Tool execution timed out");
} else {
System.err.println("Tool error: " + e.getMessage());
}
}
// Pattern 3: Retry with fallback
String executeWithRetry(SyncMcpToolCallback callback, String input, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
try {
return callback.call(input);
} catch (ToolExecutionException e) {
if (i == maxRetries - 1) throw e;
System.out.println("Retry " + (i + 1) + " after error: " + e.getMessage());
try {
Thread.sleep(1000 * (i + 1)); // Exponential backoff
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw e;
}
}
}
throw new IllegalStateException("Should not reach here");
}
// Pattern 4: Graceful degradation
String executeWithFallback(SyncMcpToolCallback callback, String input, String fallback) {
try {
return callback.call(input);
} catch (ToolExecutionException e) {
System.err.println("Tool failed, using fallback: " + e.getMessage());
return fallback;
}
}SyncMcpToolCallback instances are immutable after construction and are thread-safe. The same callback instance can be safely used concurrently across multiple threads.
call() simultaneouslyMcpSyncClient implementationSyncMcpToolCallback callback = // ... create once
ExecutorService executor = Executors.newFixedThreadPool(10);
// Submit concurrent tasks
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 100; i++) {
String input = "{\"id\": " + i + "}";
Future<String> future = executor.submit(() -> callback.call(input));
futures.add(future);
}
// Collect results
for (Future<String> future : futures) {
try {
String result = future.get();
System.out.println("Result: " + result);
} catch (Exception e) {
System.err.println("Task failed: " + e.getMessage());
}
}
executor.shutdown();call methods block until the MCP tool execution completesAsyncMcpToolCallback for long-running operationsMcpSyncClient should handle connection pooling appropriately"{}" to ensure valid JSON// Good: Create once, reuse many times
SyncMcpToolCallback callback = SyncMcpToolCallback.builder()
.mcpClient(mcpClient)
.tool(tool)
.build();
for (int i = 0; i < 1000; i++) {
String result = callback.call(input); // Reuse callback
}
// Bad: Create on every call
for (int i = 0; i < 1000; i++) {
SyncMcpToolCallback callback = SyncMcpToolCallback.builder()
.mcpClient(mcpClient)
.tool(tool)
.build();
String result = callback.call(input); // Creates new instance each time
}While the callback doesn't validate input against the schema, you can implement validation:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
void validateAndCall(SyncMcpToolCallback callback, String input) {
ObjectMapper mapper = new ObjectMapper();
ToolDefinition definition = callback.getToolDefinition();
String schemaJson = definition.inputSchema();
// Create schema validator
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
JsonSchema schema = factory.getSchema(schemaJson);
// Validate input
try {
JsonNode inputNode = mapper.readTree(input);
Set<ValidationMessage> errors = schema.validate(inputNode);
if (!errors.isEmpty()) {
throw new IllegalArgumentException("Invalid input: " + errors);
}
// Input is valid, execute tool
String result = callback.call(input);
System.out.println("Result: " + result);
} catch (Exception e) {
System.err.println("Validation or execution failed: " + e.getMessage());
}
}// MCP server may return empty results
String result = callback.call("{}");
// result could be: "", "{}", "[]", or "null"
// Always check result before parsing
if (result != null && !result.isEmpty() && !result.equals("null")) {
// Process result
}// For large inputs, ensure MCP client has appropriate buffer sizes
String largeInput = generateLargeJson(); // e.g., 10MB
try {
String result = callback.call(largeInput);
// May timeout or fail with OutOfMemoryError
} catch (ToolExecutionException e) {
if (e.getCause() instanceof OutOfMemoryError) {
System.err.println("Input too large");
}
}// Some tools accept no parameters
SyncMcpToolCallback callback = // ... tool with empty input schema
String result = callback.call("{}"); // Empty object
String result2 = callback.call(null); // Also becomes "{}"// Configure timeout at MCP client level
McpSyncClient client = // ... create client with timeout configuration
SyncMcpToolCallback callback = SyncMcpToolCallback.builder()
.mcpClient(client)
.tool(tool)
.build();
// Timeout enforced by client, not callback
try {
String result = callback.call(input);
} catch (ToolExecutionException e) {
if (e.getCause() instanceof SocketTimeoutException) {
System.err.println("Tool execution timed out");
}
}