Skip to content

toolnexus.a2a/parse-agents-config

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

(toolnexus.a2a/parse-agents-config
{:planner {:card "http://127.0.0.1:8080/.well-known/agent-card.json"
:headers {"authorization" "Bearer ${PEER_TOKEN}"}
:timeout 30000
:pollEvery 250}
:retired {:card "http://127.0.0.1:8082/.well-known/agent-card.json"
:disabled true}})
;; => [{:card "http://127.0.0.1:8080/.well-known/agent-card.json"
;; :headers {"authorization" "Bearer ${PEER_TOKEN}"}
;; :timeout 30000
;; :poll-every 250}]

parse-agents-config reads the top-level agents block of a parsed config object and returns a vector of descriptors — exactly the maps remote-agent and agent-tools accept. It is pure: no network, no card fetched, nothing connected. Feed the result to either of those to actually reach the peers.

The block is a name → descriptor map, mirroring mcpServers deliberately, and the same two disable spellings apply: :disabled true and :enabled false both drop an entry. So does an entry whose :card is missing or is not a string — that is not an agent, and §0.3’s isolation rule reaches config parsing too, so it is skipped rather than raised.

The output is ordered by name, not by insertion. Two runtimes reading the same config must not disagree about which peer registered first, because §7A tool names are derived from the card’s own name and the later registration wins a collision. Sorting by key makes that deterministic across Clojure and cljgo.

Config files spell the poll interval pollEvery; Clojure code spells it :poll-every. Both are read on the way in, and the descriptor always comes out kebab-case.

  • Peers live in a config file — the same document that carries mcpServers can carry an agents block, parsed here.
  • Feature-flagging a peer off:disabled true stops an entry producing tools without deleting it from the config.
  • Validating before connecting — the parse is offline, so a startup check can assert the shape of the block without a single network call.

It never fetches, so a descriptor coming out of here says nothing about whether the peer is alive. Liveness is remote-agent’s :error.

One config document, MCP servers and peers together

Section titled “One config document, MCP servers and peers together”
(require '[toolnexus.core :as core]
'[koine.json :as json]
'[koine.fs :as fs])
(def cfg (json/read-str (fs/read-file "config.json")))
;; {"mcpServers": {...}, "agents": {"planner": {"card": "..."}}}
;; build reads BOTH blocks off the same parsed map
(def tk (core/build {:mcp cfg}))

To take the peers but not the MCP servers, parse the block yourself and pass descriptors:

(require '[toolnexus.a2a :as a2a]
'[toolnexus.core :as core])
(def tk (core/build {:agents (a2a/parse-agents-config (:agents cfg))}))

Everything unusable is dropped, never raised

Section titled “Everything unusable is dropped, never raised”
(require '[toolnexus.a2a :as a2a])
(a2a/parse-agents-config nil) ;; => []
(a2a/parse-agents-config {}) ;; => []
(a2a/parse-agents-config {:nocard {:timeout 1}}) ;; => []
(a2a/parse-agents-config {:off {:card "http://a" :disabled true}}) ;; => []
(a2a/parse-agents-config {:off {:card "http://a" :enabled false}}) ;; => []

A malformed agents block degrades to “no peers”, which is still a working toolkit. Log the count yourself if a silently empty peer list would be a surprise.

(require '[toolnexus.a2a :as a2a])
(mapv :card (a2a/parse-agents-config {:zeta {:card "z-card"}
:mid {:card "m-card"}
:alfa {:card "c-card"}}))
;; => ["c-card" "m-card" "z-card"] sorted by the entry NAME: alfa, mid, zeta
;; resolve them with the failure reason visible, rather than silently empty
(doseq [d (a2a/parse-agents-config (:agents cfg))]
(let [peer (a2a/remote-agent d)]
(println (:card d)
(if (:error peer)
(str "DOWN: " (:error peer))
(str "ok, " (count (:tools peer)) " tools")))))
Key Effect
:card Required, must be a string. Absent or non-string ⇒ the entry is skipped.
:headers Copied through verbatim; ${ENV_VAR} is expanded later, at call time.
:timeout Copied through as :timeout.
:pollEvery / :poll-every Either spelling is read; the descriptor always carries :poll-every.
:disabled true skips the entry.
:enabled false skips the entry.

Keys other than these are not carried onto the descriptor.

Shape When
[{:card ... :headers? ... :timeout? ... :poll-every? ...} ...] Sorted by entry name. Optional keys appear only when the config supplied them.
[] The argument was not a map, was empty, or every entry was skipped.