-
Notifications
You must be signed in to change notification settings - Fork 3k
Description
Description
In McpHub.connectToServer(), when connecting to an MCP server of type stdio, the transport is started (spawning a child process) before client.connect(transport) is called. If the MCP handshake fails, the catch block marks the connection as "disconnected" but never calls transport.close(), leaving the child process running as an orphan.
Root Cause
File: src/services/mcp/McpHub.ts
// Line ~755: Transport is started (spawns child process)
await transport.start()
// Line ~875: Connection pushed to this.connections BEFORE connect()
this.connections.push(connection)
// Line ~878: If this throws, the catch block doesn't close the transport
await client.connect(transport)
// Lines ~887-895: Catch block
} catch (error) {
// ❌ No transport.close() — child process stays alive
connection.server.status = "disconnected"
// ... error handling, but transport is abandoned
}Impact
sequenceDiagram
participant Hub as McpHub
participant T as StdioTransport
participant CP as Child Process
participant Client as MCP Client
Hub->>T: transport.start()
T->>CP: spawn child process ✅
Hub->>Hub: this.connections.push(connection)
Hub->>Client: client.connect(transport)
Client-->>Hub: throws (handshake failed)
Hub->>Hub: catch: set status = "disconnected"
Note over T,CP: transport.close() never called ⚠️
Note over CP: Child process stays alive forever ⚠️
Each retry spawns a new orphan. If the MCP server binary starts successfully but fails the protocol handshake (e.g., prints output but doesn't speak MCP), every connection attempt leaks another child process. Over time, this causes:
- Accumulating zombie processes
- Memory and CPU waste
- Potential port/resource exhaustion if the child process binds resources
Reproduction
- Configure an MCP server of type
stdiopointing to a binary that starts but doesn't speak MCP (e.g.,echo "hello"or a web server binary) - Observe that the child process remains alive after the connection error
- Trigger a retry (automatic or manual) — another orphaned process spawns
- Check with
ps aux | grep <binary-name>— processes accumulate
Proposed Fix
Add transport.close() in the catch block:
} catch (error) {
try {
await transport.close()
} catch {
// Ignore close errors
}
connection.server.status = "disconnected"
// ... rest of error handling
}Or use a try/finally pattern to ensure cleanup regardless of success/failure:
let connected = false
try {
await client.connect(transport)
connected = true
} finally {
if (!connected) {
try { await transport.close() } catch {}
}
}Environment
- Roo-Code: latest
- File:
src/services/mcp/McpHub.ts - Affects: All stdio-type MCP server connections
I'm happy to open a PR for this fix if you'd like.