spring-projects/spring-ai · error · McpToolListChangedConsumerMethodException

Error invoking tool list changed consumer method:

Error message

Error invoking tool list changed consumer method: 

What it means

This error is thrown by SyncMcpToolListChangedMethodCallback.accept() when reflective invocation of the user-annotated @McpToolListChanged consumer method on the target bean fails. The library wraps any underlying exception (reflective access failure or exception thrown inside the callback method) in an McpToolListChangedConsumerMethodException, preserving the cause. The message includes the method name; the real cause is in the chained exception.

Source

Thrown at mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/tool/SyncMcpToolListChangedMethodCallback.java:67

	 * tool list changed consumer method
	 * @throws IllegalArgumentException if the updatedTools is null
	 */
	@Override
	public void accept(List<McpSchema.Tool> updatedTools) {
		if (updatedTools == null) {
			throw new IllegalArgumentException("Updated tools list must not be null");
		}

		try {
			// Build arguments for the method call
			Object[] args = this.buildArgs(this.method, null, updatedTools);

			// Invoke the method
			this.method.setAccessible(true);
			this.method.invoke(this.bean, args);
		}
		catch (Exception e) {
			throw new McpToolListChangedConsumerMethodException(
					"Error invoking tool list changed consumer method: " + this.method.getName(), e);
		}
	}

	/**
	 * Validates that the method return type is compatible with the tool list changed
	 * consumer 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();

		if (returnType != void.class) {
			throw new IllegalArgumentException("Method must have void return type: " + method.getName() + " in "
					+ method.getDeclaringClass().getName() + " returns " + returnType.getName());
		}

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Inspect the chained cause (e.getCause()) of the McpToolListChangedConsumerMethodException to find the root exception thrown by your method or by reflection.
  2. Verify the annotated method has the exact expected signature: void method(List<McpSchema.Tool> newTools) (or matching variant) on the registered bean.
  3. Ensure the bean instance passed to the callback is the one actually declaring the method and that the method is accessible (public, or module-opens if JPMS).
  4. Wrap the body of your @McpToolListChanged method in its own try-catch/logging so user-level exceptions don't surface as invocation failures.

Example fix

// before
@McpToolListChanged(clients = "client1")
public void onToolsChanged(List<McpSchema.Tool> tools) {
    tools.get(0).name(); // NPE risk inside handler
}
// after
@McpToolListChanged(clients = "client1")
public void onToolsChanged(List<McpSchema.Tool> tools) {
    if (tools == null || tools.isEmpty()) { return; }
    LOG.info("tools changed: {}", tools);
}
Defensive patterns

Strategy: try-catch

Validate before calling

Method m = bean.getClass().getMethod("onToolsChanged", List.class);
if (!m.getReturnType().equals(void.class)) throw new IllegalStateException("handler must be void");
if (!Modifier.isPublic(m.getModifiers())) throw new IllegalStateException("handler must be accessible");

Try / catch

try {
    toolCallbackRegistry.register(spec);
} catch (McpToolListChangedConsumerMethodException e) {
    LOG.error("tool-list-changed handler failed", e.getCause());
}

Prevention

When it happens

Trigger: An MCP server tool list changes (tool added/removed/updated) and the registered consumer method is invoked reflectively via this.method.invoke(this.bean, args); the method is inaccessible (setAccessible fails), has the wrong signature, the bean instance is wrong, or the callback method itself throws.

Common situations: Security manager or module restrictions blocking setAccessible; refactoring the handler method signature after annotation registration; a null or stale bean reference; an exception thrown inside user handler code (e.g., NPE while diffing tool lists).

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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