# `Snodo.Client`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.1.0/lib/snodo/client.ex#L1)

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-RPC `result` object as the server sent it, with
    string keys. A `tools/call` result with `"isError" => true` is a
    successful response and arrives here.
  * `{:input_required, result}`: a multi round-trip request. Call again with
    `input_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`, and `data` are the server's, and
    `kind` is 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 have `kind: :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.

# `list_kind`

```elixir
@type list_kind() :: :tools | :resources | :resource_templates | :prompts
```

# `response`

```elixir
@type response() ::
  {:ok, map()} | {:input_required, map()} | {:error, Snodo.Error.t()}
```

# `t`

```elixir
@type t() :: %Snodo.Client{
  client_capabilities: map(),
  client_info: map(),
  dialect: module(),
  protocol: String.t(),
  timeout: timeout(),
  transport: {module(), Snodo.Client.Transport.state()}
}
```

# `target`

```elixir
@type target() ::
  {:stdio, String.t(), [String.t()]} | {:http, String.t()} | {module(), term()}
```

# `call_tool`

```elixir
@spec call_tool(t(), String.t() | map(), map(), keyword()) :: response()
```

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.

# `close`

```elixir
@spec close(t()) :: :ok
```

Closes the client's connection. Closing an in-process client does nothing.

# `connect`

```elixir
@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}` - runs `command` and speaks newline-delimited
    JSON-RPC over its stdin and stdout. See `Snodo.Client.Stdio` for `:env`
    and `:cd`. The connection closes when the calling process exits.
  * `{:http, url}` - posts each request to a Streamable HTTP endpoint. See
    `Snodo.Client.HTTP` for `:headers`, `:ssl`, and `:connect_timeout`.
  * `{module, init_arg}` - any `Snodo.Client.Transport`.

Options for every target:

  * `:protocol` - defaults to `"2026-07-28"`, the only supported version.
  * `:client_capabilities` and `:client_info` - as for `direct/2`.
  * `:timeout` - the default request timeout in milliseconds, 30,000 unless
    set. Each request can override it with `timeout:`.

# `direct`

```elixir
@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` - the `Implementation` sent as
    `io.modelcontextprotocol/clientInfo` with 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 as
    `context.auth`, as a transport would supply it after authenticating.

# `discover`

```elixir
@spec discover(t()) :: response()
```

Requests `server/discover`.

# `get_prompt`

```elixir
@spec get_prompt(t(), String.t(), map(), keyword()) :: response()
```

Gets a rendered prompt. Options are those of `request/4`.

# `list_page`

```elixir
@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.

# `list_prompts`

```elixir
@spec list_prompts(t()) :: {:ok, [map()]} | {:error, Snodo.Error.t()}
```

Lists every prompt, following `nextCursor` until the last page.

# `list_resource_templates`

```elixir
@spec list_resource_templates(t()) :: {:ok, [map()]} | {:error, Snodo.Error.t()}
```

Lists every resource template, following `nextCursor` until the last page.

# `list_resources`

```elixir
@spec list_resources(t()) :: {:ok, [map()]} | {:error, Snodo.Error.t()}
```

Lists every direct resource, following `nextCursor` until the last page.

# `list_tools`

```elixir
@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.

# `read_resource`

```elixir
@spec read_resource(t(), String.t(), keyword()) :: response()
```

Reads a resource by exact URI. Options are those of `request/4`.

# `request`

```elixir
@spec request(t(), String.t(), map(), keyword()) :: response()
```

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 as `inputResponses`.
  * `:request_state` - the `"requestState"` of a previous
    `{:input_required, result}`. Sent as `requestState`.
  * `:meta` - extra `_meta` entries, such as a `"progressToken"`. These win
    over the dialect's metadata and over `params["_meta"]`.

`subscriptions/listen` raises `ArgumentError`: it needs a stream to deliver
events on, and dispatching it would open the application's source.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
