spring-projects/spring-ai · error · java.lang.IllegalArgumentException
Mono return type must be Mono<Void>: {method.getName()} in {
Error message
Mono return type must be Mono<Void>: {method.getName()} in {method.getDeclaringClass().getName()} returns {returnType.getName()} What it means
Async progress methods may return void or Mono<Void>. When a Mono is detected, validateReturnType additionally requires its generic type argument to be exactly Void. A Mono of any other type (Mono<String>, Mono<Boolean>, raw Mono) is rejected with this IllegalArgumentException so reactive progress handlers never emit an unexpected value downstream.
Source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/progress/AsyncMcpProgressMethodCallback.java:62
Class<?> returnType = method.getReturnType();
// Check if return type is void or Mono<Void>
if (returnType == void.class) {
// void is acceptable - we'll wrap it in Mono
return;
}
if (Mono.class.isAssignableFrom(returnType)) {
// Check if it's Mono<Void>
Type genericReturnType = method.getGenericReturnType();
if (genericReturnType instanceof ParameterizedType paramType) {
Type[] typeArguments = paramType.getActualTypeArguments();
if (typeArguments.length == 1 && typeArguments[0] == Void.class) {
// Mono<Void> is acceptable
return;
}
else {
throw new IllegalArgumentException("Mono return type must be Mono<Void>: " + method.getName()
+ " in " + method.getDeclaringClass().getName() + " returns " + returnType.getName());
}
}
}
throw new IllegalArgumentException(
"Asynchronous progress methods must return void or Mono<Void>: " + method.getName() + " in "
+ method.getDeclaringClass().getName() + " returns " + returnType.getName());
}
/**
* Apply the progress notification and process it asynchronously.
* <p>
* This method builds the arguments for the method call and invokes the method,
* returning a Mono<Void>.
* @param notification The progress notification, must not be null
* @return A Mono<Void> representing the asynchronous operation
* @throws McpProgressMethodException if there is an error invoking the progressView on GitHub (pinned to 98a7beda4f)
Solutions
- Change the return type to Mono<Void> and end the reactive chain with .then().
- Or return plain void and perform the work synchronously inside the method.
- Replace Mono<SomeType> with Mono<Void> and consume the value inside the chain before then().
Example fix
// before
@McpProgress
public Mono<String> onProgress(Double p, String token) { return Mono.just("ok"); }
// after
@McpProgress
public Mono<Void> onProgress(Double p, String token) { return doWork(p, token).then(); } Defensive patterns
Strategy: type-guard
Validate before calling
if (Mono.class.equals(method.getReturnType())) {
Type arg = ((ParameterizedType) method.getGenericReturnType()).getActualTypeArguments()[0];
if (arg != Void.class) throw new IllegalStateException(method + " must return Mono<Void>");
} Type guard
boolean isMonoVoid(Method m) {
if (!Mono.class.equals(m.getReturnType())) return false;
if (!(m.getGenericReturnType() instanceof ParameterizedType pt)) return false;
return pt.getActualTypeArguments().length == 1 && pt.getActualTypeArguments()[0] == Void.class;
} Try / catch
try {
asyncRegistry.register(builder.method(m).bean(b).build());
} catch (IllegalArgumentException e) {
if (e.getMessage().startsWith("Mono return type must be Mono<Void>")) {
throw new ConfigurationException("End reactive chains with .then() to get Mono<Void>", e);
}
throw e;
} Prevention
- Standardize on ending progress handlers with .then() or Mono.empty().
- Avoid raw Mono return types.
- Assert return types of @McpProgress methods in a unit test.
When it happens
Trigger: Annotating a method that returns Mono<String>, Mono<Boolean>, or a raw/parameterized Mono with a non-Void type argument, then registering it with AsyncMcpProgressMethodCallback; validateReturnType runs at registration.
Common situations: Returning the result of a reactive pipeline (e.g. Mono.just("done")) instead of terminating with .then() or Mono.empty(); raw Mono from legacy Java 6-style code without generics.
Related errors
- Asynchronous progress methods must return void or Mono<Void>
- Synchronous progress methods must return void: {method.getNa
- ASYNC Providers don't support imperative (non-reactive) retu
- SYNC Providers don't support reactive return types. Skipping
- Expected Mono<Void> but got Mono<
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/954f76d39a98224e.
Report an issue: GitHub.