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

Unsupported Async exchange type: {syncServerExchange.getClas

Error message

Unsupported Async exchange type: {syncServerExchange.getClass().getName()} for Async method: {method.getName()} in {method.getDeclaringClass().getName()}

What it means

In AsyncMcpPromptMethodCallback.assignExchangeType, when a method parameter is a McpTransportContext but the runtime exchange passed by the server is a McpSyncServerExchange, the callback cannot supply a transport context from a sync exchange and throws this IllegalArgumentException at invocation time. It indicates a sync/async mismatch: an async-declared method is being driven by a sync server exchange.

Source

Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/prompt/AsyncMcpPromptMethodCallback.java:69

	@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());

			}
			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());
		}

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Register the method with the correct callback variant: async methods only on McpAsyncServer / async annotation processor.
  2. Change the parameter to McpAsyncServerExchange if the surrounding server is sync-driven, or vice versa.
  3. Audit duplicated registrations so the same @McpPrompt method is not wired to both sync and async servers.

Example fix

// before (registered on sync server)
@McpPrompt(name = "report")
public Mono<String> report(McpTransportContext ctx) { ... }
// after (register with async server, or change parameter)
@McpPrompt(name = "report")
public Mono<String> report(McpAsyncServerExchange exchange) { ... }
Defensive patterns

Strategy: validation

Validate before calling

// before registering:
if (server instanceof McpSyncServer && usesTransportContextOrAsyncParams(method)) {
    throw new IllegalStateException("Method must be registered on the async server: " + method);
}

Type guard

boolean isSyncExchange(Object exchange) { return exchange instanceof McpSyncServerExchange; }

Try / catch

try {
    return callback.apply(exchange, request);
} catch (IllegalArgumentException e) {
    if (e.getMessage() != null && e.getMessage().contains("Unsupported Async exchange type")) {
        throw new IllegalStateException("Method registered with wrong server variant (sync vs async)", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: An async prompt method declares `McpTransportContext ctx` but the callback is invoked with a McpSyncServerExchange instance (e.g. the method was registered on both a sync and async server, or the sync callback dispatched it).

Common situations: Registering the same prompt bean with both McpSyncServer and McpAsyncServer; mixing sync/async annotation processors over the same bean; refactoring a sync handler to async parameter types without changing server wiring.

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/e248d9772310090a. Report an issue: GitHub.