Expand description
The stdio client: a child MCP server, spawned and supervised.
§What this is
primary::stdio is the byte layer of a
stdio conversation, and it is deliberately agnostic about where the bytes
come from: StdioSource<R> and StdioSink<W> are generic over
AsyncRead and AsyncWrite precisely so that a process’s pipes can be
dropped into them. This module supplies what the byte layer was missing —
the other end of the conversation. StdioClient starts a child server,
hands its stdout to the source and its stdin to the sink, drains its stderr
somewhere that is not the protocol, and takes the process down with it.
There is no second framing and no second encoder here, and that is the
whole argument for reusing those two types rather than writing a client
transport of one’s own. Framing is where the security properties live — the
ceiling, the single delimiter, the decision that a frame the peer wrote and
did not terminate is dropped rather than reported — and a second
implementation would be a second set of those properties to get right. The
request and reply documents in engine::request are the exception, and they are
documents rather than framing: the shapes reply
writes and this reads are one wire format with one serialiser, not two.
§The two things that are easy to get wrong
Kill-on-drop is opt-in. tokio::process::Child does not kill the
process it is dropped with. Without an explicit kill_on_drop(true), a
client that returns an error half way through a handshake leaves a server
running with nobody reading its stdout — an orphan holding a pipe, holding
whatever the user asked it to do. child::spawn is where the flag is set,
and it is set on the one path that can spawn a child.
The child’s stderr is not the protocol. Diagnostics is bound to
stderr and has no constructor that can be pointed at stdout, for the same
reason the server’s does not: on stdio, stdout is the conversation. The
forwarder in child::forward_stderr moves those bytes across one line at
a time and nowhere else, so a server that logs to stderr cannot corrupt a
frame — and, just as importantly, a log line cannot be mistaken for a
frame the peer failed to parse.
§The two Nones
StdioSource::next_frame answers Ok(None) when the peer has closed, and
the decoder underneath it answers Ok(None) when a line has no terminator
yet — opposites, and forwarding the second up would turn “keep reading”
into “the child died” and “the child died” into “read again”. The source
already gets this right; this module passes its answer through unaltered
and names the distinction at every call site, because the two failures it
produces (a child that exited early, and a child that has not answered yet)
are the two a caller most needs told apart.
§Where the conversation is
The bytes stay in the byte layer. What this module adds is the lifecycle —
spec::ChildSpec for the program, child for the process, and
client::StdioClient for the exchange — plus the request documents in
engine::request, which are the client’s mirror of the server’s
reply module.
Re-exports§
pub use bridge_peer::StdioPeer;pub use child::SpawnError;pub use client::ClientConfig;pub use client::StdioClient;pub use client::StdioClientError;pub use spec::ChildSpec;pub use spec::ChildSpecError;
Modules§
- bridge_
peer - Carries envelopes over a
stdiochild. - child
- The process: spawned once, drained on stderr, killed on drop.
- client
- The client: a running conversation with a child server.
- spec
- The child program, as typed values rather than a command line.