Skip to content

listMcpTools — inventory without connecting

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

List what each configured server would expose, plus per-server status, without wiring it into a toolkit.

Split the question in two, because the two halves have genuinely different costs.

Is the config sane? That is offline, free and instant. toolnexus.mcp/parse-config hands back one map per server with the kind inferred, the enablement resolved and the timeout defaulted — no process, no socket. A misspelled server block, a server with neither command nor url, an entry you thought was enabled and is not: all visible here.

(require '[toolnexus.mcp :as mcp])
(def servers (mcp/parse-config (slurp "mcp.json")))
(mapv (juxt :name :kind :enabled :timeout) servers)
;; => [["docs" "remote" true 5000] ["fs" "local" true 30000] ["old" "unknown" false 30000]]
;; a "kind" of "unknown" means neither command nor url — that server can only fail
(filterv #(= "unknown" (:kind %)) servers)

What tools does it actually expose? That question cannot be answered without talking to the server — MCP has no static manifest, tools/list is a live RPC. So the honest answer is from-config, which connects and hands you the inventory and the per-server outcome in one value:

(require '[toolnexus.mcp :as mcp])
(def res (mcp/from-config (slurp "mcp.json")))
(try
;; the inventory
(mapv (juxt :server :remote-name :name) (:tools res))
;; => [["docs" "search" "docs_search"] ["fs" "read file" "fs_read_file"]]
;; the per-server outcome, in the same value
(:statuses res) ;; => {"docs" "connected" "fs" "connected" "old" "disabled"}
(:errors res) ;; => {} when everything worked
;; a doctor command is this and nothing more
(when-let [bad (seq (:errors res))]
(doseq [[server message] bad] (println "FAILED" server "-" message)))
(finally
(mcp/disconnect-all res)))

If what you actually want is the name a server’s tool will get — to write an allowlist, or to check a collision against a builtin — you can compute that without connecting, because SPEC §0.2 naming is pure:

(require '[toolnexus.mcp :as mcp])
(mcp/mcp-tool-name "remote api" "zebra note") ;; => "remote_api_zebra_note"
(mcp/mcp-tool-name "remote api" "alpha/stats") ;; => "remote_api_alpha_stats"

The one thing to avoid is treating a connect-and-disconnect as a cheap probe. Connecting spawns a child process or opens an HTTP session per server, serially; always pair it with mcp/disconnect-all in a finally, and keep the :timeout short if the probe is on a startup path.