{"record":{"id":"893bf37c5019c517","repo":"spring-projects/spring-ai","slug":"async-complete-methods-should-use-mcpasyncrequestc","errorCode":null,"errorMessage":"Async complete methods should use McpAsyncRequestContext instead of McpSyncRequestContext parameter: {method} in {class}","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/complete/AbstractMcpCompleteMethodCallback.java","lineNumber":206,"sourceCode":"\t\t\t}\n\n\t\t\t// Skip McpMeta parameters from validation\n\t\t\tif (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\tcontinue;\n\t\t\t}\n\n\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\"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\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\"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}","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/complete/AbstractMcpCompleteMethodCallback.java#L188-L224","documentation":"Thrown when a @McpComplete-annotated method has a reactive (async) return type (e.g. Mono/Flux) but declares an McpSyncRequestContext parameter. Spring AI MCP requires the context type to match the method's sync/async style: reactive completions must use McpAsyncRequestContext. The mismatch is detected during builder validation via validateParameters().","triggerScenarios":"Declaring a completion method returning Mono<CompleteResult>/Flux while taking McpSyncRequestContext as a parameter; registering such a method through the AbstractMcpCompleteMethodCallback builder.","commonSituations":"Developers copy a sync completion example and change only the return type to Mono/Flux (or vice versa after a refactor); migrating code between sync and async MCP server setups and forgetting to swap the context parameter type.","solutions":["Replace the McpSyncRequestContext parameter with McpAsyncRequestContext","Or change the return type to a non-reactive type (e.g. String or CompleteResult) to keep McpSyncRequestContext","Rebuild/re-register the completion callback after fixing the signature"],"exampleFix":"// before\n@McpComplete(prompt=\"language\")\npublic Mono<CompleteResult> complete(McpSyncRequestContext ctx, CompleteRequest req) { ... }\n// after\n@McpComplete(prompt=\"language\")\npublic Mono<CompleteResult> complete(McpAsyncRequestContext ctx, CompleteRequest req) { ... }","handlingStrategy":"validation","validationCode":"boolean isReactive = Mono.class.isAssignableFrom(m.getReturnType()) || Flux.class.isAssignableFrom(m.getReturnType());\nboolean hasSyncCtx = Arrays.stream(m.getParameterTypes()).anyMatch(McpSyncRequestContext.class::isAssignableFrom);\nboolean hasAsyncCtx = Arrays.stream(m.getParameterTypes()).anyMatch(McpAsyncRequestContext.class::isAssignableFrom);\nif (isReactive && hasSyncCtx || !isReactive && hasAsyncCtx) throw new IllegalStateException(\"Context type must match return type: \" + m);","typeGuard":"boolean contextMatchesReturn(Method m) {\n  boolean reactive = Mono.class.isAssignableFrom(m.getReturnType()) || Flux.class.isAssignableFrom(m.getReturnType());\n  boolean asyncCtx = Arrays.stream(m.getParameterTypes()).anyMatch(McpAsyncRequestContext.class::isAssignableFrom);\n  return reactive == asyncCtx;\n}","tryCatchPattern":"try {\n    builder.method(m).bean(bean).build();\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"McpAsyncRequestContext\")) { /* fix signature: swap context param type */ }\n    throw e;\n}","preventionTips":["Pick the context type AFTER fixing the return type: Mono/Flux -> McpAsyncRequestContext, plain -> McpSyncRequestContext","Write a unit test that builds all @McpComplete callbacks at startup so mismatches fail fast","Keep sync and async completion examples in separate packages to avoid copy-paste drift"],"tags":["mcp","method-validation","reactive","request-context"],"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"}