diff --git a/README.md b/README.md index b2b077d..b8191b0 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ The Spring integration module provides seamless integration with Spring AI and S - **`@McpLogging`** - Annotates methods that handle logging message notifications from MCP servers (requires `clientId` parameter) - **`@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 +- **`@McpProgress`** - Annotates methods that handle progress notifications for long-running operations (requires `clientId` parameter) - **`@McpToolListChanged`** - Annotates methods that handle tool list change notifications from MCP servers - **`@McpResourceListChanged`** - Annotates methods that handle resource list change notifications from MCP servers - **`@McpPromptListChanged`** - Annotates methods that handle prompt list change notifications from MCP servers @@ -1103,9 +1103,10 @@ public class ProgressHandler { /** * Handle progress notifications with a single parameter. + * Note: clientId is now required for all @McpProgress annotations. * @param notification The progress notification */ - @McpProgress + @McpProgress(clientId = "default-client") public void handleProgressNotification(ProgressNotification notification) { System.out.println(String.format("Progress: %.2f%% - %s", notification.progress() * 100, @@ -1114,12 +1115,13 @@ public class ProgressHandler { /** * Handle progress notifications with individual parameters. + * Note: clientId is now required for all @McpProgress annotations. * @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 + @McpProgress(clientId = "default-client") public void handleProgressWithParams(String progressToken, double progress, Double total, String message) { if (total != null) { System.out.println(String.format("Progress [%s]: %.0f/%.0f - %s", diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpProgress.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpProgress.java index 8f6f35b..ecb71e7 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpProgress.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/annotation/McpProgress.java @@ -21,9 +21,9 @@ * *

* Example usage:

{@code
- * @McpProgress
+ * @McpProgress(clientId = "my-client-id")
  * public void handleProgressMessage(ProgressNotification notification) {
- *     // Handle the notification *
+ *     // Handle the progress notification
  * }
* * @author Christian Tzolov @@ -36,9 +36,9 @@ 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. + * Used as connection or client identifier to select the MCP client, the progress + * consumer is associated with. */ - String clientId() default ""; + String clientId(); } diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/progress/AsyncProgressSpecification.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/progress/AsyncProgressSpecification.java index 9e38290..74b9f67 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/progress/AsyncProgressSpecification.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/progress/AsyncProgressSpecification.java @@ -4,6 +4,7 @@ package org.springaicommunity.mcp.method.progress; +import java.util.Objects; import java.util.function.Function; import io.modelcontextprotocol.spec.McpSchema.ProgressNotification; @@ -17,4 +18,12 @@ * @author Christian Tzolov */ public record AsyncProgressSpecification(String clientId, Function> progressHandler) { + public AsyncProgressSpecification { + Objects.requireNonNull(clientId, "clientId must not be null"); + if (clientId.trim().isEmpty()) { + throw new IllegalArgumentException("clientId must not be empty"); + } + Objects.requireNonNull(progressHandler, "progressHandler must not be null"); + } + } diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/progress/SyncProgressSpecification.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/progress/SyncProgressSpecification.java index 3a14b67..e72f708 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/progress/SyncProgressSpecification.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/progress/SyncProgressSpecification.java @@ -4,6 +4,7 @@ package org.springaicommunity.mcp.method.progress; +import java.util.Objects; import java.util.function.Consumer; import io.modelcontextprotocol.spec.McpSchema.ProgressNotification; @@ -16,4 +17,13 @@ * @author Christian Tzolov */ public record SyncProgressSpecification(String clientId, Consumer progressHandler) { + + public SyncProgressSpecification { + Objects.requireNonNull(clientId, "clientId must not be null"); + if (clientId.trim().isEmpty()) { + throw new IllegalArgumentException("clientId must not be empty"); + } + Objects.requireNonNull(progressHandler, "progressHandler must not be null"); + } + } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/AsyncMcpProgressMethodCallbackExample.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/AsyncMcpProgressMethodCallbackExample.java index 2d7ec6f..b1aa4f6 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/AsyncMcpProgressMethodCallbackExample.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/AsyncMcpProgressMethodCallbackExample.java @@ -33,7 +33,7 @@ public static class AsyncProgressService { * @param notification the progress notification * @return Mono completing when processing is done */ - @McpProgress + @McpProgress(clientId = "my-client-id") public Mono handleProgressNotificationAsync(ProgressNotification notification) { return Mono.fromRunnable(() -> { int count = notificationCount.incrementAndGet(); @@ -51,7 +51,7 @@ public Mono handleProgressNotificationAsync(ProgressNotification notificat * @param progressToken the progress token identifier * @param total the total value as string */ - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithParams(Double progress, String progressToken, String total) { System.out.printf("[Sync in Async] Progress: %.2f%% for token %s (Total: %s)%n", progress * 100, progressToken, total); @@ -64,7 +64,7 @@ public void handleProgressWithParams(Double progress, String progressToken, Stri * @param total the total value as string * @return Mono completing when processing is done */ - @McpProgress + @McpProgress(clientId = "my-client-id") public Mono handleProgressWithParamsAsync(Double progress, String progressToken, String total) { return Mono.fromRunnable(() -> { System.out.printf("[Async Params] Progress: %.2f%% for token %s (Total: %s)%n", progress * 100, @@ -78,7 +78,7 @@ public Mono handleProgressWithParamsAsync(Double progress, String progress * @param progressToken the progress token identifier * @param total the total value as string */ - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressPrimitive(double progress, String progressToken, String total) { System.out.printf("[Primitive] Processing: %.1f%% complete (Token: %s)%n", progress * 100, progressToken); } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/AsyncMcpProgressMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/AsyncMcpProgressMethodCallbackTests.java index a07c933..a2c5416 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/AsyncMcpProgressMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/AsyncMcpProgressMethodCallbackTests.java @@ -45,25 +45,25 @@ static class ValidMethods { private String lastTotal; - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressVoid(ProgressNotification notification) { this.lastNotification = notification; } - @McpProgress + @McpProgress(clientId = "my-client-id") public Mono handleProgressMono(ProgressNotification notification) { this.lastNotification = notification; return Mono.empty(); } - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithParams(Double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; this.lastTotal = total; } - @McpProgress + @McpProgress(clientId = "my-client-id") public Mono handleProgressWithParamsMono(Double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; @@ -71,7 +71,7 @@ public Mono handleProgressWithParamsMono(Double progress, String progressT return Mono.empty(); } - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithPrimitiveDouble(double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; @@ -85,42 +85,42 @@ public void handleProgressWithPrimitiveDouble(double progress, String progressTo */ static class InvalidMethods { - @McpProgress + @McpProgress(clientId = "my-client-id") public String invalidReturnType(ProgressNotification notification) { return "Invalid"; } - @McpProgress + @McpProgress(clientId = "my-client-id") public Mono invalidMonoReturnType(ProgressNotification notification) { return Mono.just("Invalid"); } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidParameterCount(ProgressNotification notification, String extra) { // Invalid parameter count } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidParameterType(String invalidType) { // Invalid parameter type } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidParameterTypes(String progress, int progressToken, boolean total) { // Invalid parameter types } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidFirstParameterType(String progress, String progressToken, String total) { // Invalid first parameter type } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidSecondParameterType(Double progress, int progressToken, String total) { // Invalid second parameter type } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidThirdParameterType(Double progress, String progressToken, int total) { // Invalid third parameter type } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/SyncMcpProgressMethodCallbackExample.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/SyncMcpProgressMethodCallbackExample.java index fb7aa99..791a3a9 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/SyncMcpProgressMethodCallbackExample.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/SyncMcpProgressMethodCallbackExample.java @@ -28,7 +28,7 @@ public static class ProgressService { * Handle progress notification with the full notification object. * @param notification the progress notification */ - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressNotification(ProgressNotification notification) { notificationCount++; System.out.printf("Progress Update #%d: Token=%s, Progress=%.2f%%, Total=%.0f, Message=%s%n", @@ -42,7 +42,7 @@ public void handleProgressNotification(ProgressNotification notification) { * @param progressToken the progress token identifier * @param total the total value as string */ - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithParams(Double progress, String progressToken, String total) { System.out.printf("Progress: %.2f%% for token %s (Total: %s)%n", progress * 100, progressToken, total); } @@ -53,7 +53,7 @@ public void handleProgressWithParams(Double progress, String progressToken, Stri * @param progressToken the progress token identifier * @param total the total value as string */ - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressPrimitive(double progress, String progressToken, String total) { System.out.printf("Processing: %.1f%% complete (Token: %s)%n", progress * 100, progressToken); } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/SyncMcpProgressMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/SyncMcpProgressMethodCallbackTests.java index 7909ad3..4f31369 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/SyncMcpProgressMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/progress/SyncMcpProgressMethodCallbackTests.java @@ -43,19 +43,19 @@ static class ValidMethods { private String lastTotal; - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressNotification(ProgressNotification notification) { this.lastNotification = notification; } - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithParams(Double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; this.lastTotal = total; } - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithPrimitiveDouble(double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; @@ -69,37 +69,37 @@ public void handleProgressWithPrimitiveDouble(double progress, String progressTo */ static class InvalidMethods { - @McpProgress + @McpProgress(clientId = "my-client-id") public String invalidReturnType(ProgressNotification notification) { return "Invalid"; } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidParameterCount(ProgressNotification notification, String extra) { // Invalid parameter count } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidParameterType(String invalidType) { // Invalid parameter type } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidParameterTypes(String progress, int progressToken, boolean total) { // Invalid parameter types } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidFirstParameterType(String progress, String progressToken, String total) { // Invalid first parameter type } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidSecondParameterType(Double progress, int progressToken, String total) { // Invalid second parameter type } - @McpProgress + @McpProgress(clientId = "my-client-id") public void invalidThirdParameterType(Double progress, String progressToken, int total) { // Invalid third parameter type } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/progress/AsyncMcpProgressProviderTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/progress/AsyncMcpProgressProviderTests.java index 2dd9d48..2287266 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/progress/AsyncMcpProgressProviderTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/progress/AsyncMcpProgressProviderTests.java @@ -37,25 +37,25 @@ static class AsyncProgressHandler { private String lastTotal; - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressVoid(ProgressNotification notification) { this.lastNotification = notification; } - @McpProgress + @McpProgress(clientId = "my-client-id") public Mono handleProgressMono(ProgressNotification notification) { this.lastNotification = notification; return Mono.empty(); } - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithParams(Double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; this.lastTotal = total; } - @McpProgress + @McpProgress(clientId = "my-client-id") public Mono handleProgressWithParamsMono(Double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; @@ -63,7 +63,7 @@ public Mono handleProgressWithParamsMono(Double progress, String progressT return Mono.empty(); } - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithPrimitiveDouble(double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; @@ -77,13 +77,13 @@ public Mono notAnnotatedMethod(ProgressNotification notification) { } // This method has invalid return type and should be ignored - @McpProgress + @McpProgress(clientId = "my-client-id") public String invalidReturnType(ProgressNotification notification) { return "Invalid"; } // This method has invalid Mono return type and should be ignored - @McpProgress + @McpProgress(clientId = "my-client-id") public Mono invalidMonoReturnType(ProgressNotification notification) { return Mono.just("Invalid"); } @@ -171,8 +171,8 @@ void testClientIdExtraction() { List specifications = provider.getProgressSpecifications(); - // All specifications should have empty clientId (default value from annotation) - assertThat(specifications).allMatch(spec -> spec.clientId().equals("")); + // All specifications should have non-empty clientId + assertThat(specifications).allMatch(spec -> !spec.clientId().isEmpty()); } @Test @@ -180,7 +180,7 @@ void testErrorHandling() { // Test class with method that throws an exception class ErrorHandler { - @McpProgress + @McpProgress(clientId = "my-client-id") public Mono handleProgressWithError(ProgressNotification notification) { return Mono.error(new RuntimeException("Test error")); } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/progress/SyncMcpProgressProviderTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/progress/SyncMcpProgressProviderTests.java index b554b1e..958f6b9 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/progress/SyncMcpProgressProviderTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/progress/SyncMcpProgressProviderTests.java @@ -35,19 +35,19 @@ static class ProgressHandler { private String lastTotal; - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressNotification(ProgressNotification notification) { this.lastNotification = notification; } - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithParams(Double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; this.lastTotal = total; } - @McpProgress + @McpProgress(clientId = "my-client-id") public void handleProgressWithPrimitiveDouble(double progress, String progressToken, String total) { this.lastProgress = progress; this.lastProgressToken = progressToken; @@ -60,7 +60,7 @@ public void notAnnotatedMethod(ProgressNotification notification) { } // This method has invalid return type and should be ignored - @McpProgress + @McpProgress(clientId = "my-client-id") public String invalidReturnType(ProgressNotification notification) { return "Invalid"; } @@ -145,8 +145,8 @@ void testClientIdExtraction() { List specifications = provider.getProgressSpecifications(); - // All specifications should have empty clientId (default value from annotation) - assertThat(specifications).allMatch(spec -> spec.clientId().equals("")); + // All specifications should have non-empty clientId + assertThat(specifications).allMatch(spec -> !spec.clientId().isEmpty()); } }