spring-projects/spring-ai · error · java.lang.IllegalArgumentException
Unsupported exchange type: {exchange != null ? exchange.getC
Error message
Unsupported exchange type: {exchange != null ? exchange.getClass().getName() : "null"} for Async method: {method.getName()} in {method.getDeclaringClass().getName()} What it means
When the async prompt method declares a McpAsyncServerExchange parameter but the exchange object supplied at invocation is not a McpAsyncServerExchange (it is something else, or null), assignExchangeType throws this IllegalArgumentException. The declared parameter cannot be satisfied, so the invocation aborts with a message naming the actual exchange class (or "null").
Source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/prompt/AsyncMcpPromptMethodCallback.java:83
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());
}
else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
return asyncServerExchange.transportContext();
}
}
else if (McpAsyncServerExchange.class.isAssignableFrom(paramType)) {
if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
return asyncServerExchange;
}
throw new IllegalArgumentException(
"Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ " for Async method: " + method.getName() + " in " + method.getDeclaringClass().getName());
}
throw new IllegalArgumentException(
"Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
+ " for method: " + method.getName() + " in " + method.getDeclaringClass().getName());
}
/**
* Apply the callback to the given exchange and request.
* <p>
* This method builds the arguments for the method call, invokes the method, and
* converts the result to a GetPromptResult.
* @param exchange The server exchange, may be null if the method doesn't require it
* @param request The prompt request, must not be null
* @return A Mono that emits the prompt result
* @throws McpError if there is an error invoking the prompt methodView on GitHub (pinned to 98a7beda4f)
Solutions
- Ensure the async callback builder has an exchange set (builder.exchange(asyncServerExchange)) before use.
- Pass a McpAsyncServerExchange when invoking the callback; construct one from the async server if needed.
- If a sync exchange is what you actually have, declare McpSyncServerExchange and use the sync callback variant.
Example fix
// before
var callback = AsyncMcpPromptMethodCallback.builder().method(m).bean(b).prompt(p).build(); // no exchange
// after
var callback = AsyncMcpPromptMethodCallback.builder().method(m).bean(b).prompt(p)
.exchange(asyncServerExchange).build(); Defensive patterns
Strategy: type-guard
Validate before calling
if (exchange == null) {
throw new IllegalStateException("Async callback requires a non-null McpAsyncServerExchange");
} Type guard
boolean hasAsyncExchange(Object exchange) { return exchange instanceof McpAsyncServerExchange; } Try / catch
try {
return callback.apply(exchange, request);
} catch (IllegalArgumentException e) {
if (e.getMessage() != null && e.getMessage().contains("Unsupported exchange type")) {
log.error("Exchange missing or wrong type for async prompt callback", e);
throw new IllegalStateException("Provide a McpAsyncServerExchange", e);
}
throw e;
} Prevention
- Always call .exchange(...) on AsyncMcpPromptMethodCallback.builder() before build().
- In tests, construct a real or mocked McpAsyncServerExchange, not null.
- Guard apply() call sites with an instanceof check on the exchange.
When it happens
Trigger: Method declares McpAsyncServerExchange but buildArgs passes a null or foreign exchange object — e.g. the callback was invoked with no exchange set on the builder, or a custom caller supplied a McpSyncServerExchange/McpTransportContext instead.
Common situations: Forgetting to call .exchange(...) on the callback builder in programmatic registration; a test harness invoking apply() without an exchange; sync/async wiring mixups where a sync exchange reaches an async handler.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- Async prompt method must not declare parameter of type: {par
- Unsupported Async exchange type: {syncServerExchange.getClas
- Unsupported exchange type: {exchange != null ? exchange.getC
- Unsupported exchange type: {exchangeClassName|null} for Sync
- Method must not be null
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/5b637ecf6dd6910f.
Report an issue: GitHub.