{"record":{"id":"b81e2cb389c516d7","repo":"spring-projects/spring-ai","slug":"method-must-return-either-readresourceresult-list-b81e2c","errorCode":null,"errorMessage":"Method must return either ReadResourceResult, List<ResourceContents>, List<String>, ResourceContents, or String: {methodName} in {className} returns {returnTypeName}","messagePattern":"Method must return either ReadResourceResult, List<ResourceContents>, List<String>, ResourceContents, or String: (.+?) in (.+?) returns (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/SyncMcpResourceMethodCallback.java","lineNumber":162,"sourceCode":"\t\t\tthrow McpError.builder(ErrorCodes.INVALID_PARAMS)\n\t\t\t\t.message(\"Error invoking resource method: \" + this.method.getName() + \" in \"\n\t\t\t\t\t\t+ this.bean.getClass().getName() + \". /nCause: \"\n\t\t\t\t\t\t+ ErrorUtils.findCauseUsingPlainJava(e).getMessage())\n\t\t\t\t.data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())\n\t\t\t\t.build();\n\t\t}\n\t}\n\n\t@Override\n\tprotected void validateReturnType(Method method) {\n\t\tClass<?> returnType = method.getReturnType();\n\n\t\tboolean validReturnType = ReadResourceResult.class.isAssignableFrom(returnType)\n\t\t\t\t|| List.class.isAssignableFrom(returnType) || ResourceContents.class.isAssignableFrom(returnType)\n\t\t\t\t|| String.class.isAssignableFrom(returnType);\n\n\t\tif (!validReturnType) {\n\t\t\tthrow new IllegalArgumentException(\n\t\t\t\t\t\"Method must return either ReadResourceResult, List<ResourceContents>, List<String>, \"\n\t\t\t\t\t\t\t+ \"ResourceContents, or String: \" + method.getName() + \" in \"\n\t\t\t\t\t\t\t+ method.getDeclaringClass().getName() + \" returns \" + returnType.getName());\n\t\t}\n\t}\n\n\t@Override\n\tprotected boolean isExchangeOrContextType(Class<?> paramType) {\n\t\treturn McpSyncServerExchange.class.isAssignableFrom(paramType)\n\t\t\t\t|| McpTransportContext.class.isAssignableFrom(paramType);\n\t}\n\n\t/**\n\t * Create a new builder.\n\t * @return A new builder instance\n\t */\n\tpublic static Builder builder() {\n\t\treturn new Builder();","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/SyncMcpResourceMethodCallback.java#L144-L180","documentation":"validateReturnType enforces that a sync @McpResource method returns one of: ReadResourceResult, a List (of String or ResourceContents), ResourceContents, or String. Anything else cannot be converted into a ReadResourceResult by the framework, so registration fails with IllegalArgumentException naming the method and its actual return type.","triggerScenarios":"Annotating a resource method whose return type is a DTO, record, Map, Optional<String>, byte[], custom wrapper, or void. Validation runs when the method callback is built/registered on the server.","commonSituations":"Returning domain objects expecting JSON auto-conversion, returning Optional to model absence, or refactoring a method's return type without updating the annotation contract.","solutions":["Change the return type to ReadResourceResult for full control over contents and metadata.","Return String (or List<String>) for text resources, or ResourceContents (or List<ResourceContents>) for typed content.","Serialize custom objects to JSON yourself and return the resulting String with an appropriate mimeType.","Replace Optional<String> with a nullable/plain String; handle absence by returning empty contents."],"exampleFix":"// before\n@McpResource(uri = \"user://{id}\")\npublic Optional<User> getUser(String id) { ... }\n\n// after\n@McpResource(uri = \"user://{id}\")\npublic String getUser(String id) { return objectMapper.writeValueAsString(repo.findUser(id)); }","handlingStrategy":"validation","validationCode":"static boolean validResourceReturnType(Class<?> rt) {\n    return ReadResourceResult.class.isAssignableFrom(rt)\n        || List.class.isAssignableFrom(rt)\n        || ResourceContents.class.isAssignableFrom(rt)\n        || String.class.isAssignableFrom(rt);\n}","typeGuard":"static boolean isConvertibleReturn(Object r) {\n    return r instanceof ReadResourceResult || r instanceof String\n        || r instanceof ResourceContents || r instanceof List<?>;\n}","tryCatchPattern":"try {\n    server.addResource(resource, callback);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Method must return either ReadResourceResult\")) {\n        log.error(\"Change @McpResource method return type to ReadResourceResult/String/ResourceContents/List\", e);\n    } throw e;\n}","preventionTips":["Adopt a team convention: resource methods return ReadResourceResult or String only.","Never use Optional or domain types as resource method return types; serialize explicitly.","Add a reflection-based startup test that validates return types of all @McpResource methods."],"tags":["mcp","return-type","method-validation","illegal-argument"],"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"}