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 validate

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Return Mono.empty() (or .then()) from the async handler instead of Mono.just(value)
  2. If a value is needed internally, consume it in the chain and finish with .then(): monoResult.map(v -> ...).then()
  3. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/fb40b041178352e3. Report an issue: GitHub.