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

annotation-scanner.mddocs/reference/

Annotation-Based MCP Handlers

Annotation-based handler scanning for Model Context Protocol (MCP) client operations, enabling declarative MCP handler methods using Spring annotations.

Overview

The annotation scanner auto-configuration automatically discovers and registers Spring bean methods annotated with MCP annotations. This provides a declarative way to handle MCP protocol events and operations without manually implementing handler interfaces.

When enabled (default), the scanner looks for methods annotated with MCP annotations like @McpLogging, @McpSampling, @McpProgress, and others. These methods are automatically registered with the appropriate MCP client handler registry.

Key Features

  • Automatic Discovery: Scans all Spring-managed beans for MCP annotations
  • Type-Safe: Compile-time checking of annotated method signatures
  • Flexible: Works with both synchronous and asynchronous clients
  • GraalVM Compatible: Full support for native image compilation
  • Thread-Safe: Handler registration is thread-safe and happens at startup only

Auto-Configuration Class

package org.springframework.ai.mcp.client.common.autoconfigure.annotations;

/**
 * Auto-configuration for annotation-based MCP handler scanning.
 * Scans for annotated methods and registers them with handler registries.
 * Enabled by default unless explicitly disabled.
 * Thread-safe - scanning happens during Spring context initialization.
 */
@org.springframework.boot.autoconfigure.AutoConfiguration
@org.springframework.boot.autoconfigure.condition.ConditionalOnClass(
    org.springaicommunity.mcp.annotation.McpLogging.class
)
@org.springframework.boot.context.properties.EnableConfigurationProperties(
    McpClientAnnotationScannerProperties.class
)
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
    prefix = "spring.ai.mcp.client.annotation-scanner",
    name = "enabled",
    havingValue = "true",
    matchIfMissing = true
)
public class McpClientAnnotationScannerAutoConfiguration {
}

Conditional Activation

This auto-configuration is conditionally enabled when:

  1. Class Presence: org.springaicommunity.mcp.annotation.McpLogging is on the classpath (from spring-ai-mcp-annotations dependency)
  2. Property Check: spring.ai.mcp.client.annotation-scanner.enabled is true (default) or not specified

If these conditions are not met, annotation scanning is skipped and no handler registries are created.

Dependency Requirements

Required Maven dependency:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp-annotations</artifactId>
    <version>${spring-ai.version}</version>
</dependency>

Or Gradle:

implementation 'org.springframework.ai:spring-ai-mcp-annotations:${springAiVersion}'

Handler Registry Beans

Synchronous Handler Registry

package org.springframework.ai.mcp.client.common.autoconfigure.annotations;

/**
 * Registry for synchronous MCP handler methods discovered via annotation scanning.
 * Used by synchronous MCP clients to register annotated handler methods.
 * Created only when spring.ai.mcp.client.type=SYNC (default).
 * Thread-safe for concurrent registration and access.
 * Immutable after registration phase completes.
 *
 * @return ClientMcpSyncHandlersRegistry instance (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.annotation.spring.ClientMcpSyncHandlersRegistry clientMcpSyncHandlersRegistry();

Created when: spring.ai.mcp.client.type=SYNC (default)

Registry Characteristics:

  • Thread Safety: Concurrent-safe for handler registration
  • Lifecycle: Populated during Spring context refresh phase
  • Handler Storage: Internal map of annotation type to handler list
  • Handler Invocation: Synchronous, blocking calls
  • Error Handling: Exceptions thrown from handlers are propagated

Asynchronous Handler Registry

package org.springframework.ai.mcp.client.common.autoconfigure.annotations;

/**
 * Registry for asynchronous MCP handler methods discovered via annotation scanning.
 * Used by asynchronous MCP clients to register annotated handler methods.
 * Created only when spring.ai.mcp.client.type=ASYNC.
 * Thread-safe for concurrent registration and access.
 * Immutable after registration phase completes.
 *
 * @return ClientMcpAsyncHandlersRegistry instance (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.annotation.spring.ClientMcpAsyncHandlersRegistry clientMcpAsyncHandlersRegistry();

Created when: spring.ai.mcp.client.type=ASYNC

Registry Characteristics:

  • Thread Safety: Concurrent-safe for handler registration
  • Lifecycle: Populated during Spring context refresh phase
  • Handler Storage: Internal map of annotation type to handler list
  • Handler Invocation: Asynchronous, returns Mono or Flux
  • Error Handling: Errors handled via reactive error channels

Supported MCP Annotations

The annotation scanner recognizes the following MCP annotations from the org.springaicommunity.mcp.annotation package:

@McpLogging

Marks a method as a handler for MCP logging operations. Use this to implement custom logging logic for MCP protocol messages.

Package: org.springaicommunity.mcp.annotation.McpLogging

Method Signature (Sync):

@McpLogging
public void handleLogging(String connectionName, String level, String message);
// OR with additional parameters
@McpLogging
public void handleLogging(String connectionName, String level, String message, Map<String, Object> data);

Method Signature (Async):

@McpLogging
public Mono<Void> handleLogging(String connectionName, String level, String message);
// OR
@McpLogging
public Mono<Void> handleLogging(String connectionName, String level, String message, Map<String, Object> data);

Parameters:

  • connectionName: Name of the MCP connection (never null)
  • level: Log level (e.g., "debug", "info", "warning", "error") (never null)
  • message: Log message content (may be null)
  • data: Optional additional log data (may be null)

@McpSampling

Marks a method as a handler for MCP sampling operations. Use this to handle AI model sampling requests from MCP servers.

Package: org.springaicommunity.mcp.annotation.McpSampling

Method Signature (Sync):

@McpSampling
public McpSchema.CreateMessageResult handleSampling(
    String connectionName,
    McpSchema.CreateMessageRequest request
);

Method Signature (Async):

@McpSampling
public Mono<McpSchema.CreateMessageResult> handleSampling(
    String connectionName,
    McpSchema.CreateMessageRequest request
);

Parameters:

  • connectionName: Name of the MCP connection (never null)
  • request: Sampling request with messages and model preferences (never null)

Return Value:

  • CreateMessageResult: Generated message result with role, content, and stop reason

@McpElicitation

Marks a method as a handler for MCP elicitation operations. Use this to handle information elicitation requests from MCP servers.

Package: org.springaicommunity.mcp.annotation.McpElicitation

Method Signature (Sync):

@McpElicitation
public String handleElicitation(String connectionName, String prompt, Map<String, Object> context);

Method Signature (Async):

@McpElicitation
public Mono<String> handleElicitation(String connectionName, String prompt, Map<String, Object> context);

Parameters:

  • connectionName: Name of the MCP connection (never null)
  • prompt: Elicitation prompt text (never null)
  • context: Additional context data (may be null)

Return Value:

  • String: Elicited information response

@McpProgress

Marks a method as a handler for MCP progress notifications. Use this to track and respond to progress updates during long-running operations.

Package: org.springaicommunity.mcp.annotation.McpProgress

Method Signature (Sync):

@McpProgress
public void handleProgress(String connectionName, long current, long total, String progressToken);
// OR simplified
@McpProgress
public void handleProgress(String connectionName, long current, long total);

Method Signature (Async):

@McpProgress
public Mono<Void> handleProgress(String connectionName, long current, long total, String progressToken);

Parameters:

  • connectionName: Name of the MCP connection (never null)
  • current: Current progress value (0-based)
  • total: Total progress value (max value)
  • progressToken: Optional progress tracking token (may be null)

@McpToolListChanged

Marks a method as a handler for MCP tool list change notifications. Use this to respond when the available tools on an MCP server change.

Package: org.springaicommunity.mcp.annotation.McpToolListChanged

Method Signature (Sync):

@McpToolListChanged
public void handleToolListChanged(String connectionName, List<McpSchema.Tool> tools);

Method Signature (Async):

@McpToolListChanged
public Mono<Void> handleToolListChanged(String connectionName, List<McpSchema.Tool> tools);

Parameters:

  • connectionName: Name of the MCP connection (never null)
  • tools: Updated list of available tools (never null, may be empty)

Tool Properties:

  • name(): Tool name (unique within server)
  • description(): Tool description (may be null)
  • inputSchema(): JSON schema for tool parameters

@McpResourceListChanged

Marks a method as a handler for MCP resource list change notifications. Use this to respond when the available resources on an MCP server change.

Package: org.springaicommunity.mcp.annotation.McpResourceListChanged

Method Signature (Sync):

@McpResourceListChanged
public void handleResourceListChanged(String connectionName, List<McpSchema.Resource> resources);

Method Signature (Async):

@McpResourceListChanged
public Mono<Void> handleResourceListChanged(String connectionName, List<McpSchema.Resource> resources);

Parameters:

  • connectionName: Name of the MCP connection (never null)
  • resources: Updated list of available resources (never null, may be empty)

Resource Properties:

  • uri(): Resource URI (unique identifier)
  • name(): Human-readable resource name
  • description(): Resource description (may be null)
  • mimeType(): MIME type (may be null)

@McpPromptListChanged

Marks a method as a handler for MCP prompt list change notifications. Use this to respond when the available prompts on an MCP server change.

Package: org.springaicommunity.mcp.annotation.McpPromptListChanged

Method Signature (Sync):

@McpPromptListChanged
public void handlePromptListChanged(String connectionName, List<McpSchema.Prompt> prompts);

Method Signature (Async):

@McpPromptListChanged
public Mono<Void> handlePromptListChanged(String connectionName, List<McpSchema.Prompt> prompts);

Parameters:

  • connectionName: Name of the MCP connection (never null)
  • prompts: Updated list of available prompts (never null, may be empty)

Prompt Properties:

  • name(): Prompt name (unique within server)
  • description(): Prompt description (may be null)
  • arguments(): List of prompt argument definitions

Configuration

Enable/Disable Annotation Scanning

Configure annotation scanning in application.yml:

spring.ai.mcp.client:
  annotation-scanner:
    enabled: true  # default

To disable annotation scanning:

spring.ai.mcp.client:
  annotation-scanner:
    enabled: false

See Configuration Properties for complete property details.

Usage Examples

Logging Handler Example

import org.springaicommunity.mcp.annotation.McpLogging;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Handler for MCP logging events.
 * Receives log messages from all connected MCP servers.
 * Thread-safe - can be invoked concurrently from multiple connections.
 */
@Component
public class McpLoggingHandler {

    private static final Logger log = LoggerFactory.getLogger(McpLoggingHandler.class);

    /**
     * Handle logging messages from MCP servers.
     * Invoked synchronously when servers send log notifications.
     *
     * @param connectionName Name of the connection sending the log
     * @param level Log level (debug, info, warning, error)
     * @param message Log message content
     */
    @McpLogging
    public void handleLogging(String connectionName, String level, String message) {
        // Map MCP log levels to SLF4J levels
        switch (level.toLowerCase()) {
            case "debug" -> log.debug("[{}] {}", connectionName, message);
            case "info" -> log.info("[{}] {}", connectionName, message);
            case "warning" -> log.warn("[{}] {}", connectionName, message);
            case "error" -> log.error("[{}] {}", connectionName, message);
            default -> log.info("[{}] [{}] {}", connectionName, level, message);
        }
    }
}

Progress Handler Example

import org.springaicommunity.mcp.annotation.McpProgress;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Handler for MCP progress notifications.
 * Tracks progress for long-running operations across all connections.
 * Thread-safe with concurrent map for progress tracking.
 */
@Component
public class McpProgressHandler {

    // Track progress per connection and token
    private final Map<String, Double> progressCache = new ConcurrentHashMap<>();

    /**
     * Handle progress notifications from MCP servers.
     * Invoked when servers report progress for long-running operations.
     *
     * @param connectionName Name of the connection reporting progress
     * @param current Current progress value
     * @param total Total progress value
     * @param progressToken Optional token identifying the operation
     */
    @McpProgress
    public void handleProgress(String connectionName, long current, long total, String progressToken) {
        double percentage = (current * 100.0) / total;
        
        String key = progressToken != null 
            ? connectionName + ":" + progressToken
            : connectionName;
        
        progressCache.put(key, percentage);
        
        System.out.printf("Progress on %s: %.2f%% (%d/%d)%n",
            connectionName, percentage, current, total);
        
        // Remove from cache when complete
        if (current >= total) {
            progressCache.remove(key);
        }
    }
    
    /**
     * Get current progress for a connection.
     * 
     * @param connectionName Connection to check
     * @return Progress percentage (0-100), or null if no progress tracked
     */
    public Double getProgress(String connectionName) {
        return progressCache.get(connectionName);
    }
}

Tool List Changed Handler Example

import io.modelcontextprotocol.spec.McpSchema;
import org.springaicommunity.mcp.annotation.McpToolListChanged;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Handler for MCP tool list changes.
 * Maintains a cache of available tools per connection.
 * Thread-safe with concurrent map.
 */
@Component
public class McpToolChangeHandler {

    // Cache of tools per connection
    private final Map<String, List<McpSchema.Tool>> toolsCache = new ConcurrentHashMap<>();

    /**
     * Handle tool list change notifications.
     * Invoked when MCP servers add, remove, or modify their tool list.
     *
     * @param connectionName Name of the connection with changed tools
     * @param tools Updated list of tools
     */
    @McpToolListChanged
    public void handleToolListChanged(String connectionName, List<McpSchema.Tool> tools) {
        toolsCache.put(connectionName, List.copyOf(tools)); // Defensive copy
        
        System.out.println("Tools changed for connection: " + connectionName);
        System.out.println("Available tools: " + tools.size());
        
        tools.forEach(tool -> {
            System.out.printf("  - %s: %s%n", tool.name(), 
                tool.description() != null ? tool.description() : "No description");
        });
    }
    
    /**
     * Get cached tools for a connection.
     *
     * @param connectionName Connection to query
     * @return List of tools, or empty list if connection not found
     */
    public List<McpSchema.Tool> getTools(String connectionName) {
        return toolsCache.getOrDefault(connectionName, List.of());
    }
    
    /**
     * Check if a specific tool is available on a connection.
     *
     * @param connectionName Connection to check
     * @param toolName Tool name to look for
     * @return true if tool is available
     */
    public boolean hasTool(String connectionName, String toolName) {
        return getTools(connectionName).stream()
            .anyMatch(tool -> tool.name().equals(toolName));
    }
}

Resource List Changed Handler Example

import io.modelcontextprotocol.spec.McpSchema;
import org.springaicommunity.mcp.annotation.McpResourceListChanged;
import org.springframework.stereotype.Component;
import org.springframework.context.ApplicationEventPublisher;
import java.util.List;

/**
 * Handler for MCP resource list changes.
 * Publishes Spring events when resources change for downstream processing.
 */
@Component
public class McpResourceChangeHandler {

    private final ApplicationEventPublisher eventPublisher;

    public McpResourceChangeHandler(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    /**
     * Handle resource list change notifications.
     * Publishes a custom Spring event for other components to react to.
     *
     * @param connectionName Name of the connection with changed resources
     * @param resources Updated list of resources
     */
    @McpResourceListChanged
    public void handleResourceListChanged(String connectionName,
                                         List<McpSchema.Resource> resources) {
        System.out.println("Resources changed for connection: " + connectionName);
        System.out.println("Available resources: " + resources.size());
        
        resources.forEach(resource ->
            System.out.printf("  - %s (%s): %s%n", 
                resource.name(), 
                resource.uri(), 
                resource.mimeType() != null ? resource.mimeType() : "unknown type")
        );
        
        // Publish custom event for other components
        eventPublisher.publishEvent(new ResourcesChangedEvent(this, connectionName, resources));
    }
    
    // Custom event class (should be defined in appropriate package)
    public static class ResourcesChangedEvent extends org.springframework.context.ApplicationEvent {
        private final String connectionName;
        private final List<McpSchema.Resource> resources;
        
        public ResourcesChangedEvent(Object source, String connectionName, List<McpSchema.Resource> resources) {
            super(source);
            this.connectionName = connectionName;
            this.resources = resources;
        }
        
        public String getConnectionName() { return connectionName; }
        public List<McpSchema.Resource> getResources() { return resources; }
    }
}

Prompt List Changed Handler Example

import io.modelcontextprotocol.spec.McpSchema;
import org.springaicommunity.mcp.annotation.McpPromptListChanged;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Handler for MCP prompt list changes.
 * Logs available prompts and their arguments for debugging.
 */
@Component
public class McpPromptChangeHandler {

    /**
     * Handle prompt list change notifications.
     * Provides detailed logging of prompts and their argument schemas.
     *
     * @param connectionName Name of the connection with changed prompts
     * @param prompts Updated list of prompts
     */
    @McpPromptListChanged
    public void handlePromptListChanged(String connectionName,
                                       List<McpSchema.Prompt> prompts) {
        System.out.println("Prompts changed for connection: " + connectionName);
        System.out.println("Available prompts: " + prompts.size());
        
        prompts.forEach(prompt -> {
            System.out.printf("  - %s: %s%n", prompt.name(), 
                prompt.description() != null ? prompt.description() : "No description");
            
            // Log prompt arguments if present
            if (prompt.arguments() != null && !prompt.arguments().isEmpty()) {
                String args = prompt.arguments().stream()
                    .map(arg -> arg.name() + (arg.required() ? "*" : ""))
                    .collect(Collectors.joining(", "));
                System.out.printf("    Arguments: %s%n", args);
            }
        });
    }
}

Sampling Handler Example

import io.modelcontextprotocol.spec.McpSchema;
import org.springaicommunity.mcp.annotation.McpSampling;
import org.springframework.stereotype.Component;
import org.springframework.ai.chat.client.ChatClient;

/**
 * Handler for MCP sampling requests.
 * Integrates with Spring AI chat client to generate responses.
 * Thread-safe - chat client handles concurrency internally.
 */
@Component
public class McpSamplingHandler {

    private final ChatClient chatClient;

    public McpSamplingHandler(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    /**
     * Handle sampling requests from MCP servers.
     * Called when servers need AI completion/generation.
     *
     * @param connectionName Name of the connection requesting sampling
     * @param request Sampling request with messages and preferences
     * @return Generated message result
     */
    @McpSampling
    public McpSchema.CreateMessageResult handleSampling(
            String connectionName,
            McpSchema.CreateMessageRequest request) {
        
        // Convert MCP messages to Spring AI format
        String prompt = convertMessagesToPrompt(request.messages());
        
        // Generate response using Spring AI
        String response = chatClient.prompt()
            .user(prompt)
            .call()
            .content();
        
        // Convert back to MCP format
        return new McpSchema.CreateMessageResult(
            McpSchema.Role.ASSISTANT,
            new McpSchema.TextContent(response),
            null, // model (optional)
            McpSchema.StopReason.END_TURN
        );
    }
    
    private String convertMessagesToPrompt(List<McpSchema.SamplingMessage> messages) {
        return messages.stream()
            .map(msg -> {
                if (msg.content() instanceof McpSchema.TextContent textContent) {
                    return textContent.text();
                }
                return "";
            })
            .collect(Collectors.joining("\n"));
    }
}

How It Works

  1. Component Scanning: The annotation scanner discovers all Spring beans with MCP annotation-marked methods during application startup
  2. Method Validation: Each annotated method is validated for correct signature (parameter types, return types)
  3. Handler Registration: Valid methods are registered with the appropriate handler registry (ClientMcpSyncHandlersRegistry or ClientMcpAsyncHandlersRegistry)
  4. Client Integration: When MCP clients are created, they automatically receive handlers from the registry
  5. Event Dispatch: When MCP protocol events occur, the registered handlers are invoked automatically

Scanning Process

Timing: During Spring context refresh, after bean instantiation but before bean post-processing completes

Order:

  1. Create handler registry bean
  2. Scan all Spring-managed beans
  3. Find methods with MCP annotations
  4. Validate method signatures
  5. Register valid handlers with registry
  6. Log warnings for invalid handlers
  7. Make registry immutable

Handler Invocation

Synchronous Handlers:

  • Invoked on the calling thread (usually MCP transport thread)
  • Blocking - MCP protocol waits for handler to complete
  • Exceptions propagate to MCP client
  • Should be fast to avoid blocking protocol

Asynchronous Handlers:

  • Return Mono or Flux for reactive execution
  • Non-blocking - MCP protocol subscribes to result
  • Errors handled via reactive error channels
  • Can be long-running without blocking

Integration with MCP Clients

The handler registries are automatically injected into MCP client beans. You don't need to manually wire handlers to clients - the auto-configuration handles this:

// Automatic integration - no manual wiring needed
import io.modelcontextprotocol.client.McpSyncClient;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class McpService {

    private final List<McpSyncClient> mcpClients;

    /**
     * Constructor injection of MCP clients.
     * Clients already have all annotated handlers registered automatically.
     *
     * @param mcpClients Auto-configured MCP clients with handlers
     */
    public McpService(List<McpSyncClient> mcpClients) {
        // Clients already have annotated handlers registered
        this.mcpClients = mcpClients;
    }
    
    // Use clients directly - handlers are already configured
}

GraalVM Native Image Support

The annotation scanner includes AOT (Ahead-of-Time) processing support for GraalVM native image compilation. All MCP annotations are automatically registered for reflection during AOT processing.

AOT Processor

The auto-configuration includes a ClientAnnotatedBeanFactoryInitializationAotProcessor that processes annotated beans during AOT compilation:

package org.springframework.ai.mcp.client.common.autoconfigure.annotations;

/**
 * AOT processor that ensures annotation-based handlers work in native images.
 * Automatically registers all MCP annotations and handler beans for reflection.
 * Runs during Spring AOT processing phase (build time).
 * Generates reflection hints and proxy configurations.
 */
@org.springframework.context.annotation.Bean
public static ClientAnnotatedBeanFactoryInitializationAotProcessor clientAnnotatedBeanFactoryInitializationAotProcessor();

The processor automatically:

  • Registers Annotations: All MCP annotation types for reflection
  • Registers Handlers: Annotated handler methods are accessible at runtime
  • Registers Types: All parameter and return types used in handler methods
  • Generates Hints: Runtime hints for the handler registry
  • Proxy Configuration: Dynamic proxy configuration for handler interfaces

No additional configuration is required - native image compilation works automatically when using this starter.

Native Image Build

To build a native image with MCP annotation handlers:

./mvnw -Pnative native:compile
# OR for Gradle
./gradlew nativeCompile

Native image features:

  • Fast Startup: Handlers are pre-registered during build
  • Low Memory: No reflection overhead at runtime
  • Full Support: All annotation types supported
  • Build Time: AOT processing adds 5-15 seconds

Best Practices

  1. Use @Component: Ensure classes with MCP annotation handlers are Spring-managed beans (use @Component, @Service, etc.)
  2. Keep Handlers Lightweight: Handler methods should be fast and non-blocking (especially sync handlers)
  3. Handle Errors: Include error handling in your handler methods
  4. Log Appropriately: Use proper logging instead of System.out in production
  5. Consider Async: For long-running operations, consider using async MCP clients with reactive handler implementations
  6. Thread Safety: Handlers can be invoked concurrently - ensure thread safety
  7. Avoid Blocking: Don't block in async handlers - use reactive operators
  8. Test Handlers: Unit test handlers independently of MCP infrastructure
  9. Document Parameters: Document expected parameter values and behavior
  10. Validate Inputs: Validate handler parameters (null checks, range checks)

Anti-Patterns to Avoid

Don't Block in Async Handlers:

// BAD - blocks reactive chain
@McpProgress
public Mono<Void> handleProgress(String conn, long curr, long total) {
    Thread.sleep(1000); // Don't block!
    return Mono.empty();
}

// GOOD - use reactive delay
@McpProgress
public Mono<Void> handleProgress(String conn, long curr, long total) {
    return Mono.delay(Duration.ofSeconds(1)).then();
}

Don't Throw Unchecked Exceptions Carelessly:

// BAD - crashes MCP client
@McpLogging
public void handleLogging(String conn, String level, String msg) {
    throw new RuntimeException("Oops!"); // Will break MCP connection!
}

// GOOD - handle errors gracefully
@McpLogging
public void handleLogging(String conn, String level, String msg) {
    try {
        // ... handler logic ...
    } catch (Exception e) {
        log.error("Error in logging handler", e);
        // Don't propagate exception
    }
}

Don't Access External Resources Without Timeouts:

// BAD - may hang indefinitely
@McpToolListChanged
public void handleTools(String conn, List<Tool> tools) {
    httpClient.get("http://slow-server/update"); // No timeout!
}

// GOOD - use timeouts
@McpToolListChanged
public void handleTools(String conn, List<Tool> tools) {
    try {
        httpClient.get("http://slow-server/update")
            .timeout(Duration.ofSeconds(5));
    } catch (TimeoutException e) {
        log.warn("Update notification timed out", e);
    }
}

Disabling Individual Handlers

If you need to conditionally enable/disable specific handlers, use Spring's @ConditionalOnProperty:

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

/**
 * Optional logging handler that can be disabled via configuration.
 * Enabled only when myapp.mcp.logging.enabled=true
 */
@Component
@ConditionalOnProperty(name = "myapp.mcp.logging.enabled", havingValue = "true")
public class OptionalLoggingHandler {

    @McpLogging
    public void handleLogging(String connectionName, String level, String message) {
        // This handler only active if myapp.mcp.logging.enabled=true
        System.out.printf("[%s] %s: %s%n", connectionName, level, message);
    }
}

Troubleshooting

Handlers Not Invoked

If handlers are not being called:

  1. Check Annotations: Verify correct annotation package (org.springaicommunity.mcp.annotation)
  2. Check Bean Registration: Ensure handler class is a Spring-managed bean (@Component)
  3. Check Method Signature: Verify method signature matches annotation requirements
  4. Check Scanner Enable: Verify spring.ai.mcp.client.annotation-scanner.enabled=true
  5. Check Client Type: Ensure handler type (sync/async) matches client type configuration
  6. Check Logs: Look for scanner warnings about invalid handler methods
  7. Check Dependencies: Verify spring-ai-mcp-annotations is on classpath

Invalid Handler Signature Errors

If you see warnings about invalid signatures:

  1. Parameter Types: Check parameter types match annotation requirements
  2. Return Types: Sync handlers return void or specific types, async return Mono/Flux
  3. Parameter Count: Verify correct number of parameters
  4. Null Annotations: Some parameters may be nullable - check documentation

Handler Exceptions

If handlers throw exceptions:

  1. Sync Handlers: Exceptions propagate to MCP client and may close connection
  2. Async Handlers: Use reactive error handling (onErrorResume, onErrorReturn)
  3. Log Errors: Always log errors before handling them
  4. Graceful Degradation: Handle errors without breaking MCP protocol

Performance Issues

If handlers are slow:

  1. Profile Handlers: Use profiling tools to identify bottlenecks
  2. Async Processing: Move slow operations to async handlers or background threads
  3. Avoid Blocking: Don't block in critical path (especially sync handlers)
  4. Cache Results: Cache expensive computations
  5. Batch Operations: Batch multiple events 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