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 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-25 04:05:32 +00:00
co-authored by Claude Haiku 5.5
parent 5971d46391
commit 22f896536d
2 changed files with 33 additions and 4 deletions
+11 -4
View File
@@ -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()?;
+22
View File
@@ -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<Value> = 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();