Skip to content

toolnexus.serve/agent-card

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

(toolnexus.serve/agent-card cfg base skills)
;; cfg the :a2a profile map — :name :description :version :protocolVersion
;; :provider :skills (an allowlist of skill NAMES)
;; base the server's base URL; the card's `url` is (str base "/")
;; skills a SkillSource: a list-skills result, a name->info index, or a seq
;; => {:name "tn-agent" :description "" :version "0.1.0" :protocolVersion "0.3.0"
;; :capabilities {:streaming false :pushNotifications false}
;; :defaultInputModes ["text"] :defaultOutputModes ["text"]
;; :skills [{:id "hello_world" :name "hello-world" :description "..."}]
;; :url "http://127.0.0.1:8080/"}

agent-card builds the JSON document a peer fetches from /.well-known/agent-card.json to discover what you can do. serve calls it on every card request, so the card always reflects the live skill source; calling it yourself is for tests, static publication and admin views.

The advertised skills come from the toolkit’s SkillSource — never from its tools. That is deliberate: a peer asks an A2A agent for capabilities, not for a function catalog, and exposing every bash and grep on the card would be both noisy and wrong. Pass :skills in the profile to narrow it further; unknown names in that allowlist are ignored, matching §7C’s explicit rule for mcp.tools.

Two naming rules sit side by side here. A skill’s id is sanitized — that is the token peers concatenate into a tool name, so it must be safe. Its name is passed through untouched, because that is the human-readable label. Skills are emitted sorted by name so two runtimes cannot serve different cards from the same configuration.

:provider is present only when configured — absent, not null. Everything else has a spec default, which is why (agent-card {} base skills) already produces a valid card.

  • Asserting on the card in a test without standing up a server or making an HTTP request.
  • Publishing a static card — writing /.well-known/agent-card.json to a CDN or behind a gateway that terminates in front of your process.
  • An admin or registry view — rendering “here is what this agent advertises” from the same function the wire uses, so the page cannot drift from reality.

Note the base argument is yours to supply. serve passes its own resolved http://127.0.0.1:<port>, which is right for local peers and wrong the moment a reverse proxy or a public hostname is in front — in that case build the card yourself with the externally reachable base.

(require '[toolnexus.serve :as serve])
(serve/agent-card {} "http://127.0.0.1:8080"
[{:name "hello-world" :description "Greets the world."}])
;; => {:name "toolnexus-agent"
;; :description ""
;; :version "0.1.0"
;; :protocolVersion "0.3.0"
;; :capabilities {:streaming false :pushNotifications false}
;; :defaultInputModes ["text"] :defaultOutputModes ["text"]
;; :skills [{:id "hello_world" :name "hello-world" :description "Greets the world."}]
;; :url "http://127.0.0.1:8080/"}

Note :capabilities — this port serves neither streaming nor push notifications, and says so truthfully rather than advertising a capability it would then fail to honour.

Section titled “Advertise a subset, from a real skill source”
(require '[toolnexus.skill :as skill]
'[toolnexus.serve :as serve])
(def skills (skill/list-skills "examples/skills"))
(serve/agent-card {:name "release-bot"
:description "Cuts releases"
:version "1.2.0"
:provider {:organization "toolnexus"
:url "https://example.invalid"}
;; only these names are advertised; unknown names are ignored
:skills ["hello-world" "no-such-skill"]}
"https://agents.example.com/release-bot"
skills)

agent-card accepts the {:skills [...] :skipped [...]} shape list-skills returns, a name→info index, or a plain sequence of maps with :name and :description — so a data-defined skill list works without touching disk.

(require '[koine.fs :as fs]
'[koine.json :as json]
'[toolnexus.serve :as serve])
(fs/write-file "public/.well-known/agent-card.json"
(json/write-str
(serve/agent-card {:name "release-bot" :version "1.2.0"}
"https://agents.example.com" ;; the PUBLIC base
skills)))

The card’s url becomes https://agents.example.com/, which is where peers will POST — so it has to be the address that reaches your serve process, not the loopback one it bound to.

Key Default Meaning
:name "toolnexus-agent" The agent name. Peers sanitize it into the prefix of every tool name they derive.
:description "" Free text.
:version "0.1.0" Your agent’s version.
:protocolVersion "0.3.0" The A2A protocol version advertised.
:provider absent Any map, e.g. {:organization ... :url ...}. Omitted from the card entirely when unset.
:skills all Allowlist of skill names. Unknown names are ignored, not errors.
Field Source
:capabilities Always {:streaming false :pushNotifications false} — this port implements neither.
:defaultInputModes / :defaultOutputModes Always ["text"].
:skills From the SkillSource, filtered, sorted by name. :id is sanitized; :name is verbatim; :description defaults to "".
:url (str base "/") — the JSON-RPC POST endpoint.