CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux

Spring Boot starter providing auto-configuration for Model Context Protocol (MCP) client with Spring WebFlux, enabling reactive AI applications to connect to MCP servers via SSE and Streamable HTTP transports

Overview
Eval results
Files

real-world-scenarios.mddocs/examples/

Real-World Scenarios

Practical examples demonstrating common use cases for Spring AI MCP Client WebFlux.

Scenario 1: Multi-Tool AI Assistant

Build an AI assistant that uses tools from multiple MCP servers.

Configuration

spring.ai.mcp.client:
  type: SYNC
  request-timeout: 30s
  sse:
    connections:
      weather-service:
        url: http://localhost:8080
      database-service:
        url: http://localhost:9000
      calendar-service:
        url: http://localhost:9001

Implementation

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class AssistantService {
    private final ChatClient chatClient;
    
    public AssistantService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }
    
    public String handleQuery(String userQuery) {
        // AI automatically discovers and uses tools from all three servers
        return chatClient.prompt()
            .user(userQuery)
            .call()
            .content();
    }
}

Usage

// Query that uses multiple tools
String result = assistantService.handleQuery(
    "What's the weather in San Francisco, " +
    "find meetings today, " +
    "and check database for users from that city"
);

// AI will automatically:
// 1. Call weather tool via weather-service
// 2. Call calendar tool via calendar-service
// 3. Call database query tool via database-service
// 4. Synthesize results into response

Scenario 2: Local Development Tools

Use local MCP tool servers for development environment.

Configuration

spring.ai.mcp.client:
  stdio:
    connections:
      filesystem-tools:
        command: npx
        args:
          - -y
          - "@modelcontextprotocol/server-filesystem"
          - ${user.home}/projects
      git-tools:
        command: npx
        args:
          - -y
          - "@modelcontextprotocol/server-git"
          - ${user.home}/projects
        env:
          GIT_SSH_COMMAND: ssh -i ~/.ssh/id_rsa

Implementation

import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.stereotype.Service;

@Service
public class DevToolsService {
    private final List<McpSyncClient> clients;
    
    public DevToolsService(List<McpSyncClient> clients) {
        this.clients = clients;
    }
    
    public String listFiles(String directory) {
        return executeToolOnServer("filesystem-tools", "list_directory", 
            Map.of("path", directory));
    }
    
    public String readFile(String path) {
        return executeToolOnServer("filesystem-tools", "read_file",
            Map.of("path", path));
    }
    
    public String gitStatus() {
        return executeToolOnServer("git-tools", "git_status", Map.of());
    }
    
    private String executeToolOnServer(String serverPattern, String toolName, 
                                       Map<String, Object> args) {
        for (McpSyncClient client : clients) {
            String clientName = client.getClientInfo().name();
            if (clientName.contains(serverPattern)) {
                var request = McpSchema.CallToolRequest.builder()
                    .name(toolName)
                    .arguments(args)
                    .build();
                var result = client.callTool(request);
                return result.content().toString();
            }
        }
        throw new IllegalStateException("Server not found: " + serverPattern);
    }
}

Scenario 3: Production with Filtering and Monitoring

Secure production deployment with tool filtering and monitoring.

Configuration

spring.ai.mcp.client:
  type: ASYNC
  request-timeout: 15s
  streamable-http:
    connections:
      production-api:
        url: https://api.production.com
        endpoint: /mcp/v1

Security Filter

import org.springframework.ai.mcp.McpToolFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityConfig {
    
    @Bean
    public McpToolFilter productionToolFilter() {
        return (connectionInfo, tool) -> {
            String toolName = tool.name();
            
            // Block destructive operations
            if (toolName.matches("(delete|drop|truncate|destroy).*")) {
                return false;
            }
            
            // Block file system access
            if (toolName.startsWith("file_") || toolName.startsWith("fs_")) {
                return false;
            }
            
            // Only from trusted servers
            String serverName = connectionInfo.initializeResult()
                .serverInfo().name();
            if (!serverName.contains("production")) {
                return false;
            }
            
            // Require description (quality check)
            return tool.description() != null && !tool.description().isEmpty();
        };
    }
}

Monitoring

import org.springframework.ai.mcp.McpToolsChangedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import io.micrometer.core.instrument.MeterRegistry;

@Component
public class McpMonitoring {
    
    private final MeterRegistry meterRegistry;
    
    public McpMonitoring(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }
    
    @Async
    @EventListener
    public void monitorToolChanges(McpToolsChangedEvent event) {
        // Record metrics
        meterRegistry.gauge("mcp.tools.count",
            List.of(Tag.of("connection", event.getConnectionName())),
            event.getTools().size());
        
        // Alert if no tools
        if (event.getTools().isEmpty()) {
            sendAlert("No tools available: " + event.getConnectionName());
        }
    }
    
    private void sendAlert(String message) {
        // Alert implementation
    }
}

Scenario 4: Multi-Environment Setup

Different configurations for dev, test, and production.

application-dev.yml

spring.ai.mcp.client:
  type: SYNC
  request-timeout: 2m  # Long timeout for debugging
  stdio:
    connections:
      local-dev-server:
        command: npm
        args:
          - run
          - dev-server
        env:
          DEBUG: "true"
          LOG_LEVEL: DEBUG

application-test.yml

spring.ai.mcp.client:
  type: SYNC
  request-timeout: 30s
  sse:
    connections:
      test-server:
        url: http://test-mcp-server:8080

application-prod.yml

spring.ai.mcp.client:
  type: ASYNC  # Use async for production scale
  request-timeout: 10s  # Fail fast
  streamable-http:
    connections:
      production-api:
        url: https://api.production.com
        endpoint: /mcp/v1

Environment-Specific Beans

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
public class EnvironmentConfig {
    
    @Bean
    @Profile("dev")
    public McpSyncClientCustomizer devCustomizer() {
        return (name, spec) -> {
            spec.loggingConsumer(log ->
                System.out.println("[DEV] " + log.data())
            );
        };
    }
    
    @Bean
    @Profile("prod")
    public McpSyncClientCustomizer prodCustomizer() {
        return (name, spec) -> {
            // Production settings - minimal logging
            spec.requestTimeout(Duration.ofSeconds(10));
        };
    }
}

Scenario 5: Tool Caching and Performance

Optimize performance with caching and parallel execution.

Tool Cache Service

import org.springframework.ai.mcp.McpToolsChangedEvent;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

@Service
public class CachedToolService {
    
    @Cacheable("mcp-tools")
    public List<Tool> getTools(String connectionName) {
        // Tools cached - only fetched once
        return clients.stream()
            .filter(c -> c.getClientInfo().name().contains(connectionName))
            .findFirst()
            .map(McpSyncClient::listTools)
            .orElse(List.of());
    }
    
    @CacheEvict(value = "mcp-tools", allEntries = true)
    @EventListener
    public void evictCache(McpToolsChangedEvent event) {
        // Invalidate cache when tools change
    }
}

Parallel Tool Execution

import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
import reactor.core.publisher.Flux;

@Service
public class ParallelExecutionService {
    private final List<McpAsyncClient> clients;
    
    public ParallelExecutionService(List<McpAsyncClient> clients) {
        this.clients = clients;
    }
    
    public Map<String, Object> executeToolsInParallel(
            Map<String, Map<String, Object>> toolRequests) {
        
        return Flux.fromIterable(toolRequests.entrySet())
            .flatMap(entry -> {
                String toolName = entry.getKey();
                Map<String, Object> args = entry.getValue();
                
                return findAndExecuteTool(toolName, args)
                    .map(result -> Map.entry(toolName, result));
            })
            .collectMap(Map.Entry::getKey, Map.Entry::getValue)
            .block();
    }
    
    private Mono<Object> findAndExecuteTool(String toolName, 
                                            Map<String, Object> args) {
        // Find and execute tool across all clients
        return Flux.fromIterable(clients)
            .flatMap(client -> 
                client.listTools()
                    .flatMapMany(Flux::fromIterable)
                    .filter(tool -> tool.name().equals(toolName))
                    .next()
                    .flatMap(tool -> {
                        var request = CallToolRequest.builder()
                            .name(toolName)
                            .arguments(args)
                            .build();
                        return client.callTool(request)
                            .map(CallToolResult::content);
                    })
            )
            .next();
    }
}

Scenario 6: Custom WebClient for Corporate Proxy

Configure WebClient for corporate environment with proxy and authentication.

Proxy Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.ProxyProvider;

@Configuration
public class ProxyConfig {
    
    @Bean
    public WebClient.Builder webClientBuilder() {
        HttpClient httpClient = HttpClient.create()
            .proxy(proxy -> proxy
                .type(ProxyProvider.Proxy.HTTP)
                .host("proxy.corporate.com")
                .port(8080)
                .username("user")
                .password(s -> getProxyPassword())
            );
        
        return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .defaultHeader("User-Agent", "CorporateApp-MCP/1.0");
    }
    
    private String getProxyPassword() {
        // Load from secure source
        return System.getenv("PROXY_PASSWORD");
    }
}

Scenario 7: Dynamic Server Discovery

Discover MCP servers dynamically from service registry.

Service Discovery

import org.springframework.ai.mcp.client.common.autoconfigure.McpSseClientConnectionDetails;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DynamicDiscoveryConfig {
    
    private final DiscoveryClient discoveryClient;
    
    public DynamicDiscoveryConfig(DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
    }
    
    @Bean
    public McpSseClientConnectionDetails dynamicConnectionDetails() {
        return () -> {
            Map<String, SseParameters> connections = new HashMap<>();
            
            // Discover MCP services from registry
            List<ServiceInstance> instances = 
                discoveryClient.getInstances("mcp-service");
            
            for (ServiceInstance instance : instances) {
                String connectionName = instance.getInstanceId();
                String url = instance.getUri().toString();
                
                connections.put(connectionName,
                    new SseParameters(url, "/sse"));
            }
            
            return connections;
        };
    }
}

Scenario 8: Tool Result Transformation

Transform MCP tool results before returning to AI.

Result Transformer

import org.springframework.ai.mcp.ToolContextToMcpMetaConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ToolTransformationConfig {
    
    @Bean
    public ToolContextToMcpMetaConverter metadataConverter() {
        return toolContext -> {
            Map<String, Object> metadata = new HashMap<>();
            
            // Add request tracking
            metadata.put("requestId", UUID.randomUUID().toString());
            metadata.put("timestamp", System.currentTimeMillis());
            
            // Add user context
            String userId = (String) toolContext.getContext().get("userId");
            metadata.put("userId", userId);
            
            // Add application version
            metadata.put("appVersion", "1.0.0");
            
            return metadata;
        };
    }
}

Scenario 9: Graceful Degradation

Handle server failures gracefully with fallbacks.

Resilient Service

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.stereotype.Service;

@Service
public class ResilientMcpService {
    
    private final List<McpSyncClient> clients;
    
    public ResilientMcpService(List<McpSyncClient> clients) {
        this.clients = clients;
    }
    
    @CircuitBreaker(name = "mcp-tools", fallbackMethod = "fallbackExecute")
    @Retry(name = "mcp-tools")
    public Object executeTool(String toolName, Map<String, Object> args) {
        for (McpSyncClient client : clients) {
            try {
                List<Tool> tools = client.listTools();
                if (tools.stream().anyMatch(t -> t.name().equals(toolName))) {
                    var request = CallToolRequest.builder()
                        .name(toolName)
                        .arguments(args)
                        .build();
                    return client.callTool(request).content();
                }
            } catch (Exception e) {
                log.warn("Failed to execute on client: {}", 
                    client.getClientInfo().name(), e);
                // Continue to next client
            }
        }
        throw new IllegalStateException("Tool not found or all servers failed");
    }
    
    private Object fallbackExecute(String toolName, Map<String, Object> args,
                                   Throwable throwable) {
        log.error("All attempts failed for tool: {}", toolName, throwable);
        return Map.of("error", "Service temporarily unavailable",
                     "toolName", toolName);
    }
}

Scenario 10: Tool Metadata and Tracking

Track tool usage for analytics and debugging.

Tool Usage Tracker

import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Component
public class ToolUsageTracker {
    
    private final Map<String, AtomicLong> usageCount = new ConcurrentHashMap<>();
    private final Map<String, List<ToolExecution>> executionHistory = 
        new ConcurrentHashMap<>();
    
    public void trackExecution(String toolName, Map<String, Object> args,
                              Object result, long durationMs) {
        // Increment usage count
        usageCount.computeIfAbsent(toolName, k -> new AtomicLong())
            .incrementAndGet();
        
        // Record execution
        ToolExecution execution = new ToolExecution(
            toolName,
            args,
            result,
            durationMs,
            System.currentTimeMillis()
        );
        
        executionHistory.computeIfAbsent(toolName, k -> new ArrayList<>())
            .add(execution);
    }
    
    public Map<String, Long> getUsageStats() {
        return usageCount.entrySet().stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> e.getValue().get()
            ));
    }
    
    public record ToolExecution(
        String toolName,
        Map<String, Object> args,
        Object result,
        long durationMs,
        long timestamp
    ) {}
}

Aspect for Automatic Tracking

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ToolExecutionAspect {
    
    private final ToolUsageTracker tracker;
    
    public ToolExecutionAspect(ToolUsageTracker tracker) {
        this.tracker = tracker;
    }
    
    @Around("execution(* io.modelcontextprotocol.client.McpSyncClient.callTool(..))")
    public Object trackToolExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        CallToolRequest request = (CallToolRequest) joinPoint.getArgs()[0];
        
        try {
            CallToolResult result = (CallToolResult) joinPoint.proceed();
            long duration = System.currentTimeMillis() - startTime;
            
            tracker.trackExecution(
                request.name(),
                request.arguments(),
                result.content(),
                duration
            );
            
            return result;
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            tracker.trackExecution(request.name(), request.arguments(), 
                "ERROR: " + e.getMessage(), duration);
            throw e;
        }
    }
}

Scenario 11: Custom Tool Name Strategy

Implement intelligent tool naming for multi-server scenarios.

Smart Naming

import org.springframework.ai.mcp.McpToolNamePrefixGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ToolNamingConfig {
    
    @Bean
    public McpToolNamePrefixGenerator smartPrefixGenerator() {
        return (connectionInfo, tool) -> {
            String serverName = connectionInfo.initializeResult()
                .serverInfo().name();
            String toolName = tool.name();
            
            // For common tools, add server prefix
            if (isCommonToolName(toolName)) {
                return sanitize(serverName) + "_" + toolName;
            }
            
            // For unique tools, keep original name
            return toolName;
        };
    }
    
    private boolean isCommonToolName(String name) {
        Set<String> common = Set.of("search", "query", "list", "get", "fetch");
        return common.stream().anyMatch(name::startsWith);
    }
    
    private String sanitize(String name) {
        return name.toLowerCase().replaceAll("[^a-z0-9]", "_");
    }
}

Scenario 12: Resource Access with Caching

Access MCP resources with intelligent caching.

Resource Service

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ResourceService {
    
    private final List<McpSyncClient> clients;
    
    public ResourceService(List<McpSyncClient> clients) {
        this.clients = clients;
    }
    
    @Cacheable(value = "mcp-resources", key = "#uri")
    public String readResource(String uri) {
        for (McpSyncClient client : clients) {
            try {
                var request = ReadResourceRequest.builder()
                    .uri(uri)
                    .build();
                var result = client.readResource(request);
                return extractContent(result.contents());
            } catch (Exception e) {
                log.debug("Failed to read resource from client", e);
            }
        }
        throw new ResourceNotFoundException("Resource not found: " + uri);
    }
    
    public List<Resource> listAllResources() {
        return clients.stream()
            .flatMap(client -> {
                try {
                    return client.listResources().stream();
                } catch (Exception e) {
                    log.error("Failed to list resources", e);
                    return Stream.empty();
                }
            })
            .toList();
    }
    
    private String extractContent(Object contents) {
        // Extract text content from various formats
        if (contents instanceof String text) {
            return text;
        } else if (contents instanceof List<?> list) {
            return list.stream()
                .map(Object::toString)
                .collect(Collectors.joining("\n"));
        }
        return contents.toString();
    }
}

Scenario 13: Annotation-Based Integration

Use annotations for clean integration.

Handler Component

import org.springaicommunity.mcp.annotation.*;
import org.springframework.stereotype.Component;

@Component
public class McpHandlers {
    
    @McpLogging
    public void handleLogging(String connection, String level, String message) {
        log.info("[{}] {}: {}", connection, level, message);
    }
    
    @McpProgress
    public void handleProgress(String connection, long current, long total) {
        double pct = (current * 100.0) / total;
        log.info("Progress on {}: {:.2f}%", connection, pct);
    }
    
    @McpToolListChanged
    public void handleToolChange(String connection, List<Tool> tools) {
        log.info("Tools updated on {}: {} tools", connection, tools.size());
        refreshToolCache(connection, tools);
    }
    
    @McpSampling
    public CreateMessageResult handleSampling(String connection, 
                                             CreateMessageRequest request) {
        // Delegate to AI service
        String response = aiService.generate(
            convertMessages(request.messages())
        );
        
        return new CreateMessageResult(
            Role.ASSISTANT,
            new TextContent(response),
            null,
            StopReason.END_TURN
        );
    }
    
    private void refreshToolCache(String connection, List<Tool> tools) {
        // Cache update logic
    }
    
    private String convertMessages(List<SamplingMessage> messages) {
        // Message conversion logic
        return messages.stream()
            .map(this::extractText)
            .collect(Collectors.joining("\n"));
    }
    
    private String extractText(SamplingMessage message) {
        if (message.content() instanceof TextContent text) {
            return text.text();
        }
        return "";
    }
}

Related Documentation

  • Edge Cases - Advanced scenarios and corner cases
  • Quick Start Guide - Basic setup
  • Configuration Reference - All configuration options
  • MCP Clients Reference - Complete client API
tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux@1.1.0

docs

examples

edge-cases.md

real-world-scenarios.md

index.md

tile.json