Skip to content

Toolnexus.Mcp.list_tools

Elixir · package toolnexus · SPEC §2 · elixir/lib/toolnexus/mcp.ex

@spec list_tools(String.t() | map(), keyword()) :: Toolnexus.Mcp.Inventory.t()
def list_tools(input, opts \\ [])
# %Toolnexus.Mcp.Inventory{
# tools: %{String.t() => [%{"name" => String.t(), "description" => String.t(), "inputSchema" => map()}]},
# status: %{String.t() => "connected" | "failed" | "disabled"}
# }

Connects to every enabled server, calls tools/list, and disconnects everything before returning — no supervisor to close, no connection left running. Unlike Toolnexus.Mcp.load/2, the tool names in the result are the servers’ original, unprefixed names, and any per-server "tools" allowlist in the config is ignored — this is a raw inventory of what a server would expose, not what an agent would actually see wired up.

  • Inspect a config before committing to it — see what each server offers, without holding a process open or filtering by an allowlist you have not written yet.
  • Author a tools allowlist — call list_tools/2 first to see every tool’s real name, then write the per-server "tools" map that load/2 will apply.
  • A doctor / CI check — assert a server’s tool surface hasn’t silently changed, with nothing left connected afterward.

1. A disabled server contributes nothing, and nothing is left running

Section titled “1. A disabled server contributes nothing, and nothing is left running”
alias Toolnexus.Mcp
config = %{
"mcpServers" => %{
"off" => %{"type" => "local", "command" => ["true"], "enabled" => false}
}
}
inventory = Mcp.list_tools(config)
true = inventory.status == %{"off" => "disabled"}
true = inventory.tools == %{}
IO.puts("ok: #{inspect(inventory.status)}")

2. Per-server isolation — a failing server doesn’t block the others’ entries

Section titled “2. Per-server isolation — a failing server doesn’t block the others’ entries”
alias Toolnexus.Mcp
config = %{
"mcpServers" => %{
"off" => %{"type" => "local", "command" => ["true"], "enabled" => false},
"broken" => %{
"type" => "local",
"command" => ["/nonexistent-toolnexus-docs-binary"],
"timeout" => 500
}
}
}
inventory = Mcp.list_tools(config)
true = inventory.status["off"] == "disabled"
true = inventory.status["broken"] == "failed"
# A failed server has no entry in `tools` at all — not even an empty list.
false = Map.has_key?(inventory.tools, "broken")
IO.puts("ok: #{inspect(inventory.status)}")

3. Malformed config raises, and a deadline bounds a hung server

Section titled “3. Malformed config raises, and a deadline bounds a hung server”

list_tools/2 parses config the same way load/2 does — a JSON error raises before anything connects. It also accepts the same :deadline/:timeout option, since listing has the identical “one server can hang” problem as loading does.

alias Toolnexus.Mcp
raised_on_bad_json? =
try do
Mcp.list_tools(~s({not valid json))
false
rescue
_ -> true
end
true = raised_on_bad_json?
config = %{
"mcpServers" => %{
"slow" => %{"type" => "local", "command" => ["sleep", "5"], "timeout" => 30_000}
}
}
{message, raised_on_deadline?} =
try do
Mcp.list_tools(config, deadline: 100)
{nil, false}
rescue
e -> {Exception.message(e), true}
end
true = raised_on_deadline?
true = String.contains?(message, "deadline")
IO.puts("ok: malformed config raised, deadline exceeded raised (#{message})")
Option Aliases Default Effect
:deadline :timeout :infinity Overall bound in ms across all servers; on exceed, raises "toolnexus: MCP list deadline exceeded". Same semantics as load/2’s deadline.
Field Type What it is
tools %{server => [%{"name", "description", "inputSchema"}]} Original (unprefixed) tool definitions per connected server only.
status %{server => status} "connected", "failed", or "disabled" per configured server.