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

What it means

When a sync prompt method declares an McpSyncServerExchange parameter, assignExchangeType requires the runtime exchange to actually be a McpSyncServerExchange. Any other (or null) exchange reaches the fallback throw, indicating the callback was invoked from an incompatible server path.

Source

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

		if (McpTransportContext.class.isAssignableFrom(paramType)) {
			if (exchange instanceof McpTransportContext transportContext) {
				return transportContext;
			}
			else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
				return syncServerExchange.transportContext();
			}
			else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
				throw new IllegalArgumentException("Unsupported Async exchange type: "
						+ asyncServerExchange.getClass().getName() + " for Sync method: " + method.getName() + " in "
						+ method.getDeclaringClass().getName());
			}
		}
		else if (McpSyncServerExchange.class.isAssignableFrom(paramType)) {
			if (exchange instanceof McpSyncServerExchange syncServerExchange) {
				return syncServerExchange;
			}

			throw new IllegalArgumentException(
					"Unsupported exchange type: " + (exchange != null ? exchange.getClass().getName() : "null")
							+ " for Sync 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 The prompt result
	 * @throws McpError if there is an error invoking the prompt method

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Ensure the sync server supplies a McpSyncServerExchange to the callback
  2. Register the callback only with a sync MCP server
  3. If the exchange is null, fix the dispatch/transport configuration so a real exchange is passed

Example fix

// before
asyncCallback applied on sync path with McpTransportContext
// after
syncCallback applied on sync server path receiving McpSyncServerExchange
Defensive patterns

Strategy: type-guard

Validate before calling

if (exchange == null || !(exchange instanceof McpSyncServerExchange)) throw new IllegalStateException("expected McpSyncServerExchange");

Type guard

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

Try / catch

try { r = callback.apply(exchange, req); } catch (IllegalArgumentException e) { log.error("Bad exchange {} for sync prompt", exchange, e); }

Prevention

When it happens

Trigger: A method expecting McpSyncServerExchange is invoked with a different exchange object (async exchange, transport context, or null).

Common situations: Custom or misconfigured transports calling the callback directly; swapping server implementations without re-registering callbacks; version mismatches between MCP modules.

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