spring-projects/spring-ai · error · IllegalArgumentException

Method must return Mono<CreateMessageResult> or CreateMessag

Error message

Method must return Mono<CreateMessageResult> or CreateMessageResult: {methodName} in {className} returns {returnTypeName}

What it means

AsyncMcpSamplingMethodCallback.validateReturnType throws IllegalArgumentException when a method registered as an async sampling callback does not return Mono<CreateMessageResult> or a CreateMessageResult subtype. The async callback path must be able to treat the outcome reactively or wrap it directly, so any other return type is rejected at callback construction time.

Source

Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/sampling/AsyncMcpSamplingMethodCallback.java:102

			}
		}
		catch (Exception e) {
			return Mono
				.error(new McpSamplingMethodException("Error invoking sampling method: " + this.method.getName(), e));
		}
	}

	/**
	 * Validates that the method return type is compatible with the sampling 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) && !CreateMessageResult.class.isAssignableFrom(returnType)) {
			throw new IllegalArgumentException(
					"Method must return Mono<CreateMessageResult> or CreateMessageResult: " + 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 sampling methods
		return false;
	}

	/**

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Change the method to return Mono<CreateMessageResult> (wrap a plain result with Mono.just)
  2. Or return CreateMessageResult directly if a synchronous response is acceptable in this path
  3. Verify with method.getReturnType() before registering

Example fix

// before
CreateMessageResult sample(CreateMessageRequest req) { ... }
// after
Mono<CreateMessageResult> sample(CreateMessageRequest req) {
    return Mono.just(doSample(req));
}
Defensive patterns

Strategy: validation

Validate before calling

Class<?> rt = method.getReturnType();
if (!Mono.class.isAssignableFrom(rt) && !CreateMessageResult.class.isAssignableFrom(rt)) {
    throw new IllegalArgumentException(method + " must return Mono<CreateMessageResult> or CreateMessageResult");
}

Try / catch

try { builder.build(); } catch (IllegalArgumentException e) { log.error("Async sampling return type invalid: {}", e.getMessage()); }

Prevention

When it happens

Trigger: Registering an @McpSampling-annotated method (or manually built AsyncMcpSamplingMethodCallback) whose signature returns e.g. String, void, or Flux<CreateMessageResult>, then building the callback.

Common situations: Writing a sampling handler method and forgetting the reactive wrapper; copying a sync callback method (returns CreateMessageResult-only sibling contract) into an async specification; upgrading from sync to async specs without changing return types.

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/0f4efe6e0d1bd1f5. Report an issue: GitHub.