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

Asynchronous progress methods must return void or Mono<Void>

Error message

Asynchronous progress methods must return void or Mono<Void>: {method.getName()} in {method.getDeclaringClass().getName()} returns {returnType.getName()}

What it means

Asynchronous (@McpProgress with async callback) methods must return either void or Mono<Void>. validateReturnType throws this IllegalArgumentException as the terminal check when the declared return type is neither, e.g. a concrete type, CompletableFuture, or Flux. It enforces the async progress handler contract at registration time.

Source

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

		}

		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
	 * method
	 * @throws IllegalArgumentException if the notification is null
	 */
	@Override
	public Mono<Void> apply(ProgressNotification notification) {
		if (notification == null) {

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Change the method's return type to void.
  2. Or change it to Mono<Void> and make the body fully reactive, ending with .then().
  3. If other callers need the current return type, create a dedicated @McpProgress wrapper method returning void that delegates to it.

Example fix

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

Strategy: type-guard

Validate before calling

Class<?> rt = method.getReturnType();
boolean ok = void.class.equals(rt)
    || (Mono.class.equals(rt) && isMonoVoid(method));
if (!ok) throw new IllegalStateException(method + " must return void or Mono<Void>");

Type guard

boolean isValidAsyncProgressReturn(Method m) {
    Class<?> rt = m.getReturnType();
    if (void.class.equals(rt) || Void.class.equals(rt)) return true;
    return Mono.class.equals(rt) && isMonoVoid(m);
}

Try / catch

try {
    registerAsyncHandler(method, bean);
} catch (IllegalArgumentException e) {
    if (e.getMessage().startsWith("Asynchronous progress methods must return")) {
        throw new ConfigurationException("Change return type to void or Mono<Void>", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Registering an async progress callback whose method returns boolean, CompletableFuture<Void>, Flux<Void>, ProgressNotification, or any other non-void/non-Mono<Void> type.

Common situations: Reusing an existing service method as a progress handler without adapting its return type; expecting async results via CompletableFuture out of habit; returning the notification object for chaining.

Related errors


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