spring-projects/spring-ai · error · IllegalArgumentException
Async complete methods should use McpAsyncRequestContext ins
Error message
Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: {method} in {class} What it means
Thrown when a @McpComplete-annotated method has a reactive (async) return type (e.g. Mono/Flux) but declares an McpSyncRequestContext parameter. Spring AI MCP requires the context type to match the method's sync/async style: reactive completions must use McpAsyncRequestContext. The mismatch is detected during builder validation via validateParameters().
Source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/complete/AbstractMcpCompleteMethodCallback.java:206
}
// Skip McpMeta parameters from validation
if (McpMeta.class.isAssignableFrom(paramType)) {
if (hasMetaParam) {
throw new IllegalArgumentException("Method cannot have more than one McpMeta parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
}
hasMetaParam = true;
continue;
}
if (McpSyncRequestContext.class.isAssignableFrom(paramType)) {
if (hasRequestContextParam) {
throw new IllegalArgumentException("Method cannot have more than one request context parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
}
if (McpPredicates.isReactiveReturnType.test(method)) {
throw new IllegalArgumentException(
"Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
}
hasRequestContextParam = true;
}
else if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) {
if (hasRequestContextParam) {
throw new IllegalArgumentException("Method cannot have more than one request context parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
}
if (McpPredicates.isNotReactiveReturnType.test(method)) {
throw new IllegalArgumentException(
"Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter: "
+ method.getName() + " in " + method.getDeclaringClass().getName());
}
hasRequestContextParam = true;
}View on GitHub (pinned to 98a7beda4f)
Solutions
- Replace the McpSyncRequestContext parameter with McpAsyncRequestContext
- Or change the return type to a non-reactive type (e.g. String or CompleteResult) to keep McpSyncRequestContext
- Rebuild/re-register the completion callback after fixing the signature
Example fix
// before
@McpComplete(prompt="language")
public Mono<CompleteResult> complete(McpSyncRequestContext ctx, CompleteRequest req) { ... }
// after
@McpComplete(prompt="language")
public Mono<CompleteResult> complete(McpAsyncRequestContext ctx, CompleteRequest req) { ... } Defensive patterns
Strategy: validation
Validate before calling
boolean isReactive = Mono.class.isAssignableFrom(m.getReturnType()) || Flux.class.isAssignableFrom(m.getReturnType());
boolean hasSyncCtx = Arrays.stream(m.getParameterTypes()).anyMatch(McpSyncRequestContext.class::isAssignableFrom);
boolean hasAsyncCtx = Arrays.stream(m.getParameterTypes()).anyMatch(McpAsyncRequestContext.class::isAssignableFrom);
if (isReactive && hasSyncCtx || !isReactive && hasAsyncCtx) throw new IllegalStateException("Context type must match return type: " + m); Type guard
boolean contextMatchesReturn(Method m) {
boolean reactive = Mono.class.isAssignableFrom(m.getReturnType()) || Flux.class.isAssignableFrom(m.getReturnType());
boolean asyncCtx = Arrays.stream(m.getParameterTypes()).anyMatch(McpAsyncRequestContext.class::isAssignableFrom);
return reactive == asyncCtx;
} Try / catch
try {
builder.method(m).bean(bean).build();
} catch (IllegalArgumentException e) {
if (e.getMessage().contains("McpAsyncRequestContext")) { /* fix signature: swap context param type */ }
throw e;
} Prevention
- Pick the context type AFTER fixing the return type: Mono/Flux -> McpAsyncRequestContext, plain -> McpSyncRequestContext
- Write a unit test that builds all @McpComplete callbacks at startup so mismatches fail fast
- Keep sync and async completion examples in separate packages to avoid copy-paste drift
When it happens
Trigger: Declaring a completion method returning Mono<CompleteResult>/Flux while taking McpSyncRequestContext as a parameter; registering such a method through the AbstractMcpCompleteMethodCallback builder.
Common situations: Developers copy a sync completion example and change only the return type to Mono/Flux (or vice versa after a refactor); migrating code between sync and async MCP server setups and forgetting to swap the context parameter 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
- Sync complete methods should use McpSyncRequestContext inste
- Method cannot have more than one transport context parameter
- Method cannot have more than one exchange parameter: {method
- Method cannot have more than one CompleteRequest parameter:
- Method cannot have more than one CompleteArgument parameter:
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/893bf37c5019c517.
Report an issue: GitHub.