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
Spring Application Events published by the MCP client auto-configuration.
The MCP client starter publishes Spring ApplicationEvents when significant changes occur in MCP servers, such as when the list of available tools changes. You can listen to these events to react to dynamic changes in MCP server capabilities.
package org.springframework.ai.mcp;
/**
* Spring ApplicationEvent published when the list of tools changes for an MCP connection.
*
* This event is published by the built-in McpSyncToolsChangeEventEmmiter or
* McpAsyncToolsChangeEventEmmiter customizers when an MCP server notifies the client
* that its tool list has changed.
*
* Immutable and thread-safe after construction.
* Event source is the ApplicationEventPublisher that published the event.
*
* @see McpSyncToolsChangeEventEmmiter
* @see McpAsyncToolsChangeEventEmmiter
*/
public class McpToolsChangedEvent extends org.springframework.context.ApplicationEvent {
private final String connectionName;
private final java.util.List<io.modelcontextprotocol.spec.McpSchema.Tool> tools;
/**
* Create a new McpToolsChangedEvent.
*
* @param source Event source (typically the event publisher)
* @param connectionName Name of the connection with changed tools (never null)
* @param tools Updated list of tools (never null, may be empty)
*/
public McpToolsChangedEvent(Object source, String connectionName,
java.util.List<io.modelcontextprotocol.spec.McpSchema.Tool> tools) {
super(source);
this.connectionName = connectionName;
this.tools = java.util.List.copyOf(tools); // Defensive copy for immutability
}
/**
* Get the connection name for which tools changed.
*
* @return The MCP connection name (never null)
*/
public String getConnectionName() {
return connectionName;
}
/**
* Get the updated list of tools.
* The list is immutable - modifications will throw UnsupportedOperationException.
*
* @return List of MCP tools (never null, may be empty if all tools removed)
*/
public java.util.List<io.modelcontextprotocol.spec.McpSchema.Tool> getTools() {
return tools;
}
}Each McpSchema.Tool in the list has these properties:
package io.modelcontextprotocol.spec;
/**
* MCP Tool schema representing an available tool on a server.
* Immutable - all fields are final.
* Thread-safe.
*/
public interface McpSchema.Tool {
/**
* Unique tool name within the server.
* Used to call the tool.
*
* @return Tool name (never null)
*/
String name();
/**
* Human-readable tool description.
* Describes what the tool does and when to use it.
*
* @return Tool description (may be null)
*/
String description();
/**
* JSON Schema defining tool input parameters.
* Describes expected parameter structure and validation rules.
*
* @return JSON Schema object (never null, may be empty schema)
*/
Object inputSchema();
}Events are automatically published when:
tools/list_changed notification capabilitynotifications/tools/list_changed messageMcpToolsChangedEvent with updated tool listApplicationEventPublisherimport org.springframework.ai.mcp.McpToolsChangedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import io.modelcontextprotocol.spec.McpSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Event listener for MCP tool changes.
* Executes synchronously on event publication thread by default.
* Should be fast to avoid blocking MCP protocol.
*/
@Component
public class McpEventListener {
private static final Logger log = LoggerFactory.getLogger(McpEventListener.class);
/**
* Handle tool list change events.
* Invoked synchronously when event is published.
* Should be fast - avoid blocking operations.
*
* @param event The tools changed event (never null)
*/
@EventListener
public void handleToolsChanged(McpToolsChangedEvent event) {
String connectionName = event.getConnectionName();
var tools = event.getTools();
log.info("Tools changed for connection: {}", connectionName);
log.info("New tool count: {}", tools.size());
tools.forEach(tool ->
log.info(" - {}: {}", tool.name(),
tool.description() != null ? tool.description() : "No description")
);
}
}import org.springframework.ai.mcp.McpToolsChangedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* Type-safe event listener implementing ApplicationListener interface.
* Alternative to @EventListener annotation.
* Executes synchronously on event publication thread.
*/
@Component
public class McpToolsChangeListener implements ApplicationListener<McpToolsChangedEvent> {
private static final Logger log = LoggerFactory.getLogger(McpToolsChangeListener.class);
/**
* Handle application event.
* Called automatically by Spring when McpToolsChangedEvent is published.
*
* @param event The tools changed event (never null)
*/
@Override
public void onApplicationEvent(McpToolsChangedEvent event) {
log.info("Connection '{}' now has {} tools",
event.getConnectionName(),
event.getTools().size());
// Process tools...
}
}import org.springframework.ai.mcp.McpToolsChangedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* Asynchronous event listener for slow operations.
* Executes on separate thread pool to avoid blocking MCP protocol.
* Requires @EnableAsync on configuration class.
*/
@Component
public class AsyncMcpEventListener {
private static final Logger log = LoggerFactory.getLogger(AsyncMcpEventListener.class);
/**
* Handle tool change events asynchronously.
* Executes on async executor thread pool.
* Does not block MCP protocol processing.
*
* @param event The tools changed event (never null)
*/
@Async
@EventListener
public void handleToolsChangedAsync(McpToolsChangedEvent event) {
// Process event asynchronously
// Safe to perform slow operations here
log.info("Async processing tools for {}", event.getConnectionName());
try {
refreshToolCache(event.getConnectionName(), event.getTools());
updateDatabase(event.getConnectionName(), event.getTools());
notifyAdministrators(event.getConnectionName(), event.getTools().size());
} catch (Exception e) {
log.error("Error processing tools changed event", e);
}
}
private void refreshToolCache(String connectionName, var tools) {
// Update cache, may take time
}
private void updateDatabase(String connectionName, var tools) {
// Update database, may take time
}
private void notifyAdministrators(String connectionName, int toolCount) {
// Send notifications, may take time
}
}Enable Async Processing:
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* Configuration for async event processing.
* Configures thread pool for @Async methods.
*/
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
/**
* Configure async executor for event listeners.
* Thread pool for @Async annotated methods.
*
* @return Configured executor
*/
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("mcp-events-");
executor.initialize();
return executor;
}
}import org.springframework.ai.mcp.McpToolsChangedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.Map;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* Service that maintains a cache of available tools per connection.
* Thread-safe with concurrent map.
* Updated automatically via event listener.
*/
@Service
public class ToolCacheService {
private static final Logger log = LoggerFactory.getLogger(ToolCacheService.class);
/**
* Thread-safe cache of tools per connection.
* Key: connection name
* Value: immutable list of tools
*/
private final Map<String, List<McpSchema.Tool>> toolCache = new ConcurrentHashMap<>();
/**
* Update cache when tools change.
* Invoked automatically by Spring event system.
* Thread-safe - uses concurrent map.
*
* @param event Tools changed event
*/
@EventListener
public void updateCache(McpToolsChangedEvent event) {
String connectionName = event.getConnectionName();
List<McpSchema.Tool> tools = event.getTools();
toolCache.put(connectionName, tools);
log.info("Tool cache updated for: {} ({} tools)", connectionName, tools.size());
}
/**
* Get cached tools for a connection.
* Thread-safe.
*
* @param connectionName Connection to query
* @return List of tools (never null, may be empty)
*/
public List<McpSchema.Tool> getTools(String connectionName) {
return toolCache.getOrDefault(connectionName, List.of());
}
/**
* Get all cached tools across all connections.
* Returns defensive copy.
*
* @return Map of all tools
*/
public Map<String, List<McpSchema.Tool>> getAllTools() {
return Map.copyOf(toolCache);
}
/**
* Check if a specific tool is available.
*
* @param connectionName Connection to check
* @param toolName Tool name to find
* @return true if tool is available
*/
public boolean hasTool(String connectionName, String toolName) {
return getTools(connectionName).stream()
.anyMatch(tool -> tool.name().equals(toolName));
}
/**
* Find tool by name across all connections.
*
* @param toolName Tool name to find
* @return Map of connection names to tools with matching name
*/
public Map<String, McpSchema.Tool> findTool(String toolName) {
Map<String, McpSchema.Tool> results = new HashMap<>();
toolCache.forEach((conn, tools) -> {
tools.stream()
.filter(tool -> tool.name().equals(toolName))
.findFirst()
.ifPresent(tool -> results.put(conn, tool));
});
return results;
}
}import org.springframework.ai.mcp.McpToolsChangedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
/**
* Service that sends notifications when tools change.
* Uses async processing to avoid blocking MCP protocol.
*/
@Service
public class NotificationService {
private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
private final JavaMailSender mailSender;
private final MetricsService metricsService;
public NotificationService(JavaMailSender mailSender, MetricsService metricsService) {
this.mailSender = mailSender;
this.metricsService = metricsService;
}
/**
* Notify administrators of tool changes.
* Executes asynchronously to avoid blocking.
* Includes error handling to prevent listener failures.
*
* @param event Tools changed event
*/
@Async
@EventListener
public void notifyAdmins(McpToolsChangedEvent event) {
try {
String message = String.format(
"MCP server '%s' tools updated. New count: %d",
event.getConnectionName(),
event.getTools().size()
);
sendEmailToAdmins(message);
logToMonitoring(event.getConnectionName(), event.getTools().size());
log.info("Notifications sent for {}", event.getConnectionName());
} catch (Exception e) {
log.error("Failed to send notifications", e);
// Don't rethrow - keep event system working
}
}
private void sendEmailToAdmins(String message) {
SimpleMailMessage email = new SimpleMailMessage();
email.setTo("admins@example.com");
email.setSubject("MCP Tools Changed");
email.setText(message);
mailSender.send(email);
}
private void logToMonitoring(String connection, int toolCount) {
metricsService.recordToolCount(connection, toolCount);
}
}import org.springframework.ai.mcp.McpToolsChangedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Service that dynamically registers and tracks new tools.
* Maintains history of tool additions and removals.
*/
@Service
public class DynamicToolRegistry {
private static final Logger log = LoggerFactory.getLogger(DynamicToolRegistry.class);
/**
* Set of all tool names ever seen (for tracking new tools).
* Thread-safe concurrent set.
*/
private final Set<String> seenTools = ConcurrentHashMap.newKeySet();
/**
* History of tool changes.
* Key: connection name
* Value: list of change events
*/
private final Map<String, List<ToolChange>> changeHistory = new ConcurrentHashMap<>();
/**
* Register tools and track changes.
* Identifies new, removed, and modified tools.
*
* @param event Tools changed event
*/
@EventListener
public void registerTools(McpToolsChangedEvent event) {
String connectionName = event.getConnectionName();
List<McpSchema.Tool> newTools = event.getTools();
// Get previous tools for comparison
List<McpSchema.Tool> oldTools = getPreviousTools(connectionName);
// Find new, removed, and modified tools
Set<String> newToolNames = newTools.stream()
.map(McpSchema.Tool::name)
.collect(Collectors.toSet());
Set<String> oldToolNames = oldTools.stream()
.map(McpSchema.Tool::name)
.collect(Collectors.toSet());
// New tools
Set<String> added = new HashSet<>(newToolNames);
added.removeAll(oldToolNames);
// Removed tools
Set<String> removed = new HashSet<>(oldToolNames);
removed.removeAll(newToolNames);
// Log changes
if (!added.isEmpty()) {
log.info("New tools on {}: {}", connectionName, added);
added.forEach(toolName -> {
if (seenTools.add(toolName)) {
log.info("First time seeing tool: {}", toolName);
registerNewTool(connectionName, findTool(newTools, toolName));
}
});
}
if (!removed.isEmpty()) {
log.warn("Removed tools on {}: {}", connectionName, removed);
}
// Record change in history
recordChange(connectionName, new ToolChange(
System.currentTimeMillis(),
added.size(),
removed.size(),
newTools.size()
));
}
private List<McpSchema.Tool> getPreviousTools(String connectionName) {
// Implementation to retrieve previous tools
return List.of(); // Placeholder
}
private McpSchema.Tool findTool(List<McpSchema.Tool> tools, String name) {
return tools.stream()
.filter(tool -> tool.name().equals(name))
.findFirst()
.orElseThrow();
}
private void registerNewTool(String connection, McpSchema.Tool tool) {
log.info("Registering new tool: {} on {}", tool.name(), connection);
// Registration logic...
}
private void recordChange(String connectionName, ToolChange change) {
changeHistory.computeIfAbsent(connectionName, k -> new ArrayList<>()).add(change);
}
/**
* Get change history for a connection.
*
* @param connectionName Connection to query
* @return List of changes (newest first)
*/
public List<ToolChange> getChangeHistory(String connectionName) {
return changeHistory.getOrDefault(connectionName, List.of())
.stream()
.sorted(Comparator.comparingLong(ToolChange::timestamp).reversed())
.toList();
}
/**
* Record of a tool change event.
*/
public record ToolChange(long timestamp, int added, int removed, int total) {}
}Listen only to specific connections:
import org.springframework.ai.mcp.McpToolsChangedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* Event listener with SpEL-based filtering.
* Only processes events matching specific criteria.
*/
@Component
public class FilteredEventListener {
private static final Logger log = LoggerFactory.getLogger(FilteredEventListener.class);
/**
* Handle events only from production-server connection.
* Filter condition uses Spring Expression Language (SpEL).
*
* @param event Tools changed event from production-server
*/
@EventListener(condition = "#event.connectionName == 'production-server'")
public void handleProductionTools(McpToolsChangedEvent event) {
// Only handle events from production-server
log.info("Production tools updated: {} tools", event.getTools().size());
// Critical path - handle production changes
validateProductionTools(event.getTools());
}
/**
* Handle events only when many tools are available.
* Useful for detecting unusual situations.
*
* @param event Tools changed event with large toolset
*/
@EventListener(condition = "#event.tools.size() > 10")
public void handleLargeToolsets(McpToolsChangedEvent event) {
// Only handle when many tools are available
log.warn("Large toolset detected on {}: {} tools",
event.getConnectionName(), event.getTools().size());
// May indicate configuration issue
checkToolsetSize(event.getConnectionName(), event.getTools().size());
}
/**
* Handle events only when no tools are available.
* May indicate server issue.
*
* @param event Tools changed event with empty toolset
*/
@EventListener(condition = "#event.tools.isEmpty()")
public void handleEmptyToolset(McpToolsChangedEvent event) {
log.error("No tools available on {}", event.getConnectionName());
// Alert - server may be misconfigured
sendAlert("No tools available", event.getConnectionName());
}
/**
* Handle events only for specific tool names.
* Checks if any tool matches pattern.
*
* @param event Tools changed event
*/
@EventListener
public void handleCriticalTools(McpToolsChangedEvent event) {
// Filter in code for complex conditions
boolean hasCriticalTool = event.getTools().stream()
.anyMatch(tool -> tool.name().startsWith("critical_"));
if (hasCriticalTool) {
log.warn("Critical tools changed on {}", event.getConnectionName());
notifySecurity(event.getConnectionName(), event.getTools());
}
}
private void validateProductionTools(List<McpSchema.Tool> tools) {
// Validation logic
}
private void checkToolsetSize(String connection, int size) {
// Size check logic
}
private void sendAlert(String message, String connection) {
// Alert logic
}
private void notifySecurity(String connection, List<McpSchema.Tool> tools) {
// Security notification logic
}
}/**
* Event listener with programmatic filtering.
* More flexible than SpEL expressions.
*/
@Component
public class ProgrammaticFilterListener {
private final Set<String> monitoredConnections = Set.of(
"production-server",
"staging-server",
"critical-service"
);
/**
* Handle events with custom filtering logic.
* Can implement complex conditions not expressible in SpEL.
*
* @param event Tools changed event
*/
@EventListener
public void handleMonitoredConnections(McpToolsChangedEvent event) {
// Custom filtering logic
if (!shouldProcess(event)) {
return; // Skip this event
}
log.info("Processing monitored connection: {}", event.getConnectionName());
processEvent(event);
}
private boolean shouldProcess(McpToolsChangedEvent event) {
String connection = event.getConnectionName();
List<McpSchema.Tool> tools = event.getTools();
// Complex filtering logic
return monitoredConnections.contains(connection) &&
tools.size() > 0 &&
hasRequiredTools(tools) &&
isBusinessHours();
}
private boolean hasRequiredTools(List<McpSchema.Tool> tools) {
Set<String> required = Set.of("essential-tool-1", "essential-tool-2");
Set<String> available = tools.stream()
.map(McpSchema.Tool::name)
.collect(Collectors.toSet());
return available.containsAll(required);
}
private boolean isBusinessHours() {
// Only process during business hours
int hour = LocalTime.now().getHour();
return hour >= 9 && hour < 17;
}
private void processEvent(McpToolsChangedEvent event) {
// Event processing logic
}
}The event emitters are automatically configured for both sync and async clients:
package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Bean that creates the sync tools change event emitter.
* Automatically registered when using SYNC client type.
* Implements McpSyncClientCustomizer to add event publishing to clients.
* Thread-safe - event publishing is thread-safe.
*
* @param applicationEventPublisher Spring event publisher
* @return Sync event emitter customizer
*/
@org.springframework.context.annotation.Bean
@org.springframework.core.annotation.Order(100)
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "type",
havingValue = "SYNC",
matchIfMissing = true
)
public McpSyncToolsChangeEventEmmiter mcpSyncToolChangeEventEmmiter(
org.springframework.context.ApplicationEventPublisher applicationEventPublisher
);package org.springframework.ai.mcp.client.common.autoconfigure;
/**
* Bean that creates the async tools change event emitter.
* Automatically registered when using ASYNC client type.
* Implements McpAsyncClientCustomizer to add event publishing to clients.
* Thread-safe - event publishing is thread-safe.
*
* @param applicationEventPublisher Spring event publisher
* @return Async event emitter customizer
*/
@org.springframework.context.annotation.Bean
@org.springframework.core.annotation.Order(100)
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "type",
havingValue = "ASYNC"
)
public McpAsyncToolsChangeEventEmmiter mcpAsyncToolChangeEventEmmiter(
org.springframework.context.ApplicationEventPublisher applicationEventPublisher
);To disable automatic event publishing, you can exclude the built-in event emitters by providing your own customizers without event publishing:
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* Configuration that disables default event publishing.
* Provides no-op customizer to replace default event emitter.
*/
@Configuration
public class NoEventsConfig {
/**
* No-op customizer that replaces the default event emitter.
* Being @Primary, it takes precedence over the default implementation.
* Tools will still change but no events will be published.
*
* @return No-op customizer
*/
@Bean
@Primary
public McpSyncClientCustomizer noOpCustomizer() {
return (name, spec) -> {
// Customizer that doesn't emit events
// Being @Primary, it replaces the default event emitter
log.debug("Event publishing disabled for: {}", name);
};
}
}You can publish custom events for other MCP-related changes:
/**
* Custom event for resource list changes.
* Follows same pattern as McpToolsChangedEvent.
*/
public class McpResourcesChangedEvent extends ApplicationEvent {
private final String connectionName;
private final List<McpSchema.Resource> resources;
public McpResourcesChangedEvent(Object source, String connectionName,
List<McpSchema.Resource> resources) {
super(source);
this.connectionName = connectionName;
this.resources = List.copyOf(resources);
}
public String getConnectionName() { return connectionName; }
public List<McpSchema.Resource> getResources() { return resources; }
}
/**
* Customizer that publishes custom resource change events.
*/
@Component
public class ResourceEventEmitter implements McpSyncClientCustomizer {
private final ApplicationEventPublisher eventPublisher;
public ResourceEventEmitter(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
@Override
public void customize(String name, McpClient.SyncSpec spec) {
spec.resourceListChangeNotificationHandler(resources -> {
eventPublisher.publishEvent(
new McpResourcesChangedEvent(this, name, resources)
);
});
}
}Control the order in which listeners are invoked:
import org.springframework.core.annotation.Order;
/**
* Event listeners with explicit ordering.
* Lower @Order values are invoked first.
*/
@Component
public class OrderedEventListeners {
/**
* First listener - highest priority.
* Invoked before other listeners.
*
* @param event Tools changed event
*/
@EventListener
@Order(1)
public void firstListener(McpToolsChangedEvent event) {
log.info("First listener: {}", event.getConnectionName());
// Critical processing first
}
/**
* Second listener - medium priority.
* Invoked after first listener.
*
* @param event Tools changed event
*/
@EventListener
@Order(2)
public void secondListener(McpToolsChangedEvent event) {
log.info("Second listener: {}", event.getConnectionName());
// Normal processing
}
/**
* Last listener - lowest priority.
* Invoked after all other listeners.
*
* @param event Tools changed event
*/
@EventListener
@Order(Ordered.LOWEST_PRECEDENCE)
public void lastListener(McpToolsChangedEvent event) {
log.info("Last listener: {}", event.getConnectionName());
// Cleanup or logging
}
}/**
* Event listener with robust error handling.
* Prevents one failing listener from affecting others.
*/
@Component
public class RobustEventListener {
/**
* Handle events with comprehensive error handling.
* Catches all exceptions to prevent propagation.
*
* @param event Tools changed event
*/
@EventListener
public void handleWithErrorHandling(McpToolsChangedEvent event) {
try {
// Main processing logic
processTools(event.getTools());
updateCache(event.getConnectionName(), event.getTools());
} catch (DatabaseException e) {
log.error("Database error processing tools for {}",
event.getConnectionName(), e);
// Handle database errors specifically
handleDatabaseError(e);
} catch (NetworkException e) {
log.error("Network error processing tools for {}",
event.getConnectionName(), e);
// Handle network errors specifically
scheduleRetry(event);
} catch (Exception e) {
log.error("Unexpected error processing tools for {}",
event.getConnectionName(), e);
// Handle all other errors
reportError(event.getConnectionName(), e);
} finally {
// Always execute cleanup
log.debug("Finished processing tools for {}", event.getConnectionName());
}
}
private void processTools(List<McpSchema.Tool> tools) {
// Processing logic that may throw exceptions
}
private void updateCache(String connection, List<McpSchema.Tool> tools) {
// Cache update that may throw exceptions
}
private void handleDatabaseError(DatabaseException e) {
// Database error handling
}
private void scheduleRetry(McpToolsChangedEvent event) {
// Retry scheduling
}
private void reportError(String connection, Exception e) {
// Error reporting
}
}import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.ApplicationEventPublisher;
import org.junit.jupiter.api.Test;
/**
* Test class for event listeners.
* Demonstrates how to test event handling logic.
*/
@SpringBootTest
class EventListenerTest {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private ToolCacheService toolCacheService;
@Test
void testToolCacheUpdate() {
// Given
List<McpSchema.Tool> tools = List.of(
createTool("tool1", "Description 1"),
createTool("tool2", "Description 2")
);
// When
eventPublisher.publishEvent(
new McpToolsChangedEvent(this, "test-connection", tools)
);
// Then
List<McpSchema.Tool> cachedTools = toolCacheService.getTools("test-connection");
assertThat(cachedTools).hasSize(2);
assertThat(cachedTools).extracting(McpSchema.Tool::name)
.containsExactly("tool1", "tool2");
}
private McpSchema.Tool createTool(String name, String description) {
// Create mock tool for testing
return new MockTool(name, description, new Object());
}
}tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux@1.1.0