{"record":{"id":"c419a7a712e24527","repo":"spring-projects/spring-ai","slug":"error-invoking-tool-list-changed-consumer-method","errorCode":null,"errorMessage":"Error invoking tool list changed consumer method: ","messagePattern":"Error invoking tool list changed consumer method: ","errorType":"exception","errorClass":"McpToolListChangedConsumerMethodException","httpStatus":null,"severity":"error","filePath":"mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/tool/SyncMcpToolListChangedMethodCallback.java","lineNumber":67,"sourceCode":"\t * tool list changed consumer method\n\t * @throws IllegalArgumentException if the updatedTools is null\n\t */\n\t@Override\n\tpublic void accept(List<McpSchema.Tool> updatedTools) {\n\t\tif (updatedTools == null) {\n\t\t\tthrow new IllegalArgumentException(\"Updated tools list must not be null\");\n\t\t}\n\n\t\ttry {\n\t\t\t// Build arguments for the method call\n\t\t\tObject[] args = this.buildArgs(this.method, null, updatedTools);\n\n\t\t\t// Invoke the method\n\t\t\tthis.method.setAccessible(true);\n\t\t\tthis.method.invoke(this.bean, args);\n\t\t}\n\t\tcatch (Exception e) {\n\t\t\tthrow new McpToolListChangedConsumerMethodException(\n\t\t\t\t\t\"Error invoking tool list changed consumer method: \" + this.method.getName(), e);\n\t\t}\n\t}\n\n\t/**\n\t * Validates that the method return type is compatible with the tool list changed\n\t * consumer callback.\n\t * @param method The method to validate\n\t * @throws IllegalArgumentException if the return type is not compatible\n\t */\n\t@Override\n\tprotected void validateReturnType(Method method) {\n\t\tClass<?> returnType = method.getReturnType();\n\n\t\tif (returnType != void.class) {\n\t\t\tthrow new IllegalArgumentException(\"Method must have void return type: \" + method.getName() + \" in \"\n\t\t\t\t\t+ method.getDeclaringClass().getName() + \" returns \" + returnType.getName());\n\t\t}","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/mcp/mcp-annotations/src/main/java/org/springframework/ai/mcp/annotation/method/changed/tool/SyncMcpToolListChangedMethodCallback.java#L49-L85","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before\n@McpToolListChanged(clients = \"client1\")\npublic void onToolsChanged(List<McpSchema.Tool> tools) {\n    tools.get(0).name(); // NPE risk inside handler\n}\n// after\n@McpToolListChanged(clients = \"client1\")\npublic void onToolsChanged(List<McpSchema.Tool> tools) {\n    if (tools == null || tools.isEmpty()) { return; }\n    LOG.info(\"tools changed: {}\", tools);\n}","handlingStrategy":"try-catch","validationCode":"Method m = bean.getClass().getMethod(\"onToolsChanged\", List.class);\nif (!m.getReturnType().equals(void.class)) throw new IllegalStateException(\"handler must be void\");\nif (!Modifier.isPublic(m.getModifiers())) throw new IllegalStateException(\"handler must be accessible\");","typeGuard":null,"tryCatchPattern":"try {\n    toolCallbackRegistry.register(spec);\n} catch (McpToolListChangedConsumerMethodException e) {\n    LOG.error(\"tool-list-changed handler failed\", e.getCause());\n}","preventionTips":["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."],"tags":["mcp","reflection","tool-list-changed","java"],"backgroundTag":"internal-invariant-violation","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}