Behaviour for the connection underneath Snodo.Client.
A transport moves one complete JSON-RPC request to a server and returns the
complete JSON-RPC response. Snodo.Client builds the request and decodes the
response, so a transport never interprets result or error.
request/3 receives these options:
:timeout- milliseconds to wait for the response.:dialect- the protocol dialect module that built the request. HTTP uses itstransport_policy/1to derive headers.:extension- an optional extension module for the request method. HTTP uses itstransport_policy/2to derive extension routing headers; other transports may ignore it.:headers- request headers the client adds for the negotiated session, as{name, value}string pairs:MCP-Protocol-VersionandMcp-Session-Idon an initialize-era connection. A transport that has no headers ignores them.:on_response_headers- present forinitialize. A function of one argument to call, in the process that calledrequest/3, with the response headers as{name, value}pairs with lowercase names, before the response is returned. The client readsMcp-Session-Idfrom them.:on_server_request- a function of one argument that answers a request the server sends to the client while this request is in flight, such aselicitation/createon an initialize-era connection. It receives the request as sent and returns the JSON-RPC response object to send back. A transport that carries server requests on the response of the request in flight calls it in the calling process;connect/2receives the same option for a transport whose connection outlives one request.:on_progress- present when the caller asked for progress. A function of one argument to call, in the process that calledrequest/3, with theparamsof eachnotifications/progresswhoseprogressTokenis the request'sparams["_meta"]["progressToken"], before returning the response.:reset_timeout_on_progress- whentrue, each delivered progress notification restarts:timeout, up to:max_total_timeoutmilliseconds after the request started. Past that the request fails withmax_total_timeout_error/1.
A transport that ignores the progress options delivers no progress and keeps a fixed timeout.
Three callbacks are optional. notify/3 sends a notification, a message
without an id, and returns once the transport has delivered it; it
receives :timeout, :dialect, and :headers. The client needs it for
notifications/initialized, so a transport without it cannot open an
initialize-era connection. delete_session/2 is called by
Snodo.Client.close/1 before close/1 when the server issued a session
id; it receives :headers (Mcp-Session-Id and MCP-Protocol-Version)
and :timeout. Streamable HTTP sends a DELETE; a transport without it
simply closes.
The optional listen/3 opens a subscriptions/listen stream for
Snodo.Client.listen/3. It sends the request, waits :timeout milliseconds
for the server's notifications/subscriptions/acknowledged, and returns
{:ok, accepted_filter, pid}: the filter from the acknowledgement and the
process that receives the stream. That process keeps the subscription's
buffer (the internal module the built-in transports share, built from the
:owner, :ref, :max_buffer, and :overflow options), pushes each later
notification to it as {:notification, method, params}, closes it with
:complete or {:error, %Snodo.Error{}} at the terminal response or a
connection failure, and exits once the buffer is done. It handles
{:mcp_client_demand, ref, n} messages by adding demand, and a
{:mcp_client_close, ref} call by cancelling the stream on the server and
replying :ok. It monitors the owner and cancels the stream when the owner
exits. Snodo.Client.Subscription.next/2 follows its demand with a
{:mcp_client_next, ref, reply_to} message, where reply_to is a process
alias. The process must handle it or discard it: a process that exits once
the buffer is done may discard it, for example with a catch-all
handle_info/2 clause, and a GenServer without such a clause crashes on
it. A process that outlives its streams, as the stdio connection does,
answers it with send(reply_to, {reply_to, :ended}) for a ref it no
longer has, so that next/2 on an ended stream returns at once. A JSON-RPC error response before the acknowledgement is returned as
{:error, %Snodo.Error{}}. Snodo.Client.listen/3 raises ArgumentError
for a transport without listen/3.
Failures of the connection itself are %Snodo.Error{kind: :transport}. Use
connection_error/2 (-32000) and timeout_error/1 (-32001), the codes the
official TypeScript SDK uses for the same client-side conditions.
Summary
Functions
The connection is closed, unreachable, or produced an unusable response.
Progress kept moving the deadline of a request, and the response had not
arrived max_total_timeout milliseconds after the request started.
No response arrived within timeout milliseconds.
Types
@type state() :: term()
Callbacks
@callback close(state()) :: :ok
@callback connect(init_arg :: term(), opts :: keyword()) :: {:ok, state()} | {:error, Snodo.Error.t()}
@callback notify(state(), message :: map(), opts :: keyword()) :: :ok | {:error, Snodo.Error.t()}
@callback request(state(), message :: map(), opts :: keyword()) :: {:ok, map()} | {:error, Snodo.Error.t()}
Functions
@spec connection_error(String.t(), term()) :: Snodo.Error.t()
The connection is closed, unreachable, or produced an unusable response.
@spec max_total_timeout_error(pos_integer()) :: Snodo.Error.t()
Progress kept moving the deadline of a request, and the response had not
arrived max_total_timeout milliseconds after the request started.
@spec timeout_error(timeout()) :: Snodo.Error.t()
No response arrived within timeout milliseconds.