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

real-world-scenarios.mddocs/examples/

Real-World Scenarios

This document provides complete, production-ready examples of using Spring AI MCP in real applications.

Scenario 1: Multi-Server Integration with Filtering

Use Case: Enterprise application integrating weather, database, and filesystem MCP servers with security filtering.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ai.mcp.*;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.McpClient;

@Configuration
public class EnterpriseM cpConfiguration {

    @Bean
    public McpSyncClient weatherClient() {
        return McpClient.sync()
            .serverInfo(McpSchema.Implementation.builder()
                .name("weather-service")
                .version("1.0.0")
                .build())
            .build();
    }

    @Bean
    public McpSyncClient databaseClient() {
        return McpClient.sync()
            .serverInfo(McpSchema.Implementation.builder()
                .name("database-service")
                .version("2.1.0")
                .build())
            .build();
    }

    @Bean
    public McpSyncClient filesystemClient() {
        return McpClient.sync()
            .serverInfo(McpSchema.Implementation.builder()
                .name("filesystem-service")
                .version("1.5.0")
                .build())
            .build();
    }

    @Bean
    public McpToolFilter securityFilter() {
        return (connectionInfo, tool) -> {
            String serverName = connectionInfo.clientInfo().name();
            String toolName = tool.name();
            
            // Production database: only read operations
            if (serverName.contains("database")) {
                return toolName.startsWith("select_") || 
                       toolName.startsWith("query_") ||
                       toolName.startsWith("get_");
            }
            
            // Filesystem: no delete or write operations
            if (serverName.contains("filesystem")) {
                return !toolName.contains("delete") && 
                       !toolName.contains("write") &&
                       !toolName.contains("remove");
            }
            
            // Weather: all tools allowed
            if (serverName.contains("weather")) {
                return true;
            }
            
            // Default: reject unknown servers
            return false;
        };
    }

    @Bean
    public SyncMcpToolCallbackProvider mcpToolProvider(
            List<McpSyncClient> mcpClients,
            McpToolFilter securityFilter) {
        
        return SyncMcpToolCallbackProvider.builder()
            .mcpClients(mcpClients)
            .toolFilter(securityFilter)
            .toolNamePrefixGenerator(new DefaultMcpToolNamePrefixGenerator())
            .build();
    }

    @Bean
    public ChatClient aiChatClient(ChatModel chatModel,
                                   SyncMcpToolCallbackProvider toolProvider) {
        return ChatClient.builder(chatModel)
            .defaultFunctions(toolProvider.getToolCallbacks())
            .build();
    }
}

Scenario 2: Reactive WebFlux Application with Async Tools

Use Case: High-throughput API service using async MCP tools with backpressure.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.*;
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
import io.modelcontextprotocol.client.McpAsyncClient;
import reactor.core.publisher.Mono;

@Configuration
public class ReactiveAiConfiguration {

    @Bean
    public McpAsyncClient asyncWeatherClient() {
        return McpClient.async()
            .serverInfo(/* connection details */)
            .build();
    }

    @Bean
    public McpAsyncClient asyncDatabaseClient() {
        return McpClient.async()
            .serverInfo(/* connection details */)
            .build();
    }

    @Bean
    public AsyncMcpToolCallbackProvider asyncToolProvider(
            List<McpAsyncClient> asyncClients) {
        
        return AsyncMcpToolCallbackProvider.builder()
            .mcpClients(asyncClients)
            .toolFilter((info, tool) -> 
                !tool.description().contains("[deprecated]"))
            .build();
    }

    @Bean
    public RouterFunction<ServerResponse> aiRoutes(ChatModel chatModel,
                                                   AsyncMcpToolCallbackProvider toolProvider) {
        
        ChatClient chatClient = ChatClient.builder(chatModel)
            .defaultFunctions(toolProvider.getToolCallbacks())
            .build();
        
        return RouterFunctions.route()
            .POST("/chat", request -> 
                request.bodyToMono(String.class)
                    .map(message -> chatClient.prompt()
                        .user(message)
                        .call()
                        .content())
                    .flatMap(response -> 
                        ServerResponse.ok().bodyValue(response)))
            .build();
    }
}

@RestController
@RequestMapping("/api")
public class ReactiveAiController {

    private final AsyncMcpToolCallbackProvider toolProvider;
    private final ChatClient chatClient;

    public ReactiveAiController(ChatModel chatModel,
                               AsyncMcpToolCallbackProvider toolProvider) {
        this.toolProvider = toolProvider;
        this.chatClient = ChatClient.builder(chatModel)
            .defaultFunctions(toolProvider.getToolCallbacks())
            .build();
    }

    @PostMapping("/chat")
    public Mono<String> chat(@RequestBody Mono<String> messageMono) {
        return messageMono.flatMap(message -> 
            Mono.fromCallable(() -> chatClient.prompt()
                .user(message)
                .call()
                .content())
        );
    }

    @PostMapping("/refresh-tools")
    public Mono<Map<String, Object>> refreshTools() {
        return Mono.fromRunnable(() -> toolProvider.invalidateCache())
            .then(Mono.fromCallable(() -> {
                ToolCallback[] callbacks = toolProvider.getToolCallbacks();
                return Map.of(
                    "toolCount", callbacks.length,
                    "tools", Arrays.stream(callbacks)
                        .map(cb -> cb.getToolDefinition().name())
                        .toList()
                );
            }));
    }
}

Scenario 3: Event-Driven Tool Updates

Use Case: Microservice that dynamically updates tools when MCP servers change.

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.ai.mcp.McpToolsChangedEvent;

@Component
public class McpToolMonitor {

    private final ApplicationEventPublisher eventPublisher;
    private final List<McpSyncClient> mcpClients;
    private final Map<String, List<String>> lastKnownTools = new ConcurrentHashMap<>();

    @Autowired
    public McpToolMonitor(ApplicationEventPublisher eventPublisher,
                          List<McpSyncClient> mcpClients) {
        this.eventPublisher = eventPublisher;
        this.mcpClients = mcpClients;
    }

    // Check for tool changes every 60 seconds
    @Scheduled(fixedDelay = 60000)
    public void monitorToolChanges() {
        for (McpSyncClient client : mcpClients) {
            String clientName = client.getClientInfo().name();
            
            try {
                // Get current tools
                McpSchema.ListToolsResult result = client.listTools();
                List<String> currentToolNames = result.tools().stream()
                    .map(McpSchema.Tool::name)
                    .sorted()
                    .toList();
                
                // Check if changed
                List<String> previousTools = lastKnownTools.get(clientName);
                
                if (previousTools == null || !previousTools.equals(currentToolNames)) {
                    // Tools have changed - publish event
                    eventPublisher.publishEvent(
                        new McpToolsChangedEvent(clientName, result.tools())
                    );
                    
                    lastKnownTools.put(clientName, currentToolNames);
                    
                    System.out.println("Tools changed for " + clientName + 
                        ": " + currentToolNames.size() + " tools");
                }
            } catch (Exception e) {
                System.err.println("Error monitoring " + clientName + ": " + 
                    e.getMessage());
            }
        }
    }
}

@Component
public class ToolChangeHandler {

    private final MetricsService metrics;

    @EventListener
    public void handleToolsChanged(McpToolsChangedEvent event) {
        System.out.println("=== Tools Changed Event ===");
        System.out.println("Connection: " + event.getConnectionName());
        System.out.println("New tool count: " + event.getTools().size());
        
        // Log new tools
        for (McpSchema.Tool tool : event.getTools()) {
            System.out.println("  - " + tool.name() + ": " + tool.description());
        }
        
        // Update metrics
        metrics.recordToolCount(event.getConnectionName(), event.getTools().size());
        
        // Note: SyncMcpToolCallbackProvider automatically invalidates cache
        // No manual action needed - next getToolCallbacks() will refresh
    }
}

Scenario 4: Context Propagation for User Tracking

Use Case: Pass user context through to MCP tools for auditing and permissions.

import org.springframework.ai.mcp.ToolContextToMcpMetaConverter;
import org.springframework.ai.chat.model.ToolContext;
import java.util.Map;
import java.util.HashMap;

@Configuration
public class ContextAwareConfiguration {

    @Bean
    public ToolContextToMcpMetaConverter userContextConverter() {
        return toolContext -> {
            if (toolContext == null) return Map.of();
            
            Map<String, Object> metadata = new HashMap<>();
            Map<String, Object> context = toolContext.getContext();
            
            // Pass user identification
            if (context.containsKey("userId")) {
                metadata.put("user_id", context.get("userId"));
            }
            
            // Pass permissions
            if (context.containsKey("permissions")) {
                metadata.put("permissions", context.get("permissions"));
            }
            
            // Pass request tracking
            if (context.containsKey("requestId")) {
                metadata.put("request_id", context.get("requestId"));
            }
            
            if (context.containsKey("traceId")) {
                metadata.put("trace_id", context.get("traceId"));
            }
            
            // Add timestamp
            metadata.put("timestamp", System.currentTimeMillis());
            
            // Add environment
            metadata.put("environment", System.getenv("ENVIRONMENT"));
            
            return metadata;
        };
    }

    @Bean
    public SyncMcpToolCallbackProvider contextAwareProvider(
            List<McpSyncClient> mcpClients,
            ToolContextToMcpMetaConverter contextConverter) {
        
        return SyncMcpToolCallbackProvider.builder()
            .mcpClients(mcpClients)
            .toolContextToMcpMetaConverter(contextConverter)
            .build();
    }
}

@Service
public class ContextAwareChatService {

    private final ChatClient chatClient;

    @Autowired
    public ContextAwareChatService(ChatModel chatModel,
                                   SyncMcpToolCallbackProvider toolProvider) {
        this.chatClient = ChatClient.builder(chatModel)
            .defaultFunctions(toolProvider.getToolCallbacks())
            .build();
    }

    public String chatWithUserContext(String message, String userId, 
                                      List<String> permissions) {
        // Build context with user information
        Map<String, Object> context = Map.of(
            "userId", userId,
            "permissions", permissions,
            "requestId", UUID.randomUUID().toString(),
            "traceId", MDC.get("traceId")  // From logging MDC
        );
        
        return chatClient.prompt()
            .user(message)
            .toolContext(context)
            .call()
            .content();
    }
}

@RestController
@RequestMapping("/chat")
public class ContextAwareController {

    @Autowired
    private ContextAwareChatService chatService;

    @PostMapping
    public String chat(@RequestBody ChatRequest request,
                      @AuthenticationPrincipal UserDetails user) {
        
        // Extract user permissions
        List<String> permissions = user.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .toList();
        
        return chatService.chatWithUserContext(
            request.getMessage(),
            user.getUsername(),
            permissions
        );
    }
}

Scenario 5: Error Handling with Retry and Fallback

Use Case: Resilient tool execution with automatic retries and fallback strategies.

@Service
public class ResilientChatService {

    private final SyncMcpToolCallbackProvider toolProvider;
    private final ChatClient chatClient;
    private final FallbackService fallbackService;

    @Autowired
    public ResilientChatService(ChatModel chatModel,
                               SyncMcpToolCallbackProvider toolProvider,
                               FallbackService fallbackService) {
        this.toolProvider = toolProvider;
        this.fallbackService = fallbackService;
        this.chatClient = ChatClient.builder(chatModel)
            .defaultFunctions(toolProvider.getToolCallbacks())
            .build();
    }

    public String chatWithRetry(String message, int maxRetries) {
        for (int attempt = 0; attempt < maxRetries; attempt++) {
            try {
                return chatClient.prompt()
                    .user(message)
                    .call()
                    .content();
                    
            } catch (Exception e) {
                System.err.println("Attempt " + (attempt + 1) + " failed: " + 
                    e.getMessage());
                
                // Last attempt - use fallback
                if (attempt == maxRetries - 1) {
                    return fallbackService.getFallbackResponse(message);
                }
                
                // Wait before retry (exponential backoff)
                try {
                    Thread.sleep((long) Math.pow(2, attempt) * 1000);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Interrupted during retry", ie);
                }
                
                // Refresh tools before retry
                if (attempt > 0) {
                    toolProvider.invalidateCache();
                }
            }
        }
        
        throw new IllegalStateException("Should not reach here");
    }

    public String chatWithCircuitBreaker(String message) {
        CircuitBreaker breaker = CircuitBreakerRegistry.ofDefaults()
            .circuitBreaker("mcpChat");
        
        Supplier<String> chatSupplier = CircuitBreaker.decorateSupplier(
            breaker,
            () -> chatClient.prompt().user(message).call().content()
        );
        
        try {
            return chatSupplier.get();
        } catch (Exception e) {
            System.err.println("Circuit breaker open or call failed: " + 
                e.getMessage());
            return fallbackService.getFallbackResponse(message);
        }
    }
}

@Service
public class FallbackService {

    private final ChatModel chatModel;

    public String getFallbackResponse(String message) {
        // Use ChatModel without tools as fallback
        ChatClient simpleChatClient = ChatClient.builder(chatModel).build();
        
        return simpleChatClient.prompt()
            .user("Without using any external tools, please respond to: " + message)
            .call()
            .content();
    }
}

Scenario 6: Native Image Deployment

Use Case: Deploy Spring AI MCP application as GraalVM native image.

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-mcp</artifactId>
        <version>1.1.2</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <configuration>
                <buildArgs>
                    <arg>--initialize-at-build-time=org.springframework.ai.mcp</arg>
                </buildArgs>
            </configuration>
        </plugin>
    </plugins>
</build>
@SpringBootApplication
@ImportRuntimeHints(McpHints.class)
public class NativeMcpApplication {

    public static void main(String[] args) {
        SpringApplication.run(NativeMcpApplication.class, args);
    }

    @Bean
    public SyncMcpToolCallbackProvider nativeToolProvider(
            List<McpSyncClient> mcpClients) {
        // Works seamlessly in native image
        return SyncMcpToolCallbackProvider.builder()
            .mcpClients(mcpClients)
            .build();
    }
}

Build and run:

# Build native image
mvn -Pnative native:compile

# Run native executable
./target/native-mcp-app

# Fast startup (< 100ms) and low memory footprint

Scenario 7: Exposing Spring Functions as MCP Server

Use Case: Create an MCP server that exposes Spring AI functions to other applications.

import org.springframework.ai.mcp.McpToolUtils;
import org.springframework.ai.tool.ToolCallback;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;

@Configuration
public class McpServerConfiguration {

    @Bean
    public ToolCallback weatherToolCallback() {
        return ToolCallback.builder()
            .name("get_weather")
            .description("Get current weather for a location")
            .inputSchema(/* JSON schema */)
            .function((input) -> {
                // Implement weather lookup
                return fetchWeather(input);
            })
            .build();
    }

    @Bean
    public ToolCallback databaseToolCallback() {
        return ToolCallback.builder()
            .name("query_database")
            .description("Query the database")
            .inputSchema(/* JSON schema */)
            .function((input) -> {
                // Implement database query
                return queryDatabase(input);
            })
            .build();
    }

    @Bean
    public McpSyncServer springAiMcpServer(List<ToolCallback> allCallbacks) {
        // Convert Spring AI callbacks to MCP specifications
        List<McpServerFeatures.SyncToolSpecification> toolSpecs =
            McpToolUtils.toSyncToolSpecification(allCallbacks);
        
        // Create MCP server
        return McpServer.sync()
            .serverInfo(McpSchema.Implementation.builder()
                .name("spring-ai-server")
                .version("1.0.0")
                .build())
            .capabilities(McpSchema.ServerCapabilities.builder()
                .tools(McpSchema.ServerCapabilities.Tools.builder()
                    .listChanged(true)
                    .build())
                .build())
            .tools(toolSpecs)
            .build();
    }

    @Bean
    public RouterFunction<ServerResponse> mcpServerRoutes(
            McpSyncServer springAiMcpServer) {
        // Expose via HTTP
        return McpSpringWebMvc.createRouterFunction("/mcp", springAiMcpServer);
    }
}

Additional Resources

  • Edge Cases - Handle advanced scenarios
  • Architecture Reference - Design patterns
  • Error Handling Guide - Comprehensive error strategies

Install with Tessl CLI

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

docs

examples

edge-cases.md

real-world-scenarios.md

index.md

tile.json