From df566d4186eaa70ace040e0214242c3ec74db516 Mon Sep 17 00:00:00 2001 From: Gilles Ceyssat Date: Tue, 12 May 2026 07:26:33 +0400 Subject: [PATCH] fix(mcp): StdioTransport.send_notification must not read response The base MCPTransport.send_notification falls back to self.send() which writes a request AND reads a response. For stdio MCP servers this hangs forever because notifications (per JSON-RPC 2.0 spec) have no response. This affected every stdio MCP server attached to OpenJarvis: after MCPClient.initialize() sent its `initialize` request and received the server capabilities, it sent the spec-required `notifications/initialized` notification, which then blocked indefinitely on proc.stdout.readline(). StreamableHTTPTransport already overrides send_notification correctly (transport.py:213). This adds the symmetric fix for StdioTransport so stdio MCP servers can complete the handshake and be discovered. Reproduced with the CyberStrikeAI cmd/mcp-stdio server (78 tools): without the fix, `_discover_external_mcp` hangs forever in `MCPClient.initialize`. With the fix, all 78 tools are discovered cleanly. --- src/openjarvis/mcp/transport.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/openjarvis/mcp/transport.py b/src/openjarvis/mcp/transport.py index 2c40bacc..11f8020f 100644 --- a/src/openjarvis/mcp/transport.py +++ b/src/openjarvis/mcp/transport.py @@ -88,6 +88,20 @@ class StdioTransport(MCPTransport): raise RuntimeError("No response from subprocess") return MCPResponse.from_json(response_line.strip()) + def send_notification(self, request: MCPRequest) -> None: + """Send a JSON-RPC notification — write only, never read. + + Overrides the base implementation: stdio servers do not reply + to notifications, so the default ``send()`` would block forever + on ``proc.stdout.readline()``. + """ + proc = self._process + if proc is None or proc.stdin is None: + raise RuntimeError("Transport process is not running") + line = request.to_json() + "\n" + proc.stdin.write(line) + proc.stdin.flush() + def close(self) -> None: """Terminate the subprocess.""" if self._process is not None: