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
Guide to using the MCP client beans provided by the auto-configuration for direct interaction with MCP servers.
The Spring AI MCP Client WebFlux starter automatically creates MCP client beans that you can inject into your Spring components. These clients provide direct access to MCP protocol operations like listing tools, executing tools, accessing resources, and using prompts.
There are two types of clients:
Configure which type of client to create using the spring.ai.mcp.client.type property:
spring.ai.mcp.client:
type: SYNC # or ASYNC (default: SYNC)Mutually Exclusive: Only one client type is created per application (either all SYNC or all ASYNC).
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Auto-configuration for MCP client support.
* Creates either synchronous or asynchronous MCP clients based on configuration.
* Conditionally enabled based on class presence and properties.
* Thread-safe - clients are created during Spring context initialization.
*/
@org.springframework.boot.autoconfigure.AutoConfiguration
@org.springframework.boot.autoconfigure.condition.ConditionalOnClass(io.modelcontextprotocol.spec.McpSchema.class)
@org.springframework.boot.context.properties.EnableConfigurationProperties(
org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties.class
)
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "enabled",
havingValue = "true",
matchIfMissing = true
)
public class McpClientAutoConfiguration {
}package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Creates a list of synchronous MCP clients, one per configured transport.
* Created only when spring.ai.mcp.client.type=SYNC (default).
* Thread-safe for concurrent use - all operations are synchronized internally.
*
* @param mcpSyncClientConfigurer Configurer for customizing client creation
* @param commonProperties Common MCP client properties (timeout, name, etc.)
* @param transportsProvider Provider of named MCP transports (SSE, stdio, streamable HTTP)
* @param clientMcpSyncHandlersRegistry Optional registry for annotation-based handlers
* @return List of configured synchronous MCP clients (never null, may be empty)
* @throws IllegalStateException if client creation fails
*/
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "type",
havingValue = "SYNC",
matchIfMissing = true
)
public java.util.List<io.modelcontextprotocol.client.McpSyncClient> mcpSyncClients(
org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpSyncClientConfigurer mcpSyncClientConfigurer,
org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties commonProperties,
org.springframework.beans.factory.ObjectProvider<java.util.List<NamedClientMcpTransport>> transportsProvider,
org.springframework.beans.factory.ObjectProvider<org.springframework.ai.mcp.annotation.spring.ClientMcpSyncHandlersRegistry> clientMcpSyncHandlersRegistry
);import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* Service demonstrating synchronous MCP client usage.
* All operations block until complete - suitable for traditional code.
* Thread-safe - multiple threads can safely use these clients.
*/
@Service
public class McpToolService {
private final List<McpSyncClient> mcpClients;
/**
* Constructor injection of MCP clients.
* Clients are already initialized and ready to use (if initialized=true in config).
*
* @param mcpClients Auto-configured MCP sync clients
*/
public McpToolService(List<McpSyncClient> mcpClients) {
this.mcpClients = mcpClients;
}
/**
* List all available tools from all connected servers.
* Blocks until all clients respond.
* Thread-safe.
*
* @throws io.modelcontextprotocol.spec.McpException if MCP protocol error occurs
* @throws java.util.concurrent.TimeoutException if request times out
* @throws java.io.IOException if transport error occurs
*/
public void listToolsFromAllServers() {
for (McpSyncClient client : mcpClients) {
// Get client info (local, doesn't require server call)
McpSchema.Implementation clientInfo = client.getClientInfo();
System.out.println("Client: " + clientInfo.name() + " v" + clientInfo.version());
// Get server initialization result for server info (cached, no server call)
McpSchema.InitializeResult initResult = client.getCurrentInitializationResult();
System.out.println("Server: " + initResult.serverInfo().name());
// List available tools (makes server call, may block)
List<McpSchema.Tool> tools = client.listTools();
tools.forEach(tool ->
System.out.println(" Tool: " + tool.name() + " - " +
(tool.description() != null ? tool.description() : "No description"))
);
}
}
/**
* Execute a tool with given arguments.
* Finds first client that has the tool and executes it.
* Blocks until execution completes.
*
* @param toolName Name of tool to execute
* @param arguments Tool arguments as map
* @return Tool execution result content
* @throws IllegalArgumentException if tool not found
*/
public Object executeTool(String toolName, Map<String, Object> arguments) {
// Find the first client that has the specified tool
for (McpSyncClient client : mcpClients) {
List<McpSchema.Tool> tools = client.listTools();
boolean hasTool = tools.stream()
.anyMatch(t -> t.name().equals(toolName));
if (hasTool) {
// Build call tool request
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
.name(toolName)
.arguments(arguments)
.build();
// Execute tool (blocks until complete)
McpSchema.CallToolResult result = client.callTool(request);
// Return result content (may be text, image, or embedded resource)
return result.content();
}
}
throw new IllegalArgumentException("Tool not found: " + toolName);
}
/**
* List all available resources from first client.
* Resources are server-provided data (files, data, etc.).
*
* @return List of resources
*/
public List<McpSchema.Resource> listResources() {
if (!mcpClients.isEmpty()) {
return mcpClients.get(0).listResources();
}
return List.of();
}
/**
* Read resource content from server.
*
* @param uri Resource URI to read
* @return Resource content
*/
public McpSchema.ReadResourceResult readResource(String uri) {
if (!mcpClients.isEmpty()) {
McpSchema.ReadResourceRequest request = McpSchema.ReadResourceRequest.builder()
.uri(uri)
.build();
return mcpClients.get(0).readResource(request);
}
throw new IllegalStateException("No MCP clients available");
}
/**
* List all available prompts from first client.
* Prompts are server-provided prompt templates.
*
* @return List of prompts
*/
public List<McpSchema.Prompt> listPrompts() {
if (!mcpClients.isEmpty()) {
return mcpClients.get(0).listPrompts();
}
return List.of();
}
}The auto-configuration also creates a CloseableMcpSyncClients wrapper bean that implements AutoCloseable to ensure proper cleanup of MCP client resources when the application context is closed.
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Creates a closeable wrapper for synchronous MCP clients.
* This bean ensures all MCP clients are properly closed when the Spring
* application context shuts down, preventing resource leaks.
* Automatic - you don't need to close clients manually.
*
* @param clients List of MCP sync clients to wrap
* @return Closeable wrapper for MCP sync clients
*/
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "type",
havingValue = "SYNC",
matchIfMissing = true
)
public McpClientAutoConfiguration.CloseableMcpSyncClients makeSyncClientsClosable(
java.util.List<io.modelcontextprotocol.client.McpSyncClient> clients
);CloseableMcpSyncClients Record:
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Closeable wrapper for MCP sync clients.
* Implements AutoCloseable to ensure proper resource cleanup.
* Registered as Spring bean with destroy method.
* Thread-safe - close is idempotent and synchronized.
*/
public record CloseableMcpSyncClients(
java.util.List<io.modelcontextprotocol.client.McpSyncClient> clients
) implements AutoCloseable {
/**
* Closes all wrapped MCP sync clients, releasing their resources.
* Called automatically by Spring during context shutdown.
* Thread-safe and idempotent - safe to call multiple times.
* Closes clients in parallel for faster shutdown.
*
* @throws Exception if any client fails to close (logged but not propagated)
*/
@Override
public void close();
}This wrapper is automatically managed by Spring's lifecycle and will close all MCP clients when the application context is destroyed.
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Creates a list of asynchronous MCP clients, one per configured transport.
* Created only when spring.ai.mcp.client.type=ASYNC.
* Uses reactive streams (Project Reactor) for non-blocking operations.
* Thread-safe - operations are non-blocking and use event loops.
*
* @param mcpAsyncClientConfigurer Configurer for customizing client creation
* @param commonProperties Common MCP client properties (timeout, name, etc.)
* @param transportsProvider Provider of named MCP transports (SSE, stdio, streamable HTTP)
* @param clientMcpAsyncHandlersRegistry Optional registry for annotation-based handlers
* @return List of configured asynchronous MCP clients (never null, may be empty)
* @throws IllegalStateException if client creation fails
*/
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "type",
havingValue = "ASYNC"
)
public java.util.List<io.modelcontextprotocol.client.McpAsyncClient> mcpAsyncClients(
org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpAsyncClientConfigurer mcpAsyncClientConfigurer,
org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties commonProperties,
org.springframework.beans.factory.ObjectProvider<java.util.List<NamedClientMcpTransport>> transportsProvider,
org.springframework.beans.factory.ObjectProvider<org.springframework.ai.mcp.annotation.spring.ClientMcpAsyncHandlersRegistry> clientMcpAsyncHandlersRegistry
);import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
/**
* Service demonstrating asynchronous MCP client usage.
* All operations are non-blocking and return reactive types.
* High throughput for concurrent operations.
*/
@Service
public class McpAsyncToolService {
private final List<McpAsyncClient> mcpClients;
/**
* Constructor injection of MCP async clients.
* Clients use reactive streams for non-blocking operations.
*
* @param mcpClients Auto-configured MCP async clients
*/
public McpAsyncToolService(List<McpAsyncClient> mcpClients) {
this.mcpClients = mcpClients;
}
/**
* List all tools from all clients reactively.
* Non-blocking - returns immediately with a Flux.
* Operations execute in parallel on event loops.
*
* @return Flux of all tools from all clients
*/
public Flux<McpSchema.Tool> listAllTools() {
// List tools from all clients reactively
return Flux.fromIterable(mcpClients)
.flatMap(client -> client.listTools().flatMapMany(Flux::fromIterable));
}
/**
* Execute tool on the first client that has it.
* Non-blocking - returns Mono that completes when tool execution finishes.
*
* @param toolName Tool name to execute
* @param arguments Tool arguments
* @return Mono with tool execution result
*/
public Mono<Object> executeTool(String toolName, Map<String, Object> arguments) {
// Execute tool on the first client that has it
return Flux.fromIterable(mcpClients)
.flatMap(client ->
client.listTools()
.flatMapMany(Flux::fromIterable)
.filter(tool -> tool.name().equals(toolName))
.next()
.flatMap(tool -> {
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
.name(toolName)
.arguments(arguments)
.build();
return client.callTool(request)
.map(McpSchema.CallToolResult::content);
})
)
.next()
.switchIfEmpty(Mono.error(
new IllegalArgumentException("Tool not found: " + toolName)
));
}
/**
* List all resources from all clients.
* Non-blocking with parallel execution.
*
* @return Flux of all resources
*/
public Flux<McpSchema.Resource> listAllResources() {
return Flux.fromIterable(mcpClients)
.flatMap(client -> client.listResources().flatMapMany(Flux::fromIterable));
}
/**
* Read resource from first available client.
*
* @param uri Resource URI
* @return Mono with resource content
*/
public Mono<McpSchema.ReadResourceResult> readResource(String uri) {
if (!mcpClients.isEmpty()) {
McpSchema.ReadResourceRequest request = McpSchema.ReadResourceRequest.builder()
.uri(uri)
.build();
return mcpClients.get(0).readResource(request);
}
return Mono.error(new IllegalStateException("No MCP clients available"));
}
/**
* Execute multiple tools in parallel.
* Demonstrates parallel non-blocking execution.
*
* @param toolRequests Map of tool names to arguments
* @return Flux of execution results
*/
public Flux<ToolResult> executeToolsInParallel(Map<String, Map<String, Object>> toolRequests) {
return Flux.fromIterable(toolRequests.entrySet())
.flatMap(entry ->
executeTool(entry.getKey(), entry.getValue())
.map(result -> new ToolResult(entry.getKey(), result))
.onErrorResume(error ->
Mono.just(new ToolResult(entry.getKey(), error))
)
);
}
/**
* Result of tool execution (success or error).
*/
public record ToolResult(String toolName, Object result) {}
}The auto-configuration also creates a CloseableMcpAsyncClients wrapper bean that implements AutoCloseable to ensure proper cleanup of MCP client resources when the application context is closed.
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Creates a closeable wrapper for asynchronous MCP clients.
* This bean ensures all MCP clients are properly closed when the Spring
* application context shuts down, preventing resource leaks.
* Automatic - you don't need to close clients manually.
*
* @param clients List of MCP async clients to wrap
* @return Closeable wrapper for MCP async clients
*/
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "type",
havingValue = "ASYNC"
)
public McpClientAutoConfiguration.CloseableMcpAsyncClients makeAsyncClientsClosable(
java.util.List<io.modelcontextprotocol.client.McpAsyncClient> clients
);CloseableMcpAsyncClients Record:
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Closeable wrapper for MCP async clients.
* Implements AutoCloseable to ensure proper resource cleanup.
* Registered as Spring bean with destroy method.
* Thread-safe - close is idempotent and uses reactive coordination.
*/
public record CloseableMcpAsyncClients(
java.util.List<io.modelcontextprotocol.client.McpAsyncClient> clients
) implements AutoCloseable {
/**
* Closes all wrapped MCP async clients, releasing their resources.
* Called automatically by Spring during context shutdown.
* Thread-safe and idempotent - safe to call multiple times.
* Closes clients reactively for proper resource cleanup.
*
* @throws Exception if any client fails to close (logged but not propagated)
*/
@Override
public void close();
}This wrapper is automatically managed by Spring's lifecycle and will close all MCP clients when the application context is destroyed.
Both sync and async clients support the same MCP protocol operations:
// Get client information (Sync) - local operation, no server call
McpSchema.Implementation clientInfo = syncClient.getClientInfo();
System.out.println("Client: " + clientInfo.name() + " v" + clientInfo.version());
// Get server initialization result including server info (Sync) - cached, no server call
McpSchema.InitializeResult initResult = syncClient.getCurrentInitializationResult();
McpSchema.ServerInfo serverInfo = initResult.serverInfo();
System.out.println("Server: " + serverInfo.name() + " v" + serverInfo.version());
System.out.println("Protocol version: " + initResult.protocolVersion());
// Access server capabilities
McpSchema.ServerCapabilities serverCaps = initResult.capabilities();
System.out.println("Supports tools: " + (serverCaps.tools() != null));
System.out.println("Supports resources: " + (serverCaps.resources() != null));
System.out.println("Supports prompts: " + (serverCaps.prompts() != null));
// Async - client info (local operation)
McpSchema.Implementation asyncClientInfo = asyncClient.getClientInfo();
// Async - server initialization result (cached)
McpSchema.InitializeResult asyncInitResult = asyncClient.getCurrentInitializationResult();// List tools (Sync) - makes server call, may block
List<McpSchema.Tool> tools = syncClient.listTools();
tools.forEach(tool -> {
System.out.println("Tool: " + tool.name());
System.out.println(" Description: " + tool.description());
System.out.println(" Input schema: " + tool.inputSchema());
});
// List tools (Async) - non-blocking, returns Mono
Mono<List<McpSchema.Tool>> toolsMono = asyncClient.listTools();
// Call a tool (Sync) - blocks until execution completes
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
.name("toolName")
.arguments(Map.of("param1", "value1", "param2", "value2"))
.build();
McpSchema.CallToolResult result = syncClient.callTool(request);
// Access result content (may be various types)
Object content = result.content();
boolean isError = result.isError();
// Call a tool (Async) - non-blocking
Mono<McpSchema.CallToolResult> resultMono = asyncClient.callTool(request);// List resources (Sync) - makes server call
List<McpSchema.Resource> resources = syncClient.listResources();
resources.forEach(resource -> {
System.out.println("Resource: " + resource.uri());
System.out.println(" Name: " + resource.name());
System.out.println(" MIME type: " + resource.mimeType());
System.out.println(" Description: " + resource.description());
});
// List resources (Async)
Mono<List<McpSchema.Resource>> resourcesMono = asyncClient.listResources();
// Read a resource (Sync) - blocks until content is retrieved
McpSchema.ReadResourceRequest request = McpSchema.ReadResourceRequest.builder()
.uri("file:///path/to/resource")
.build();
McpSchema.ReadResourceResult result = syncClient.readResource(request);
// Access resource content
Object contents = result.contents(); // May be text, blob, or other types
// Read a resource (Async)
Mono<McpSchema.ReadResourceResult> resultMono = asyncClient.readResource(request);// List prompts (Sync)
List<McpSchema.Prompt> prompts = syncClient.listPrompts();
prompts.forEach(prompt -> {
System.out.println("Prompt: " + prompt.name());
System.out.println(" Description: " + prompt.description());
prompt.arguments().forEach(arg -> {
System.out.println(" Argument: " + arg.name() +
(arg.required() ? " (required)" : " (optional)"));
});
});
// List prompts (Async)
Mono<List<McpSchema.Prompt>> promptsMono = asyncClient.listPrompts();
// Get a prompt (Sync)
McpSchema.GetPromptRequest request = McpSchema.GetPromptRequest.builder()
.name("promptName")
.arguments(Map.of("arg", "value"))
.build();
McpSchema.GetPromptResult result = syncClient.getPrompt(request);
// Access prompt messages
List<McpSchema.PromptMessage> messages = result.messages();
// Get a prompt (Async)
Mono<McpSchema.GetPromptResult> resultMono = asyncClient.getPrompt(request);// Request sampling (LLM completion from server) - Sync
McpSchema.CreateMessageRequest request = McpSchema.CreateMessageRequest.builder()
.messages(List.of(McpSchema.SamplingMessage.builder()
.role(McpSchema.Role.USER)
.content(McpSchema.TextContent.builder()
.text("Complete this: Hello,")
.build())
.build()))
.metadata(Map.of())
.build();
McpSchema.CreateMessageResult result = syncClient.createMessage(request);
// Access result
McpSchema.Role responseRole = result.role();
Object responseContent = result.content();
McpSchema.StopReason stopReason = result.stopReason();
// Request sampling (Async)
Mono<McpSchema.CreateMessageResult> resultMono = asyncClient.createMessage(request);By default, clients are automatically initialized when created. You can control this with:
spring.ai.mcp.client:
initialized: true # default, auto-initialize on creationIf set to false, you must manually initialize clients:
// Sync client - check if initialized
McpSyncClient client = mcpClients.get(0);
if (!client.isInitialized()) {
client.initialize(); // Blocks until initialization completes
}
// Async client - reactive initialization
McpAsyncClient asyncClient = mcpAsyncClients.get(0);
asyncClient.initialize().block(); // Block for initialization
// OR subscribe for non-blocking
asyncClient.initialize()
.doOnSuccess(v -> log.info("Client initialized"))
.doOnError(e -> log.error("Initialization failed", e))
.subscribe();Initialization Process:
Clients are automatically closed when the Spring application context shuts down. The auto-configuration creates closeable wrapper beans that handle cleanup:
package org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration;
/**
* Wrapper that ensures proper cleanup of MCP sync clients.
* Implements AutoCloseable for automatic resource management.
* Thread-safe.
*/
public record CloseableMcpSyncClients(
java.util.List<io.modelcontextprotocol.client.McpSyncClient> clients
) implements AutoCloseable {
/**
* Close all clients, releasing resources.
* Idempotent - safe to call multiple times.
* Logs errors but doesn't propagate them.
*/
@Override
public void close();
}
/**
* Wrapper that ensures proper cleanup of MCP async clients.
* Implements AutoCloseable for automatic resource management.
* Thread-safe with reactive coordination.
*/
public record CloseableMcpAsyncClients(
java.util.List<io.modelcontextprotocol.client.McpAsyncClient> clients
) implements AutoCloseable {
/**
* Close all clients, releasing resources.
* Idempotent - safe to call multiple times.
* Uses reactive close for proper cleanup.
*/
@Override
public void close();
}You don't need to manually close clients - Spring handles this automatically.
Lifecycle Phases:
initialized=true)Each client corresponds to a configured transport connection. To identify which client connects to which server, use the client info:
for (McpSyncClient client : mcpClients) {
// Get client information
McpSchema.Implementation clientInfo = client.getClientInfo();
// Get server information
McpSchema.InitializeResult initResult = client.getCurrentInitializationResult();
McpSchema.ServerInfo serverInfo = initResult.serverInfo();
// Client and server names help identify connections
System.out.println("Client: " + clientInfo.name());
System.out.println("Server: " + serverInfo.name());
System.out.println("Protocol: " + initResult.protocolVersion());
}The client name follows the pattern: {configured-client-name} - {connection-name}
For example, with this configuration:
spring.ai.mcp.client:
name: my-app
sse:
connections:
weather-api:
url: http://localhost:8080The client name will be: my-app - weather-api
Common client configuration properties:
spring.ai.mcp.client:
enabled: true # Enable MCP client (default: true)
type: SYNC # Client type: SYNC or ASYNC (default: SYNC)
name: my-app # Client name (default: "spring-ai-mcp-client")
version: 1.0.0 # Client version (default: "1.0.0")
initialized: true # Auto-initialize (default: true)
request-timeout: 20s # Request timeout (default: 20 seconds)
root-change-notification: true # Enable root change notifications (default: true)See Configuration Properties for complete details.
import io.modelcontextprotocol.spec.McpException;
import java.util.concurrent.TimeoutException;
import java.io.IOException;
try {
McpSchema.CallToolResult result = client.callTool(request);
// Process result
} catch (McpException e) {
// MCP protocol errors (e.g., server error, invalid request)
log.error("MCP error: code={}, message={}", e.getCode(), e.getMessage(), e);
} catch (TimeoutException e) {
// Request timeout
log.error("Request timed out after {} seconds",
client.getRequestTimeout().getSeconds(), e);
} catch (IOException e) {
// Transport error (e.g., connection lost)
log.error("Transport error: {}", e.getMessage(), e);
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
// JSON serialization/deserialization error
log.error("JSON processing error: {}", e.getMessage(), e);
} catch (IllegalStateException e) {
// Client not initialized or connection closed
log.error("Client state error: {}", e.getMessage(), e);
} catch (Exception e) {
// Unexpected error
log.error("Unexpected error: {}", e.getMessage(), e);
}asyncClient.callTool(request)
.doOnNext(result -> {
// Process result
log.info("Tool executed successfully");
})
.onErrorResume(McpException.class, e -> {
// Handle MCP protocol errors
log.error("MCP error: {}", e.getMessage(), e);
return Mono.empty(); // or provide fallback
})
.onErrorResume(TimeoutException.class, e -> {
// Handle timeout
log.error("Request timed out", e);
return Mono.empty();
})
.onErrorResume(IOException.class, e -> {
// Handle transport errors
log.error("Transport error", e);
return Mono.empty();
})
.onErrorResume(e -> {
// Handle other errors
log.error("Unexpected error", e);
return Mono.empty();
})
.subscribe();request-timeout based on expected operation durationinitialized=false)Don't Block in Async Context:
// BAD - blocks reactive chain
Mono<String> result = asyncClient.listTools()
.map(tools -> {
Thread.sleep(1000); // DON'T DO THIS!
return processTools(tools);
});
// GOOD - use reactive operators
Mono<String> result = asyncClient.listTools()
.delayElement(Duration.ofSeconds(1))
.map(this::processTools);Don't Ignore Errors:
// BAD - error silently lost
asyncClient.callTool(request).subscribe();
// GOOD - handle errors
asyncClient.callTool(request)
.doOnError(e -> log.error("Tool execution failed", e))
.subscribe();Don't Create Clients Manually:
// BAD - bypasses Spring lifecycle management
McpSyncClient client = McpClient.sync(transport)...build();
// GOOD - inject auto-configured clients
@Service
public class MyService {
private final List<McpSyncClient> clients;
public MyService(List<McpSyncClient> clients) {
this.clients = clients;
}
}tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux@1.1.0