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 clientOne 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.
When to use it
Section titled “When to use it”- 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-promptreads the catalogbuildattached, so the system prompt and theskilltool cannot disagree about what exists. - When you are about to hand the tools to a provider.
to-openai,to-anthropicandto-geminiare re-exported here, so a caller needs onerequirerather than four.
Why this and not the alternative
Section titled “Why this and not the alternative”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.
Options
Section titled “Options”| 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.
What you get back
Section titled “What you get back”| 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. |
Examples
Section titled “Examples”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"})) ;; => falseA :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; idempotentshutdown! 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.
See also
Section titled “See also”toolnexus.mcp/from-config— the MCP source on its owntoolnexus.mcp/parse-config— validate anmcp.jsonwithout connectingtoolnexus.tool/tool— the shape everything in:toolshastoolnexus.native/native-tool— build the Tools you pass as:toolstoolnexus.client/wait-for— the §10 resolver:wait-forexpectstoolnexus.mcp/elicitation->request— what that resolver receives from a server- OpenAI adapter — what
to-openaiemits