Skip to main content

mcp/rpc/
frame.rs

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