Skip to content

Toolnexus.Mcp.load

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

@spec load(String.t() | map(), keyword()) :: Toolnexus.Mcp.Source.t()
def load(input, opts \\ [])
# %Toolnexus.Mcp.Source{
# tools: [%Toolnexus.Tool{}],
# status: %{String.t() => "connected" | "failed" | "disabled"},
# sup: pid(),
# connections: %{String.t() => pid()}
# }

Parses an mcp.json (path, raw JSON, or a map — see Toolnexus.Mcp.parse_config/1), then connects to every enabled server concurrently — local stdio and remote streamable-HTTP alike — and converts each server’s tools into uniform Toolnexus.Tool structs named sanitize(server)_sanitize(tool). One server failing to connect never takes the others down: it is isolated to a "failed" status entry, and load/2 still returns.

  • Wiring MCP servers into an agent — this is the whole point of the MCP source: config in, a flat list of callable Tool structs out.
  • You want per-server outcome, not just a tool liststatus tells you which servers connected, which were skipped as disabled, and which failed, so you can log or surface it.
  • You need the supervisorsup is a DynamicSupervisor holding every connection; pass the whole Source to close/1 when you are done to terminate stdio children cleanly.

If you only want the tool definitions without leaving anything connected — for an inventory, a doctor command, a CI check — use Toolnexus.Mcp.list_tools/2 instead; it connects, lists, and disconnects everything before returning.

1. The smallest useful call — a disabled server

Section titled “1. The smallest useful call — a disabled server”

enabled: false (or disabled: true) servers are skipped entirely — no connection attempt, no process spawned — and show up in status as "disabled".

alias Toolnexus.Mcp
config = %{
"mcpServers" => %{
"off" => %{"type" => "local", "command" => ["true"], "enabled" => false}
}
}
source = Mcp.load(config)
[] = source.tools
true = source.status == %{"off" => "disabled"}
true = is_pid(source.sup)
Mcp.close(source)
IO.puts("ok: #{inspect(source.status)}")

2. Per-server isolation — one disabled, one that fails to connect

Section titled “2. Per-server isolation — one disabled, one that fails to connect”

A server whose command cannot even start (or whose handshake fails) does not stop the others — it is recorded as "failed" and simply contributes no tools. This is what makes load/2 safe to call against config you do not fully trust.

alias Toolnexus.Mcp
config = %{
"mcpServers" => %{
"off" => %{"type" => "local", "command" => ["true"], "enabled" => false},
"broken" => %{
"type" => "local",
"command" => ["/nonexistent-toolnexus-docs-binary"],
"timeout" => 500
}
}
}
source = Mcp.load(config)
true = source.status["off"] == "disabled"
true = source.status["broken"] == "failed"
# Neither server contributed a tool.
[] = source.tools
# Only isolation matters here — a failed server still leaves the supervisor alive
# for the servers that DID connect.
true = Process.alive?(source.sup)
Mcp.close(source)
false = Process.alive?(source.sup)
IO.puts("ok: #{inspect(source.status)}")

3. Malformed config raises before anything connects

Section titled “3. Malformed config raises before anything connects”

load/2 calls parse_config/1 first — a JSON syntax error surfaces immediately, as a raise, not as a per-server "failed" status. No process is spawned.

alias Toolnexus.Mcp
raised? =
try do
Mcp.load(~s({not valid json))
false
rescue
_ -> true
end
true = raised?
IO.puts("ok: malformed config raised before connecting")
Option Default What it does
:wait_for nil (Request.t() -> Answer.t()). When set, the client advertises the MCP elicitation capability and bridges an inbound elicitation/create onto it (§10). See Toolnexus.Mcp.Protocol.elicitation_to_request.
:deadline (alias :timeout) :infinity Overall load deadline in ms. See the ctx-aware form.
Field Type What it is
tools [%Toolnexus.Tool{}] Every tool from every connected server, concatenated.
status %{name => status} "connected", "failed", or "disabled" per configured server.
sup pid() The DynamicSupervisor holding all connections — pass to close/1.
connections %{name => pid()} The connection process per connected server.