# `ConduitMcp.Client.Request`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/client/request.ex#L1)

The caller side of a client request, and the request envelope.

`ConduitMcp.Client` functions call `run/4` **in the calling process**. It
never uses `GenServer.call`: it monitors the connection with an alias,

    mref = :erlang.monitor(:process, connection, [{:alias, :demonitor}])

sends `{:"$mcp_request", mref, self(), method, params, wire_opts}`, and
waits in one `receive` for messages tagged with `mref`:

  * `{:accepted, id}` - the connection sent the request with JSON-RPC id
    `id`.
  * `{:progress, params}` - a `notifications/progress` for it; passed to
    `:on_progress`.
  * `{:result, map}` - the connection completed the request itself (for
    example a modern `set_log_level`, which sends nothing).
  * `{:deadline, ms}` - the deadline moved: a legacy server request tied
    to this one is running in the client's handler (the deadline is
    paused, bounded by `:max_total_timeout`), or ended.
  * `{:error, %ConduitMcp.Client.Error{}}` - the connection refused or
    failed it.
  * `{:DOWN, mref, ...}` - the connection exited (`kind:
    :connection_down`).

The connection owns the deadline: when it passes, the connection cancels
the request on the wire and answers `kind: :timeout`. The caller's own
`after` (the deadline plus a short grace) is only a safety net for a
wedged connection; it sends `{:"$mcp_cancel", mref, :caller_timeout}`.
`:erlang.demonitor(mref, [:flush])` deactivates the alias when `run/4`
returns, so a response racing the timeout is dropped by the runtime and
never reaches the caller's mailbox.

If the caller dies, the connection sees it through its own monitor and
cancels the request (`ConduitMcp.Client.Transport` "Cancellation").

A JSON-RPC error answer `-32022` (the server no longer accepts the
negotiated version) is retried once: the connection re-negotiates before
it sends the retry.

## Telemetry

Each request sent is one `[:conduit_mcp, :client, :request, :start |
:stop | :exception]` span, emitted in the caller (see
`ConduitMcp.Telemetry`).

# `envelope_info`

```elixir
@type envelope_info() :: %{
  :era =&gt; :modern | :legacy,
  :version =&gt; String.t(),
  :client_capabilities =&gt; map(),
  :client_info =&gt; map(),
  optional(:log_level) =&gt; String.t() | nil,
  optional(:progress_token) =&gt; term(),
  optional(:traceparent) =&gt; String.t() | nil
}
```

What `envelope/2` needs from the negotiated connection.

# `on_progress`

```elixir
@type on_progress() :: (map() -&gt; any()) | pid() | nil
```

`fun.(params)`, or a pid receiving `{:mcp_progress, client, params}`.

# `op`

```elixir
@type op() :: String.t() | :ping | {:set_log_level, String.t()} | :roots_list_changed
```

What to send: a JSON-RPC method, or an operation whose wire form depends on
the negotiated era (resolved by the connection when it sends):

  * `:ping` - legacy `ping`, modern `server/discover`.
  * `{:set_log_level, level}` - legacy `logging/setLevel`; modern stores
    the level for the `logLevel` `_meta` of later requests.
  * `:roots_list_changed` - legacy `notifications/roots/list_changed`;
    refused in the modern era.

# `call`

```elixir
@spec call(term(), term(), timeout()) :: term()
```

Sends a control `op` to `client`'s connection and returns its reply, or
`{:error, %Error{}}` on timeout or when the connection is gone.

# `envelope`

```elixir
@spec envelope(map(), envelope_info()) :: map()
```

The wire `params` of a request: `params` with the era's reserved `_meta`
keys merged over the caller's `"_meta"`.

  * Modern: `io.modelcontextprotocol/protocolVersion`, `clientCapabilities`,
    `clientInfo`, `logLevel` when one is set, and `traceparent` (unless the
    caller's `_meta` has one) when `info.traceparent` is set.
  * Both eras: `progressToken` when `info.progress_token` is set.

Reserved keys always win: any caller key named `progressToken`, or whose
prefix (before `/`) has a `modelcontextprotocol` or `mcp` label, is
dropped. An empty `_meta` is omitted.

    iex> ConduitMcp.Client.Request.envelope(
    ...>   %{"name" => "echo", "_meta" => %{"progressToken" => "x", "trace" => "t"}},
    ...>   %{era: :legacy, version: "2025-11-25", client_capabilities: %{},
    ...>     client_info: %{}, progress_token: 7}
    ...> )
    %{"name" => "echo", "_meta" => %{"progressToken" => 7, "trace" => "t"}}

# `lookup`

```elixir
@spec lookup(term()) :: {:ok, pid(), map()} | {:error, ConduitMcp.Client.Error.t()}
```

Resolves `client` to its connection pid and its caller view: the value the
connection registers under `{:client, name}` (see
`ConduitMcp.Client.Registry`) merged with its current status from
`ConduitMcp.Client.Cache` (`state`, `era`, `version`, `reason`, `origin`,
`server`).

# `run`

```elixir
@spec run(term(), op(), map(), keyword()) ::
  {:ok, map()} | {:error, ConduitMcp.Client.Error.t()}
```

Sends `op` with `params` through `client` and waits for the answer.

Returns the JSON-RPC `result` map, or `{:error, %ConduitMcp.Client.Error{}}`
(a JSON-RPC error answer becomes `kind: :protocol` with reason
`{:jsonrpc, code}` via `ConduitMcp.Client.Error.from_rpc_error/1`).

`params` may carry a user `"_meta"` map; the connection merges the era's
reserved keys over it (`envelope/2`).

## Options

  * `:timeout` - milliseconds before the request is cancelled (default
    the client's `:request_timeout`).
  * `:on_progress` - a one-arity function called with each
    `notifications/progress` params, or a pid sent
    `{:mcp_progress, client, params}`. Setting it asks the server for
    progress (a `progressToken` equal to the request id).
  * `:reset_timeout_on_progress` - each progress notification restarts
    the `:timeout` (default `false`), never past `:max_total_timeout`.
  * `:max_total_timeout` - the cap for the above (default the client's
    `:max_total_timeout`), and for a deadline paused while a legacy
    server request tied to this one runs in the client's handler.
  * `:assigns` - a map passed to `ConduitMcp.Client.Handler` callbacks as
    `ctx.assigns` for legacy server requests tied to this request (default
    the client's `:assigns`).
  * `:extensions` - extension capabilities to declare in this request's
    modern `clientCapabilities` (for example
    `%{"io.modelcontextprotocol/tasks" => %{}}`).
  * `:headers` - extra HTTP headers for a modern request over an `:http`
    transport, as lowercase `{name, value}` string pairs (the
    `Mcp-Param-*` headers of `ConduitMcp.Headers.param_headers/2`);
    ignored by other transports.
  * `:traceparent` - the W3C trace context to propagate (default: the
    caller's `Logger.metadata()[:traceparent]`). Sent in a modern
    request's `_meta` when it is well formed (`ConduitMcp.Era.traceparent/1`),
    unless the caller's `"_meta"` already has one; `tracestate` and
    `baggage` are never added.

## Example

    {:ok, %{"tools" => _}} = ConduitMcp.Client.Request.run(MyApp.Docs, "tools/list", %{})

---

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