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

Unsupported Sync exchange type: {syncServerExchange.getClass

Error message

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

What it means

When an async stateless prompt method declares an McpTransportContext parameter, assignExchangeType converts the runtime exchange object into that context. A McpSyncServerExchange arriving at an async (stateless) method is a Sync exchange, which is not convertible, so IllegalArgumentException is thrown indicating the mismatch.

Source

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

		if (McpSyncServerExchange.class.isAssignableFrom(paramType)
				|| McpAsyncServerExchange.class.isAssignableFrom(paramType)) {

			throw new IllegalArgumentException(
					"Stateless Streamable-Http prompt method must not declare parameter of type: " + paramType.getName()
							+ ". Use McpTransportContext 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 Sync exchange type: "
						+ syncServerExchange.getClass().getName() + " for Sync method: " + method.getName() + " in "
						+ method.getDeclaringClass().getName());

			}
			else if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {
				return asyncServerExchange.transportContext();
			}
		}

		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 context and request.
	 * <p>
	 * This method builds the arguments for the method call, invokes the method, and

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Register the annotated bean with the async MCP server so it receives async exchanges/transport contexts
  2. Use separate beans (or separate callbacks) for sync and async servers
  3. Verify the server type (McpAsyncServer vs McpSyncServer) matches the callback type

Example fix

// before
McpServer.sync(mcpTransportProvider).prompts(...).build(); // with async stateless callback
// after
McpServer.async(mcpStreamableServerTransportProvider).prompts(...).build();
Defensive patterns

Strategy: validation

Validate before calling

assert server instanceof McpAsyncServer : "Async stateless callbacks require an async server";

Type guard

boolean compatible(Object exchange) { return exchange instanceof McpTransportContext || exchange instanceof McpAsyncServerExchange; }

Try / catch

try { result = callback.apply(exchange, request); } catch (IllegalArgumentException e) { throw new McpError("Exchange/server mismatch: " + e.getMessage()); }

Prevention

When it happens

Trigger: A method declaring McpTransportContext is invoked, but the callback receives a McpSyncServerExchange as the exchange argument — e.g. the method callback is wired into a sync server dispatch path.

Common situations: Mixing sync and async MCP servers with the same annotated bean; registering an async stateless callback on a sync server transport.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


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