CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-mcp

Spring Framework integration for Model Context Protocol (MCP), providing Spring AI function calling capabilities and Spring-friendly abstractions for MCP clients and MCP servers

Overview
Eval results
Files

sync-tool-callbacks.mddocs/reference/

Synchronous Tool Callbacks

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

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;

Class Declaration

public class SyncMcpToolCallback implements ToolCallback {
    // Immutable, thread-safe implementation
}

Creating Tool Callbacks

Using Builder (Recommended)

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
}

Builder Parameters

  • mcpClient (required): The McpSyncClient instance for tool execution

    • Must be non-null and initialized
    • Should be configured with appropriate timeouts
    • Connection should be established before creating callback
  • tool (required): The McpSchema.Tool definition from the MCP server

    • Obtained via mcpClient.listTools()
    • Contains name, description, and input schema
    • Must be non-null
  • prefixedToolName (optional): Custom prefixed name for the tool

    • If not specified, defaults to the formatted original tool name
    • Used to avoid naming conflicts with multiple servers
    • Should be unique across all registered tools
    • Maximum length: 64 characters
  • toolContextToMcpMetaConverter (optional): Converter for tool context to MCP metadata

    • Defaults to ToolContextToMcpMetaConverter.defaultConverter()
    • Can be set to ToolContextToMcpMetaConverter.noOp() to skip conversion
    • Custom implementations can transform context data

Usage Example

McpSyncClient 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();

Using Constructor (Deprecated)

@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.

Tool Callback Methods

Get Tool Definition

ToolDefinition getToolDefinition();

Returns the Spring AI ToolDefinition for this MCP tool, including the name, description, and input schema.

Returns

  • ToolDefinition containing:
    • name(): Tool name (prefixed if specified)
    • description(): Tool description from MCP server
    • inputSchema(): JSON schema for tool inputs (normalized)

Usage Example

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);

Get Original Tool Name

String getOriginalToolName();

Returns the original MCP tool name without any prefix modification. Useful when you need to reference the server-side tool name.

Returns

  • String: Original tool name from MCP server (without prefix)

Usage Example

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"

Execute Tool

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.

Parameters

  • toolCallInput: JSON string containing the tool arguments

    • Must be valid JSON object syntax
    • If null or empty, defaults to "{}"
    • Should match the tool's input schema
    • Example: "{\"path\": \"/home/user\", \"recursive\": true}"
  • toolContext (optional): Spring AI tool context that can be converted to MCP metadata

    • Can contain arbitrary key-value pairs
    • Converted via toolContextToMcpMetaConverter
    • Filtered to exclude reserved keys (e.g., "exchange")
    • Example: new ToolContext(Map.of("userId", "user123"))

Returns

  • String: JSON string containing the tool execution results from the MCP server
    • Always returns a JSON-formatted string
    • May contain success data or error information
    • Should be parsed according to tool's output schema

Throws

  • ToolExecutionException: If tool execution fails or the MCP server returns an error
    • Contains the ToolDefinition for context
    • Wraps the underlying exception cause
    • Includes error message from MCP server if available

Execution Flow

  1. Input validation and normalization (null/empty → "{}")
  2. Context conversion to MCP metadata (if provided)
  3. Build CallToolRequest with tool name, arguments, and metadata
  4. Invoke mcpClient.callTool() (blocking call)
  5. Check result for errors via isError()
  6. Extract and return result content
  7. Wrap any errors in ToolExecutionException

Usage Example

SyncMcpToolCallback 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
}

Complete Example

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());

Integration with Spring AI

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();
    }
}

Error Handling

The call methods throw ToolExecutionException when:

  1. The MCP client throws an exception during tool invocation
  2. The MCP server returns a result with isError() set to true
  3. JSON parsing fails during input/output processing
  4. Network connectivity issues occur

Error Handling Patterns

import 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;
    }
}

Thread Safety

SyncMcpToolCallback instances are immutable after construction and are thread-safe. The same callback instance can be safely used concurrently across multiple threads.

Thread Safety Characteristics

  • Immutable State: All fields are final and set during construction
  • No Shared Mutable State: No instance variables are modified after construction
  • Concurrent Execution: Multiple threads can call call() simultaneously
  • Client Thread Safety: Thread safety depends on the underlying McpSyncClient implementation

Concurrent Usage Example

SyncMcpToolCallback 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();

Performance Considerations

Blocking Operations

  • All call methods block until the MCP tool execution completes
  • Execution time = network latency + server processing time
  • Not suitable for tools with execution time > 30 seconds
  • Consider AsyncMcpToolCallback for long-running operations

Connection Pooling

  • The underlying McpSyncClient should handle connection pooling appropriately
  • Reuse callback instances to avoid repeated client/tool lookups
  • Connection state is managed by the MCP client, not the callback

Null Input Handling

  • Empty or null input is automatically converted to "{}" to ensure valid JSON
  • This conversion happens before sending to MCP server
  • No performance penalty for null/empty input handling

Caching Strategy

// 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
}

Input Schema Validation

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());
    }
}

Edge Cases and Special Scenarios

Empty Tool Results

// 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
}

Large Input/Output

// 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");
    }
}

Tool with No Parameters

// 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 "{}"

Timeout Configuration

// 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");
    }
}

Related Components

  • Asynchronous Tool Callbacks - Non-blocking alternative
  • Synchronous Tool Discovery - Automatic tool discovery
  • Context and Metadata Conversion - Converting tool context to MCP metadata
  • MCP Tool Utilities - Utility methods for tool management

Install with Tessl CLI

npx tessl i tessl/maven-org-springframework-ai--spring-ai-mcp@1.1.0

docs

index.md

tile.json