spring-projects/spring-ai · error · ClassCastException
Expected Mono<Void> 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.
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)
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
- Method must have void or Mono<Void> return type:
- Method must have void or Mono<Void> return type:
- Method must return Mono<ElicitResult> or Mono<StructuredElic
- Expected Mono<Void> but got Mono<
- Method must return a Mono<T> where T is one of GetPromptResu
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/9210e7ff6d3d3012.
Report an issue: GitHub.