spring-projects/spring-ai · error · IllegalArgumentException
Method must return either ReadResourceResult, List<ResourceC
Error message
Method must return either ReadResourceResult, List<ResourceContents>, List<String>, ResourceContents, or String: {methodName} in {className} returns {returnTypeName} What it means
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.
Source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/SyncMcpResourceMethodCallback.java:162
throw McpError.builder(ErrorCodes.INVALID_PARAMS)
.message("Error invoking resource method: " + this.method.getName() + " in "
+ this.bean.getClass().getName() + ". /nCause: "
+ ErrorUtils.findCauseUsingPlainJava(e).getMessage())
.data(ErrorUtils.findCauseUsingPlainJava(e).getMessage())
.build();
}
}
@Override
protected void validateReturnType(Method method) {
Class<?> returnType = method.getReturnType();
boolean validReturnType = ReadResourceResult.class.isAssignableFrom(returnType)
|| List.class.isAssignableFrom(returnType) || ResourceContents.class.isAssignableFrom(returnType)
|| String.class.isAssignableFrom(returnType);
if (!validReturnType) {
throw new IllegalArgumentException(
"Method must return either ReadResourceResult, List<ResourceContents>, List<String>, "
+ "ResourceContents, or String: " + method.getName() + " in "
+ method.getDeclaringClass().getName() + " returns " + returnType.getName());
}
}
@Override
protected boolean isExchangeOrContextType(Class<?> paramType) {
return McpSyncServerExchange.class.isAssignableFrom(paramType)
|| McpTransportContext.class.isAssignableFrom(paramType);
}
/**
* Create a new builder.
* @return A new builder instance
*/
public static Builder builder() {
return new Builder();View on GitHub (pinned to 98a7beda4f)
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.
Example fix
// before
@McpResource(uri = "user://{id}")
public Optional<User> getUser(String id) { ... }
// after
@McpResource(uri = "user://{id}")
public String getUser(String id) { return objectMapper.writeValueAsString(repo.findUser(id)); } Defensive patterns
Strategy: validation
Validate before calling
static boolean validResourceReturnType(Class<?> rt) {
return ReadResourceResult.class.isAssignableFrom(rt)
|| List.class.isAssignableFrom(rt)
|| ResourceContents.class.isAssignableFrom(rt)
|| String.class.isAssignableFrom(rt);
} Type guard
static boolean isConvertibleReturn(Object r) {
return r instanceof ReadResourceResult || r instanceof String
|| r instanceof ResourceContents || r instanceof List<?>;
} Try / catch
try {
server.addResource(resource, callback);
} catch (IllegalArgumentException e) {
if (e.getMessage().startsWith("Method must return either ReadResourceResult")) {
log.error("Change @McpResource method return type to ReadResourceResult/String/ResourceContents/List", e);
} throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: Returning domain objects expecting JSON auto-conversion, returning Optional to model absence, or refactoring a method's return type without updating the annotation contract.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Sync prompt method must not declare parameter of type: {para
- Stateless Streamable-Http prompt method must not declare par
- Expected Mono<Void> but got Mono<
- Method must have void or Mono<Void> return type:
- Method must have void return type:
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/b81e2cb389c516d7.
Report an issue: GitHub.