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
- Replace the exchange parameter with McpTransportContext in the annotated method.
- Drop the exchange parameter entirely if no transport metadata is needed.
- 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
- In stateless deployments, always use McpTransportContext (or no exchange param)
- Separate session-based and stateless provider classes to avoid signature reuse
- Document that stateless mode has no per-session exchange
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
- Method must have exactly 1 parameter (List<McpSchema.Resourc
- Single parameter must be of type ElicitRequest:
- Currently only methods with a single ElicitRequest parameter
- Method must have either 1 parameter (LoggingMessageNotificat
- Method must have either 1 parameter (ProgressNotification) o
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/49fecea6967851f4.
Report an issue: GitHub.