{"record":{"id":"4664099e1cc34311","repo":"spring-projects/spring-ai","slug":"async-prompt-method-must-not-declare-parameter-of-466409","errorCode":null,"errorMessage":"Async prompt method must not declare parameter of type: ${paramType}. Use McpAsyncServerExchange instead. Method: ${method} in ${declaringClass}","messagePattern":"Async prompt method must not declare parameter of type: (.+?)\\. Use McpAsyncServerExchange 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/AsyncMcpResourceMethodCallback.java","lineNumber":64,"sourceCode":" * @author Christian Tzolov\n * @author Alexandros Pappas\n * @author Vadzim Shurmialiou\n * @author Craig Walls\n */\npublic final class AsyncMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback\n\t\timplements BiFunction<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> {\n\n\tprivate AsyncMcpResourceMethodCallback(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\tthrow new IllegalArgumentException(\"Async prompt method must not declare parameter of type: \"\n\t\t\t\t\t+ paramType.getName() + \". Use McpAsyncServerExchange instead.\" + \" Method: \"\n\t\t\t\t\t+ this.method.getName() + \" in \" + 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 Async exchange type: \"\n\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}","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/AsyncMcpResourceMethodCallback.java#L46-L82","documentation":"This IllegalArgumentException is thrown by AsyncMcpResourceMethodCallback.validateParamType when an @McpResource-annotated method that runs in async mode declares a parameter of type McpSyncServerExchange (or a subtype). Async callbacks are wired to McpAsyncServerExchange, so a sync exchange parameter can never be satisfied and Spring AI MCP rejects the method up front. Use McpAsyncServerExchange (or McpTransportContext) instead.","triggerScenarios":"Registering an @McpResource method on an async MCP server (McpAsyncServer / async spec builder) whose signature includes a McpSyncServerExchange parameter; this validation runs during callback construction at registration time.","commonSituations":"Migrating an app from a sync MCP server to an async one and copying the old method signatures; copy-pasting example code from sync-based docs; sharing a resource class between both server types.","solutions":["Change the parameter type from McpSyncServerExchange to McpAsyncServerExchange in the annotated method.","If no session state is needed, use McpTransportContext as the exchange parameter, which works in both sync and async modes.","Ensure the resource method is registered against the server type matching its exchange parameter (sync exchange -> sync server)."],"exampleFix":"// before\n@McpResource(uri = \"docs://{id}\")\npublic ReadResourceResult read(McpSyncServerExchange exchange, String id) { ... }\n\n// after\n@McpResource(uri = \"docs://{id}\")\npublic ReadResourceResult read(McpAsyncServerExchange exchange, String id) { ... }","handlingStrategy":"validation","validationCode":"// Fail fast in a startup test\nStream.of(resourceProvider.getClass().getDeclaredMethods())\n    .filter(m -> m.isAnnotationPresent(McpResource.class))\n    .forEach(m -> Arrays.stream(m.getParameterTypes())\n        .filter(p -> McpSyncServerExchange.class.isAssignableFrom(p))\n        .findAny()\n        .ifPresent(p -> { throw new IllegalStateException(\"Async resource method \" + m + \" must not take \" + p); }));","typeGuard":"static boolean isSyncExchangeParam(Class<?> p) { return McpSyncServerExchange.class.isAssignableFrom(p); }","tryCatchPattern":"try {\n    asyncMcpServer.annotate(resourceProvider);\n} catch (IllegalArgumentException e) {\n    log.error(\"Bad resource method signature: {}\", e.getMessage());\n    throw new ConfigurationException(e);\n}","preventionTips":["Never write McpSyncServerExchange in methods registered on async servers","Prefer McpTransportContext when session state is not needed","Add a startup test that registers all annotated providers","Keep sync and async providers in separate packages/classes"],"tags":["mcp","java","async","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"}