Noizu.MCP.Client is a supervised GenServer: one client per server
connection, addressable by name or pid.
children = [
{Noizu.MCP.Client,
name: MyApp.FS,
transport: {:stdio,
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]},
handler: MyApp.MCPHandler,
client_info: %{name: "myapp", version: "1.0.0"}}
]Transports:
transport: {:stdio, command: "...", args: [...]}
transport: {:streamable_http, url: "https://api.example.com/mcp"}
transport: {:streamable_http, url: ..., auth: {Noizu.MCP.Auth.Static, token: token}}
transport: {MyTransport, opts} # any Noizu.MCP.Transport.Client implRequests issued before the handshake completes are queued; use
Noizu.MCP.Client.await_ready(client, timeout) when you need to block on
connection (e.g. in scripts).
Calling tools
{:ok, tools} = Noizu.MCP.Client.list_tools(MyApp.FS)
{:ok, result} =
Noizu.MCP.Client.call_tool(MyApp.FS, "read_file", %{"path" => "/tmp/a.txt"},
timeout: 60_000,
progress: fn %{"progress" => p} -> ProgressBar.update(p) end)
result.content # [%Noizu.MCP.Types.Content{}]
result.structured # structuredContent map or nil
result.is_error # execution errors come back as results, not {:error, _}Passing progress: automatically attaches a progress token; the callback
runs in its own task. A request that exceeds timeout: is cancelled on
the server (notifications/cancelled) and returns {:error, :timeout}.
Note that list_tools/1 only shows what the server chooses to list — servers
may keep tools hidden yet callable by name (a noizu_mcp server's catalog
tool, when registered, reveals them; see
Toolkits, Categories & Hidden Tools).
The rest of the surface
{:ok, resources} = Noizu.MCP.Client.list_resources(client)
{:ok, templates} = Noizu.MCP.Client.list_resource_templates(client)
{:ok, contents} = Noizu.MCP.Client.read_resource(client, "config://app")
:ok = Noizu.MCP.Client.subscribe_resource(client, "config://app")
:ok = Noizu.MCP.Client.unsubscribe_resource(client, "config://app")
{:ok, prompts} = Noizu.MCP.Client.list_prompts(client)
{:ok, %{messages: messages}} = Noizu.MCP.Client.get_prompt(client, "code_review", %{"code" => src})
{:ok, completion} =
Noizu.MCP.Client.complete(client, {:prompt, "code_review"}, "style", "fr")
:ok = Noizu.MCP.Client.set_log_level(client, :warning)
{:ok, %{}} = Noizu.MCP.Client.ping(client)
Noizu.MCP.Client.server_info(client) # %{name: ..., version: ...}
Noizu.MCP.Client.server_capabilities(client)
Noizu.MCP.Client.instructions(client)list_* functions auto-paginate by default; pass page: :first (then
page: cursor) for manual paging. Generic escape hatches: request/4,
notify/3.
Async requests
ref = Noizu.MCP.Client.async(client, "tools/call", %{"name" => "slow", "arguments" => %{}})
# ... do other work ...
case Noizu.MCP.Client.await(client, ref, 5_000) do
{:ok, result} -> result
{:error, :timeout} -> Noizu.MCP.Client.cancel(client, ref, "took too long")
endHandling server-initiated requests
Servers may call you: LLM sampling, user elicitation, roots listing.
Implement Noizu.MCP.Client.Handler — the capabilities you implement are
the capabilities the client advertises:
defmodule MyApp.MCPHandler do
@behaviour Noizu.MCP.Client.Handler
@impl true
def handle_sampling(params, _state) do
text = MyApp.LLM.complete(params["messages"], max_tokens: params["maxTokens"])
{:ok, %{"role" => "assistant",
"content" => %{"type" => "text", "text" => text},
"model" => "my-model"}}
end
@impl true
def handle_elicitation(%{"message" => msg}, _state) do
case MyApp.UI.confirm(msg) do
{:confirmed, fields} -> {:ok, :accept, fields}
:declined -> {:ok, :decline}
:dismissed -> {:ok, :cancel}
end
end
@impl true
def handle_notification(method, params, _state) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "mcp", {method, params})
:ok
end
endPass it as handler: MyApp.MCPHandler or handler: {MyApp.MCPHandler, state}.
Handler callbacks run in supervised tasks — blocking is fine.
Roots are managed on the client itself (the server is notified of changes automatically):
:ok = Noizu.MCP.Client.set_roots(client, [%Noizu.MCP.Types.Root{uri: "file:///work", name: "work"}])Or implement list_roots/1 on the handler for dynamic roots. To mirror raw
server notifications to a process instead of (or in addition to) a handler,
pass on_notification: pid — messages arrive as
{:mcp_notification, method, params}.
Errors
All functions return {:ok, _} | {:error, reason} where reason is a
%Noizu.MCP.Error{} (protocol error from the server), :timeout,
:closed, or a transport-specific term. Telemetry mirrors the server:
[:noizu_mcp, :client, :request, :start | :stop | :exception].