spring-projects/spring-ai · error · IllegalArgumentException

Stateless Streamable-Http prompt method must not declare par

Error message

Stateless Streamable-Http prompt method must not declare parameter of type: ${paramType}. Use McpTransportContext instead. Method: ${method} in ${declaringClass}

What it means

Thrown by AsyncStatelessMcpResourceMethodCallback.validateParamType when a stateless Streamable-Http resource method declares an McpSyncServerExchange or McpAsyncServerExchange parameter. Stateless mode has no per-session exchange, so session-bound exchange types are illegal; only McpTransportContext is accepted.

Source

Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AsyncStatelessMcpResourceMethodCallback.java:66

 * @author Vadzim Shurmialiou
 * @author Craig Walls
 */
public final class AsyncStatelessMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback
		implements BiFunction<McpTransportContext, ReadResourceRequest, Mono<ReadResourceResult>> {

	private AsyncStatelessMcpResourceMethodCallback(Builder builder) {
		super(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,
				builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType, builder.meta);
		this.validateMethod(this.method);
	}

	@Override
	protected void validateParamType(Class<?> paramType) {

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

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Replace the exchange parameter with McpTransportContext in the annotated method.
  2. Drop the exchange parameter entirely if no transport metadata is needed.
  3. If per-session state is required, run a stateful server instead of the stateless Streamable-Http one.

Example fix

// before
@McpResource(uri = "cfg://{key}")
public ReadResourceResult get(McpAsyncServerExchange ex, String key) { ... }

// after
@McpResource(uri = "cfg://{key}")
public ReadResourceResult get(McpTransportContext ctx, String key) { ... }
Defensive patterns

Strategy: validation

Validate before calling

Arrays.stream(method.getParameterTypes())
    .filter(p -> McpSyncServerExchange.class.isAssignableFrom(p)
              || McpAsyncServerExchange.class.isAssignableFrom(p))
    .findAny()
    .ifPresent(p -> { throw new IllegalStateException("Stateless resource method must not take " + p); });

Type guard

static boolean isSessionExchange(Class<?> p) {
    return McpSyncServerExchange.class.isAssignableFrom(p) || McpAsyncServerExchange.class.isAssignableFrom(p);
}

Try / catch

try {
    statelessServer.annotate(provider);
} catch (IllegalArgumentException e) {
    throw new IllegalStateException("Stateless providers must use McpTransportContext, not exchange types", e);
}

Prevention

When it happens

Trigger: Building a stateless Streamable-Http MCP server and registering an @McpResource method whose signature includes either exchange type; validation fires when the stateless callback is constructed.

Common situations: Reusing session-based resource methods in a stateless HTTP deployment (e.g., serverless/ horizontally scaled backends); switching the server builder to stateless without updating signatures.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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