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

Synchronous progress methods must return void: {method.getNa

Error message

Synchronous progress methods must return void: {method.getName()} in {method.getDeclaringClass().getName()} returns {method.getReturnType().getName()}

What it means

Synchronous (@McpProgress with SyncMcpProgressMethodCallback) progress methods must return void. validateReturnType throws this IllegalArgumentException when the method declares any other return type, because the sync callback processes the notification inline and discards results.

Source

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

 * Synchronous implementation of a progress method callback.
 *
 * This class creates a Consumer that invokes a method annotated with @McpProgress
 * synchronously when a progress notification is received.
 *
 * @author Christian Tzolov
 */
public final class SyncMcpProgressMethodCallback extends AbstractMcpProgressMethodCallback
		implements Consumer<ProgressNotification> {

	private SyncMcpProgressMethodCallback(Builder builder) {
		super(builder.method, builder.bean);
	}

	@Override
	protected void validateReturnType(Method method) {
		// Synchronous methods must return void
		if (!void.class.equals(method.getReturnType())) {
			throw new IllegalArgumentException("Synchronous progress methods must return void: " + method.getName()
					+ " in " + method.getDeclaringClass().getName() + " returns " + method.getReturnType().getName());
		}
	}

	/**
	 * Accept the progress notification and process it.
	 * <p>
	 * This method builds the arguments for the method call and invokes the method.
	 * @param notification The progress notification, must not be null
	 * @throws McpProgressMethodException if there is an error invoking the progress
	 * method
	 * @throws IllegalArgumentException if the notification is null
	 */
	@Override
	public void accept(ProgressNotification notification) {
		if (notification == null) {
			throw new IllegalArgumentException("Notification must not be null");
		}

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Change the method's return type to void.
  2. If a reactive implementation is needed, register with AsyncMcpProgressMethodCallback and return Mono<Void> instead.
  3. Expose testability via side effects (record state to a field/mock) rather than a return value.

Example fix

// before
@McpProgress
public boolean onProgress(Double p, String token) { return true; }
// after
@McpProgress
public void onProgress(Double p, String token) { /* handle */ }
Defensive patterns

Strategy: type-guard

Validate before calling

if (!void.class.equals(method.getReturnType())) {
    throw new IllegalStateException(method + " must return void for sync progress callback");
}

Type guard

boolean isValidSyncProgressReturn(Method m) {
    return void.class.equals(m.getReturnType());
}

Try / catch

try {
    syncRegistry.register(SyncMcpProgressMethodCallback.builder().method(m).bean(b).build());
} catch (IllegalArgumentException e) {
    if (e.getMessage().startsWith("Synchronous progress methods must return void")) {
        throw new ConfigurationException("Use void (sync) or Mono<Void> with the async callback", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Registering SyncMcpProgressMethodCallback with a method that returns boolean, Double, String, or Mono — anything other than void/primitive void — e.g. a handler written as `public boolean onProgress(Double p, String token)`.

Common situations: Using the sync callback with a handler originally written for the async API (returns Mono<Void>); handlers that return a status flag out of habit; refactoring that changed void to a return type for testability.

Related errors


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