{"record":{"id":"d45509a7de6dabd3","repo":"spring-projects/spring-ai","slug":"method-must-return-either-readresourceresult-list","errorCode":null,"errorMessage":"Method must return either ReadResourceResult, List<ResourceContents>, List<String>, ResourceContents, String, or Mono<T>: ${method} in ${declaringClass} returns ${returnType}","messagePattern":"Method must return either ReadResourceResult, List<ResourceContents>, List<String>, ResourceContents, String, or Mono<T>: (.+?) in (.+?) returns (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AsyncMcpResourceMethodCallback.java","lineNumber":183,"sourceCode":"\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Validates that the method return type is compatible with the resource callback.\n\t * @param method The method to validate\n\t * @throws IllegalArgumentException if the return type is not compatible\n\t */\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) || Mono.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, String, or Mono<T>: \" + 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/**\n\t * Checks if a parameter type is compatible with the exchange type.\n\t * @param paramType The parameter type to check\n\t * @return true if the parameter type is compatible with the exchange type, false\n\t * otherwise\n\t */\n\t@Override\n\tprotected boolean isExchangeOrContextType(Class<?> paramType) {\n\t\treturn McpAsyncServerExchange.class.isAssignableFrom(paramType)\n\t\t\t\t|| McpTransportContext.class.isAssignableFrom(paramType);\n\t}\n","sourceCodeStart":165,"sourceCodeEnd":201,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AsyncMcpResourceMethodCallback.java#L165-L201","documentation":"Thrown by AsyncMcpResourceMethodCallback.validateReturnType at registration time when an @McpResource method's declared return type is none of ReadResourceResult, List<ResourceContents>, List<String>, ResourceContents, String, or a Mono of these. The framework must convert the method result into a ReadResourceResult and cannot handle other shapes.","triggerScenarios":"Registering an async @McpResource method returning e.g. a custom DTO, Map, byte[], Optional<String>, or Flux instead of one of the supported types (wrapped in Mono where reactive).","commonSituations":"Returning an application-specific domain object and expecting the framework to serialize it; using Reactor Flux for streaming reads (not supported here); upgrading from a version with looser conversion rules.","solutions":["Return ReadResourceResult (optionally as Mono<ReadResourceResult>) built from TextResourceContents/BlobResourceContents.","Alternatively return String, ResourceContents, List<String>, or List<ResourceContents>.","Move unsupported data into a JSON String payload inside TextResourceContents."],"exampleFix":"// before\n@McpResource(uri = \"db://{id}\")\npublic Mono<MyDocument> load(String id) { ... }\n\n// after\n@McpResource(uri = \"db://{id}\")\npublic Mono<ReadResourceResult> load(String id) {\n    return repo.find(id).map(d -> ReadResourceResult.builder()\n        .contents(TextResourceContents.builder().uri(uri).text(d.toJson()).build()).build());\n}","handlingStrategy":"validation","validationCode":"static boolean isValidAsyncResourceReturnType(Class<?> rt) {\n    return ReadResourceResult.class.isAssignableFrom(rt) || List.class.isAssignableFrom(rt)\n        || ResourceContents.class.isAssignableFrom(rt) || String.class.isAssignableFrom(rt)\n        || Mono.class.isAssignableFrom(rt);\n}","typeGuard":"static boolean isReactiveOk(Class<?> rt) { return Mono.class.isAssignableFrom(rt); }","tryCatchPattern":"try {\n    asyncMcpServer.annotate(provider);\n} catch (IllegalArgumentException e) {\n    if (e.getMessage().startsWith(\"Method must return either\")) {\n        throw new IllegalStateException(\"Change return type of resource method\", e);\n    }\n    throw e;\n}","preventionTips":["Standardize on ReadResourceResult (or Mono<ReadResourceResult>) for resource methods","Do not use Flux; wrap lists in Mono<List<T>> instead","Serialize domain objects to JSON strings yourself","Add an architecture test enforcing allowed return types"],"tags":["mcp","java","async","return-type"],"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"}