spring-projects/spring-ai · error · IllegalArgumentException

Method must return either ReadResourceResult, List

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.

Solutions

  1. Change the return type to ReadResourceResult for full control over contents and metadata.
  2. Return String (or List<String>) for text resources, or ResourceContents (or List<ResourceContents>) for typed content.
  3. Serialize custom objects to JSON yourself and return the resulting String with an appropriate mimeType.
  4. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/b81e2cb389c516d7. Report an issue: GitHub.

Appendix: 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)