> {
+
+ private AsyncStatelessMcpCompleteMethodCallback(Builder builder) {
+ super(builder.method, builder.bean, builder.prompt, builder.uri, builder.uriTemplateManagerFactory);
+ this.validateMethod(this.method);
+ }
+
+ /**
+ * Apply the callback to the given context and request.
+ *
+ * This method builds the arguments for the method call, invokes the method, and
+ * converts the result to a CompleteResult.
+ * @param context The transport context, may be null if the method doesn't require it
+ * @param request The complete request, must not be null
+ * @return A Mono that emits the complete result
+ * @throws McpCompleteMethodException if there is an error invoking the complete
+ * method
+ * @throws IllegalArgumentException if the request is null
+ */
+ @Override
+ public Mono apply(McpTransportContext context, CompleteRequest request) {
+ if (request == null) {
+ return Mono.error(new IllegalArgumentException("Request must not be null"));
+ }
+
+ return Mono.defer(() -> {
+ try {
+ // Build arguments for the method call
+ Object[] args = this.buildArgs(this.method, context, request);
+
+ // Invoke the method
+ this.method.setAccessible(true);
+ Object result = this.method.invoke(this.bean, args);
+
+ // Handle the result based on its type
+ if (result instanceof Mono>) {
+ // If the result is already a Mono, map it to a CompleteResult
+ return ((Mono>) result).map(r -> convertToCompleteResult(r));
+ }
+ else {
+ // Otherwise, convert the result to a CompleteResult and wrap in a
+ // Mono
+ return Mono.just(convertToCompleteResult(result));
+ }
+ }
+ catch (Exception e) {
+ return Mono.error(
+ new McpCompleteMethodException("Error invoking complete method: " + this.method.getName(), e));
+ }
+ });
+ }
+
+ /**
+ * Converts a result object to a CompleteResult.
+ * @param result The result object
+ * @return The CompleteResult
+ */
+ private CompleteResult convertToCompleteResult(Object result) {
+ if (result == null) {
+ return new CompleteResult(new CompleteCompletion(List.of(), 0, false));
+ }
+
+ if (result instanceof CompleteResult) {
+ return (CompleteResult) result;
+ }
+
+ if (result instanceof CompleteCompletion) {
+ return new CompleteResult((CompleteCompletion) result);
+ }
+
+ if (result instanceof List) {
+ List> list = (List>) result;
+ List values = new ArrayList<>();
+
+ for (Object item : list) {
+ if (item instanceof String) {
+ values.add((String) item);
+ }
+ else {
+ throw new IllegalArgumentException("List items must be of type String");
+ }
+ }
+
+ return new CompleteResult(new CompleteCompletion(values, values.size(), false));
+ }
+
+ if (result instanceof String) {
+ return new CompleteResult(new CompleteCompletion(List.of((String) result), 1, false));
+ }
+
+ throw new IllegalArgumentException("Unsupported return type: " + result.getClass().getName());
+ }
+
+ /**
+ * Builder for creating AsyncStatelessMcpCompleteMethodCallback instances.
+ *
+ * This builder provides a fluent API for constructing
+ * AsyncStatelessMcpCompleteMethodCallback instances with the required parameters.
+ */
+ public static class Builder extends AbstractBuilder {
+
+ /**
+ * Constructor for Builder.
+ */
+ public Builder() {
+ this.uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
+ }
+
+ /**
+ * Build the callback.
+ * @return A new AsyncStatelessMcpCompleteMethodCallback instance
+ */
+ @Override
+ public AsyncStatelessMcpCompleteMethodCallback build() {
+ validate();
+ return new AsyncStatelessMcpCompleteMethodCallback(this);
+ }
+
+ }
+
+ /**
+ * Create a new builder.
+ * @return A new builder instance
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Validates that the method return type is compatible with the complete callback.
+ * @param method The method to validate
+ * @throws IllegalArgumentException if the return type is not compatible
+ */
+ @Override
+ protected void validateReturnType(Method method) {
+ Class> returnType = method.getReturnType();
+
+ boolean validReturnType = CompleteResult.class.isAssignableFrom(returnType)
+ || CompleteCompletion.class.isAssignableFrom(returnType) || List.class.isAssignableFrom(returnType)
+ || String.class.isAssignableFrom(returnType) || Mono.class.isAssignableFrom(returnType);
+
+ if (!validReturnType) {
+ throw new IllegalArgumentException(
+ "Method must return either CompleteResult, CompleteCompletion, List, "
+ + "String, or Mono: " + method.getName() + " in " + method.getDeclaringClass().getName()
+ + " returns " + returnType.getName());
+ }
+ }
+
+ /**
+ * Checks if a parameter type is compatible with the exchange type.
+ * @param paramType The parameter type to check
+ * @return true if the parameter type is compatible with the exchange type, false
+ * otherwise
+ */
+ @Override
+ protected boolean isExchangeType(Class> paramType) {
+ return McpTransportContext.class.isAssignableFrom(paramType);
+ }
+
+}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/complete/SyncStatelessMcpCompleteMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/complete/SyncStatelessMcpCompleteMethodCallback.java
new file mode 100644
index 0000000..4669c9f
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/complete/SyncStatelessMcpCompleteMethodCallback.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+
+package org.springaicommunity.mcp.method.complete;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.springaicommunity.mcp.annotation.McpComplete;
+
+import io.modelcontextprotocol.server.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
+import io.modelcontextprotocol.spec.McpSchema.CompleteResult;
+import io.modelcontextprotocol.spec.McpSchema.CompleteResult.CompleteCompletion;
+import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
+
+/**
+ * Class for creating BiFunction callbacks around complete methods for stateless contexts.
+ *
+ * This class provides a way to convert methods annotated with {@link McpComplete} into
+ * callback functions that can be used to handle completion requests in stateless
+ * environments. It supports various method signatures and return types, and handles both
+ * prompt and URI template completions.
+ *
+ * @author Christian Tzolov
+ */
+public final class SyncStatelessMcpCompleteMethodCallback extends AbstractMcpCompleteMethodCallback
+ implements BiFunction {
+
+ private SyncStatelessMcpCompleteMethodCallback(Builder builder) {
+ super(builder.method, builder.bean, builder.prompt, builder.uri, builder.uriTemplateManagerFactory);
+ this.validateMethod(this.method);
+ }
+
+ /**
+ * Apply the callback to the given context and request.
+ *
+ * This method builds the arguments for the method call, invokes the method, and
+ * converts the result to a CompleteResult.
+ * @param context The transport context, may be null if the method doesn't require it
+ * @param request The complete request, must not be null
+ * @return The complete result
+ * @throws McpCompleteMethodException if there is an error invoking the complete
+ * method
+ * @throws IllegalArgumentException if the request is null
+ */
+ @Override
+ public CompleteResult apply(McpTransportContext context, CompleteRequest request) {
+ if (request == null) {
+ throw new IllegalArgumentException("Request must not be null");
+ }
+
+ try {
+ // Build arguments for the method call
+ Object[] args = this.buildArgs(this.method, context, request);
+
+ // Invoke the method
+ this.method.setAccessible(true);
+ Object result = this.method.invoke(this.bean, args);
+
+ // Convert the result to a CompleteResult
+ return convertToCompleteResult(result);
+ }
+ catch (Exception e) {
+ throw new McpCompleteMethodException("Error invoking complete method: " + this.method.getName(), e);
+ }
+ }
+
+ /**
+ * Converts the method result to a CompleteResult.
+ * @param result The method result
+ * @return The CompleteResult
+ */
+ private CompleteResult convertToCompleteResult(Object result) {
+ if (result == null) {
+ return new CompleteResult(new CompleteCompletion(List.of(), 0, false));
+ }
+
+ if (result instanceof CompleteResult) {
+ return (CompleteResult) result;
+ }
+
+ if (result instanceof CompleteCompletion) {
+ return new CompleteResult((CompleteCompletion) result);
+ }
+
+ if (result instanceof List) {
+ List> list = (List>) result;
+ List values = new ArrayList<>();
+
+ for (Object item : list) {
+ if (item instanceof String) {
+ values.add((String) item);
+ }
+ else {
+ throw new IllegalArgumentException("List items must be of type String");
+ }
+ }
+
+ return new CompleteResult(new CompleteCompletion(values, values.size(), false));
+ }
+
+ if (result instanceof String) {
+ return new CompleteResult(new CompleteCompletion(List.of((String) result), 1, false));
+ }
+
+ throw new IllegalArgumentException("Unsupported return type: " + result.getClass().getName());
+ }
+
+ /**
+ * Builder for creating SyncStatelessMcpCompleteMethodCallback instances.
+ *
+ * This builder provides a fluent API for constructing
+ * SyncStatelessMcpCompleteMethodCallback instances with the required parameters.
+ */
+ public static class Builder extends AbstractBuilder {
+
+ /**
+ * Constructor for Builder.
+ */
+ public Builder() {
+ this.uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
+ }
+
+ /**
+ * Build the callback.
+ * @return A new SyncStatelessMcpCompleteMethodCallback instance
+ */
+ @Override
+ public SyncStatelessMcpCompleteMethodCallback build() {
+ validate();
+ return new SyncStatelessMcpCompleteMethodCallback(this);
+ }
+
+ }
+
+ /**
+ * Create a new builder.
+ * @return A new builder instance
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Validates that the method return type is compatible with the complete callback.
+ * @param method The method to validate
+ * @throws IllegalArgumentException if the return type is not compatible
+ */
+ @Override
+ protected void validateReturnType(Method method) {
+ Class> returnType = method.getReturnType();
+
+ boolean validReturnType = CompleteResult.class.isAssignableFrom(returnType)
+ || CompleteCompletion.class.isAssignableFrom(returnType) || List.class.isAssignableFrom(returnType)
+ || String.class.isAssignableFrom(returnType);
+
+ if (!validReturnType) {
+ throw new IllegalArgumentException(
+ "Method must return either CompleteResult, CompleteCompletion, List, " + "or String: "
+ + method.getName() + " in " + method.getDeclaringClass().getName() + " returns "
+ + returnType.getName());
+ }
+ }
+
+ /**
+ * Checks if a parameter type is compatible with the exchange type.
+ * @param paramType The parameter type to check
+ * @return true if the parameter type is compatible with the exchange type, false
+ * otherwise
+ */
+ @Override
+ protected boolean isExchangeType(Class> paramType) {
+ return McpTransportContext.class.isAssignableFrom(paramType);
+ }
+
+}
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 ad3bdee..7f9625c 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
@@ -73,7 +73,7 @@ protected void validateMethod(Method method) {
* @return true if the parameter type is compatible with the exchange type, false
* otherwise
*/
- protected abstract boolean isExchangeType(Class> paramType);
+ protected abstract boolean isExchangeOrContextType(Class> paramType);
/**
* Validates method parameters.
@@ -91,7 +91,7 @@ protected void validateParameters(Method method) {
for (java.lang.reflect.Parameter param : parameters) {
Class> paramType = param.getType();
- if (isExchangeType(paramType)) {
+ if (isExchangeOrContextType(paramType)) {
if (hasExchangeParam) {
throw new IllegalArgumentException("Method cannot have more than one exchange parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
@@ -134,7 +134,7 @@ protected Object[] buildArgs(Method method, Object exchange, GetPromptRequest re
java.lang.reflect.Parameter param = parameters[i];
Class> paramType = param.getType();
- if (isExchangeType(paramType)) {
+ if (isExchangeOrContextType(paramType)) {
args[i] = exchange;
}
else if (GetPromptRequest.class.isAssignableFrom(paramType)) {
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AsyncMcpPromptMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AsyncMcpPromptMethodCallback.java
index df92962..198c981 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AsyncMcpPromptMethodCallback.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AsyncMcpPromptMethodCallback.java
@@ -76,7 +76,7 @@ public Mono apply(McpAsyncServerExchange exchange, GetPromptReq
}
@Override
- protected boolean isExchangeType(Class> paramType) {
+ protected boolean isExchangeOrContextType(Class> paramType) {
return McpAsyncServerExchange.class.isAssignableFrom(paramType);
}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AsyncStatelessMcpPromptMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AsyncStatelessMcpPromptMethodCallback.java
new file mode 100644
index 0000000..7f41c9b
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/AsyncStatelessMcpPromptMethodCallback.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+
+package org.springaicommunity.mcp.method.prompt;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.springaicommunity.mcp.annotation.McpPrompt;
+
+import io.modelcontextprotocol.server.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
+import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+import reactor.core.publisher.Mono;
+
+/**
+ * Class for creating BiFunction callbacks around prompt methods with asynchronous
+ * processing for stateless contexts.
+ *
+ * This class provides a way to convert methods annotated with {@link McpPrompt} into
+ * callback functions that can be used to handle prompt requests asynchronously in
+ * stateless environments. It supports various method signatures and return types.
+ *
+ * @author Christian Tzolov
+ */
+public final class AsyncStatelessMcpPromptMethodCallback extends AbstractMcpPromptMethodCallback
+ implements BiFunction> {
+
+ private AsyncStatelessMcpPromptMethodCallback(Builder builder) {
+ super(builder.method, builder.bean, builder.prompt);
+ }
+
+ /**
+ * Apply the callback to the given context and request.
+ *
+ * This method builds the arguments for the method call, invokes the method, and
+ * converts the result to a GetPromptResult.
+ * @param context The transport context, may be null if the method doesn't require it
+ * @param request The prompt request, must not be null
+ * @return A Mono that emits the prompt result
+ * @throws McpPromptMethodException if there is an error invoking the prompt method
+ * @throws IllegalArgumentException if the request is null
+ */
+ @Override
+ public Mono apply(McpTransportContext context, GetPromptRequest request) {
+ if (request == null) {
+ return Mono.error(new IllegalArgumentException("Request must not be null"));
+ }
+
+ return Mono.defer(() -> {
+ try {
+ // Build arguments for the method call
+ Object[] args = this.buildArgs(this.method, context, request);
+
+ // Invoke the method
+ this.method.setAccessible(true);
+ Object result = this.method.invoke(this.bean, args);
+
+ // Handle the result based on its type
+ if (result instanceof Mono>) {
+ // If the result is already a Mono, map it to a GetPromptResult
+ return ((Mono>) result).map(r -> convertToGetPromptResult(r));
+ }
+ else {
+ // Otherwise, convert the result to a GetPromptResult and wrap in a
+ // Mono
+ return Mono.just(convertToGetPromptResult(result));
+ }
+ }
+ catch (Exception e) {
+ return Mono
+ .error(new McpPromptMethodException("Error invoking prompt method: " + this.method.getName(), e));
+ }
+ });
+ }
+
+ @Override
+ protected boolean isExchangeOrContextType(Class> paramType) {
+ return McpTransportContext.class.isAssignableFrom(paramType);
+ }
+
+ @Override
+ protected void validateReturnType(Method method) {
+ Class> returnType = method.getReturnType();
+
+ boolean validReturnType = GetPromptResult.class.isAssignableFrom(returnType)
+ || List.class.isAssignableFrom(returnType) || PromptMessage.class.isAssignableFrom(returnType)
+ || String.class.isAssignableFrom(returnType) || Mono.class.isAssignableFrom(returnType);
+
+ if (!validReturnType) {
+ throw new IllegalArgumentException("Method must return either GetPromptResult, List, "
+ + "List, PromptMessage, String, or Mono: " + method.getName() + " in "
+ + method.getDeclaringClass().getName() + " returns " + returnType.getName());
+ }
+ }
+
+ /**
+ * Builder for creating AsyncStatelessMcpPromptMethodCallback instances.
+ *
+ * This builder provides a fluent API for constructing
+ * AsyncStatelessMcpPromptMethodCallback instances with the required parameters.
+ */
+ public static class Builder extends AbstractBuilder {
+
+ /**
+ * Build the callback.
+ * @return A new AsyncStatelessMcpPromptMethodCallback instance
+ */
+ @Override
+ public AsyncStatelessMcpPromptMethodCallback build() {
+ validate();
+ return new AsyncStatelessMcpPromptMethodCallback(this);
+ }
+
+ }
+
+ /**
+ * Create a new builder.
+ * @return A new builder instance
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/SyncMcpPromptMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/SyncMcpPromptMethodCallback.java
index 138bddc..4f4c6a4 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/SyncMcpPromptMethodCallback.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/SyncMcpPromptMethodCallback.java
@@ -67,7 +67,7 @@ public GetPromptResult apply(McpSyncServerExchange exchange, GetPromptRequest re
}
@Override
- protected boolean isExchangeType(Class> paramType) {
+ protected boolean isExchangeOrContextType(Class> paramType) {
return McpSyncServerExchange.class.isAssignableFrom(paramType);
}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/SyncStatelessMcpPromptMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/SyncStatelessMcpPromptMethodCallback.java
new file mode 100644
index 0000000..2ac2aae
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/prompt/SyncStatelessMcpPromptMethodCallback.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+
+package org.springaicommunity.mcp.method.prompt;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.springaicommunity.mcp.annotation.McpPrompt;
+
+import io.modelcontextprotocol.server.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
+import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
+import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
+
+/**
+ * Class for creating BiFunction callbacks around prompt methods for stateless contexts.
+ *
+ * This class provides a way to convert methods annotated with {@link McpPrompt} into
+ * callback functions that can be used to handle prompt requests in stateless
+ * environments. It supports various method signatures and return types.
+ *
+ * @author Christian Tzolov
+ */
+public final class SyncStatelessMcpPromptMethodCallback extends AbstractMcpPromptMethodCallback
+ implements BiFunction {
+
+ private SyncStatelessMcpPromptMethodCallback(Builder builder) {
+ super(builder.method, builder.bean, builder.prompt);
+ }
+
+ /**
+ * Apply the callback to the given context and request.
+ *
+ * This method builds the arguments for the method call, invokes the method, and
+ * converts the result to a GetPromptResult.
+ * @param context The transport context, may be null if the method doesn't require it
+ * @param request The prompt request, must not be null
+ * @return The prompt result
+ * @throws McpPromptMethodException if there is an error invoking the prompt method
+ * @throws IllegalArgumentException if the request is null
+ */
+ @Override
+ public GetPromptResult apply(McpTransportContext context, GetPromptRequest request) {
+ if (request == null) {
+ throw new IllegalArgumentException("Request must not be null");
+ }
+
+ try {
+ // Build arguments for the method call
+ Object[] args = this.buildArgs(this.method, context, request);
+
+ // Invoke the method
+ this.method.setAccessible(true);
+ Object result = this.method.invoke(this.bean, args);
+
+ // Convert the result to a GetPromptResult
+ GetPromptResult promptResult = this.convertToGetPromptResult(result);
+
+ return promptResult;
+ }
+ catch (Exception e) {
+ throw new McpPromptMethodException("Error invoking prompt method: " + this.method.getName(), e);
+ }
+ }
+
+ @Override
+ protected boolean isExchangeOrContextType(Class> paramType) {
+ return McpTransportContext.class.isAssignableFrom(paramType);
+ }
+
+ @Override
+ protected void validateReturnType(Method method) {
+ Class> returnType = method.getReturnType();
+
+ boolean validReturnType = GetPromptResult.class.isAssignableFrom(returnType)
+ || List.class.isAssignableFrom(returnType) || PromptMessage.class.isAssignableFrom(returnType)
+ || String.class.isAssignableFrom(returnType);
+
+ if (!validReturnType) {
+ throw new IllegalArgumentException("Method must return either GetPromptResult, List, "
+ + "List, PromptMessage, or String: " + method.getName() + " in "
+ + method.getDeclaringClass().getName() + " returns " + returnType.getName());
+ }
+ }
+
+ /**
+ * Builder for creating SyncStatelessMcpPromptMethodCallback instances.
+ *
+ * This builder provides a fluent API for constructing
+ * SyncStatelessMcpPromptMethodCallback instances with the required parameters.
+ */
+ public static class Builder extends AbstractBuilder {
+
+ /**
+ * Build the callback.
+ * @return A new SyncStatelessMcpPromptMethodCallback instance
+ */
+ @Override
+ public SyncStatelessMcpPromptMethodCallback build() {
+ validate();
+ return new SyncStatelessMcpPromptMethodCallback(this);
+ }
+
+ }
+
+ /**
+ * Create a new builder.
+ * @return A new builder instance
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+}
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 1e16c8d..7d3ec89 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
@@ -158,7 +158,7 @@ protected void validateParametersWithoutUriVariables(Method method) {
for (Parameter param : parameters) {
Class> paramType = param.getType();
- if (isExchangeType(paramType)) {
+ if (isExchangeOrContextType(paramType)) {
if (hasExchangeParam) {
throw new IllegalArgumentException("Method cannot have more than one exchange parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
@@ -205,7 +205,7 @@ protected void validateParametersWithUriVariables(Method method) {
for (Parameter param : parameters) {
Class> paramType = param.getType();
- if (isExchangeType(paramType)) {
+ if (isExchangeOrContextType(paramType)) {
exchangeParamCount++;
}
else if (ReadResourceRequest.class.isAssignableFrom(paramType)) {
@@ -240,7 +240,7 @@ else if (ReadResourceRequest.class.isAssignableFrom(paramType)) {
// Check that all non-special parameters are String type (for URI variables)
for (Parameter param : parameters) {
Class> paramType = param.getType();
- if (!isExchangeType(paramType) && !ReadResourceRequest.class.isAssignableFrom(paramType)
+ if (!isExchangeOrContextType(paramType) && !ReadResourceRequest.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()
@@ -293,7 +293,7 @@ protected void buildArgsWithUriVariables(Parameter[] parameters, Object[] args,
// First pass: assign special parameters (exchange and request)
for (int i = 0; i < parameters.length; i++) {
Class> paramType = parameters[i].getType();
- if (isExchangeType(paramType)) {
+ if (isExchangeOrContextType(paramType)) {
args[i] = exchange;
}
else if (ReadResourceRequest.class.isAssignableFrom(paramType)) {
@@ -339,7 +339,7 @@ protected void buildArgsWithoutUriVariables(Parameter[] parameters, Object[] arg
Parameter param = parameters[i];
Class> paramType = param.getType();
- if (isExchangeType(paramType)) {
+ if (isExchangeOrContextType(paramType)) {
args[i] = exchange;
}
else if (ReadResourceRequest.class.isAssignableFrom(paramType)) {
@@ -361,7 +361,7 @@ else if (String.class.isAssignableFrom(paramType)) {
* @return true if the parameter type is compatible with the exchange type, false
* otherwise
*/
- protected abstract boolean isExchangeType(Class> paramType);
+ protected abstract boolean isExchangeOrContextType(Class> paramType);
/**
* Returns the content type of the resource.
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallback.java
index 5790e9e..aa4ae06 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallback.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AsyncMcpResourceMethodCallback.java
@@ -159,7 +159,7 @@ protected void validateReturnType(Method method) {
* otherwise
*/
@Override
- protected boolean isExchangeType(Class> paramType) {
+ protected boolean isExchangeOrContextType(Class> paramType) {
return McpAsyncServerExchange.class.isAssignableFrom(paramType);
}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallback.java
new file mode 100644
index 0000000..eff3594
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallback.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+package org.springaicommunity.mcp.method.resource;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import org.springaicommunity.mcp.annotation.McpResource;
+
+import io.modelcontextprotocol.server.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
+import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
+import reactor.core.publisher.Mono;
+
+/**
+ * Class for creating BiFunction callbacks around resource methods with asynchronous
+ * processing for stateless contexts.
+ *
+ * This class provides a way to convert methods annotated with {@link McpResource} into
+ * callback functions that can be used to handle resource requests asynchronously in
+ * stateless environments. It supports various method signatures and return types, and
+ * handles URI template variables.
+ *
+ * @author Christian Tzolov
+ */
+public final class AsyncStatelessMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback
+ implements BiFunction> {
+
+ private AsyncStatelessMcpResourceMethodCallback(Builder builder) {
+ super(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,
+ builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType);
+ this.validateMethod(this.method);
+ }
+
+ /**
+ * Apply the callback to the given context and request.
+ *
+ * This method extracts URI variable values from the request URI, builds the arguments
+ * for the method call, invokes the method, and converts the result to a
+ * ReadResourceResult.
+ * @param context The transport context, may be null if the method doesn't require it
+ * @param request The resource request, must not be null
+ * @return A Mono that emits the resource result
+ * @throws McpResourceMethodException if there is an error invoking the resource
+ * method
+ * @throws IllegalArgumentException if the request is null or if URI variable
+ * extraction fails
+ */
+ @Override
+ public Mono apply(McpTransportContext context, ReadResourceRequest request) {
+ if (request == null) {
+ return Mono.error(new IllegalArgumentException("Request must not be null"));
+ }
+
+ return Mono.defer(() -> {
+ try {
+ // Extract URI variable values from the request URI
+ Map uriVariableValues = this.uriTemplateManager.extractVariableValues(request.uri());
+
+ // Verify all URI variables were extracted if URI variables are expected
+ if (!this.uriVariables.isEmpty() && uriVariableValues.size() != this.uriVariables.size()) {
+ return Mono
+ .error(new IllegalArgumentException("Failed to extract all URI variables from request URI: "
+ + request.uri() + ". Expected variables: " + this.uriVariables + ", but found: "
+ + uriVariableValues.keySet()));
+ }
+
+ // Build arguments for the method call
+ Object[] args = this.buildArgs(this.method, context, request, uriVariableValues);
+
+ // Invoke the method
+ this.method.setAccessible(true);
+ Object result = this.method.invoke(this.bean, args);
+
+ // Handle the result based on its type
+ if (result instanceof Mono>) {
+ // If the result is already a Mono, use it
+ return ((Mono>) result).map(r -> this.resultConverter.convertToReadResourceResult(r,
+ request.uri(), this.mimeType, this.contentType));
+ }
+ else {
+ // Otherwise, convert the result to a ReadResourceResult and wrap in a
+ // Mono
+ return Mono.just(this.resultConverter.convertToReadResourceResult(result, request.uri(),
+ this.mimeType, this.contentType));
+ }
+ }
+ catch (Exception e) {
+ return Mono.error(
+ new McpResourceMethodException("Error invoking resource method: " + this.method.getName(), e));
+ }
+ });
+ }
+
+ /**
+ * Builder for creating AsyncStatelessMcpResourceMethodCallback instances.
+ *
+ * This builder provides a fluent API for constructing
+ * AsyncStatelessMcpResourceMethodCallback instances with the required parameters.
+ */
+ public static class Builder extends AbstractBuilder {
+
+ /**
+ * Constructor for Builder.
+ */
+ public Builder() {
+ this.resultConverter = new DefaultMcpReadResourceResultConverter();
+ }
+
+ /**
+ * Build the callback.
+ * @return A new AsyncStatelessMcpResourceMethodCallback instance
+ */
+ @Override
+ public AsyncStatelessMcpResourceMethodCallback build() {
+ validate();
+ return new AsyncStatelessMcpResourceMethodCallback(this);
+ }
+
+ }
+
+ /**
+ * Create a new builder.
+ * @return A new builder instance
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Validates that the method return type is compatible with the resource callback.
+ * @param method The method to validate
+ * @throws IllegalArgumentException if the return type is not compatible
+ */
+ @Override
+ protected void validateReturnType(Method method) {
+ Class> returnType = method.getReturnType();
+
+ boolean validReturnType = ReadResourceResult.class.isAssignableFrom(returnType)
+ || List.class.isAssignableFrom(returnType) || ResourceContents.class.isAssignableFrom(returnType)
+ || String.class.isAssignableFrom(returnType) || Mono.class.isAssignableFrom(returnType);
+
+ if (!validReturnType) {
+ throw new IllegalArgumentException(
+ "Method must return either ReadResourceResult, List, List, "
+ + "ResourceContents, String, or Mono: " + method.getName() + " in "
+ + method.getDeclaringClass().getName() + " returns " + returnType.getName());
+ }
+ }
+
+ /**
+ * Checks if a parameter type is compatible with the exchange type.
+ * @param paramType The parameter type to check
+ * @return true if the parameter type is compatible with the exchange type, false
+ * otherwise
+ */
+ @Override
+ protected boolean isExchangeOrContextType(Class> paramType) {
+ return McpTransportContext.class.isAssignableFrom(paramType);
+ }
+
+}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallback.java
index 37ab4bd..f0b2d25 100644
--- a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallback.java
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/SyncMcpResourceMethodCallback.java
@@ -129,7 +129,7 @@ protected void validateReturnType(Method method) {
}
@Override
- protected boolean isExchangeType(Class> paramType) {
+ protected boolean isExchangeOrContextType(Class> paramType) {
return McpSyncServerExchange.class.isAssignableFrom(paramType);
}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallback.java
new file mode 100644
index 0000000..46c7ea6
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallback.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+
+package org.springaicommunity.mcp.method.resource;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import org.springaicommunity.mcp.annotation.McpResource;
+
+import io.modelcontextprotocol.server.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
+import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
+import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
+
+/**
+ * Class for creating BiFunction callbacks around resource methods for stateless contexts.
+ *
+ * This class provides a way to convert methods annotated with {@link McpResource} into
+ * callback functions that can be used to handle resource requests in stateless
+ * environments. It supports various method signatures and return types, and handles URI
+ * template variables.
+ *
+ * @author Christian Tzolov
+ */
+public final class SyncStatelessMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback
+ implements BiFunction {
+
+ private SyncStatelessMcpResourceMethodCallback(Builder builder) {
+ super(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,
+ builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType);
+ this.validateMethod(this.method);
+ }
+
+ /**
+ * Apply the callback to the given context and request.
+ *
+ * This method extracts URI variable values from the request URI, builds the arguments
+ * for the method call, invokes the method, and converts the result to a
+ * ReadResourceResult.
+ * @param context The transport context, may be null if the method doesn't require it
+ * @param request The resource request, must not be null
+ * @return The resource result
+ * @throws McpResourceMethodException if there is an error invoking the resource
+ * method
+ * @throws IllegalArgumentException if the request is null or if URI variable
+ * extraction fails
+ */
+ @Override
+ public ReadResourceResult apply(McpTransportContext context, ReadResourceRequest request) {
+ if (request == null) {
+ throw new IllegalArgumentException("Request must not be null");
+ }
+
+ try {
+ // Extract URI variable values from the request URI
+ Map uriVariableValues = this.uriTemplateManager.extractVariableValues(request.uri());
+
+ // Verify all URI variables were extracted if URI variables are expected
+ if (!this.uriVariables.isEmpty() && uriVariableValues.size() != this.uriVariables.size()) {
+ throw new IllegalArgumentException("Failed to extract all URI variables from request URI: "
+ + request.uri() + ". Expected variables: " + this.uriVariables + ", but found: "
+ + uriVariableValues.keySet());
+ }
+
+ // Build arguments for the method call
+ Object[] args = this.buildArgs(this.method, context, request, uriVariableValues);
+
+ // Invoke the method
+ this.method.setAccessible(true);
+ Object result = this.method.invoke(this.bean, args);
+
+ // Convert the result to a ReadResourceResult using the converter
+ return this.resultConverter.convertToReadResourceResult(result, request.uri(), this.mimeType,
+ this.contentType);
+ }
+ catch (Exception e) {
+ throw new McpResourceMethodException("Access error invoking resource method: " + this.method.getName(), e);
+ }
+ }
+
+ /**
+ * Builder for creating SyncStatelessMcpResourceMethodCallback instances.
+ *
+ * This builder provides a fluent API for constructing
+ * SyncStatelessMcpResourceMethodCallback instances with the required parameters.
+ */
+ public static class Builder extends AbstractBuilder {
+
+ /**
+ * Constructor for Builder.
+ */
+ private Builder() {
+ this.resultConverter = new DefaultMcpReadResourceResultConverter();
+ }
+
+ @Override
+ public SyncStatelessMcpResourceMethodCallback build() {
+ validate();
+ return new SyncStatelessMcpResourceMethodCallback(this);
+ }
+
+ }
+
+ /**
+ * Create a new builder.
+ * @return A new builder instance
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ @Override
+ protected void validateReturnType(Method method) {
+ Class> returnType = method.getReturnType();
+
+ boolean validReturnType = ReadResourceResult.class.isAssignableFrom(returnType)
+ || List.class.isAssignableFrom(returnType) || ResourceContents.class.isAssignableFrom(returnType)
+ || String.class.isAssignableFrom(returnType);
+
+ if (!validReturnType) {
+ throw new IllegalArgumentException(
+ "Method must return either ReadResourceResult, List, List, "
+ + "ResourceContents, or String: " + method.getName() + " in "
+ + method.getDeclaringClass().getName() + " returns " + returnType.getName());
+ }
+ }
+
+ @Override
+ protected boolean isExchangeOrContextType(Class> paramType) {
+ return McpTransportContext.class.isAssignableFrom(paramType);
+ }
+
+}
diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractAsyncMcpToolMethodCallback.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractAsyncMcpToolMethodCallback.java
new file mode 100644
index 0000000..6e5b100
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/method/tool/AbstractAsyncMcpToolMethodCallback.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springaicommunity.mcp.method.tool;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.reactivestreams.Publisher;
+import org.springaicommunity.mcp.annotation.McpTool;
+import org.springaicommunity.mcp.method.tool.utils.JsonParser;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * Abstract base class for creating Function callbacks around async tool methods.
+ *
+ * This class provides common functionality for converting methods annotated with
+ * {@link McpTool} into callback functions that can be used to handle tool requests
+ * asynchronously.
+ *
+ * @param The type of the context parameter (e.g., McpAsyncServerExchange or
+ * McpTransportContext)
+ * @author Christian Tzolov
+ */
+public abstract class AbstractAsyncMcpToolMethodCallback {
+
+ private static final TypeReference