From 9ef617289fbb4594c28a19a5afa9519be566882c Mon Sep 17 00:00:00 2001 From: Preventnetworkhacking Date: Sat, 28 Feb 2026 20:14:57 -0800 Subject: [PATCH] fix(mcp): stdio transport reads server notifications as tool responses, registering 0 tools [CDV-2327] Replace single read with deadline-bounded loop that skips JSON-RPC messages where id is None (server notifications like notifications/initialized). Some MCP servers send notifications/initialized after the initialize response but before the tools/list response. The old code would read this notification as the tools/list reply, see result: None, and report 0 tools registered. The fix uses a deadline-bounded loop to skip any JSON-RPC message where id is None while preserving the total timeout across all iterations. Fixes: zeroclaw-labs/zeroclaw#2327 --- src/tools/mcp_transport.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/tools/mcp_transport.rs b/src/tools/mcp_transport.rs index 61052a343..27398451c 100644 --- a/src/tools/mcp_transport.rs +++ b/src/tools/mcp_transport.rs @@ -107,12 +107,27 @@ impl McpTransportConn for StdioTransport { error: None, }); } - let resp_line = timeout(Duration::from_secs(RECV_TIMEOUT_SECS), self.recv_raw()) - .await - .context("timeout waiting for MCP response")??; - let resp: JsonRpcResponse = serde_json::from_str(&resp_line) - .with_context(|| format!("invalid JSON-RPC response: {}", resp_line))?; - Ok(resp) + let deadline = std::time::Instant::now() + Duration::from_secs(RECV_TIMEOUT_SECS); + loop { + let remaining = deadline.saturating_duration_since(std::time::Instant::now()); + if remaining.is_zero() { + bail!("timeout waiting for MCP response"); + } + let resp_line = timeout(remaining, self.recv_raw()) + .await + .context("timeout waiting for MCP response")??; + let resp: JsonRpcResponse = serde_json::from_str(&resp_line) + .with_context(|| format!("invalid JSON-RPC response: {}", resp_line))?; + if resp.id.is_none() { + // Server-sent notification (e.g. `notifications/initialized`) — skip and + // keep waiting for the actual response to our request. + tracing::debug!( + "MCP stdio: skipping server notification while waiting for response" + ); + continue; + } + return Ok(resp); + } } async fn close(&mut self) -> Result<()> {