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
- Take a fresh JSON-RPC id from
:next_id(ids are never reused) and call:sendwith the method and params. - 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:protocolerror. - For
"input_required": check every input request first (method known and declared in:capabilities; an undeclared one is a:protocolerror and no handler runs), then call:handle_inputfor each, in key order, and retry with the same method and params plusinputResponsesand therequestStateechoed byte-exact. A missingrequestStateis omitted on the retry, never sent empty; a missinginputRequestsmeans retry at once withrequestStatealone.
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; aninput_requiredanswer 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 ofrequestState.
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
Answers one input request: (key, %{"method" => _, "params" => _}).
@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()}
@type send_fun() :: (term(), String.t(), map() -> {:ok, term()} | {:error, ConduitMcp.Client.Error.t()})
Sends one request: (id, method, params).
Functions
@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- theclientCapabilitiesthis request declared (default%{}, which refuses every input request).:result_types- extraresultTypevalues to accept as final (default[]).:max_rounds,:max_input_requests,:max_request_state_bytes- see the moduledoc.