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 method: {method.getName()} in {method.getDeclaringClass().getName()}

What it means

The fall-through case of AsyncMcpPromptMethodCallback.assignExchangeType: the parameter type is not any supported exchange/context type, yet an exchange binding was attempted. The callback throws this IllegalArgumentException naming the runtime exchange class (or "null") and the method, indicating the parameter can never receive the exchange. Note the wording says "for method" (not "for Async method") — this is the terminal, unsupported-parameter branch.

Source

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

						+ 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
	 * @throws IllegalArgumentException if the request is null
	 */
	@Override
	public Mono<GetPromptResult> apply(McpAsyncServerExchange exchange, GetPromptRequest request) {
		if (request == null) {

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Declare the parameter as one of the supported types: McpAsyncServerExchange or McpTransportContext (async) / McpSyncServerExchange (sync).
  2. If you subclass the callback, make assignExchangeType handle every type your isSupportedExchangeOrContextType accepts.
  3. Remove the stray parameter and pass equivalent data via McpMeta, McpTransportContext, or method-local logic.

Example fix

// before (custom predicate accepts MyContext but assignExchangeType does not)
protected boolean isSupportedExchangeOrContextType(Class<?> t) { return MyContext.class.isAssignableFrom(t) || super.isSupportedExchangeOrContextType(t); }
// after
@Override
protected Object assignExchangeType(Class<?> paramType, Object exchange) {
    if (MyContext.class.isAssignableFrom(paramType) && exchange instanceof MyContext mc) return mc;
    return super.assignExchangeType(paramType, exchange);
}
Defensive patterns

Strategy: validation

Validate before calling

for (Parameter p : method.getParameters()) {
    Class<?> t = p.getType();
    boolean supported = McpAsyncServerExchange.class.isAssignableFrom(t)
        || McpTransportContext.class.isAssignableFrom(t)
        || McpSyncServerExchange.class.isAssignableFrom(t);
    // custom exchange types must be handled by an overridden assignExchangeType
    if (!supported && !isHandledByCustomAssignExchange(t)) {
        throw new IllegalStateException("Parameter type cannot receive exchange: " + t + " in " + method);
    }
}

Type guard

boolean isRecognizedExchangeParam(Class<?> t) {
    return McpAsyncServerExchange.class.isAssignableFrom(t)
        || McpSyncServerExchange.class.isAssignableFrom(t)
        || McpTransportContext.class.isAssignableFrom(t);
}

Try / catch

try {
    return callback.apply(exchange, request);
} catch (IllegalArgumentException e) {
    if (e.getMessage() != null && e.getMessage().contains("Unsupported exchange type")) {
        throw new IllegalStateException("Handler signature declares an unsupported exchange-like parameter", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: A prompt method declares a parameter whose type is none of McpTransportContext/McpSyncServerExchange/McpAsyncServerExchange but the argument-building path routes it to assignExchangeType — typically caused by a custom subclass overriding isSupportedExchangeOrContextType to accept a type that assignExchangeType does not actually know how to build.

Common situations: Extending AbstractMcpPromptMethodCallback/AsyncMcpPromptMethodCallback with a custom supported-exchange predicate that is out of sync with assignExchangeType's instanceof chain; framework version upgrades that changed the recognized context types.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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