spring-projects/spring-ai · error · IllegalArgumentException

Async prompt method must not declare parameter of type: ${pa

Error message

Async prompt method must not declare parameter of type: ${paramType}. Use McpAsyncServerExchange instead. Method: ${method} in ${declaringClass}

What it means

This IllegalArgumentException is thrown by AsyncMcpResourceMethodCallback.validateParamType when an @McpResource-annotated method that runs in async mode declares a parameter of type McpSyncServerExchange (or a subtype). Async callbacks are wired to McpAsyncServerExchange, so a sync exchange parameter can never be satisfied and Spring AI MCP rejects the method up front. Use McpAsyncServerExchange (or McpTransportContext) instead.

Source

Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/resource/AsyncMcpResourceMethodCallback.java:64

 * @author Christian Tzolov
 * @author Alexandros Pappas
 * @author Vadzim Shurmialiou
 * @author Craig Walls
 */
public final class AsyncMcpResourceMethodCallback extends AbstractMcpResourceMethodCallback
		implements BiFunction<McpAsyncServerExchange, ReadResourceRequest, Mono<ReadResourceResult>> {

	private AsyncMcpResourceMethodCallback(Builder builder) {
		super(builder.method, builder.bean, builder.uri, builder.name, builder.description, builder.mimeType,
				builder.resultConverter, builder.uriTemplateManagerFactory, builder.contentType, builder.meta);
		this.validateMethod(this.method);
	}

	@Override
	protected void validateParamType(Class<?> paramType) {

		if (McpSyncServerExchange.class.isAssignableFrom(paramType)) {
			throw new IllegalArgumentException("Async prompt method must not declare parameter of type: "
					+ paramType.getName() + ". Use McpAsyncServerExchange instead." + " Method: "
					+ this.method.getName() + " in " + this.method.getDeclaringClass().getName());
		}
	}

	@Override
	protected Object assignExchangeType(Class<?> paramType, Object exchange) {

		if (McpTransportContext.class.isAssignableFrom(paramType)) {
			if (exchange instanceof McpTransportContext transportContext) {
				return transportContext;
			}
			else if (exchange instanceof McpSyncServerExchange syncServerExchange) {
				throw new IllegalArgumentException("Unsupported Async exchange type: "
						+ syncServerExchange.getClass().getName() + " for Async method: " + method.getName() + " in "
						+ method.getDeclaringClass().getName());

			}

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Change the parameter type from McpSyncServerExchange to McpAsyncServerExchange in the annotated method.
  2. If no session state is needed, use McpTransportContext as the exchange parameter, which works in both sync and async modes.
  3. Ensure the resource method is registered against the server type matching its exchange parameter (sync exchange -> sync server).

Example fix

// before
@McpResource(uri = "docs://{id}")
public ReadResourceResult read(McpSyncServerExchange exchange, String id) { ... }

// after
@McpResource(uri = "docs://{id}")
public ReadResourceResult read(McpAsyncServerExchange exchange, String id) { ... }
Defensive patterns

Strategy: validation

Validate before calling

// Fail fast in a startup test
Stream.of(resourceProvider.getClass().getDeclaredMethods())
    .filter(m -> m.isAnnotationPresent(McpResource.class))
    .forEach(m -> Arrays.stream(m.getParameterTypes())
        .filter(p -> McpSyncServerExchange.class.isAssignableFrom(p))
        .findAny()
        .ifPresent(p -> { throw new IllegalStateException("Async resource method " + m + " must not take " + p); }));

Type guard

static boolean isSyncExchangeParam(Class<?> p) { return McpSyncServerExchange.class.isAssignableFrom(p); }

Try / catch

try {
    asyncMcpServer.annotate(resourceProvider);
} catch (IllegalArgumentException e) {
    log.error("Bad resource method signature: {}", e.getMessage());
    throw new ConfigurationException(e);
}

Prevention

When it happens

Trigger: Registering an @McpResource method on an async MCP server (McpAsyncServer / async spec builder) whose signature includes a McpSyncServerExchange parameter; this validation runs during callback construction at registration time.

Common situations: Migrating an app from a sync MCP server to an async one and copying the old method signatures; copy-pasting example code from sync-based docs; sharing a resource class between both server types.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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