{"record":{"id":"e5cbeaa8e294cb9c","repo":"spring-projects/spring-ai","slug":"sync-complete-methods-should-use-mcpsyncrequestcon-e5cbea","errorCode":null,"errorMessage":"Sync complete methods should use McpSyncRequestContext instead of McpAsyncRequestContext parameter: ${method} in ${declaringClass}","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/resource/AbstractMcpResourceMethodCallback.java","lineNumber":216,"sourceCode":"\t\tboolean hasRequestOrUriParam = false;\n\t\tboolean hasMetaParam = false;\n\t\tboolean hasRequestContextParam = false;\n\n\t\tfor (Parameter param : parameters) {\n\t\t\t// Skip @McpProgressToken annotated parameters\n\t\t\tif (param.isAnnotationPresent(McpProgressToken.class)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tClass<?> paramType = param.getType();\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\"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)) {","sourceCodeStart":198,"sourceCodeEnd":234,"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#L198-L234","documentation":"A sync resource method (non-reactive return type) declared an McpAsyncRequestContext parameter. Async request contexts only exist for reactive return types (Mono/Flux); combining a sync return type with an async context is contradictory, so validateParametersWithoutUriVariables throws IllegalArgumentException advising McpSyncRequestContext.","triggerScenarios":"Declaring @McpResource method with a plain/String/POJO return type but taking McpAsyncRequestContext; McpPredicates.isReactiveReturnType detects the sync return type and the validator throws on the mismatch.","commonSituations":"Mixing examples from async and sync resource documentation; migrating a method from reactive to sync style and forgetting to swap the context type; IDE auto-complete picking the async context class.","solutions":["Replace McpAsyncRequestContext with McpSyncRequestContext in the method signature.","Alternatively, make the return type reactive (Mono/Flux) and use the async callback variant if async semantics are needed.","Align context type and callback type: sync callback + sync context, async callback + async context."],"exampleFix":"// before\n@McpResource(uri = \"file:///docs/{name}\")\npublic String read(McpAsyncRequestContext ctx, String name) { ... }\n\n// after\n@McpResource(uri = \"file:///docs/{name}\")\npublic String read(McpSyncRequestContext ctx, String name) { ... }","handlingStrategy":"validation","validationCode":"static boolean contextMatchesReturnType(Method m) {\n    boolean reactiveReturn = reactor.core.publisher.Mono.class.isAssignableFrom(m.getReturnType())\n        || reactor.core.publisher.Flux.class.isAssignableFrom(m.getReturnType());\n    boolean hasAsyncCtx = java.util.Arrays.stream(m.getParameterTypes())\n        .anyMatch(t -> McpAsyncRequestContext.class.isAssignableFrom(t));\n    return !hasAsyncCtx || reactiveReturn;\n}","typeGuard":null,"tryCatchPattern":"try {\n    resourceManager.register(callback);\n} catch (IllegalArgumentException e) {\n    throw new IllegalStateException(\"Sync resource method must use McpSyncRequestContext\", e);\n}","preventionTips":["Pair sync return types with McpSyncRequestContext and reactive return types with McpAsyncRequestContext.","When converting a method from reactive to sync style, swap the context parameter too.","Add a signature-lint test that checks context type vs return type across all @McpResource methods."],"tags":["mcp","java","sync-async-mismatch","request-context","resource-method"],"backgroundTag":"type-mismatch","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"}