Skip to content

toolnexus.a2a/agent-tools

Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §7A · clojure/src/toolnexus/a2a.cljc

(toolnexus.a2a/agent-tools
{:card "https://peer.example/.well-known/agent-card.json" ; required
:headers {"authorization" "Bearer ${PEER_TOKEN}"}
:timeout 300000
:poll-every 1000})
;; => [Tool ...] one per advertised skill, :source "a2a"
;; => [] the card could not be fetched — the reason is discarded

agent-tools is exactly (:tools (remote-agent opts)). It takes the same options, fetches the same card, and produces the same tools — it just throws the card and the error string away and hands you the vector, which is the shape :tools on toolnexus.core/build already wants.

Each returned tool is named sanitize(card name)_sanitize(skill id) and takes an object with a required string task. Executing one runs the full §7A cycle: one SendMessage, then GetTask polling until a terminal state, a timeout or an abort. Everything documented on remote-agent about output strings, metadata and error isolation applies unchanged, because it is the same code path.

The isolation guarantee is what makes the one-liner safe. A peer that is down, slow to hand back its card, or serving malformed JSON contributes [] — never a thrown exception, never a half-built toolkit. into a vector of peers and the toolkit is whatever peers were actually reachable at startup.

  • Composing a toolkit in one expression — peers spliced into :tools next to your native and HTTP tools, with no intermediate bindings.
  • Fan-out over several peers(into [] (mapcat agent-tools) peers) collects every reachable peer’s skills and quietly drops the rest.
  • You have already checked liveness elsewhere — a health endpoint or a startup probe is reporting peer status, so the per-call :error is redundant.

Both functions fetch the card at call time, so neither is free — call them once at startup and reuse the tools, rather than per request.

(require '[toolnexus.a2a :as a2a]
'[toolnexus.core :as core])
(def tk
(core/build {:mcp "examples/mcp.json"
:skills "examples/skills"
:tools (a2a/agent-tools
{:card "http://127.0.0.1:8080/.well-known/agent-card.json"})}))
(core/tool-names tk)
(require '[toolnexus.a2a :as a2a]
'[toolnexus.core :as core])
(def peers
[{:card "http://127.0.0.1:8080/.well-known/agent-card.json"}
{:card "http://127.0.0.1:8081/.well-known/agent-card.json" :timeout 5000}
;; deliberately dead — contributes [] and nothing else
{:card "http://127.0.0.1:9/.well-known/agent-card.json" :timeout 250}])
(def tk (core/build {:tools (into [] (mapcat a2a/agent-tools) peers)}))

Two peers up and one down yields the union of the two live catalogs. Note that §7A tool names collide the moment two cards share a name, and a later tool wins the collision — give your peers distinct card names.

(require '[toolnexus.a2a :as a2a]
'[toolnexus.core :as core]
'[koine.fs :as fs]
'[koine.json :as json])
(def cfg (json/read-str (fs/read-file "config.json")))
(def tk
(core/build {:tools (into [] (mapcat a2a/agent-tools)
(a2a/parse-agents-config (:agents cfg)))}))

parse-agents-config normalises the block into exactly the descriptors agent-tools accepts, drops disabled entries, and orders peers by name so two runtimes cannot disagree about who registered first.

Most of the time you do not need either call: toolnexus.core/build takes descriptors on :agents and resolves them itself — and reads a top-level agents block straight off a parsed :mcp config map.

(core/build {:agents [{:card "http://127.0.0.1:8080/.well-known/agent-card.json"}]})
(core/build {:mcp cfg}) ;; picks up cfg's "mcpServers" AND its "agents" block

Identical to remote-agent:

Key Default Meaning
:card Required. URL of the peer’s Agent Card.
:headers none Card GET and every JSON-RPC call. ${ENV_VAR} expanded, never logged.
:timeout 300000 Per-request and overall task budget, in ms.
:poll-every 1000 GetTask interval in ms. :pollEvery also accepted.
Shape When
[Tool ...] The card was fetched. One tool per entry in the card’s skills, in card order.
[] The card had no skills, or the fetch failed. The two are indistinguishable here.