Skip to content

toolnexus.core/build

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

(toolnexus.core/build
{:mcp "…mcp.json…" ; a JSON string or an already-parsed map (§2/§0.3)
:skills "./skills" ; a root, a seq of roots, or a seq of skill defs (§3)
:skill-defs [{:name "triage" :description "" :content ""}] ; skills as data (§3 S1)
:skill-provider (fn [] [{...}]) ; a 0-arg fn producing more defs (§3 S1)
:skills-filter {"triage" true} ; name -> bool over the catalog (§3 S2)
:skill-sample-limit 0 ; 0 default · n>0 cap · -1 no <skill_files> (§3 S5)
:disable-skills ["legacy"] ; sugar over a :skills-filter drop-list
:builtins {} ; false | {:disabled ..} | {:enabled ..} | {:tools {..}}
:agents [{:card "…/agent-card.json" :timeout 3000}] ; remote A2A (§7A)
:tools [a-tool] ; extra Tools of your own
:disable-tools ["bash" "mine"] ; final exposed names to drop, across every source
:wait-for (fn [request] answer)}) ; the ONE §10 host resolver
;; =>
{:tools {"read" {...} "skill" {...} "docs_search" {...}} ; name -> Tool
:sources {"builtin" "connected"}
:skills {...} ; the loaded skill catalog
:statuses {"builtin" "connected" "docs" "connected" "old" "failed"}
:errors {"old" "mcp server \"old\" failed at initialize: transport (connect-failed)"}
:connections [{...}] ; live MCP connections
:wait-for #function[...]} ; carried through for the client

One call, every source, one flat namespace of tools. build is the integration layer: it folds every skill-shaped option into a single catalog and one skill tool, resolves A2A agent cards, connects MCP servers, adds the builtins and your own Tools, applies the drop-list, and hands back a toolkit that toolnexus.core/execute and the adapters consume unchanged.

Registration order is the contract, not a detail. toolnexus.tool/add-tools lets a later tool win a name collision, and SPEC §0.11 requires MCP to take precedence over a builtin of the same name — so the order is builtins, then the skill tool, then agents, then your :tools, then MCP last. :disable-tools is applied after all of them, by final exposed name, so it can drop anything from any source.

Every source is isolated. A source that fails contributes an entry in :errors and a status in :statuses, never an exception: a malformed :mcp string, a server that will not start, a :skill-provider that throws, an unreachable agent card. Each leaves you with a working toolkit containing everything that did load. That is the whole point of the status map.

  • The normal entry point for an application. With more than one source, this is the call that gets precedence, filtering and isolation right for you.
  • When you want the skills preamble. toolnexus.core/skills-prompt reads the catalog build attached, so the system prompt and the skill tool cannot disagree about what exists.
  • When you are about to hand the tools to a provider. to-openai, to-anthropic and to-gemini are re-exported here, so a caller needs one require rather than four.

Note that build opens things: a connected MCP server is a child process or a live HTTP session. Pair it with toolnexus.core/shutdown!, which closes every connection the toolkit opened and is idempotent.

Option Effect
:mcp An mcp.json string or a parsed map. Servers are connected serially in name order — see from-config. Registered last, so MCP wins a name collision (§0.11). A top-level agents block on this map is honoured too, mirroring how mcpServers is read off the same object.
:skills A skills root, a seq of roots, or a seq of skill defs.
:skill-defs Skills supplied as data, bypassing the filesystem (§3 S1).
:skill-provider A 0-arg fn producing skill defs, resolved during the build. One that throws contributes nothing and every other source still loads.
:skills-filter name -> bool over the catalog (§3 S2). One or more true entries make it an allowlist; a false entry drops that name.
:skill-sample-limit 0 ⇒ the default of 10 · n>0 ⇒ cap · -1 ⇒ omit the <skill_files> block entirely (§3 S5).
:disable-skills Names to drop from the catalog. Folded in first, so an explicit :skills-filter entry overrides it — the merge order is the rule.
:builtins The §0.11 toggle. false, {:disabled true} or {:enabled false} turn the whole source off; {:tools {:bash false}} drops individual tools from the all-on baseline of ten. Absent means on.
:agents Remote A2A agents (§7A). Each advertised skill becomes a tool named sanitize(card-name)_sanitize(skill-id). A failing agent contributes no tools and never a throw.
:tools Extra Tools of your own — native, HTTP, A2A, anything.
:disable-tools Final exposed names to drop, applied after the merge across every source. An empty vector changes nothing.
:wait-for The one §10 host resolver, (fn [request] answer). Threaded into the MCP source so a connected server may elicit input mid-tools/call; absent, the elicitation capability is never advertised.

The three skill sources compose: :skills, :skill-defs and :skill-provider are resolved in that order into one catalog, and the existing first-name-wins rule settles collisions across them without a second rule.

Key What it is
:tools name -> Tool, the merged namespace after :disable-tools. toolnexus.core/tool-names returns the sorted names.
:sources The source-status map the builtin toolkit was seeded with.
:skills The loaded skill catalog — read by skills-prompt and by the skill tool. {} when no skill option was given.
:statuses "builtin" plus one entry per MCP server: "connected", "disabled" or "failed".
:errors server -> message for the failures only, including "<config>" when :mcp itself could not be parsed.
:connections The live MCP connections, for shutdown!.
:wait-for The resolver you passed, carried through so the client can use the same one.

1. Three sources at once, in one flat namespace

Section titled “1. Three sources at once, in one flat namespace”
(require '[toolnexus.core :as tn]
'[toolnexus.tool :as tool])
(def tk
(tn/build {:skills "./skills"
:builtins {:tools {:bash false}} ; nine builtins, not ten
:tools [(tool/tool {:name "mine" :source "native"
:execute (fn [_] (tool/success "ok"))})]}))
(contains? (:tools tk) "skill") ;; => true — the §3 progressive-disclosure tool
(contains? (:tools tk) "read") ;; => true — a builtin
(contains? (:tools tk) "mine") ;; => true — yours
(contains? (:tools tk) "bash") ;; => false — dropped by the per-tool toggle
(get (:statuses tk) "builtin") ;; => "connected"

Turning the source off entirely is :builtins false, and then the :tools map is never consulted at all — a whole-source-off short-circuits, so {:disabled true :tools {:bash true}} still yields nothing.

2. Shaping the catalog: data, a provider, and a filter

Section titled “2. Shaping the catalog: data, a provider, and a filter”
(require '[toolnexus.core :as tn])
(def tk
(tn/build {:builtins false
:skills "./skills"
:skill-defs [{:name "triage" :description "Triage a bug report"
:content "1. Reproduce it."}]
:skill-provider (fn [] [{:name "release" :description "Cut a release"
:content "1. Bump the version."}])
:disable-skills ["legacy"]}))
;; the prompt catalog is the union of all three sources, minus the drop-list
(tn/skills-prompt tk)
;; => "## Available Skills\n- **hello-world**: …\n- **triage**: …\n- **release**: …"
;; the `skill` tool and the prompt always agree — a dropped skill is not loadable
(:isError (tn/execute tk "skill" {"name" "legacy"})) ;; => true
;; an explicit filter entry beats :disable-skills, because the filter is merged second
(def tk2 (tn/build {:builtins false :skill-defs [{:name "legacy" :description "d" :content "b"}]
:disable-skills ["legacy"] :skills-filter {"legacy" true}}))
(:isError (tn/execute tk2 "skill" {"name" "legacy"})) ;; => false

A :skill-provider that throws is isolated exactly the way a dead MCP server is: it contributes nothing, and the directory and data sources still load.

3. The whole pipeline — build, prompt, adapt, shut down

Section titled “3. The whole pipeline — build, prompt, adapt, shut down”
(require '[toolnexus.core :as tn]
'[toolnexus.client :as client])
(def tk
(tn/build {:mcp (slurp "mcp.json")
:skills "./skills"
:builtins {}
:agents [{:card "https://peer.example/.well-known/agent-card.json"
:timeout 3000}]
:disable-tools ["bash"]
:wait-for (fn [request] (client/make-answer (:id request) true))}))
(try
;; the §3/§0.6 preamble for the system prompt, derived from the same catalog
;; the `skill` tool reads — the two cannot drift
(tn/skills-prompt tk)
;; one tool set, three wire formats; all three agree on contents AND on order
(count (tn/to-openai tk))
(count (tn/to-anthropic tk))
(count (:functionDeclarations (first (tn/to-gemini tk))))
(tn/tool-names tk) ;; sorted, so two runtimes emit the same array
(tn/execute tk "read" {:path "README.md"})
(finally
(tn/shutdown! tk))) ;; closes every MCP connection; idempotent

shutdown! matters more than it looks. A connected stdio server is a child process, and while this port is careful never to hold your process open itself — no future in library code, ever — the child is yours to close.