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

async-tool-discovery.mddocs/reference/

Asynchronous Tool Discovery

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

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;

Class Declaration

public class AsyncMcpToolCallbackProvider
    implements ToolCallbackProvider, ApplicationListener<McpToolsChangedEvent>

Creating Providers

Using Builder (Recommended)

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

Builder Parameters

  • mcpClients (required): One or more McpAsyncClient instances. Can be provided as a List or varargs array
  • toolFilter (optional): Predicate for filtering discovered tools. Defaults to accepting all tools (mcpClient, tool) -> true
  • toolNamePrefixGenerator (optional): Strategy for generating prefixed tool names. Defaults to DefaultMcpToolNamePrefixGenerator
  • toolContextToMcpMetaConverter (optional): Converter for tool context to MCP metadata. Defaults to ToolContextToMcpMetaConverter.defaultConverter()

Usage Examples

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

Using Constructors (All Deprecated)

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

Provider Methods

Get Tool Callbacks

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.

Usage Example

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

Invalidate Cache

void invalidateCache()

Forces re-discovery of tools from async MCP servers on the next getToolCallbacks() call.

Usage Example

AsyncMcpToolCallbackProvider provider = // ... create provider

// Force refresh of tool list
provider.invalidateCache();

// Next call will re-discover all tools
ToolCallback[] updatedCallbacks = provider.getToolCallbacks();

Handle Tool Change Events

void onApplicationEvent(McpToolsChangedEvent event)

Automatically called by Spring when a McpToolsChangedEvent is published. Invalidates the cache to trigger re-discovery.

Static Factory Methods

Async Tool Callbacks Factory

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.

Usage Example

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

Complete Example

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

Integration with Spring AI

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

Spring Configuration Example

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

Reactive Tool Discovery Pattern

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

Tool Name Collision Handling

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
}

Caching Behavior

  • Tool callbacks are cached after the first getToolCallbacks() call
  • Cache is invalidated when:
    • invalidateCache() is called explicitly
    • A McpToolsChangedEvent is received
  • Subsequent calls return cached results until invalidation
  • Thread-safe caching for concurrent access

Performance Considerations

  • Async Discovery: Tool discovery uses reactive patterns, allowing efficient handling of multiple MCP servers
  • Blocking on Get: The getToolCallbacks() method blocks on reactive execution. For fully non-blocking apps, use the asyncToolCallbacks() factory method
  • Backpressure: The reactive discovery pipeline supports backpressure through Flux
  • Parallel Discovery: Tools from multiple servers can be discovered in parallel through reactive composition

Comparison with SyncMcpToolCallbackProvider

FeatureAsyncMcpToolCallbackProviderSyncMcpToolCallbackProvider
Client TypeMcpAsyncClientMcpSyncClient
Discovery ModelReactive (Flux)Blocking
Parallel DiscoveryYes (via reactive streams)No
Static Factory ReturnFlux<ToolCallback>List<ToolCallback>
Best ForReactive apps, many serversSimple apps, few servers

Related Components

  • Asynchronous Tool Callbacks - Individual async tool callback
  • Synchronous Tool Discovery - Blocking alternative
  • Tool Filtering and Naming - Customizing tool selection and naming
  • Event Handling - Tool change events

Install with Tessl CLI

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

docs

index.md

tile.json