{"record":{"id":"49fecea6967851f4","repo":"spring-projects/spring-ai","slug":"stateless-streamable-http-prompt-method-must-not-d-49fece","errorCode":null,"errorMessage":"Stateless Streamable-Http prompt method must not declare parameter of type: ${paramType}. Use McpTransportContext instead. Method: ${method} in ${declaringClass}","messagePattern":"Stateless Streamable-Http prompt method must not declare parameter of type: (.+?)\\. Use McpTransportContext instead\\. Method: (.+?) in (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AsyncStatelessMcpResourceMethodCallback.java","lineNumber":66,"sourceCode":" * @author Vadzim Shurmialiou\n * @author Craig Walls\n */\npublic final class AsyncStatelessMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback\n\t\timplements BiFunction<McpTransportContext, ReadResourceRequest, Mono<ReadResourceResult>> {\n\n\tprivate AsyncStatelessMcpResourceMethodCallback(Builder builder) {\n\t\tsuper(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,\n\t\t\t\tbuilder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType, builder.meta);\n\t\tthis.validateMethod(this.method);\n\t}\n\n\t@Override\n\tprotected void validateParamType(Class<?> paramType) {\n\n\t\tif (McpSyncServerExchange.class.isAssignableFrom(paramType)\n\t\t\t\t|| McpAsyncServerExchange.class.isAssignableFrom(paramType)) {\n\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\"Stateless Streamable-Http prompt method must not declare parameter of type: \" + paramType.getName()\n\t\t\t\t\t\t\t+ \". Use McpTransportContext instead.\" + \" Method: \" + this.method.getName() + \" in \"\n\t\t\t\t\t\t\t+ this.method.getDeclaringClass().getName());\n\t\t}\n\t}\n\n\t@Override\n\tprotected Object assignExchangeType(Class<?> paramType, Object exchange) {\n\n\t\tif (McpTransportContext.class.isAssignableFrom(paramType)) {\n\t\t\tif (exchange instanceof McpTransportContext transportContext) {\n\t\t\t\treturn transportContext;\n\t\t\t}\n\t\t\telse if (exchange instanceof McpSyncServerExchange syncServerExchange) {\n\t\t\t\tthrow new IllegalArgumentException(\"Unsupported Sync exchange type: \"\n\t\t\t\t\t\t+ syncServerExchange.getClass().getName() + \" for Sync method: \" + method.getName() + \" in \"\n\t\t\t\t\t\t+ method.getDeclaringClass().getName());\n","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AsyncStatelessMcpResourceMethodCallback.java#L48-L84","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n@McpResource(uri = \"cfg://{key}\")\npublic ReadResourceResult get(McpAsyncServerExchange ex, String key) { ... }\n\n// after\n@McpResource(uri = \"cfg://{key}\")\npublic ReadResourceResult get(McpTransportContext ctx, String key) { ... }","handlingStrategy":"validation","validationCode":"Arrays.stream(method.getParameterTypes())\n    .filter(p -> McpSyncServerExchange.class.isAssignableFrom(p)\n              || McpAsyncServerExchange.class.isAssignableFrom(p))\n    .findAny()\n    .ifPresent(p -> { throw new IllegalStateException(\"Stateless resource method must not take \" + p); });","typeGuard":"static boolean isSessionExchange(Class<?> p) {\n    return McpSyncServerExchange.class.isAssignableFrom(p) || McpAsyncServerExchange.class.isAssignableFrom(p);\n}","tryCatchPattern":"try {\n    statelessServer.annotate(provider);\n} catch (IllegalArgumentException e) {\n    throw new IllegalStateException(\"Stateless providers must use McpTransportContext, not exchange types\", e);\n}","preventionTips":["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"],"tags":["mcp","java","stateless","http","method-signature"],"backgroundTag":"invalid-argument-value","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}