diff --git a/README.md b/README.md index c5bf3b7..9ebb37c 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,35 @@ The core module provides a set of annotations and callback implementations for p Each operation type has both synchronous and asynchronous implementations, allowing for flexible integration with different application architectures. +### Method Filtering by Server Type + +The library automatically filters methods based on the server type and method characteristics: + +#### Synchronous vs Asynchronous Servers + +- **Synchronous Providers** (`SyncMcpToolProvider`, `SyncMcpResourceProvider`, `SyncMcpPromptProvider`, `SyncMcpCompleteProvider`): + - Accept methods with **non-reactive return types** (e.g., `String`, `int`, `CallToolResult`, `ReadResourceResult`) + - **Filter out** methods returning reactive types (`Mono`, `Flux`, `Publisher`) + - Methods with reactive return types are logged as warnings and skipped + +- **Asynchronous Providers** (`AsyncMcpToolProvider`, `AsyncMcpResourceProvider`, `AsyncMcpPromptProvider`, `AsyncMcpCompleteProvider`): + - Accept methods with **reactive return types** (`Mono`, `Flux`, `Publisher`) + - **Filter out** methods with non-reactive return types + - Methods with non-reactive return types are logged as warnings and skipped + +#### Stateful vs Stateless Servers + +- **Stateful Providers** (using `McpSyncServerExchange` or `McpAsyncServerExchange`): + - Accept methods with **bidirectional parameters**: `McpSyncRequestContext`, `McpAsyncRequestContext`, `McpSyncServerExchange`, `McpAsyncServerExchange` + - Support full server exchange functionality including roots, elicitation, and sampling capabilities + +- **Stateless Providers** (`SyncStatelessMcpToolProvider`, `AsyncStatelessMcpToolProvider`, `SyncStatelessMcpResourceProvider`, `AsyncStatelessMcpResourceProvider`, `SyncStatelessMcpPromptProvider`, `AsyncStatelessMcpPromptProvider`, `SyncStatelessMcpCompleteProvider`, `AsyncStatelessMcpCompleteProvider`): + - **Filter out** methods with bidirectional parameters (`McpSyncRequestContext`, `McpAsyncRequestContext`, `McpSyncServerExchange`, `McpAsyncServerExchange`) + - Accept methods with `McpTransportContext` or no context parameter + - Methods with bidirectional parameters are logged as warnings and skipped + - Do not support bidirectional operations (roots, elicitation, sampling) + + ## Key Components ### Annotations @@ -120,15 +149,15 @@ Each operation type has both synchronous and asynchronous implementations, allow - **`@McpToolParam`** - Annotates tool method parameters with descriptions and requirement specifications #### Special Parameters and Annotations -- **`McpSyncRequestContext`** - Special parameter type for synchronous operations that provides a unified interface for accessing MCP request context, including the original request, server exchange (for stateful operations), transport context (for stateless operations), and convenient methods for logging, progress, sampling, and elicitation. This parameter is automatically injected and excluded from JSON schema generation -- **`McpAsyncRequestContext`** - Special parameter type for asynchronous operations that provides the same unified interface as `McpSyncRequestContext` but with reactive (Mono-based) return types. This parameter is automatically injected and excluded from JSON schema generation -- **(Deprecated and replaced by `McpSyncRequestContext`) `McpSyncServerExchange`** - Special parameter type for stateful synchronous operations that provides access to server exchange functionality including logging notifications, progress updates, and other server-side operations. This parameter is automatically injected and excluded from JSON schema generation. -- **(Deprecated and replaced by `McpAsyncRequestContext`) `McpAsyncServerExchange`** - Special parameter type for stateful asynchronous operations that provides access to server exchange functionality with reactive support. This parameter is automatically injected and excluded from JSON schema generation +- **`McpSyncRequestContext`** - Special parameter type for synchronous operations that provides a unified interface for accessing MCP request context, including the original request, server exchange (for stateful operations), transport context (for stateless operations), and convenient methods for logging, progress, sampling, and elicitation. This parameter is automatically injected and excluded from JSON schema generation. **Supported in Complete, Prompt, Resource, and Tool methods.** +- **`McpAsyncRequestContext`** - Special parameter type for asynchronous operations that provides the same unified interface as `McpSyncRequestContext` but with reactive (Mono-based) return types. This parameter is automatically injected and excluded from JSON schema generation. **Supported in Complete, Prompt, Resource, and Tool methods.** +- **(Deprecated and replaced by `McpSyncRequestContext`) `McpSyncServerExchange`** - Legacy parameter type for stateful synchronous operations. Use `McpSyncRequestContext` instead for a unified interface that works with both stateful and stateless operations. +- **(Deprecated and replaced by `McpAsyncRequestContext`) `McpAsyncServerExchange`** - Legacy parameter type for stateful asynchronous operations. Use `McpAsyncRequestContext` instead for a unified interface that works with both stateful and stateless operations. - **`McpTransportContext`** - Special parameter type for stateless operations that provides lightweight access to transport-level context without full server exchange functionality. This parameter is automatically injected and excluded from JSON schema generation -- **(Deprecated. Handled internally by `McpSyncRequestContext` and `McpAsyncRequestContext`)`@McpProgressToken`** - Marks a method parameter to receive the progress token from the request. This parameter is automatically injected and excluded from the generated JSON schema -**Note:** if using the `McpSyncRequestContext` or `McpAsyncRequestContext` the progress token is handled internally. +- **`@McpProgressToken`** - Marks a method parameter to receive the progress token from the request. This parameter is automatically injected and excluded from the generated JSON schema. **Supported in Complete, Prompt, Resource, and Tool methods.** +**Note:** When using `McpSyncRequestContext` or `McpAsyncRequestContext`, the progress token can be accessed via `ctx.request().progressToken()` instead of using this annotation. - **`McpMeta`** - Special parameter type that provides access to metadata from MCP requests, notifications, and results. This parameter is automatically injected and excluded from parameter count limits and JSON schema generation. -**Note:** if using the McpSyncRequestContext or McpAsyncRequestContext the meta can be obatined via `requestMeta()` instead. +**Note:** When using `McpSyncRequestContext` or `McpAsyncRequestContext`, metadata can be obtained via `ctx.requestMeta()` instead. ### Method Callbacks @@ -197,7 +226,7 @@ The modules provide callback implementations for each operation type: The project includes provider classes that scan for annotated methods and create appropriate callbacks: -#### Stateful Providers (using McpSyncServerExchange/McpAsyncServerExchange) +#### Stateful Providers (using McpSyncRequestContext/McpAsyncRequestContext) - `SyncMcpCompleteProvider` - Processes `@McpComplete` annotations for synchronous operations - `AsyncMcpCompleteProvider` - Processes `@McpComplete` annotations for asynchronous operations - `SyncMcpPromptProvider` - Processes `@McpPrompt` annotations for synchronous operations @@ -241,14 +270,12 @@ public class PromptProvider { @McpPrompt(name = "personalized-message", description = "Generates a personalized message based on user information") - public GetPromptResult personalizedMessage(McpSyncServerExchange exchange, + public GetPromptResult personalizedMessage(McpSyncRequestContext context, @McpArg(name = "name", description = "The user's name", required = true) String name, @McpArg(name = "age", description = "The user's age", required = false) Integer age, @McpArg(name = "interests", description = "The user's interests", required = false) String interests) { - exchange.loggingNotification(LoggingMessageNotification.builder() - .level(LoggingLevel.INFO) - .data("personalized-message event").build()); + context.info("personalized-message event"); StringBuilder message = new StringBuilder(); message.append("Hello, ").append(name).append("!\n\n"); @@ -384,13 +411,10 @@ public class MyResourceProvider { @McpResource(uri = "user-profile-exchange://{username}", name = "User Profile with Exchange", - description = "Provides user profile information with server exchange context") - public ReadResourceResult getProfileWithExchange(McpSyncServerExchange exchange, String username) { + description = "Provides user profile information with request context") + public ReadResourceResult getProfileWithContext(McpSyncRequestContext context, String username) { - exchange.loggingNotification(LoggingMessageNotification.builder() - .level(LoggingLevel.INFO) - .data("user-profile-exchange") - .build()); + context.info("user-profile-exchange"); String profileInfo = formatProfileInfo(userProfiles.getOrDefault(username.toLowerCase(), new HashMap<>())); @@ -435,15 +459,12 @@ public class CalculatorToolProvider { return new AreaResult(area, "square units"); } - @McpTool(name = "process-data", description = "Process data with exchange context") + @McpTool(name = "process-data", description = "Process data with request context") public String processData( - McpSyncServerExchange exchange, + McpSyncRequestContext context, @McpToolParam(description = "Data to process", required = true) String data) { - exchange.loggingNotification(LoggingMessageNotification.builder() - .level(LoggingLevel.INFO) - .data("Processing data: " + data) - .build()); + context.info("Processing data: " + data); return "Processed: " + data.toUpperCase(); } @@ -916,7 +937,7 @@ public String processWithContext( // Check if running in stateful mode if (!context.isStateless()) { // Access server exchange for stateful operations - McpSyncServerExchange exchange = context.exchange().orElseThrow(); + McpSyncServerExchange exchange = context.exchange(); // Use exchange for additional operations... } @@ -2120,7 +2141,7 @@ public class StatelessResourceProvider { ``` **Important Note on Stateless Operations:** -Stateless server methods cannot use bidirectional parameters like `McpSyncRequestContext`, `McpAsyncRequestContext`, `McpSyncServerExchange`, or `McpAsyncServerExchange`. These parameters require client capabilities (roots, elicitation, sampling) that are not available in stateless mode. Methods with these parameters will be automatically filtered out and not registered as stateless operations. +Stateless server methods can use `McpSyncRequestContext` and `McpAsyncRequestContext`, but bidirectional operations (roots, elicitation, sampling) will not be available. The legacy `McpSyncServerExchange` and `McpAsyncServerExchange` parameters are not supported in stateless mode. Methods using the legacy parameters will be automatically filtered out and not registered as stateless operations. #### Stateless Tool Example @@ -2242,7 +2263,7 @@ Override `AbstractMcpToolProvider#doGetToolCallException()` to customize the exc - **Annotation-based method handling** - Simplifies the creation and registration of MCP methods - **Support for both synchronous and asynchronous operations** - Flexible integration with different application architectures -- **Stateful and stateless implementations** - Choose between full server exchange context (`McpSyncServerExchange`/`McpAsyncServerExchange`) or lightweight transport context (`McpTransportContext`) for all MCP operations +- **Stateful and stateless implementations** - Choose between unified request context (`McpSyncRequestContext`/`McpAsyncRequestContext`) or lightweight transport context (`McpTransportContext`) for all MCP operations - **Comprehensive stateless support** - All MCP operations (Complete, Prompt, Resource, Tool) support stateless implementations for scenarios where full server context is not needed - **Builder pattern for callback creation** - Clean and fluent API for creating method callbacks - **Comprehensive validation** - Ensures method signatures are compatible with MCP operations diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/McpProviderUtils.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/McpPredicates.java similarity index 97% rename from mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/McpProviderUtils.java rename to mcp-annotations/src/main/java/org/springaicommunity/mcp/McpPredicates.java index db7bbeb..9f5c9cb 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/McpProviderUtils.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/McpPredicates.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springaicommunity.mcp.provider; +package org.springaicommunity.mcp; import java.lang.reflect.Method; import java.util.function.Predicate; @@ -30,9 +30,9 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -public class McpProviderUtils { +public class McpPredicates { - private static final Logger logger = LoggerFactory.getLogger(McpProviderUtils.class); + private static final Logger logger = LoggerFactory.getLogger(McpPredicates.class); private static final Pattern URI_VARIABLE_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/StructuredElicitResult.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/StructuredElicitResult.java index 0afd5f5..f0f5b91 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/StructuredElicitResult.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/StructuredElicitResult.java @@ -4,9 +4,11 @@ package org.springaicommunity.mcp.context; +import java.util.HashMap; import java.util.Map; import io.modelcontextprotocol.spec.McpSchema.ElicitResult.Action; +import io.modelcontextprotocol.util.Assert; /** * A record representing the result of a structured elicit action. @@ -16,4 +18,78 @@ */ public record StructuredElicitResult(Action action, T structuredContent, Map meta) { + public static Builder builder() { + return new Builder<>(); + } + + public static class Builder { + + private Action action = Action.ACCEPT; + + private T structuredContent; + + private Map meta = new HashMap<>(); + + /** + * Private constructor to enforce builder pattern usage. + */ + private Builder() { + this.meta = new HashMap<>(); + } + + /** + * Sets the action. + * @param action the action to set + * @return this builder instance + */ + public Builder action(Action action) { + Assert.notNull(action, "Action must not be null"); + this.action = action; + return this; + } + + /** + * Sets the structured content. + * @param the type of the structured content + * @param structuredContent the structured content to set + * @return this builder instance with the correct type + */ + @SuppressWarnings("unchecked") + public Builder structuredContent(U structuredContent) { + Builder typedBuilder = (Builder) this; + typedBuilder.structuredContent = structuredContent; + return typedBuilder; + } + + /** + * Sets the meta map. + * @param meta the meta map to set + * @return this builder instance + */ + public Builder meta(Map meta) { + this.meta = meta != null ? new HashMap<>(meta) : new HashMap<>(); + return this; + } + + /** + * Adds a single meta entry. + * @param key the meta key + * @param value the meta value + * @return this builder instance + */ + public Builder addMeta(String key, Object value) { + this.meta.put(key, value); + return this; + } + + /** + * Builds the {@link StructuredElicitResult} instance. + * @return a new StructuredElicitResult instance + */ + public StructuredElicitResult build() { + return new StructuredElicitResult<>(this.action, this.structuredContent, this.meta); + } + + } + } diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/StructuredElicitResultBuilder.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/StructuredElicitResultBuilder.java deleted file mode 100644 index 1cf67f2..0000000 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/context/StructuredElicitResultBuilder.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2025-2025 the original author or authors. - */ - -package org.springaicommunity.mcp.context; - -import java.util.HashMap; -import java.util.Map; - -import io.modelcontextprotocol.spec.McpSchema.ElicitResult.Action; -import io.modelcontextprotocol.util.Assert; - -/** - * Builder for {@link StructuredElicitResult}. - * - * @param the type of the structured content - * @author Christian Tzolov - */ -public class StructuredElicitResultBuilder { - - private Action action = Action.ACCEPT; - - private T structuredContent; - - private Map meta = new HashMap<>(); - - /** - * Private constructor to enforce builder pattern usage. - */ - private StructuredElicitResultBuilder() { - this.meta = new HashMap<>(); - } - - /** - * Creates a new builder instance. - * @param the type of the structured content - * @return a new builder instance - */ - public static StructuredElicitResultBuilder builder() { - return new StructuredElicitResultBuilder<>(); - } - - /** - * Sets the action. - * @param action the action to set - * @return this builder instance - */ - public StructuredElicitResultBuilder action(Action action) { - Assert.notNull(action, "Action must not be null"); - this.action = action; - return this; - } - - /** - * Sets the structured content. - * @param structuredContent the structured content to set - * @return this builder instance - */ - public StructuredElicitResultBuilder structuredContent(T structuredContent) { - this.structuredContent = structuredContent; - return this; - } - - /** - * Sets the meta map. - * @param meta the meta map to set - * @return this builder instance - */ - public StructuredElicitResultBuilder meta(Map meta) { - this.meta = meta != null ? new HashMap<>(meta) : new HashMap<>(); - return this; - } - - /** - * Adds a single meta entry. - * @param key the meta key - * @param value the meta value - * @return this builder instance - */ - public StructuredElicitResultBuilder addMeta(String key, Object value) { - this.meta.put(key, value); - return this; - } - - /** - * Builds the {@link StructuredElicitResult} instance. - * @return a new StructuredElicitResult instance - */ - public StructuredElicitResult build() { - return new StructuredElicitResult<>(this.action, this.structuredContent, this.meta); - } - -} diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/complete/AbstractMcpCompleteMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/complete/AbstractMcpCompleteMethodCallback.java index 698f253..ec99be1 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/complete/AbstractMcpCompleteMethodCallback.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/complete/AbstractMcpCompleteMethodCallback.java @@ -9,6 +9,8 @@ import java.util.ArrayList; import java.util.List; +import io.modelcontextprotocol.server.McpAsyncServerExchange; +import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CompleteReference; import io.modelcontextprotocol.spec.McpSchema.CompleteRequest; @@ -16,10 +18,15 @@ import io.modelcontextprotocol.util.DefaultMcpUriTemplateManagerFactory; import io.modelcontextprotocol.util.McpUriTemplateManager; import io.modelcontextprotocol.util.McpUriTemplateManagerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.adapter.CompleteAdapter; import org.springaicommunity.mcp.annotation.McpComplete; import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpProgressToken; +import org.springaicommunity.mcp.context.DefaultMcpAsyncRequestContext; +import org.springaicommunity.mcp.context.DefaultMcpSyncRequestContext; +import org.springaicommunity.mcp.context.McpAsyncRequestContext; +import org.springaicommunity.mcp.context.McpSyncRequestContext; /** * Abstract base class for creating callbacks around complete methods. @@ -150,6 +157,7 @@ protected void validateParameters(Method method) { boolean hasArgumentParam = false; boolean hasProgressTokenParam = false; boolean hasMetaParam = false; + boolean hasRequestContextParam = false; for (Parameter param : parameters) { Class paramType = param.getType(); @@ -174,7 +182,32 @@ protected void validateParameters(Method method) { continue; } - if (isExchangeType(paramType)) { + if (McpSyncRequestContext.class.isAssignableFrom(paramType)) { + if (hasRequestContextParam) { + throw new IllegalArgumentException("Method cannot have more than one request context parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + if (McpPredicates.isReactiveReturnType.test(method)) { + throw new IllegalArgumentException( + "Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + + hasRequestContextParam = true; + } + else if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) { + if (hasRequestContextParam) { + throw new IllegalArgumentException("Method cannot have more than one request context parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + if (McpPredicates.isNotReactiveReturnType.test(method)) { + throw new IllegalArgumentException( + "Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + hasRequestContextParam = true; + } + else if (isExchangeType(paramType)) { if (hasExchangeParam) { throw new IllegalArgumentException("Method cannot have more than one exchange parameter: " + method.getName() + " in " + method.getDeclaringClass().getName()); @@ -224,10 +257,7 @@ protected Object[] buildArgs(Method method, Object exchange, CompleteRequest req // Handle @McpProgressToken annotated parameters if (param.isAnnotationPresent(McpProgressToken.class)) { - // CompleteRequest doesn't have a progressToken method in the current spec - // Set to null for now - this would need to be updated when the spec - // supports it - args[i] = null; + args[i] = request.progressToken(); } // Handle McpMeta parameters else if (McpMeta.class.isAssignableFrom(paramType)) { @@ -236,6 +266,18 @@ else if (McpMeta.class.isAssignableFrom(paramType)) { else if (isExchangeType(paramType)) { args[i] = exchange; } + else if (McpSyncRequestContext.class.isAssignableFrom(paramType)) { + args[i] = DefaultMcpSyncRequestContext.builder() + .exchange((McpSyncServerExchange) exchange) + .request(request) + .build(); + } + else if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) { + args[i] = DefaultMcpAsyncRequestContext.builder() + .exchange((McpAsyncServerExchange) exchange) + .request(request) + .build(); + } else if (CompleteRequest.class.isAssignableFrom(paramType)) { args[i] = request; } diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AbstractMcpPromptMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AbstractMcpPromptMethodCallback.java index a6ab94c..8207a00 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AbstractMcpPromptMethodCallback.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AbstractMcpPromptMethodCallback.java @@ -9,9 +9,14 @@ import java.util.List; import java.util.Map; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpArg; import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpProgressToken; +import org.springaicommunity.mcp.context.DefaultMcpAsyncRequestContext; +import org.springaicommunity.mcp.context.DefaultMcpSyncRequestContext; +import org.springaicommunity.mcp.context.McpAsyncRequestContext; +import org.springaicommunity.mcp.context.McpSyncRequestContext; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpAsyncServerExchange; import io.modelcontextprotocol.server.McpSyncServerExchange; @@ -96,6 +101,7 @@ protected void validateParameters(Method method) { boolean hasMapParam = false; boolean hasProgressTokenParam = false; boolean hasMetaParam = false; + boolean hasRequestContextParam = false; for (java.lang.reflect.Parameter param : parameters) { Class paramType = param.getType(); @@ -122,7 +128,31 @@ protected void validateParameters(Method method) { continue; } - if (isSupportedExchangeOrContextType(paramType)) { + if (McpSyncRequestContext.class.isAssignableFrom(paramType)) { + if (hasRequestContextParam) { + throw new IllegalArgumentException("Method cannot have more than one request context parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + if (McpPredicates.isReactiveReturnType.test(method)) { + throw new IllegalArgumentException( + "Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + hasRequestContextParam = true; + } + else if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) { + if (hasRequestContextParam) { + throw new IllegalArgumentException("Method cannot have more than one request context parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + if (McpPredicates.isNotReactiveReturnType.test(method)) { + throw new IllegalArgumentException( + "Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + hasRequestContextParam = true; + } + else if (isSupportedExchangeOrContextType(paramType)) { if (hasExchangeParam) { throw new IllegalArgumentException("Method cannot have more than one exchange parameter: " + method.getName() + " in " + method.getDeclaringClass().getName()); @@ -197,6 +227,18 @@ protected Object[] buildArgs(Method method, Object exchange, GetPromptRequest re args[i] = this.assignExchangeType(paramType, exchange); } + else if (McpSyncRequestContext.class.isAssignableFrom(paramType)) { + args[i] = DefaultMcpSyncRequestContext.builder() + .exchange((McpSyncServerExchange) exchange) + .request(request) + .build(); + } + else if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) { + args[i] = DefaultMcpAsyncRequestContext.builder() + .exchange((McpAsyncServerExchange) exchange) + .request(request) + .build(); + } else if (GetPromptRequest.class.isAssignableFrom(paramType)) { args[i] = request; } diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AbstractMcpResourceMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AbstractMcpResourceMethodCallback.java index fe183fb..ef5e22c 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AbstractMcpResourceMethodCallback.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AbstractMcpResourceMethodCallback.java @@ -10,8 +10,13 @@ import java.util.List; import java.util.Map; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpProgressToken; +import org.springaicommunity.mcp.context.DefaultMcpAsyncRequestContext; +import org.springaicommunity.mcp.context.DefaultMcpSyncRequestContext; +import org.springaicommunity.mcp.context.McpAsyncRequestContext; +import org.springaicommunity.mcp.context.McpSyncRequestContext; import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.server.McpAsyncServerExchange; import io.modelcontextprotocol.server.McpSyncServerExchange; @@ -149,9 +154,12 @@ protected void validateParametersWithoutUriVariables(Method method) { // Count parameters excluding @McpProgressToken and McpMeta annotated ones int nonSpecialParamCount = 0; + for (Parameter param : parameters) { - if (!param.isAnnotationPresent(McpProgressToken.class) - && !McpMeta.class.isAssignableFrom(param.getType())) { + if (!param.isAnnotationPresent(McpProgressToken.class) && !McpMeta.class.isAssignableFrom(param.getType()) + && !McpSyncRequestContext.class.isAssignableFrom(param.getType()) + && !McpAsyncRequestContext.class.isAssignableFrom(param.getType()) + && !isExchangeOrContextType(param.getType())) { nonSpecialParamCount++; } } @@ -169,6 +177,7 @@ protected void validateParametersWithoutUriVariables(Method method) { boolean hasExchangeParam = false; boolean hasRequestOrUriParam = false; boolean hasMetaParam = false; + boolean hasRequestContextParam = false; for (Parameter param : parameters) { // Skip @McpProgressToken annotated parameters @@ -178,7 +187,31 @@ protected void validateParametersWithoutUriVariables(Method method) { Class paramType = param.getType(); - if (McpMeta.class.isAssignableFrom(paramType)) { + if (McpSyncRequestContext.class.isAssignableFrom(paramType)) { + if (hasRequestContextParam) { + throw new IllegalArgumentException("Method cannot have more than one request context parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + if (McpPredicates.isReactiveReturnType.test(method)) { + throw new IllegalArgumentException( + "Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + hasRequestContextParam = true; + } + else if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) { + if (hasRequestContextParam) { + throw new IllegalArgumentException("Method cannot have more than one request context parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + if (McpPredicates.isNotReactiveReturnType.test(method)) { + throw new IllegalArgumentException( + "Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + hasRequestContextParam = true; + } + else if (McpMeta.class.isAssignableFrom(paramType)) { if (hasMetaParam) { throw new IllegalArgumentException("Method cannot have more than one McpMeta parameter: " + method.getName() + " in " + method.getDeclaringClass().getName()); @@ -234,6 +267,7 @@ protected void validateParametersWithUriVariables(Method method) { int requestParamCount = 0; int progressTokenParamCount = 0; int metaParamCount = 0; + boolean hasRequestContextParam = false; for (Parameter param : parameters) { if (param.isAnnotationPresent(McpProgressToken.class)) { @@ -253,6 +287,32 @@ else if (isExchangeOrContextType(paramType)) { else if (ReadResourceRequest.class.isAssignableFrom(paramType)) { requestParamCount++; } + else if (McpSyncRequestContext.class.isAssignableFrom(paramType)) { + if (hasRequestContextParam) { + throw new IllegalArgumentException( + "Method cannot have more than one request context parameter: " + method.getName() + + " in " + method.getDeclaringClass().getName()); + } + if (McpPredicates.isReactiveReturnType.test(method)) { + throw new IllegalArgumentException( + "Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + hasRequestContextParam = true; + } + else if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) { + if (hasRequestContextParam) { + throw new IllegalArgumentException( + "Method cannot have more than one request context parameter: " + method.getName() + + " in " + method.getDeclaringClass().getName()); + } + if (McpPredicates.isNotReactiveReturnType.test(method)) { + throw new IllegalArgumentException( + "Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: " + + method.getName() + " in " + method.getDeclaringClass().getName()); + } + hasRequestContextParam = true; + } } } @@ -275,7 +335,9 @@ else if (ReadResourceRequest.class.isAssignableFrom(paramType)) { } // Calculate how many parameters should be for URI variables - int specialParamCount = exchangeParamCount + requestParamCount + progressTokenParamCount + metaParamCount; + int requestContextParamCount = hasRequestContextParam ? 1 : 0; + int specialParamCount = exchangeParamCount + requestParamCount + progressTokenParamCount + metaParamCount + + requestContextParamCount; int uriVarParamCount = parameters.length - specialParamCount; // Check if we have the right number of parameters for URI variables @@ -294,7 +356,9 @@ else if (ReadResourceRequest.class.isAssignableFrom(paramType)) { } Class paramType = param.getType(); - if (!isExchangeOrContextType(paramType) && !ReadResourceRequest.class.isAssignableFrom(paramType) + if (!McpSyncRequestContext.class.isAssignableFrom(paramType) + && !McpAsyncRequestContext.class.isAssignableFrom(paramType) && !isExchangeOrContextType(paramType) + && !ReadResourceRequest.class.isAssignableFrom(paramType) && !McpMeta.class.isAssignableFrom(paramType) && !String.class.isAssignableFrom(paramType)) { throw new IllegalArgumentException("URI variable parameters must be of type String: " + method.getName() + " in " + method.getDeclaringClass().getName() + ", parameter of type " + paramType.getName() @@ -338,6 +402,18 @@ else if (McpTransportContext.class.isAssignableFrom(paramType) args[i] = this.assignExchangeType(paramType, exchange); } + else if (McpSyncRequestContext.class.isAssignableFrom(paramType)) { + args[i] = DefaultMcpSyncRequestContext.builder() + .exchange((McpSyncServerExchange) exchange) + .request(request) + .build(); + } + else if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) { + args[i] = DefaultMcpAsyncRequestContext.builder() + .exchange((McpAsyncServerExchange) exchange) + .request(request) + .build(); + } } if (!this.uriVariables.isEmpty()) { @@ -424,6 +500,8 @@ protected void buildArgsWithoutUriVariables(Parameter[] parameters, Object[] arg // (already handled) if (parameters[i].isAnnotationPresent(McpProgressToken.class) || McpMeta.class.isAssignableFrom(parameters[i].getType()) + || McpSyncRequestContext.class.isAssignableFrom(parameters[i].getType()) + || McpAsyncRequestContext.class.isAssignableFrom(parameters[i].getType()) || isExchangeOrContextType(parameters[i].getType())) { continue; } 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 8b6db0c..7b18c00 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 @@ -23,10 +23,10 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpPromptListChanged; import org.springaicommunity.mcp.method.changed.prompt.AsyncMcpPromptListChangedMethodCallback; import org.springaicommunity.mcp.method.changed.prompt.AsyncPromptListChangedSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -82,7 +82,7 @@ public List getPromptListChangedSpecificati .stream() .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpPromptListChanged.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptListChangedConsumerMethod -> { var promptListChangedAnnotation = mcpPromptListChangedConsumerMethod 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 9d7d079..5b69b86 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 @@ -23,10 +23,10 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpPromptListChanged; import org.springaicommunity.mcp.method.changed.prompt.SyncMcpPromptListChangedMethodCallback; import org.springaicommunity.mcp.method.changed.prompt.SyncPromptListChangedSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous prompt list changed consumer callbacks. @@ -80,7 +80,7 @@ public List getPromptListChangedSpecificatio .stream() .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpPromptListChanged.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptListChangedConsumerMethod -> { var promptListChangedAnnotation = mcpPromptListChangedConsumerMethod 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 c647058..7118e29 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 @@ -23,10 +23,10 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpResourceListChanged; import org.springaicommunity.mcp.method.changed.resource.AsyncMcpResourceListChangedMethodCallback; import org.springaicommunity.mcp.method.changed.resource.AsyncResourceListChangedSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -82,7 +82,7 @@ public List getResourceListChangedSpecifi .stream() .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpResourceListChanged.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceListChangedConsumerMethod -> { var resourceListChangedAnnotation = mcpResourceListChangedConsumerMethod 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 48119f1..e950963 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 @@ -23,10 +23,10 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpResourceListChanged; import org.springaicommunity.mcp.method.changed.resource.SyncMcpResourceListChangedMethodCallback; import org.springaicommunity.mcp.method.changed.resource.SyncResourceListChangedSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous resource list changed consumer callbacks. @@ -80,7 +80,7 @@ public List getResourceListChangedSpecific .stream() .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpResourceListChanged.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceListChangedConsumerMethod -> { var resourceListChangedAnnotation = mcpResourceListChangedConsumerMethod 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 bcccce6..7bf8add 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 @@ -23,10 +23,10 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpToolListChanged; import org.springaicommunity.mcp.method.changed.tool.AsyncMcpToolListChangedMethodCallback; import org.springaicommunity.mcp.method.changed.tool.AsyncToolListChangedSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -81,7 +81,7 @@ public List getToolListChangedSpecifications( List toolListChangedConsumers = this.toolListChangedConsumerObjects.stream() .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpToolListChanged.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolListChangedConsumerMethod -> { var toolListChangedAnnotation = mcpToolListChangedConsumerMethod 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 33a2933..03cf56c 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 @@ -23,10 +23,10 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpToolListChanged; import org.springaicommunity.mcp.method.changed.tool.SyncMcpToolListChangedMethodCallback; import org.springaicommunity.mcp.method.changed.tool.SyncToolListChangedSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous tool list changed consumer callbacks. @@ -79,7 +79,7 @@ public List getToolListChangedSpecifications() List toolListChangedConsumers = this.toolListChangedConsumerObjects.stream() .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpToolListChanged.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolListChangedConsumerMethod -> { var toolListChangedAnnotation = mcpToolListChangedConsumerMethod 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 ccc8c05..8435e8d 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 @@ -25,10 +25,10 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.adapter.CompleteAdapter; import org.springaicommunity.mcp.annotation.McpComplete; import org.springaicommunity.mcp.method.complete.AsyncMcpCompleteMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for asynchronous MCP complete methods. @@ -64,7 +64,7 @@ public List getCompleteSpecifications() { List asyncCompleteSpecification = this.completeObjects.stream() .map(completeObject -> Stream.of(doGetClassMethods(completeObject)) .filter(method -> method.isAnnotationPresent(McpComplete.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpCompleteMethod -> { var completeAnnotation = mcpCompleteMethod.getAnnotation(McpComplete.class); 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 0c59858..4832cbd 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 @@ -28,10 +28,10 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.adapter.CompleteAdapter; import org.springaicommunity.mcp.annotation.McpComplete; import org.springaicommunity.mcp.method.complete.AsyncStatelessMcpCompleteMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -68,8 +68,8 @@ public List getCompleteSpecifications() { List completeSpecs = this.completeObjects.stream() .map(completeObject -> Stream.of(doGetClassMethods(completeObject)) .filter(method -> method.isAnnotationPresent(McpComplete.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) - .filter(McpProviderUtils.filterMethodWithBidirectionalParameters()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterMethodWithBidirectionalParameters()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpCompleteMethod -> { var completeAnnotation = mcpCompleteMethod.getAnnotation(McpComplete.class); 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 b84848e..dee1be3 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 @@ -22,10 +22,10 @@ import io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.adapter.CompleteAdapter; import org.springaicommunity.mcp.annotation.McpComplete; import org.springaicommunity.mcp.method.complete.SyncMcpCompleteMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** */ @@ -43,7 +43,7 @@ public List getCompleteSpecifications() { List syncCompleteSpecification = this.completeObjects.stream() .map(completeObject -> Stream.of(doGetClassMethods(completeObject)) .filter(method -> method.isAnnotationPresent(McpComplete.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpCompleteMethod -> { var completeAnnotation = mcpCompleteMethod.getAnnotation(McpComplete.class); 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 8a16160..e0a8d20 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 @@ -28,10 +28,10 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.adapter.CompleteAdapter; import org.springaicommunity.mcp.annotation.McpComplete; import org.springaicommunity.mcp.method.complete.SyncStatelessMcpCompleteMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous stateless MCP complete methods. @@ -67,8 +67,8 @@ public List getCompleteSpecifications() { List completeSpecs = this.completeObjects.stream() .map(completeObject -> Stream.of(doGetClassMethods(completeObject)) .filter(method -> method.isAnnotationPresent(McpComplete.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) - .filter(McpProviderUtils.filterMethodWithBidirectionalParameters()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterMethodWithBidirectionalParameters()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpCompleteMethod -> { var completeAnnotation = mcpCompleteMethod.getAnnotation(McpComplete.class); 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 0f58af7..33b42e0 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 @@ -23,10 +23,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpElicitation; import org.springaicommunity.mcp.method.elicitation.AsyncElicitationSpecification; import org.springaicommunity.mcp.method.elicitation.AsyncMcpElicitationMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; import io.modelcontextprotocol.spec.McpSchema.ElicitResult; import io.modelcontextprotocol.util.Assert; @@ -89,7 +89,7 @@ public List getElicitationSpecifications() { .filter(method -> method.isAnnotationPresent(McpElicitation.class)) .filter(method -> method.getParameterCount() == 1 && ElicitRequest.class.isAssignableFrom(method.getParameterTypes()[0])) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .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 6103fbe..c12af86 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 @@ -26,11 +26,11 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpElicitation; import org.springaicommunity.mcp.context.StructuredElicitResult; import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification; import org.springaicommunity.mcp.method.elicitation.SyncMcpElicitationMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous elicitation callbacks. @@ -87,7 +87,7 @@ public List getElicitationSpecifications() { List elicitationHandlers = this.elicitationObjects.stream() .map(elicitationObject -> Stream.of(doGetClassMethods(elicitationObject)) .filter(method -> method.isAnnotationPresent(McpElicitation.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .filter(method -> ElicitResult.class.isAssignableFrom(method.getReturnType()) || StructuredElicitResult.class.isAssignableFrom(method.getReturnType())) .filter(method -> method.getParameterCount() == 1 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 c8aff18..d12560b 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 @@ -21,10 +21,10 @@ import java.util.function.Function; import java.util.stream.Stream; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpLogging; import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification; import org.springaicommunity.mcp.method.logging.AsyncMcpLoggingMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.util.Assert; import reactor.core.publisher.Mono; @@ -80,7 +80,7 @@ public List getLoggingSpecifications() { List loggingConsumers = this.loggingConsumerObjects.stream() .map(consumerObject -> Stream.of(this.doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpLogging.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .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 3fa9698..40a8263 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 @@ -23,10 +23,10 @@ import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpLogging; import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification; import org.springaicommunity.mcp.method.logging.SyncMcpLoggingMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous logging consumer callbacks. @@ -81,7 +81,7 @@ public List getLoggingSpecifications() { List loggingConsumers = this.loggingConsumerObjects.stream() .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpLogging.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .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/SyncMcpLoggingProvider.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/SyncMcpLoggingProvider.java index a2cdb52..d036c2d 100644 --- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/SyncMcpLoggingProvider.java +++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/provider/logging/SyncMcpLoggingProvider.java @@ -23,10 +23,10 @@ import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpLogging; import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification; import org.springaicommunity.mcp.method.logging.SyncMcpLoggingMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous logging consumer callbacks. @@ -79,7 +79,7 @@ public List getLoggingSpecifications() { List loggingConsumers = this.loggingConsumerObjects.stream() .map(consumerObject -> Stream.of(doGetClassMethods(consumerObject)) .filter(method -> method.isAnnotationPresent(McpLogging.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .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 8c55abc..e9a3fa2 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 @@ -23,9 +23,9 @@ import java.util.function.Function; import java.util.stream.Stream; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpProgress; import org.springaicommunity.mcp.method.progress.AsyncProgressSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; import org.springaicommunity.mcp.method.progress.AsyncMcpProgressMethodCallback; import io.modelcontextprotocol.spec.McpSchema.ProgressNotification; @@ -81,7 +81,7 @@ public List getProgressSpecifications() { List progressHandlers = this.progressObjects.stream() .map(progressObject -> Stream.of(doGetClassMethods(progressObject)) .filter(method -> method.isAnnotationPresent(McpProgress.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .filter(method -> { // Check if it's specifically Mono Type genericReturnType = method.getGenericReturnType(); 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 4e220e6..26923b2 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 @@ -22,10 +22,10 @@ import java.util.stream.Stream; import io.modelcontextprotocol.spec.McpSchema.ProgressNotification; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpProgress; import org.springaicommunity.mcp.method.progress.SyncMcpProgressMethodCallback; import org.springaicommunity.mcp.method.progress.SyncProgressSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous progress callbacks. @@ -77,7 +77,7 @@ public List getProgressSpecifications() { List progressConsumers = this.progressObjects.stream() .map(progressObject -> Stream.of(doGetClassMethods(progressObject)) .filter(method -> method.isAnnotationPresent(McpProgress.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .filter(method -> method.getReturnType() == void.class) // Only void // return type is // valid for sync 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 00f7239..defe5e2 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 @@ -28,10 +28,10 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.adapter.PromptAdapter; import org.springaicommunity.mcp.annotation.McpPrompt; import org.springaicommunity.mcp.method.prompt.AsyncMcpPromptMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -68,7 +68,7 @@ public List getPromptSpecifications() { List promptSpecs = this.promptObjects.stream() .map(promptObject -> Stream.of(doGetClassMethods(promptObject)) .filter(method -> method.isAnnotationPresent(McpPrompt.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptMethod -> { var promptAnnotation = mcpPromptMethod.getAnnotation(McpPrompt.class); 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 2da76f7..2ea39f2 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 @@ -28,10 +28,10 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.adapter.PromptAdapter; import org.springaicommunity.mcp.annotation.McpPrompt; import org.springaicommunity.mcp.method.prompt.AsyncStatelessMcpPromptMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -68,8 +68,8 @@ public List getPromptSpecifications() { List promptSpecs = this.promptObjects.stream() .map(promptObject -> Stream.of(doGetClassMethods(promptObject)) .filter(method -> method.isAnnotationPresent(McpPrompt.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) - .filter(McpProviderUtils.filterMethodWithBidirectionalParameters()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterMethodWithBidirectionalParameters()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptMethod -> { var promptAnnotation = mcpPromptMethod.getAnnotation(McpPrompt.class); 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 edf2048..56ad38a 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 @@ -22,10 +22,10 @@ import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.adapter.PromptAdapter; import org.springaicommunity.mcp.annotation.McpPrompt; import org.springaicommunity.mcp.method.prompt.SyncMcpPromptMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -44,7 +44,7 @@ public List getPromptSpecifications() { List syncPromptSpecification = this.promptObjects.stream() .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) .filter(method -> method.isAnnotationPresent(McpPrompt.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptMethod -> { var promptAnnotation = mcpPromptMethod.getAnnotation(McpPrompt.class); 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 9da3e10..7a5ebf1 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 @@ -28,10 +28,10 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.adapter.PromptAdapter; import org.springaicommunity.mcp.annotation.McpPrompt; import org.springaicommunity.mcp.method.prompt.SyncStatelessMcpPromptMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous stateless MCP prompt methods. @@ -67,8 +67,8 @@ public List getPromptSpecifications() { List promptSpecs = this.promptObjects.stream() .map(promptObject -> Stream.of(doGetClassMethods(promptObject)) .filter(method -> method.isAnnotationPresent(McpPrompt.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) - .filter(McpProviderUtils.filterMethodWithBidirectionalParameters()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterMethodWithBidirectionalParameters()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpPromptMethod -> { var promptAnnotation = mcpPromptMethod.getAnnotation(McpPrompt.class); 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 da4e9c9..87c91b7 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 @@ -31,9 +31,9 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpResource; import org.springaicommunity.mcp.method.resource.AsyncMcpResourceMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -70,7 +70,7 @@ public List getResourceSpecifications() { List resourceSpecs = this.resourceObjects.stream() .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) .filter(method -> method.isAnnotationPresent(McpResource.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { @@ -78,7 +78,7 @@ public List getResourceSpecifications() { var uri = resourceAnnotation.uri(); - if (McpProviderUtils.isUriTemplate(uri)) { + if (McpPredicates.isUriTemplate(uri)) { return null; } @@ -121,7 +121,7 @@ public List getResourceTemplateSpecification List resourceSpecs = this.resourceObjects.stream() .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) .filter(method -> method.isAnnotationPresent(McpResource.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { @@ -129,7 +129,7 @@ public List getResourceTemplateSpecification var uri = resourceAnnotation.uri(); - if (!McpProviderUtils.isUriTemplate(uri)) { + if (!McpPredicates.isUriTemplate(uri)) { return null; } 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 24f58b3..4674236 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 @@ -31,9 +31,9 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpResource; import org.springaicommunity.mcp.method.resource.AsyncStatelessMcpResourceMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -70,8 +70,8 @@ public List getResourceSpecifications() { List resourceSpecs = this.resourceObjects.stream() .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) .filter(method -> method.isAnnotationPresent(McpResource.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) - .filter(McpProviderUtils.filterMethodWithBidirectionalParameters()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterMethodWithBidirectionalParameters()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { @@ -79,7 +79,7 @@ public List getResourceSpecifications() { var uri = resourceAnnotation.uri(); - if (McpProviderUtils.isUriTemplate(uri)) { + if (McpPredicates.isUriTemplate(uri)) { return null; } @@ -122,7 +122,7 @@ public List getResourceTemplateSpecification List resourceSpecs = this.resourceObjects.stream() .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) .filter(method -> method.isAnnotationPresent(McpResource.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { @@ -130,7 +130,7 @@ public List getResourceTemplateSpecification var uri = resourceAnnotation.uri(); - if (!McpProviderUtils.isUriTemplate(uri)) { + if (!McpPredicates.isUriTemplate(uri)) { return null; } 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 a56d63c..ad9fd15 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 @@ -25,9 +25,9 @@ import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpResource; import org.springaicommunity.mcp.method.resource.SyncMcpResourceMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** */ @@ -45,14 +45,14 @@ public List getResourceSpecifications() { List methodCallbacks = this.resourceObjects.stream() .map(resourceObject -> Stream.of(this.doGetClassMethods(resourceObject)) .filter(resourceMethod -> resourceMethod.isAnnotationPresent(McpResource.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { var resourceAnnotation = mcpResourceMethod.getAnnotation(McpResource.class); var uri = resourceAnnotation.uri(); - if (McpProviderUtils.isUriTemplate(uri)) { + if (McpPredicates.isUriTemplate(uri)) { return null; } @@ -88,14 +88,14 @@ public List getResourceTemplateSpecifications List methodCallbacks = this.resourceObjects.stream() .map(resourceObject -> Stream.of(this.doGetClassMethods(resourceObject)) .filter(resourceMethod -> resourceMethod.isAnnotationPresent(McpResource.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { var resourceAnnotation = mcpResourceMethod.getAnnotation(McpResource.class); var uri = resourceAnnotation.uri(); - if (!McpProviderUtils.isUriTemplate(uri)) { + if (!McpPredicates.isUriTemplate(uri)) { return null; } 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 7ad4faf..6905cd0 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 @@ -31,9 +31,9 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpResource; import org.springaicommunity.mcp.method.resource.SyncStatelessMcpResourceMethodCallback; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous stateless MCP resource methods. @@ -69,8 +69,8 @@ public List getResourceSpecifications() { List resourceSpecs = this.resourceObjects.stream() .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) .filter(method -> method.isAnnotationPresent(McpResource.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) - .filter(McpProviderUtils.filterMethodWithBidirectionalParameters()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterMethodWithBidirectionalParameters()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { @@ -78,7 +78,7 @@ public List getResourceSpecifications() { var uri = resourceAnnotation.uri(); - if (McpProviderUtils.isUriTemplate(uri)) { + if (McpPredicates.isUriTemplate(uri)) { return null; } @@ -121,7 +121,7 @@ public List getResourceTemplateSpecifications List resourceSpecs = this.resourceObjects.stream() .map(resourceObject -> Stream.of(doGetClassMethods(resourceObject)) .filter(method -> method.isAnnotationPresent(McpResource.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpResourceMethod -> { @@ -129,7 +129,7 @@ public List getResourceTemplateSpecifications var uri = resourceAnnotation.uri(); - if (!McpProviderUtils.isUriTemplate(uri)) { + if (!McpPredicates.isUriTemplate(uri)) { return null; } 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 5611cec..8bd2035 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 @@ -26,10 +26,10 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpSampling; import org.springaicommunity.mcp.method.sampling.AsyncMcpSamplingMethodCallback; import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -89,7 +89,7 @@ public List getSamplingSpecifictions() { .filter(method -> method.isAnnotationPresent(McpSampling.class)) .filter(method -> method.getParameterCount() == 1 && CreateMessageRequest.class.isAssignableFrom(method.getParameterTypes()[0])) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .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 95b02f9..f5d015a 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 @@ -26,10 +26,10 @@ import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpSampling; import org.springaicommunity.mcp.method.sampling.SyncMcpSamplingMethodCallback; import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous sampling callbacks. @@ -86,7 +86,7 @@ public List getSamplingSpecifications() { List samplingHandlers = this.samplingObjects.stream() .map(samplingObject -> Stream.of(doGetClassMethods(samplingObject)) .filter(method -> method.isAnnotationPresent(McpSampling.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .filter(method -> CreateMessageResult.class.isAssignableFrom(method.getReturnType())) .filter(method -> method.getParameterCount() == 1 && CreateMessageRequest.class.isAssignableFrom(method.getParameterTypes()[0])) 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 5231c08..0be1e8d 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 @@ -28,13 +28,13 @@ import io.modelcontextprotocol.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.method.tool.AsyncMcpToolMethodCallback; import org.springaicommunity.mcp.method.tool.ReactiveUtils; import org.springaicommunity.mcp.method.tool.ReturnMode; import org.springaicommunity.mcp.method.tool.utils.ClassUtils; import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -63,7 +63,7 @@ public List getToolSpecifications() { List toolSpecs = this.toolObjects.stream() .map(toolObject -> Stream.of(this.doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(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 e97ebe1..bfc7049 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 @@ -28,13 +28,13 @@ import io.modelcontextprotocol.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.method.tool.AsyncStatelessMcpToolMethodCallback; import org.springaicommunity.mcp.method.tool.ReactiveUtils; import org.springaicommunity.mcp.method.tool.ReturnMode; import org.springaicommunity.mcp.method.tool.utils.ClassUtils; import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator; -import org.springaicommunity.mcp.provider.McpProviderUtils; import reactor.core.publisher.Mono; /** @@ -67,8 +67,8 @@ public List getToolSpecifications() { List toolSpecs = this.toolObjects.stream() .map(toolObject -> Stream.of(doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) - .filter(McpProviderUtils.filterNonReactiveReturnTypeMethod()) - .filter(McpProviderUtils.filterMethodWithBidirectionalParameters()) + .filter(McpPredicates.filterNonReactiveReturnTypeMethod()) + .filter(McpPredicates.filterMethodWithBidirectionalParameters()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(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 d08e32c..e209b73 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 @@ -28,12 +28,12 @@ import io.modelcontextprotocol.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.method.tool.ReturnMode; import org.springaicommunity.mcp.method.tool.SyncMcpToolMethodCallback; import org.springaicommunity.mcp.method.tool.utils.ClassUtils; import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * @author Christian Tzolov @@ -61,7 +61,7 @@ public List getToolSpecifications() { List toolSpecs = this.toolObjects.stream() .map(toolObject -> Stream.of(this.doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(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 4a2151d..c1ce5f6 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 @@ -28,12 +28,12 @@ import io.modelcontextprotocol.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.McpPredicates; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.method.tool.ReturnMode; import org.springaicommunity.mcp.method.tool.SyncStatelessMcpToolMethodCallback; import org.springaicommunity.mcp.method.tool.utils.ClassUtils; import org.springaicommunity.mcp.method.tool.utils.JsonSchemaGenerator; -import org.springaicommunity.mcp.provider.McpProviderUtils; /** * Provider for synchronous stateless MCP tool methods. @@ -64,8 +64,8 @@ public List getToolSpecifications() { List toolSpecs = this.toolObjects.stream() .map(toolObject -> Stream.of(this.doGetClassMethods(toolObject)) .filter(method -> method.isAnnotationPresent(McpTool.class)) - .filter(McpProviderUtils.filterReactiveReturnTypeMethod()) - .filter(McpProviderUtils.filterMethodWithBidirectionalParameters()) + .filter(McpPredicates.filterReactiveReturnTypeMethod()) + .filter(McpPredicates.filterMethodWithBidirectionalParameters()) .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) .map(mcpToolMethod -> { diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/McpProviderUtilsTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/McpPredicatesTests.java similarity index 74% rename from mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/McpProviderUtilsTests.java rename to mcp-annotations/src/test/java/org/springaicommunity/mcp/McpPredicatesTests.java index 5099d60..5492efb 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/provider/McpProviderUtilsTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/McpPredicatesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springaicommunity.mcp.provider; +package org.springaicommunity.mcp; import java.lang.reflect.Method; import java.util.List; @@ -32,11 +32,11 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Tests for {@link McpProviderUtils}. + * Tests for {@link McpPredicates}. * * @author Christian Tzolov */ -public class McpProviderUtilsTests { +public class McpPredicatesTests { // Test classes for method reflection tests static class TestMethods { @@ -94,60 +94,60 @@ public String methodWithoutBidirectionalParams(String param1, int param2) { @Test public void testIsUriTemplateWithSimpleVariable() { - assertThat(McpProviderUtils.isUriTemplate("/api/{id}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/{id}")).isTrue(); } @Test public void testIsUriTemplateWithMultipleVariables() { - assertThat(McpProviderUtils.isUriTemplate("/api/{userId}/posts/{postId}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/{userId}/posts/{postId}")).isTrue(); } @Test public void testIsUriTemplateWithVariableAtStart() { - assertThat(McpProviderUtils.isUriTemplate("{id}/details")).isTrue(); + assertThat(McpPredicates.isUriTemplate("{id}/details")).isTrue(); } @Test public void testIsUriTemplateWithVariableAtEnd() { - assertThat(McpProviderUtils.isUriTemplate("/api/users/{id}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/users/{id}")).isTrue(); } @Test public void testIsUriTemplateWithComplexVariableName() { - assertThat(McpProviderUtils.isUriTemplate("/api/{user_id}")).isTrue(); - assertThat(McpProviderUtils.isUriTemplate("/api/{userId123}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/{user_id}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/{userId123}")).isTrue(); } @Test public void testIsUriTemplateWithNoVariables() { - assertThat(McpProviderUtils.isUriTemplate("/api/users")).isFalse(); + assertThat(McpPredicates.isUriTemplate("/api/users")).isFalse(); } @Test public void testIsUriTemplateWithEmptyString() { - assertThat(McpProviderUtils.isUriTemplate("")).isFalse(); + assertThat(McpPredicates.isUriTemplate("")).isFalse(); } @Test public void testIsUriTemplateWithOnlySlashes() { - assertThat(McpProviderUtils.isUriTemplate("/")).isFalse(); - assertThat(McpProviderUtils.isUriTemplate("//")).isFalse(); + assertThat(McpPredicates.isUriTemplate("/")).isFalse(); + assertThat(McpPredicates.isUriTemplate("//")).isFalse(); } @Test public void testIsUriTemplateWithIncompleteBraces() { - assertThat(McpProviderUtils.isUriTemplate("/api/{id")).isFalse(); - assertThat(McpProviderUtils.isUriTemplate("/api/id}")).isFalse(); + assertThat(McpPredicates.isUriTemplate("/api/{id")).isFalse(); + assertThat(McpPredicates.isUriTemplate("/api/id}")).isFalse(); } @Test public void testIsUriTemplateWithEmptyBraces() { - assertThat(McpProviderUtils.isUriTemplate("/api/{}")).isFalse(); + assertThat(McpPredicates.isUriTemplate("/api/{}")).isFalse(); } @Test public void testIsUriTemplateWithNestedPath() { - assertThat(McpProviderUtils.isUriTemplate("/api/v1/users/{userId}/posts/{postId}/comments")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/v1/users/{userId}/posts/{postId}/comments")).isTrue(); } // Reactive Return Type Predicate Tests @@ -155,37 +155,37 @@ public void testIsUriTemplateWithNestedPath() { @Test public void testIsReactiveReturnTypeWithMono() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("monoMethod"); - assertThat(McpProviderUtils.isReactiveReturnType.test(method)).isTrue(); + assertThat(McpPredicates.isReactiveReturnType.test(method)).isTrue(); } @Test public void testIsReactiveReturnTypeWithFlux() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("fluxMethod"); - assertThat(McpProviderUtils.isReactiveReturnType.test(method)).isTrue(); + assertThat(McpPredicates.isReactiveReturnType.test(method)).isTrue(); } @Test public void testIsReactiveReturnTypeWithPublisher() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("publisherMethod"); - assertThat(McpProviderUtils.isReactiveReturnType.test(method)).isTrue(); + assertThat(McpPredicates.isReactiveReturnType.test(method)).isTrue(); } @Test public void testIsReactiveReturnTypeWithNonReactive() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("nonReactiveMethod"); - assertThat(McpProviderUtils.isReactiveReturnType.test(method)).isFalse(); + assertThat(McpPredicates.isReactiveReturnType.test(method)).isFalse(); } @Test public void testIsReactiveReturnTypeWithVoid() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("voidMethod"); - assertThat(McpProviderUtils.isReactiveReturnType.test(method)).isFalse(); + assertThat(McpPredicates.isReactiveReturnType.test(method)).isFalse(); } @Test public void testIsReactiveReturnTypeWithList() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("listMethod"); - assertThat(McpProviderUtils.isReactiveReturnType.test(method)).isFalse(); + assertThat(McpPredicates.isReactiveReturnType.test(method)).isFalse(); } // Non-Reactive Return Type Predicate Tests @@ -193,37 +193,37 @@ public void testIsReactiveReturnTypeWithList() throws NoSuchMethodException { @Test public void testIsNotReactiveReturnTypeWithMono() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("monoMethod"); - assertThat(McpProviderUtils.isNotReactiveReturnType.test(method)).isFalse(); + assertThat(McpPredicates.isNotReactiveReturnType.test(method)).isFalse(); } @Test public void testIsNotReactiveReturnTypeWithFlux() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("fluxMethod"); - assertThat(McpProviderUtils.isNotReactiveReturnType.test(method)).isFalse(); + assertThat(McpPredicates.isNotReactiveReturnType.test(method)).isFalse(); } @Test public void testIsNotReactiveReturnTypeWithPublisher() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("publisherMethod"); - assertThat(McpProviderUtils.isNotReactiveReturnType.test(method)).isFalse(); + assertThat(McpPredicates.isNotReactiveReturnType.test(method)).isFalse(); } @Test public void testIsNotReactiveReturnTypeWithNonReactive() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("nonReactiveMethod"); - assertThat(McpProviderUtils.isNotReactiveReturnType.test(method)).isTrue(); + assertThat(McpPredicates.isNotReactiveReturnType.test(method)).isTrue(); } @Test public void testIsNotReactiveReturnTypeWithVoid() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("voidMethod"); - assertThat(McpProviderUtils.isNotReactiveReturnType.test(method)).isTrue(); + assertThat(McpPredicates.isNotReactiveReturnType.test(method)).isTrue(); } @Test public void testIsNotReactiveReturnTypeWithList() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("listMethod"); - assertThat(McpProviderUtils.isNotReactiveReturnType.test(method)).isTrue(); + assertThat(McpPredicates.isNotReactiveReturnType.test(method)).isTrue(); } // Filter Non-Reactive Return Type Method Tests @@ -231,14 +231,14 @@ public void testIsNotReactiveReturnTypeWithList() throws NoSuchMethodException { @Test public void testFilterNonReactiveReturnTypeMethodWithReactiveType() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("monoMethod"); - Predicate filter = McpProviderUtils.filterNonReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterNonReactiveReturnTypeMethod(); assertThat(filter.test(method)).isTrue(); } @Test public void testFilterNonReactiveReturnTypeMethodWithNonReactiveType() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("nonReactiveMethod"); - Predicate filter = McpProviderUtils.filterNonReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterNonReactiveReturnTypeMethod(); // This should return false and log a warning assertThat(filter.test(method)).isFalse(); } @@ -246,14 +246,14 @@ public void testFilterNonReactiveReturnTypeMethodWithNonReactiveType() throws No @Test public void testFilterNonReactiveReturnTypeMethodWithFlux() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("fluxMethod"); - Predicate filter = McpProviderUtils.filterNonReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterNonReactiveReturnTypeMethod(); assertThat(filter.test(method)).isTrue(); } @Test public void testFilterNonReactiveReturnTypeMethodWithPublisher() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("publisherMethod"); - Predicate filter = McpProviderUtils.filterNonReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterNonReactiveReturnTypeMethod(); assertThat(filter.test(method)).isTrue(); } @@ -262,7 +262,7 @@ public void testFilterNonReactiveReturnTypeMethodWithPublisher() throws NoSuchMe @Test public void testFilterReactiveReturnTypeMethodWithReactiveType() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("monoMethod"); - Predicate filter = McpProviderUtils.filterReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterReactiveReturnTypeMethod(); // This should return false and log a warning assertThat(filter.test(method)).isFalse(); } @@ -270,14 +270,14 @@ public void testFilterReactiveReturnTypeMethodWithReactiveType() throws NoSuchMe @Test public void testFilterReactiveReturnTypeMethodWithNonReactiveType() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("nonReactiveMethod"); - Predicate filter = McpProviderUtils.filterReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterReactiveReturnTypeMethod(); assertThat(filter.test(method)).isTrue(); } @Test public void testFilterReactiveReturnTypeMethodWithFlux() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("fluxMethod"); - Predicate filter = McpProviderUtils.filterReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterReactiveReturnTypeMethod(); // This should return false and log a warning assertThat(filter.test(method)).isFalse(); } @@ -285,7 +285,7 @@ public void testFilterReactiveReturnTypeMethodWithFlux() throws NoSuchMethodExce @Test public void testFilterReactiveReturnTypeMethodWithPublisher() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("publisherMethod"); - Predicate filter = McpProviderUtils.filterReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterReactiveReturnTypeMethod(); // This should return false and log a warning assertThat(filter.test(method)).isFalse(); } @@ -293,7 +293,7 @@ public void testFilterReactiveReturnTypeMethodWithPublisher() throws NoSuchMetho @Test public void testFilterReactiveReturnTypeMethodWithVoid() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("voidMethod"); - Predicate filter = McpProviderUtils.filterReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterReactiveReturnTypeMethod(); assertThat(filter.test(method)).isTrue(); } @@ -302,7 +302,7 @@ public void testFilterReactiveReturnTypeMethodWithVoid() throws NoSuchMethodExce @Test public void testFilterMethodWithBidirectionalParametersWithSyncContext() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("methodWithSyncContext", McpSyncRequestContext.class); - Predicate filter = McpProviderUtils.filterMethodWithBidirectionalParameters(); + Predicate filter = McpPredicates.filterMethodWithBidirectionalParameters(); // This should return false and log a warning assertThat(filter.test(method)).isFalse(); } @@ -310,7 +310,7 @@ public void testFilterMethodWithBidirectionalParametersWithSyncContext() throws @Test public void testFilterMethodWithBidirectionalParametersWithAsyncContext() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("methodWithAsyncContext", McpAsyncRequestContext.class); - Predicate filter = McpProviderUtils.filterMethodWithBidirectionalParameters(); + Predicate filter = McpPredicates.filterMethodWithBidirectionalParameters(); // This should return false and log a warning assertThat(filter.test(method)).isFalse(); } @@ -318,7 +318,7 @@ public void testFilterMethodWithBidirectionalParametersWithAsyncContext() throws @Test public void testFilterMethodWithBidirectionalParametersWithSyncExchange() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("methodWithSyncExchange", McpSyncServerExchange.class); - Predicate filter = McpProviderUtils.filterMethodWithBidirectionalParameters(); + Predicate filter = McpPredicates.filterMethodWithBidirectionalParameters(); // This should return false and log a warning assertThat(filter.test(method)).isFalse(); } @@ -326,7 +326,7 @@ public void testFilterMethodWithBidirectionalParametersWithSyncExchange() throws @Test public void testFilterMethodWithBidirectionalParametersWithAsyncExchange() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("methodWithAsyncExchange", McpAsyncServerExchange.class); - Predicate filter = McpProviderUtils.filterMethodWithBidirectionalParameters(); + Predicate filter = McpPredicates.filterMethodWithBidirectionalParameters(); // This should return false and log a warning assertThat(filter.test(method)).isFalse(); } @@ -335,7 +335,7 @@ public void testFilterMethodWithBidirectionalParametersWithAsyncExchange() throw public void testFilterMethodWithBidirectionalParametersWithMultipleParams() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("methodWithMultipleParams", String.class, McpSyncRequestContext.class, int.class); - Predicate filter = McpProviderUtils.filterMethodWithBidirectionalParameters(); + Predicate filter = McpPredicates.filterMethodWithBidirectionalParameters(); // This should return false because it has a bidirectional parameter assertThat(filter.test(method)).isFalse(); } @@ -343,14 +343,14 @@ public void testFilterMethodWithBidirectionalParametersWithMultipleParams() thro @Test public void testFilterMethodWithBidirectionalParametersWithoutBidirectionalParams() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("methodWithoutBidirectionalParams", String.class, int.class); - Predicate filter = McpProviderUtils.filterMethodWithBidirectionalParameters(); + Predicate filter = McpPredicates.filterMethodWithBidirectionalParameters(); assertThat(filter.test(method)).isTrue(); } @Test public void testFilterMethodWithBidirectionalParametersWithNoParams() throws NoSuchMethodException { Method method = TestMethods.class.getMethod("nonReactiveMethod"); - Predicate filter = McpProviderUtils.filterMethodWithBidirectionalParameters(); + Predicate filter = McpPredicates.filterMethodWithBidirectionalParameters(); assertThat(filter.test(method)).isTrue(); } @@ -366,8 +366,8 @@ public void testCombinedFiltersForStatelessSyncProvider() throws NoSuchMethodExc Method reactiveMethod = TestMethods.class.getMethod("monoMethod"); Method bidirectionalMethod = TestMethods.class.getMethod("methodWithSyncContext", McpSyncRequestContext.class); - Predicate reactiveFilter = McpProviderUtils.filterReactiveReturnTypeMethod(); - Predicate bidirectionalFilter = McpProviderUtils.filterMethodWithBidirectionalParameters(); + Predicate reactiveFilter = McpPredicates.filterReactiveReturnTypeMethod(); + Predicate bidirectionalFilter = McpPredicates.filterMethodWithBidirectionalParameters(); Predicate combinedFilter = reactiveFilter.and(bidirectionalFilter); assertThat(combinedFilter.test(validMethod)).isTrue(); @@ -386,8 +386,8 @@ public void testCombinedFiltersForStatelessAsyncProvider() throws NoSuchMethodEx Method bidirectionalMethod = TestMethods.class.getMethod("methodWithAsyncContext", McpAsyncRequestContext.class); - Predicate nonReactiveFilter = McpProviderUtils.filterNonReactiveReturnTypeMethod(); - Predicate bidirectionalFilter = McpProviderUtils.filterMethodWithBidirectionalParameters(); + Predicate nonReactiveFilter = McpPredicates.filterNonReactiveReturnTypeMethod(); + Predicate bidirectionalFilter = McpPredicates.filterMethodWithBidirectionalParameters(); Predicate combinedFilter = nonReactiveFilter.and(bidirectionalFilter); assertThat(combinedFilter.test(validMethod)).isTrue(); @@ -399,30 +399,30 @@ public void testCombinedFiltersForStatelessAsyncProvider() throws NoSuchMethodEx @Test public void testIsUriTemplateWithSpecialCharacters() { - assertThat(McpProviderUtils.isUriTemplate("/api/{user-id}")).isTrue(); - assertThat(McpProviderUtils.isUriTemplate("/api/{user.id}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/{user-id}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/{user.id}")).isTrue(); } @Test public void testIsUriTemplateWithQueryParameters() { // Query parameters are not URI template variables - assertThat(McpProviderUtils.isUriTemplate("/api/users?id={id}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/users?id={id}")).isTrue(); } @Test public void testIsUriTemplateWithFragment() { - assertThat(McpProviderUtils.isUriTemplate("/api/users#{id}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/api/users#{id}")).isTrue(); } @Test public void testIsUriTemplateWithMultipleConsecutiveVariables() { - assertThat(McpProviderUtils.isUriTemplate("/{id}{name}")).isTrue(); + assertThat(McpPredicates.isUriTemplate("/{id}{name}")).isTrue(); } @Test public void testPredicatesAreReusable() throws NoSuchMethodException { // Test that predicates can be reused multiple times - Predicate filter = McpProviderUtils.filterReactiveReturnTypeMethod(); + Predicate filter = McpPredicates.filterReactiveReturnTypeMethod(); Method method1 = TestMethods.class.getMethod("nonReactiveMethod"); Method method2 = TestMethods.class.getMethod("monoMethod"); diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/complete/AsyncMcpCompleteMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/complete/AsyncMcpCompleteMethodCallbackTests.java index b4b3308..22ca6a5 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/complete/AsyncMcpCompleteMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/complete/AsyncMcpCompleteMethodCallbackTests.java @@ -18,6 +18,8 @@ import org.springaicommunity.mcp.annotation.McpComplete; import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpProgressToken; +import org.springaicommunity.mcp.context.McpAsyncRequestContext; +import org.springaicommunity.mcp.context.McpSyncRequestContext; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -25,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for {@link AsyncMcpCompleteMethodCallback}. @@ -173,6 +176,33 @@ public Mono duplicateMetaParameters(McpMeta meta1, McpMeta meta2 return Mono.just(new CompleteResult(new CompleteCompletion(List.of(), 0, false))); } + public Mono getCompletionWithAsyncRequestContext(McpAsyncRequestContext context) { + CompleteRequest request = (CompleteRequest) context.request(); + return Mono.just(new CompleteResult(new CompleteCompletion( + List.of("Async completion with async context for: " + request.argument().value()), 1, false))); + } + + public Mono getCompletionWithAsyncRequestContextAndValue(McpAsyncRequestContext context, + String value) { + CompleteRequest request = (CompleteRequest) context.request(); + return Mono.just(new CompleteResult(new CompleteCompletion(List + .of("Async completion with async context and value: " + value + " for: " + request.argument().value()), + 1, false))); + } + + public Mono duplicateAsyncRequestContextParameters(McpAsyncRequestContext context1, + McpAsyncRequestContext context2) { + return Mono.just(new CompleteResult(new CompleteCompletion(List.of(), 0, false))); + } + + public Mono invalidSyncRequestContextInAsyncMethod(McpSyncRequestContext context) { + return Mono.just(new CompleteResult(new CompleteCompletion(List.of(), 0, false))); + } + + public CompleteResult invalidAsyncRequestContextInSyncMethod(McpAsyncRequestContext context) { + return new CompleteResult(new CompleteCompletion(List.of(), 0, false)); + } + } // Helper method to create a mock McpComplete annotation @@ -849,4 +879,119 @@ public void testDuplicateMetaParameters() throws Exception { .hasMessageContaining("Method cannot have more than one McpMeta parameter"); } + @Test + public void testCallbackWithAsyncRequestContext() throws Exception { + TestAsyncCompleteProvider provider = new TestAsyncCompleteProvider(); + Method method = TestAsyncCompleteProvider.class.getMethod("getCompletionWithAsyncRequestContext", + McpAsyncRequestContext.class); + + BiFunction> callback = AsyncMcpCompleteMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build(); + + McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class); + CompleteRequest request = new CompleteRequest(new PromptReference("test-prompt"), + new CompleteRequest.CompleteArgument("test", "value")); + + Mono resultMono = callback.apply(exchange, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.completion()).isNotNull(); + assertThat(result.completion().values()).hasSize(1); + assertThat(result.completion().values().get(0)).isEqualTo("Async completion with async context for: value"); + }).verifyComplete(); + } + + @Test + public void testCallbackWithAsyncRequestContextAndValue() throws Exception { + TestAsyncCompleteProvider provider = new TestAsyncCompleteProvider(); + Method method = TestAsyncCompleteProvider.class.getMethod("getCompletionWithAsyncRequestContextAndValue", + McpAsyncRequestContext.class, String.class); + + BiFunction> callback = AsyncMcpCompleteMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build(); + + McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class); + CompleteRequest request = new CompleteRequest(new PromptReference("test-prompt"), + new CompleteRequest.CompleteArgument("test", "value")); + + Mono resultMono = callback.apply(exchange, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.completion()).isNotNull(); + assertThat(result.completion().values()).hasSize(1); + assertThat(result.completion().values().get(0)) + .isEqualTo("Async completion with async context and value: value for: value"); + }).verifyComplete(); + } + + @Test + public void testDuplicateAsyncRequestContextParameters() throws Exception { + TestAsyncCompleteProvider provider = new TestAsyncCompleteProvider(); + Method method = TestAsyncCompleteProvider.class.getMethod("duplicateAsyncRequestContextParameters", + McpAsyncRequestContext.class, McpAsyncRequestContext.class); + + assertThatThrownBy(() -> AsyncMcpCompleteMethodCallback.builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Method cannot have more than one request context parameter"); + } + + @Test + public void testInvalidSyncRequestContextInAsyncMethod() throws Exception { + TestAsyncCompleteProvider provider = new TestAsyncCompleteProvider(); + Method method = TestAsyncCompleteProvider.class.getMethod("invalidSyncRequestContextInAsyncMethod", + McpSyncRequestContext.class); + + assertThatThrownBy(() -> AsyncMcpCompleteMethodCallback.builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter"); + } + + @Test + public void testCallbackWithProgressTokenNonNull() throws Exception { + TestAsyncCompleteProvider provider = new TestAsyncCompleteProvider(); + Method method = TestAsyncCompleteProvider.class.getMethod("getCompletionWithProgressToken", String.class, + CompleteRequest.class); + + BiFunction> callback = AsyncMcpCompleteMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build(); + + McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class); + // Create a CompleteRequest with progressToken using a mock + CompleteRequest request = mock(CompleteRequest.class); + when(request.ref()).thenReturn(new PromptReference("test-prompt")); + when(request.argument()).thenReturn(new CompleteRequest.CompleteArgument("test", "value")); + when(request.progressToken()).thenReturn("progress-123"); + + Mono resultMono = callback.apply(exchange, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.completion()).isNotNull(); + assertThat(result.completion().values()).hasSize(1); + assertThat(result.completion().values().get(0)) + .isEqualTo("Async completion with progress (token: progress-123) for: value"); + }).verifyComplete(); + } + } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/complete/SyncMcpCompleteMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/complete/SyncMcpCompleteMethodCallbackTests.java index 73a3817..0a547f4 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/complete/SyncMcpCompleteMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/complete/SyncMcpCompleteMethodCallbackTests.java @@ -18,10 +18,15 @@ import org.springaicommunity.mcp.annotation.McpComplete; import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpProgressToken; +import org.springaicommunity.mcp.context.McpAsyncRequestContext; +import org.springaicommunity.mcp.context.McpSyncRequestContext; + +import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for {@link SyncMcpCompleteMethodCallback}. @@ -145,6 +150,32 @@ public CompleteResult duplicateMetaParameters(McpMeta meta1, McpMeta meta2) { return new CompleteResult(new CompleteCompletion(List.of(), 0, false)); } + public CompleteResult getCompletionWithSyncRequestContext(McpSyncRequestContext context) { + CompleteRequest request = (CompleteRequest) context.request(); + return new CompleteResult(new CompleteCompletion( + List.of("Completion with sync context for: " + request.argument().value()), 1, false)); + } + + public CompleteResult getCompletionWithSyncRequestContextAndValue(McpSyncRequestContext context, String value) { + CompleteRequest request = (CompleteRequest) context.request(); + return new CompleteResult(new CompleteCompletion( + List.of("Completion with sync context and value: " + value + " for: " + request.argument().value()), + 1, false)); + } + + public CompleteResult duplicateSyncRequestContextParameters(McpSyncRequestContext context1, + McpSyncRequestContext context2) { + return new CompleteResult(new CompleteCompletion(List.of(), 0, false)); + } + + public CompleteResult invalidAsyncRequestContextInSyncMethod(McpAsyncRequestContext context) { + return new CompleteResult(new CompleteCompletion(List.of(), 0, false)); + } + + public Mono invalidSyncRequestContextInAsyncMethod(McpSyncRequestContext context) { + return Mono.just(new CompleteResult(new CompleteCompletion(List.of(), 0, false))); + } + } // Helper method to create a mock McpComplete annotation @@ -682,4 +713,116 @@ public void testDuplicateMetaParameters() throws Exception { .hasMessageContaining("Method cannot have more than one McpMeta parameter"); } + @Test + public void testCallbackWithSyncRequestContext() throws Exception { + TestCompleteProvider provider = new TestCompleteProvider(); + Method method = TestCompleteProvider.class.getMethod("getCompletionWithSyncRequestContext", + McpSyncRequestContext.class); + + BiFunction callback = SyncMcpCompleteMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build(); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + CompleteRequest request = new CompleteRequest(new PromptReference("test-prompt"), + new CompleteRequest.CompleteArgument("test", "value")); + + CompleteResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.completion()).isNotNull(); + assertThat(result.completion().values()).hasSize(1); + assertThat(result.completion().values().get(0)).isEqualTo("Completion with sync context for: value"); + } + + @Test + public void testCallbackWithSyncRequestContextAndValue() throws Exception { + TestCompleteProvider provider = new TestCompleteProvider(); + Method method = TestCompleteProvider.class.getMethod("getCompletionWithSyncRequestContextAndValue", + McpSyncRequestContext.class, String.class); + + BiFunction callback = SyncMcpCompleteMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build(); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + CompleteRequest request = new CompleteRequest(new PromptReference("test-prompt"), + new CompleteRequest.CompleteArgument("test", "value")); + + CompleteResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.completion()).isNotNull(); + assertThat(result.completion().values()).hasSize(1); + assertThat(result.completion().values().get(0)) + .isEqualTo("Completion with sync context and value: value for: value"); + } + + @Test + public void testDuplicateSyncRequestContextParameters() throws Exception { + TestCompleteProvider provider = new TestCompleteProvider(); + Method method = TestCompleteProvider.class.getMethod("duplicateSyncRequestContextParameters", + McpSyncRequestContext.class, McpSyncRequestContext.class); + + assertThatThrownBy(() -> SyncMcpCompleteMethodCallback.builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Method cannot have more than one request context parameter"); + } + + @Test + public void testInvalidAsyncRequestContextInSyncMethod() throws Exception { + TestCompleteProvider provider = new TestCompleteProvider(); + Method method = TestCompleteProvider.class.getMethod("invalidAsyncRequestContextInSyncMethod", + McpAsyncRequestContext.class); + + assertThatThrownBy(() -> SyncMcpCompleteMethodCallback.builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter"); + } + + @Test + public void testCallbackWithProgressTokenNonNull() throws Exception { + TestCompleteProvider provider = new TestCompleteProvider(); + Method method = TestCompleteProvider.class.getMethod("getCompletionWithProgressToken", String.class, + CompleteRequest.class); + + BiFunction callback = SyncMcpCompleteMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt("test-prompt") + .build(); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + // Create a CompleteRequest with progressToken using reflection or a builder + // pattern + // Since the exact constructor signature is not clear, we'll test with a mock that + // returns the progressToken + CompleteRequest request = mock(CompleteRequest.class); + when(request.ref()).thenReturn(new PromptReference("test-prompt")); + when(request.argument()).thenReturn(new CompleteRequest.CompleteArgument("test", "value")); + when(request.progressToken()).thenReturn("progress-123"); + + CompleteResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.completion()).isNotNull(); + assertThat(result.completion().values()).hasSize(1); + assertThat(result.completion().values().get(0)) + .isEqualTo("Completion with progress (token: progress-123) for: value"); + } + } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/AsyncMcpPromptMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/AsyncMcpPromptMethodCallbackTests.java index db2e412..42b61f6 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/AsyncMcpPromptMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/AsyncMcpPromptMethodCallbackTests.java @@ -25,6 +25,8 @@ import org.springaicommunity.mcp.annotation.McpArg; import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpPrompt; +import org.springaicommunity.mcp.context.McpAsyncRequestContext; +import org.springaicommunity.mcp.context.McpSyncRequestContext; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -183,6 +185,32 @@ public Mono getPromptWithTransportContext(McpTransportContext c new TextContent("Hello with transport context from " + request.name()))))); } + @McpPrompt(name = "async-request-context-prompt", description = "A prompt with async request context") + public Mono getPromptWithAsyncRequestContext(McpAsyncRequestContext context) { + GetPromptRequest request = (GetPromptRequest) context.request(); + return Mono + .just(new GetPromptResult("Async request context prompt", List.of(new PromptMessage(Role.ASSISTANT, + new TextContent("Hello with async context from " + request.name()))))); + } + + @McpPrompt(name = "async-context-with-args", description = "A prompt with async context and arguments") + public Mono getPromptWithAsyncContextAndArgs(McpAsyncRequestContext context, + @McpArg(name = "name", description = "The user's name", required = true) String name) { + GetPromptRequest request = (GetPromptRequest) context.request(); + return Mono + .just(new GetPromptResult("Async context with args prompt", List.of(new PromptMessage(Role.ASSISTANT, + new TextContent("Hello " + name + " with async context from " + request.name()))))); + } + + public Mono duplicateAsyncRequestContextParameters(McpAsyncRequestContext context1, + McpAsyncRequestContext context2) { + return Mono.just(new GetPromptResult("Invalid", List.of())); + } + + public Mono invalidSyncRequestContextInAsyncMethod(McpSyncRequestContext context) { + return Mono.just(new GetPromptResult("Invalid", List.of())); + } + } private Prompt createTestPrompt(String name, String description) { @@ -581,4 +609,99 @@ public void testCallbackWithTransportContext() throws Exception { }).verifyComplete(); } + @Test + public void testCallbackWithAsyncRequestContext() throws Exception { + TestPromptProvider provider = new TestPromptProvider(); + Method method = TestPromptProvider.class.getMethod("getPromptWithAsyncRequestContext", + McpAsyncRequestContext.class); + + Prompt prompt = createTestPrompt("async-request-context-prompt", "A prompt with async request context"); + + BiFunction> callback = AsyncMcpPromptMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt(prompt) + .build(); + + McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class); + Map args = new HashMap<>(); + args.put("name", "John"); + GetPromptRequest request = new GetPromptRequest("async-request-context-prompt", args); + + Mono resultMono = callback.apply(exchange, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.description()).isEqualTo("Async request context prompt"); + assertThat(result.messages()).hasSize(1); + PromptMessage message = result.messages().get(0); + assertThat(message.role()).isEqualTo(Role.ASSISTANT); + assertThat(((TextContent) message.content()).text()) + .isEqualTo("Hello with async context from async-request-context-prompt"); + }).verifyComplete(); + } + + @Test + public void testCallbackWithAsyncRequestContextAndArgs() throws Exception { + TestPromptProvider provider = new TestPromptProvider(); + Method method = TestPromptProvider.class.getMethod("getPromptWithAsyncContextAndArgs", + McpAsyncRequestContext.class, String.class); + + Prompt prompt = createTestPrompt("async-context-with-args", "A prompt with async context and arguments"); + + BiFunction> callback = AsyncMcpPromptMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt(prompt) + .build(); + + McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class); + Map args = new HashMap<>(); + args.put("name", "John"); + GetPromptRequest request = new GetPromptRequest("async-context-with-args", args); + + Mono resultMono = callback.apply(exchange, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.description()).isEqualTo("Async context with args prompt"); + assertThat(result.messages()).hasSize(1); + PromptMessage message = result.messages().get(0); + assertThat(message.role()).isEqualTo(Role.ASSISTANT); + assertThat(((TextContent) message.content()).text()) + .isEqualTo("Hello John with async context from async-context-with-args"); + }).verifyComplete(); + } + + @Test + public void testDuplicateAsyncRequestContextParameters() throws Exception { + TestPromptProvider provider = new TestPromptProvider(); + Method method = TestPromptProvider.class.getMethod("duplicateAsyncRequestContextParameters", + McpAsyncRequestContext.class, McpAsyncRequestContext.class); + + Prompt prompt = createTestPrompt("invalid", "Invalid parameters"); + + assertThatThrownBy( + () -> AsyncMcpPromptMethodCallback.builder().method(method).bean(provider).prompt(prompt).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Method cannot have more than one request context parameter"); + } + + @Test + public void testInvalidSyncRequestContextInAsyncMethod() throws Exception { + TestPromptProvider provider = new TestPromptProvider(); + Method method = TestPromptProvider.class.getMethod("invalidSyncRequestContextInAsyncMethod", + McpSyncRequestContext.class); + + Prompt prompt = createTestPrompt("invalid", "Invalid parameter type"); + + assertThatThrownBy( + () -> AsyncMcpPromptMethodCallback.builder().method(method).bean(provider).prompt(prompt).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter"); + } + } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/SyncMcpPromptMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/SyncMcpPromptMethodCallbackTests.java index 4e59b18..359b553 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/SyncMcpPromptMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/SyncMcpPromptMethodCallbackTests.java @@ -19,8 +19,11 @@ import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpProgressToken; import org.springaicommunity.mcp.annotation.McpPrompt; +import org.springaicommunity.mcp.context.McpAsyncRequestContext; +import org.springaicommunity.mcp.context.McpSyncRequestContext; import io.modelcontextprotocol.server.McpSyncServerExchange; +import reactor.core.publisher.Mono; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; @@ -162,6 +165,34 @@ public GetPromptResult duplicateMetaParameters(McpMeta meta1, McpMeta meta2) { return new GetPromptResult("Invalid", List.of()); } + @McpPrompt(name = "sync-request-context-prompt", description = "A prompt with sync request context") + public GetPromptResult getPromptWithSyncRequestContext(McpSyncRequestContext context) { + GetPromptRequest request = (GetPromptRequest) context.request(); + return new GetPromptResult("Sync request context prompt", List.of(new PromptMessage(Role.ASSISTANT, + new TextContent("Hello with sync context from " + request.name())))); + } + + @McpPrompt(name = "sync-context-with-args", description = "A prompt with sync context and arguments") + public GetPromptResult getPromptWithSyncContextAndArgs(McpSyncRequestContext context, + @McpArg(name = "name", description = "The user's name", required = true) String name) { + GetPromptRequest request = (GetPromptRequest) context.request(); + return new GetPromptResult("Sync context with args prompt", List.of(new PromptMessage(Role.ASSISTANT, + new TextContent("Hello " + name + " with sync context from " + request.name())))); + } + + public GetPromptResult duplicateSyncRequestContextParameters(McpSyncRequestContext context1, + McpSyncRequestContext context2) { + return new GetPromptResult("Invalid", List.of()); + } + + public GetPromptResult invalidAsyncRequestContextInSyncMethod(McpAsyncRequestContext context) { + return new GetPromptResult("Invalid", List.of()); + } + + public Mono invalidSyncRequestContextInAsyncMethod(McpSyncRequestContext context) { + return Mono.just(new GetPromptResult("Invalid", List.of())); + } + } private Prompt createTestPrompt(String name, String description) { @@ -733,4 +764,95 @@ public void testDuplicateMetaParameters() throws Exception { .hasMessageContaining("Method cannot have more than one McpMeta parameter"); } + @Test + public void testCallbackWithSyncRequestContext() throws Exception { + TestPromptProvider provider = new TestPromptProvider(); + Method method = TestPromptProvider.class.getMethod("getPromptWithSyncRequestContext", + McpSyncRequestContext.class); + + Prompt prompt = createTestPrompt("sync-request-context-prompt", "A prompt with sync request context"); + + BiFunction callback = SyncMcpPromptMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt(prompt) + .build(); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + Map args = new HashMap<>(); + args.put("name", "John"); + GetPromptRequest request = new GetPromptRequest("sync-request-context-prompt", args); + + GetPromptResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.description()).isEqualTo("Sync request context prompt"); + assertThat(result.messages()).hasSize(1); + PromptMessage message = result.messages().get(0); + assertThat(message.role()).isEqualTo(Role.ASSISTANT); + assertThat(((TextContent) message.content()).text()) + .isEqualTo("Hello with sync context from sync-request-context-prompt"); + } + + @Test + public void testCallbackWithSyncRequestContextAndArgs() throws Exception { + TestPromptProvider provider = new TestPromptProvider(); + Method method = TestPromptProvider.class.getMethod("getPromptWithSyncContextAndArgs", + McpSyncRequestContext.class, String.class); + + Prompt prompt = createTestPrompt("sync-context-with-args", "A prompt with sync context and arguments"); + + BiFunction callback = SyncMcpPromptMethodCallback + .builder() + .method(method) + .bean(provider) + .prompt(prompt) + .build(); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + Map args = new HashMap<>(); + args.put("name", "John"); + GetPromptRequest request = new GetPromptRequest("sync-context-with-args", args); + + GetPromptResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.description()).isEqualTo("Sync context with args prompt"); + assertThat(result.messages()).hasSize(1); + PromptMessage message = result.messages().get(0); + assertThat(message.role()).isEqualTo(Role.ASSISTANT); + assertThat(((TextContent) message.content()).text()) + .isEqualTo("Hello John with sync context from sync-context-with-args"); + } + + @Test + public void testDuplicateSyncRequestContextParameters() throws Exception { + TestPromptProvider provider = new TestPromptProvider(); + Method method = TestPromptProvider.class.getMethod("duplicateSyncRequestContextParameters", + McpSyncRequestContext.class, McpSyncRequestContext.class); + + Prompt prompt = createTestPrompt("invalid", "Invalid parameters"); + + assertThatThrownBy( + () -> SyncMcpPromptMethodCallback.builder().method(method).bean(provider).prompt(prompt).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Method cannot have more than one request context parameter"); + } + + @Test + public void testInvalidAsyncRequestContextInSyncMethod() throws Exception { + TestPromptProvider provider = new TestPromptProvider(); + Method method = TestPromptProvider.class.getMethod("invalidAsyncRequestContextInSyncMethod", + McpAsyncRequestContext.class); + + Prompt prompt = createTestPrompt("invalid", "Invalid parameter type"); + + assertThatThrownBy( + () -> SyncMcpPromptMethodCallback.builder().method(method).bean(provider).prompt(prompt).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter"); + } + } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java index 3eed2b5..7234cc1 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallbackTests.java @@ -26,6 +26,8 @@ import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpProgressToken; import org.springaicommunity.mcp.annotation.McpResource; +import org.springaicommunity.mcp.context.McpAsyncRequestContext; +import org.springaicommunity.mcp.context.McpSyncRequestContext; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -253,6 +255,29 @@ public Mono getResourceWithTransportContext(McpTransportCont "Content with transport context for " + request.uri())))); } + public Mono getResourceWithAsyncRequestContext(McpAsyncRequestContext context) { + ReadResourceRequest request = (ReadResourceRequest) context.request(); + return Mono.just(new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain", + "Async content with async context for " + request.uri())))); + } + + @McpResource(uri = "users/{userId}/posts/{postId}") + public Mono getResourceWithAsyncRequestContextAndUriVariables( + McpAsyncRequestContext context, String userId, String postId) { + ReadResourceRequest request = (ReadResourceRequest) context.request(); + return Mono.just(new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain", + "Async User: " + userId + ", Post: " + postId + " with async context")))); + } + + public Mono duplicateAsyncRequestContextParameters(McpAsyncRequestContext context1, + McpAsyncRequestContext context2) { + return Mono.just(new ReadResourceResult(List.of())); + } + + public Mono invalidSyncRequestContextInAsyncMethod(McpSyncRequestContext context) { + return Mono.just(new ReadResourceResult(List.of())); + } + } // Helper method to create a mock McpResource annotation @@ -1217,4 +1242,88 @@ public void testInvalidSyncExchangeParameter() throws Exception { .hasMessageContaining("McpSyncServerExchange"); } + @Test + public void testCallbackWithAsyncRequestContext() throws Exception { + TestAsyncResourceProvider provider = new TestAsyncResourceProvider(); + Method method = TestAsyncResourceProvider.class.getMethod("getResourceWithAsyncRequestContext", + McpAsyncRequestContext.class); + + BiFunction> callback = AsyncMcpResourceMethodCallback + .builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(createMockMcpResource())) + .build(); + + McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class); + ReadResourceRequest request = new ReadResourceRequest("test/resource"); + + Mono resultMono = callback.apply(exchange, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.contents()).hasSize(1); + assertThat(result.contents().get(0)).isInstanceOf(TextResourceContents.class); + TextResourceContents textContent = (TextResourceContents) result.contents().get(0); + assertThat(textContent.text()).isEqualTo("Async content with async context for test/resource"); + }).verifyComplete(); + } + + @Test + public void testCallbackWithAsyncRequestContextAndUriVariables() throws Exception { + TestAsyncResourceProvider provider = new TestAsyncResourceProvider(); + Method method = TestAsyncResourceProvider.class.getMethod("getResourceWithAsyncRequestContextAndUriVariables", + McpAsyncRequestContext.class, String.class, String.class); + McpResource resourceAnnotation = method.getAnnotation(McpResource.class); + + BiFunction> callback = AsyncMcpResourceMethodCallback + .builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(resourceAnnotation)) + .build(); + + McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class); + ReadResourceRequest request = new ReadResourceRequest("users/123/posts/456"); + + Mono resultMono = callback.apply(exchange, request); + + StepVerifier.create(resultMono).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.contents()).hasSize(1); + assertThat(result.contents().get(0)).isInstanceOf(TextResourceContents.class); + TextResourceContents textContent = (TextResourceContents) result.contents().get(0); + assertThat(textContent.text()).isEqualTo("Async User: 123, Post: 456 with async context"); + }).verifyComplete(); + } + + @Test + public void testDuplicateAsyncRequestContextParameters() throws Exception { + TestAsyncResourceProvider provider = new TestAsyncResourceProvider(); + Method method = TestAsyncResourceProvider.class.getMethod("duplicateAsyncRequestContextParameters", + McpAsyncRequestContext.class, McpAsyncRequestContext.class); + + assertThatThrownBy(() -> AsyncMcpResourceMethodCallback.builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(createMockMcpResource())) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Method cannot have more than one request context parameter"); + } + + @Test + public void testInvalidSyncRequestContextInAsyncMethod() throws Exception { + TestAsyncResourceProvider provider = new TestAsyncResourceProvider(); + Method method = TestAsyncResourceProvider.class.getMethod("invalidSyncRequestContextInAsyncMethod", + McpSyncRequestContext.class); + + assertThatThrownBy(() -> AsyncMcpResourceMethodCallback.builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(createMockMcpResource())) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter"); + } + } diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java index 1ea3875..92da973 100644 --- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java +++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallbackTests.java @@ -23,6 +23,10 @@ import org.springaicommunity.mcp.annotation.McpMeta; import org.springaicommunity.mcp.annotation.McpProgressToken; import org.springaicommunity.mcp.annotation.McpResource; +import org.springaicommunity.mcp.context.McpAsyncRequestContext; +import org.springaicommunity.mcp.context.McpSyncRequestContext; + +import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -225,6 +229,33 @@ public ReadResourceResult getResourceWithTransportContext(McpTransportContext co "Content with transport context for " + request.uri()))); } + public ReadResourceResult getResourceWithSyncRequestContext(McpSyncRequestContext context) { + ReadResourceRequest request = (ReadResourceRequest) context.request(); + return new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain", + "Content with sync context for " + request.uri()))); + } + + @McpResource(uri = "users/{userId}/posts/{postId}") + public ReadResourceResult getResourceWithSyncRequestContextAndUriVariables(McpSyncRequestContext context, + String userId, String postId) { + ReadResourceRequest request = (ReadResourceRequest) context.request(); + return new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain", + "User: " + userId + ", Post: " + postId + " with sync context"))); + } + + public ReadResourceResult duplicateSyncRequestContextParameters(McpSyncRequestContext context1, + McpSyncRequestContext context2) { + return new ReadResourceResult(List.of()); + } + + public ReadResourceResult invalidAsyncRequestContextInSyncMethod(McpAsyncRequestContext context) { + return new ReadResourceResult(List.of()); + } + + public Mono invalidSyncRequestContextInAsyncMethod(McpSyncRequestContext context) { + return Mono.just(new ReadResourceResult(List.of())); + } + } // Helper method to create a mock McpResource annotation @@ -1205,4 +1236,84 @@ public void testCallbackWithTransportContext() throws Exception { .hasMessageContaining("McpTransportContext"); } + @Test + public void testCallbackWithSyncRequestContext() throws Exception { + TestResourceProvider provider = new TestResourceProvider(); + Method method = TestResourceProvider.class.getMethod("getResourceWithSyncRequestContext", + McpSyncRequestContext.class); + + BiFunction callback = SyncMcpResourceMethodCallback + .builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(createMockMcpResource())) + .build(); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + ReadResourceRequest request = new ReadResourceRequest("test/resource"); + + ReadResourceResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.contents()).hasSize(1); + assertThat(result.contents().get(0)).isInstanceOf(TextResourceContents.class); + TextResourceContents textContent = (TextResourceContents) result.contents().get(0); + assertThat(textContent.text()).isEqualTo("Content with sync context for test/resource"); + } + + @Test + public void testCallbackWithSyncRequestContextAndUriVariables() throws Exception { + TestResourceProvider provider = new TestResourceProvider(); + Method method = TestResourceProvider.class.getMethod("getResourceWithSyncRequestContextAndUriVariables", + McpSyncRequestContext.class, String.class, String.class); + McpResource resourceAnnotation = method.getAnnotation(McpResource.class); + + BiFunction callback = SyncMcpResourceMethodCallback + .builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(resourceAnnotation)) + .build(); + + McpSyncServerExchange exchange = mock(McpSyncServerExchange.class); + ReadResourceRequest request = new ReadResourceRequest("users/123/posts/456"); + + ReadResourceResult result = callback.apply(exchange, request); + + assertThat(result).isNotNull(); + assertThat(result.contents()).hasSize(1); + assertThat(result.contents().get(0)).isInstanceOf(TextResourceContents.class); + TextResourceContents textContent = (TextResourceContents) result.contents().get(0); + assertThat(textContent.text()).isEqualTo("User: 123, Post: 456 with sync context"); + } + + @Test + public void testDuplicateSyncRequestContextParameters() throws Exception { + TestResourceProvider provider = new TestResourceProvider(); + Method method = TestResourceProvider.class.getMethod("duplicateSyncRequestContextParameters", + McpSyncRequestContext.class, McpSyncRequestContext.class); + + assertThatThrownBy(() -> SyncMcpResourceMethodCallback.builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(createMockMcpResource())) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Method cannot have more than one request context parameter"); + } + + @Test + public void testInvalidAsyncRequestContextInSyncMethod() throws Exception { + TestResourceProvider provider = new TestResourceProvider(); + Method method = TestResourceProvider.class.getMethod("invalidAsyncRequestContextInSyncMethod", + McpAsyncRequestContext.class); + + assertThatThrownBy(() -> SyncMcpResourceMethodCallback.builder() + .method(method) + .bean(provider) + .resource(ResourceAdapter.asResource(createMockMcpResource())) + .build()).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining( + "Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter"); + } + }