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
- Inspect the chained cause (e.getCause()) of the McpToolListChangedConsumerMethodException to find the root exception thrown by your method or by reflection.
- Verify the annotated method has the exact expected signature: void method(List<McpSchema.Tool> newTools) (or matching variant) on the registered bean.
- 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).
- 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
- Keep @McpToolListChanged handlers void with the exact supported signature.
- Log and swallow expected business exceptions inside the handler.
- Verify bean and method accessibility (public / JPMS opens) at startup.
- Add an integration test that fires a tool list change.
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
- Error invoking resource list changed consumer method: " + th
- Method must not be null
- Error invoking complete method:
- Error invoking complete method:
- Currently only methods with a single ElicitRequest parameter
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/c419a7a712e24527.
Report an issue: GitHub.