spring-projects/spring-ai · error · IllegalArgumentException
Method must return Mono
Error message
Method must return Mono<CreateMessageResult> or CreateMessageResult: {methodName} in {className} returns {returnTypeName} What it means
AsyncMcpSamplingMethodCallback.validateReturnType throws IllegalArgumentException when a method registered as an async sampling callback does not return Mono<CreateMessageResult> or a CreateMessageResult subtype. The async callback path must be able to treat the outcome reactively or wrap it directly, so any other return type is rejected at callback construction time.
Solutions
- Change the method to return Mono<CreateMessageResult> (wrap a plain result with Mono.just)
- Or return CreateMessageResult directly if a synchronous response is acceptable in this path
- Verify with method.getReturnType() before registering
Example fix
// before
CreateMessageResult sample(CreateMessageRequest req) { ... }
// after
Mono<CreateMessageResult> sample(CreateMessageRequest req) {
return Mono.just(doSample(req));
} Defensive patterns
Strategy: validation
Validate before calling
Class<?> rt = method.getReturnType();
if (!Mono.class.isAssignableFrom(rt) && !CreateMessageResult.class.isAssignableFrom(rt)) {
throw new IllegalArgumentException(method + " must return Mono<CreateMessageResult> or CreateMessageResult");
} Try / catch
try { builder.build(); } catch (IllegalArgumentException e) { log.error("Async sampling return type invalid: {}", e.getMessage()); } Prevention
- Annotate async sampling methods with explicit Mono<CreateMessageResult> signatures
- Add a startup-time reflection check over all registered sampling methods
- Keep sync and async handler methods in separate classes to avoid signature drift
When it happens
Trigger: Registering an @McpSampling-annotated method (or manually built AsyncMcpSamplingMethodCallback) whose signature returns e.g. String, void, or Flux<CreateMessageResult>, then building the callback.
Common situations: Writing a sampling handler method and forgetting the reactive wrapper; copying a sync callback method (returns CreateMessageResult-only sibling contract) into an async specification; upgrading from sync to async specs without changing return types.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- ASYNC Providers don't support imperative (non-reactive)…
- Expected reactive return type but got
- Method must return CreateMessageResult
- Mono return type must be Mono
- SYNC Providers don't support reactive return types…
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/0f4efe6e0d1bd1f5.
Report an issue: GitHub.
Appendix: source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/sampling/AsyncMcpSamplingMethodCallback.java:102
}
}
catch (Exception e) {
return Mono
.error(new McpSamplingMethodException("Error invoking sampling method: " + this.method.getName(), e));
}
}
/**
* Validates that the method return type is compatible with the sampling 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();
if (!Mono.class.isAssignableFrom(returnType) && !CreateMessageResult.class.isAssignableFrom(returnType)) {
throw new IllegalArgumentException(
"Method must return Mono<CreateMessageResult> or CreateMessageResult: " + 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) {
// No exchange type for sampling methods
return false;
}
/**View on GitHub (pinned to 98a7beda4f)