1- //! Hardened stdio transport — newline-delimited JSON-RPC 2.0 (MCP standard).
1+ //! Hardened stdio transport — Content-Length framed JSON-RPC 2.0 (MCP standard).
22
33use std:: io:: { BufRead , BufReader , Read , Write } ;
44
5- /// Maximum message size: 8 MiB.
6- pub const MAX_MESSAGE_BYTES : usize = 8 * 1024 * 1024 ;
5+ /// Maximum frame size: 8 MiB.
6+ pub const MAX_CONTENT_LENGTH_BYTES : usize = 8 * 1024 * 1024 ;
7+ /// JSON-RPC stdio framing header marker.
8+ pub const CONTENT_LENGTH_HEADER : & str = "content-length:" ;
9+ /// Backward-compatible alias for existing transport checks.
10+ pub const MAX_MESSAGE_BYTES : usize = MAX_CONTENT_LENGTH_BYTES ;
711
812/// Transport errors.
913#[ derive( Debug , thiserror:: Error ) ]
@@ -29,7 +33,7 @@ pub enum TransportError {
2933 Json ( #[ from] serde_json:: Error ) ,
3034}
3135
32- /// Hardened stdio transport using newline-delimited JSON .
36+ /// Hardened stdio transport using Content-Length framing .
3337pub struct StdioTransport < R : Read , W : Write > {
3438 reader : BufReader < R > ,
3539 writer : W ,
@@ -44,37 +48,62 @@ impl<R: Read, W: Write> StdioTransport<R, W> {
4448 }
4549 }
4650
47- /// Read a single newline-delimited JSON message.
51+ /// Read a single Content-Length framed JSON message.
4852 pub fn read_message ( & mut self ) -> Result < String , TransportError > {
49- let mut line = String :: new ( ) ;
50- let bytes_read = self . reader . read_line ( & mut line) ?;
51- if bytes_read == 0 {
52- return Err ( TransportError :: Io ( std:: io:: Error :: new (
53- std:: io:: ErrorKind :: UnexpectedEof ,
54- "EOF on stdin" ,
55- ) ) ) ;
53+ let mut content_length: Option < usize > = None ;
54+
55+ // Parse headers until the terminating empty line.
56+ loop {
57+ let mut line = String :: new ( ) ;
58+ let bytes_read = self . reader . read_line ( & mut line) ?;
59+ if bytes_read == 0 {
60+ return Err ( TransportError :: Io ( std:: io:: Error :: new (
61+ std:: io:: ErrorKind :: UnexpectedEof ,
62+ "EOF while reading headers" ,
63+ ) ) ) ;
64+ }
65+
66+ let trimmed = line. trim_end_matches ( [ '\r' , '\n' ] ) ;
67+ if trimmed. is_empty ( ) {
68+ break ;
69+ }
70+
71+ if let Some ( ( name, value) ) = trimmed. split_once ( ':' ) {
72+ let header_name = CONTENT_LENGTH_HEADER . trim_end_matches ( ':' ) ;
73+ if name. trim ( ) . eq_ignore_ascii_case ( header_name) {
74+ let parsed = value. trim ( ) . parse :: < usize > ( ) . map_err ( |_| {
75+ TransportError :: Io ( std:: io:: Error :: new (
76+ std:: io:: ErrorKind :: InvalidData ,
77+ "invalid Content-Length value" ,
78+ ) )
79+ } ) ?;
80+ content_length = Some ( parsed) ;
81+ }
82+ }
5683 }
5784
58- let trimmed = line. trim ( ) . to_string ( ) ;
59- if trimmed. is_empty ( ) {
60- // Skip blank lines and read the next one
61- return self . read_message ( ) ;
62- }
85+ let len = content_length. ok_or_else ( || {
86+ TransportError :: Io ( std:: io:: Error :: new (
87+ std:: io:: ErrorKind :: InvalidData ,
88+ "missing Content-Length header" ,
89+ ) )
90+ } ) ?;
6391
64- if trimmed. len ( ) > MAX_MESSAGE_BYTES {
65- return Err ( TransportError :: MessageTooLarge (
66- trimmed. len ( ) ,
67- MAX_MESSAGE_BYTES ,
68- ) ) ;
92+ if len > MAX_MESSAGE_BYTES {
93+ return Err ( TransportError :: MessageTooLarge ( len, MAX_MESSAGE_BYTES ) ) ;
6994 }
7095
71- Ok ( trimmed)
96+ let mut body = vec ! [ 0u8 ; len] ;
97+ self . reader . read_exact ( & mut body) ?;
98+ let message = String :: from_utf8 ( body) . map_err ( |_| TransportError :: InvalidUtf8 ) ?;
99+ Ok ( message)
72100 }
73101
74- /// Write a JSON message followed by a newline .
102+ /// Write a JSON message with MCP Content-Length framing .
75103 pub fn write_message ( & mut self , content : & str ) -> Result < ( ) , TransportError > {
104+ let header = format ! ( "Content-Length: {}\r \n \r \n " , content. len( ) ) ;
105+ self . writer . write_all ( header. as_bytes ( ) ) ?;
76106 self . writer . write_all ( content. as_bytes ( ) ) ?;
77- self . writer . write_all ( b"\n " ) ?;
78107 self . writer . flush ( ) ?;
79108 Ok ( ( ) )
80109 }
@@ -95,24 +124,33 @@ mod tests {
95124
96125 #[ test]
97126 fn test_read_write_message ( ) {
98- let input = " {\" test\" :true}\n ";
127+ let input = b"Content-Length: 13 \r \n \r \n {\" test\" :true}";
99128 let mut output = Vec :: new ( ) ;
100129
101- let mut transport =
102- StdioTransport :: new ( std:: io:: Cursor :: new ( input. as_bytes ( ) . to_vec ( ) ) , & mut output) ;
130+ let mut transport = StdioTransport :: new ( std:: io:: Cursor :: new ( input. to_vec ( ) ) , & mut output) ;
103131 let msg = transport. read_message ( ) . unwrap ( ) ;
104132 assert_eq ! ( msg, "{\" test\" :true}" ) ;
133+
134+ transport. write_message ( "hello" ) . unwrap ( ) ;
135+ let written = String :: from_utf8 ( output) . unwrap ( ) ;
136+ assert_eq ! ( written, "Content-Length: 5\r \n \r \n hello" ) ;
105137 }
106138
107139 #[ test]
108- fn test_skip_blank_lines ( ) {
109- let input = " \n \n { \" test \" :true} \n ";
140+ fn test_case_insensitive_content_length ( ) {
141+ let input = b"content-length: 4 \r \n \r \n test ";
110142 let mut output = Vec :: new ( ) ;
111-
112- let mut transport =
113- StdioTransport :: new ( std:: io:: Cursor :: new ( input. as_bytes ( ) . to_vec ( ) ) , & mut output) ;
143+ let mut transport = StdioTransport :: new ( std:: io:: Cursor :: new ( input. to_vec ( ) ) , & mut output) ;
114144 let msg = transport. read_message ( ) . unwrap ( ) ;
115- assert_eq ! ( msg, "{\" test\" :true}" ) ;
145+ assert_eq ! ( msg, "test" ) ;
146+ }
147+
148+ #[ test]
149+ fn test_missing_content_length_fails ( ) {
150+ let input = b"No-Header: value\r \n \r \n {}" ;
151+ let mut output = Vec :: new ( ) ;
152+ let mut transport = StdioTransport :: new ( std:: io:: Cursor :: new ( input. to_vec ( ) ) , & mut output) ;
153+ assert ! ( transport. read_message( ) . is_err( ) ) ;
116154 }
117155
118156 #[ test]
0 commit comments