{"record":{"id":"618824c5f724f8c1","repo":"spring-projects/spring-ai","slug":"sync-complete-methods-should-use-mcpsyncrequestcon","errorCode":null,"errorMessage":"Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter: {method} in {class}","messagePattern":"Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext 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":219,"sourceCode":"\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}\n\t\t\telse if (McpTransportContext.class.isAssignableFrom(paramType)) {\n\t\t\t\tif (hasTransportContext) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"Method cannot have more than one transport 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\thasTransportContext = true;\n\t\t\t}\n\t\t\telse if (isExchangeType(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":201,"sourceCodeEnd":237,"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#L201-L237","documentation":"Thrown when a @McpComplete method has a non-reactive (sync) return type but declares an McpAsyncRequestContext parameter. Sync completions must use McpSyncRequestContext; the async context is reserved for reactive return types (Mono/Flux).","triggerScenarios":"Declaring a completion method returning String/CompleteResult (not Mono/Flux) while taking McpAsyncRequestContext as a parameter.","commonSituations":"Copying an async completion example into a sync server setup; switching the return type from Mono to a plain type during a refactor without changing the context parameter.","solutions":["Replace the McpAsyncRequestContext parameter with McpSyncRequestContext","Or make the return type reactive (Mono/Flux) to keep McpAsyncRequestContext","Rebuild and re-register the callback"],"exampleFix":"// before\n@McpComplete(uri=\"completion://{lang}\")\npublic String complete(McpAsyncRequestContext ctx, CompleteRequest req) { ... }\n// after\n@McpComplete(uri=\"completion://{lang}\")\npublic String complete(McpSyncRequestContext ctx, CompleteRequest req) { ... }","handlingStrategy":"validation","validationCode":"boolean reactive = Mono.class.isAssignableFrom(m.getReturnType()) || Flux.class.isAssignableFrom(m.getReturnType());\nif (!reactive && Arrays.stream(m.getParameterTypes()).anyMatch(McpAsyncRequestContext.class::isAssignableFrom))\n    throw new IllegalStateException(\"Sync method must use McpSyncRequestContext: \" + m);","typeGuard":"boolean syncMethodUsesSyncContext(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 : true;\n}","tryCatchPattern":"try {\n    callbackBuilder.build();\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().contains(\"Sync complete methods should use McpSyncRequestContext\")) { /* swap to McpSyncRequestContext */ }\n    throw e;\n}","preventionTips":["Mirror rule: async context only with Mono/Flux returns, sync context otherwise","When converting Mono<T> returns back to plain types, always update the context parameter too","Add a startup test that registers every completion method"],"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"}