Spring Framework integration for Model Context Protocol (MCP), providing Spring AI function calling capabilities and Spring-friendly abstractions for MCP clients and MCP servers
React to tool changes in MCP servers through Spring's application event system using McpToolsChangedEvent.
import org.springframework.ai.mcp.McpToolsChangedEvent;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ApplicationEventPublisher;
import java.util.List;public class McpToolsChangedEvent extends ApplicationEvent {
public McpToolsChangedEvent(String connectionName, List<McpSchema.Tool> tools);
String getConnectionName();
List<McpSchema.Tool> getTools();
}Event class indicating that tools have changed in an MCP connection.
public McpToolsChangedEvent(String connectionName, List<McpSchema.Tool> tools)String getConnectionName()Returns the name of the MCP connection where tools changed.
List<McpSchema.Tool> getTools()Returns the updated list of tools available from the connection.
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class McpConnectionManager {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void notifyToolsChanged(String serverName, List<McpSchema.Tool> newTools) {
McpToolsChangedEvent event = new McpToolsChangedEvent(serverName, newTools);
eventPublisher.publishEvent(event);
}
public void refreshServerTools(McpSyncClient client) {
String serverName = client.getClientInfo().name();
// Get updated tool list
McpSchema.ListToolsResult result = client.listTools();
List<McpSchema.Tool> tools = result.tools();
// Notify listeners
notifyToolsChanged(serverName, tools);
}
}Both SyncMcpToolCallbackProvider and AsyncMcpToolCallbackProvider automatically listen for McpToolsChangedEvent and invalidate their caches when received.
// Providers automatically implement ApplicationListener<McpToolsChangedEvent>
public class SyncMcpToolCallbackProvider
implements ToolCallbackProvider, ApplicationListener<McpToolsChangedEvent> {
@Override
public void onApplicationEvent(McpToolsChangedEvent event) {
this.invalidateCache(); // Automatically invalidates on event
}
}import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class McpToolEventHandler {
@EventListener
public void handleToolsChanged(McpToolsChangedEvent event) {
System.out.println("Tools changed in connection: " + event.getConnectionName());
System.out.println("New tool count: " + event.getTools().size());
// Log each tool
for (McpSchema.Tool tool : event.getTools()) {
System.out.println(" - " + tool.name() + ": " + tool.description());
}
}
}import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class CustomMcpListener implements ApplicationListener<McpToolsChangedEvent> {
@Override
public void onApplicationEvent(McpToolsChangedEvent event) {
String connectionName = event.getConnectionName();
List<McpSchema.Tool> tools = event.getTools();
// Custom handling logic
updateToolRegistry(connectionName, tools);
notifyClients(connectionName);
}
private void updateToolRegistry(String connection, List<McpSchema.Tool> tools) {
// Update your tool registry
}
private void notifyClients(String connection) {
// Notify connected clients
}
}import org.springframework.ai.mcp.*;
import org.springframework.context.*;
import org.springframework.stereotype.Component;
import org.springframework.scheduling.annotation.Scheduled;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
@Component
public class McpToolMonitor {
private final ApplicationEventPublisher eventPublisher;
private final List<McpSyncClient> mcpClients;
private final Map<String, List<McpSchema.Tool>> lastKnownTools = new ConcurrentHashMap<>();
@Autowired
public McpToolMonitor(ApplicationEventPublisher eventPublisher,
List<McpSyncClient> mcpClients) {
this.eventPublisher = eventPublisher;
this.mcpClients = mcpClients;
}
// Periodically check for tool changes
@Scheduled(fixedDelay = 60000) // Every 60 seconds
public void checkForToolChanges() {
for (McpSyncClient client : mcpClients) {
String clientName = client.getClientInfo().name();
try {
// Get current tools
McpSchema.ListToolsResult result = client.listTools();
List<McpSchema.Tool> currentTools = result.tools();
// Check if tools have changed
List<McpSchema.Tool> previousTools = lastKnownTools.get(clientName);
if (previousTools == null || !toolsEqual(previousTools, currentTools)) {
// Tools have changed - publish event
McpToolsChangedEvent event =
new McpToolsChangedEvent(clientName, currentTools);
eventPublisher.publishEvent(event);
// Update cache
lastKnownTools.put(clientName, currentTools);
System.out.println("Tools changed for " + clientName);
}
} catch (Exception e) {
System.err.println("Error checking tools for " + clientName + ": " + e.getMessage());
}
}
}
private boolean toolsEqual(List<McpSchema.Tool> tools1, List<McpSchema.Tool> tools2) {
if (tools1.size() != tools2.size()) return false;
Set<String> names1 = tools1.stream().map(McpSchema.Tool::name).collect(Collectors.toSet());
Set<String> names2 = tools2.stream().map(McpSchema.Tool::name).collect(Collectors.toSet());
return names1.equals(names2);
}
}
@Component
public class McpToolChangeLogger {
@EventListener
public void logToolChanges(McpToolsChangedEvent event) {
System.out.println("=== MCP Tools Changed ===");
System.out.println("Connection: " + event.getConnectionName());
System.out.println("Tool Count: " + event.getTools().size());
for (McpSchema.Tool tool : event.getTools()) {
System.out.println(" - " + tool.name());
}
System.out.println("========================");
}
}import org.springframework.ai.mcp.*;
import org.springframework.stereotype.Service;
@Service
public class DynamicToolService {
private final SyncMcpToolCallbackProvider toolProvider;
private final ApplicationEventPublisher eventPublisher;
@Autowired
public DynamicToolService(SyncMcpToolCallbackProvider toolProvider,
ApplicationEventPublisher eventPublisher) {
this.toolProvider = toolProvider;
this.eventPublisher = eventPublisher;
}
public void refreshTools(String connectionName) {
// Manually trigger a refresh
McpToolsChangedEvent event = new McpToolsChangedEvent(connectionName, List.of());
eventPublisher.publishEvent(event);
// The toolProvider automatically invalidates its cache
// Next call to getToolCallbacks() will re-discover tools
ToolCallback[] freshCallbacks = toolProvider.getToolCallbacks();
System.out.println("Refreshed " + freshCallbacks.length + " tools");
}
public ToolCallback[] getCurrentTools() {
return toolProvider.getToolCallbacks();
}
}import org.springframework.scheduling.annotation.Async;
import org.springframework.context.event.EventListener;
@Component
public class AsyncMcpToolHandler {
@Async
@EventListener
public void handleToolsChangedAsync(McpToolsChangedEvent event) {
// Handle event asynchronously
System.out.println("Async handling tools changed for: " + event.getConnectionName());
// Perform expensive operations without blocking
analyzeToolChanges(event.getTools());
updateDashboard(event.getConnectionName());
notifyWebSocketClients(event);
}
private void analyzeToolChanges(List<McpSchema.Tool> tools) {
// Expensive analysis
}
private void updateDashboard(String connectionName) {
// Update monitoring dashboard
}
private void notifyWebSocketClients(McpToolsChangedEvent event) {
// Notify WebSocket clients
}
}import org.springframework.core.annotation.Order;
import org.springframework.context.event.EventListener;
@Component
public class OrderedMcpListeners {
@EventListener
@Order(1)
public void highPriorityHandler(McpToolsChangedEvent event) {
// Runs first
System.out.println("High priority: " + event.getConnectionName());
}
@EventListener
@Order(2)
public void normalPriorityHandler(McpToolsChangedEvent event) {
// Runs second
System.out.println("Normal priority: " + event.getConnectionName());
}
@EventListener
@Order(3)
public void lowPriorityHandler(McpToolsChangedEvent event) {
// Runs last
System.out.println("Low priority: " + event.getConnectionName());
}
}