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
142 changes: 142 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ The core module provides a set of annotations and callback implementations for p
5. **Logging Consumer** - For handling logging message notifications
6. **Sampling** - For handling sampling requests
7. **Elicitation** - For handling elicitation requests to gather additional information from users
8. **Progress** - For handling progress notifications during long-running operations

Each operation type has both synchronous and asynchronous implementations, allowing for flexible integration with different application architectures.

Expand All @@ -110,6 +111,7 @@ The Spring integration module provides seamless integration with Spring AI and S
- **`@McpLoggingConsumer`** - Annotates methods that handle logging message notifications from MCP servers
- **`@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
- **`@McpArg`** - Annotates method parameters as MCP arguments

### Method Callbacks
Expand Down Expand Up @@ -160,6 +162,11 @@ The modules provide callback implementations for each operation type:
- `SyncMcpElicitationMethodCallback` - Synchronous implementation
- `AsyncMcpElicitationMethodCallback` - Asynchronous implementation using Reactor's Mono

#### Progress
- `AbstractMcpProgressMethodCallback` - Base class for progress method callbacks
- `SyncMcpProgressMethodCallback` - Synchronous implementation
- `AsyncMcpProgressMethodCallback` - Asynchronous implementation using Reactor's Mono

### Providers

The project includes provider classes that scan for annotated methods and create appropriate callbacks:
Expand All @@ -176,6 +183,8 @@ The project includes provider classes that scan for annotated methods and create
- `AsyncMcpSamplingProvider` - Processes `@McpSampling` annotations for asynchronous operations
- `SyncMcpElicitationProvider` - Processes `@McpElicitation` annotations for synchronous operations
- `AsyncMcpElicitationProvider` - Processes `@McpElicitation` annotations for asynchronous operations
- `SyncMcpProgressProvider` - Processes `@McpProgress` annotations for synchronous operations
- `AsyncMcpProgressProvider` - Processes `@McpProgress` annotations for asynchronous operations

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

### Mcp Client Progress Example

```java
public class ProgressHandler {

/**
* Handle progress notifications with a single parameter.
* @param notification The progress notification
*/
@McpProgress
public void handleProgressNotification(ProgressNotification notification) {
System.out.println(String.format("Progress: %.2f%% - %s",
notification.progress() * 100,
notification.message()));
}

/**
* Handle progress notifications with individual parameters.
* @param progressToken The progress token identifying the operation
* @param progress The current progress (0.0 to 1.0)
* @param total Optional total value for the operation
* @param message Optional progress message
*/
@McpProgress
public void handleProgressWithParams(String progressToken, double progress, Double total, String message) {
if (total != null) {
System.out.println(String.format("Progress [%s]: %.0f/%.0f - %s",
progressToken, progress, total, message));
} else {
System.out.println(String.format("Progress [%s]: %.2f%% - %s",
progressToken, progress * 100, message));
}
}

/**
* Handle progress notifications for a specific client.
* @param notification The progress notification
*/
@McpProgress(clientId = "client-1")
public void handleClient1Progress(ProgressNotification notification) {
System.out.println(String.format("Client-1 Progress: %.2f%% - %s",
notification.progress() * 100,
notification.message()));
}
}

public class AsyncProgressHandler {

/**
* Handle progress notifications asynchronously.
* @param notification The progress notification
* @return A Mono that completes when the notification is handled
*/
@McpProgress
public Mono<Void> handleAsyncProgress(ProgressNotification notification) {
return Mono.fromRunnable(() -> {
System.out.println(String.format("Async Progress: %.2f%% - %s",
notification.progress() * 100,
notification.message()));
});
}

/**
* Handle progress notifications for a specific client asynchronously.
* @param progressToken The progress token
* @param progress The current progress
* @param total Optional total value
* @param message Optional message
* @return A Mono that completes when the notification is handled
*/
@McpProgress(clientId = "client-2")
public Mono<Void> handleClient2AsyncProgress(
String progressToken,
double progress,
Double total,
String message) {

return Mono.fromRunnable(() -> {
String progressText = total != null ?
String.format("%.0f/%.0f", progress, total) :
String.format("%.2f%%", progress * 100);

System.out.println(String.format("Client-2 Progress [%s]: %s - %s",
progressToken, progressText, message));
}).then();
}
}

public class MyMcpClient {

public static McpSyncClient createSyncClientWithProgress(ProgressHandler progressHandler) {
List<Consumer<ProgressNotification>> progressConsumers =
new SyncMcpProgressProvider(List.of(progressHandler)).getProgressConsumers();

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

return client;
}

public static McpAsyncClient createAsyncClientWithProgress(AsyncProgressHandler asyncProgressHandler) {
List<Function<ProgressNotification, Mono<Void>>> progressHandlers =
new AsyncMcpProgressProvider(List.of(asyncProgressHandler)).getProgressHandlers();

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

return client;
}
}
```

### Mcp Client Elicitation Example

```java
Expand Down Expand Up @@ -1213,6 +1342,18 @@ public class McpConfig {
return SpringAiMcpAnnotationProvider.createAsyncElicitationSpecifications(asyncElicitationHandlers);
}

@Bean
public List<SyncProgressSpecification> syncProgressSpecifications(
List<ProgressHandler> progressHandlers) {
return SpringAiMcpAnnotationProvider.createSyncProgressSpecifications(progressHandlers);
}

@Bean
public List<AsyncProgressSpecification> asyncProgressSpecifications(
List<AsyncProgressHandler> asyncProgressHandlers) {
return SpringAiMcpAnnotationProvider.createAsyncProgressSpecifications(asyncProgressHandlers);
}

// Stateless Spring Integration Examples

@Bean
Expand Down Expand Up @@ -1248,6 +1389,7 @@ public class McpConfig {
- **Dynamic schema support via CallToolRequest** - Tools can accept `CallToolRequest` parameters to handle dynamic schemas at runtime
- **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
- **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 @@ -20,9 +20,11 @@

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.AsyncMcpElicitationProvider;
import org.springaicommunity.mcp.provider.AsyncMcpLoggingConsumerProvider;
import org.springaicommunity.mcp.provider.AsyncMcpProgressProvider;
import org.springaicommunity.mcp.provider.AsyncMcpSamplingProvider;
import org.springaicommunity.mcp.provider.AsyncMcpToolProvider;
import org.springaicommunity.mcp.provider.AsyncStatelessMcpPromptProvider;
Expand Down Expand Up @@ -128,6 +130,19 @@ protected Method[] doGetClassMethods(Object bean) {

}

private static class SpringAiAsyncMcpProgressProvider extends AsyncMcpProgressProvider {

public SpringAiAsyncMcpProgressProvider(List<Object> progressObjects) {
super(progressObjects);
}

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

}

public static List<AsyncLoggingSpecification> createAsyncLoggingSpecifications(List<Object> loggingObjects) {
return new SpringAiAsyncMcpLoggingConsumerProvider(loggingObjects).getLoggingSpecifications();
}
Expand Down Expand Up @@ -160,4 +175,8 @@ public static List<McpStatelessServerFeatures.AsyncResourceSpecification> create
return new SpringAiAsyncStatelessResourceProvider(resourceObjects).getResourceSpecifications();
}

public static List<AsyncProgressSpecification> createAsyncProgressSpecifications(List<Object> progressObjects) {
return new SpringAiAsyncMcpProgressProvider(progressObjects).getProgressSpecifications();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@

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.SyncMcpCompletionProvider;
import org.springaicommunity.mcp.provider.SyncMcpElicitationProvider;
import org.springaicommunity.mcp.provider.SyncMcpLoggingConsumerProvider;
import org.springaicommunity.mcp.provider.SyncMcpProgressProvider;
import org.springaicommunity.mcp.provider.SyncMcpPromptProvider;
import org.springaicommunity.mcp.provider.SyncMcpResourceProvider;
import org.springaicommunity.mcp.provider.SyncMcpSamplingProvider;
Expand Down Expand Up @@ -173,6 +175,19 @@ protected Method[] doGetClassMethods(Object bean) {

}

private static class SpringAiSyncMcpProgressProvider extends SyncMcpProgressProvider {

public SpringAiSyncMcpProgressProvider(List<Object> progressObjects) {
super(progressObjects);
}

@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 @@ -217,4 +232,8 @@ public static List<SyncElicitationSpecification> createSyncElicitationSpecificat
return new SpringAiSyncMcpElicitationProvider(elicitationObjects).getElicitationSpecifications();
}

public static List<SyncProgressSpecification> createSyncProgressSpecifications(List<Object> progressObjects) {
return new SpringAiSyncMcpProgressProvider(progressObjects).getProgressSpecifications();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
import java.lang.annotation.Target;

/**
* Annotation for methods that handle elicitation requests from MCP servers.
* Annotation for methods that handle elicitation requests from MCP servers. This
* annotation is applicable only for MCP clients.
*
* <p>
* Methods annotated with this annotation can be used to process elicitation requests from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
import java.lang.annotation.Target;

/**
* Annotation for methods that handle logging message notifications from MCP servers.
* Annotation for methods that handle logging message notifications from MCP servers. This
* annotation is applicable only for MCP clients.
*
* <p>
* Methods annotated with this annotation can be used to consume logging messages from MCP
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 progress notifications from MCP servers. This
* annotation is applicable only for MCP clients.
*
* <p>
* Methods annotated with this annotation can be used to consume progress messages from
* MCP servers. The methods takes a single parameter of type
* {@code ProgressMessageNotification}
*
*
* <p>
* Example usage: <pre>{@code
* &#64;McpProgress
* public void handleProgressMessage(ProgressMessageNotification notification) {
* // Handle the notification *
* }</pre>
*
* @author Christian Tzolov
*
* @see io.modelcontextprotocol.spec.McpSchema.ProgressMessageNotification
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpProgress {

/**
* Used as connection or client identifier to select the MCP client, the logging
* consumer is associated with. If not specified, is applied to all clients.
*/
String clientId() default "";

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
import java.lang.annotation.Target;

/**
* Annotation for methods that handle sampling requests from MCP servers.
* Annotation for methods that handle sampling requests from MCP servers. This annotation
* is applicable only for MCP clients.
*
* <p>
* Methods annotated with this annotation can be used to process sampling requests from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import org.springaicommunity.mcp.annotation.McpLoggingConsumer;

import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.util.Assert;
Expand Down Expand Up @@ -144,15 +143,6 @@ protected Object[] buildArgs(Method method, Object exchange, LoggingMessageNotif
return args;
}

/**
* Checks if a parameter type is compatible with the exchange type. This method should
* be implemented by subclasses to handle specific exchange type checking.
* @param paramType The parameter type to check
* @return true if the parameter type is compatible with the exchange type, false
* otherwise
*/
protected abstract boolean isExchangeType(Class<?> paramType);

/**
* Exception thrown when there is an error invoking a logging consumer method.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,6 @@ protected void validateReturnType(Method method) {
}
}

/**
* Checks if a parameter type is compatible with the exchange type.
* @param paramType The parameter type to check
* @return true if the parameter type is compatible with the exchange type, false
* otherwise
*/
@Override
protected boolean isExchangeType(Class<?> paramType) {
// No exchange type for logging consumer methods
return false;
}

/**
* Builder for creating AsyncMcpLoggingConsumerMethodCallback instances.
* <p>
Expand Down
Loading