Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ The Spring integration module provides seamless integration with Spring AI and S
- **`@McpSampling`** - Annotates methods that handle sampling requests from MCP servers
- **`@McpElicitation`** - Annotates methods that handle elicitation requests to gather additional information from users
- **`@McpProgress`** - Annotates methods that handle progress notifications for long-running operations
- **`@McpToolListChanged`** - Annotates methods that handle tool list change notifications from MCP servers

#### Server
- **`@McpComplete`** - Annotates methods that provide completion functionality for prompts or URI templates
Expand Down Expand Up @@ -182,6 +183,11 @@ The modules provide callback implementations for each operation type:
- `SyncMcpProgressMethodCallback` - Synchronous implementation
- `AsyncMcpProgressMethodCallback` - Asynchronous implementation using Reactor's Mono

#### Tool List Changed
- `AbstractMcpToolListChangedMethodCallback` - Base class for tool list changed method callbacks
- `SyncMcpToolListChangedMethodCallback` - Synchronous implementation
- `AsyncMcpToolListChangedMethodCallback` - Asynchronous implementation using Reactor's Mono

### Providers

The project includes provider classes that scan for annotated methods and create appropriate callbacks:
Expand All @@ -200,6 +206,8 @@ The project includes provider classes that scan for annotated methods and create
- `AsyncMcpElicitationProvider` - Processes `@McpElicitation` annotations for asynchronous operations
- `SyncMcpProgressProvider` - Processes `@McpProgress` annotations for synchronous operations
- `AsyncMcpProgressProvider` - Processes `@McpProgress` annotations for asynchronous operations
- `SyncMcpToolListChangedProvider` - Processes `@McpToolListChanged` annotations for synchronous operations
- `AsyncMcpToolListChangedProvider` - Processes `@McpToolListChanged` annotations for asynchronous operations

#### Stateless Providers (using McpTransportContext)
- `SyncStatelessMcpCompleteProvider` - Processes `@McpComplete` annotations for synchronous stateless operations
Expand Down Expand Up @@ -1189,6 +1197,124 @@ public class MyMcpClient {
}
```

### Mcp Client Tool List Changed Example

```java
public class ToolListChangedHandler {

/**
* Handle tool list change notifications with a single parameter.
* @param updatedTools The updated list of tools after the change
*/
@McpToolListChanged
public void handleToolListChanged(List<McpSchema.Tool> updatedTools) {
System.out.println("Tool list updated, now contains " + updatedTools.size() + " tools:");
for (McpSchema.Tool tool : updatedTools) {
System.out.println(" - " + tool.name() + ": " + tool.description());
}
}

/**
* Handle tool list change notifications for a specific client.
* @param updatedTools The updated list of tools after the change
*/
@McpToolListChanged(clientId = "client-1")
public void handleClient1ToolListChanged(List<McpSchema.Tool> updatedTools) {
System.out.println("Client-1 tool list updated with " + updatedTools.size() + " tools");
// Process the updated tool list for client-1
updateClientToolCache("client-1", updatedTools);
}

/**
* Handle tool list change notifications for another specific client.
* @param updatedTools The updated list of tools after the change
*/
@McpToolListChanged(clientId = "client-2")
public void handleClient2ToolListChanged(List<McpSchema.Tool> updatedTools) {
System.out.println("Client-2 tool list updated with " + updatedTools.size() + " tools");
// Process the updated tool list for client-2
updateClientToolCache("client-2", updatedTools);
}

private void updateClientToolCache(String clientId, List<McpSchema.Tool> tools) {
// Implementation to update tool cache for specific client
System.out.println("Updated tool cache for " + clientId + " with " + tools.size() + " tools");
}
}

public class AsyncToolListChangedHandler {

/**
* Handle tool list change notifications asynchronously.
* @param updatedTools The updated list of tools after the change
* @return A Mono that completes when the notification is handled
*/
@McpToolListChanged
public Mono<Void> handleAsyncToolListChanged(List<McpSchema.Tool> updatedTools) {
return Mono.fromRunnable(() -> {
System.out.println("Async tool list update: " + updatedTools.size() + " tools");
// Process the updated tool list asynchronously
processToolListUpdate(updatedTools);
});
}

/**
* Handle tool list change notifications for a specific client asynchronously.
* @param updatedTools The updated list of tools after the change
* @return A Mono that completes when the notification is handled
*/
@McpToolListChanged(clientId = "client-2")
public Mono<Void> handleClient2AsyncToolListChanged(List<McpSchema.Tool> updatedTools) {
return Mono.fromRunnable(() -> {
System.out.println("Client-2 async tool list update: " + updatedTools.size() + " tools");
// Process the updated tool list for client-2 asynchronously
processClientToolListUpdate("client-2", updatedTools);
}).then();
}

private void processToolListUpdate(List<McpSchema.Tool> tools) {
// Implementation to process tool list update
System.out.println("Processing tool list update with " + tools.size() + " tools");
}

private void processClientToolListUpdate(String clientId, List<McpSchema.Tool> tools) {
// Implementation to process tool list update for specific client
System.out.println("Processing tool list update for " + clientId + " with " + tools.size() + " tools");
}
}

public class MyMcpClient {

public static McpSyncClient createSyncClientWithToolListChanged(ToolListChangedHandler toolListChangedHandler) {
List<Consumer<List<McpSchema.Tool>>> toolListChangedConsumers =
new SyncMcpToolListChangedProvider(List.of(toolListChangedHandler)).getToolListChangedConsumers();

McpSyncClient client = McpClient.sync(transport)
.capabilities(ClientCapabilities.builder()
// Enable capabilities...
.build())
.toolListChangedConsumers(toolListChangedConsumers)
.build();

return client;
}

public static McpAsyncClient createAsyncClientWithToolListChanged(AsyncToolListChangedHandler asyncToolListChangedHandler) {
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolListChangedHandlers =
new AsyncMcpToolListChangedProvider(List.of(asyncToolListChangedHandler)).getToolListChangedHandlers();

McpAsyncClient client = McpClient.async(transport)
.capabilities(ClientCapabilities.builder()
// Enable capabilities...
.build())
.toolListChangedHandlers(toolListChangedHandlers)
.build();

return client;
}
}
```

### Mcp Client Elicitation Example

```java
Expand Down Expand Up @@ -1607,6 +1733,18 @@ public class McpConfig {
return SpringAiMcpAnnotationProvider.createAsyncProgressSpecifications(asyncProgressHandlers);
}

@Bean
public List<SyncToolListChangedSpecification> syncToolListChangedSpecifications(
List<ToolListChangedHandler> toolListChangedHandlers) {
return SpringAiMcpAnnotationProvider.createSyncToolListChangedSpecifications(toolListChangedHandlers);
}

@Bean
public List<AsyncToolListChangedSpecification> asyncToolListChangedSpecifications(
List<AsyncToolListChangedHandler> asyncToolListChangedHandlers) {
return SpringAiMcpAnnotationProvider.createAsyncToolListChangedSpecifications(asyncToolListChangedHandlers);
}

// Stateless Spring Integration Examples

@Bean
Expand Down Expand Up @@ -1643,6 +1781,7 @@ public class McpConfig {
- **Logging consumer support** - Handle logging message notifications from MCP servers
- **Sampling support** - Handle sampling requests from MCP servers
- **Progress notification support** - Handle progress notifications for long-running operations
- **Tool list changed support** - Handle tool list change notifications from MCP servers when tools are dynamically added, removed, or modified
- **Spring integration** - Seamless integration with Spring Framework and Spring AI, including support for both stateful and stateless operations
- **AOP proxy support** - Proper handling of Spring AOP proxies when processing annotations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
import java.lang.reflect.Method;
import java.util.List;

import org.springaicommunity.mcp.method.changed.tool.AsyncToolListChangedSpecification;
import org.springaicommunity.mcp.method.elicitation.AsyncElicitationSpecification;
import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification;
import org.springaicommunity.mcp.method.progress.AsyncProgressSpecification;
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
import org.springaicommunity.mcp.provider.changed.tool.AsyncMcpToolListChangedProvider;
import org.springaicommunity.mcp.provider.elicitation.AsyncMcpElicitationProvider;
import org.springaicommunity.mcp.provider.logging.AsyncMcpLoggingProvider;
import org.springaicommunity.mcp.provider.progress.AsyncMcpProgressProvider;
Expand Down Expand Up @@ -143,6 +145,19 @@ protected Method[] doGetClassMethods(Object bean) {

}

private static class SpringAiAsyncMcpToolListChangedProvider extends AsyncMcpToolListChangedProvider {

public SpringAiAsyncMcpToolListChangedProvider(List<Object> toolListChangedObjects) {
super(toolListChangedObjects);
}

@Override
protected Method[] doGetClassMethods(Object bean) {
return AnnotationProviderUtil.beanMethods(bean);
}

}

public static List<AsyncLoggingSpecification> createAsyncLoggingSpecifications(List<Object> loggingObjects) {
return new SpringAiAsyncMcpLoggingProvider(loggingObjects).getLoggingSpecifications();
}
Expand Down Expand Up @@ -179,4 +194,9 @@ public static List<AsyncProgressSpecification> createAsyncProgressSpecifications
return new SpringAiAsyncMcpProgressProvider(progressObjects).getProgressSpecifications();
}

public static List<AsyncToolListChangedSpecification> createAsyncToolListChangedSpecifications(
List<Object> toolListChangedObjects) {
return new SpringAiAsyncMcpToolListChangedProvider(toolListChangedObjects).getToolListChangedSpecifications();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
import java.lang.reflect.Method;
import java.util.List;

import org.springaicommunity.mcp.method.changed.tool.SyncToolListChangedSpecification;
import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification;
import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification;
import org.springaicommunity.mcp.method.progress.SyncProgressSpecification;
import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification;
import org.springaicommunity.mcp.provider.changed.tool.SyncMcpToolListChangedProvider;
import org.springaicommunity.mcp.provider.complete.SyncMcpCompletionProvider;
import org.springaicommunity.mcp.provider.elicitation.SyncMcpElicitationProvider;
import org.springaicommunity.mcp.provider.logging.SyncMcpLogginProvider;
Expand Down Expand Up @@ -188,6 +190,19 @@ protected Method[] doGetClassMethods(Object bean) {

}

private static class SpringAiSyncMcpToolListChangedProvider extends SyncMcpToolListChangedProvider {

public SpringAiSyncMcpToolListChangedProvider(List<Object> toolListChangedObjects) {
super(toolListChangedObjects);
}

@Override
protected Method[] doGetClassMethods(Object bean) {
return AnnotationProviderUtil.beanMethods(bean);
}

}

public static List<SyncToolSpecification> createSyncToolSpecifications(List<Object> toolObjects) {
return new SpringAiSyncToolProvider(toolObjects).getToolSpecifications();
}
Expand Down Expand Up @@ -236,4 +251,9 @@ public static List<SyncProgressSpecification> createSyncProgressSpecifications(L
return new SpringAiSyncMcpProgressProvider(progressObjects).getProgressSpecifications();
}

public static List<SyncToolListChangedSpecification> createSyncToolListChangedSpecifications(
List<Object> toolListChangedObjects) {
return new SpringAiSyncMcpToolListChangedProvider(toolListChangedObjects).getToolListChangedSpecifications();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2025-2025 the original author or authors.
*/

package org.springaicommunity.mcp.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation for methods that handle tool list change notifications from MCP servers.
* This annotation is applicable only for MCP clients.
*
* <p>
* Methods annotated with this annotation are used to listen for notifications when the
* list of available tools changes on an MCP server. According to the MCP specification,
* servers that declare the {@code listChanged} capability will send notifications when
* their tool list is modified.
*
* <p>
* The annotated method must have a void return type for synchronous consumers, or can
* return {@code Mono<Void>} for asynchronous consumers. The method should accept a single
* parameter of type {@code List<McpSchema.Tool>} that represents the updated list of
* tools after the change notification.
*
* <p>
* Example usage: <pre>{@code
* &#64;McpToolListChanged
* public void onToolListChanged(List<McpSchema.Tool> updatedTools) {
* // Handle tool list change notification with the updated tools
* logger.info("Tool list updated, now contains {} tools", updatedTools.size());
* // Process the updated tool list
* }
*
* &#64;McpToolListChanged
* public Mono<Void> onToolListChangedAsync(List<McpSchema.Tool> updatedTools) {
* // Handle tool list change notification asynchronously
* return processUpdatedTools(updatedTools);
* }
* }</pre>
*
* @author Christian Tzolov
* @see <a href=
* "https://modelcontextprotocol.io/specification/2025-06-18/server/tools#list-changed-notification">MCP
* Tool List Changed Notification</a>
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpToolListChanged {

/**
* Used as connection or client identifier to select the MCP client that the tool
* change listener is associated with. If not specified, the listener is applied to
* all clients and will receive notifications from any connected MCP server that
* supports tool list change notifications.
* @return the client identifier, or empty string to listen to all clients
*/
String clientId() default "";

}
Loading