spring-projects/spring-ai · error · IllegalArgumentException
Async prompt method must not declare parameter of type: ${pa
Error message
Async prompt method must not declare parameter of type: ${paramType}. Use McpAsyncServerExchange instead. Method: ${method} in ${declaringClass} What it means
This IllegalArgumentException is thrown by AsyncMcpResourceMethodCallback.validateParamType when an @McpResource-annotated method that runs in async mode declares a parameter of type McpSyncServerExchange (or a subtype). Async callbacks are wired to McpAsyncServerExchange, so a sync exchange parameter can never be satisfied and Spring AI MCP rejects the method up front. Use McpAsyncServerExchange (or McpTransportContext) instead.
Source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AsyncMcpResourceMethodCallback.java:64
* @author Christian Tzolov
* @author Alexandros Pappas
* @author Vadzim Shurmialiou
* @author Craig Walls
*/
public final class AsyncMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback
implements BiFunction<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> {
private AsyncMcpResourceMethodCallback(Builder builder) {
super(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,
builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType, builder.meta);
this.validateMethod(this.method);
}
@Override
protected void validateParamType(Class<?> paramType) {
if (McpSyncServerExchange.class.isAssignableFrom(paramType)) {
throw new IllegalArgumentException("Async prompt method must not declare parameter of type: "
+ paramType.getName() + ". Use McpAsyncServerExchange instead." + " Method: "
+ this.method.getName() + " in " + this.method.getDeclaringClass().getName());
}
}
@Override
protected Object assignExchangeType(Class<?> paramType, Object exchange) {
if (McpTransportContext.class.isAssignableFrom(paramType)) {
if (exchange instanceof McpTransportContext transportContext) {
return transportContext;
}
else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
throw new IllegalArgumentException("Unsupported Async exchange type: "
+ syncServerExchange.getClass().getName() + " for Async method: " + method.getName() + " in "
+ method.getDeclaringClass().getName());
}View on GitHub (pinned to 98a7beda4f)
Solutions
- Change the parameter type from McpSyncServerExchange to McpAsyncServerExchange in the annotated method.
- If no session state is needed, use McpTransportContext as the exchange parameter, which works in both sync and async modes.
- Ensure the resource method is registered against the server type matching its exchange parameter (sync exchange -> sync server).
Example fix
// before
@McpResource(uri = "docs://{id}")
public ReadResourceResult read(McpSyncServerExchange exchange, String id) { ... }
// after
@McpResource(uri = "docs://{id}")
public ReadResourceResult read(McpAsyncServerExchange exchange, String id) { ... } Defensive patterns
Strategy: validation
Validate before calling
// Fail fast in a startup test
Stream.of(resourceProvider.getClass().getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(McpResource.class))
.forEach(m -> Arrays.stream(m.getParameterTypes())
.filter(p -> McpSyncServerExchange.class.isAssignableFrom(p))
.findAny()
.ifPresent(p -> { throw new IllegalStateException("Async resource method " + m + " must not take " + p); })); Type guard
static boolean isSyncExchangeParam(Class<?> p) { return McpSyncServerExchange.class.isAssignableFrom(p); } Try / catch
try {
asyncMcpServer.annotate(resourceProvider);
} catch (IllegalArgumentException e) {
log.error("Bad resource method signature: {}", e.getMessage());
throw new ConfigurationException(e);
} Prevention
- Never write McpSyncServerExchange in methods registered on async servers
- Prefer McpTransportContext when session state is not needed
- Add a startup test that registers all annotated providers
- Keep sync and async providers in separate packages/classes
When it happens
Trigger: Registering an @McpResource method on an async MCP server (McpAsyncServer / async spec builder) whose signature includes a McpSyncServerExchange parameter; this validation runs during callback construction at registration time.
Common situations: Migrating an app from a sync MCP server to an async one and copying the old method signatures; copy-pasting example code from sync-based docs; sharing a resource class between both server types.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Method must have exactly 1 parameter (List<McpSchema.Resourc
- Single parameter must be of type ElicitRequest:
- Currently only methods with a single ElicitRequest parameter
- Method must have either 1 parameter (LoggingMessageNotificat
- Method must have either 1 parameter (ProgressNotification) o
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/4664099e1cc34311.
Report an issue: GitHub.