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 customizing MCP client behavior using the customizer interfaces.
The MCP client auto-configuration provides extension points through customizer interfaces. Implement these interfaces to modify client specifications before they're built and initialized.
package org.springframework.ai.mcp.customizer;
/**
* Interface for customizing synchronous MCP client configurations.
*
* Implement this interface and register as a Spring bean to customize
* sync client behavior before initialization.
*
* Functional interface - can be implemented as lambda or method reference.
* Customizers are applied in order determined by @Order annotation.
* Thread-safe - called once during client creation on single thread.
*
* @see McpAsyncClientCustomizer for async client customization
*/
@FunctionalInterface
public interface McpSyncClientCustomizer {
/**
* Customize the MCP sync client specification.
* Called during client creation before client is built and initialized.
* Modify the spec to configure client behavior.
*
* @param name The connection name for this client (never null)
* @param spec The client specification to customize (never null, mutable builder)
*/
void customize(String name, io.modelcontextprotocol.client.McpClient.SyncSpec spec);
}package org.springframework.ai.mcp.customizer;
/**
* Interface for customizing asynchronous MCP client configurations.
*
* Implement this interface and register as a Spring bean to customize
* async client behavior before initialization.
*
* Functional interface - can be implemented as lambda or method reference.
* Customizers are applied in order determined by @Order annotation.
* Thread-safe - called once during client creation on single thread.
*
* @see McpSyncClientCustomizer for sync client customization
*/
@FunctionalInterface
public interface McpAsyncClientCustomizer {
/**
* Customize the MCP async client specification.
* Called during client creation before client is built and initialized.
* Modify the spec to configure client behavior.
*
* @param name The connection name for this client (never null)
* @param spec The client specification to customize (never null, mutable builder)
*/
void customize(String name, io.modelcontextprotocol.client.McpClient.AsyncSpec spec);
}import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Configuration for custom MCP logging.
* Adds logging consumer to all MCP sync clients.
*/
@Configuration
public class McpCustomizerConfig {
private static final Logger log = LoggerFactory.getLogger(McpCustomizerConfig.class);
/**
* Customizer that adds logging consumer to MCP clients.
* Logs all MCP protocol log messages from servers.
* Thread-safe - LoggerFactory provides thread-safe loggers.
*
* @return Logging customizer bean
*/
@Bean
public McpSyncClientCustomizer loggingCustomizer() {
return (String name, McpClient.SyncSpec spec) -> {
spec.loggingConsumer(notification -> {
String level = notification.level();
Object data = notification.data();
// Map MCP log levels to SLF4J
switch (level.toLowerCase()) {
case "debug" -> log.debug("[{}] {}", name, data);
case "info" -> log.info("[{}] {}", name, data);
case "warning" -> log.warn("[{}] {}", name, data);
case "error" -> log.error("[{}] {}", name, data);
case "critical" -> log.error("[{}] CRITICAL: {}", name, data);
default -> log.info("[{}] [{}] {}", name, level, data);
}
});
};
}
}import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration for customizing MCP client capabilities.
* Sets specific capabilities that the client advertises to servers.
*/
@Configuration
public class CapabilitiesCustomizer {
/**
* Customizer that configures client capabilities.
* Declares what features this client supports.
* Servers use this information to determine which operations they can perform.
*
* @return Capabilities customizer bean
*/
@Bean
public McpSyncClientCustomizer capabilitiesCustomizer() {
return (String name, McpClient.SyncSpec spec) -> {
// Customize client capabilities
// Enable roots capability for notifying server of root changes
McpSchema.ClientCapabilities.RootsCapability roots =
new McpSchema.ClientCapabilities.RootsCapability(
true // listChanged - notify server when roots change
);
// Enable sampling capability for LLM sampling requests
McpSchema.ClientCapabilities.SamplingCapability sampling =
new McpSchema.ClientCapabilities.SamplingCapability();
// Create full capabilities object
McpSchema.ClientCapabilities capabilities =
new McpSchema.ClientCapabilities(
roots,
sampling,
null // experimental - no experimental features
);
spec.capabilities(capabilities);
log.info("Configured capabilities for client: {}", name);
};
}
}import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import io.modelcontextprotocol.client.McpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
/**
* Configuration for connection-specific customizations.
* Different settings for different MCP server connections.
*/
@Configuration
public class ConnectionSpecificCustomizer {
/**
* Customizer that applies different settings based on connection name.
* Allows per-connection timeout configuration.
* Thread-safe - no shared mutable state.
*
* @return Connection-specific customizer bean
*/
@Bean
public McpSyncClientCustomizer timeoutCustomizer() {
return (String name, McpClient.SyncSpec spec) -> {
// Different timeouts for different servers
Duration timeout = switch (name) {
case "slow-server" -> {
log.info("Using extended timeout for slow-server");
yield Duration.ofMinutes(2);
}
case "fast-server" -> {
log.info("Using short timeout for fast-server");
yield Duration.ofSeconds(5);
}
case "reliable-server" -> {
log.info("Using very short timeout for reliable-server");
yield Duration.ofSeconds(3);
}
default -> {
log.info("Using default timeout for {}", name);
yield Duration.ofSeconds(30);
}
};
spec.requestTimeout(timeout);
};
}
}import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Configuration for progress tracking across all async clients.
* Maintains progress state for long-running operations.
*/
@Configuration
public class ProgressCustomizer {
/**
* Thread-safe map to track progress per connection and operation.
* Key format: "{connectionName}:{progressToken}"
*/
private final Map<String, ProgressInfo> progressMap = new ConcurrentHashMap<>();
/**
* Customizer that adds progress tracking to async clients.
* Records progress for all operations reporting progress.
* Thread-safe with concurrent map.
*
* @return Progress tracking customizer bean
*/
@Bean
public McpAsyncClientCustomizer progressCustomizer() {
return (String name, McpClient.AsyncSpec spec) -> {
spec.progressConsumer(notification -> {
long progress = notification.progress();
long total = notification.total();
String token = notification.progressToken();
String key = name + ":" + (token != null ? token : "default");
progressMap.put(key, new ProgressInfo(progress, total, System.currentTimeMillis()));
double percentage = (progress * 100.0) / total;
System.out.printf("[%s] Progress: %.2f%% (%d/%d)%n",
name, percentage, progress, total);
// Clean up completed operations
if (progress >= total) {
progressMap.remove(key);
}
});
};
}
/**
* Get current progress for a connection and operation.
*
* @param connectionName Connection name
* @param token Progress token (null for default)
* @return Progress info, or null if not found
*/
public ProgressInfo getProgress(String connectionName, String token) {
String key = connectionName + ":" + (token != null ? token : "default");
return progressMap.get(key);
}
/**
* Record of progress information.
*
* @param current Current progress value
* @param total Total progress value
* @param timestamp When progress was last updated
*/
public record ProgressInfo(long current, long total, long timestamp) {
public double percentage() {
return (current * 100.0) / total;
}
}
}import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.stream.Collectors;
/**
* Configuration for custom sampling (LLM completion) handling.
* Integrates MCP sampling requests with Spring AI chat client.
*/
@Configuration
public class SamplingCustomizer {
private final ChatClient chatClient;
/**
* Constructor injection of chat client.
*
* @param chatClientBuilder Spring AI chat client builder
*/
public SamplingCustomizer(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
/**
* Customizer that adds sampling handler using Spring AI.
* Handles LLM completion requests from MCP servers.
* Thread-safe - ChatClient is thread-safe.
*
* @return Sampling customizer bean
*/
@Bean
public McpSyncClientCustomizer samplingCustomizer() {
return (String name, McpClient.SyncSpec spec) -> {
spec.sampling(samplingRequest -> {
// Log sampling request
log.info("Sampling request from {}: {} messages",
name, samplingRequest.messages().size());
// Convert MCP messages to Spring AI prompt
String prompt = samplingRequest.messages().stream()
.map(this::messageToText)
.collect(Collectors.joining("\n"));
// Get completion from Spring AI
String completion = chatClient.prompt()
.user(prompt)
.call()
.content();
// Convert to MCP result format
return new McpSchema.CreateMessageResult(
McpSchema.Role.ASSISTANT,
new McpSchema.TextContent(completion),
null, // model (optional)
McpSchema.StopReason.END_TURN
);
});
log.info("Configured sampling handler for client: {}", name);
};
}
/**
* Convert MCP message to text for prompt.
*
* @param message MCP sampling message
* @return Text content
*/
private String messageToText(McpSchema.SamplingMessage message) {
if (message.content() instanceof McpSchema.TextContent textContent) {
return textContent.text();
} else if (message.content() instanceof McpSchema.ImageContent imageContent) {
return "[Image: " + imageContent.mimeType() + "]";
}
return "[Unknown content type]";
}
}import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
import io.modelcontextprotocol.client.McpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* Configuration for elicitation (information extraction) handling.
* Handles requests for information from MCP servers.
*/
@Configuration
public class ElicitationCustomizer {
/**
* Customizer that adds elicitation handler to async clients.
* Handles information extraction requests from servers.
* Returns reactive Mono for non-blocking operation.
*
* @return Elicitation customizer bean
*/
@Bean
public McpAsyncClientCustomizer elicitationCustomizer() {
return (String name, McpClient.AsyncSpec spec) -> {
spec.elicitationFunction(elicitationRequest -> {
// Extract prompt and context
String prompt = elicitationRequest.prompt();
Map<String, Object> context = elicitationRequest.context();
log.info("Elicitation request from {}: {}", name, prompt);
// Process elicitation request
return Mono.fromCallable(() -> {
// Implement your elicitation logic here
// This could query a database, call an API, etc.
String result = processElicitation(prompt, context);
return result;
});
});
};
}
/**
* Process elicitation request.
* Override this method with actual implementation.
*
* @param prompt Elicitation prompt
* @param context Additional context
* @return Elicited information
*/
private String processElicitation(String prompt, Map<String, Object> context) {
// Placeholder implementation
return "Elicited information for: " + prompt;
}
}The auto-configuration uses configurer classes to apply all customizers:
package org.springframework.ai.mcp.client.common.autoconfigure.configurer;
/**
* Configurer that applies all registered McpSyncClientCustomizer instances
* to client specifications.
*
* Created as a Spring bean by auto-configuration.
* Thread-safe - customizers are applied sequentially during single-threaded client creation.
* Immutable after construction.
*/
public class McpSyncClientConfigurer {
/**
* Apply all customizers to the specification.
* Customizers are applied in order determined by @Order annotation.
* Lower order values are applied first.
*
* @param name The connection name (never null)
* @param spec The specification to configure (never null, mutable)
* @return The configured specification (same instance, modified)
*/
public io.modelcontextprotocol.client.McpClient.SyncSpec configure(
String name,
io.modelcontextprotocol.client.McpClient.SyncSpec spec
);
}package org.springframework.ai.mcp.client.common.autoconfigure.configurer;
/**
* Configurer that applies all registered McpAsyncClientCustomizer instances
* to client specifications.
*
* Created as a Spring bean by auto-configuration.
* Thread-safe - customizers are applied sequentially during single-threaded client creation.
* Immutable after construction.
*/
public class McpAsyncClientConfigurer {
/**
* Apply all customizers to the specification.
* Customizers are applied in order determined by @Order annotation.
* Lower order values are applied first.
*
* @param name The connection name (never null)
* @param spec The specification to configure (never null, mutable)
* @return The configured specification (same instance, modified)
*/
public io.modelcontextprotocol.client.McpClient.AsyncSpec configure(
String name,
io.modelcontextprotocol.client.McpClient.AsyncSpec spec
);
}The auto-configuration creates these configurers as beans:
Sync Configurer Bean:
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Creates the synchronous client configurer bean.
* Aggregates all McpSyncClientCustomizer instances.
* Created only when spring.ai.mcp.client.type=SYNC (default).
*
* @param customizers Provider of all registered customizer beans
* @return Configurer bean (never null)
*/
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "type",
havingValue = "SYNC",
matchIfMissing = true
)
public org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpSyncClientConfigurer mcpSyncClientConfigurer(
org.springframework.beans.factory.ObjectProvider<org.springframework.ai.mcp.customizer.McpSyncClientCustomizer> customizers
);Async Configurer Bean:
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Creates the asynchronous client configurer bean.
* Aggregates all McpAsyncClientCustomizer instances.
* Created only when spring.ai.mcp.client.type=ASYNC.
*
* @param customizers Provider of all registered customizer beans
* @return Configurer bean (never null)
*/
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "type",
havingValue = "ASYNC"
)
public org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpAsyncClientConfigurer mcpAsyncClientConfigurer(
org.springframework.beans.factory.ObjectProvider<org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer> customizers
);You can override these beans by providing your own @Bean methods if you need custom configurer behavior.
You can register multiple customizers - they'll all be applied in order:
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import io.modelcontextprotocol.client.McpClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* Configuration demonstrating multiple ordered customizers.
* Customizers are applied in order determined by @Order annotation.
*/
@Configuration
public class MultipleCustomizers {
/**
* First customizer - applied before others.
* Sets base configuration that other customizers can override.
* Order value: 1 (lower values applied first)
*
* @return First customizer
*/
@Bean
@Order(1)
public McpSyncClientCustomizer firstCustomizer() {
return (name, spec) -> {
log.info("First customizer for: {}", name);
// Set default timeout
spec.requestTimeout(Duration.ofSeconds(30));
// Set default capabilities
spec.capabilities(new McpSchema.ClientCapabilities(null, null, null));
};
}
/**
* Second customizer - applied after first.
* Can override or augment first customizer's settings.
* Order value: 2
*
* @return Second customizer
*/
@Bean
@Order(2)
public McpSyncClientCustomizer secondCustomizer() {
return (name, spec) -> {
log.info("Second customizer for: {}", name);
// Override timeout for specific connections
if (name.equals("slow-server")) {
spec.requestTimeout(Duration.ofMinutes(2));
}
};
}
/**
* Third customizer - applied last.
* Final configuration adjustments.
* Order value: 3
*
* @return Third customizer
*/
@Bean
@Order(3)
public McpSyncClientCustomizer thirdCustomizer() {
return (name, spec) -> {
log.info("Third customizer for: {}", name);
// Add logging (won't be overridden by others)
spec.loggingConsumer(notification -> {
log.info("[{}] {}: {}", name, notification.level(), notification.data());
});
};
}
}Default Order: Customizers without @Order annotation have default order Ordered.LOWEST_PRECEDENCE (applied last)
Order Values:
@Order(1) applied before @Order(2)Ordered.HIGHEST_PRECEDENCE = earliestOrdered.LOWEST_PRECEDENCE = latestBuilt-In Customizer Orders:
@Order(100) (applied relatively early)The starter includes built-in customizers:
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Built-in customizer that emits McpToolsChangedEvent when tools change.
* Automatically registered by the auto-configuration.
* Order: 100 (applied early in customizer chain)
* Thread-safe - event publishing is thread-safe.
*
* @see org.springframework.ai.mcp.McpToolsChangedEvent
*/
@Order(100)
public class McpSyncToolsChangeEventEmmiter implements org.springframework.ai.mcp.customizer.McpSyncClientCustomizer {
/**
* Customize client to publish Spring events when tool list changes.
*
* @param name Connection name
* @param spec Client specification to customize
*/
@Override
public void customize(String name, io.modelcontextprotocol.client.McpClient.SyncSpec spec);
}package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Built-in customizer that emits McpToolsChangedEvent when tools change.
* Automatically registered by the auto-configuration for async clients.
* Order: 100 (applied early in customizer chain)
* Thread-safe - event publishing is thread-safe.
*
* @see org.springframework.ai.mcp.McpToolsChangedEvent
*/
@Order(100)
public class McpAsyncToolsChangeEventEmmiter implements org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer {
/**
* Customize client to publish Spring events when tool list changes.
*
* @param name Connection name
* @param spec Client specification to customize
*/
@Override
public void customize(String name, io.modelcontextprotocol.client.McpClient.AsyncSpec spec);
}These customizers are automatically created and enable the MCP Events functionality.
To replace built-in customizers, use @Primary:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* Configuration that overrides built-in event emitter.
* Uses @Primary to take precedence over default implementation.
*/
@Configuration
public class CustomEventEmitterConfig {
/**
* Custom event emitter that replaces the default.
* Marked @Primary to override the auto-configured bean.
*
* @param eventPublisher Application event publisher
* @return Custom event emitter customizer
*/
@Bean
@Primary
public McpSyncClientCustomizer customEventEmitter(
ApplicationEventPublisher eventPublisher) {
return (name, spec) -> {
// Custom event publishing logic
spec.toolListChangeNotificationHandler(tools -> {
// Custom event with additional metadata
eventPublisher.publishEvent(
new EnhancedToolsChangedEvent(this, name, tools, System.currentTimeMillis())
);
});
};
}
}Sync Client Spec (McpClient.SyncSpec):
package io.modelcontextprotocol.client;
/**
* Specification builder for synchronous MCP clients.
* All methods return this for fluent API.
* Not thread-safe - used during single-threaded client creation only.
*/
public interface McpClient.SyncSpec {
/**
* Set request timeout for all operations.
* @param timeout Timeout duration (must be positive)
* @return This spec for chaining
*/
McpClient.SyncSpec requestTimeout(java.time.Duration timeout);
/**
* Set client capabilities advertised to server.
* @param capabilities Client capabilities (nullable)
* @return This spec for chaining
*/
McpClient.SyncSpec capabilities(io.modelcontextprotocol.spec.McpSchema.ClientCapabilities capabilities);
/**
* Set logging consumer for MCP log messages.
* @param consumer Consumer of log notifications (nullable to disable)
* @return This spec for chaining
*/
McpClient.SyncSpec loggingConsumer(java.util.function.Consumer<io.modelcontextprotocol.spec.McpSchema.LoggingNotification> consumer);
/**
* Set sampling function for LLM completion requests.
* @param samplingFunction Function to handle sampling (nullable to disable)
* @return This spec for chaining
*/
McpClient.SyncSpec sampling(java.util.function.Function<io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest, io.modelcontextprotocol.spec.McpSchema.CreateMessageResult> samplingFunction);
/**
* Set tool list change notification handler.
* @param handler Handler for tool list changes (nullable to disable)
* @return This spec for chaining
*/
McpClient.SyncSpec toolListChangeNotificationHandler(java.util.function.Consumer<java.util.List<io.modelcontextprotocol.spec.McpSchema.Tool>> handler);
/**
* Set resource list change notification handler.
* @param handler Handler for resource list changes (nullable to disable)
* @return This spec for chaining
*/
McpClient.SyncSpec resourceListChangeNotificationHandler(java.util.function.Consumer<java.util.List<io.modelcontextprotocol.spec.McpSchema.Resource>> handler);
/**
* Set prompt list change notification handler.
* @param handler Handler for prompt list changes (nullable to disable)
* @return This spec for chaining
*/
McpClient.SyncSpec promptListChangeNotificationHandler(java.util.function.Consumer<java.util.List<io.modelcontextprotocol.spec.McpSchema.Prompt>> handler);
}Async Client Spec (McpClient.AsyncSpec):
package io.modelcontextprotocol.client;
/**
* Specification builder for asynchronous MCP clients.
* All methods return this for fluent API.
* Not thread-safe - used during single-threaded client creation only.
*/
public interface McpClient.AsyncSpec {
/**
* Set request timeout for all operations.
* @param timeout Timeout duration (must be positive)
* @return This spec for chaining
*/
McpClient.AsyncSpec requestTimeout(java.time.Duration timeout);
/**
* Set client capabilities advertised to server.
* @param capabilities Client capabilities (nullable)
* @return This spec for chaining
*/
McpClient.AsyncSpec capabilities(io.modelcontextprotocol.spec.McpSchema.ClientCapabilities capabilities);
/**
* Set progress consumer for progress notifications.
* @param consumer Consumer of progress notifications (nullable to disable)
* @return This spec for chaining
*/
McpClient.AsyncSpec progressConsumer(java.util.function.Consumer<io.modelcontextprotocol.spec.McpSchema.ProgressNotification> consumer);
/**
* Set elicitation function for information extraction requests.
* @param elicitationFunction Function returning Mono with elicited info (nullable to disable)
* @return This spec for chaining
*/
McpClient.AsyncSpec elicitationFunction(java.util.function.Function<io.modelcontextprotocol.spec.McpSchema.ElicitationRequest, reactor.core.publisher.Mono<String>> elicitationFunction);
/**
* Set sampling function for LLM completion requests.
* @param samplingFunction Function returning Mono with sampling result (nullable to disable)
* @return This spec for chaining
*/
McpClient.AsyncSpec sampling(java.util.function.Function<io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest, reactor.core.publisher.Mono<io.modelcontextprotocol.spec.McpSchema.CreateMessageResult>> samplingFunction);
}@Order annotationimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
/**
* Customizer that's only active in specific environments.
* Enabled only when feature flag is true.
*/
@Bean
@ConditionalOnProperty(name = "myapp.mcp.detailed-logging", havingValue = "true")
public McpSyncClientCustomizer conditionalLoggingCustomizer() {
return (name, spec) -> {
// Detailed logging only when enabled
spec.loggingConsumer(notification -> {
log.info("Detailed log from {}: level={}, data={}, timestamp={}",
name, notification.level(), notification.data(), System.currentTimeMillis());
});
};
}import org.springframework.context.annotation.Profile;
/**
* Customizer for development environment only.
* Provides debug-friendly settings.
*/
@Bean
@Profile("dev")
public McpSyncClientCustomizer devCustomizer() {
return (name, spec) -> {
// Development settings
spec.requestTimeout(Duration.ofMinutes(10)); // Long timeout for debugging
spec.loggingConsumer(notification -> {
System.out.println("[DEV] " + name + ": " + notification.data());
});
};
}
/**
* Customizer for production environment only.
* Provides production-optimized settings.
*/
@Bean
@Profile("prod")
public McpSyncClientCustomizer prodCustomizer() {
return (name, spec) -> {
// Production settings
spec.requestTimeout(Duration.ofSeconds(10)); // Short timeout for fast failure
// Logging handled by built-in logger
};
}/**
* Factory for creating customizers with shared configuration.
* Useful for consistent customization across multiple connections.
*/
public class McpCustomizerFactory {
/**
* Create logging customizer with specified log level.
*
* @param minLevel Minimum log level to log
* @return Logging customizer
*/
public static McpSyncClientCustomizer loggingCustomizer(String minLevel) {
return (name, spec) -> {
spec.loggingConsumer(notification -> {
if (isLevelAbove(notification.level(), minLevel)) {
log.info("[{}] {}: {}", name, notification.level(), notification.data());
}
});
};
}
/**
* Create timeout customizer with connection-specific rules.
*
* @param timeoutRules Map of connection patterns to timeouts
* @return Timeout customizer
*/
public static McpSyncClientCustomizer timeoutCustomizer(Map<String, Duration> timeoutRules) {
return (name, spec) -> {
Duration timeout = timeoutRules.entrySet().stream()
.filter(entry -> name.matches(entry.getKey()))
.map(Map.Entry::getValue)
.findFirst()
.orElse(Duration.ofSeconds(30));
spec.requestTimeout(timeout);
};
}
private static boolean isLevelAbove(String level, String minLevel) {
// Implementation of log level comparison
return true; // Placeholder
}
}If customizers aren't being applied:
@Bean)@Conditional annotations don't prevent registrationIf customizers conflict:
@OrderIf customizers cause slow startup:
tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux@1.1.0