spring-projects/spring-ai · error · java.lang.IllegalArgumentException

Unsupported exchange type

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").

Solutions

  1. Ensure the async callback builder has an exchange set (builder.exchange(asyncServerExchange)) before use.
  2. Pass a McpAsyncServerExchange when invoking the callback; construct one from the async server if needed.
  3. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/5b637ecf6dd6910f. Report an issue: GitHub.

Appendix: 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 method

View on GitHub (pinned to 98a7beda4f)