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

  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.