Concurrent newline-delimited JSON-RPC transport.
A small coordinator owns framing and serializes complete stdout writes. Work
is admitted through the optional, transport-neutral Snodo.Server.Executor, so
the coordinator never awaits a handler and execution policy is reusable by
other transports.
:write_timeout is a positive millisecond limit, defaulting to 5,000. A single
linked, monitored helper performs each write while the coordinator waits
boundedly; there is no writer queue. Cancellation and EOF handling can wait
up to this limit during a blocked write. A timeout or output error is terminal:
the transport cancels its work and closes subscriptions, without attempting
further writes. A timed-out device may already have accepted some bytes, so
the transport never retries. Supplied I/O devices are never stopped or killed.
:max_line_bytes limits one input message, counting its newline, and defaults
to 2,000,000 bytes to match the HTTP listener's :max_body_bytes. A longer
line is answered with -32600 and a null id without being decoded, the rest of
it is discarded, and the next line is served normally.
How much of a long line is held in memory depends on the input. The default
:stdio input is read in chunks that hold at most :max_line_bytes of a line
when the VM was started with -noinput, and on OTP 28 and later when standard
input is a stream socket, which is what Node.js clients provide. Any other
input is read a line at a time, and the device holds each line in full before
returning it; the VM holds a line from standard input as a character list,
many times its size. That covers a pipe on standard input, which Python and
Erlang clients provide, unless the VM was started with -noinput, for example
in a release's vm.args. With -noinput, nothing else in the VM can read
standard input.
An initialize-era client (2025-11-25 or 2025-06-18, when the runtime enables
those dialects) negotiates its version once with initialize. The transport
records the version from a successful result and gives every later message
on the connection an mcp-protocol-version request header, the header HTTP
requests carry, so dialect selection and validation are shared with HTTP. A
2026-07-28 client does not send initialize, and its messages carry no such
header.
Summary
Functions
Returns a specification to start this module under a supervisor.
Runs the stdio transport until EOF and all admitted requests finish.
Types
@type state() :: %{ runtime: Snodo.Server.Runtime.t(), input: IO.device(), output: IO.device(), writer: {IO.device(), pos_integer()}, executor: pid(), executor_monitor: reference() | nil, serve_owner_monitor: reference() | nil, owns_executor?: boolean(), request_timeout: Snodo.Server.Executor.execution_timeout() | :default, reader: pid() | nil, connection_ref: reference(), executions_by_id: map(), executions_by_ref: map(), subscriptions_by_id: map(), subscriptions_by_worker: map(), max_subscriptions: pos_integer(), max_line_bytes: pos_integer(), negotiated_version: String.t() | nil, eof?: boolean() }
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec serve(Snodo.Server.Runtime.t(), keyword()) :: :ok | {:error, term()}
Runs the stdio transport until EOF and all admitted requests finish.