Skip to content

ToAnthropic

Go · module github.com/muthuishere/toolnexus/golang · SPEC §4 · golang/adapters.go

func ToAnthropic(tools []Tool) []any

Turns a []Tool into the tools array an Anthropic Messages request expects. Anthropic’s shape is the flattest of the three: no type wrapper and no function nesting — each entry is just a name, a description and an input_schema.

When you drive the Anthropic Messages API yourself — the official SDK, a raw POST /v1/messages, Bedrock or Vertex — and need the schema block for the request body. Reach for it whenever you have tools from toolnexus and a Claude-shaped endpoint on the other end.

tk.ToAnthropic() on a Toolkit is this same function applied to that toolkit’s tools. Use the method when you hold a toolkit, the free function when you hold a slice.

Note the key: input_schema, snake-cased, at the top level of each entry.

package main
import (
"context"
"encoding/json"
"fmt"
"log"
toolnexus "github.com/muthuishere/toolnexus/golang"
)
func main() {
weather := toolnexus.NativeTool(
"get_weather",
"Current weather for a city",
toolnexus.JSONSchema{
"type": "object",
"properties": map[string]any{"city": map[string]any{"type": "string"}},
"required": []string{"city"},
},
func(ctx context.Context, args map[string]any) (string, error) {
return fmt.Sprintf("sunny in %v", args["city"]), nil
},
)
schema := toolnexus.ToAnthropic([]toolnexus.Tool{weather})
if len(schema) != 1 {
log.Fatalf("expected 1 entry, got %d", len(schema))
}
b, _ := json.Marshal(schema[0])
var got map[string]any
_ = json.Unmarshal(b, &got)
// Flat: no {"type":"function","function":{...}} wrapper.
if _, wrapped := got["function"]; wrapped {
log.Fatalf("anthropic entries are flat, got %v", got)
}
if got["name"] != "get_weather" || got["description"] != "Current weather for a city" {
log.Fatalf("unexpected entry: %v", got)
}
if _, ok := got["input_schema"].(map[string]any); !ok {
log.Fatalf("expected an input_schema object, got %v", got["input_schema"])
}
fmt.Println("ok:", got["name"])
}

2. Feeding it straight into a Messages request body

Section titled “2. Feeding it straight into a Messages request body”
package main
import (
"context"
"encoding/json"
"fmt"
"log"
toolnexus "github.com/muthuishere/toolnexus/golang"
)
func main() {
mk := func(name, desc string) toolnexus.Tool {
return toolnexus.NativeTool(name, desc,
toolnexus.JSONSchema{"type": "object", "properties": map[string]any{}},
func(ctx context.Context, args map[string]any) (string, error) { return name, nil },
)
}
tools := []toolnexus.Tool{mk("search", "Search the docs"), mk("ping", "Health check")}
body := map[string]any{
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": []any{map[string]any{"role": "user", "content": "search for adapters"}},
"tools": toolnexus.ToAnthropic(tools),
}
// The whole body marshals with encoding/json — no custom marshaller needed.
b, err := json.Marshal(body)
if err != nil {
log.Fatal(err)
}
var got map[string]any
_ = json.Unmarshal(b, &got)
entries := got["tools"].([]any)
if len(entries) != 2 {
log.Fatalf("expected 2 tools, got %d", len(entries))
}
// Order is preserved.
var names []string
for _, e := range entries {
names = append(names, e.(map[string]any)["name"].(string))
}
if names[0] != "search" || names[1] != "ping" {
log.Fatalf("order not preserved: %v", names)
}
fmt.Println("ok:", names)
}

3. Round-tripping a tool_use block back to the tool

Section titled “3. Round-tripping a tool_use block back to the tool”

Schema out, tool_use in. Anthropic hands input back as a JSON object, not a string — the one real difference from OpenAI at the call site.

package main
import (
"context"
"encoding/json"
"fmt"
"log"
toolnexus "github.com/muthuishere/toolnexus/golang"
)
func main() {
weather := toolnexus.NativeTool(
"get_weather",
"Current weather for a city",
toolnexus.JSONSchema{
"type": "object",
"properties": map[string]any{"city": map[string]any{"type": "string"}},
"required": []string{"city"},
},
func(ctx context.Context, args map[string]any) (string, error) {
return fmt.Sprintf("sunny in %v", args["city"]), nil
},
)
tools := []toolnexus.Tool{weather}
// A content block as Claude would return it.
raw := `{"type":"tool_use","id":"toolu_01","name":"get_weather","input":{"city":"Chennai"}}`
var block struct {
ID string `json:"id"`
Name string `json:"name"`
Input map[string]any `json:"input"`
}
if err := json.Unmarshal([]byte(raw), &block); err != nil {
log.Fatal(err)
}
var called *toolnexus.Tool
for i := range tools {
if tools[i].Name == block.Name {
called = &tools[i]
break
}
}
if called == nil {
log.Fatal("the advertised name should resolve back to the tool")
}
res, err := called.Execute(block.Input, nil)
if err != nil || res.Output != "sunny in Chennai" || res.IsError {
log.Fatalf("unexpected: %+v %v", res, err)
}
// The result goes back as a tool_result block keyed by tool_use_id.
reply := map[string]any{
"type": "tool_result",
"tool_use_id": block.ID,
"content": res.Output,
"is_error": res.IsError,
}
if reply["tool_use_id"] != "toolu_01" {
log.Fatalf("unexpected reply: %v", reply)
}
// An empty tool list is valid — it just means "no tools this turn".
if len(toolnexus.ToAnthropic(nil)) != 0 {
log.Fatal("expected an empty slice for no tools")
}
fmt.Println("ok:", block.Name, "->", res.Output)
}
Path From Notes
[].name Tool.Name What the tool_use block calls back with.
[].description Tool.Description
[].input_schema Tool.InputSchema Renamed — inputSchemainput_schema.

Each entry is the exported struct AnthropicTool, so you can type-assert it if you prefer that to marshalling. There is no type field and no nesting.