{"record":{"id":"ef1de0c4caa0022e","repo":"spring-projects/spring-ai","slug":"method-parameters-must-be-exchange-readresourcere","errorCode":null,"errorMessage":"Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken when no URI variables are present: ${method} in ${declaringClass} has parameter of type ${paramType}","messagePattern":"Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken when no URI variables are present: (.+?) in (.+?) has parameter of type (.+?)","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":259,"sourceCode":"\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;\n\t\t\t}\n\t\t\telse if (ReadResourceRequest.class.isAssignableFrom(paramType)\n\t\t\t\t\t|| String.class.isAssignableFrom(paramType)) {\n\t\t\t\tif (hasRequestOrUriParam) {\n\t\t\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\t\t\"Method cannot have more than one ReadResourceRequest or String 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\thasRequestOrUriParam = true;\n\t\t\t\thasValidParams = true;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\t\"Method parameters must be exchange, ReadResourceRequest, String, McpMeta, or @McpProgressToken when no URI variables are present: \"\n\t\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName()\n\t\t\t\t\t\t\t\t+ \" has parameter of type \" + paramType.getName());\n\t\t\t}\n\t\t}\n\n\t\tif (!hasValidParams && nonSpecialParamCount > 0) {\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\"Method must have either ReadResourceRequest or String parameter when no URI variables are present: \"\n\t\t\t\t\t\t\t+ method.getName() + \" in \" + method.getDeclaringClass().getName());\n\t\t}\n\t}\n\n\tprotected void validateParamType(Class<?> paramType) {\n\t}\n\n\t/**\n\t * Validates method parameters when URI variables are present. This method provides","sourceCodeStart":241,"sourceCodeEnd":277,"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#L241-L277","documentation":"Thrown when a resource method without URI variables has a parameter whose type is none of the supported ones: exchange types, ReadResourceRequest, String, McpMeta, or a parameter annotated @McpProgressToken. The framework cannot bind arbitrary types for URI-variable-less resource methods, so the method is rejected.","triggerScenarios":"A @McpResource URI contains no {var} placeholders but the handler method includes a parameter of an unsupported type (e.g. Map, custom POJO, Integer) that is not annotated with @McpProgressToken.","commonSituations":"Reusing a tool-style handler signature for a resource; forgetting that URI variables are only allowed when the URI template has placeholders; passing a custom request object instead of ReadResourceRequest.","solutions":["Remove or retype the unsupported parameter to ReadResourceRequest or String","If the parameter was meant to bind a URI variable, add {var} placeholders to the @McpResource uri and validateParametersWithUriVariables will apply instead","Annotate progress-related parameters with @McpProgressToken so they are skipped by validation"],"exampleFix":"// before\n@McpResource(uri = \"db://rows\")\npublic String read(Map<String, String> filters) { ... }\n// after\n@McpResource(uri = \"db://rows\")\npublic String read(ReadResourceRequest request) { ... }","handlingStrategy":"validation","validationCode":"Set<Class<?>> allowed = Set.of(ReadResourceRequest.class, String.class, McpMeta.class,\n    McpSyncRequestContext.class, McpAsyncRequestContext.class);\nfor (Parameter p : method.getParameters()) {\n  if (p.isAnnotationPresent(McpProgressToken.class)) continue;\n  Class<?> t = p.getType();\n  if (!allowed.stream().anyMatch(a -> a.isAssignableFrom(t))) {\n    throw new IllegalStateException(\"Unsupported param \" + t + \" in \" + method);\n  }\n}","typeGuard":"static boolean isSupportedResourceParam(Parameter p) {\n  Class<?> t = p.getType();\n  return p.isAnnotationPresent(McpProgressToken.class)\n      || McpMeta.class.isAssignableFrom(t)\n      || ReadResourceRequest.class.isAssignableFrom(t)\n      || String.class.isAssignableFrom(t);\n}","tryCatchPattern":"try { provider.build(...); }\ncatch (IllegalArgumentException e) {\n  if (e.getMessage().contains(\"parameters must be exchange, ReadResourceRequest, String, McpMeta\")) { /* retype or remove the offending param */ }\n  else throw e;\n}","preventionTips":["Only use framework-supported parameter types in URI-variable-less resource methods","Annotate progress parameters with @McpProgressToken to have them skipped","If you need URI variables, add {var} placeholders to the @McpResource uri"],"tags":["mcp","spring-ai","unsupported-parameter-type","method-validation"],"backgroundTag":"invalid-argument-value","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}