Skip to main content

mcp/rpc/
frame.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Two framings over a byte stream, sharing the JSON-RPC codec in the parent
3//! module (`crate::json`).
4//!
5//! - **NDJSON** (`read_line` / `write_line`): one JSON value per line,
6//!   no embedded newlines. The MCP stdio transport framing (RFC 0004).
7//! - **Length-prefix** (`read_frame` / `write_frame`): a 4-byte big-endian
8//!   length followed by that many payload bytes. The private supervisor↔
9//!   subagent control channel (RFC 0005) — robust to payloads (instructions,
10//!   context seeds, distilled results) that legitimately contain newlines.
11//!
12//! Both are generic over `Read`/`Write` so they drop onto pipes, unix
13//! sockets, TLS streams, and vsock alike. Lifted/adapted from the retired
14//! `intelligence/protocol.rs` length-framing (salvage list, PLAN.md).
15
16use std::io::{self, BufRead, Read, Write};
17
18/// Hard cap on a single frame/line, for both framings. A peer claiming more
19/// is a protocol error, not an allocation. 16 MiB matches the MCP-side cap.
20pub const MAX_FRAME: usize = 16 * 1024 * 1024;
21
22// ---- NDJSON (MCP stdio) ----
23
24/// Serialize `value` as compact JSON plus a trailing `\n`. Errors if the
25/// encoded form contains a newline (it cannot for valid compact JSON, but we
26/// assert the invariant the transport relies on).
27pub fn write_line<W: Write, T: serde::Serialize>(w: &mut W, value: &T) -> io::Result<()> {
28    let buf = serde_json::to_vec(value).map_err(io::Error::other)?;
29    debug_assert!(
30        !buf.contains(&b'\n'),
31        "compact JSON must not contain newlines"
32    );
33    w.write_all(&buf)?;
34    w.write_all(b"\n")?;
35    w.flush()
36}
37
38/// Read one newline-delimited frame. Returns `Ok(None)` on clean EOF (the
39/// peer closed the stream between messages — an orderly shutdown signal).
40/// A line longer than [`MAX_FRAME`] is an error.
41pub fn read_line<R: BufRead>(r: &mut R) -> io::Result<Option<Vec<u8>>> {
42    let mut buf = Vec::new();
43    loop {
44        let mut byte = [0u8; 1];
45        match r.read(&mut byte)? {
46            0 => {
47                // EOF. Mid-line EOF is a truncated frame; clean EOF is None.
48                return if buf.is_empty() {
49                    Ok(None)
50                } else {
51                    Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF mid-line"))
52                };
53            }
54            _ => {
55                if byte[0] == b'\n' {
56                    return Ok(Some(buf));
57                }
58                if buf.len() >= MAX_FRAME {
59                    return Err(io::Error::new(
60                        io::ErrorKind::InvalidData,
61                        "line exceeds MAX_FRAME",
62                    ));
63                }
64                buf.push(byte[0]);
65            }
66        }
67    }
68}
69
70// ---- Length-prefix (control channel) ----
71
72/// Write a 4-byte big-endian length prefix followed by the JSON payload.
73pub fn write_frame<W: Write, T: serde::Serialize>(w: &mut W, value: &T) -> io::Result<()> {
74    let buf = serde_json::to_vec(value).map_err(io::Error::other)?;
75    if buf.len() > MAX_FRAME {
76        return Err(io::Error::new(
77            io::ErrorKind::InvalidData,
78            "frame exceeds MAX_FRAME",
79        ));
80    }
81    w.write_all(&(buf.len() as u32).to_be_bytes())?;
82    w.write_all(&buf)?;
83    w.flush()
84}
85
86/// Read one length-prefixed frame. Returns `Ok(None)` on clean EOF before the
87/// length prefix (orderly shutdown). A declared length over [`MAX_FRAME`] is
88/// rejected before allocation.
89pub fn read_frame<R: Read>(r: &mut R) -> io::Result<Option<Vec<u8>>> {
90    let mut len_buf = [0u8; 4];
91    if !read_exact_or_eof(r, &mut len_buf)? {
92        return Ok(None); // clean EOF before any length byte
93    }
94    let len = u32::from_be_bytes(len_buf) as usize;
95    if len > MAX_FRAME {
96        return Err(io::Error::new(
97            io::ErrorKind::InvalidData,
98            "frame length exceeds MAX_FRAME",
99        ));
100    }
101    let mut buf = vec![0u8; len];
102    r.read_exact(&mut buf)?;
103    Ok(Some(buf))
104}
105
106/// Like `read_exact`, but distinguishes clean EOF (no bytes read → `false`)
107/// from a truncated read (some bytes then EOF → error).
108fn read_exact_or_eof<R: Read>(r: &mut R, buf: &mut [u8]) -> io::Result<bool> {
109    let mut filled = 0;
110    while filled < buf.len() {
111        match r.read(&mut buf[filled..])? {
112            0 => {
113                return if filled == 0 {
114                    Ok(false)
115                } else {
116                    Err(io::Error::new(
117                        io::ErrorKind::UnexpectedEof,
118                        "EOF mid-frame",
119                    ))
120                };
121            }
122            n => filled += n,
123        }
124    }
125    Ok(true)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::rpc::{Id, Response};
132    use std::io::Cursor;
133
134    #[test]
135    fn line_roundtrip() {
136        let mut buf = Vec::new();
137        write_line(&mut buf, &serde_json::json!({"a": 1})).unwrap();
138        assert_eq!(buf.last(), Some(&b'\n'));
139        let mut cur = Cursor::new(buf);
140        let line = read_line(&mut cur).unwrap().unwrap();
141        let v: serde_json::Value = serde_json::from_slice(&line).unwrap();
142        assert_eq!(v["a"], 1);
143        // clean EOF -> None
144        assert!(read_line(&mut cur).unwrap().is_none());
145    }
146
147    #[test]
148    fn frame_roundtrip() {
149        let mut buf = Vec::new();
150        let resp = Response::ok(Id::Num(1), serde_json::json!({"ok": true}));
151        write_frame(&mut buf, &resp).unwrap();
152        let mut cur = Cursor::new(buf);
153        let frame = read_frame(&mut cur).unwrap().unwrap();
154        let back: Response = serde_json::from_slice(&frame).unwrap();
155        assert_eq!(back.id, Id::Num(1));
156        assert!(read_frame(&mut cur).unwrap().is_none());
157    }
158
159    #[test]
160    fn frame_with_newline_payload_survives() {
161        // The whole point of length-framing for the control channel.
162        let mut buf = Vec::new();
163        write_frame(&mut buf, &serde_json::json!({"text": "line1\nline2"})).unwrap();
164        let mut cur = Cursor::new(buf);
165        let frame = read_frame(&mut cur).unwrap().unwrap();
166        let v: serde_json::Value = serde_json::from_slice(&frame).unwrap();
167        assert_eq!(v["text"], "line1\nline2");
168    }
169
170    #[test]
171    fn oversize_length_rejected() {
172        let mut bytes = (MAX_FRAME as u32 + 1).to_be_bytes().to_vec();
173        bytes.push(0);
174        let mut cur = Cursor::new(bytes);
175        assert!(read_frame(&mut cur).is_err());
176    }
177}