{"record":{"id":"d701081ce1055b84","repo":"spring-projects/spring-ai","slug":"no-tool-annotated-methods-found-in-s-did-you-me","errorCode":null,"errorMessage":"No @Tool annotated methods found in %s. Did you mean to pass a ToolCallback or ToolCallbackProvider? If so, use .tools(toolCallback) or .toolCallbacks(toolCallback) instead.","messagePattern":"No @Tool annotated methods found in (.+?)\\. Did you mean to pass a ToolCallback or ToolCallbackProvider\\? If so, use \\.tools\\(toolCallback\\) or \\.toolCallbacks\\(toolCallback\\) instead\\.","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"spring-ai-model/src/main/java/org/springframework/ai/tool/method/MethodToolCallbackProvider.java","lineNumber":78,"sourceCode":"\t\tAssert.notNull(toolObjects, \"toolObjects cannot be null\");\n\t\tAssert.noNullElements(toolObjects, \"toolObjects cannot contain null elements\");\n\t\tassertToolAnnotatedMethodsPresent(toolObjects);\n\t\tthis.toolObjects = toolObjects;\n\t\tvalidateToolCallbacks(getToolCallbacks());\n\t}\n\n\tprivate void assertToolAnnotatedMethodsPresent(List<Object> toolObjects) {\n\n\t\tfor (Object toolObject : toolObjects) {\n\t\t\tList<Method> toolMethods = Stream\n\t\t\t\t.of(ReflectionUtils.getDeclaredMethods(\n\t\t\t\t\t\tAopUtils.isAopProxy(toolObject) ? AopUtils.getTargetClass(toolObject) : toolObject.getClass()))\n\t\t\t\t.filter(this::isToolAnnotatedMethod)\n\t\t\t\t.filter(toolMethod -> !isFunctionalType(toolMethod))\n\t\t\t\t.toList();\n\n\t\t\tif (toolMethods.isEmpty()) {\n\t\t\t\tthrow new IllegalArgumentException(\"No @Tool annotated methods found in \" + toolObject + \". \"\n\t\t\t\t\t\t+ \"Did you mean to pass a ToolCallback or ToolCallbackProvider? If so, use\"\n\t\t\t\t\t\t+ \" .tools(toolCallback) or .toolCallbacks(toolCallback) instead.\");\n\t\t\t}\n\t\t}\n\t}\n\n\t@Override\n\tpublic ToolCallback[] getToolCallbacks() {\n\t\tvar toolCallbacks = this.toolObjects.stream()\n\t\t\t.map(toolObject -> Stream\n\t\t\t\t.of(ReflectionUtils.getDeclaredMethods(\n\t\t\t\t\t\tAopUtils.isAopProxy(toolObject) ? AopUtils.getTargetClass(toolObject) : toolObject.getClass()))\n\t\t\t\t.filter(this::isToolAnnotatedMethod)\n\t\t\t\t.filter(toolMethod -> !isFunctionalType(toolMethod))\n\t\t\t\t.filter(ReflectionUtils.USER_DECLARED_METHODS::matches)\n\t\t\t\t.map(toolMethod -> MethodToolCallback.builder()\n\t\t\t\t\t.toolDefinition(ToolDefinitions.from(toolMethod))\n\t\t\t\t\t.toolMetadata(ToolMetadata.from(toolMethod))","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/spring-ai-model/src/main/java/org/springframework/ai/tool/method/MethodToolCallbackProvider.java#L60-L96","documentation":"MethodToolCallbackProvider scans each supplied toolObject for @Tool-annotated methods (skipping functional types). If an object yields zero @Tool methods, it throws an IllegalArgumentException explaining that a ToolCallback/ToolCallbackProvider should be passed with .tools()/.toolCallbacks() instead. It fails fast because a provider over an object with no tools is almost certainly a misuse of the API.","triggerScenarios":"Passing a ToolCallback, ToolCallbackProvider, or plain object without any @Tool methods into MethodToolCallbackProvider.builder().toolObject(...); wrong imports of @Tool (not org.springframework.ai.tool.annotation.Tool).","commonSituations":"Migrating code where callbacks were previously registered via .tools() and someone switched to toolObject(); importing a custom/other framework's @Tool annotation; Lombok/AOP hiding annotations from scanning.","solutions":["Annotate at least one public method of the object with org.springframework.ai.tool.annotation.@Tool.","If you already have ToolCallbacks, use .tools(toolCallback) or .toolCallbacks(provider) instead of .toolObject(...).","Verify the @Tool import resolves to Spring AI's annotation, not another library's."],"exampleFix":"// before\nMethodToolCallbackProvider.builder().toolObject(new MyCallbackProvider()).build(); // no @Tool methods\n// after\nChatClient.builder().defaultToolCallbacks(myCallbackProvider); // or annotate methods in the object with @Tool","handlingStrategy":"validation","validationCode":"long toolCount = Arrays.stream(obj.getClass().getMethods())\n    .filter(m -> m.isAnnotationPresent(org.springframework.ai.tool.annotation.Tool.class))\n    .count();\nif (toolCount == 0) throw new IllegalArgumentException(obj + \" has no @Tool methods; use toolCallbacks instead\");","typeGuard":"boolean isToolObject = Arrays.stream(o.getClass().getMethods()).anyMatch(m -> m.isAnnotationPresent(Tool.class));","tryCatchPattern":"try { provider = MethodToolCallbackProvider.builder().toolObject(obj).build(); } catch (IllegalArgumentException e) { if (e.getMessage().startsWith(\"No @Tool annotated methods\")) throw new InvalidToolSourceException(obj, e); throw e; }","preventionTips":["Check @Tool imports resolve to org.springframework.ai.tool.annotation.Tool.","Register ToolCallbacks via .tools()/.toolCallbacks(), objects with @Tool methods via .toolObject() — never cross them.","Cover provider construction in a startup test."],"tags":["annotation","tool-registration","spring-ai","misuse"],"backgroundTag":"incompatible-source-type","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"}