ConduitMcp.Client.Request.MRTR (ConduitMCP v0.11.0)

Copy Markdown View Source

The client side of modern (2026-07-28) multi round-trip requests.

A tools/call, resources/read or prompts/get may come back with resultType: "input_required": the server needs answers (elicitation, sampling, roots) before it can finish. run/3 drives that loop. It does no I/O itself: sending a request, allocating ids and answering input requests are injected functions, so it runs in the caller and is testable without a server.

One round

  1. Take a fresh JSON-RPC id from :next_id (ids are never reused) and call :send with the method and params.
  2. A result with no resultType, or "complete", ends the loop. So does a type listed in :result_types (for example "task" when the tasks extension is in use). Any other value is invalid: a :protocol error.
  3. For "input_required": check every input request first (method known and declared in :capabilities; an undeclared one is a :protocol error and no handler runs), then call :handle_input for each, in key order, and retry with the same method and params plus inputResponses and the requestState echoed byte-exact. A missing requestState is omitted on the retry, never sent empty; a missing inputRequests means retry at once with requestState alone.

Only the original params are reused: inputResponses and requestState from a previous round never leak into the next. requestState is held as a ConduitMcp.Client.Secret between rounds and is never put in an error.

Limits (MR-4)

All three give %ConduitMcp.Client.Error{kind: :mrtr_round_limit}, with the cap that tripped as the reason:

  • :max_rounds (default 8) - requests sent, retries included; an input_required answer to the last allowed round is the error.
  • :max_input_requests (default 16) - input requests in one round.
  • :max_request_state_bytes (default 65_536) - size of requestState.

Handler failures

:handle_input returning {:error, reason} or a non-map, raising, throwing or exiting aborts the loop with kind: :input_handler; the reason is {:handler_error, key}, {:invalid_response, key} or {:raised, key}.

Summary

Types

Answers one input request: (key, %{"method" => _, "params" => _}).

Sends one request: (id, method, params).

Functions

Runs method with params to completion.

Types

input_fun()

@type input_fun() :: (String.t(), map() -> {:ok, map()} | {:error, term()})

Answers one input request: (key, %{"method" => _, "params" => _}).

option()

@type option() ::
  {:send, send_fun()}
  | {:next_id, (-> term())}
  | {:handle_input, input_fun()}
  | {:capabilities, map()}
  | {:result_types, [String.t()]}
  | {:max_rounds, pos_integer()}
  | {:max_input_requests, pos_integer()}
  | {:max_request_state_bytes, pos_integer()}

send_fun()

@type send_fun() :: (term(), String.t(), map() ->
                 {:ok, term()} | {:error, ConduitMcp.Client.Error.t()})

Sends one request: (id, method, params).

Functions

run(method, params, opts)

@spec run(String.t(), map(), [option()]) ::
  {:ok, term(), %{rounds: pos_integer()}}
  | {:error, ConduitMcp.Client.Error.t()}

Runs method with params to completion.

Returns {:ok, result, %{rounds: n}} with the final string-keyed result map and the number of requests sent, or {:error, %Error{}}; an error from :send is returned unchanged.

Options

  • :send (required) - send_fun/0.
  • :next_id (required) - zero-arity function returning a fresh id.
  • :handle_input (required) - input_fun/0.
  • :capabilities - the clientCapabilities this request declared (default %{}, which refuses every input request).
  • :result_types - extra resultType values to accept as final (default []).
  • :max_rounds, :max_input_requests, :max_request_state_bytes - see the moduledoc.