Spring Framework integration for Model Context Protocol (MCP), providing Spring AI function calling capabilities and Spring-friendly abstractions for MCP clients and MCP servers
This document provides complete, production-ready examples of using Spring AI MCP in real applications.
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();
}
}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()
);
}));
}
}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
}
}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
);
}
}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();
}
}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 footprintUse 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);
}
}