Skip to content

toolnexus.adapter/to-anthropic

Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §4 · clojure/src/toolnexus/adapter.cljc

(to-anthropic tools-or-toolkit)
;; => [{:name "echo"
;; :description "Echo the input back."
;; :input_schema {:type "object" :properties {…} :required […]}}
;; …]

to-anthropic renders each tool as a flat map — no wrapper object, no nested :function key. That is the first thing that differs from to-openai, and the second is the schema’s key: Anthropic calls it :input_schema, in snake_case.

The underscore is deliberate and must survive. It is a wire name, not a Clojure convention, so it is never kebab-ised to :input-schema on the way out — the tool’s internal key is :input-schema and the emitted key is :input_schema, and the two are not the same keyword. Any JSON encoder you pass the result to will write it as input_schema verbatim.

Everything else matches the other adapters. Schema only, no network. A toolkit is sorted by tool name because map order is not a contract on either host; an explicit seq of tools keeps the order you gave it. With no tools you get [] — Gemini is the one adapter that still emits a wrapper.

  • Calling the Anthropic Messages API — this is the tools array it expects.
  • Rendering a subset per request — pass a vector instead of the toolkit.
  • Checking the wire keys — this is the adapter where a silent key rename would break a live call, so it is worth asserting on in a test.
  • Building a payload without an SDK — the output is data, so a plain HTTP POST works.

toolnexus.core/to-anthropic re-exports this function, so toolnexus.core alone is enough for a caller who does not want a second require.

Render a toolkit, and confirm the wire key

Section titled “Render a toolkit, and confirm the wire key”
(require '[toolnexus.core :as toolnexus]
'[toolnexus.adapter :as adapter])
(def tk (toolnexus/build {:skills "examples/skills"}))
(first (toolnexus/to-anthropic tk))
;; => {:name "apply_patch"
;; :description "Apply an add/update/delete patch."
;; :input_schema {:type "object"
;; :properties {:patchText {:type "string" …}}
;; :required ["patchText"]}}
;; snake_case out, kebab-case in — these are two different keywords.
(contains? (first (adapter/to-anthropic tk)) :input_schema) ;=> true
(contains? (first (adapter/to-anthropic tk)) :input-schema) ;=> false
(map :name (toolnexus/to-anthropic tk))
;; => ("apply_patch" "bash" "edit" "glob" "grep" "question" "read"
;; "skill" "todowrite" "webfetch" "write")
(require '[toolnexus.adapter :as adapter]
'[toolnexus.http :as http]
'[toolnexus.native :as native])
(def lookup
(http/http-tool {:name "lookup_order" :description "Fetch one order by id"
:input-schema {:type "object"
:properties {:id {:type "string"}}
:required ["id"]}
:url "https://api.example.com/orders/{id}"
:headers {"authorization" "Bearer ${API_TOKEN}"}}))
(def refund
(native/native-tool {:name "refund" :description "Refund an order"
:run (fn [args] (str "refunded " (:id args)))}))
;; A vector preserves this order; the toolkit form would sort it.
(adapter/to-anthropic [lookup refund])
;; => [{:name "lookup_order" :description "Fetch one order by id"
;; :input_schema {:type "object" :properties {:id {:type "string"}} :required ["id"]}}
;; {:name "refund" :description "Refund an order" :input_schema {:type "object"}}]
(adapter/to-anthropic []) ;=> []

The tool source makes no difference to the payload. An HTTP tool, a native tool, an MCP tool and the skill tool all render identically — that uniformity is the point of the Tool abstraction.

Closing the loop: tool_use block back to execute

Section titled “Closing the loop: tool_use block back to execute”
(require '[toolnexus.core :as toolnexus])
(def tk (toolnexus/build {:skills "examples/skills"}))
(def request {:model "claude-sonnet-4-5" :messages []
:tools (toolnexus/to-anthropic tk)})
;; Anthropic hands back a tool_use content block: id, name, and args already
;; parsed as an object — no JSON string to decode, unlike OpenAI.
(defn tool-result-block [tk block]
(let [res (toolnexus/execute tk (get block "name") (get block "input"))]
{:type "tool_result"
:tool_use_id (get block "id")
:content (:output res)
;; A failed tool is reported, not thrown — the model sees it and retries.
:is_error (:isError res)}))
(tool-result-block tk {"id" "toolu_1" "name" "skill" "input" {"name" "hello-world"}})
;; => {:type "tool_result" :tool_use_id "toolu_1"
;; :content "<skill_content name=\"hello-world\">…" :is_error false}
Emitted key Source
:name The tool’s :name.
:description The tool’s :description, "" when unset — never nil.
:input_schema The tool’s :input-schema verbatim, {:type "object"} when unset. Snake_case on the wire.
Input Order
a toolkit Sorted by tool name.
a seq of tools Exactly as given.
an empty toolkit or seq [].