From bde4d427e3fd0cb74a1b147421115c49decd6855 Mon Sep 17 00:00:00 2001 From: "Anaz S. Aji" Date: Wed, 10 Jun 2026 09:25:34 +0700 Subject: [PATCH] fix: MCP stdio parsing for multi-line JSON-RPC + dismiss test secrets alerts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code Scanning fixes: - Alert #88-90: replaced line-based stdin parsing with byte-level brace-depth tracking that handles pretty-printed JSON-RPC messages - Alert #82,85-87,91: dismissed — test fixtures for secrets detection (AKIAIOSFODNN7EXAMPLE is AWS example key) --- src/mcp/server.rs | 103 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/src/mcp/server.rs b/src/mcp/server.rs index 1687447..3d86740 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -3,7 +3,7 @@ //! Reads JSON-RPC requests from stdin, dispatches to tool handlers, //! writes responses to stdout. -use std::io::{self, BufRead, Write}; +use std::io::{self, Read, Write}; use tracing::{debug, error, info}; @@ -22,47 +22,88 @@ const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); pub fn run_server() -> anyhow::Result<()> { info!("Starting cora MCP server on stdio"); - let stdin = io::stdin(); let stdout = io::stdout(); let mut stdout_lock = stdout.lock(); + let mut buffer = String::new(); - for line in stdin.lock().lines() { - let line = line?; - let trimmed = line.trim(); + // Read stdin byte-by-byte to handle multi-line JSON-RPC messages. + // Line-based parsing breaks on pretty-printed JSON. + // MCP spec: each message is a complete JSON object, optionally followed by newline. + let mut brace_depth: i32 = 0; + let mut in_string = false; + let mut escape_next = false; - if trimmed.is_empty() { + for b in io::BufReader::new(io::stdin()).bytes() { + let byte = b?; + let ch = byte as char; + + if escape_next { + escape_next = false; + buffer.push(ch); + continue; + } + + if ch == '\\' && in_string { + escape_next = true; + buffer.push(ch); continue; } - debug!(input = trimmed, "received request"); - - let request: JsonRpcRequest = match serde_json::from_str(trimmed) { - Ok(req) => req, - Err(e) => { - error!(error = %e, "failed to parse request"); - let err_resp = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: None, - result: None, - error: Some(JsonRpcError { - code: -32700, - message: format!("Parse error: {e}"), - data: None, - }), - }; - write_response(&mut stdout_lock, &err_resp)?; + if ch == '"' { + in_string = !in_string; + buffer.push(ch); + continue; + } + + if !in_string { + if ch == '{' { + brace_depth += 1; + } else if ch == '}' { + brace_depth -= 1; + } + } + + buffer.push(ch); + + // Complete JSON object found when braces are balanced and buffer is non-empty + if brace_depth == 0 && !buffer.trim().is_empty() { + let trimmed = buffer.trim().to_string(); + buffer.clear(); + + if trimmed.is_empty() { continue; } - }; - let response = handle_request(&request); - write_response(&mut stdout_lock, &response)?; - stdout_lock.flush()?; + debug!(input = %trimmed, "received request"); + + let request: JsonRpcRequest = match serde_json::from_str(&trimmed) { + Ok(req) => req, + Err(e) => { + error!(error = %e, "failed to parse request"); + let err_resp = JsonRpcResponse { + jsonrpc: "2.0".to_string(), + id: None, + result: None, + error: Some(JsonRpcError { + code: -32700, + message: format!("Parse error: {e}"), + data: None, + }), + }; + write_response(&mut stdout_lock, &err_resp)?; + continue; + } + }; - // Exit on shutdown notification - if request.method == "notifications/cancelled" || request.method == "shutdown" { - info!("Shutting down MCP server"); - break; + let response = handle_request(&request); + write_response(&mut stdout_lock, &response)?; + stdout_lock.flush()?; + + // Exit on shutdown notification + if request.method == "notifications/cancelled" || request.method == "shutdown" { + info!("Shutting down MCP server"); + break; + } } }