ConduitMcp.Client.Transport.Stdio.Framer (ConduitMCP v0.11.0)

Copy Markdown View Source

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 \r is stripped, so CRLF output (Windows servers) frames the same as LF.
  • Blank lines (empty after the \r strip) 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

t()

@opaque t()

Functions

buffered_bytes(framer)

@spec buffered_bytes(t()) :: non_neg_integer()

Bytes held for an incomplete message (0 while discarding).

feed(state, chunk)

@spec feed(t(), binary()) ::
  {:ok, [binary()], t()} | {:error, :message_too_large, [binary()], t()}

Feeds the next chunk; returns the complete messages it finished, in order.

new(opts \\ [])

@spec new(keyword()) :: t()

A fresh framer.

Options

  • :max_message_bytes - positive integer, default 8 MiB.