toolnexus.serve/exposed-tools
Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §7C · clojure/src/toolnexus/serve.cljc
(toolnexus.serve/exposed-tools toolkit mcp-cfg)
;; mcp-cfg the :mcp profile map:;; :tools allowlist of tool NAMES; omitted ⇒ every toolkit tool;; :name :version serverInfo, defaults "toolnexus" / "0.1.0";; :protocolVersion default "2024-11-05"
;; => (Tool ...) sorted by :nameexposed-tools answers one question: which of a toolkit’s tools does the §7C inbound MCP profile
show to a connecting client. Given :tools in the profile it keeps exactly those names — unknown
names in the list are ignored, which §7C states explicitly — and given no :tools it keeps
everything. The result is sorted by name so two runtimes serving the same toolkit cannot disagree
about order.
It is also authoritative for calls, not just for listing. §7C’s text says only that mcp.tools
filters the list; ported literally, a tool hidden from tools/list would still be callable through
tools/call, turning what every reader takes for an allowlist into a cosmetic filter — on the one
surface whose entire job is exposing a toolkit to strangers. tools/call therefore resolves out of
this same filtered set, and an excluded tool answers -32602 Unknown tool: <name> exactly as a
nonexistent one does. That is a deliberate, security-motivated deviation, reported upward rather than
left for six ports to each guess at.
Tool names go out verbatim. They were already sanitized at registration, and re-sanitizing at a
gateway double-mangles names on every hop — so a toolkit holding calc.sum serves calc.sum, and
the string calc_sum appears nowhere. This is the opposite of the §7A/§7B rule, where skill ids
are sanitized, and the asymmetry is intentional.
On the wire, a call’s result becomes one {:type "text"} content part carrying the tool’s output,
with isError propagated. A tool that throws is caught by toolnexus.tool/execute and becomes an
error result — never an exception on the server thread.
When to use it
Section titled “When to use it”- Verifying an allowlist in a test — assert the exact set a profile exposes without opening a socket or speaking JSON-RPC.
- Rendering what a client will see — a startup log line or an admin page listing the served catalog.
- Deciding what to expose — diff
(exposed-tools tk {})against(exposed-tools tk cfg)to see precisely what an allowlist removes.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”What a profile actually exposes
Section titled “What a profile actually exposes”(require '[toolnexus.serve :as serve] '[toolnexus.core :as core])
(def tk (core/build {:mcp "examples/mcp.json" :skills "examples/skills"}))
;; no allowlist ⇒ everything, sorted(mapv :name (serve/exposed-tools tk {}))
;; an allowlist ⇒ exactly these; "no-such-tool" is ignored, not an error(mapv :name (serve/exposed-tools tk {:tools ["skill" "no-such-tool"]}));; => ["skill"]Serve the profile and connect to it
Section titled “Serve the profile and connect to it”(require '[toolnexus.core :as core] '[toolnexus.serve :as serve])
(def tk (core/build {:skills "examples/skills"}))
(def h (serve/serve tk {:port 0 :mcp {:name "tn-mcp" :version "1.0.0" :tools ["skill"]} :on-call (fn [c] (println "called" (:name c) "from" (:source c) "isError" (:isError c)))}))
;; POST <url>/mcp -> initialize | tools/list | tools/call;; No :a2a profile here, so the Agent Card path and POST / both 404.(println (str (:url h) "/mcp")):on-call fires once per inbound tools/call with {:name :source :isError} — enough for an
audit log. Result metadata is not on the MCP wire; :on-call is where you see it.
The three MCP methods, from a client’s side
Section titled “The three MCP methods, from a client’s side”(require '[koine.http :as http] '[koine.json :as json])
(defn rpc! [url method params] (-> (http/post-json url {} (json/write-str {:jsonrpc "2.0" :id 1 :method method :params params})) :body json/read-str :result))
(rpc! mcp-url "initialize" {});; => {:protocolVersion "2024-11-05" :capabilities {:tools {}};; :serverInfo {:name "tn-mcp" :version "1.0.0"}}
(rpc! mcp-url "tools/list" {});; => {:tools [{:name "skill" :description "..." :inputSchema {...}}]}
(rpc! mcp-url "tools/call" {:name "skill" :arguments {:name "hello-world"}});; => {:content [{:type "text" :text "<skill_content ...>"}] :isError false}
;; a tool the allowlist excluded answers exactly like one that does not exist:;; JSON-RPC error -32602 "Unknown tool: bash"Options (the :mcp profile map)
Section titled “Options (the :mcp profile map)”| Key | Default | Meaning |
|---|---|---|
:tools |
all | Allowlist of tool names. Unknown names ignored. Governs tools/list and tools/call. |
:name |
"toolnexus" |
serverInfo.name returned by initialize. |
:version |
"0.1.0" |
serverInfo.version. |
:protocolVersion |
"2024-11-05" |
Advertised MCP protocol version. |
What goes on the wire per tool
Section titled “What goes on the wire per tool”| MCP field | Source |
|---|---|
name |
(:name tool) — verbatim, never re-sanitized. |
description |
(:description tool). |
inputSchema |
(:input-schema tool), as registered. |
Error codes
Section titled “Error codes”| Code | Cause |
|---|---|
-32700 |
The request body was not decodable JSON. Answered on HTTP 200. |
-32601 |
A method other than initialize / tools/list / tools/call. |
-32602 |
tools/call for a name that is unknown or excluded by :tools. |
-32603 |
An internal error that escaped a handler. The server stays up. |
See also
Section titled “See also”toolnexus.serve/serve— mounts/mcpand calls thistoolnexus.mcp/from-config— the outbound direction: consuming someone else’s MCP servertoolnexus.core/build— filter at registration instead of at the gateway