From bf9066a602a3ebf9c130b977511a3fb3b5b2ea2c Mon Sep 17 00:00:00 2001 From: Philippe Branchu Date: Sun, 22 Mar 2026 16:49:28 +0000 Subject: [PATCH] Fix MCP bridge dropping tool results from servers that send notifications Read response lines until finding a JSON-RPC response matching the request ID. Previously, the bridge read one line and assumed it was the response, causing "No result from MCP tools/call" when MCP servers send notifications or log lines before the actual result. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/openfang-runtime/src/mcp.rs | 61 +++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/crates/openfang-runtime/src/mcp.rs b/crates/openfang-runtime/src/mcp.rs index 0fbfbb98..c77f1c7f 100644 --- a/crates/openfang-runtime/src/mcp.rs +++ b/crates/openfang-runtime/src/mcp.rs @@ -328,24 +328,51 @@ impl McpConnection { .await .map_err(|e| format!("Failed to flush stdin: {e}"))?; - // Read response line - let mut line = String::new(); - let timeout = tokio::time::Duration::from_secs(self.config.timeout_secs); - match tokio::time::timeout(timeout, stdout.read_line(&mut line)).await { - Ok(Ok(0)) => return Err("MCP server closed connection".to_string()), - Ok(Ok(_)) => {} - Ok(Err(e)) => return Err(format!("Failed to read MCP response: {e}")), - Err(_) => return Err("MCP request timed out".to_string()), + // Read response lines until we find one matching our request ID. + // MCP servers may send notifications or log lines before the + // actual response. + let timeout_dur = tokio::time::Duration::from_secs(self.config.timeout_secs); + let deadline = tokio::time::Instant::now() + timeout_dur; + + loop { + let mut line = String::new(); + let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); + if remaining.is_zero() { + return Err("MCP request timed out".to_string()); + } + match tokio::time::timeout(remaining, stdout.read_line(&mut line)).await { + Ok(Ok(0)) => return Err("MCP server closed connection".to_string()), + Ok(Ok(_)) => {} + Ok(Err(e)) => return Err(format!("Failed to read MCP response: {e}")), + Err(_) => return Err("MCP request timed out".to_string()), + } + + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + // Try to parse as JSON-RPC response + let parsed: Result = serde_json::from_str(trimmed); + match parsed { + Ok(response) if response.id == Some(id) => { + if let Some(err) = response.error { + return Err(format!("{err}")); + } + return Ok(response.result); + } + Ok(_other) => { + // Response for a different ID or notification — skip + debug!("MCP: skipping non-matching response line"); + continue; + } + Err(_) => { + // Not valid JSON-RPC — skip (could be a log line) + debug!("MCP: skipping non-JSON line from server"); + continue; + } + } } - - let response: JsonRpcResponse = serde_json::from_str(line.trim()) - .map_err(|e| format!("Invalid MCP JSON-RPC response: {e}"))?; - - if let Some(err) = response.error { - return Err(format!("{err}")); - } - - Ok(response.result) } McpTransportHandle::Sse { client, url } => { let response = client