{"record":{"id":"5ba0b7b8fc57b86c","repo":"spring-projects/spring-ai","slug":"async-complete-methods-should-use-mcpasyncrequestc-5ba0b7","errorCode":null,"errorMessage":"Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: ${method} in ${declaringClass}","messagePattern":"Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: (.+?) in (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AbstractMcpResourceMethodCallback.java","lineNumber":228,"sourceCode":"\t\t\tif (McpSyncRequestContext.class.isAssignableFrom(paramType)) {\n\t\t\t\tif (hasRequestContextParam) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"Method cannot have more than one request context parameter: \"\n\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t\t\t}\n\t\t\t\tif (McpPredicates.isReactiveReturnType.test(method)) {\n\t\t\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\t\t\"Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter: \"\n\t\t\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t\t\t}\n\t\t\t\thasRequestContextParam = true;\n\t\t\t}\n\t\t\telse if (McpAsyncRequestContext.class.isAssignableFrom(paramType)) {\n\t\t\t\tif (hasRequestContextParam) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"Method cannot have more than one request context parameter: \"\n\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t\t\t}\n\t\t\t\tif (McpPredicates.isNotReactiveReturnType.test(method)) {\n\t\t\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\t\t\"Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: \"\n\t\t\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t\t\t}\n\t\t\t\thasRequestContextParam = true;\n\t\t\t}\n\t\t\telse if (McpMeta.class.isAssignableFrom(paramType)) {\n\t\t\t\tif (hasMetaParam) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"Method cannot have more than one McpMeta parameter: \"\n\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t\t\t}\n\t\t\t\thasMetaParam = true;\n\t\t\t}\n\t\t\telse if (isExchangeOrContextType(paramType)) {\n\t\t\t\tif (hasExchangeParam) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"Method cannot have more than one exchange parameter: \"\n\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t\t\t}\n\t\t\t\thasExchangeParam = true;","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AbstractMcpResourceMethodCallback.java#L210-L246","documentation":"Thrown when a resource method takes an McpAsyncRequestContext parameter but its return type is not reactive (i.e. not a Mono/Flux-style reactive return). The async context only makes sense for async/reactive completion; a non-reactive return type indicates the developer should use McpSyncRequestContext. The framework rejects the mismatch at method-registration time.","triggerScenarios":"A @McpResource method (no URI variables) declares McpAsyncRequestContext while McpPredicates.isNotReactiveReturnType.test(method) is true — e.g. the method returns String/List/void instead of Mono/Flux.","commonSituations":"Converting a sync handler to async by changing only the parameter but not the return type; mixing blocking and reactive styles after a Spring AI version upgrade; copying an async example's signature onto a non-reactive method.","solutions":["Change the parameter to McpSyncRequestContext if the method returns a plain (non-reactive) type","Alternatively change the return type to a reactive type (e.g. Mono<String>) to match McpAsyncRequestContext","Keep exactly one context parameter and align it with the return type"],"exampleFix":"// before\n@McpResource(uri = \"doc://read\")\npublic String read(McpAsyncRequestContext ctx) { return \"data\"; }\n// after\n@McpResource(uri = \"doc://read\")\npublic String read(McpSyncRequestContext ctx) { return \"data\"; }","handlingStrategy":"validation","validationCode":"boolean hasAsyncCtx = Arrays.stream(method.getParameterTypes())\n    .anyMatch(McpAsyncRequestContext.class::isAssignableFrom);\nboolean reactive = Mono.class.isAssignableFrom(method.getReturnType())\n    || Flux.class.isAssignableFrom(method.getReturnType());\nif (hasAsyncCtx && !reactive) throw new IllegalStateException(method + \" uses McpAsyncRequestContext with a non-reactive return type\");","typeGuard":"static boolean isReactive(Method m) {\n  return Mono.class.isAssignableFrom(m.getReturnType()) || Flux.class.isAssignableFrom(m.getReturnType());\n}","tryCatchPattern":"try { provider.build(...); }\ncatch (IllegalArgumentException e) {\n  if (e.getMessage().contains(\"Async complete methods should use McpAsyncRequestContext\")) { /* align param and return type */ }\n  else throw e;\n}","preventionTips":["Pair McpAsyncRequestContext only with Mono/Flux return types","Pair McpSyncRequestContext only with plain return types","When converting a handler, change parameter and return type together"],"tags":["mcp","spring-ai","reactive","sync-async-mismatch"],"backgroundTag":"type-mismatch","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"}