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, String, or Mono<T>: ${method} in ${declaringClass} returns ${returnType} What it means
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.
Source
Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AsyncMcpResourceMethodCallback.java:183
}
});
}
/**
* Validates that the method return type is compatible with the resource callback.
* @param method The method to validate
* @throws IllegalArgumentException if the return type is not compatible
*/
@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) || Mono.class.isAssignableFrom(returnType);
if (!validReturnType) {
throw new IllegalArgumentException(
"Method must return either ReadResourceResult, List<ResourceContents>, List<String>, "
+ "ResourceContents, String, or Mono<T>: " + method.getName() + " in "
+ method.getDeclaringClass().getName() + " returns " + returnType.getName());
}
}
/**
* Checks if a parameter type is compatible with the exchange type.
* @param paramType The parameter type to check
* @return true if the parameter type is compatible with the exchange type, false
* otherwise
*/
@Override
protected boolean isExchangeOrContextType(Class<?> paramType) {
return McpAsyncServerExchange.class.isAssignableFrom(paramType)
|| McpTransportContext.class.isAssignableFrom(paramType);
}
View on GitHub (pinned to 98a7beda4f)
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.
Example fix
// before
@McpResource(uri = "db://{id}")
public Mono<MyDocument> load(String id) { ... }
// after
@McpResource(uri = "db://{id}")
public Mono<ReadResourceResult> load(String id) {
return repo.find(id).map(d -> ReadResourceResult.builder()
.contents(TextResourceContents.builder().uri(uri).text(d.toJson()).build()).build());
} Defensive patterns
Strategy: validation
Validate before calling
static boolean isValidAsyncResourceReturnType(Class<?> rt) {
return ReadResourceResult.class.isAssignableFrom(rt) || List.class.isAssignableFrom(rt)
|| ResourceContents.class.isAssignableFrom(rt) || String.class.isAssignableFrom(rt)
|| Mono.class.isAssignableFrom(rt);
} Type guard
static boolean isReactiveOk(Class<?> rt) { return Mono.class.isAssignableFrom(rt); } Try / catch
try {
asyncMcpServer.annotate(provider);
} catch (IllegalArgumentException e) {
if (e.getMessage().startsWith("Method must return either")) {
throw new IllegalStateException("Change return type of resource method", e);
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
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
- Method must have void or Mono<Void> return type:
- Method must have void return type: " + method.getName() + "
- Method must have void return type:
- Method must return either CompleteResult, CompleteCompletion
- Method must return either CompleteResult, CompleteCompletion
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/d45509a7de6dabd3.
Report an issue: GitHub.