{"record":{"id":"ff412e58292bed8e","repo":"spring-projects/spring-ai","slug":"stateless-streamable-http-prompt-method-must-not-d-ff412e","errorCode":null,"errorMessage":"Stateless Streamable-Http prompt method must not declare parameter of type: {paramTypeName}. Use McpTransportContext instead. Method: {methodName} in {className}","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/SyncStatelessMcpResourceMethodCallback.java","lineNumber":64,"sourceCode":" * @author Vadzim Shurmialiou\n * @author Craig Walls\n */\npublic final class SyncStatelessMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback\n\t\timplements BiFunction<McpTransportContext, ReadResourceRequest, ReadResourceResult> {\n\n\tprivate SyncStatelessMcpResourceMethodCallback(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\treturn syncServerExchange.transportContext();\n\t\t\t}\n\t\t\telse if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {\n\t\t\t\tthrow new IllegalArgumentException(\"Unsupported Async exchange type: \"","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/SyncStatelessMcpResourceMethodCallback.java#L46-L82","documentation":"SyncStatelessMcpResourceMethodCallback.validateParamType rejects resource methods used in stateless Streamable-HTTP mode that declare McpSyncServerExchange or McpAsyncServerExchange parameters. Stateless handling has no server session/exchange; only McpTransportContext is available, so the method fails registration with IllegalArgumentException.","triggerScenarios":"Configuring an MCP server in stateless Streamable-HTTP mode while an @McpResource method signature includes either exchange type. Validation fires when the method callback is created.","commonSituations":"Reusing the same annotated resource class across stateful and stateless server setups, or migrating a stateful server to stateless mode for horizontal scaling without removing exchange parameters.","solutions":["Replace McpSyncServerExchange/McpAsyncServerExchange parameters with McpTransportContext.","Remove the exchange parameter entirely if the handler does not need transport metadata.","If exchange state is genuinely required, run the server in stateful mode instead of stateless Streamable-HTTP."],"exampleFix":"// before\n@McpResource(uri = \"data://{id}\")\npublic String get(McpSyncServerExchange exchange, String id) { ... }\n\n// after\n@McpResource(uri = \"data://{id}\")\npublic String get(McpTransportContext context, String id) { ... }","handlingStrategy":"type-guard","validationCode":"void checkStatelessParams(Class<?>[] paramTypes) {\n    for (Class<?> p : paramTypes) {\n        if (McpSyncServerExchange.class.isAssignableFrom(p) || McpAsyncServerExchange.class.isAssignableFrom(p))\n            throw new IllegalArgumentException(\"Stateless methods must use McpTransportContext, not \" + p.getName());\n    }\n}","typeGuard":"static boolean validStatelessParam(Class<?> p) {\n    return !McpSyncServerExchange.class.isAssignableFrom(p)\n        && !McpAsyncServerExchange.class.isAssignableFrom(p);\n}","tryCatchPattern":"try {\n    statelessServer.addResource(resource, callback);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"Use McpTransportContext instead\")) {\n        log.error(\"Stateless Streamable-HTTP handler cannot take server exchange; switch to McpTransportContext\", e);\n    } throw e;\n}","preventionTips":["Maintain separate resource classes for stateful and stateless server deployments.","Add an ArchUnit rule banning exchange parameters in packages registered with the stateless server.","When migrating to stateless mode, sweep all @McpResource signatures for exchange parameters first."],"tags":["mcp","stateless","streamable-http","method-validation","illegal-argument"],"backgroundTag":"incompatible-source-type","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}