spring-projects/spring-ai · error · ClassCastException
Expected Mono but got Mono<
Error message
Expected Mono<Void> but got Mono<
What it means
A ClassCastException thrown at runtime by AsyncMcpPromptListChangedMethodCallback when the async handler method returns a Mono whose element value is non-null (i.e., not Mono<Void>). Async prompt-list-changed handlers must signal completion only; a value-bearing Mono indicates the method returns actual data where none is allowed. The check wraps the result in flatMap and throws when an emitted value is observed.
Solutions
- Change the handler method to return Mono<Void> and end the reactive chain with .then() so nothing is emitted.
- If you need the returned data elsewhere, side-effect it inside the chain (doOnNext/doOnSuccess) and still return Mono<Void>.
- If the logic is synchronous, keep the method void — the async callback also accepts plain void return types.
Example fix
// before
public Mono<List<McpSchema.Prompt>> onPromptsChanged(List<McpSchema.Prompt> prompts) {
return Mono.just(prompts);
}
// after
public Mono<Void> onPromptsChanged(List<McpSchema.Prompt> prompts) {
return refreshCache(prompts).then();
} Defensive patterns
Strategy: type-guard
Validate before calling
Method m = handler.getClass().getMethod("onPromptsChanged", List.class);
if (!m.getReturnType().equals(Mono.class)) {
throw new IllegalStateException("async handler must return Mono<Void>");
} Type guard
static boolean isValidAsyncHandler(Method m) {
Class<?> rt = m.getReturnType();
return rt == void.class || (Mono.class.isAssignableFrom(rt));
} Try / catch
try {
callback.accept(updatedPrompts).block();
} catch (ClassCastException e) {
if (e.getMessage().startsWith("Expected Mono<Void>")) {
log.error("Handler returned a value-bearing Mono; change signature to Mono<Void> and end with .then()", e);
} else {
throw e;
}
} Prevention
- End every reactive handler chain with .then() so nothing is emitted.
- Never return Mono<T> with data from a changed-callback; side-effect via doOnNext instead.
- Add an architecture test asserting handler return types at build time.
When it happens
Trigger: Registering an async prompt list changed handler whose method returns Mono<Prompt>, Mono<List<Prompt>>, Mono<Boolean>, etc. The error fires only when the method is invoked and the Mono emits a non-null value, not at registration time.
Common situations: Reusing an existing service method that returns data instead of writing a dedicated void/Mono<Void> consumer; migrating a sync handler (void) to async but keeping a return value; copy-pasting a tool-call handler that returns results into a changed-callback slot.
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
- Expected Mono but got Mono<
- Method must have void or Mono
- Method must have void or Mono
- Method must return a Mono
- Method must return Mono
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/9210e7ff6d3d3012.
Report an issue: GitHub.
Appendix: source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/prompt/AsyncMcpPromptListChangedMethodCallback.java:82
// Build arguments for the method call
Object[] args = this.buildArgs(this.method, null, updatedPrompts);
// Invoke the method
this.method.setAccessible(true);
Object result = this.method.invoke(this.bean, args);
// If the method returns a Mono, handle it
if (result instanceof Mono) {
// We need to handle the case where the Mono is not a Mono<Void>
// This is expected by the test testInvalidMonoReturnType
Mono<?> monoResult = (Mono<?>) result;
// Convert the Mono to a Mono<Void> by checking the value
// If the value is not null (i.e., not Void), throw a ClassCastException
return monoResult.flatMap(value -> {
if (value != null) {
// This will be caught by the test testInvalidMonoReturnType
throw new ClassCastException(
"Expected Mono<Void> but got Mono<" + value.getClass().getName() + ">");
}
return Mono.empty();
}).then();
}
// If the method returns void, return an empty Mono
return Mono.empty();
}
catch (Exception e) {
return Mono.error(new McpPromptListChangedConsumerMethodException(
"Error invoking prompt list changed consumer method: " + this.method.getName(), e));
}
}
/**
* Validates that the method return type is compatible with the prompt list changed
* consumer callback.
* @param method The method to validateView on GitHub (pinned to 98a7beda4f)