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 SyncMcpToolCallbackProvider class automatically discovers and manages tool callbacks from one or more synchronous MCP servers. It implements Spring AI's ToolCallbackProvider interface and handles tool listing, filtering, caching, and lifecycle management.
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.McpToolFilter;
import org.springframework.ai.mcp.McpToolNamePrefixGenerator;
import org.springframework.ai.mcp.ToolContextToMcpMetaConverter;
import io.modelcontextprotocol.client.McpSyncClient;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;public class SyncMcpToolCallbackProvider
implements ToolCallbackProvider, ApplicationListener<McpToolsChangedEvent>SyncMcpToolCallbackProvider.Builder builder();
class Builder {
Builder mcpClients(List<McpSyncClient> mcpClients);
Builder mcpClients(McpSyncClient... mcpClients);
Builder addMcpClient(McpSyncClient mcpClient);
Builder toolFilter(McpToolFilter toolFilter);
Builder toolNamePrefixGenerator(McpToolNamePrefixGenerator toolNamePrefixGenerator);
Builder toolContextToMcpMetaConverter(ToolContextToMcpMetaConverter toolContextToMcpMetaConverter);
SyncMcpToolCallbackProvider build();
}McpSyncClient instances. Can be provided as a List, varargs array, or added individually with addMcpClient()(mcpClient, tool) -> trueDefaultMcpToolNamePrefixGeneratorToolContextToMcpMetaConverter.defaultConverter()// Single MCP client
McpSyncClient client = // ... initialize client
SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder()
.mcpClients(client)
.build();
// Multiple MCP clients
SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder()
.mcpClients(client1, client2, client3)
.build();
// With custom configuration
SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder()
.mcpClients(client1, client2)
.toolFilter((connectionInfo, tool) -> !tool.name().startsWith("internal_"))
.toolNamePrefixGenerator(new DefaultMcpToolNamePrefixGenerator())
.toolContextToMcpMetaConverter(ToolContextToMcpMetaConverter.defaultConverter())
.build();
// Adding clients one by one
SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder()
.addMcpClient(client1)
.addMcpClient(client2)
.toolFilter((info, tool) -> tool.description().contains("approved"))
.build();@Deprecated
public SyncMcpToolCallbackProvider(McpToolFilter toolFilter, List<McpSyncClient> mcpClients)
@Deprecated
public SyncMcpToolCallbackProvider(List<McpSyncClient> mcpClients)
@Deprecated
public SyncMcpToolCallbackProvider(McpToolFilter toolFilter,
McpToolNamePrefixGenerator toolNamePrefixGenerator,
McpSyncClient... mcpClients)
@Deprecated
public SyncMcpToolCallbackProvider(McpSyncClient... mcpClients)Note: All constructors are deprecated. Use the builder pattern instead.
ToolCallback[] getToolCallbacks()Returns an array of all discovered tool callbacks from the configured MCP clients. Results are cached until invalidateCache() is called or a McpToolsChangedEvent is received.
SyncMcpToolCallbackProvider provider = // ... create provider
// Get all tool callbacks
ToolCallback[] callbacks = provider.getToolCallbacks();
// Use with Spring AI
for (ToolCallback callback : callbacks) {
System.out.println("Tool: " + callback.getToolDefinition().name());
}void invalidateCache()Forces re-discovery of tools from MCP servers on the next getToolCallbacks() call. This is useful when you know tools have been added or removed from the MCP servers.
SyncMcpToolCallbackProvider provider = // ... create provider
// Force refresh of tool list
provider.invalidateCache();
// Next call will re-discover all tools
ToolCallback[] updatedCallbacks = provider.getToolCallbacks();void onApplicationEvent(McpToolsChangedEvent event)Automatically called by Spring when a McpToolsChangedEvent is published. Invalidates the cache to trigger re-discovery.
import org.springframework.context.ApplicationEventPublisher;
// In your Spring component
@Component
public class McpServerManager {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void notifyToolsChanged(String serverName, List<McpSchema.Tool> tools) {
eventPublisher.publishEvent(new McpToolsChangedEvent(serverName, tools));
// SyncMcpToolCallbackProvider will automatically invalidate cache
}
}static List<ToolCallback> syncToolCallbacks(List<McpSyncClient> mcpClients)Convenience method to quickly create tool callbacks from multiple MCP clients with default settings.
List<McpSyncClient> clients = List.of(client1, client2);
List<ToolCallback> callbacks = SyncMcpToolCallbackProvider.syncToolCallbacks(clients);import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.McpToolFilter;
import org.springframework.ai.mcp.DefaultMcpToolNamePrefixGenerator;
import io.modelcontextprotocol.client.McpSyncClient;
// Create multiple MCP clients
McpSyncClient weatherClient = // ... weather service MCP client
McpSyncClient databaseClient = // ... database MCP client
McpSyncClient filesystemClient = // ... filesystem MCP client
// Custom tool filter - only allow certain tools
McpToolFilter filter = (connectionInfo, tool) -> {
String toolName = tool.name();
// Allow all weather tools
if (connectionInfo.clientInfo().name().contains("weather")) {
return true;
}
// Only allow read operations for database
if (connectionInfo.clientInfo().name().contains("database")) {
return toolName.startsWith("select_") || toolName.startsWith("query_");
}
// Allow specific filesystem operations
return toolName.equals("list_directory") || toolName.equals("read_file");
};
// Create provider with multiple clients and custom filter
SyncMcpToolCallbackProvider provider = SyncMcpToolCallbackProvider.builder()
.mcpClients(weatherClient, databaseClient, filesystemClient)
.toolFilter(filter)
.toolNamePrefixGenerator(new DefaultMcpToolNamePrefixGenerator())
.build();
// Get all discovered and filtered tool callbacks
ToolCallback[] toolCallbacks = provider.getToolCallbacks();
System.out.println("Discovered " + toolCallbacks.length + " tools");
for (ToolCallback callback : toolCallbacks) {
System.out.println(" - " + callback.getToolDefinition().name());
}
// Later, if tools change
provider.invalidateCache();
ToolCallback[] updatedCallbacks = provider.getToolCallbacks();import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class AiChatService {
private final ChatClient chatClient;
@Autowired
public AiChatService(ChatModel chatModel,
SyncMcpToolCallbackProvider mcpToolProvider) {
this.chatClient = ChatClient.builder(chatModel)
.defaultFunctions(mcpToolProvider.getToolCallbacks())
.build();
}
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class McpConfiguration {
@Bean
public SyncMcpToolCallbackProvider mcpToolProvider(
List<McpSyncClient> mcpClients) {
return SyncMcpToolCallbackProvider.builder()
.mcpClients(mcpClients)
.toolFilter((info, tool) -> {
// Custom filtering logic
return !tool.description().contains("[internal]");
})
.toolNamePrefixGenerator(new DefaultMcpToolNamePrefixGenerator())
.build();
}
@Bean
public McpSyncClient weatherMcpClient() {
// Create and configure weather MCP client
return // ... client instance
}
@Bean
public McpSyncClient databaseMcpClient() {
// Create and configure database MCP client
return // ... client instance
}
}The provider automatically validates that all discovered tools have unique names after prefixing. If duplicate names are detected, an IllegalStateException is thrown during getToolCallbacks().
try {
ToolCallback[] callbacks = provider.getToolCallbacks();
} catch (IllegalStateException e) {
// Handle duplicate tool names
System.err.println("Duplicate tool names detected: " + e.getMessage());
// Consider using a custom McpToolNamePrefixGenerator to avoid conflicts
}getToolCallbacks() callinvalidateCache() is called explicitlyMcpToolsChangedEvent is receivedgetToolCallbacks() return cached results until invalidationReentrantLock for concurrent accessgetToolCallbacks() queries all MCP servers, which may be slow for many servers or slow networksgetToolCallbacks() concurrentlyinvalidateCache() judiciously as it forces expensive re-discovery