NativeTool
Go · module github.com/muthuishere/toolnexus/golang · SPEC §6 · golang/native.go
func NativeTool( name, description string, inputSchema JSONSchema, fn func(ctx context.Context, args map[string]any) (string, error),) ToolThe shortest path from a function you already have to a tool a model can call. You supply four
things — a name, a description, a JSON-Schema object, and the function — and get back a
Tool with Source: "native", ready to register on a toolkit or hand to an
adapter.
When to use it
Section titled “When to use it”Whenever the capability is your own code: a database query, a pricing calculation, an internal RPC, a feature flag lookup. This is the workhorse of the library — most tools in a real agent are native ones, with MCP and skills filling in around them.
Why this and not the alternative
Section titled “Why this and not the alternative”Hand-building the Tool struct yourself is the other alternative — reach for
that only when you are generating many tools from data, since NativeTool already fills in Source
and the error handling for you.
Examples
Section titled “Examples”1. A function and four arguments
Section titled “1. A function and four arguments”fn returns (string, error). That is the entire contract.
package main
import ( "context" "fmt" "log" "strings"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { upper := toolnexus.NativeTool( "shout", "Uppercase the given text", toolnexus.JSONSchema{ "type": "object", "properties": map[string]any{"text": map[string]any{"type": "string"}}, "required": []string{"text"}, }, func(ctx context.Context, args map[string]any) (string, error) { text, _ := args["text"].(string) return strings.ToUpper(text), nil }, )
if upper.Name != "shout" || upper.Source != toolnexus.SourceNative { log.Fatalf("unexpected tool: %+v", upper) }
res, err := upper.Execute(map[string]any{"text": "hello"}, nil) if err != nil || res.IsError || res.Output != "HELLO" { log.Fatalf("unexpected: %+v %v", res, err) }
fmt.Println("ok:", res.Output)}2. Returning an error, and the schema-less case
Section titled “2. Returning an error, and the schema-less case”A returned error is not a Go failure the caller must handle — it becomes model-facing output
with IsError: true, so the model can read the message and correct itself. And a nil schema means
“takes no arguments”.
package main
import ( "context" "errors" "fmt" "log" "time"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { lookup := toolnexus.NativeTool( "get_order", "Look up an order by id", toolnexus.JSONSchema{ "type": "object", "properties": map[string]any{"id": map[string]any{"type": "string"}}, "required": []string{"id"}, }, func(ctx context.Context, args map[string]any) (string, error) { id, _ := args["id"].(string) if id != "A-1" { return "", errors.New("no such order: " + id) } return "order A-1: shipped", nil }, )
ok, err := lookup.Execute(map[string]any{"id": "A-1"}, nil) if err != nil || ok.IsError || ok.Output != "order A-1: shipped" { log.Fatalf("unexpected: %+v %v", ok, err) }
// The error message becomes the OUTPUT. Execute itself returns a nil error. bad, err := lookup.Execute(map[string]any{"id": "Z-9"}, nil) if err != nil { log.Fatalf("Execute should not surface the error: %v", err) } if !bad.IsError || bad.Output != "no such order: Z-9" { log.Fatalf("unexpected error result: %+v", bad) }
// nil schema ⇒ {"type":"object","properties":{},"additionalProperties":false}. now := toolnexus.NativeTool("now", "Current server time", nil, func(ctx context.Context, args map[string]any) (string, error) { return time.Now().UTC().Format(time.RFC3339), nil }, ) if now.InputSchema["type"] != "object" { log.Fatalf("unexpected default schema: %v", now.InputSchema) } if now.InputSchema["additionalProperties"] != false { log.Fatalf("expected additionalProperties:false, got %v", now.InputSchema) } // nil args are normalised to an empty map before fn runs. res, err := now.Execute(nil, nil) if err != nil || res.IsError || res.Output == "" { log.Fatalf("unexpected: %+v %v", res, err) }
fmt.Println("ok:", ok.Output, "| error path:", bad.Output)}3. Context, cancellation, and registering on a toolkit
Section titled “3. Context, cancellation, and registering on a toolkit”The ctx your function receives comes from the ToolContext the loop
passes in — honour it for anything slow.
package main
import ( "context" "encoding/json" "fmt" "log"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { slow := toolnexus.NativeTool( "fetch_report", "Generate a report for a region", toolnexus.JSONSchema{ "type": "object", "properties": map[string]any{ "region": map[string]any{ "type": "string", "enum": []string{"emea", "apac"}, "description": "Which region to report on", }, }, "required": []string{"region"}, "additionalProperties": false, }, func(ctx context.Context, args map[string]any) (string, error) { // Bail out the moment the caller gives up. if err := ctx.Err(); err != nil { return "", err } return fmt.Sprintf("report for %v", args["region"]), nil }, )
// No ToolContext at all: fn still gets a usable context.Background(). res, err := slow.Execute(map[string]any{"region": "apac"}, nil) if err != nil || res.Output != "report for apac" { log.Fatalf("unexpected: %+v %v", res, err) }
// A cancelled context surfaces as an error RESULT, not a returned error. cancelled, cancel := context.WithCancel(context.Background()) cancel() stopped, err := slow.Execute( map[string]any{"region": "emea"}, &toolnexus.ToolContext{Ctx: cancelled}, ) if err != nil || !stopped.IsError || stopped.Output != "context canceled" { log.Fatalf("unexpected cancellation: %+v %v", stopped, err) }
// Register it on a toolkit alongside every other source. Builtins are ON by // default — turn them off so this toolkit holds only our tool. tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{ ExtraTools: []toolnexus.Tool{slow}, Builtins: false, }) if err != nil { log.Fatal(err) } defer tk.Close()
if _, ok := tk.Get("fetch_report"); !ok { log.Fatal("expected the tool on the toolkit") } out, err := tk.Execute(context.Background(), "fetch_report", map[string]any{"region": "emea"}) if err != nil || out.Output != "report for emea" { log.Fatalf("unexpected: %+v %v", out, err) }
// The schema you wrote is what the model sees, enum and all. if len(tk.Tools()) != 1 { log.Fatalf("expected only our tool, got %d", len(tk.Tools())) } b, _ := json.Marshal(toolnexus.ToOpenAI(tk.Tools())[0]) var got map[string]any _ = json.Unmarshal(b, &got) params := got["function"].(map[string]any)["parameters"].(map[string]any) props := params["properties"].(map[string]any)["region"].(map[string]any) if len(props["enum"].([]any)) != 2 { log.Fatalf("enum did not survive: %v", props) }
fmt.Println("ok:", res.Output, "| cancelled:", stopped.Output)}Arguments
Section titled “Arguments”| Argument | Type | What it is |
|---|---|---|
name |
string |
The name the model calls. Must match [a-zA-Z0-9_-] — run untrusted names through Sanitize. |
description |
string |
What the model reads to decide whether to call it. This is prompt engineering, not a code comment. |
inputSchema |
JSONSchema |
A JSON-Schema object (map[string]any). nil ⇒ {"type":"object","properties":{},"additionalProperties":false}. |
fn |
func(context.Context, map[string]any) (string, error) |
Your code. ctx is never nil; args is never nil. |
Return mapping:
fn returns |
Resulting ToolResult |
|---|---|
("text", nil) |
{Output: "text", IsError: false} |
("", err) |
{Output: err.Error(), IsError: true} — and Execute’s own error stays nil |
See also
Section titled “See also”NativeToolReflect— derive the schema from a struct insteadTool— what you get backToolContext— wherectxcomes fromCreateToolkit— register it viaExtraTools