diff --git a/mcp-annotations/src/main/java/org/springaicommunity/mcp/ErrorUtils.java b/mcp-annotations/src/main/java/org/springaicommunity/mcp/ErrorUtils.java
new file mode 100644
index 0000000..fca6806
--- /dev/null
+++ b/mcp-annotations/src/main/java/org/springaicommunity/mcp/ErrorUtils.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2025-2025 the original author or authors.
+ */
+
+package org.springaicommunity.mcp;
+
+import java.util.Objects;
+
+public class ErrorUtils {
+
+ public static Throwable findCauseUsingPlainJava(Throwable throwable) {
+ Objects.requireNonNull(throwable);
+ Throwable rootCause = throwable;
+ while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
+ rootCause = rootCause.getCause();
+ }
+ return rootCause;
+ }
+
+}
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 ac12e41..a6ab94c 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
@@ -12,7 +12,9 @@
import org.springaicommunity.mcp.annotation.McpArg;
import org.springaicommunity.mcp.annotation.McpMeta;
import org.springaicommunity.mcp.annotation.McpProgressToken;
-
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.Prompt;
@@ -75,7 +77,10 @@ protected void validateMethod(Method method) {
* @return true if the parameter type is compatible with the exchange type, false
* otherwise
*/
- protected abstract boolean isExchangeOrContextType(Class> paramType);
+ protected abstract boolean isSupportedExchangeOrContextType(Class> paramType);
+
+ protected void validateParamType(Class> paramType) {
+ }
/**
* Validates method parameters.
@@ -95,6 +100,8 @@ protected void validateParameters(Method method) {
for (java.lang.reflect.Parameter param : parameters) {
Class> paramType = param.getType();
+ this.validateParamType(paramType);
+
// Skip @McpProgressToken annotated parameters from validation
if (param.isAnnotationPresent(McpProgressToken.class)) {
if (hasProgressTokenParam) {
@@ -115,7 +122,7 @@ protected void validateParameters(Method method) {
continue;
}
- if (isExchangeOrContextType(paramType)) {
+ if (isSupportedExchangeOrContextType(paramType)) {
if (hasExchangeParam) {
throw new IllegalArgumentException("Method cannot have more than one exchange parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
@@ -140,6 +147,8 @@ else if (Map.class.isAssignableFrom(paramType)) {
}
}
+ protected abstract Object assignExchangeType(Class> paramType, Object exchange);
+
/**
* Builds the arguments array for invoking the method.
*
@@ -182,8 +191,11 @@ protected Object[] buildArgs(Method method, Object exchange, GetPromptRequest re
java.lang.reflect.Parameter param = parameters[i];
Class> paramType = param.getType();
- if (isExchangeOrContextType(paramType)) {
- args[i] = exchange;
+ if (McpTransportContext.class.isAssignableFrom(paramType)
+ || McpSyncServerExchange.class.isAssignableFrom(paramType)
+ || McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+
+ args[i] = this.assignExchangeType(paramType, exchange);
}
else if (GetPromptRequest.class.isAssignableFrom(paramType)) {
args[i] = request;
@@ -367,30 +379,4 @@ protected void validate() {
}
- /**
- * Exception thrown when there is an error invoking a prompt method.
- */
- public static class McpPromptMethodException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a new exception with the specified detail message and cause.
- * @param message The detail message
- * @param cause The cause
- */
- public McpPromptMethodException(String message, Throwable cause) {
- super(message, cause);
- }
-
- /**
- * Constructs a new exception with the specified detail message.
- * @param message The detail message
- */
- public McpPromptMethodException(String message) {
- super(message);
- }
-
- }
-
}
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 198c981..9e44c08 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
@@ -7,9 +7,13 @@
import java.lang.reflect.Method;
import java.util.function.BiFunction;
+import org.springaicommunity.mcp.ErrorUtils;
import org.springaicommunity.mcp.annotation.McpPrompt;
-
+import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import reactor.core.publisher.Mono;
@@ -31,6 +35,48 @@ private AsyncMcpPromptMethodCallback(Builder builder) {
super(builder.method, builder.bean, builder.prompt);
}
+ @Override
+ protected void validateParamType(Class> paramType) {
+
+ if (McpSyncServerExchange.class.isAssignableFrom(paramType)) {
+ throw new IllegalArgumentException("Async prompt method must not declare parameter of type: "
+ + paramType.getName() + ". Use McpAsyncServerExchange instead." + " Method: "
+ + this.method.getName() + " in " + this.method.getDeclaringClass().getName());
+ }
+ }
+
+ @Override
+ protected Object assignExchangeType(Class> paramType, Object exchange) {
+
+ if (McpTransportContext.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpTransportContext transportContext) {
+ return transportContext;
+ }
+ else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ throw new IllegalArgumentException("Unsupported Async exchange type: "
+ + syncServerExchange.getClass().getName() + " for Async method: " + method.getName() + " in "
+ + method.getDeclaringClass().getName());
+
+ }
+ else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ return asyncServerExchange.transportContext();
+ }
+ }
+ else if (McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ return asyncServerExchange;
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for Async method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
/**
* Apply the callback to the given exchange and request.
*
@@ -69,15 +115,24 @@ public Mono apply(McpAsyncServerExchange exchange, GetPromptReq
}
}
catch (Exception e) {
- return Mono
- .error(new McpPromptMethodException("Error invoking prompt method: " + this.method.getName(), e));
+ if (e instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
+ return Mono.error(mcpError);
+ }
+
+ return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Error invoking prompt method: " + this.method.getName() + " in "
+ + this.bean.getClass().getName() + ". /nCause: "
+ + ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .build());
}
});
}
@Override
- protected boolean isExchangeOrContextType(Class> paramType) {
- return McpAsyncServerExchange.class.isAssignableFrom(paramType);
+ protected boolean isSupportedExchangeOrContextType(Class> paramType) {
+ return (McpAsyncServerExchange.class.isAssignableFrom(paramType)
+ || McpTransportContext.class.isAssignableFrom(paramType));
}
@Override
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
index 6197f49..760ade9 100644
--- 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
@@ -8,8 +8,13 @@
import java.util.List;
import java.util.function.BiFunction;
+import org.springaicommunity.mcp.ErrorUtils;
import org.springaicommunity.mcp.annotation.McpPrompt;
import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
@@ -32,6 +37,42 @@ private AsyncStatelessMcpPromptMethodCallback(Builder builder) {
super(builder.method, builder.bean, builder.prompt);
}
+ @Override
+ protected void validateParamType(Class> paramType) {
+
+ if (McpSyncServerExchange.class.isAssignableFrom(paramType)
+ || McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+
+ throw new IllegalArgumentException(
+ "Stateless Streamable-Http prompt method must not declare parameter of type: " + paramType.getName()
+ + ". Use McpTransportContext instead." + " Method: " + this.method.getName() + " in "
+ + this.method.getDeclaringClass().getName());
+ }
+ }
+
+ @Override
+ protected Object assignExchangeType(Class> paramType, Object exchange) {
+
+ if (McpTransportContext.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpTransportContext transportContext) {
+ return transportContext;
+ }
+ else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ throw new IllegalArgumentException("Unsupported Sync exchange type: "
+ + syncServerExchange.getClass().getName() + " for Sync method: " + method.getName() + " in "
+ + method.getDeclaringClass().getName());
+
+ }
+ else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ return asyncServerExchange.transportContext();
+ }
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
/**
* Apply the callback to the given context and request.
*
@@ -70,14 +111,23 @@ public Mono apply(McpTransportContext context, GetPromptRequest
}
}
catch (Exception e) {
- return Mono
- .error(new McpPromptMethodException("Error invoking prompt method: " + this.method.getName(), e));
+
+ if (e instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
+ return Mono.error(mcpError);
+ }
+
+ return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Error invoking prompt method: " + this.method.getName() + " in "
+ + this.bean.getClass().getName() + ". /nCause: "
+ + ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .build());
}
});
}
@Override
- protected boolean isExchangeOrContextType(Class> paramType) {
+ protected boolean isSupportedExchangeOrContextType(Class> paramType) {
return McpTransportContext.class.isAssignableFrom(paramType);
}
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 4f4c6a4..8d7a087 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
@@ -8,9 +8,13 @@
import java.util.List;
import java.util.function.BiFunction;
+import org.springaicommunity.mcp.ErrorUtils;
import org.springaicommunity.mcp.annotation.McpPrompt;
-
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
@@ -31,6 +35,47 @@ private SyncMcpPromptMethodCallback(Builder builder) {
super(builder.method, builder.bean, builder.prompt);
}
+ @Override
+ protected void validateParamType(Class> paramType) {
+
+ if (McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+ throw new IllegalArgumentException("Sync prompt method must not declare parameter of type: "
+ + paramType.getName() + ". Use McpSyncServerExchange instead." + " Method: " + this.method.getName()
+ + " in " + this.method.getDeclaringClass().getName());
+ }
+ }
+
+ @Override
+ protected Object assignExchangeType(Class> paramType, Object exchange) {
+
+ if (McpTransportContext.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpTransportContext transportContext) {
+ return transportContext;
+ }
+ else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ return syncServerExchange.transportContext();
+ }
+ else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ throw new IllegalArgumentException("Unsupported Async exchange type: "
+ + asyncServerExchange.getClass().getName() + " for Sync method: " + method.getName() + " in "
+ + method.getDeclaringClass().getName());
+ }
+ }
+ else if (McpSyncServerExchange.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ return syncServerExchange;
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for Sync method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
/**
* Apply the callback to the given exchange and request.
*
@@ -62,13 +107,23 @@ public GetPromptResult apply(McpSyncServerExchange exchange, GetPromptRequest re
return promptResult;
}
catch (Exception e) {
- throw new McpPromptMethodException("Error invoking prompt method: " + this.method.getName(), e);
+ if (e instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
+ throw mcpError;
+ }
+
+ throw McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Error invoking prompt method: " + this.method.getName() + " in "
+ + this.bean.getClass().getName() + "./nCause: "
+ + ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .build();
}
}
@Override
- protected boolean isExchangeOrContextType(Class> paramType) {
- return McpSyncServerExchange.class.isAssignableFrom(paramType);
+ protected boolean isSupportedExchangeOrContextType(Class> paramType) {
+ return (McpSyncServerExchange.class.isAssignableFrom(paramType)
+ || McpTransportContext.class.isAssignableFrom(paramType));
}
@Override
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
index cb829bb..785c9a8 100644
--- 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
@@ -8,8 +8,13 @@
import java.util.List;
import java.util.function.BiFunction;
+import org.springaicommunity.mcp.ErrorUtils;
import org.springaicommunity.mcp.annotation.McpPrompt;
import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
@@ -30,6 +35,41 @@ private SyncStatelessMcpPromptMethodCallback(Builder builder) {
super(builder.method, builder.bean, builder.prompt);
}
+ @Override
+ protected void validateParamType(Class> paramType) {
+
+ if (McpSyncServerExchange.class.isAssignableFrom(paramType)
+ || McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+
+ throw new IllegalArgumentException(
+ "Stateless Streamable-Http prompt method must not declare parameter of type: " + paramType.getName()
+ + ". Use McpTransportContext instead." + " Method: " + this.method.getName() + " in "
+ + this.method.getDeclaringClass().getName());
+ }
+ }
+
+ @Override
+ protected Object assignExchangeType(Class> paramType, Object exchange) {
+
+ if (McpTransportContext.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpTransportContext transportContext) {
+ return transportContext;
+ }
+ else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ return syncServerExchange.transportContext();
+ }
+ else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ throw new IllegalArgumentException("Unsupported Async exchange type: "
+ + asyncServerExchange.getClass().getName() + " for Sync method: " + method.getName() + " in "
+ + method.getDeclaringClass().getName());
+ }
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
/**
* Apply the callback to the given context and request.
*
@@ -61,12 +101,21 @@ public GetPromptResult apply(McpTransportContext context, GetPromptRequest reque
return promptResult;
}
catch (Exception e) {
- throw new McpPromptMethodException("Error invoking prompt method: " + this.method.getName(), e);
+ if (e instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
+ throw mcpError;
+ }
+
+ throw McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Error invoking prompt method: " + this.method.getName() + " in "
+ + this.bean.getClass().getName() + ". /nCause: "
+ + ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .build();
}
}
@Override
- protected boolean isExchangeOrContextType(Class> paramType) {
+ protected boolean isSupportedExchangeOrContextType(Class> paramType) {
return McpTransportContext.class.isAssignableFrom(paramType);
}
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 eb6645e..70113c3 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
@@ -12,7 +12,9 @@
import org.springaicommunity.mcp.annotation.McpMeta;
import org.springaicommunity.mcp.annotation.McpProgressToken;
-
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.util.Assert;
@@ -215,6 +217,9 @@ else if (ReadResourceRequest.class.isAssignableFrom(paramType)
}
}
+ protected void validateParamType(Class> paramType) {
+ }
+
/**
* Validates method parameters when URI variables are present. This method provides
* common validation logic and delegates exchange type checking to subclasses.
@@ -236,6 +241,9 @@ protected void validateParametersWithUriVariables(Method method) {
}
else {
Class> paramType = param.getType();
+
+ this.validateParamType(paramType);
+
if (McpMeta.class.isAssignableFrom(paramType)) {
metaParamCount++;
}
@@ -295,6 +303,8 @@ else if (ReadResourceRequest.class.isAssignableFrom(paramType)) {
}
}
+ protected abstract Object assignExchangeType(Class> paramType, Object exchange);
+
/**
* Builds the arguments array for invoking the method.
*
@@ -313,14 +323,21 @@ protected Object[] buildArgs(Method method, Object exchange, ReadResourceRequest
// First, handle @McpProgressToken and McpMeta parameters
for (int i = 0; i < parameters.length; i++) {
+ Class> paramType = parameters[i].getType();
if (parameters[i].isAnnotationPresent(McpProgressToken.class)) {
// Get progress token from request
args[i] = request != null ? request.progressToken() : null;
}
- else if (McpMeta.class.isAssignableFrom(parameters[i].getType())) {
+ else if (McpMeta.class.isAssignableFrom(paramType)) {
// Inject McpMeta with request metadata
args[i] = request != null ? new McpMeta(request.meta()) : new McpMeta(null);
}
+ else if (McpTransportContext.class.isAssignableFrom(paramType)
+ || McpSyncServerExchange.class.isAssignableFrom(paramType)
+ || McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+
+ args[i] = this.assignExchangeType(paramType, exchange);
+ }
}
if (!this.uriVariables.isEmpty()) {
@@ -354,15 +371,13 @@ protected void buildArgsWithUriVariables(Parameter[] parameters, Object[] args,
// Skip if parameter is annotated with @McpProgressToken or is McpMeta
// (already handled)
if (parameters[i].isAnnotationPresent(McpProgressToken.class)
- || McpMeta.class.isAssignableFrom(parameters[i].getType())) {
+ || McpMeta.class.isAssignableFrom(parameters[i].getType())
+ || isExchangeOrContextType(parameters[i].getType())) {
continue;
}
Class> paramType = parameters[i].getType();
- if (isExchangeOrContextType(paramType)) {
- args[i] = exchange;
- }
- else if (ReadResourceRequest.class.isAssignableFrom(paramType)) {
+ if (ReadResourceRequest.class.isAssignableFrom(paramType)) {
args[i] = request;
}
}
@@ -408,17 +423,15 @@ protected void buildArgsWithoutUriVariables(Parameter[] parameters, Object[] arg
// Skip if parameter is annotated with @McpProgressToken or is McpMeta
// (already handled)
if (parameters[i].isAnnotationPresent(McpProgressToken.class)
- || McpMeta.class.isAssignableFrom(parameters[i].getType())) {
+ || McpMeta.class.isAssignableFrom(parameters[i].getType())
+ || isExchangeOrContextType(parameters[i].getType())) {
continue;
}
Parameter param = parameters[i];
Class> paramType = param.getType();
- if (isExchangeOrContextType(paramType)) {
- args[i] = exchange;
- }
- else if (ReadResourceRequest.class.isAssignableFrom(paramType)) {
+ if (ReadResourceRequest.class.isAssignableFrom(paramType)) {
args[i] = request;
}
else if (String.class.isAssignableFrom(paramType)) {
@@ -447,32 +460,6 @@ public ContentType contentType() {
return this.contentType;
}
- /**
- * Exception thrown when there is an error invoking a resource method.
- */
- public static class McpResourceMethodException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a new exception with the specified detail message and cause.
- * @param message The detail message
- * @param cause The cause
- */
- public McpResourceMethodException(String message, Throwable cause) {
- super(message, cause);
- }
-
- /**
- * Constructs a new exception with the specified detail message.
- * @param message The detail message
- */
- public McpResourceMethodException(String message) {
- super(message);
- }
-
- }
-
/**
* Abstract builder for creating McpResourceMethodCallback instances.
*
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 aa4ae06..e53b193 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
@@ -8,9 +8,13 @@
import java.util.Map;
import java.util.function.BiFunction;
+import org.springaicommunity.mcp.ErrorUtils;
import org.springaicommunity.mcp.annotation.McpResource;
-
+import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
@@ -36,6 +40,48 @@ private AsyncMcpResourceMethodCallback(Builder builder) {
this.validateMethod(this.method);
}
+ @Override
+ protected void validateParamType(Class> paramType) {
+
+ if (McpSyncServerExchange.class.isAssignableFrom(paramType)) {
+ throw new IllegalArgumentException("Async prompt method must not declare parameter of type: "
+ + paramType.getName() + ". Use McpAsyncServerExchange instead." + " Method: "
+ + this.method.getName() + " in " + this.method.getDeclaringClass().getName());
+ }
+ }
+
+ @Override
+ protected Object assignExchangeType(Class> paramType, Object exchange) {
+
+ if (McpTransportContext.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpTransportContext transportContext) {
+ return transportContext;
+ }
+ else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ throw new IllegalArgumentException("Unsupported Async exchange type: "
+ + syncServerExchange.getClass().getName() + " for Async method: " + method.getName() + " in "
+ + method.getDeclaringClass().getName());
+
+ }
+ else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ return asyncServerExchange.transportContext();
+ }
+ }
+ else if (McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ return asyncServerExchange;
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for Async method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
/**
* Apply the callback to the given exchange and request.
*
@@ -90,8 +136,16 @@ public Mono apply(McpAsyncServerExchange exchange, ReadResou
}
}
catch (Exception e) {
- return Mono.error(
- new McpResourceMethodException("Error invoking resource method: " + this.method.getName(), e));
+ if (e instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
+ return Mono.error(mcpError);
+ }
+
+ return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Error invoking resource method: " + this.method.getName() + " in "
+ + this.bean.getClass().getName() + ". /nCause: "
+ + ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .build());
}
});
}
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
index c636aa5..b222105 100644
--- 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
@@ -8,8 +8,13 @@
import java.util.Map;
import java.util.function.BiFunction;
+import org.springaicommunity.mcp.ErrorUtils;
import org.springaicommunity.mcp.annotation.McpResource;
import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
@@ -35,6 +40,42 @@ private AsyncStatelessMcpResourceMethodCallback(Builder builder) {
this.validateMethod(this.method);
}
+ @Override
+ protected void validateParamType(Class> paramType) {
+
+ if (McpSyncServerExchange.class.isAssignableFrom(paramType)
+ || McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+
+ throw new IllegalArgumentException(
+ "Stateless Streamable-Http prompt method must not declare parameter of type: " + paramType.getName()
+ + ". Use McpTransportContext instead." + " Method: " + this.method.getName() + " in "
+ + this.method.getDeclaringClass().getName());
+ }
+ }
+
+ @Override
+ protected Object assignExchangeType(Class> paramType, Object exchange) {
+
+ if (McpTransportContext.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpTransportContext transportContext) {
+ return transportContext;
+ }
+ else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ throw new IllegalArgumentException("Unsupported Sync exchange type: "
+ + syncServerExchange.getClass().getName() + " for Sync method: " + method.getName() + " in "
+ + method.getDeclaringClass().getName());
+
+ }
+ else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ return asyncServerExchange.transportContext();
+ }
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
/**
* Apply the callback to the given context and request.
*
@@ -89,8 +130,16 @@ public Mono apply(McpTransportContext context, ReadResourceR
}
}
catch (Exception e) {
- return Mono.error(
- new McpResourceMethodException("Error invoking resource method: " + this.method.getName(), e));
+ if (e instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
+ return Mono.error(mcpError);
+ }
+
+ return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Error invoking resource method: " + this.method.getName() + " in "
+ + this.bean.getClass().getName() + ". /nCause: "
+ + ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .build());
}
});
}
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 f0b2d25..ed4f592 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
@@ -9,9 +9,13 @@
import java.util.Map;
import java.util.function.BiFunction;
+import org.springaicommunity.mcp.ErrorUtils;
import org.springaicommunity.mcp.annotation.McpResource;
-
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
@@ -34,6 +38,47 @@ private SyncMcpResourceMethodCallback(Builder builder) {
this.validateMethod(this.method);
}
+ @Override
+ protected void validateParamType(Class> paramType) {
+
+ if (McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+ throw new IllegalArgumentException("Sync prompt method must not declare parameter of type: "
+ + paramType.getName() + ". Use McpSyncServerExchange instead." + " Method: " + this.method.getName()
+ + " in " + this.method.getDeclaringClass().getName());
+ }
+ }
+
+ @Override
+ protected Object assignExchangeType(Class> paramType, Object exchange) {
+
+ if (McpTransportContext.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpTransportContext transportContext) {
+ return transportContext;
+ }
+ else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ return syncServerExchange.transportContext();
+ }
+ else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ throw new IllegalArgumentException("Unsupported Async exchange type: "
+ + asyncServerExchange.getClass().getName() + " for Sync method: " + method.getName() + " in "
+ + method.getDeclaringClass().getName());
+ }
+ }
+ else if (McpSyncServerExchange.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ return syncServerExchange;
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for Sync method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
/**
* Apply the callback to the given exchange and request.
*
@@ -77,7 +122,16 @@ public ReadResourceResult apply(McpSyncServerExchange exchange, ReadResourceRequ
this.contentType);
}
catch (Exception e) {
- throw new McpResourceMethodException("Access error invoking resource method: " + this.method.getName(), e);
+ if (e instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
+ throw mcpError;
+ }
+
+ throw McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Error invoking resource method: " + this.method.getName() + " in "
+ + this.bean.getClass().getName() + ". /nCause: "
+ + ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .build();
}
}
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
index a4d14d4..d203665 100644
--- 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
@@ -9,8 +9,13 @@
import java.util.Map;
import java.util.function.BiFunction;
+import org.springaicommunity.mcp.ErrorUtils;
import org.springaicommunity.mcp.annotation.McpResource;
import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
+import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceContents;
@@ -34,6 +39,41 @@ private SyncStatelessMcpResourceMethodCallback(Builder builder) {
this.validateMethod(this.method);
}
+ @Override
+ protected void validateParamType(Class> paramType) {
+
+ if (McpSyncServerExchange.class.isAssignableFrom(paramType)
+ || McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
+
+ throw new IllegalArgumentException(
+ "Stateless Streamable-Http prompt method must not declare parameter of type: " + paramType.getName()
+ + ". Use McpTransportContext instead." + " Method: " + this.method.getName() + " in "
+ + this.method.getDeclaringClass().getName());
+ }
+ }
+
+ @Override
+ protected Object assignExchangeType(Class> paramType, Object exchange) {
+
+ if (McpTransportContext.class.isAssignableFrom(paramType)) {
+ if (exchange instanceof McpTransportContext transportContext) {
+ return transportContext;
+ }
+ else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
+ return syncServerExchange.transportContext();
+ }
+ else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
+ throw new IllegalArgumentException("Unsupported Async exchange type: "
+ + asyncServerExchange.getClass().getName() + " for Sync method: " + method.getName() + " in "
+ + method.getDeclaringClass().getName());
+ }
+ }
+
+ throw new IllegalArgumentException(
+ "Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ + " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
+ }
+
/**
* Apply the callback to the given context and request.
*
@@ -77,7 +117,16 @@ public ReadResourceResult apply(McpTransportContext context, ReadResourceRequest
this.contentType);
}
catch (Exception e) {
- throw new McpResourceMethodException("Access error invoking resource method: " + this.method.getName(), e);
+ if (e instanceof McpError mcpError && mcpError.getJsonRpcError() != null) {
+ throw mcpError;
+ }
+
+ throw McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Error invoking resource method: " + this.method.getName() + " in "
+ + this.bean.getClass().getName() + ". /nCause: "
+ + ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())
+ .build();
}
}
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 589c24b..db2e412 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
@@ -10,7 +10,10 @@
import java.util.Map;
import java.util.function.BiFunction;
+import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.Prompt;
@@ -29,6 +32,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 AsyncMcpPromptMethodCallback}.
@@ -161,6 +165,24 @@ public Mono duplicateMetaParameters(McpMeta meta1, McpMeta meta
return Mono.just(new GetPromptResult("Invalid", List.of()));
}
+ @McpPrompt(name = "failing-prompt", description = "A prompt that throws an exception")
+ public Mono getFailingPrompt(GetPromptRequest request) {
+ throw new RuntimeException("Test exception");
+ }
+
+ // Invalid parameter types for async methods
+ public Mono invalidSyncExchangeParameter(McpSyncServerExchange exchange,
+ GetPromptRequest request) {
+ return Mono.just(new GetPromptResult("Invalid", List.of()));
+ }
+
+ @McpPrompt(name = "transport-context-prompt", description = "A prompt with transport context")
+ public Mono getPromptWithTransportContext(McpTransportContext context,
+ GetPromptRequest request) {
+ return Mono.just(new GetPromptResult("Transport context prompt", List.of(new PromptMessage(Role.ASSISTANT,
+ new TextContent("Hello with transport context from " + request.name())))));
+ }
+
}
private Prompt createTestPrompt(String name, String description) {
@@ -476,4 +498,87 @@ public void testDuplicateMetaParameters() throws Exception {
.hasMessageContaining("Method cannot have more than one McpMeta parameter");
}
+ @Test
+ public void testMethodInvocationError() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("getFailingPrompt", GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("failing-prompt", "A prompt that throws an exception");
+
+ 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("failing-prompt", args);
+
+ Mono resultMono = callback.apply(exchange, request);
+
+ // The new error handling should throw McpError instead of custom exceptions
+ StepVerifier.create(resultMono)
+ .expectErrorMatches(throwable -> throwable instanceof McpError
+ && throwable.getMessage().contains("Error invoking prompt method"))
+ .verify();
+ }
+
+ @Test
+ public void testInvalidSyncExchangeParameter() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("invalidSyncExchangeParameter", McpSyncServerExchange.class,
+ GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("invalid", "Invalid parameter type");
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(
+ () -> AsyncMcpPromptMethodCallback.builder().method(method).bean(provider).prompt(prompt).build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Async prompt method must not declare parameter of type")
+ .hasMessageContaining("McpSyncServerExchange")
+ .hasMessageContaining("Use McpAsyncServerExchange instead");
+ }
+
+ @Test
+ public void testCallbackWithTransportContext() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("getPromptWithTransportContext", McpTransportContext.class,
+ GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("transport-context-prompt", "A prompt with transport context");
+
+ BiFunction> callback = AsyncMcpPromptMethodCallback
+ .builder()
+ .method(method)
+ .bean(provider)
+ .prompt(prompt)
+ .build();
+
+ McpAsyncServerExchange exchange = mock(McpAsyncServerExchange.class);
+ McpTransportContext context = mock(McpTransportContext.class);
+
+ // Mock the exchange to return the transport context
+ when(exchange.transportContext()).thenReturn(context);
+
+ Map args = new HashMap<>();
+ args.put("name", "John");
+ GetPromptRequest request = new GetPromptRequest("transport-context-prompt", args);
+
+ Mono resultMono = callback.apply(exchange, request);
+
+ StepVerifier.create(resultMono).assertNext(result -> {
+ assertThat(result).isNotNull();
+ assertThat(result.description()).isEqualTo("Transport 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 transport context from transport-context-prompt");
+ }).verifyComplete();
+ }
+
}
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/AsyncStatelessMcpPromptMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/AsyncStatelessMcpPromptMethodCallbackTests.java
index 66a56b6..70fdeca 100644
--- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/AsyncStatelessMcpPromptMethodCallbackTests.java
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/AsyncStatelessMcpPromptMethodCallbackTests.java
@@ -11,6 +11,9 @@
import java.util.function.BiFunction;
import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.Prompt;
@@ -164,6 +167,22 @@ public Mono duplicateMetaParameters(McpMeta meta1, McpMeta meta
return Mono.just(new GetPromptResult("Invalid", List.of()));
}
+ @McpPrompt(name = "failing-prompt", description = "A prompt that throws an exception")
+ public Mono getFailingPrompt(GetPromptRequest request) {
+ throw new RuntimeException("Test exception");
+ }
+
+ // Invalid parameter types for stateless methods
+ public Mono invalidSyncExchangeParameter(McpSyncServerExchange exchange,
+ GetPromptRequest request) {
+ return Mono.just(new GetPromptResult("Invalid", List.of()));
+ }
+
+ public Mono invalidAsyncExchangeParameter(McpAsyncServerExchange exchange,
+ GetPromptRequest request) {
+ return Mono.just(new GetPromptResult("Invalid", List.of()));
+ }
+
}
private Prompt createTestPrompt(String name, String description) {
@@ -826,4 +845,70 @@ public void testDuplicateMetaParameters() throws Exception {
.hasMessageContaining("Method cannot have more than one McpMeta parameter");
}
+ @Test
+ public void testMethodInvocationError() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("getFailingPrompt", GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("failing-prompt", "A prompt that throws an exception");
+
+ BiFunction> callback = AsyncStatelessMcpPromptMethodCallback
+ .builder()
+ .method(method)
+ .bean(provider)
+ .prompt(prompt)
+ .build();
+
+ McpTransportContext context = mock(McpTransportContext.class);
+ Map args = new HashMap<>();
+ args.put("name", "John");
+ GetPromptRequest request = new GetPromptRequest("failing-prompt", args);
+
+ Mono resultMono = callback.apply(context, request);
+
+ // The new error handling should throw McpError instead of custom exceptions
+ StepVerifier.create(resultMono)
+ .expectErrorMatches(throwable -> throwable instanceof McpError
+ && throwable.getMessage().contains("Error invoking prompt method"))
+ .verify();
+ }
+
+ @Test
+ public void testInvalidSyncExchangeParameter() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("invalidSyncExchangeParameter", McpSyncServerExchange.class,
+ GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("invalid", "Invalid parameter type");
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> AsyncStatelessMcpPromptMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .prompt(prompt)
+ .build()).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Stateless Streamable-Http prompt method must not declare parameter of type")
+ .hasMessageContaining("McpSyncServerExchange")
+ .hasMessageContaining("Use McpTransportContext instead");
+ }
+
+ @Test
+ public void testInvalidAsyncExchangeParameter() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("invalidAsyncExchangeParameter",
+ McpAsyncServerExchange.class, GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("invalid", "Invalid parameter type");
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> AsyncStatelessMcpPromptMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .prompt(prompt)
+ .build()).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Stateless Streamable-Http prompt method must not declare parameter of type")
+ .hasMessageContaining("McpAsyncServerExchange")
+ .hasMessageContaining("Use McpTransportContext instead");
+ }
+
}
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 16f80c8..4e59b18 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
@@ -21,6 +21,7 @@
import org.springaicommunity.mcp.annotation.McpPrompt;
import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.Prompt;
@@ -38,6 +39,11 @@ public class SyncMcpPromptMethodCallbackTests {
private static class TestPromptProvider {
+ @McpPrompt(name = "failing-prompt", description = "A prompt that throws an exception")
+ public GetPromptResult getFailingPrompt(GetPromptRequest request) {
+ throw new RuntimeException("Test exception");
+ }
+
@McpPrompt(name = "greeting", description = "A simple greeting prompt")
public GetPromptResult getPromptWithRequest(GetPromptRequest request) {
return new GetPromptResult("Greeting prompt",
@@ -163,6 +169,31 @@ private Prompt createTestPrompt(String name, String description) {
new PromptArgument("age", "User's age", false)));
}
+ @Test
+ public void testMethodInvocationError() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("getFailingPrompt", GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("failing-prompt", "A prompt that throws an exception");
+
+ 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("failing-prompt", args);
+
+ // The new error handling should throw McpError instead of
+ // McpPromptMethodException
+ assertThatThrownBy(() -> callback.apply(exchange, request)).isInstanceOf(McpError.class)
+ .hasMessageContaining("Error invoking prompt method");
+ }
+
@Test
public void testCallbackWithRequestParameter() throws Exception {
TestPromptProvider provider = new TestPromptProvider();
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/SyncStatelessMcpPromptMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/SyncStatelessMcpPromptMethodCallbackTests.java
index 46c4bd4..67f035a 100644
--- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/SyncStatelessMcpPromptMethodCallbackTests.java
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/prompt/SyncStatelessMcpPromptMethodCallbackTests.java
@@ -20,6 +20,9 @@
import org.springaicommunity.mcp.annotation.McpPrompt;
import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.Prompt;
@@ -131,6 +134,21 @@ public GetPromptResult duplicateMetaParameters(McpMeta meta1, McpMeta meta2) {
return new GetPromptResult("Invalid", List.of());
}
+ @McpPrompt(name = "failing-prompt", description = "A prompt that throws an exception")
+ public GetPromptResult getFailingPrompt(GetPromptRequest request) {
+ throw new RuntimeException("Test exception");
+ }
+
+ // Invalid parameter types for stateless methods
+ public GetPromptResult invalidSyncExchangeParameter(McpSyncServerExchange exchange, GetPromptRequest request) {
+ return new GetPromptResult("Invalid", List.of());
+ }
+
+ public GetPromptResult invalidAsyncExchangeParameter(McpAsyncServerExchange exchange,
+ GetPromptRequest request) {
+ return new GetPromptResult("Invalid", List.of());
+ }
+
}
private Prompt createTestPrompt(String name, String description) {
@@ -609,4 +627,66 @@ public void testDuplicateMetaParameters() throws Exception {
.hasMessageContaining("Method cannot have more than one McpMeta parameter");
}
+ @Test
+ public void testMethodInvocationError() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("getFailingPrompt", GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("failing-prompt", "A prompt that throws an exception");
+
+ BiFunction callback = SyncStatelessMcpPromptMethodCallback
+ .builder()
+ .method(method)
+ .bean(provider)
+ .prompt(prompt)
+ .build();
+
+ McpTransportContext context = mock(McpTransportContext.class);
+ Map args = new HashMap<>();
+ args.put("name", "John");
+ GetPromptRequest request = new GetPromptRequest("failing-prompt", args);
+
+ // The new error handling should throw McpError instead of the old exception type
+ assertThatThrownBy(() -> callback.apply(context, request)).isInstanceOf(McpError.class)
+ .hasMessageContaining("Error invoking prompt method");
+ }
+
+ @Test
+ public void testInvalidSyncExchangeParameter() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("invalidSyncExchangeParameter", McpSyncServerExchange.class,
+ GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("invalid", "Invalid parameter type");
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> SyncStatelessMcpPromptMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .prompt(prompt)
+ .build()).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Stateless Streamable-Http prompt method must not declare parameter of type")
+ .hasMessageContaining("McpSyncServerExchange")
+ .hasMessageContaining("Use McpTransportContext instead");
+ }
+
+ @Test
+ public void testInvalidAsyncExchangeParameter() throws Exception {
+ TestPromptProvider provider = new TestPromptProvider();
+ Method method = TestPromptProvider.class.getMethod("invalidAsyncExchangeParameter",
+ McpAsyncServerExchange.class, GetPromptRequest.class);
+
+ Prompt prompt = createTestPrompt("invalid", "Invalid parameter type");
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> SyncStatelessMcpPromptMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .prompt(prompt)
+ .build()).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Stateless Streamable-Http prompt method must not declare parameter of type")
+ .hasMessageContaining("McpAsyncServerExchange")
+ .hasMessageContaining("Use McpTransportContext instead");
+ }
+
}
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 447a990..3eed2b5 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
@@ -9,7 +9,10 @@
import java.util.Map;
import java.util.function.BiFunction;
+import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
@@ -233,6 +236,23 @@ public ReadResourceResult getResourceWithMultipleMetas(McpMeta meta1, McpMeta me
return new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain", content)));
}
+ @McpResource(uri = "failing-resource://resource", description = "A resource that throws an exception")
+ public Mono getFailingResource(ReadResourceRequest request) {
+ throw new RuntimeException("Test exception");
+ }
+
+ // Invalid parameter types for async methods
+ public Mono invalidSyncExchangeParameter(McpSyncServerExchange exchange,
+ ReadResourceRequest request) {
+ return Mono.just(new ReadResourceResult(List.of()));
+ }
+
+ public Mono getResourceWithTransportContext(McpTransportContext context,
+ ReadResourceRequest request) {
+ return Mono.just(new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain",
+ "Content with transport context for " + request.uri()))));
+ }
+
}
// Helper method to create a mock McpResource annotation
@@ -718,9 +738,8 @@ public McpUriTemplateManager create(String uriTemplate) {
Mono resultMono = callbackWithMockTemplate.apply(exchange, request);
StepVerifier.create(resultMono)
- .expectErrorMatches(
- throwable -> throwable instanceof AsyncMcpResourceMethodCallback.McpResourceMethodException
- && throwable.getMessage().contains("Error invoking resource method"))
+ .expectErrorMatches(throwable -> throwable instanceof McpError
+ && throwable.getMessage().contains("Error invoking resource method"))
.verify();
}
@@ -1155,4 +1174,47 @@ public void testCallbackWithMultipleMetas() throws Exception {
.hasMessageContaining("Method cannot have more than one McpMeta parameter");
}
+ @Test
+ public void testNewMethodInvocationError() throws Exception {
+ TestAsyncResourceProvider provider = new TestAsyncResourceProvider();
+ Method method = TestAsyncResourceProvider.class.getMethod("getFailingResource", ReadResourceRequest.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("failing-resource://resource");
+
+ Mono resultMono = callback.apply(exchange, request);
+
+ // The new error handling should throw McpError instead of custom exceptions
+ StepVerifier.create(resultMono)
+ .expectErrorMatches(throwable -> throwable instanceof McpError
+ && throwable.getMessage().contains("Error invoking resource method"))
+ .verify();
+ }
+
+ @Test
+ public void testInvalidSyncExchangeParameter() throws Exception {
+ TestAsyncResourceProvider provider = new TestAsyncResourceProvider();
+ Method method = TestAsyncResourceProvider.class.getMethod("invalidSyncExchangeParameter",
+ McpSyncServerExchange.class, ReadResourceRequest.class);
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> AsyncMcpResourceMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .resource(ResourceAdapter.asResource(createMockMcpResource()))
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(
+ "Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken")
+ .hasMessageContaining("McpSyncServerExchange");
+ }
+
}
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java
index 73bdb70..be03bea 100644
--- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/AsyncStatelessMcpResourceMethodCallbackTests.java
@@ -10,6 +10,9 @@
import java.util.function.BiFunction;
import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
@@ -188,6 +191,22 @@ public ReadResourceResult getResourceWithMultipleMetas(McpMeta meta1, McpMeta me
return new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain", content)));
}
+ @McpResource(uri = "failing-resource://resource", description = "A resource that throws an exception")
+ public Mono getFailingResource(ReadResourceRequest request) {
+ throw new RuntimeException("Test exception");
+ }
+
+ // Invalid parameter types for stateless methods
+ public Mono invalidSyncExchangeParameter(McpSyncServerExchange exchange,
+ ReadResourceRequest request) {
+ return Mono.just(new ReadResourceResult(List.of()));
+ }
+
+ public Mono invalidAsyncExchangeParameter(McpAsyncServerExchange exchange,
+ ReadResourceRequest request) {
+ return Mono.just(new ReadResourceResult(List.of()));
+ }
+
}
// Helper method to create a mock McpResource annotation
@@ -681,9 +700,8 @@ public McpUriTemplateManager create(String uriTemplate) {
Mono resultMono = callbackWithMockTemplate.apply(context, request);
StepVerifier.create(resultMono)
- .expectErrorMatches(
- throwable -> throwable instanceof AsyncStatelessMcpResourceMethodCallback.McpResourceMethodException
- && throwable.getMessage().contains("Error invoking resource method"))
+ .expectErrorMatches(throwable -> throwable instanceof McpError
+ && throwable.getMessage().contains("Error invoking resource method"))
.verify();
}
@@ -965,4 +983,66 @@ public void testCallbackWithMultipleMetas() throws Exception {
.hasMessageContaining("Method cannot have more than one McpMeta parameter");
}
+ @Test
+ public void testNewMethodInvocationError() throws Exception {
+ TestAsyncStatelessResourceProvider provider = new TestAsyncStatelessResourceProvider();
+ Method method = TestAsyncStatelessResourceProvider.class.getMethod("getFailingResource",
+ ReadResourceRequest.class);
+ McpResource resourceAnnotation = method.getAnnotation(McpResource.class);
+
+ BiFunction> callback = AsyncStatelessMcpResourceMethodCallback
+ .builder()
+ .method(method)
+ .bean(provider)
+ .resource(ResourceAdapter.asResource(resourceAnnotation))
+ .build();
+
+ McpTransportContext context = mock(McpTransportContext.class);
+ ReadResourceRequest request = new ReadResourceRequest("failing-resource://resource");
+
+ Mono resultMono = callback.apply(context, request);
+
+ // The new error handling should throw McpError instead of custom exceptions
+ StepVerifier.create(resultMono)
+ .expectErrorMatches(throwable -> throwable instanceof McpError
+ && throwable.getMessage().contains("Error invoking resource method"))
+ .verify();
+ }
+
+ @Test
+ public void testInvalidSyncExchangeParameter() throws Exception {
+ TestAsyncStatelessResourceProvider provider = new TestAsyncStatelessResourceProvider();
+ Method method = TestAsyncStatelessResourceProvider.class.getMethod("invalidSyncExchangeParameter",
+ McpSyncServerExchange.class, ReadResourceRequest.class);
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> AsyncStatelessMcpResourceMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .resource(ResourceAdapter.asResource(createMockMcpResource()))
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(
+ "Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken")
+ .hasMessageContaining("McpSyncServerExchange");
+ }
+
+ @Test
+ public void testInvalidAsyncExchangeParameter() throws Exception {
+ TestAsyncStatelessResourceProvider provider = new TestAsyncStatelessResourceProvider();
+ Method method = TestAsyncStatelessResourceProvider.class.getMethod("invalidAsyncExchangeParameter",
+ McpAsyncServerExchange.class, ReadResourceRequest.class);
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> AsyncStatelessMcpResourceMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .resource(ResourceAdapter.asResource(createMockMcpResource()))
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(
+ "Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken")
+ .hasMessageContaining("McpAsyncServerExchange");
+ }
+
}
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 476d7bc..1ea3875 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
@@ -8,7 +8,10 @@
import java.util.List;
import java.util.function.BiFunction;
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
@@ -204,6 +207,24 @@ public ReadResourceResult duplicateRequestParameters(ReadResourceRequest request
return new ReadResourceResult(List.of());
}
+ @McpResource(uri = "failing-resource://resource", description = "A resource that throws an exception")
+ public ReadResourceResult getFailingResource(ReadResourceRequest request) {
+ throw new RuntimeException("Test exception");
+ }
+
+ // Invalid parameter types for sync methods
+ public ReadResourceResult invalidAsyncExchangeParameter(McpAsyncServerExchange exchange,
+ ReadResourceRequest request) {
+ return new ReadResourceResult(List.of());
+ }
+
+ @McpResource(uri = "transport-context://resource", description = "A resource with transport context")
+ public ReadResourceResult getResourceWithTransportContext(McpTransportContext context,
+ ReadResourceRequest request) {
+ return new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain",
+ "Content with transport context for " + request.uri())));
+ }
+
}
// Helper method to create a mock McpResource annotation
@@ -1125,4 +1146,63 @@ public void testCallbackWithMultipleMetas() throws Exception {
.hasMessageContaining("Method cannot have more than one McpMeta parameter");
}
+ @Test
+ public void testMethodInvocationError() throws Exception {
+ TestResourceProvider provider = new TestResourceProvider();
+ Method method = TestResourceProvider.class.getMethod("getFailingResource", ReadResourceRequest.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("failing-resource://resource");
+
+ // The new error handling should throw McpError instead of custom exceptions
+ assertThatThrownBy(() -> callback.apply(exchange, request)).isInstanceOf(McpError.class)
+ .hasMessageContaining("Error invoking resource method");
+ }
+
+ @Test
+ public void testInvalidAsyncExchangeParameter() throws Exception {
+ TestResourceProvider provider = new TestResourceProvider();
+ Method method = TestResourceProvider.class.getMethod("invalidAsyncExchangeParameter",
+ McpAsyncServerExchange.class, ReadResourceRequest.class);
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> SyncMcpResourceMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .resource(ResourceAdapter.asResource(createMockMcpResource()))
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(
+ "Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken")
+ .hasMessageContaining("McpAsyncServerExchange");
+ }
+
+ @Test
+ public void testCallbackWithTransportContext() throws Exception {
+ TestResourceProvider provider = new TestResourceProvider();
+ Method method = TestResourceProvider.class.getMethod("getResourceWithTransportContext",
+ McpTransportContext.class, ReadResourceRequest.class);
+ McpResource resourceAnnotation = method.getAnnotation(McpResource.class);
+
+ // Should fail during callback creation due to parameter validation -
+ // McpTransportContext is not supported in resource methods
+ assertThatThrownBy(() -> SyncMcpResourceMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .resource(ResourceAdapter.asResource(resourceAnnotation))
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(
+ "Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken")
+ .hasMessageContaining("McpTransportContext");
+ }
+
}
diff --git a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java
index 5348e78..eee5647 100644
--- a/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java
+++ b/mcp-annotations/src/test/java/org/springaicommunity/mcp/method/resource/SyncStatelessMcpResourceMethodCallbackTests.java
@@ -10,6 +10,9 @@
import java.util.function.BiFunction;
import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.server.McpAsyncServerExchange;
+import io.modelcontextprotocol.server.McpSyncServerExchange;
+import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest;
import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
@@ -170,6 +173,22 @@ public ReadResourceResult getResourceWithMultipleMetas(McpMeta meta1, McpMeta me
return new ReadResourceResult(List.of(new TextResourceContents(request.uri(), "text/plain", content)));
}
+ @McpResource(uri = "failing-resource://resource", description = "A resource that throws an exception")
+ public ReadResourceResult getFailingResource(ReadResourceRequest request) {
+ throw new RuntimeException("Test exception");
+ }
+
+ // Invalid parameter types for stateless methods
+ public ReadResourceResult invalidSyncExchangeParameter(McpSyncServerExchange exchange,
+ ReadResourceRequest request) {
+ return new ReadResourceResult(List.of());
+ }
+
+ public ReadResourceResult invalidAsyncExchangeParameter(McpAsyncServerExchange exchange,
+ ReadResourceRequest request) {
+ return new ReadResourceResult(List.of());
+ }
+
}
// Helper method to create a mock McpResource annotation
@@ -739,9 +758,8 @@ public void testUriVariableExtraction() throws Exception {
// Test with mismatched URI that doesn't contain expected variables
ReadResourceRequest invalidRequest = new ReadResourceRequest("invalid/uri/format");
- assertThatThrownBy(() -> callback.apply(context, invalidRequest))
- .isInstanceOf(AbstractMcpResourceMethodCallback.McpResourceMethodException.class)
- .hasMessageContaining("Access error invoking resource method");
+ assertThatThrownBy(() -> callback.apply(context, invalidRequest)).isInstanceOf(McpError.class)
+ .hasMessageContaining("Failed to extract all URI variables from request URI: invalid/uri/format.");
}
// Tests for @McpMeta functionality
@@ -925,4 +943,61 @@ public void testCallbackWithMultipleMetas() throws Exception {
.hasMessageContaining("Method cannot have more than one McpMeta parameter");
}
+ @Test
+ public void testMethodInvocationError() throws Exception {
+ TestResourceProvider provider = new TestResourceProvider();
+ Method method = TestResourceProvider.class.getMethod("getFailingResource", ReadResourceRequest.class);
+ McpResource resourceAnnotation = method.getAnnotation(McpResource.class);
+
+ BiFunction callback = SyncStatelessMcpResourceMethodCallback
+ .builder()
+ .method(method)
+ .bean(provider)
+ .resource(ResourceAdapter.asResource(resourceAnnotation))
+ .build();
+
+ McpTransportContext context = mock(McpTransportContext.class);
+ ReadResourceRequest request = new ReadResourceRequest("failing-resource://resource");
+
+ // The new error handling should throw McpError instead of custom exceptions
+ assertThatThrownBy(() -> callback.apply(context, request)).isInstanceOf(McpError.class)
+ .hasMessageContaining("Error invoking resource method");
+ }
+
+ @Test
+ public void testInvalidSyncExchangeParameter() throws Exception {
+ TestResourceProvider provider = new TestResourceProvider();
+ Method method = TestResourceProvider.class.getMethod("invalidSyncExchangeParameter",
+ McpSyncServerExchange.class, ReadResourceRequest.class);
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> SyncStatelessMcpResourceMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .resource(ResourceAdapter.asResource(createMockMcpResource()))
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(
+ "Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken")
+ .hasMessageContaining("McpSyncServerExchange");
+ }
+
+ @Test
+ public void testInvalidAsyncExchangeParameter() throws Exception {
+ TestResourceProvider provider = new TestResourceProvider();
+ Method method = TestResourceProvider.class.getMethod("invalidAsyncExchangeParameter",
+ McpAsyncServerExchange.class, ReadResourceRequest.class);
+
+ // Should fail during callback creation due to parameter validation
+ assertThatThrownBy(() -> SyncStatelessMcpResourceMethodCallback.builder()
+ .method(method)
+ .bean(provider)
+ .resource(ResourceAdapter.asResource(createMockMcpResource()))
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(
+ "Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken")
+ .hasMessageContaining("McpAsyncServerExchange");
+ }
+
}