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 validate

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Change the handler method to return Mono<Void> and end the reactive chain with .then() so nothing is emitted.
  2. If you need the returned data elsewhere, side-effect it inside the chain (doOnNext/doOnSuccess) and still return Mono<Void>.
  3. 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

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


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