Splits a stdio byte stream into newline-delimited MCP messages.
The stdio transport reads its port in :stream mode, so chunks arrive in
arbitrary splits; this pure state machine turns them back into messages.
iex> framer = ConduitMcp.Client.Transport.Stdio.Framer.new()
iex> {:ok, [], framer} = ConduitMcp.Client.Transport.Stdio.Framer.feed(framer, "{\"a\":")
iex> {:ok, messages, _framer} = ConduitMcp.Client.Transport.Stdio.Framer.feed(framer, "1}\r\n{}\n")
iex> messages
["{\"a\":1}", "{}"]Rules
- A message ends at
\n. One trailing\ris stripped, so CRLF output (Windows servers) frames the same as LF. - Blank lines (empty after the
\rstrip) are skipped. - Bytes are scanned once: the held partial line is never rescanned, only each new chunk is searched for newlines.
- Bytes after the last newline wait for the next chunk; at end of stream they are an incomplete message and are dropped by the caller.
Oversized messages and resync
A message longer than :max_message_bytes (default 8 MiB, \r and \n
excluded) is detected as soon as the held bytes pass the cap, without
buffering more. The framer then discards every byte up to and including
the next \n and resumes framing after it, so one oversized message
costs that message only.
feed/2 reports a discard by returning
{:error, :message_too_large, messages, state}: the whole chunk was still
consumed, messages holds every complete message in it (before and after
the discarded one), and state is ready for the next chunk. A caller that
treats oversize as fatal closes the connection; a lenient caller keeps
feeding.
Summary
Functions
Bytes held for an incomplete message (0 while discarding).
Feeds the next chunk; returns the complete messages it finished, in order.
A fresh framer.
Types
Functions
@spec buffered_bytes(t()) :: non_neg_integer()
Bytes held for an incomplete message (0 while discarding).
Feeds the next chunk; returns the complete messages it finished, in order.
A fresh framer.
Options
:max_message_bytes- positive integer, default 8 MiB.