{"record":{"id":"296f5e997de5ba0d","repo":"spring-projects/spring-ai","slug":"unsupported-exchange-type-exchange-null-exc-296f5e","errorCode":null,"errorMessage":"Unsupported exchange type: {exchange != null ? exchange.getClass().getName() : \"null\"} for method: {method.getName()} in {method.getDeclaringClass().getName()}","messagePattern":"Unsupported exchange type: (.+?) for method: (.+?) in (.+?)","errorType":"validation","errorClass":"java.lang.IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/prompt/AsyncMcpPromptMethodCallback.java","lineNumber":88,"sourceCode":"\t\t\t\t\t\t+ syncServerExchange.getClass().getName() + \" for Async method: \" + method.getName() + \" in \"\n\t\t\t\t\t\t+ method.getDeclaringClass().getName());\n\n\t\t\t}\n\t\t\telse if (exchange instanceof McpAsyncServerExchange asyncServerExchange) {\n\t\t\t\treturn asyncServerExchange.transportContext();\n\t\t\t}\n\t\t}\n\t\telse if (McpAsyncServerExchange.class.isAssignableFrom(paramType)) {\n\t\t\tif (exchange instanceof McpAsyncServerExchange asyncServerExchange) {\n\t\t\t\treturn asyncServerExchange;\n\t\t\t}\n\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\"Unsupported exchange type: \" + (exchange != null ? exchange.getClass().getName() : \"null\")\n\t\t\t\t\t\t\t+ \" for Async method: \" + method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\n\t\t\t\t\"Unsupported exchange type: \" + (exchange != null ? exchange.getClass().getName() : \"null\")\n\t\t\t\t\t\t+ \" for method: \" + method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t}\n\n\t/**\n\t * Apply the callback to the given exchange and request.\n\t * <p>\n\t * This method builds the arguments for the method call, invokes the method, and\n\t * converts the result to a GetPromptResult.\n\t * @param exchange The server exchange, may be null if the method doesn't require it\n\t * @param request The prompt request, must not be null\n\t * @return A Mono that emits the prompt result\n\t * @throws McpError if there is an error invoking the prompt method\n\t * @throws IllegalArgumentException if the request is null\n\t */\n\t@Override\n\tpublic Mono<GetPromptResult> apply(McpAsyncServerExchange exchange, GetPromptRequest request) {\n\t\tif (request == null) {","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/prompt/AsyncMcpPromptMethodCallback.java#L70-L106","documentation":"The fall-through case of AsyncMcpPromptMethodCallback.assignExchangeType: the parameter type is not any supported exchange/context type, yet an exchange binding was attempted. The callback throws this IllegalArgumentException naming the runtime exchange class (or \"null\") and the method, indicating the parameter can never receive the exchange. Note the wording says \"for method\" (not \"for Async method\") — this is the terminal, unsupported-parameter branch.","triggerScenarios":"A prompt method declares a parameter whose type is none of McpTransportContext/McpSyncServerExchange/McpAsyncServerExchange but the argument-building path routes it to assignExchangeType — typically caused by a custom subclass overriding isSupportedExchangeOrContextType to accept a type that assignExchangeType does not actually know how to build.","commonSituations":"Extending AbstractMcpPromptMethodCallback/AsyncMcpPromptMethodCallback with a custom supported-exchange predicate that is out of sync with assignExchangeType's instanceof chain; framework version upgrades that changed the recognized context types.","solutions":["Declare the parameter as one of the supported types: McpAsyncServerExchange or McpTransportContext (async) / McpSyncServerExchange (sync).","If you subclass the callback, make assignExchangeType handle every type your isSupportedExchangeOrContextType accepts.","Remove the stray parameter and pass equivalent data via McpMeta, McpTransportContext, or method-local logic."],"exampleFix":"// before (custom predicate accepts MyContext but assignExchangeType does not)\nprotected boolean isSupportedExchangeOrContextType(Class<?> t) { return MyContext.class.isAssignableFrom(t) || super.isSupportedExchangeOrContextType(t); }\n// after\n@Override\nprotected Object assignExchangeType(Class<?> paramType, Object exchange) {\n    if (MyContext.class.isAssignableFrom(paramType) && exchange instanceof MyContext mc) return mc;\n    return super.assignExchangeType(paramType, exchange);\n}","handlingStrategy":"validation","validationCode":"for (Parameter p : method.getParameters()) {\n    Class<?> t = p.getType();\n    boolean supported = McpAsyncServerExchange.class.isAssignableFrom(t)\n        || McpTransportContext.class.isAssignableFrom(t)\n        || McpSyncServerExchange.class.isAssignableFrom(t);\n    // custom exchange types must be handled by an overridden assignExchangeType\n    if (!supported && !isHandledByCustomAssignExchange(t)) {\n        throw new IllegalStateException(\"Parameter type cannot receive exchange: \" + t + \" in \" + method);\n    }\n}","typeGuard":"boolean isRecognizedExchangeParam(Class<?> t) {\n    return McpAsyncServerExchange.class.isAssignableFrom(t)\n        || McpSyncServerExchange.class.isAssignableFrom(t)\n        || McpTransportContext.class.isAssignableFrom(t);\n}","tryCatchPattern":"try {\n    return callback.apply(exchange, request);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"Unsupported exchange type\")) {\n        throw new IllegalStateException(\"Handler signature declares an unsupported exchange-like parameter\", e);\n    }\n    throw e;\n}","preventionTips":["When subclassing the callback, keep isSupportedExchangeOrContextType and assignExchangeType in sync.","Only use framework-recognized exchange/context parameter types.","After framework upgrades, re-run signature validation tests for custom callback extensions."],"tags":["mcp","async","exchange-type","unsupported-parameter"],"backgroundTag":"unsupported-operation","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"}