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: 138 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ The Spring integration module provides seamless integration with Spring AI and S
- **`@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
- **`@McpResourceListChanged`** - Annotates methods that handle resource list change notifications from MCP servers

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

#### Resource List Changed
- `AbstractMcpResourceListChangedMethodCallback` - Base class for resource list changed method callbacks
- `SyncMcpResourceListChangedMethodCallback` - Synchronous implementation
- `AsyncMcpResourceListChangedMethodCallback` - Asynchronous implementation using Reactor's Mono

### Providers

The project includes provider classes that scan for annotated methods and create appropriate callbacks:
Expand All @@ -208,6 +214,8 @@ The project includes provider classes that scan for annotated methods and create
- `AsyncMcpProgressProvider` - Processes `@McpProgress` annotations for asynchronous operations
- `SyncMcpToolListChangedProvider` - Processes `@McpToolListChanged` annotations for synchronous operations
- `AsyncMcpToolListChangedProvider` - Processes `@McpToolListChanged` annotations for asynchronous operations
- `SyncMcpResourceListChangedProvider` - Processes `@McpResourceListChanged` annotations for synchronous operations
- `AsyncMcpResourceListChangedProvider` - Processes `@McpResourceListChanged` annotations for asynchronous operations

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

### Mcp Client Resource List Changed Example

```java
public class ResourceListChangedHandler {

/**
* Handle resource list change notifications with a single parameter.
* @param updatedResources The updated list of resources after the change
*/
@McpResourceListChanged
public void handleResourceListChanged(List<McpSchema.Resource> updatedResources) {
System.out.println("Resource list updated, now contains " + updatedResources.size() + " resources:");
for (McpSchema.Resource resource : updatedResources) {
System.out.println(" - " + resource.name() + ": " + resource.description());
}
}

/**
* Handle resource list change notifications for a specific client.
* @param updatedResources The updated list of resources after the change
*/
@McpResourceListChanged(clientId = "client-1")
public void handleClient1ResourceListChanged(List<McpSchema.Resource> updatedResources) {
System.out.println("Client-1 resource list updated with " + updatedResources.size() + " resources");
// Process the updated resource list for client-1
updateClientResourceCache("client-1", updatedResources);
}

/**
* Handle resource list change notifications for another specific client.
* @param updatedResources The updated list of resources after the change
*/
@McpResourceListChanged(clientId = "client-2")
public void handleClient2ResourceListChanged(List<McpSchema.Resource> updatedResources) {
System.out.println("Client-2 resource list updated with " + updatedResources.size() + " resources");
// Process the updated resource list for client-2
updateClientResourceCache("client-2", updatedResources);
}

private void updateClientResourceCache(String clientId, List<McpSchema.Resource> resources) {
// Implementation to update resource cache for specific client
System.out.println("Updated resource cache for " + clientId + " with " + resources.size() + " resources");
}
}

public class AsyncResourceListChangedHandler {

/**
* Handle resource list change notifications asynchronously.
* @param updatedResources The updated list of resources after the change
* @return A Mono that completes when the notification is handled
*/
@McpResourceListChanged
public Mono<Void> handleAsyncResourceListChanged(List<McpSchema.Resource> updatedResources) {
return Mono.fromRunnable(() -> {
System.out.println("Async resource list update: " + updatedResources.size() + " resources");
// Process the updated resource list asynchronously
processResourceListUpdate(updatedResources);
});
}

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

private void processResourceListUpdate(List<McpSchema.Resource> resources) {
// Implementation to process resource list update
System.out.println("Processing resource list update with " + resources.size() + " resources");
}

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

public class MyMcpClient {

public static McpSyncClient createSyncClientWithResourceListChanged(ResourceListChangedHandler resourceListChangedHandler) {
List<Consumer<List<McpSchema.Resource>>> resourceListChangedConsumers =
new SyncMcpResourceListChangedProvider(List.of(resourceListChangedHandler)).getResourceListChangedConsumers();

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

return client;
}

public static McpAsyncClient createAsyncClientWithResourceListChanged(AsyncResourceListChangedHandler asyncResourceListChangedHandler) {
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourceListChangedHandlers =
new AsyncMcpResourceListChangedProvider(List.of(asyncResourceListChangedHandler)).getResourceListChangedHandlers();

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

return client;
}
}
```

### Mcp Client Elicitation Example

```java
Expand Down Expand Up @@ -1466,7 +1592,6 @@ public class MyMcpClient {
}
```


### Stateless Examples

The library supports stateless implementations that use `McpTransportContext` instead of `McpSyncServerExchange` or `McpAsyncServerExchange`. This is useful for scenarios where you don't need the full server exchange context.
Expand Down Expand Up @@ -1745,6 +1870,18 @@ public class McpConfig {
return SpringAiMcpAnnotationProvider.createAsyncToolListChangedSpecifications(asyncToolListChangedHandlers);
}

@Bean
public List<SyncResourceListChangedSpecification> syncResourceListChangedSpecifications(
List<ResourceListChangedHandler> resourceListChangedHandlers) {
return SpringAiMcpAnnotationProvider.createSyncResourceListChangedSpecifications(resourceListChangedHandlers);
}

@Bean
public List<AsyncResourceListChangedSpecification> asyncResourceListChangedSpecifications(
List<AsyncResourceListChangedHandler> asyncResourceListChangedHandlers) {
return SpringAiMcpAnnotationProvider.createAsyncResourceListChangedSpecifications(asyncResourceListChangedHandlers);
}

// Stateless Spring Integration Examples

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

import org.springaicommunity.mcp.method.changed.resource.AsyncResourceListChangedSpecification;
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.resource.AsyncMcpResourceListChangedProvider;
import org.springaicommunity.mcp.provider.changed.tool.AsyncMcpToolListChangedProvider;
import org.springaicommunity.mcp.provider.elicitation.AsyncMcpElicitationProvider;
import org.springaicommunity.mcp.provider.logging.AsyncMcpLoggingProvider;
Expand Down Expand Up @@ -158,6 +160,19 @@ protected Method[] doGetClassMethods(Object bean) {

}

private static class SpringAiAsyncMcpResourceListChangedProvider extends AsyncMcpResourceListChangedProvider {

public SpringAiAsyncMcpResourceListChangedProvider(List<Object> resourceListChangedObjects) {
super(resourceListChangedObjects);
}

@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 @@ -199,4 +214,10 @@ public static List<AsyncToolListChangedSpecification> createAsyncToolListChanged
return new SpringAiAsyncMcpToolListChangedProvider(toolListChangedObjects).getToolListChangedSpecifications();
}

public static List<AsyncResourceListChangedSpecification> createAsyncResourceListChangedSpecifications(
List<Object> resourceListChangedObjects) {
return new SpringAiAsyncMcpResourceListChangedProvider(resourceListChangedObjects)
.getResourceListChangedSpecifications();
}

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

import org.springaicommunity.mcp.method.changed.resource.SyncResourceListChangedSpecification;
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.resource.SyncMcpResourceListChangedProvider;
import org.springaicommunity.mcp.provider.changed.tool.SyncMcpToolListChangedProvider;
import org.springaicommunity.mcp.provider.complete.SyncMcpCompletionProvider;
import org.springaicommunity.mcp.provider.elicitation.SyncMcpElicitationProvider;
Expand Down Expand Up @@ -203,6 +205,19 @@ protected Method[] doGetClassMethods(Object bean) {

}

private static class SpringAiSyncMcpResourceListChangedProvider extends SyncMcpResourceListChangedProvider {

public SpringAiSyncMcpResourceListChangedProvider(List<Object> resourceListChangedObjects) {
super(resourceListChangedObjects);
}

@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 @@ -256,4 +271,10 @@ public static List<SyncToolListChangedSpecification> createSyncToolListChangedSp
return new SpringAiSyncMcpToolListChangedProvider(toolListChangedObjects).getToolListChangedSpecifications();
}

public static List<SyncResourceListChangedSpecification> createSyncResourceListChangedSpecifications(
List<Object> resourceListChangedObjects) {
return new SpringAiSyncMcpResourceListChangedProvider(resourceListChangedObjects)
.getResourceListChangedSpecifications();
}

}
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 resource 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 resources changes on an MCP server. According to the MCP
* specification, servers that declare the {@code listChanged} capability will send
* notifications when their resource 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.Resource>} that represents the updated list of
* resources after the change notification.
*
* <p>
* Example usage: <pre>{@code
* &#64;McpResourceListChanged
* public void onResourceListChanged(List<McpSchema.Resource> updatedResources) {
* // Handle resource list change notification with the updated resources
* logger.info("Resource list updated, now contains {} resources", updatedResources.size());
* // Process the updated resource list
* }
*
* &#64;McpResourceListChanged
* public Mono<Void> onResourceListChangedAsync(List<McpSchema.Resource> updatedResources) {
* // Handle resource list change notification asynchronously
* return processUpdatedResources(updatedResources);
* }
* }</pre>
*
* @author Christian Tzolov
* @see <a href=
* "https://modelcontextprotocol.io/specification/2025-06-18/server/resources#list-changed-notification">MCP
* Resource List Changed Notification</a>
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpResourceListChanged {

/**
* Used as connection or client identifier to select the MCP client that the resource
* 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 resource list change notifications.
* @return the client identifier, or empty string to listen to all clients
*/
String clientId() default "";

}
Loading