spring-projects/spring-ai · error · IllegalArgumentException

Method must return Mono<ElicitResult> or Mono<StructuredElic

Error message

Method must return Mono<ElicitResult> or Mono<StructuredElicitResult>: 

What it means

Thrown by AsyncMcpElicitationMethodCallback.validateReturnType when an async @McpElicitation-annotated method's return type is not a Mono. Async elicitation handlers must return Mono<ElicitResult> (or Mono<StructuredElicitResult>) so the framework can subscribe reactively.

Source

Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/elicitation/AsyncMcpElicitationMethodCallback.java:119

					"Method must return Mono<ElicitResult> or Mono<StructuredElicitResult>: " + this.method.getName()));
		}
		catch (Exception e) {
			return Mono.error(new McpElicitationMethodException(
					"Error invoking elicitation method: " + this.method.getName(), e));
		}
	}

	/**
	 * Validates that the method return type is compatible with the elicitation callback.
	 * @param method The method to validate
	 * @throws IllegalArgumentException if the return type is not compatible
	 */
	@Override
	protected void validateReturnType(Method method) {
		Class<?> returnType = method.getReturnType();

		if (!Mono.class.isAssignableFrom(returnType)) {
			throw new IllegalArgumentException(
					"Method must return Mono<ElicitResult> or Mono<StructuredElicitResult>: " + method.getName()
							+ " in " + method.getDeclaringClass().getName() + " returns " + returnType.getName());
		}
	}

	/**
	 * Checks if a parameter type is compatible with the exchange type.
	 * @param paramType The parameter type to check
	 * @return true if the parameter type is compatible with the exchange type, false
	 * otherwise
	 */
	@Override
	protected boolean isExchangeType(Class<?> paramType) {
		// No exchange type for elicitation methods
		return false;
	}

	/**

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Change the return type to Mono<ElicitResult>
  2. Wrap a blocking computation with Mono.fromCallable(() -> computeResult(req))
  3. If the method is inherently synchronous, register it via the sync specification/callback instead of the async one
  4. For structured payloads return Mono<StructuredElicitResult> instead

Example fix

// before
@McpElicitation
public ElicitResult handle(ElicitRequest request) { return compute(request); }

// after
@McpElicitation
public Mono<ElicitResult> handle(ElicitRequest request) {
    return Mono.fromCallable(() -> compute(request));
}
Defensive patterns

Strategy: validation

Validate before calling

if (!Mono.class.isAssignableFrom(handlerMethod.getReturnType())) {
    throw new IllegalArgumentException("Async elicitation handler must return Mono<ElicitResult>: " + handlerMethod);
}

Type guard

boolean isAsyncElicitationHandler(Method m) {
    return Mono.class.isAssignableFrom(m.getReturnType());
}

Prevention

When it happens

Trigger: Registering a handler on the async callback path declared as public ElicitResult handle(ElicitRequest req) (sync return) or returning Flux/CompletableFuture instead of Mono.

Common situations: Using @McpElicitation in a reactive (WebFlux / async MCP client) setup but writing the handler in blocking style, or migrating a sync handler to async without changing the return type.

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/4c3b2240abe2988a. Report an issue: GitHub.