{"record":{"id":"fb259cadb9f7d2bb","repo":"spring-projects/spring-ai","slug":"unsupported-exchange-type-exchangeclassname-null-fb259c","errorCode":null,"errorMessage":"Unsupported exchange type: {exchangeClassName|null} for method: {methodName} in {className}","messagePattern":"Unsupported exchange type: (.+?) for 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":88,"sourceCode":"\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: \"\n\t\t\t\t\t\t+ asyncServerExchange.getClass().getName() + \" for Sync method: \" + method.getName() + \" in \"\n\t\t\t\t\t\t+ method.getDeclaringClass().getName());\n\t\t\t}\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 context and request.\n\t * <p>\n\t * This method extracts URI variable values from the request URI, builds the arguments\n\t * for the method call, invokes the method, and converts the result to a\n\t * ReadResourceResult.\n\t * @param context The transport context, may be null if the method doesn't require it\n\t * @param request The resource request, must not be null\n\t * @return The resource result\n\t * @throws McpError if there is an error invoking the resource method\n\t * @throws IllegalArgumentException if the request is null or if URI variable\n\t * extraction fails\n\t */\n\t@Override","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/resource/SyncStatelessMcpResourceMethodCallback.java#L70-L106","documentation":"Thrown by assignExchangeType when an MCP annotated resource method is declared sync but the framework injects an exchange object of an unsupported (async) class, or the exchange is null and no matching parameter binding applies. The library distinguishes Sync vs Async callbacks and only allows the exchange type matching the callback flavor.","triggerScenarios":"Registering a resource method whose exchange parameter is an async type (e.g. McpAsyncServerExchange or a reactive exchange, or an unexpected type/null) on a sync stateless resource callback (SyncStatelessMcpResourceMethodCallback).","commonSituations":"Mixing sync and async MCP APIs: annotating a method with an async exchange parameter but registering the bean/handler via the sync server builder; passing null exchange in tests; version drift where a new exchange class is not in the supported list.","solutions":["Change the method's exchange parameter to the sync exchange type matching the callback (or remove the parameter if not needed)","Register the method/callback on the async server builder if the method uses an async exchange type","Check that the exchange is non-null and of the exact class the sync callback supports (inspect the class name in the message)","Align library versions so the supported exchange classes match those your code imports"],"exampleFix":"// before\n@McpResource(uri = \"file://{path}\")\npublic String read(McpAsyncServerExchange exchange, String path) { ... }\n// after\n@McpResource(uri = \"file://{path}\")\npublic String read(McpSyncServerExchange exchange, String path) { ... }","handlingStrategy":"validation","validationCode":"boolean ok = java.util.stream.Stream.of(method.getParameterTypes())\n    .allMatch(t -> !McpAsyncServerExchange.class.isAssignableFrom(t));\nif (!ok) throw new IllegalStateException(\"Sync resource method must not take an async exchange\");","typeGuard":"boolean isSyncExchange(Object ex) {\n    return ex instanceof McpSyncServerExchange;\n}","tryCatchPattern":"try {\n    return callback.apply(context, request);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Unsupported exchange type\")) {\n        // fall back to async callback or re-register method with sync exchange\n    }\n    throw e;\n}","preventionTips":["Keep exchange parameter types consistent with sync vs async server builders","Review method signatures at registration; the failure occurs at startup, not per request","Write a startup smoke test that constructs all callbacks"],"tags":["mcp","java","sync-async","exchange-type"],"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"}