# `Sake.Client`
[🔗](https://github.com/hattoriai/sake/blob/v0.4.3/lib/sake/client.ex#L1)

A client for MCP servers over Streamable HTTP or stdio, on both protocol
eras.

    {:ok, session} = Sake.Client.connect("https://mcp.example.com/mcp", headers: [{"authorization", "Bearer ..."}])
    {:ok, tools, session} = Sake.Client.list_tools(session)
    {:ok, result, session} = Sake.Client.call_tool(session, "search", %{"query" => "x"})

The client is functional: every call returns the updated session. Pass
`on_input:` to answer elicitation requests, `sampling:` to answer
sampling requests (`fn params, session -> {:ok, result} | {:error,
error} end`), `roots:` to share roots (a list of `%{"uri" => ...,
"name" => ...}` maps, or a function of the session that returns one),
`on_notification:` (a function of a method and its params) to see the
progress and log notifications of a call, `log_level:` (a level name
such as `"info"`) to have the server send log messages at or above it
(see `set_log_level/2`), and `auth:` to have the client
run the MCP authorization flow itself on a 401 (see `Sake.Client.Auth`);
without it, a 401 comes back as
`{:error, {:unauthorized, %Sake.Auth.Challenge{}}}`, or, when its
challenge is RFC 9470's `insufficient_user_authentication` (the person
must sign in more strongly or more recently), as
`{:error, {:insufficient_user_authentication, %Sake.Auth.Challenge{}}}`.

On the modern protocol the client declares the tasks extension, so a
server may answer a tool call with a task; `call_tool/3` follows it to
the end (`task_timeout:`, in ms, default five minutes), and
`call_tool_async/3` hands it back instead. `tasks: false` leaves the
extension out.

`max_response_size:` (bytes, default 16 MiB) caps what the client reads
of one HTTP response: a JSON body, or one Server-Sent Event still being
received. A server that sends more ends the request with
`{:error, :response_too_large}`.

`ui: true` declares MCP Apps (see `Sake.UI`), on both eras, for a client
that renders them: the server then lists its app-only tools and UI
resources, and each `Sake.Tool.Spec` from `list_tools/1` carries the
`ui://` URI of its app in `ui` and who may call it in `visibility`.

`{:stdio, command, args}` in place of a URL launches the server as a
subprocess and talks to it over its standard input and output:

    {:ok, session} = Sake.Client.connect({:stdio, "npx", ["-y", "@modelcontextprotocol/server-everything"]})

Stdio options: `env:` (a map or list of name-value pairs; a `nil` value
unsets the variable), `cd:`, `stderr:` (`:inherit`, the default, shares
this VM's standard error; `:ignore` discards it), `probe_timeout:` (ms
to wait for an answer to the modern probe before falling back to
`initialize`, default 10 000), `close_timeout:` (ms `close/1` waits for
the server to exit before killing it, default 5 000).

`Sake.Test` connects the client to a server in the same VM, with the
server itself as the transport, for the server's tests (see
`guides/testing.md`).

With `opentelemetry_api` in the host, each request is a CLIENT span and
carries the current trace context in its `_meta`, so the server's spans
join the caller's trace; the span of `call_tool/3` covers the whole
call, its retries with the client's input and a task's polls included.
Each request is also the telemetry span `[:sake, :client, :request]`,
with `method`, `era`, `protocol_version` and `transport` in the
metadata and `error_type` (`nil` on success) on `:stop`. See
`guides/observability.md`.

# `call_tool`

```elixir
@spec call_tool(Sake.Client.Session.t(), String.t(), map()) ::
  {:ok, Sake.Client.ToolResult.t(), Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

Calls a tool and returns its result. When the server answers with a task
(see `Sake.Tasks`), the call polls it to the end, answering its input
requests as a retry would, and returns the final result: a failed task
is `{:error, {:rpc, error}, session}`, a cancelled one `{:error,
:cancelled, session}`, and one still running after `task_timeout:` is
cancelled and returned as `{:error, :task_timeout, session}`.

# `call_tool_async`

```elixir
@spec call_tool_async(Sake.Client.Session.t(), String.t(), map()) ::
  {:ok, Sake.Client.ToolResult.t() | {:task, map()}, Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

Calls a tool and returns at once: the result, or `{:task, task}` when the
server made a task of the call. Follow the task with `get_task/2`,
answer its `inputRequests` with `update_task/3`, and stop it with
`cancel_task/2`.

# `cancel_task`

```elixir
@spec cancel_task(Sake.Client.Session.t(), String.t()) ::
  {:ok, Sake.Client.Session.t()} | {:error, term(), Sake.Client.Session.t()}
```

Asks the server to cancel a task. Cancellation is cooperative: the task may still end otherwise.

# `close`

```elixir
@spec close(Sake.Client.Session.t()) :: :ok
```

# `complete`

```elixir
@spec complete(
  Sake.Client.Session.t(),
  {:prompt, String.t()} | {:resource, String.t()},
  String.t(),
  String.t()
) ::
  {:ok, map(), Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

# `connect`

```elixir
@spec connect(
  String.t() | {:stdio, String.t(), [String.t()]} | {:local, struct()},
  keyword()
) ::
  {:ok, Sake.Client.Session.t()} | {:error, term()}
```

# `discover`

```elixir
@spec discover(Sake.Client.Session.t()) ::
  {:ok, map(), Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

# `get_prompt`

```elixir
@spec get_prompt(Sake.Client.Session.t(), String.t(), map()) ::
  {:ok, map(), Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

# `get_task`

```elixir
@spec get_task(Sake.Client.Session.t(), String.t()) ::
  {:ok, map(), Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

The task as the server has it now: its status, and `inputRequests`, `result` or `error`.

# `list_prompts`

```elixir
@spec list_prompts(Sake.Client.Session.t()) ::
  {:ok, [map()], Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

# `list_resource_templates`

```elixir
@spec list_resource_templates(Sake.Client.Session.t()) ::
  {:ok, [map()], Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

# `list_resources`

```elixir
@spec list_resources(Sake.Client.Session.t()) ::
  {:ok, [map()], Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

# `list_tools`

```elixir
@spec list_tools(Sake.Client.Session.t()) ::
  {:ok, [Sake.Tool.Spec.t()], Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

# `listen`

```elixir
@spec listen(Sake.Client.Session.t(), map(), (String.t(), map() -&gt; any())) ::
  {:ok, Sake.Client.Session.t()} | {:error, term(), Sake.Client.Session.t()}
```

Follows the server's own notifications until it ends the stream:
`fun` gets the method and params of each list change
(`"notifications/tools/list_changed"` and the like) and resource update
(`"notifications/resources/updated"`). `filter` names what to follow,
as a `subscriptions/listen` request's `notifications` does:
`"toolsListChanged"`, `"promptsListChanged"` and
`"resourcesListChanged"` (`true`), and `"resourceSubscriptions"` (a list
of URIs).

On the modern protocol this is a `subscriptions/listen` request; on a
legacy session, the session's GET stream (over stdio, the notifications
the server sends between calls), with a `resources/subscribe` for each
URI, sent before the stream opens and again once it is open (a server
may keep the session's subscriptions on its stream, as Sake does). It
blocks until the stream ends, so call it from a process of its own, and
stop that process to stop listening. Returns `{:ok, session}` when the
server ended the stream.

# `ping`

```elixir
@spec ping(Sake.Client.Session.t()) ::
  {:ok, Sake.Client.Session.t()} | {:error, term(), Sake.Client.Session.t()}
```

# `read_resource`

```elixir
@spec read_resource(Sake.Client.Session.t(), String.t()) ::
  {:ok, [Sake.Content.t()], Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

# `request`

```elixir
@spec request(Sake.Client.Session.t(), String.t(), map(), keyword()) ::
  {:ok, map(), Sake.Client.Session.t()}
  | {:error, term(), Sake.Client.Session.t()}
```

Sends one request and returns its result as the server sent it. Unlike
the calls above, an input-required result (`"resultType" =>
"input_required"`) or a task (`"task"`) comes back as it is, to answer
or follow yourself: `Sake.Proxy` forwards both this way. A question a
legacy server asks during the call is still answered with the
session's `on_input:`, `sampling:` and `roots:`. Options: `tool:` (the
`Sake.Tool.Spec` of a `tools/call`, for its `Mcp-Param-*` headers) and
`timeout:` (ms).

# `set_log_level`

```elixir
@spec set_log_level(Sake.Client.Session.t(), String.t() | atom()) ::
  {:ok, Sake.Client.Session.t()} | {:error, term(), Sake.Client.Session.t()}
```

Asks the server for log messages at `level` or above (`"debug"` to
`"emergency"`), which then reach `on_notification:` during a call. On
the modern protocol the level travels in every request's `_meta`; on a
legacy session the client sends `logging/setLevel`.

# `update_task`

```elixir
@spec update_task(Sake.Client.Session.t(), String.t(), map()) ::
  {:ok, Sake.Client.Session.t()} | {:error, term(), Sake.Client.Session.t()}
```

Answers some or all of a task's `inputRequests`, by key.

---

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