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

  1. Return ReadResourceResult (optionally as Mono<ReadResourceResult>) built from TextResourceContents/BlobResourceContents.
  2. Alternatively return String, ResourceContents, List<String>, or List<ResourceContents>.
  3. 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

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


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