use serde_json::Value as JsonValue;
use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt};
const MAX_JSONRPC_FRAME_BYTES: usize = 16 * 1024 * 1024;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum JsonRpcStdioFrameStyle {
#[default]
Line,
ContentLength,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct JsonRpcStdioFrame {
pub body: Vec<u8>,
pub style: JsonRpcStdioFrameStyle,
}
impl JsonRpcStdioFrame {
pub fn parse_json(&self) -> Result<JsonValue, serde_json::Error> {
serde_json::from_slice(&self.body)
}
}
pub async fn read_jsonrpc_stdio_frame<R>(
reader: &mut R,
) -> Result<Option<JsonRpcStdioFrame>, String>
where
R: AsyncBufRead + Unpin,
{
loop {
let mut first_line = String::new();
let bytes = reader
.read_line(&mut first_line)
.await
.map_err(|error| format!("stdin read failed: {error}"))?;
if bytes == 0 {
return Ok(None);
}
let line = first_line.trim_end_matches(['\r', '\n']);
if line.trim().is_empty() {
continue;
}
if let Some(length) = content_length_from_header_line(line)? {
return read_content_length_frame(reader, length).await.map(Some);
}
return Ok(Some(JsonRpcStdioFrame {
body: line.trim().as_bytes().to_vec(),
style: JsonRpcStdioFrameStyle::Line,
}));
}
}
pub async fn write_jsonrpc_stdio_message<W>(
writer: &mut W,
value: &JsonValue,
style: JsonRpcStdioFrameStyle,
) -> Result<(), String>
where
W: AsyncWrite + Unpin,
{
let encoded = serde_json::to_vec(value).map_err(|error| format!("serialize error: {error}"))?;
match style {
JsonRpcStdioFrameStyle::Line => {
writer
.write_all(&encoded)
.await
.map_err(|error| format!("stdout write failed: {error}"))?;
writer
.write_all(b"\n")
.await
.map_err(|error| format!("stdout write failed: {error}"))?;
}
JsonRpcStdioFrameStyle::ContentLength => {
let header = format!("Content-Length: {}\r\n\r\n", encoded.len());
writer
.write_all(header.as_bytes())
.await
.map_err(|error| format!("stdout write failed: {error}"))?;
writer
.write_all(&encoded)
.await
.map_err(|error| format!("stdout write failed: {error}"))?;
}
}
writer
.flush()
.await
.map_err(|error| format!("stdout flush failed: {error}"))
}
fn content_length_from_header_line(line: &str) -> Result<Option<usize>, String> {
let Some((name, value)) = line.split_once(':') else {
return Ok(None);
};
if !name.trim().eq_ignore_ascii_case("content-length") {
return Ok(None);
}
let length = value
.trim()
.parse::<usize>()
.map_err(|error| format!("invalid MCP Content-Length header: {error}"))?;
if length > MAX_JSONRPC_FRAME_BYTES {
return Err(format!(
"MCP Content-Length {length} exceeds limit {MAX_JSONRPC_FRAME_BYTES} bytes"
));
}
Ok(Some(length))
}
async fn read_content_length_frame<R>(
reader: &mut R,
length: usize,
) -> Result<JsonRpcStdioFrame, String>
where
R: AsyncBufRead + Unpin,
{
loop {
let mut header_line = String::new();
let bytes = reader
.read_line(&mut header_line)
.await
.map_err(|error| format!("stdin read failed: {error}"))?;
if bytes == 0 {
return Err("stdin closed while reading MCP Content-Length headers".to_string());
}
if header_line.trim_end_matches(['\r', '\n']).is_empty() {
break;
}
}
let mut body = vec![0; length];
reader
.read_exact(&mut body)
.await
.map_err(|error| format!("stdin read failed while reading MCP body: {error}"))?;
Ok(JsonRpcStdioFrame {
body,
style: JsonRpcStdioFrameStyle::ContentLength,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::io::Cursor;
use tokio::io::BufReader;
#[tokio::test]
async fn reads_newline_delimited_jsonrpc_frames() {
let mut reader = BufReader::new(Cursor::new(br#"{"jsonrpc":"2.0","id":1}"#.to_vec()));
let frame = read_jsonrpc_stdio_frame(&mut reader)
.await
.expect("read")
.expect("frame");
assert_eq!(frame.style, JsonRpcStdioFrameStyle::Line);
assert_eq!(frame.parse_json().expect("json")["id"], json!(1));
}
#[tokio::test]
async fn reads_content_length_jsonrpc_frames() {
let body = br#"{"jsonrpc":"2.0","id":7}"#;
let input = format!("Content-Length: {}\r\nX-Ignored: yes\r\n\r\n", body.len());
let mut bytes = input.into_bytes();
bytes.extend_from_slice(body);
let mut reader = BufReader::new(Cursor::new(bytes));
let frame = read_jsonrpc_stdio_frame(&mut reader)
.await
.expect("read")
.expect("frame");
assert_eq!(frame.style, JsonRpcStdioFrameStyle::ContentLength);
assert_eq!(frame.parse_json().expect("json")["id"], json!(7));
}
#[tokio::test]
async fn oversized_content_length_is_rejected_without_allocating() {
let declared = MAX_JSONRPC_FRAME_BYTES + 1;
let input = format!("Content-Length: {declared}\r\n\r\nabc");
let mut reader = BufReader::new(Cursor::new(input.into_bytes()));
let error = read_jsonrpc_stdio_frame(&mut reader)
.await
.expect_err("oversized frame rejected");
assert!(error.contains("exceeds limit"), "unexpected error: {error}");
}
#[tokio::test]
async fn content_length_at_the_limit_is_accepted() {
let line = format!("Content-Length: {MAX_JSONRPC_FRAME_BYTES}");
assert_eq!(
content_length_from_header_line(&line).expect("at limit"),
Some(MAX_JSONRPC_FRAME_BYTES)
);
}
#[tokio::test]
async fn writes_content_length_jsonrpc_frames() {
let mut out = Vec::new();
write_jsonrpc_stdio_message(
&mut out,
&json!({"jsonrpc":"2.0","id":9,"result":{}}),
JsonRpcStdioFrameStyle::ContentLength,
)
.await
.expect("write");
let header_end = out
.windows(4)
.position(|window| window == b"\r\n\r\n")
.expect("header end");
let header = std::str::from_utf8(&out[..header_end]).expect("header utf8");
let body = &out[header_end + 4..];
assert_eq!(header, format!("Content-Length: {}", body.len()));
assert_eq!(
serde_json::from_slice::<JsonValue>(body).unwrap()["id"],
json!(9)
);
}
}