NativeTool.of
Java · package io.github.muthuishere:toolnexus · SPEC §6 · NativeTool.java
public static NativeTool of(String name, String description, Map<String, Object> inputSchema, Function<Map<String, Object>, Object> fn)
public static NativeTool of(String name, String description, Map<String, Object> inputSchema, BiFunction<Map<String, Object>, ToolContext, Object> fn)Wraps a lambda as a Tool with source() = "native". Two overloads, picked by arity: take just
the args, or take the args and the ToolContext.
When to use it
Section titled “When to use it”Whenever the thing the model should be able to do is already a method you have — a repository lookup, a price calculation, a call into your service layer. This is the shortest path from that method to something an LLM can call, and it needs no MCP server, no HTTP hop, no annotations.
Why this and not the alternative
Section titled “Why this and not the alternative”Against implementing Tool by hand: Tool has five members and is not a
functional interface, so a bare lambda cannot satisfy it. NativeTool.of is that boilerplate,
already written — plus the two things people forget: a thrown exception becomes an error result
rather than escaping the loop, and a non-String return is JSON-encoded for you.
Examples
Section titled “Examples”1. One function, one tool
Section titled “1. One function, one tool”import io.github.muthuishere.toolnexus.*;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) { Tool greet = NativeTool.of( "greet", "Greet someone by name", Map.of("type", "object", "properties", Map.of("name", Map.of("type", "string")), "required", List.of("name")), (Map<String, Object> a) -> "Hello, " + a.get("name") + "!" );
if (!greet.source().equals("native")) throw new AssertionError(greet.source());
ToolResult res = greet.execute(Map.of("name", "Muthu"), new ToolContext()); if (res.isError() || !res.output().equals("Hello, Muthu!")) throw new AssertionError(res.output());
System.out.println("ok: " + res.output()); }}Annotate the lambda parameter ((Map<String, Object> a) -> …) so Java picks the single-argument
overload — a bare a -> … is ambiguous between the two of signatures.
2. Returning objects, and failing safely
Section titled “2. Returning objects, and failing safely”Return a String and it becomes the output verbatim. Return anything else and it is JSON-encoded.
Throw, and you get an error result — the loop survives and the model sees the message.
import io.github.muthuishere.toolnexus.*;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;
public class Example { public static void main(String[] args) { Tool lookup = NativeTool.of( "lookup_order", "Fetch an order by id", Map.of("type", "object", "properties", Map.of("id", Map.of("type", "string")), "required", List.of("id")), (Map<String, Object> a) -> { String id = String.valueOf(a.get("id")); if (!id.equals("A1")) throw new IllegalArgumentException("no such order: " + id); Map<String, Object> order = new LinkedHashMap<>(); order.put("id", id); order.put("total", 42); return order; // not a String — JSON-encoded on the way out } );
ToolResult ok = lookup.execute(Map.of("id", "A1"), new ToolContext()); if (ok.isError()) throw new AssertionError(ok.output()); if (!ok.output().contains("\"total\"")) throw new AssertionError(ok.output());
// A thrown exception NEVER escapes — it lands as an error result. ToolResult bad = lookup.execute(Map.of("id", "Z9"), new ToolContext()); if (!bad.isError()) throw new AssertionError("expected isError"); if (!bad.output().equals("no such order: Z9")) throw new AssertionError(bad.output());
System.out.println("ok: " + ok.output() + " | error path: " + bad.output()); }}3. The context overload, metadata, and a null schema
Section titled “3. The context overload, metadata, and a null schema”The two-argument overload receives the ToolContext, which is where cancellation and the per-call
timeout live. Return a ToolResult yourself when you want to control isError or attach metadata.
import io.github.muthuishere.toolnexus.*;import java.util.List;import java.util.Map;import java.util.function.BiFunction;
public class Example { public static void main(String[] args) { BiFunction<Map<String, Object>, ToolContext, Object> body = (a, ctx) -> { // ctx may be null on a direct call — always guard it. if (ctx != null && ctx.isCancelled()) return ToolResult.error("cancelled"); long n = ((Number) a.get("n")).longValue(); if (n < 0) return ToolResult.error("n must be >= 0"); return ToolResult.ok(String.valueOf(n * 2), Map.of("title", "double", "input", n)); };
Tool doubler = NativeTool.of( "double", "Double a number", Map.of("type", "object", "properties", Map.of("n", Map.of("type", "integer")), "required", List.of("n")), body );
ToolResult res = doubler.execute(Map.of("n", 21), new ToolContext()); if (!res.output().equals("42")) throw new AssertionError(res.output()); if (!res.metadata().get("title").equals("double")) throw new AssertionError("metadata");
// Cancellation is cooperative — the tool decides what to do about it. ToolContext cancelled = new ToolContext(); cancelled.cancel(); if (!doubler.execute(Map.of("n", 1), cancelled).isError()) throw new AssertionError("expected cancel");
// A null schema falls back to the empty-object schema; null args become {}. Tool now = NativeTool.of("now", "Server time", null, (Map<String, Object> a) -> "2026-01-01"); if (!now.inputSchema().get("type").equals("object")) throw new AssertionError("schema type"); if (!now.inputSchema().get("additionalProperties").equals(false)) throw new AssertionError("schema strictness"); if (!now.execute(null, null).output().equals("2026-01-01")) throw new AssertionError("null args");
// Ready for any adapter. List<Map<String, Object>> schema = Adapters.toOpenAI(List.of(doubler, now)); if (schema.size() != 2) throw new AssertionError("adapter");
System.out.println("ok: " + res.output() + ", " + now.execute(null, null).output()); }}Parameters
Section titled “Parameters”| Parameter | Type | Meaning |
|---|---|---|
name |
String |
The name the model calls. Must match [a-zA-Z0-9_-] — Tool.sanitize if it is derived from data. |
description |
String |
What the model reads to decide whether to call it. |
inputSchema |
Map<String, Object> |
JSON-Schema object. Null ⇒ {"type":"object","properties":{},"additionalProperties":false}. |
fn |
Function | BiFunction |
The body. One arg ⇒ args only; two ⇒ args + ToolContext. |
What fn may return
Section titled “What fn may return”| Return | Becomes |
|---|---|
String |
ToolResult.ok(value) |
ToolResult |
passed through unchanged — your isError and metadata survive |
| anything else | ToolResult.ok(json(value)) |
| throws | ToolResult.error(message) — the cause’s message when the exception is wrapped |
source() is always "native", and args is never null inside fn — a null map is replaced with
an empty one.
See also
Section titled “See also”Tools.fromObject— sweep a class for@Toolmethods instead@Toolannotation — derive the schema from the signatureTool— the interface this implementsToolkit.create— pass these viaextraTools(...)