From 22f896536d65ded91bfc891f470c9fa3fa38130e Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 25 Oct 2026 04:05:32 +0000 Subject: [PATCH] Reply with a parse error on non-UTF-8 lines in MCP serve Found while testing crates/mcp/src/server.rs:62-64 (Server::serve) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/mcp/src/server.rs | 15 +++++++++++---- crates/mcp/src/tests.rs | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/mcp/src/server.rs b/crates/mcp/src/server.rs index 328df76d3..b5d2d8175 100644 --- a/crates/mcp/src/server.rs +++ b/crates/mcp/src/server.rs @@ -59,10 +59,17 @@ impl Server { /// Serve newline-delimited JSON-RPC until `input` closes. Logs go to stderr only (stdout is /// the protocol stream). - pub fn serve(&mut self, input: impl BufRead, mut output: impl Write) -> std::io::Result<()> { - for line in input.lines() { - let line = line?; - if let Some(reply) = self.handle_line(&line) { + pub fn serve(&mut self, mut input: impl BufRead, mut output: impl Write) -> std::io::Result<()> { + let mut buf = Vec::new(); + loop { + buf.clear(); + if input.read_until(b'\n', &mut buf)? == 0 { + break; + } + // Invalid UTF-8 becomes U+FFFD, so the line gets a parse error instead of ending the stream. + let line = String::from_utf8_lossy(&buf); + let line = line.trim_end_matches(['\r', '\n']); + if let Some(reply) = self.handle_line(line) { output.write_all(reply.as_bytes())?; output.write_all(b"\n")?; output.flush()?; diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index 21270e5e8..66ea74b55 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -76,6 +76,28 @@ fn framing_basics() { assert_eq!(rpc(&mut s, 5, "tools/call", json!({}))["error"]["code"], -32602); } +#[test] +fn serve_survives_invalid_utf8_line() { + let mut s = server(); + let mut input = Vec::new(); + input.extend_from_slice(json!({"jsonrpc":"2.0","id":1,"method":"ping"}).to_string().as_bytes()); + input.extend_from_slice(b"\n\xff\xfe not utf-8\n"); + input.extend_from_slice(json!({"jsonrpc":"2.0","id":2,"method":"ping"}).to_string().as_bytes()); + input.extend_from_slice(b"\n\r\n"); + input.extend_from_slice(json!({"jsonrpc":"2.0","id":3,"method":"ping"}).to_string().as_bytes()); + input.extend_from_slice(b"\r\n"); + let mut out = Vec::new(); + let res = s.serve(input.as_slice(), &mut out); + assert!(res.is_ok(), "serve stopped: {res:?}"); + let lines: Vec = String::from_utf8(out).unwrap().lines().map(|l| serde_json::from_str(l).unwrap()).collect(); + assert_eq!(lines.len(), 4, "{lines:?}"); + assert_eq!(lines[0]["id"], 1); + assert_eq!(lines[1]["error"]["code"], -32700); + assert!(lines[1]["id"].is_null()); + assert_eq!(lines[2]["id"], 2); + assert_eq!(lines[3]["id"], 3); +} + #[test] fn protocol_round_trip_over_serve() { let mut s = server();