Skip to main content

Module transport

Module transport 

Source
Expand description

Resilient stdio transport for MCP server (v1.4.90 P0-A).

§Why this exists

rmcp::transport::stdio() (i.e. the default (Stdin, Stdout) adapter via AsyncRwTransport + JsonRpcMessageCodec) treats any JSON parse error as a fatal stream error. Concretely:

  1. JsonRpcMessageCodec::decode() returns Err(JsonRpcMessageCodecError::Serde(_)) when a line is malformed (e.g. {"price": Infinity} — JSON spec forbids Infinity / NaN literals, but LLM clients emit them occasionally).
  2. FramedRead yields Some(Err(_)).
  3. AsyncRwTransport::receive() does next.await.and_then(|e| e.ok()) — converting Err to None.
  4. The rmcp service loop interprets None as “input stream closed” and breaks with QuitReason::Closed, terminating the entire MCP server.

Result: a single malformed JSON line silently kills the whole server, disconnecting every client (multi-version sweep proven across v1.4.47 → v1.4.86 — 11 versions all vulnerable).

Per JSON-RPC 2.0 §5.1, the correct behavior is to return a -32700 Parse error response and keep the connection alive. This module implements that behavior as a drop-in replacement for rmcp::transport::stdio().

§Design

  • ResilientStdioTransport implements rmcp::transport::Transport<RoleServer>.
  • A background reader task owns stdin, reads newline-delimited frames, and parses each into RxJsonRpcMessage<RoleServer>. Successful parses go into an inbound mpsc channel for receive(). Parse failures cause a synthetic JsonRpcError(-32700) to be enqueued onto the outbound channel directly (bypassing receive() so the service never sees an error event), and the loop continues.
  • A background writer task owns stdout and drains the outbound channel, serialising messages as one-line JSON each.
  • send() enqueues onto the outbound channel; receive() polls the inbound channel; close() drops the senders so both tasks exit cleanly.

§What this is NOT

This is a stdio-only fix. The HTTP transport (StreamableHttpService) has its own per-request HTTP body parsing — a malformed request there returns 4xx without killing the server, so it’s not affected by this bug. Future work: upstream PR to rmcp so all transports share resilient parsing.

Structs§

ResilientStdioTransport
Resilient stdio transport — see module docs.

Enums§

TransportError

Constants§

INBOUND_BUFFER 🔒
Bound for the inbound channel. 64 frames buffered is plenty — the rmcp service loop drains promptly. If the channel ever fills up, receive() / the reader task will simply backpressure on stdin, which is fine.

Functions§

preview 🔒
Truncate a line for log output (avoid dumping arbitrary client input into the audit log unbounded).
reader_task 🔒
recover_request_id 🔒
Best-effort recovery of the id field from a malformed JSON-RPC payload. Falls back to Number(0) when extraction fails (the JSON-RPC spec says “null” is the canonical placeholder, but rmcp’s RequestId doesn’t admit a null variant — Number(0) is the closest match and round-trips cleanly).
resilient_stdio
Drop-in replacement for rmcp::transport::stdio(). Returns a transport that survives malformed JSON instead of exit(0)-ing.
writer_task 🔒

Type Aliases§

OutboundRx 🔒
OutboundTx 🔒
Outbound is unbounded because we never want a slow consumer to deadlock the parse-error path (which writes synthetically). All writes are tiny JSON lines.