Tool
Java · package io.github.muthuishere:toolnexus · SPEC §1 · Tool.java
public interface Tool { String name(); String description(); Map<String, Object> inputSchema(); String source(); ToolResult execute(Map<String, Object> args, ToolContext ctx);
static String sanitize(String value);}The one interface. An MCP server tool, an agent skill, a built-in shell tool, a remote A2A agent, an
HTTP endpoint and a plain method of your own are all the same thing to an LLM — a named,
described, schema’d callable. Tool is that thing, and every source produces it.
When to use it
Section titled “When to use it”You mostly receive Tools rather than implement them: tk.tools() hands you a List<Tool>,
and that is what you stream, filter, and pass to an adapter.
Implement it directly when you are writing a new tool source — something producing tools from a
shape toolnexus doesn’t already cover. For a single ordinary method, use
NativeTool.of or the @Tool annotation instead.
Why an interface and not a record
Section titled “Why an interface and not a record”execute returns ToolResult directly — no Optional, no checked exception, no future. A
failed tool reports isError(), it does not throw. Throwing escapes the loop and ends the run.
Examples
Section titled “Examples”1. The smallest possible tool
Section titled “1. The smallest possible tool”An anonymous class is the most direct implementation.
import io.github.muthuishere.toolnexus.*;import java.util.Map;
public class Example { public static void main(String[] args) { Tool echo = new Tool() { public String name() { return "echo"; } public String description() { return "Return whatever it is given"; } public Map<String, Object> inputSchema() { return Map.of( "type", "object", "properties", Map.of("text", Map.of("type", "string")), "required", java.util.List.of("text") ); } public String source() { return "custom"; } public ToolResult execute(Map<String, Object> a, ToolContext ctx) { return ToolResult.ok(String.valueOf(a.get("text"))); } };
ToolResult res = echo.execute(Map.of("text", "hello"), new ToolContext()); if (!res.output().equals("hello") || res.isError()) { throw new AssertionError("unexpected: " + res.output()); } System.out.println("ok: " + res.output()); }}source() is not free-form — it is one of mcp, skill, builtin, a2a, native, http,
custom. Use custom for tools you implement yourself.
2. Reporting failure, and carrying metadata
Section titled “2. Reporting failure, and carrying metadata”A tool that fails does not throw — it returns an error result. ToolResult.ok and
ToolResult.error are the shorthand, each with an optional metadata overload.
import io.github.muthuishere.toolnexus.*;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) { Tool divide = new Tool() { public String name() { return "divide"; } public String description() { return "Divide two numbers"; } public Map<String, Object> inputSchema() { return Map.of( "type", "object", "properties", Map.of("a", Map.of("type", "number"), "b", Map.of("type", "number")), "required", List.of("a", "b") ); } public String source() { return "custom"; } public ToolResult execute(Map<String, Object> a, ToolContext ctx) { double x = ((Number) a.get("a")).doubleValue(); double y = ((Number) a.get("b")).doubleValue(); if (y == 0) { // The model sees this text and can correct itself on the next turn. return ToolResult.error("Cannot divide by zero"); } return ToolResult.ok( String.valueOf(x / y), Map.of("title", "divide", "operands", List.of(x, y)) ); } };
ToolResult ok = divide.execute(Map.of("a", 10, "b", 4), new ToolContext()); if (!ok.output().equals("2.5")) throw new AssertionError("got " + ok.output()); if (!ok.metadata().get("operands").equals(List.of(10.0, 4.0))) { throw new AssertionError("metadata: " + ok.metadata()); }
ToolResult bad = divide.execute(Map.of("a", 1, "b", 0), new ToolContext()); if (!bad.isError()) throw new AssertionError("expected isError");
System.out.println("ok: " + ok.output() + " | error path: " + bad.output()); }}3. A generated tool source — the real reason this interface is public
Section titled “3. A generated tool source — the real reason this interface is public”Producing many tools from data is where you implement Tool directly. Tool.sanitize makes each
name schema-safe.
import io.github.muthuishere.toolnexus.*;import java.util.ArrayList;import java.util.List;import java.util.Map;
public class Example { record Endpoint(String key, String path) {}
public static void main(String[] args) { List<Endpoint> endpoints = List.of( new Endpoint("get user", "/users/:id"), new Endpoint("list orders", "/orders") );
List<Tool> tools = new ArrayList<>(); for (Endpoint e : endpoints) { tools.add(new Tool() { // Names must match [a-zA-Z0-9_-]; sanitize does exactly that. public String name() { return Tool.sanitize(e.key()); } public String description() { return "Call " + e.path(); } public Map<String, Object> inputSchema() { return Map.of("type", "object", "properties", Map.of("id", Map.of("type", "string"))); } public String source() { return "custom"; } public ToolResult execute(Map<String, Object> a, ToolContext ctx) { // ctx may be null on a direct call — always guard it. if (ctx != null && ctx.isCancelled()) return ToolResult.error("cancelled"); return ToolResult.ok(e.path() + " <- " + a.get("id")); } }); }
if (!tools.get(0).name().equals("get_user") || !tools.get(1).name().equals("list_orders")) { throw new AssertionError("names: " + tools.get(0).name() + "," + tools.get(1).name()); }
ToolResult res = tools.get(0).execute(Map.of("id", "42"), new ToolContext()); if (!res.output().equals("/users/:id <- 42")) throw new AssertionError(res.output());
System.out.println("ok: " + tools.get(0).name() + ", " + tools.get(1).name()); }}Members
Section titled “Members”| Member | Returns | What it is |
|---|---|---|
name() |
String |
The name the model calls. Must match [a-zA-Z0-9_-]. |
description() |
String |
What the model reads to decide whether to call it. |
inputSchema() |
Map<String, Object> |
A JSON-Schema object as a plain map. |
source() |
String |
One of mcp, skill, builtin, a2a, native, http, custom. |
execute(args, ctx) |
ToolResult |
Runs the tool. ctx may be null on a direct call. |
Tool.sanitize(v) |
String |
Static. Replaces everything outside [a-zA-Z0-9_-] with _. |
See also
Section titled “See also”ToolResult— whatexecutereturnsToolContext— whatexecutereceivesNativeTool.of— build aToolfrom a methodToolkit.create— aggregate tools from every source