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

customization.mddocs/reference/

Customizing MCP Clients

Guide to customizing MCP client behavior using the customizer interfaces.

Overview

The MCP client auto-configuration provides extension points through customizer interfaces. Implement these interfaces to modify client specifications before they're built and initialized.

Key Concepts

  • Customizer Pattern: Spring-style customizer interfaces for declarative configuration
  • Applied at Creation Time: Customizers run once during client initialization
  • Multiple Customizers: Support for multiple customizers applied in order
  • Thread-Safe: Customization happens before clients are used
  • Built-In Customizers: Starter includes customizers for event publishing

Customizer Interfaces

McpSyncClientCustomizer

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);
}

McpAsyncClientCustomizer

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);
}

Usage Examples

Adding Custom Logging

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);
                }
            });
        };
    }
}

Customizing Client Capabilities

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);
        };
    }
}

Connection-Specific Customization

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);
        };
    }
}

Adding Progress Tracking

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;
        }
    }
}

Custom Sampling Handler

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]";
    }
}

Adding Elicitation Handler

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;
    }
}

Configurer Classes

The auto-configuration uses configurer classes to apply all customizers:

McpSyncClientConfigurer

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
    );
}

McpAsyncClientConfigurer

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
    );
}

Configurer Bean Methods

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.

Multiple Customizers

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());
            });
        };
    }
}

Customizer Ordering

Default Order: Customizers without @Order annotation have default order Ordered.LOWEST_PRECEDENCE (applied last)

Order Values:

  • Lower values applied first
  • @Order(1) applied before @Order(2)
  • Negative values allowed (applied very early)
  • Ordered.HIGHEST_PRECEDENCE = earliest
  • Ordered.LOWEST_PRECEDENCE = latest

Built-In Customizer Orders:

  • Event emitter customizers: @Order(100) (applied relatively early)

Built-in Customizers

The starter includes built-in customizers:

McpSyncToolsChangeEventEmmiter

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);
}

McpAsyncToolsChangeEventEmmiter

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.

Overriding Built-In Customizers

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())
                );
            });
        };
    }
}

Available Customization Options

Client Specification Methods

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);
}

Best Practices

  1. Use Ordering: Control customizer application order with @Order annotation
  2. Be Idempotent: Customizers should be safe to apply multiple times
  3. Check Connection Name: Use connection name to apply different settings per server
  4. Log Changes: Log what each customizer does for debugging
  5. Handle Nulls: Check for null parameters and handle gracefully
  6. Don't Block: Keep customizer logic fast - they run during startup
  7. Document Customizers: Document what each customizer does and why
  8. Test Customizers: Unit test customizer logic independently
  9. Avoid State: Customizers should be stateless where possible
  10. Consider Defaults: Don't override if default behavior is acceptable

Advanced Patterns

Conditional Customization

import 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());
        });
    };
}

Profile-Specific Customization

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
    };
}

Customizer Factory Pattern

/**
 * 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
    }
}

Troubleshooting

Customizers Not Applied

If customizers aren't being applied:

  1. Check Bean Registration: Verify customizer is registered as Spring bean (@Bean)
  2. Check Client Type: Ensure using correct customizer type (sync vs async)
  3. Check Conditions: Verify @Conditional annotations don't prevent registration
  4. Check Order: Verify customizer order doesn't cause it to be overridden
  5. Check Logs: Enable debug logging to see customizer application

Customizers Conflict

If customizers conflict:

  1. Use Ordering: Apply customizers in correct order with @Order
  2. Check Overrides: Later customizers can override earlier ones
  3. Be Specific: Use connection name to avoid applying conflicting settings
  4. Test Combinations: Test all customizer combinations

Performance Issues

If customizers cause slow startup:

  1. Avoid Blocking: Don't perform slow operations in customizers
  2. Lazy Init: Move expensive setup to first use, not customizer
  3. Profile: Profile startup to identify slow customizers
  4. Simplify: Reduce number of customizers if possible

Related Documentation

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

docs

index.md

tile.json