Skip to content

Toolnexus.Mcp.load (ctx-aware)

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

@spec load(String.t() | map(), keyword()) :: Toolnexus.Mcp.Source.t()
def load(input, opts \\ [])
# opts:
# :deadline (alias :timeout) — ms, default :infinity
# :wait_for — (Toolnexus.Request.t() -> Toolnexus.Answer.t())

There is no separate function here — Elixir’s load/2 is always the ctx-aware form; the plain call on the load page is this same function with the options omitted. This page is scoped to the two options that make it cancellation-aware: :deadline (bound the whole connect phase) and :wait_for (bridge inbound elicitation).

  • A hung or slow stdio server must not stall your agent’s boot. Without :deadline, a server that starts but never completes its initialize handshake blocks load/2 forever. With it, the whole load aborts at the deadline — in-flight connects are torn down (brutal_kill), no child process is left running.
  • A server wants to ask the human something mid-connect (a form, an OAuth URL) via MCP elicitation. Pass :wait_for and the client advertises the elicitation capability and routes the server’s elicitation/create through your handler as a §10 Toolnexus.Request/Toolnexus.Answer — see Toolnexus.Mcp.Protocol.elicitation_to_request for the exact mapping.
  • Both together — a :wait_for that itself might hang waiting on a human is exactly the case :deadline exists to bound.

1. Deadline exceeded aborts the whole load

Section titled “1. Deadline exceeded aborts the whole load”

["sleep", "5"] is a local process that never speaks MCP — it stands in for a hung server without touching the network. A :deadline shorter than that raises, and no server ends up connected.

alias Toolnexus.Mcp
config = %{
"mcpServers" => %{
"slow" => %{"type" => "local", "command" => ["sleep", "5"], "timeout" => 30_000}
}
}
{message, raised?} =
try do
Mcp.load(config, deadline: 100)
{nil, false}
rescue
e -> {Exception.message(e), true}
end
true = raised?
true = String.contains?(message, "deadline")
IO.puts("ok: #{message}")

2. :timeout is accepted as the same option

Section titled “2. :timeout is accepted as the same option”
alias Toolnexus.Mcp
config = %{
"mcpServers" => %{
"slow" => %{"type" => "local", "command" => ["sleep", "5"], "timeout" => 30_000}
}
}
raised? =
try do
# :timeout is read as a fallback for :deadline — same effect, same message.
Mcp.load(config, timeout: 100)
false
rescue
_ -> true
end
true = raised?
IO.puts("ok: :timeout aborted the load")

3. :wait_for alongside a bounded deadline, with server isolation intact

Section titled “3. :wait_for alongside a bounded deadline, with server isolation intact”

Passing :wait_for never changes how disabled or failing servers are handled — it only affects servers that actually elicit. Here nothing elicits, so this demonstrates the option is safe to always pass, combined with a real deadline.

alias Toolnexus.{Answer, Mcp, Request}
# A wait_for you would actually wire up: auto-decline every elicitation.
wait_for = fn %Request{id: id} -> %Answer{id: id, ok: false, reason: "declined"} end
config = %{
"mcpServers" => %{
"off" => %{"type" => "local", "command" => ["true"], "enabled" => false}
}
}
source = Mcp.load(config, wait_for: wait_for, deadline: 5_000)
true = source.status == %{"off" => "disabled"}
[] = source.tools
Mcp.close(source)
IO.puts("ok: wait_for + deadline together, disabled server unaffected")
Option Aliases Default Effect
:deadline :timeout :infinity Overall connect-phase bound in ms. On exceed: every in-flight connect task is killed (Task.shutdown/2 with :brutal_kill), the supervisor is stopped, and load/2 raises "toolnexus: MCP load deadline exceeded".
:wait_for nil (Request.t() -> Answer.t()). Advertises the elicitation MCP capability; an inbound elicitation/create is mapped through elicitation_to_request/1, your function is called, and the answer is mapped back. Without it, the capability is not advertised and a compliant server will not elicit.