spring-projects/spring-ai · error · ClassCastException
Expected Mono<Void> but got Mono<" + value.getClass().getNam
Error message
Expected Mono<Void> but got Mono<" + value.getClass().getName() + ">
What it means
AsyncMcpToolListChangedMethodCallback.apply converts the handler's Mono result to Mono<Void>. If the underlying Mono emits a non-null, non-Void value, the flatMap throws ClassCastException because the contract requires the async handler to complete with Mono<Void> (no payload). The library enforces Mono<Void> at runtime even when the declared return type compiles as raw Mono.
Source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/tool/AsyncMcpToolListChangedMethodCallback.java:81
// Build arguments for the method call
Object[] args = this.buildArgs(this.method, null, updatedTools);
// 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 McpToolListChangedConsumerMethodException(
"Error invoking tool list changed consumer method: " + this.method.getName(), e));
}
}
/**
* Validates that the method return type is compatible with the tool list changed
* consumer callback.
* @param method The method to validateView on GitHub (pinned to 98a7beda4f)
Solutions
- Return Mono.empty() (or .then()) from the async handler instead of Mono.just(value)
- If a value is needed internally, consume it in the chain and finish with .then(): monoResult.map(v -> ...).then()
- Fix the declared return type to Mono<Void> so the compiler prevents emitting values
Example fix
// before
public Mono<?> onToolListChanged(List<McpSchema.Tool> tools) {
return Mono.just(processed(tools)); // emits non-Void value
}
// after
public Mono<Void> onToolListChanged(List<McpSchema.Tool> tools) {
return Mono.fromRunnable(() -> process(tools));
} Defensive patterns
Strategy: try-catch
Validate before calling
if (result instanceof reactor.core.publisher.Mono<?> m) { m.hasElement().subscribe(hasValue -> { if (Boolean.TRUE.equals(hasValue)) throw new IllegalStateException("Handler Mono must be Mono<Void>"); }); } Type guard
static boolean isMonoVoid(java.lang.reflect.Method m) { return reactor.core.publisher.Mono.class.isAssignableFrom(m.getReturnType()); } Try / catch
handlerResult.flatMap(v -> { if (v != null) return Mono.error(new ClassCastException("Expected Mono<Void> but got " + v.getClass().getName())); return Mono.empty(); }).onErrorResume(ClassCastException.class, e -> { log.error(e.getMessage()); return Mono.empty(); }).then(); Prevention
- Declare handlers as Mono<Void> so the compiler prevents emitting values
- End reactive chains with .then() or use Mono.fromRunnable for side-effect-only work
- Never return Mono.just(value) from a list-changed handler
When it happens
Trigger: An async handler declared to return a raw/parameterized Mono that actually emits a value, e.g. returning Mono.just("done") from a method whose declared type erases to Mono — the emitted value reaches this check and the ClassCastException is thrown inside the reactive chain (surfaced via the subscription's onError).
Common situations: Handlers that log-and-return Mono.just(true) or Mono.fromRunnable misuse, generic Mono<?> methods where generics were erased, or tests like testInvalidMonoReturnType intentionally returning non-Void payloads.
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<Void> but got Mono<
- Expected Mono<Void> but got Mono<
- Parameter must be of type List<McpSchema.Resource>:
- Method must have void or Mono<Void> return type:
- Parameter must be of type List<McpSchema.Tool>: " + method.g
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/fb40b041178352e3.
Report an issue: GitHub.