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 AsyncMcpToolCallbackProvider class automatically discovers and manages tool callbacks from one or more asynchronous MCP servers. It implements Spring AI's ToolCallbackProvider interface with reactive capabilities using Project Reactor.
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.McpToolFilter;
import org.springframework.ai.mcp.McpToolNamePrefixGenerator;
import org.springframework.ai.mcp.ToolContextToMcpMetaConverter;
import io.modelcontextprotocol.client.McpAsyncClient;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import reactor.core.publisher.Flux;public class AsyncMcpToolCallbackProvider
implements ToolCallbackProvider, ApplicationListener<McpToolsChangedEvent>AsyncMcpToolCallbackProvider.Builder builder();
class Builder {
Builder toolFilter(McpToolFilter toolFilter);
Builder mcpClients(List<McpAsyncClient> mcpClients);
Builder mcpClients(McpAsyncClient... mcpClients);
Builder toolNamePrefixGenerator(McpToolNamePrefixGenerator toolNamePrefixGenerator);
Builder toolContextToMcpMetaConverter(ToolContextToMcpMetaConverter toolContextToMcpMetaConverter);
AsyncMcpToolCallbackProvider build();
}McpAsyncClient instances. Can be provided as a List or varargs array(mcpClient, tool) -> trueDefaultMcpToolNamePrefixGeneratorToolContextToMcpMetaConverter.defaultConverter()// Single async MCP client
McpAsyncClient asyncClient = // ... initialize async client
AsyncMcpToolCallbackProvider provider = AsyncMcpToolCallbackProvider.builder()
.mcpClients(asyncClient)
.build();
// Multiple async MCP clients
AsyncMcpToolCallbackProvider provider = AsyncMcpToolCallbackProvider.builder()
.mcpClients(asyncClient1, asyncClient2, asyncClient3)
.build();
// With custom configuration
AsyncMcpToolCallbackProvider provider = AsyncMcpToolCallbackProvider.builder()
.mcpClients(List.of(asyncClient1, asyncClient2))
.toolFilter((connectionInfo, tool) -> !tool.name().startsWith("internal_"))
.toolNamePrefixGenerator(McpToolNamePrefixGenerator.noPrefix())
.toolContextToMcpMetaConverter(ToolContextToMcpMetaConverter.defaultConverter())
.build();@Deprecated
public AsyncMcpToolCallbackProvider(McpToolFilter toolFilter, List<McpAsyncClient> mcpClients)
@Deprecated
public AsyncMcpToolCallbackProvider(List<McpAsyncClient> mcpClients)
@Deprecated
public AsyncMcpToolCallbackProvider(McpToolFilter toolFilter, McpAsyncClient... mcpClients)
@Deprecated
public AsyncMcpToolCallbackProvider(McpAsyncClient... mcpClients)Note: All constructors are deprecated. Use the builder pattern instead.
ToolCallback[] getToolCallbacks()Returns an array of all discovered tool callbacks from the configured async MCP clients. Results are cached until invalidateCache() is called or a McpToolsChangedEvent is received.
AsyncMcpToolCallbackProvider provider = // ... create provider
// Get all tool callbacks (blocks on reactive discovery)
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 async MCP servers on the next getToolCallbacks() call.
AsyncMcpToolCallbackProvider 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.
static Flux<ToolCallback> asyncToolCallbacks(List<McpAsyncClient> mcpClients)Returns a reactive Flux of tool callbacks discovered from the provided async MCP clients. This method provides a fully reactive API without blocking.
import reactor.core.publisher.Flux;
List<McpAsyncClient> clients = List.of(asyncClient1, asyncClient2);
// Reactive discovery
Flux<ToolCallback> callbacksFlux = AsyncMcpToolCallbackProvider
.asyncToolCallbacks(clients);
// Subscribe and process reactively
callbacksFlux.subscribe(callback -> {
System.out.println("Discovered tool: " + callback.getToolDefinition().name());
});
// Or block to get a list
List<ToolCallback> callbacks = callbacksFlux.collectList().block();import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.McpToolFilter;
import org.springframework.ai.mcp.DefaultMcpToolNamePrefixGenerator;
import io.modelcontextprotocol.client.McpAsyncClient;
import reactor.core.publisher.Flux;
// Create multiple async MCP clients
McpAsyncClient weatherClient = // ... async weather service MCP client
McpAsyncClient databaseClient = // ... async database MCP client
McpAsyncClient filesystemClient = // ... async filesystem MCP client
// Custom tool filter
McpToolFilter filter = (connectionInfo, tool) -> {
// Only allow tools that are marked as production-ready
return tool.description() != null &&
!tool.description().contains("[beta]") &&
!tool.description().contains("[experimental]");
};
// Create provider with multiple async clients
AsyncMcpToolCallbackProvider provider = AsyncMcpToolCallbackProvider.builder()
.mcpClients(weatherClient, databaseClient, filesystemClient)
.toolFilter(filter)
.toolNamePrefixGenerator(new DefaultMcpToolNamePrefixGenerator())
.build();
// Get all discovered tool callbacks
ToolCallback[] toolCallbacks = provider.getToolCallbacks();
System.out.println("Discovered " + toolCallbacks.length + " async tools");
for (ToolCallback callback : toolCallbacks) {
System.out.println(" - " + callback.getToolDefinition().name());
}
// Alternative: Use reactive API
Flux<ToolCallback> callbacksFlux = AsyncMcpToolCallbackProvider
.asyncToolCallbacks(List.of(weatherClient, databaseClient, filesystemClient));
callbacksFlux
.filter(callback -> callback.getToolDefinition().name().contains("query"))
.subscribe(callback -> {
System.out.println("Query tool found: " + callback.getToolDefinition().name());
});import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
@Component
public class AsyncAiChatService {
private final ChatClient chatClient;
@Autowired
public AsyncAiChatService(ChatModel chatModel,
AsyncMcpToolCallbackProvider asyncMcpToolProvider) {
this.chatClient = ChatClient.builder(chatModel)
.defaultFunctions(asyncMcpToolProvider.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;
import io.modelcontextprotocol.client.McpAsyncClient;
@Configuration
public class AsyncMcpConfiguration {
@Bean
public AsyncMcpToolCallbackProvider asyncMcpToolProvider(
List<McpAsyncClient> asyncMcpClients) {
return AsyncMcpToolCallbackProvider.builder()
.mcpClients(asyncMcpClients)
.toolFilter((info, tool) -> {
// Filter out internal tools
return !tool.name().startsWith("_");
})
.toolNamePrefixGenerator(new DefaultMcpToolNamePrefixGenerator())
.build();
}
@Bean
public McpAsyncClient weatherAsyncClient() {
// Create and configure async weather MCP client
return // ... async client instance
}
@Bean
public McpAsyncClient databaseAsyncClient() {
// Create and configure async database MCP client
return // ... async client instance
}
}import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
// Discover tools reactively from multiple servers
Flux<ToolCallback> discoverTools(List<McpAsyncClient> clients) {
return Flux.fromIterable(clients)
.flatMap(client -> client.listTools()
.flatMapMany(result -> Flux.fromIterable(result.tools()))
.map(tool -> AsyncMcpToolCallback.builder()
.mcpClient(client)
.tool(tool)
.build()));
}
// Use the reactive discovery
Mono<List<ToolCallback>> toolCallbacksMono = discoverTools(clients)
.collectList();
// Subscribe or block as needed
toolCallbacksMono.subscribe(callbacks -> {
System.out.println("Discovered " + callbacks.size() + " tools");
});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) {
System.err.println("Duplicate tool names detected: " + e.getMessage());
// Consider using a custom McpToolNamePrefixGenerator
}getToolCallbacks() callinvalidateCache() is called explicitlyMcpToolsChangedEvent is receivedgetToolCallbacks() method blocks on reactive execution. For fully non-blocking apps, use the asyncToolCallbacks() factory method| Feature | AsyncMcpToolCallbackProvider | SyncMcpToolCallbackProvider |
|---|---|---|
| Client Type | McpAsyncClient | McpSyncClient |
| Discovery Model | Reactive (Flux) | Blocking |
| Parallel Discovery | Yes (via reactive streams) | No |
| Static Factory Return | Flux<ToolCallback> | List<ToolCallback> |
| Best For | Reactive apps, many servers | Simple apps, few servers |