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 method

View on GitHub (pinned to 98a7beda4f)

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.