spring-projects/spring-ai · error · IllegalArgumentException

No @Tool annotated methods found in %s. Did you mean to pass

Error message

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.

What it means

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.

Source

Thrown at spring-ai-model/src/main/java/org/springframework/ai/tool/method/MethodToolCallbackProvider.java:78

		Assert.notNull(toolObjects, "toolObjects cannot be null");
		Assert.noNullElements(toolObjects, "toolObjects cannot contain null elements");
		assertToolAnnotatedMethodsPresent(toolObjects);
		this.toolObjects = toolObjects;
		validateToolCallbacks(getToolCallbacks());
	}

	private void assertToolAnnotatedMethodsPresent(List<Object> toolObjects) {

		for (Object toolObject : toolObjects) {
			List<Method> toolMethods = Stream
				.of(ReflectionUtils.getDeclaredMethods(
						AopUtils.isAopProxy(toolObject) ? AopUtils.getTargetClass(toolObject) : toolObject.getClass()))
				.filter(this::isToolAnnotatedMethod)
				.filter(toolMethod -> !isFunctionalType(toolMethod))
				.toList();

			if (toolMethods.isEmpty()) {
				throw new IllegalArgumentException("No @Tool annotated methods found in " + toolObject + ". "
						+ "Did you mean to pass a ToolCallback or ToolCallbackProvider? If so, use"
						+ " .tools(toolCallback) or .toolCallbacks(toolCallback) instead.");
			}
		}
	}

	@Override
	public ToolCallback[] getToolCallbacks() {
		var toolCallbacks = this.toolObjects.stream()
			.map(toolObject -> Stream
				.of(ReflectionUtils.getDeclaredMethods(
						AopUtils.isAopProxy(toolObject) ? AopUtils.getTargetClass(toolObject) : toolObject.getClass()))
				.filter(this::isToolAnnotatedMethod)
				.filter(toolMethod -> !isFunctionalType(toolMethod))
				.filter(ReflectionUtils.USER_DECLARED_METHODS::matches)
				.map(toolMethod -> MethodToolCallback.builder()
					.toolDefinition(ToolDefinitions.from(toolMethod))
					.toolMetadata(ToolMetadata.from(toolMethod))

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Annotate at least one public method of the object with org.springframework.ai.tool.annotation.@Tool.
  2. If you already have ToolCallbacks, use .tools(toolCallback) or .toolCallbacks(provider) instead of .toolObject(...).
  3. Verify the @Tool import resolves to Spring AI's annotation, not another library's.

Example fix

// before
MethodToolCallbackProvider.builder().toolObject(new MyCallbackProvider()).build(); // no @Tool methods
// after
ChatClient.builder().defaultToolCallbacks(myCallbackProvider); // or annotate methods in the object with @Tool
Defensive patterns

Strategy: validation

Validate before calling

long toolCount = Arrays.stream(obj.getClass().getMethods())
    .filter(m -> m.isAnnotationPresent(org.springframework.ai.tool.annotation.Tool.class))
    .count();
if (toolCount == 0) throw new IllegalArgumentException(obj + " has no @Tool methods; use toolCallbacks instead");

Type guard

boolean isToolObject = Arrays.stream(o.getClass().getMethods()).anyMatch(m -> m.isAnnotationPresent(Tool.class));

Try / catch

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; }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


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