spring-projects/spring-ai · error · java.lang.IllegalArgumentException

Mono return type must be Mono<Void>: {method.getName()} in {

Error message

Mono return type must be Mono<Void>: {method.getName()} in {method.getDeclaringClass().getName()} returns {returnType.getName()}

What it means

Async progress methods may return void or Mono<Void>. When a Mono is detected, validateReturnType additionally requires its generic type argument to be exactly Void. A Mono of any other type (Mono<String>, Mono<Boolean>, raw Mono) is rejected with this IllegalArgumentException so reactive progress handlers never emit an unexpected value downstream.

Source

Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/progress/AsyncMcpProgressMethodCallback.java:62

		Class<?> returnType = method.getReturnType();

		// Check if return type is void or Mono<Void>
		if (returnType == void.class) {
			// void is acceptable - we'll wrap it in Mono
			return;
		}

		if (Mono.class.isAssignableFrom(returnType)) {
			// Check if it's Mono<Void>
			Type genericReturnType = method.getGenericReturnType();
			if (genericReturnType instanceof ParameterizedType paramType) {
				Type[] typeArguments = paramType.getActualTypeArguments();
				if (typeArguments.length == 1 && typeArguments[0] == Void.class) {
					// Mono<Void> is acceptable
					return;
				}
				else {
					throw new IllegalArgumentException("Mono return type must be Mono<Void>: " + method.getName()
							+ " in " + method.getDeclaringClass().getName() + " returns " + returnType.getName());
				}
			}
		}

		throw new IllegalArgumentException(
				"Asynchronous progress methods must return void or Mono<Void>: " + method.getName() + " in "
						+ method.getDeclaringClass().getName() + " returns " + returnType.getName());
	}

	/**
	 * Apply the progress notification and process it asynchronously.
	 * <p>
	 * This method builds the arguments for the method call and invokes the method,
	 * returning a Mono<Void>.
	 * @param notification The progress notification, must not be null
	 * @return A Mono<Void> representing the asynchronous operation
	 * @throws McpProgressMethodException if there is an error invoking the progress

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Change the return type to Mono<Void> and end the reactive chain with .then().
  2. Or return plain void and perform the work synchronously inside the method.
  3. Replace Mono<SomeType> with Mono<Void> and consume the value inside the chain before then().

Example fix

// before
@McpProgress
public Mono<String> onProgress(Double p, String token) { return Mono.just("ok"); }
// after
@McpProgress
public Mono<Void> onProgress(Double p, String token) { return doWork(p, token).then(); }
Defensive patterns

Strategy: type-guard

Validate before calling

if (Mono.class.equals(method.getReturnType())) {
    Type arg = ((ParameterizedType) method.getGenericReturnType()).getActualTypeArguments()[0];
    if (arg != Void.class) throw new IllegalStateException(method + " must return Mono<Void>");
}

Type guard

boolean isMonoVoid(Method m) {
    if (!Mono.class.equals(m.getReturnType())) return false;
    if (!(m.getGenericReturnType() instanceof ParameterizedType pt)) return false;
    return pt.getActualTypeArguments().length == 1 && pt.getActualTypeArguments()[0] == Void.class;
}

Try / catch

try {
    asyncRegistry.register(builder.method(m).bean(b).build());
} catch (IllegalArgumentException e) {
    if (e.getMessage().startsWith("Mono return type must be Mono<Void>")) {
        throw new ConfigurationException("End reactive chains with .then() to get Mono<Void>", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Annotating a method that returns Mono<String>, Mono<Boolean>, or a raw/parameterized Mono with a non-Void type argument, then registering it with AsyncMcpProgressMethodCallback; validateReturnType runs at registration.

Common situations: Returning the result of a reactive pipeline (e.g. Mono.just("done")) instead of terminating with .then() or Mono.empty(); raw Mono from legacy Java 6-style code without generics.

Related errors


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