{"record":{"id":"f167c4a94c4aa5fc","repo":"spring-projects/spring-ai","slug":"method-cannot-have-more-than-one-exchange-paramete-f167c4","errorCode":null,"errorMessage":"Method cannot have more than one exchange parameter: {method.getName()} in {method.getDeclaringClass().getName()}","messagePattern":"Method cannot have more than one exchange parameter: (.+?) in (.+?)","errorType":"validation","errorClass":"java.lang.IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/prompt/AbstractMcpPromptMethodCallback.java","lineNumber":173,"sourceCode":"\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 (isSupportedExchangeOrContextType(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;\n\t\t\t}\n\t\t\telse if (GetPromptRequest.class.isAssignableFrom(paramType)) {\n\t\t\t\tif (hasRequestParam) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"Method cannot have more than one GetPromptRequest parameter: \"\n\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t\t\t}\n\t\t\t\thasRequestParam = true;\n\t\t\t}\n\t\t\telse if (Map.class.isAssignableFrom(paramType)) {\n\t\t\t\tif (hasMapParam) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"Method cannot have more than one Map parameter: \"\n\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t\t\t}\n\t\t\t\thasMapParam = true;\n\t\t\t}","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/prompt/AbstractMcpPromptMethodCallback.java#L155-L191","documentation":"During prompt-method signature validation, Spring AI MCP's AbstractMcpPromptMethodCallback rejects methods that declare more than one exchange/context parameter recognized as a supported exchange type (e.g. McpSyncServerExchange/McpAsyncServerExchange/McpTransportContext depending on sync/async variant). The framework cannot decide which single exchange instance to bind, so it throws at callback registration time (constructor -> validateMethod -> validateParameters). This is a fail-fast guard: the method can never be invoked correctly.","triggerScenarios":"Registering an @McpPrompt-annotated method whose signature contains two parameters assignable to the supported exchange/context type, e.g. `myPrompt(McpSyncServerExchange ex1, McpSyncServerExchange ex2, ...)` or (in async variant) two McpTransportContext parameters. Thrown from validateParameters when isSupportedExchangeOrContextType(paramType) is true and hasExchangeParam is already true.","commonSituations":"Copy-pasting an exchange parameter while adding a transport context; refactoring to add an extra exchange for progress/cancellation without realizing the framework injects it automatically; merging two handler methods into one and keeping both exchange parameters.","solutions":["Remove the duplicate exchange parameter; the framework injects the exchange automatically, so keep exactly one (or none).","If you need transport context plus exchange, ensure only one parameter is of the supported exchange/context type and derive the other from it inside the method.","Align the parameter with the sync/async variant: use McpSyncServerExchange for sync methods and McpAsyncServerExchange or McpTransportContext for async methods, only once."],"exampleFix":"// before\n@McpPrompt(name = \"greet\")\npublic String greet(McpSyncServerExchange exchange, McpTransportContext ctx, @McpArg String name) { ... }\n// after\n@McpPrompt(name = \"greet\")\npublic String greet(McpSyncServerExchange exchange, @McpArg String name) { ... }","handlingStrategy":"validation","validationCode":"long exchangeParams = Arrays.stream(method.getParameters())\n    .map(Parameter::getType)\n    .filter(t -> McpSyncServerExchange.class.isAssignableFrom(t)\n        || McpAsyncServerExchange.class.isAssignableFrom(t)\n        || McpTransportContext.class.isAssignableFrom(t))\n    .count();\nif (exchangeParams > 1) throw new IllegalStateException(method + \" declares multiple exchange parameters\");","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Declare at most one exchange/context parameter per prompt method.","Remember the framework injects the exchange automatically — you rarely need it at all.","Run a startup-time test that constructs callbacks for all @McpPrompt beans to fail fast."],"tags":["mcp","method-signature","duplicate-parameter","registration-time"],"backgroundTag":"invalid-argument","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"}