Skip to content

toAnthropic

JavaScript · package toolnexus · SPEC §4 · js/src/adapters.ts

function toAnthropic(tools: Tool[]): {
name: string
description: string
input_schema: JSONSchema
}[]

Turns a Tool[] into the tools array an Anthropic Messages request expects. Anthropic’s shape is the flattest of the three providers — no wrapper object, no nesting; each tool is one plain record whose only rename is inputSchemainput_schema.

When you are calling the Anthropic Messages API yourself — with @anthropic-ai/sdk, a raw fetch, or through Bedrock / Vertex — and need schema to put in the request body. Anything that speaks the Messages API takes this array verbatim.

tk.toAnthropic() on a Toolkit is this same function applied to that toolkit’s tools — use the method when you have a toolkit, the free function when you have a bare array (a filtered subset, a hand-picked pair, one tool for a narrow turn).

import assert from "node:assert"
import { toAnthropic, defineTool } from "toolnexus"
const weather = defineTool({
name: "get_weather",
description: "Current weather for a city",
inputSchema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
run: async ({ city }) => `sunny in ${city}`,
})
const schema = toAnthropic([weather])
assert.equal(schema.length, 1)
assert.equal(schema[0].name, "get_weather")
assert.equal(schema[0].description, "Current weather for a city")
// The ONLY rename: inputSchema -> input_schema. No { type: "function" } wrapper.
assert.deepEqual(schema[0].input_schema.required, ["city"])
assert.equal((schema[0] as any).function, undefined)
console.log("ok:", schema[0].name)

Compare with toOpenAI, which nests the same three fields under { type: "function", function: {...} }. Same information, different envelope — that is the entire job of an adapter.

2. Feeding it straight into a Messages request body

Section titled “2. Feeding it straight into a Messages request body”

The output drops into tools untouched. Nothing else in the body needs transforming.

import assert from "node:assert"
import { toAnthropic, defineTool } from "toolnexus"
const tools = [
defineTool({
name: "search",
description: "Search the docs",
inputSchema: { type: "object", properties: { q: { type: "string" } }, required: ["q"] },
run: async ({ q }) => `results for ${q}`,
}),
defineTool({
name: "ping",
description: "Health check",
inputSchema: { type: "object", properties: {} },
run: async () => "pong",
}),
]
const body = {
model: "claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "search for adapters" }],
tools: toAnthropic(tools),
}
// Order is preserved, one entry per tool.
assert.equal(body.tools.length, 2)
assert.deepEqual(body.tools.map((t) => t.name), ["search", "ping"])
// Plain JSON — no classes, no cycles, safe to serialise.
assert.ok(JSON.stringify(body).length > 0)
console.log("ok:", body.tools.map((t) => t.name).join(", "))

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. Anthropic returns a tool_use content block whose name is the same name you advertised and whose input is already a parsed object — no JSON.parse step.

import assert from "node:assert"
import { toAnthropic, defineTool } from "toolnexus"
const weather = defineTool({
name: "get_weather",
description: "Current weather for a city",
inputSchema: { type: "object", properties: { city: { type: "string" } }, required: ["city"] },
run: async ({ city }) => `sunny in ${city}`,
})
const tools = [weather]
const schema = toAnthropic(tools)
// What a model would send back for that schema.
const block = { type: "tool_use", id: "toolu_1", name: "get_weather", input: { city: "Chennai" } }
const called = tools.find((t) => t.name === block.name)
assert.ok(called, "the advertised name resolves back to the tool")
const res = await called.execute(block.input)
assert.equal(res.output, "sunny in Chennai")
assert.equal(res.isError, false)
// The block you send on the next turn.
const toolResult = {
type: "tool_result",
tool_use_id: block.id,
content: res.output,
is_error: res.isError,
}
assert.equal(toolResult.tool_use_id, "toolu_1")
// An empty tool list is valid — it just means "no tools this turn".
assert.deepEqual(toAnthropic([]), [])
console.log("ok:", schema[0].name, "->", res.output)
Path From Notes
[].name Tool.name What the model calls back with in a tool_use block.
[].description Tool.description What the model reads to decide whether to call it.
[].input_schema Tool.inputSchema Renamed — inputSchemainput_schema.

There is no envelope key and no type discriminator: an Anthropic tool entry has exactly these three fields, and the input array’s order is preserved one-for-one.