Skip to content

@ToolMethod annotation

Java · package io.github.muthuishere:toolnexus · SPEC §6 · ToolMethod.java · Param.java

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ToolMethod {
String name() default ""; // defaults to the method name
String description() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Param {
String name() default ""; // defaults to the parameter name
String description() default "";
boolean required() default true;
}

Two annotations, not a method call: mark a method @ToolMethod and its parameters @Param, and Tools.fromObject derives the tool’s name, description and JSON-Schema inputSchema from the method signature — the Spring-AI @Tool feel, without a Spring dependency. @Param is optional per-parameter; an unannotated parameter still becomes a required schema property, using its declared name (needs javac -parameters, which this repo compiles with) and an inferred JSON type.

When you already have a plain method that does the work and want a tool wrapping it without hand-writing an inputSchema map — the common case for turning existing service methods into LLM-callable tools. It pairs with Tools.fromObject to actually collect the annotated methods into Tools; see that page for the collection step.

ToolContext is a special case: a parameter of type ToolContext is not turned into a schema property — it is recognized and the caller’s ToolContext is passed straight through.

import io.github.muthuishere.toolnexus.*;
import io.github.muthuishere.toolnexus.annotations.Param;
import io.github.muthuishere.toolnexus.annotations.ToolMethod;
import java.util.List;
import java.util.Map;
public class Example {
static final class Calc {
@ToolMethod(name = "add", description = "Add two numbers")
public String add(@Param(name = "a") int a, @Param(name = "b") int b) {
return String.valueOf(a + b);
}
}
public static void main(String[] args) {
List<Tool> tools = Tools.fromObject(new Calc());
if (tools.size() != 1) throw new AssertionError("expected 1 tool");
Tool add = tools.get(0);
if (!add.name().equals("add")) throw new AssertionError(add.name());
if (!add.description().equals("Add two numbers")) throw new AssertionError(add.description());
if (!add.source().equals("native")) throw new AssertionError(add.source());
ToolResult res = add.execute(Map.of("a", 2, "b", 3), new ToolContext());
if (!res.output().equals("5")) throw new AssertionError(res.output());
System.out.println("ok: " + add.name() + " -> " + res.output());
}
}

2. Optional parameters and the inferred schema type

Section titled “2. Optional parameters and the inferred schema type”

@Param(name = ...) names the schema property explicitly (this repo compiles its own sources with -parameters so an unannotated name works there too, but a portable snippet should not depend on that flag); @Param(required = false) opts a parameter out of required.

import io.github.muthuishere.toolnexus.*;
import io.github.muthuishere.toolnexus.annotations.Param;
import io.github.muthuishere.toolnexus.annotations.ToolMethod;
import java.util.List;
import java.util.Map;
public class Example {
static final class Greeter {
// `name` is a required string; `loud` is optional and defaults to false when omitted.
@ToolMethod(description = "Greet someone, optionally loudly")
public String greet(@Param(name = "name") String name,
@Param(name = "loud", required = false) Boolean loud) {
String base = "Hello, " + name;
return Boolean.TRUE.equals(loud) ? base.toUpperCase() + "!" : base;
}
}
public static void main(String[] args) {
Tool greet = Tools.fromObject(new Greeter()).get(0);
// No explicit name() on @ToolMethod -> defaults to the method name.
if (!greet.name().equals("greet")) throw new AssertionError(greet.name());
@SuppressWarnings("unchecked")
Map<String, Object> props = (Map<String, Object>) greet.inputSchema().get("properties");
if (!props.containsKey("name") || !props.containsKey("loud")) throw new AssertionError(props.keySet());
@SuppressWarnings("unchecked")
List<String> required = (List<String>) greet.inputSchema().get("required");
if (!required.contains("name") || required.contains("loud")) throw new AssertionError(required);
ToolResult res = greet.execute(Map.of("name", "Ada", "loud", true), new ToolContext());
if (!res.output().equals("HELLO, ADA!")) throw new AssertionError(res.output());
System.out.println("ok: " + res.output());
}
}

3. ToolContext flows through untouched — it never becomes a schema property

Section titled “3. ToolContext flows through untouched — it never becomes a schema property”
import io.github.muthuishere.toolnexus.*;
import io.github.muthuishere.toolnexus.annotations.ToolMethod;
import java.util.List;
import java.util.Map;
public class Example {
static final class Cancellable {
@ToolMethod(name = "long_task", description = "A task that checks for cancellation")
public String run(ToolContext ctx) {
if (ctx != null && ctx.isCancelled()) return "cancelled early";
return "completed";
}
}
public static void main(String[] args) {
Tool task = Tools.fromObject(new Cancellable()).get(0);
// ToolContext is not a schema property.
@SuppressWarnings("unchecked")
Map<String, Object> props = (Map<String, Object>) task.inputSchema().get("properties");
if (!props.isEmpty()) throw new AssertionError("expected no properties, got " + props.keySet());
ToolResult ran = task.execute(Map.of(), new ToolContext());
if (!ran.output().equals("completed")) throw new AssertionError(ran.output());
ToolContext cancelled = new ToolContext();
cancelled.cancel();
ToolResult stopped = task.execute(Map.of(), cancelled);
if (!stopped.output().equals("cancelled early")) throw new AssertionError(stopped.output());
System.out.println("ok: " + ran.output() + " / " + stopped.output());
}
}
Member What it is
@ToolMethod.name Tool name; empty (default) ⇒ the method’s own name.
@ToolMethod.description Tool description, verbatim.
@Param.name Schema property name; empty (default) ⇒ the parameter’s own name (javac -parameters).
@Param.description Added to the property’s schema fragment when non-empty.
@Param.required Default true. false omits the name from the schema’s required array.
ToolContext parameter Recognized by type; excluded from the schema, passed through from execute’s ctx.
Inferred JSON type String/char → string; boolean/Booleanboolean; any numeric type → number; Collection/array → array; everything else → object.
  • Tools.fromObject — the collector that actually reflects over an object’s @ToolMethod methods and builds the Tools.
  • NativeTool.of — build a Tool from a plain method when the schema needs more control than inference gives you.