From d4c8b2d57904f40783bd60c5dd516df1e24eacd8 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Tue, 9 Sep 2025 07:43:11 +0200 Subject: [PATCH] feat: add deterministic method sorting to all MCP providers - Sort annotated methods by name in all provider classes to ensure consistent processing order - Apply sorting to 32 provider classes across tool, resource, prompt, complete, progress, logging, sampling, and elicitation providers - Include both sync/async and stateless variants - Add test infrastructure improvements with CountDownLatch for better async testing - Ensures deterministic behavior across different JVM runs and environments This change improves reliability by eliminating non-deterministic method ordering that could lead to inconsistent behavior in MCP server implementations. Signed-off-by: Christian Tzolov --- .../AsyncMcpPromptListChangedProvider.java | 1 + .../SyncMcpPromptListChangedProvider.java | 1 + .../AsyncMcpResourceListChangedProvider.java | 1 + .../SyncMcpResourceListChangedProvider.java | 1 + .../tool/AsyncMcpToolListChangedProvider.java | 1 + .../tool/SyncMcpToolListChangedProvider.java | 1 + .../complete/AsyncMcpCompleteProvider.java | 1 + .../AsyncStatelessMcpCompleteProvider.java | 1 + .../complete/SyncMcpCompleteProvider.java | 1 + .../SyncStatelessMcpCompleteProvider.java | 1 + .../AsyncMcpElicitationProvider.java | 1 + .../SyncMcpElicitationProvider.java | 1 + .../logging/AsyncMcpLoggingProvider.java | 3 ++- .../logging/SyncMcpLogginProvider.java | 1 + .../progress/AsyncMcpProgressProvider.java | 1 + .../progress/SyncMcpProgressProvider.java | 1 + .../prompt/AsyncMcpPromptProvider.java | 1 + .../AsyncStatelessMcpPromptProvider.java | 1 + .../prompt/SyncMcpPromptProvider.java | 1 + .../SyncStatelessMcpPromptProvider.java | 1 + .../resource/AsyncMcpResourceProvider.java | 1 + .../AsyncStatelessMcpResourceProvider.java | 1 + .../resource/SyncMcpResourceProvider.java | 1 + .../SyncStatelessMcpResourceProvider.java | 1 + .../sampling/AsyncMcpSamplingProvider.java | 1 + .../sampling/SyncMcpSamplingProvider.java | 1 + .../provider/tool/AsyncMcpToolProvider.java | 1 + .../tool/AsyncStatelessMcpToolProvider.java | 1 + .../provider/tool/SyncMcpToolProvider.java | 1 + .../tool/SyncStatelessMcpToolProvider.java | 1 + .../logging/SyncMcpLoggingProviderTests.java | 2 ++ .../AsyncMcpProgressProviderTests.java | 26 ++++++++++++++++++- 32 files changed, 58 insertions(+), 2 deletions(-) diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/prompt/AsyncMcpPromptListChangedProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/prompt/AsyncMcpPromptListChangedProvider.java index 8eb6e25..c965fb5 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/prompt/AsyncMcpPromptListChangedProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/prompt/AsyncMcpPromptListChangedProvider.java @@ -84,6 +84,7 @@ public List getPromptListChangedSpecificati .filter(method -> method.isAnnotationPresent(McpPromptListChanged.class)) .filter(method -> method.getReturnType() == void.class || Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptListChangedConsumerMethod -> { var promptListChangedAnnotation = mcpPromptListChangedConsumerMethod .getAnnotation(McpPromptListChanged.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/prompt/SyncMcpPromptListChangedProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/prompt/SyncMcpPromptListChangedProvider.java index 26e3799..b4cfce4 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/prompt/SyncMcpPromptListChangedProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/prompt/SyncMcpPromptListChangedProvider.java @@ -82,6 +82,7 @@ public List getPromptListChangedSpecificatio .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpPromptListChanged.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptListChangedConsumerMethod -> { var promptListChangedAnnotation = mcpPromptListChangedConsumerMethod .getAnnotation(McpPromptListChanged.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/resource/AsyncMcpResourceListChangedProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/resource/AsyncMcpResourceListChangedProvider.java index 77f947f..9796a8c 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/resource/AsyncMcpResourceListChangedProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/resource/AsyncMcpResourceListChangedProvider.java @@ -84,6 +84,7 @@ public List getResourceListChangedSpecifi .filter(method -> method.isAnnotationPresent(McpResourceListChanged.class)) .filter(method -> method.getReturnType() == void.class || Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceListChangedConsumerMethod -> { var resourceListChangedAnnotation = mcpResourceListChangedConsumerMethod .getAnnotation(McpResourceListChanged.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/resource/SyncMcpResourceListChangedProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/resource/SyncMcpResourceListChangedProvider.java index 981f23a..cf0ca76 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/resource/SyncMcpResourceListChangedProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/resource/SyncMcpResourceListChangedProvider.java @@ -82,6 +82,7 @@ public List getResourceListChangedSpecific .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpResourceListChanged.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceListChangedConsumerMethod -> { var resourceListChangedAnnotation = mcpResourceListChangedConsumerMethod .getAnnotation(McpResourceListChanged.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/tool/AsyncMcpToolListChangedProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/tool/AsyncMcpToolListChangedProvider.java index 91138ff..0b6fe5a 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/tool/AsyncMcpToolListChangedProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/tool/AsyncMcpToolListChangedProvider.java @@ -83,6 +83,7 @@ public List getToolListChangedSpecifications( .filter(method -> method.isAnnotationPresent(McpToolListChanged.class)) .filter(method -> method.getReturnType() == void.class || Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolListChangedConsumerMethod -> { var toolListChangedAnnotation = mcpToolListChangedConsumerMethod .getAnnotation(McpToolListChanged.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/tool/SyncMcpToolListChangedProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/tool/SyncMcpToolListChangedProvider.java index f33d70b..1a9ef48 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/tool/SyncMcpToolListChangedProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/changed/tool/SyncMcpToolListChangedProvider.java @@ -81,6 +81,7 @@ public List getToolListChangedSpecifications() .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpToolListChanged.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolListChangedConsumerMethod -> { var toolListChangedAnnotation = mcpToolListChangedConsumerMethod .getAnnotation(McpToolListChanged.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/AsyncMcpCompleteProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/AsyncMcpCompleteProvider.java index 413cb00..74809fe 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/AsyncMcpCompleteProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/AsyncMcpCompleteProvider.java @@ -69,6 +69,7 @@ public List getCompleteSpecifications() { .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || Flux.class.isAssignableFrom(method.getReturnType()) || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpCompleteMethod -> { var completeAnnotation = mcpCompleteMethod.getAnnotation(McpComplete.class); var completeRef = CompleteAdapter.asCompleteReference(completeAnnotation, mcpCompleteMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/AsyncStatelessMcpCompleteProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/AsyncStatelessMcpCompleteProvider.java index f7f8647..2cbe89b 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/AsyncStatelessMcpCompleteProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/AsyncStatelessMcpCompleteProvider.java @@ -72,6 +72,7 @@ public List getCompleteSpecifications() { .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || Flux.class.isAssignableFrom(method.getReturnType()) || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpCompleteMethod -> { var completeAnnotation = mcpCompleteMethod.getAnnotation(McpComplete.class); var completeRef = CompleteAdapter.asCompleteReference(completeAnnotation, mcpCompleteMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/SyncMcpCompleteProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/SyncMcpCompleteProvider.java index d5cda18..d865492 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/SyncMcpCompleteProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/SyncMcpCompleteProvider.java @@ -44,6 +44,7 @@ public List getCompleteSpecifications() { .map(completeObject -> Stream.of(doGetClassMethods(completeObject)) .filter(method -> method.isAnnotationPresent(McpComplete.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpCompleteMethod -> { var completeAnnotation = mcpCompleteMethod.getAnnotation(McpComplete.class); var completeRef = CompleteAdapter.asCompleteReference(completeAnnotation, mcpCompleteMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/SyncStatelessMcpCompleteProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/SyncStatelessMcpCompleteProvider.java index 169c971..8d61a3f 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/SyncStatelessMcpCompleteProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/complete/SyncStatelessMcpCompleteProvider.java @@ -68,6 +68,7 @@ public List getCompleteSpecifications() { .map(completeObject -> Stream.of(doGetClassMethods(completeObject)) .filter(method -> method.isAnnotationPresent(McpComplete.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpCompleteMethod -> { var completeAnnotation = mcpCompleteMethod.getAnnotation(McpComplete.class); var completeRef = CompleteAdapter.asCompleteReference(completeAnnotation, mcpCompleteMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/elicitation/AsyncMcpElicitationProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/elicitation/AsyncMcpElicitationProvider.java index ee83abf..47fd8e7 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/elicitation/AsyncMcpElicitationProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/elicitation/AsyncMcpElicitationProvider.java @@ -91,6 +91,7 @@ public List getElicitationSpecifications() { && ElicitRequest.class.isAssignableFrom(method.getParameterTypes()[0])) .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || ElicitResult.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpElicitationMethod -> { var elicitationAnnotation = mcpElicitationMethod.getAnnotation(McpElicitation.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/elicitation/SyncMcpElicitationProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/elicitation/SyncMcpElicitationProvider.java index a271b10..78cfece 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/elicitation/SyncMcpElicitationProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/elicitation/SyncMcpElicitationProvider.java @@ -91,6 +91,7 @@ public List getElicitationSpecifications() { .filter(method -> ElicitResult.class.isAssignableFrom(method.getReturnType())) .filter(method -> method.getParameterCount() == 1 && ElicitRequest.class.isAssignableFrom(method.getParameterTypes()[0])) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpElicitationMethod -> { var elicitationAnnotation = mcpElicitationMethod.getAnnotation(McpElicitation.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/AsyncMcpLoggingProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/AsyncMcpLoggingProvider.java index 03c45ee..7324349 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/AsyncMcpLoggingProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/AsyncMcpLoggingProvider.java @@ -78,8 +78,9 @@ public AsyncMcpLoggingProvider(List loggingConsumerObjects) { public List getLoggingSpecifications() { List loggingConsumers = this.loggingConsumerObjects.stream() - .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) + .map(consumerObject -> Stream.of(this.doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpLogging.class)) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpLoggingConsumerMethod -> { var loggingConsumerAnnotation = mcpLoggingConsumerMethod.getAnnotation(McpLogging.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/SyncMcpLogginProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/SyncMcpLogginProvider.java index 2279fdd..2fe08af 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/SyncMcpLogginProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/SyncMcpLogginProvider.java @@ -81,6 +81,7 @@ public List getLoggingSpecifications() { .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpLogging.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpLoggingConsumerMethod -> { var loggingConsumerAnnotation = mcpLoggingConsumerMethod.getAnnotation(McpLogging.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/progress/AsyncMcpProgressProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/progress/AsyncMcpProgressProvider.java index 8d7e626..3805d1a 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/progress/AsyncMcpProgressProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/progress/AsyncMcpProgressProvider.java @@ -97,6 +97,7 @@ public List getProgressSpecifications() { } return false; }) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpProgressMethod -> { var progressAnnotation = mcpProgressMethod.getAnnotation(McpProgress.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/progress/SyncMcpProgressProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/progress/SyncMcpProgressProvider.java index ef929d7..8285970 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/progress/SyncMcpProgressProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/progress/SyncMcpProgressProvider.java @@ -82,6 +82,7 @@ public List getProgressSpecifications() { .filter(method -> method.getReturnType() == void.class) // Only void // return type is // valid for sync + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpProgressMethod -> { var progressAnnotation = mcpProgressMethod.getAnnotation(McpProgress.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/AsyncMcpPromptProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/AsyncMcpPromptProvider.java index 726a21e..6bb5d5b 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/AsyncMcpPromptProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/AsyncMcpPromptProvider.java @@ -72,6 +72,7 @@ public List getPromptSpecifications() { .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || Flux.class.isAssignableFrom(method.getReturnType()) || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptMethod -> { var promptAnnotation = mcpPromptMethod.getAnnotation(McpPrompt.class); var mcpPrompt = PromptAdapter.asPrompt(promptAnnotation, mcpPromptMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/AsyncStatelessMcpPromptProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/AsyncStatelessMcpPromptProvider.java index 8b30881..4a9bb11 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/AsyncStatelessMcpPromptProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/AsyncStatelessMcpPromptProvider.java @@ -72,6 +72,7 @@ public List getPromptSpecifications() { .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || Flux.class.isAssignableFrom(method.getReturnType()) || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptMethod -> { var promptAnnotation = mcpPromptMethod.getAnnotation(McpPrompt.class); var mcpPrompt = PromptAdapter.asPrompt(promptAnnotation, mcpPromptMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/SyncMcpPromptProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/SyncMcpPromptProvider.java index 876c2c3..f2bb7b7 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/SyncMcpPromptProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/SyncMcpPromptProvider.java @@ -44,6 +44,7 @@ public List getPromptSpecifications() { .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) .filter(method -> method.isAnnotationPresent(McpPrompt.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptMethod -> { var promptAnnotation = mcpPromptMethod.getAnnotation(McpPrompt.class); var mcpPrompt = PromptAdapter.asPrompt(promptAnnotation, mcpPromptMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/SyncStatelessMcpPromptProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/SyncStatelessMcpPromptProvider.java index 4cb2b75..6a44163 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/SyncStatelessMcpPromptProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/prompt/SyncStatelessMcpPromptProvider.java @@ -68,6 +68,7 @@ public List getPromptSpecifications() { .map(promptObject -> Stream.of(doGetClassMethods(promptObject)) .filter(method -> method.isAnnotationPresent(McpPrompt.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptMethod -> { var promptAnnotation = mcpPromptMethod.getAnnotation(McpPrompt.class); var mcpPrompt = PromptAdapter.asPrompt(promptAnnotation, mcpPromptMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java index 7786a49..1f7d91b 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncMcpResourceProvider.java @@ -73,6 +73,7 @@ public List getResourceSpecifications() { .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || Flux.class.isAssignableFrom(method.getReturnType()) || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java index f19ee4f..249a4ec 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/AsyncStatelessMcpResourceProvider.java @@ -73,6 +73,7 @@ public List getResourceSpecifications() { .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || Flux.class.isAssignableFrom(method.getReturnType()) || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java index c4a9a0b..5e72a0a 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncMcpResourceProvider.java @@ -45,6 +45,7 @@ public List getResourceSpecifications() { .map(resourceObject -> Stream.of(this.doGetClassMethods(resourceObject)) .filter(resourceMethod -> resourceMethod.isAnnotationPresent(McpResource.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { var resourceAnnotation = mcpResourceMethod.getAnnotation(McpResource.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java index cd9d2f0..b9e9b68 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/resource/SyncStatelessMcpResourceProvider.java @@ -69,6 +69,7 @@ public List getResourceSpecifications() { .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) .filter(method -> method.isAnnotationPresent(McpResource.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { var resourceAnnotation = doGetMcpResourceAnnotation(mcpResourceMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/sampling/AsyncMcpSamplingProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/sampling/AsyncMcpSamplingProvider.java index 2db8080..11fcf9b 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/sampling/AsyncMcpSamplingProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/sampling/AsyncMcpSamplingProvider.java @@ -91,6 +91,7 @@ public List getSamplingSpecifictions() { && CreateMessageRequest.class.isAssignableFrom(method.getParameterTypes()[0])) .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || CreateMessageResult.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpSamplingMethod -> { var samplingAnnotation = mcpSamplingMethod.getAnnotation(McpSampling.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/sampling/SyncMcpSamplingProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/sampling/SyncMcpSamplingProvider.java index 7d35d06..8b76a92 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/sampling/SyncMcpSamplingProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/sampling/SyncMcpSamplingProvider.java @@ -91,6 +91,7 @@ public List getSamplingSpecifications() { .filter(method -> CreateMessageResult.class.isAssignableFrom(method.getReturnType())) .filter(method -> method.getParameterCount() == 1 && CreateMessageRequest.class.isAssignableFrom(method.getParameterTypes()[0])) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpSamplingMethod -> { var samplingAnnotation = mcpSamplingMethod.getAnnotation(McpSampling.class); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java index 12483ed..3d98a1d 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncMcpToolProvider.java @@ -73,6 +73,7 @@ public List getToolSpecifications() { .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || Flux.class.isAssignableFrom(method.getReturnType()) || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolMethod -> { var toolJavaAnnotation = doGetMcpToolAnnotation(mcpToolMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java index 7498a7a..668c9ec 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/AsyncStatelessMcpToolProvider.java @@ -77,6 +77,7 @@ public List getToolSpecifications() { .filter(method -> Mono.class.isAssignableFrom(method.getReturnType()) || Flux.class.isAssignableFrom(method.getReturnType()) || Publisher.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolMethod -> { var toolJavaAnnotation = doGetMcpToolAnnotation(mcpToolMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java index 2b0a942..1caf640 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncMcpToolProvider.java @@ -68,6 +68,7 @@ public List getToolSpecifications() { .map(toolObject -> Stream.of(doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolMethod -> { McpTool toolJavaAnnotation = doGetMcpToolAnnotation(mcpToolMethod); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java index 863e178..17eda58 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/tool/SyncStatelessMcpToolProvider.java @@ -72,6 +72,7 @@ public List getToolSpecifications() { .map(toolObject -> Stream.of(doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) .filter(method -> !Mono.class.isAssignableFrom(method.getReturnType())) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolMethod -> { var toolJavaAnnotation = doGetMcpToolAnnotation(mcpToolMethod); diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/logging/SyncMcpLoggingProviderTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/logging/SyncMcpLoggingProviderTests.java index 7062193..6fbd11f 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/logging/SyncMcpLoggingProviderTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/logging/SyncMcpLoggingProviderTests.java @@ -38,11 +38,13 @@ static class LoggingHandler { @McpLogging(clients = "test-client") public void handleLoggingMessage(LoggingMessageNotification notification) { + System.out.println("1"); this.lastNotification = notification; } @McpLogging(clients = "test-client") public void handleLoggingMessageWithParams(LoggingLevel level, String logger, String data) { + System.out.println("2"); this.lastLevel = level; this.lastLogger = logger; this.lastData = data; 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 46f8d79..bf2e5cf 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 @@ -7,6 +7,8 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -29,6 +31,8 @@ public class AsyncMcpProgressProviderTests { */ static class AsyncProgressHandler { + final CountDownLatch latch; + private ProgressNotification lastNotification; private Double lastProgress; @@ -37,6 +41,14 @@ static class AsyncProgressHandler { private String lastTotal; + public AsyncProgressHandler(CountDownLatch latch) { + this.latch = latch; + } + + public AsyncProgressHandler() { + this.latch = new CountDownLatch(2); + } + @McpProgress(clients = "my-client-id") public void handleProgressVoid(ProgressNotification notification) { this.lastNotification = notification; @@ -45,6 +57,7 @@ public void handleProgressVoid(ProgressNotification notification) { @McpProgress(clients = "my-client-id") public Mono handleProgressMono(ProgressNotification notification) { this.lastNotification = notification; + latch.countDown(); return Mono.empty(); } @@ -60,6 +73,7 @@ public Mono handleProgressWithParamsMono(Double progress, String progressT this.lastProgress = progress; this.lastProgressToken = progressToken; this.lastTotal = total; + latch.countDown(); return Mono.empty(); } @@ -92,7 +106,8 @@ public Mono invalidMonoReturnType(ProgressNotification notification) { @Test void testGetProgressSpecifications() { - AsyncProgressHandler progressHandler = new AsyncProgressHandler(); + CountDownLatch latch = new CountDownLatch(1); + AsyncProgressHandler progressHandler = new AsyncProgressHandler(latch); AsyncMcpProgressProvider provider = new AsyncMcpProgressProvider(List.of(progressHandler)); List specifications = provider.getProgressSpecifications(); @@ -109,6 +124,15 @@ void testGetProgressSpecifications() { "Test progress message"); StepVerifier.create(handlers.get(0).apply(notification)).verifyComplete(); + + try { + // Wait for progress notifications to be processed + latch.await(3, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + e.printStackTrace(); + } + assertThat(progressHandler.lastNotification).isEqualTo(notification); // Reset