# `Snodo.Transport.Stdio`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.4.1/lib/snodo/transport/stdio.ex#L1)

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.

# `state`

```elixir
@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()
}
```

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `serve`

```elixir
@spec serve(Snodo.Server.Runtime.t(), keyword()) :: :ok | {:error, term()}
```

Runs the stdio transport until EOF and all admitted requests finish.

---

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