toolnexus.tool/tool
Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §1 · clojure/src/toolnexus/tool.cljc
(toolnexus.tool/tool {:name "get_weather" ; required :description "Current weather" ; optional — "" when absent :input-schema {:type "object"} ; optional — {:type "object"} when absent :source "native" ; optional — "custom" when absent :execute (fn [args] ...)}) ; required — may also take [args ctx]
;; =>{:name "get_weather" :description "Current weather" :input-schema {:type "object"} :source "native" :execute #function[...]}A Tool in this port is a plain map with one closure in it. No protocol, no record, no
deftype — a map of data plus a fn needs nothing but fn, get and assoc, and that is the
only shape that behaves identically on Clojure (JVM) and on cljgo, the two runtimes this one
.cljc tree runs on unchanged. Everything MCP, skills, builtins, HTTP and A2A produce is this
same map; tool is the constructor you call when the value comes from somewhere the port has no
builder for.
The constructor does exactly three things: it asserts (:pre) that :name is present and
:execute is a function, it strs the name, and it fills the three defaults above. There is no
validation of :input-schema — it goes through to the adapters verbatim.
:execute returns a ToolResult map, not a string. Build it with
toolnexus.tool/success or toolnexus.tool/failure. If it
throws instead, toolnexus.tool/execute turns the throw into a failure result: a misbehaving tool
must not take down the loop (SPEC §0.8).
When to use it
Section titled “When to use it”- Adapting a source the port does not build for you — a queue consumer, an in-process service, a fixture. Anything that can answer a map with a map can be a Tool.
- Overriding a tool by name. Registration order is the precedence rule:
toolnexus.tool/add-toolslets a later tool win a name collision, which is exactly how SPEC §0.11 gives MCP precedence over a builtin of the same name. - Tests — a two-line Tool with a deterministic result exercises the toolkit, the adapters and the client loop without a network or a child process.
Why this and not the alternative
Section titled “Why this and not the alternative”tool also does not sanitize the name. SPEC §0.2 naming is applied by the sources that derive
a name from a remote one — toolnexus.mcp/mcp-tool-name calls toolnexus.tool/sanitize for you.
A name you write by hand is used as written, so keep it inside [a-zA-Z0-9_-] yourself, or run it
through sanitize.
Options
Section titled “Options”| Key | Required | Default | What it does |
|---|---|---|---|
:name |
yes | — | The name the model calls. stred, never sanitized. |
:execute |
yes | — | (fn [args]), or a two-arity fn that also accepts ctx. Returns a ToolResult map. |
:description |
no | "" |
Handed to the model verbatim by the adapters. |
:input-schema |
no | {:type "object"} |
JSON Schema for the arguments; passed through untouched. |
:source |
no | "custom" |
Provenance label — "mcp", "native", "builtin", "a2a", "http". Not behavioural; useful for filtering and metrics. |
Examples
Section titled “Examples”1. A tool written by hand, run through a toolkit
Section titled “1. A tool written by hand, run through a toolkit”(require '[toolnexus.tool :as tool])
(def greet (tool/tool {:name "greet" :description "Greet someone by name" :input-schema {:type "object" :properties {:name {:type "string"}} :required ["name"]} :source "native" :execute (fn [args] (tool/success (str "Hello, " (:name args))))}))
(def tk (tool/toolkit [greet]))
(tool/execute tk "greet" {:name "Muthu"});; => {:output "Hello, Muthu" :isError false}
(tool/tool-names tk);; => ["greet"] — sorted, always: two runtimes must not disagree on order2. Later wins — the mechanism behind MCP precedence
Section titled “2. Later wins — the mechanism behind MCP precedence”(require '[toolnexus.tool :as tool])
(def builtin-read (tool/tool {:name "read" :source "builtin" :execute (fn [_] (tool/success "builtin"))}))
(def mcp-read (tool/tool {:name "read" :source "mcp" :execute (fn [_] (tool/success "mcp"))}))
(def tk (-> (tool/toolkit [builtin-read]) (tool/add-tools [mcp-read])))
(count (:tools tk)) ;; => 1 — one name, one slot(:output (tool/execute tk "read" {})) ;; => "mcp"This is why toolnexus.core/build registers builtins first and
MCP last. The order in that function is load-bearing, not stylistic.
3. A throwing tool is still a well-behaved tool
Section titled “3. A throwing tool is still a well-behaved tool”(require '[toolnexus.tool :as tool])
(def tk (tool/toolkit [(tool/tool {:name "boom" :execute (fn [_] (throw (ex-info "kaboom" {})))})]))
(tool/execute tk "boom" {});; => {:output "kaboom" :isError true}
(tool/execute tk "nope" {});; => {:output "unknown tool: nope" :isError true}toolnexus.tool/execute catches Throwable — a bare symbol, which is the one catch target both
hosts accept — so SPEC §0.8 is written once and holds on the JVM and on cljgo alike. An unknown
name is the same kind of answer: a value the model can read and retry from, never an exception
thrown at your loop.
See also
Section titled “See also”toolnexus.tool/success— the result envelope:executemust return- The context argument — the optional third argument to
execute toolnexus.native/native-tool— wrap a plain function insteadtoolnexus.core/build— where Tools from every source are merged