Toolnexus.Adapters.to_anthropic
Elixir · package toolnexus · SPEC §4 · elixir/lib/toolnexus/adapters.ex
@spec to_anthropic([Toolnexus.Tool.t()]) :: [map()]def to_anthropic(tools)Turns a list of tools into the tools array the Anthropic Messages API expects. It is the flattest
of the three adapters — no wrapper object, and input_schema keeps its name.
When to use it
Section titled “When to use it”When you are calling POST /v1/messages yourself (Anthropic API, Bedrock, Vertex) and need schema
for the request body. Reach for it whenever you own the loop and want toolnexus only for tool
aggregation — MCP servers, skills, HTTP endpoints and native functions arriving as one list.
Why this and not the alternative
Section titled “Why this and not the alternative”Toolnexus.Toolkit.to_anthropic/1 takes a toolkit and delegates straight here with its tools — use
that when you have a toolkit, and this when you have a bare list.
Examples
Section titled “Examples”1. One tool to Anthropic schema
Section titled “1. One tool to Anthropic schema”Keys in the emitted maps are strings, not atoms — this is wire data headed for JSON.
alias Toolnexus.{Adapters, Native}
weather = Native.define_tool(%{ name: "get_weather", description: "Current weather for a city", input_schema: %{ "type" => "object", "properties" => %{"city" => %{"type" => "string"}}, "required" => ["city"] }, execute: fn args, _ctx -> "sunny in #{args["city"]}" end })
[entry] = Adapters.to_anthropic([weather])
# Flat — three keys, no "function" wrapper (unlike OpenAI).true = entry |> Map.keys() |> Enum.sort() == ["description", "input_schema", "name"]true = entry["name"] == "get_weather"true = entry["description"] == "Current weather for a city"true = entry["input_schema"]["required"] == ["city"]
IO.puts("ok: #{entry["name"]}")input_schema is passed through verbatim — the very map that is on the tool. Anthropic is the
one provider whose key name already matches toolnexus’s own.
2. Feeding it straight into a Messages request body
Section titled “2. Feeding it straight into a Messages request body”The output is plain maps and lists, so Jason.encode!/1 handles it with no custom encoder.
alias Toolnexus.{Adapters, Native}
mk = fn name, desc -> Native.define_tool(%{ name: name, description: desc, input_schema: %{"type" => "object", "properties" => %{}}, execute: fn _args, _ctx -> name end })end
tools = [mk.("search", "Search the docs"), mk.("ping", "Health check")]
body = %{ "model" => "claude-sonnet-4-5", "max_tokens" => 1024, "messages" => [%{"role" => "user", "content" => "search for adapters"}], "tools" => Adapters.to_anthropic(tools)}
# Order is preserved — the adapter is a plain Enum.map/2.names = Enum.map(body["tools"], & &1["name"])true = names == ["search", "ping"]
json = Jason.encode!(body)true = String.contains?(json, ~s("input_schema"))
# The key comes from the environment, never a literal in your source.{"x-api-key", key} = {"x-api-key", System.get_env("ANTHROPIC_API_KEY") || "YOUR_KEY_HERE"}true = is_binary(key)
IO.puts("ok: #{Enum.join(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 call in. Unlike OpenAI, Anthropic hands you input as a real map — there is no
Jason.decode!/1 step.
alias Toolnexus.{Adapters, Context, Native}
weather = Native.define_tool(%{ name: "get_weather", description: "Current weather for a city", input_schema: %{ "type" => "object", "properties" => %{"city" => %{"type" => "string"}}, "required" => ["city"] }, execute: fn args, _ctx -> if args["city"] == "", do: raise("city is required"), else: "sunny in #{args["city"]}" end })
tools = [weather]1 = length(Adapters.to_anthropic(tools))
# A content block exactly as the model returns it.block = %{ "type" => "tool_use", "id" => "toolu_01", "name" => "get_weather", "input" => %{"city" => "Chennai"}}
called = Enum.find(tools, &(&1.name == block["name"]))true = called != nil
res = called.execute.(block["input"], %Context{})false = res.is_errortrue = res.output == "sunny in Chennai"
# What you append to the conversation for the next turn.result_block = %{ "type" => "tool_result", "tool_use_id" => block["id"], "content" => res.output, "is_error" => res.is_error}
true = result_block["tool_use_id"] == "toolu_01"
# Failure is data, not an exception — it maps onto tool_result.is_error.bad = called.execute.(%{"city" => ""}, %Context{})true = bad.is_errortrue = bad.output == "city is required"
# An empty tool list emits an empty array — no wrapper, unlike Gemini.[] = Adapters.to_anthropic([])
IO.puts("ok: #{block["name"]} -> #{res.output}")| Path | From | Notes |
|---|---|---|
[]["name"] |
Tool.name |
What the model calls back with, in tool_use.name. |
[]["description"] |
Tool.description |
What the model reads to decide whether to call it. |
[]["input_schema"] |
Tool.input_schema |
Passed through unchanged — same key name, no renaming. |
There is no "type" key and no nesting: an Anthropic tool entry has exactly these three keys.
See also
Section titled “See also”Adapters.to_openai·Adapters.to_geminiToolnexus.create_toolkit—Toolkit.to_anthropic/1delegates hereToolnexus.Client.create— calls the adapter for youToolnexus.Tool— wherename,descriptionandinput_schemacome from