| 
         This version is still in development and is not considered stable yet. For the latest snapshot version, please use Spring AI 1.0.3!  | 
    
MCP Client Annotations
The MCP Client Annotations provide a declarative way to implement MCP client handlers using Java annotations. These annotations simplify the handling of server notifications and client-side operations.
All MCP client annotations MUST include a clients parameter to associate the handler with a specific MCP client connection. The clients must match the connection name configured in your application properties.
 | 
Client Annotations
@McpLogging
The @McpLogging annotation handles logging message notifications from MCP servers.
@McpSampling
The @McpSampling annotation handles sampling requests from MCP servers for LLM completions.
Synchronous Implementation
@Component
public class SamplingHandler {
    @McpSampling(clients = "llm-server")
    public CreateMessageResult handleSamplingRequest(CreateMessageRequest request) {
        // Process the request and generate a response
        String response = generateLLMResponse(request);
        return CreateMessageResult.builder()
            .role(Role.ASSISTANT)
            .content(new TextContent(response))
            .model("gpt-4")
            .build();
    }
}
Asynchronous Implementation
@Component
public class AsyncSamplingHandler {
    @McpSampling(clients = "llm-server")
    public Mono<CreateMessageResult> handleAsyncSampling(CreateMessageRequest request) {
        return Mono.fromCallable(() -> {
            String response = generateLLMResponse(request);
            return CreateMessageResult.builder()
                .role(Role.ASSISTANT)
                .content(new TextContent(response))
                .model("gpt-4")
                .build();
        }).subscribeOn(Schedulers.boundedElastic());
    }
}
@McpElicitation
The @McpElicitation annotation handles elicitation requests to gather additional information from users.
Basic Usage
@Component
public class ElicitationHandler {
    @McpElicitation(clients = "interactive-server")
    public ElicitResult handleElicitationRequest(ElicitRequest request) {
        // Present the request to the user and gather input
        Map<String, Object> userData = presentFormToUser(request.requestedSchema());
        if (userData != null) {
            return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
        } else {
            return new ElicitResult(ElicitResult.Action.DECLINE, null);
        }
    }
}
With User Interaction
@McpElicitation(clients = "interactive-server")
public ElicitResult handleInteractiveElicitation(ElicitRequest request) {
    Map<String, Object> schema = request.requestedSchema();
    Map<String, Object> userData = new HashMap<>();
    // Check what information is being requested
    if (schema != null && schema.containsKey("properties")) {
        Map<String, Object> properties = (Map<String, Object>) schema.get("properties");
        // Gather user input based on schema
        if (properties.containsKey("name")) {
            userData.put("name", promptUser("Enter your name:"));
        }
        if (properties.containsKey("email")) {
            userData.put("email", promptUser("Enter your email:"));
        }
        if (properties.containsKey("preferences")) {
            userData.put("preferences", gatherPreferences());
        }
    }
    return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
}
Async Elicitation
@McpElicitation(clients = "interactive-server")
public Mono<ElicitResult> handleAsyncElicitation(ElicitRequest request) {
    return Mono.fromCallable(() -> {
        // Async user interaction
        Map<String, Object> userData = asyncGatherUserInput(request);
        return new ElicitResult(ElicitResult.Action.ACCEPT, userData);
    }).timeout(Duration.ofSeconds(30))
      .onErrorReturn(new ElicitResult(ElicitResult.Action.CANCEL, null));
}
@McpProgress
The @McpProgress annotation handles progress notifications for long-running operations.
Basic Usage
@Component
public class ProgressHandler {
    @McpProgress(clients = "my-mcp-server")
    public void handleProgressNotification(ProgressNotification notification) {
        double percentage = notification.progress() * 100;
        System.out.println(String.format("Progress: %.2f%% - %s",
            percentage, notification.message()));
    }
}
With Individual Parameters
@McpProgress(clients = "my-mcp-server")
public void handleProgressWithDetails(
        String progressToken,
        double progress,
        Double total,
        String message) {
    if (total != null) {
        System.out.println(String.format("[%s] %.0f/%.0f - %s",
            progressToken, progress, total, message));
    } else {
        System.out.println(String.format("[%s] %.2f%% - %s",
            progressToken, progress * 100, message));
    }
    // Update UI progress bar
    updateProgressBar(progressToken, progress);
}
Client-Specific Progress
@McpProgress(clients = "long-running-server")
public void handleLongRunningProgress(ProgressNotification notification) {
    // Track progress for specific server
    progressTracker.update("long-running-server", notification);
    // Send notifications if needed
    if (notification.progress() >= 1.0) {
        notifyCompletion(notification.progressToken());
    }
}
@McpToolListChanged
The @McpToolListChanged annotation handles notifications when the server’s tool list changes.
Basic Usage
@Component
public class ToolListChangedHandler {
    @McpToolListChanged(clients = "tool-server")
    public void handleToolListChanged(List<McpSchema.Tool> updatedTools) {
        System.out.println("Tool list updated: " + updatedTools.size() + " tools available");
        // Update local tool registry
        toolRegistry.updateTools(updatedTools);
        // Log new tools
        for (McpSchema.Tool tool : updatedTools) {
            System.out.println("  - " + tool.name() + ": " + tool.description());
        }
    }
}
Async Handling
@McpToolListChanged(clients = "tool-server")
public Mono<Void> handleAsyncToolListChanged(List<McpSchema.Tool> updatedTools) {
    return Mono.fromRunnable(() -> {
        // Process tool list update asynchronously
        processToolListUpdate(updatedTools);
        // Notify interested components
        eventBus.publish(new ToolListUpdatedEvent(updatedTools));
    }).then();
}
Client-Specific Tool Updates
@McpToolListChanged(clients = "dynamic-server")
public void handleDynamicServerToolUpdate(List<McpSchema.Tool> updatedTools) {
    // Handle tools from a specific server that frequently changes its tools
    dynamicToolManager.updateServerTools("dynamic-server", updatedTools);
    // Re-evaluate tool availability
    reevaluateToolCapabilities();
}
@McpResourceListChanged
The @McpResourceListChanged annotation handles notifications when the server’s resource list changes.
Basic Usage
@Component
public class ResourceListChangedHandler {
    @McpResourceListChanged(clients = "resource-server")
    public void handleResourceListChanged(List<McpSchema.Resource> updatedResources) {
        System.out.println("Resources updated: " + updatedResources.size());
        // Update resource cache
        resourceCache.clear();
        for (McpSchema.Resource resource : updatedResources) {
            resourceCache.register(resource);
        }
    }
}
With Resource Analysis
@McpResourceListChanged(clients = "resource-server")
public void analyzeResourceChanges(List<McpSchema.Resource> updatedResources) {
    // Analyze what changed
    Set<String> newUris = updatedResources.stream()
        .map(McpSchema.Resource::uri)
        .collect(Collectors.toSet());
    Set<String> removedUris = previousUris.stream()
        .filter(uri -> !newUris.contains(uri))
        .collect(Collectors.toSet());
    if (!removedUris.isEmpty()) {
        handleRemovedResources(removedUris);
    }
    // Update tracking
    previousUris = newUris;
}
@McpPromptListChanged
The @McpPromptListChanged annotation handles notifications when the server’s prompt list changes.
Basic Usage
@Component
public class PromptListChangedHandler {
    @McpPromptListChanged(clients = "prompt-server")
    public void handlePromptListChanged(List<McpSchema.Prompt> updatedPrompts) {
        System.out.println("Prompts updated: " + updatedPrompts.size());
        // Update prompt catalog
        promptCatalog.updatePrompts(updatedPrompts);
        // Refresh UI if needed
        if (uiController != null) {
            uiController.refreshPromptList(updatedPrompts);
        }
    }
}
Async Processing
@McpPromptListChanged(clients = "prompt-server")
public Mono<Void> handleAsyncPromptUpdate(List<McpSchema.Prompt> updatedPrompts) {
    return Flux.fromIterable(updatedPrompts)
        .flatMap(prompt -> validatePrompt(prompt))
        .collectList()
        .doOnNext(validPrompts -> {
            promptRepository.saveAll(validPrompts);
        })
        .then();
}
Spring Boot Integration
With Spring Boot auto-configuration, client handlers are automatically detected and registered:
@SpringBootApplication
public class McpClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(McpClientApplication.class, args);
    }
}
@Component
public class MyClientHandlers {
    @McpLogging(clients = "my-server")
    public void handleLogs(LoggingMessageNotification notification) {
        // Handle logs
    }
    @McpSampling(clients = "my-server")
    public CreateMessageResult handleSampling(CreateMessageRequest request) {
        // Handle sampling
    }
    @McpProgress(clients = "my-server")
    public void handleProgress(ProgressNotification notification) {
        // Handle progress
    }
}
The auto-configuration will:
- 
Scan for beans with MCP client annotations
 - 
Create appropriate specifications
 - 
Register them with the MCP client
 - 
Support both sync and async implementations
 - 
Handle multiple clients with client-specific handlers
 
Configuration Properties
Configure the client annotation scanner and client connections:
spring:
  ai:
    mcp:
      client:
        type: SYNC  # or ASYNC
        annotation-scanner:
          enabled: true
        # Configure client connections - the connection names become clients values
        sse:
          connections:
            my-server:  # This becomes the clients
              url: http://localhost:8080
            tool-server:  # Another clients
              url: http://localhost:8081
        stdio:
          connections:
            local-server:  # This becomes the clients
              command: /path/to/mcp-server
              args:
                - --mode=production
The clients parameter in annotations must match the connection names defined in your configuration. In the example above, valid clients values would be: "my-server", "tool-server", and "local-server".
 | 
Usage with MCP Client
The annotated handlers are automatically integrated with the MCP client:
@Autowired
private List<McpSyncClient> mcpClients;
// The clients will automatically use your annotated handlers based on clients
// No manual registration needed - handlers are matched to clients by name
For each MCP client connection, handlers with matching clients will be automatically registered and invoked when the corresponding events occur.