A client for MCP servers.
direct/2 dispatches to a runtime in the calling process. connect/2 opens
a stdio subprocess or a Streamable HTTP endpoint:
{:ok, client} = Snodo.Client.direct(EchoServer.runtime())
{:ok, client} = Snodo.Client.connect({:stdio, "elixir", ["echo_server.exs"]})
{:ok, client} = Snodo.Client.connect({:http, "http://127.0.0.1:4000/mcp"})
{:ok, [%{"name" => "echo"}]} = Snodo.Client.list_tools(client)
{:ok, result} = Snodo.Client.call_tool(client, "echo", %{"text" => "hello"})
result["content"]
#=> [%{"type" => "text", "text" => "hello"}]The client builds each request, including the metadata the selected protocol dialect requires, and decodes the response into one of:
{:ok, result}: the JSON-RPCresultobject as the server sent it, with string keys. Atools/callresult with"isError" => trueis a successful response and arrives here.{:input_required, result}: a multi round-trip request. Call again withinput_responses:and, when the result carries a"requestState",request_state:. See the interactive operations guide.{:error, %Snodo.Error{}}: a JSON-RPC error or a transport failure. For a JSON-RPC error,code,message, anddataare the server's, andkindis derived from the code: -32700 and -32600 are:json_rpc, -32601 and -32602 are:protocol, -32603 is:execution, and any other code is:protocol. Transport failures havekind: :transport: -32000 when the connection is closed, unreachable, or returns something that is not a JSON-RPC response, and -32001 when a request times out.
Each request takes a fresh integer ID, so one client can be used from many
processes at once. The client speaks the stateless 2026-07-28 protocol;
initialize-era servers, which need a session handshake, are not supported.
subscriptions/listen streams and progress notifications are not delivered.
Summary
Functions
Calls a tool, by name or with its definition from list_tools/1.
Closes the client's connection. Closing an in-process client does nothing.
Connects to a server in another process or on the network.
Builds a client that dispatches to runtime in the calling process.
Requests server/discover.
Gets a rendered prompt. Options are those of request/4.
Requests one page of a list operation.
Lists every prompt, following nextCursor until the last page.
Lists every resource template, following nextCursor until the last page.
Lists every direct resource, following nextCursor until the last page.
Lists every tool, following nextCursor to the last page.
Reads a resource by exact URI. Options are those of request/4.
Sends any request method with the given params.
Types
@type list_kind() :: :tools | :resources | :resource_templates | :prompts
@type response() :: {:ok, map()} | {:input_required, map()} | {:error, Snodo.Error.t()}
Functions
Calls a tool, by name or with its definition from list_tools/1.
Options are those of request/4. A result with "isError" => true is
returned as {:ok, result}: the tool ran and reported its own failure.
Over HTTP, arguments whose input schema property carries x-mcp-header
are also sent as Mcp-Param-* headers, which needs the tool's
inputSchema. Pass the definition map to send them on the first request.
Called by name, a tool that requires them is refused with -32020; the
client then lists the tools and retries once with the definition.
@spec close(t()) :: :ok
Closes the client's connection. Closing an in-process client does nothing.
@spec connect(target(), keyword()) :: {:ok, t()} | {:error, Snodo.Error.t()}
Connects to a server in another process or on the network.
Targets:
{:stdio, command, args}- runscommandand speaks newline-delimited JSON-RPC over its stdin and stdout. SeeSnodo.Client.Stdiofor:envand:cd. The connection closes when the calling process exits.{:http, url}- posts each request to a Streamable HTTP endpoint. SeeSnodo.Client.HTTPfor:headers,:ssl, and:connect_timeout.{module, init_arg}- anySnodo.Client.Transport.
Options for every target:
:protocol- defaults to"2026-07-28", the only supported version.:client_capabilitiesand:client_info- as fordirect/2.:timeout- the default request timeout in milliseconds, 30,000 unless set. Each request can override it withtimeout:.
@spec direct(Snodo.Server.Runtime.t(), keyword()) :: {:ok, t()} | {:error, Snodo.Error.t()}
Builds a client that dispatches to runtime in the calling process.
Options:
:protocol- the protocol version to speak. Defaults to the first stateless-era version the runtime enables. Initialize-era versions need a session, which a direct client does not hold, so they are refused.:client_capabilities- the capabilities sent with every request, for example%{"elicitation" => %{"form" => %{}}}. Defaults to%{}.:client_info- theImplementationsent asio.modelcontextprotocol/clientInfowith every request: a map with string"name"and"version"and, optionally,"title","description","websiteUrl", and"icons". Defaults to%{"name" => "snodo", "version" => <this library's version>}.:auth- the value handlers and authorization policies read ascontext.auth, as a transport would supply it after authenticating.
Requests server/discover.
Gets a rendered prompt. Options are those of request/4.
@spec list_page(t(), list_kind(), String.t() | nil) :: {:ok, Snodo.Client.Page.t()} | {:error, Snodo.Error.t()}
Requests one page of a list operation.
kind is one of :tools, :resources, :resource_templates, or
:prompts. Pass the previous page's next_cursor to continue.
@spec list_prompts(t()) :: {:ok, [map()]} | {:error, Snodo.Error.t()}
Lists every prompt, following nextCursor until the last page.
@spec list_resource_templates(t()) :: {:ok, [map()]} | {:error, Snodo.Error.t()}
Lists every resource template, following nextCursor until the last page.
@spec list_resources(t()) :: {:ok, [map()]} | {:error, Snodo.Error.t()}
Lists every direct resource, following nextCursor until the last page.
@spec list_tools(t()) :: {:ok, [map()]} | {:error, Snodo.Error.t()}
Lists every tool, following nextCursor to the last page.
Over HTTP, a tool whose input schema has an invalid x-mcp-header
annotation is left out and a warning is logged, as 2026-07-28 requires.
Reads a resource by exact URI. Options are those of request/4.
Sends any request method with the given params.
Use it for methods without a dedicated function, such as
completion/complete or a negotiated extension's methods. The dialect's
request metadata is merged under params["_meta"]; keys already present in
params["_meta"] win.
Options:
:input_responses- answers to a previous{:input_required, result}, keyed by the IDs in its"inputRequests". Sent asinputResponses.:request_state- the"requestState"of a previous{:input_required, result}. Sent asrequestState.:meta- extra_metaentries, such as a"progressToken". These win over the dialect's metadata and overparams["_meta"].
subscriptions/listen raises ArgumentError: it needs a stream to deliver
events on, and dispatching it would open the application's source.