Skip to content

/exit hangs when MCP servers are connected — processes not cleaned up #354

@angeltsalazar

Description

@angeltsalazar

Bug Report: /exit hangs when MCP servers are connected — processes not cleaned up

Environment

  • CommandCode version: 0.25.12
  • OS: macOS (Darwin, ARM64)
  • Node.js: Bundled with npm install
  • Installation: npm install -g command-code

Description

When MCP servers are configured and active, the /exit command does not properly disconnect or terminate the underlying MCP child processes (stdio transports spawned via child_process.spawn() or HTTP transports). This leaves the CommandCode process hanging indefinitely, requiring the user to send Ctrl+C (SIGINT) or kill -9 from another terminal to regain shell control.

Steps to Reproduce

  1. Configure one or more MCP servers (e.g., stdio-based via npx, or HTTP-based).
  2. Launch commandcode (or cmd).
  3. Verify MCP servers are connected using /mcp.
  4. Type /exit and press Enter.
  5. Expected: CommandCode cleanly disconnects all MCP transports, terminates child processes, and returns to the shell prompt.
  6. Actual: CommandCode displays the logo/session end and hangs. The terminal cursor blinks but never returns to the shell prompt.

Evidence

Process tree before /exit

$ ps aux | grep -i mcp
angelsalazar   38172   0.0  0.1  1234567  2345 s000  S+    3:42PM   0:02.45 node /.../mcp-server/index.js

After /exit — CommandCode hangs

# Command Code v0.25.12
# models: kimi-k2.6 · taste-1
# /Volumes/SSDWD2T/gic-projects/PRESTAMO-VEHICULAR
/exit
[HANGS — cursor blinking, no shell prompt]

After forced Ctrl+C — orphaned MCP still alive

$ ps aux | grep 38172
angelsalazar   38172   0.0  0.1  1234567  2345 s000  S     3:42PM   0:02.45 node /.../mcp-server/index.js

The MCP child process (PID 38172) remains alive even after CommandCode is forcefully interrupted.

Root Cause Analysis (from reverse-engineered dist/index.mjs)

In the command handler for /exit (found at approximately line 2xx in the bundled output):

if("/exit"===s) return t.setShouldExit(!0),{status:"handled"}

This sets a React state flag shouldExit but does not invoke any cleanup routine for active MCP connections before returning. The McpConnectionManager (which tracks StdioTransport and HttpTransport instances) has a close() method available, but it is never called during the exit flow.

Relevant code patterns found in the bundle:

StdioTransport (src/mcp/client/stdio-transport.ts):

async close() {
    this.process && (this.process.stdin?.end(), this.process.kill(), this.process = null);
    this.isConnected = false;
    this.pendingRequests.clear();
}

HttpTransport (src/mcp/client/http-transport.ts):

// Uses AbortController; connection cleanup relies on transport.close()

Neither transport's close() is invoked when setShouldExit(true) fires. The stdio child processes (node, npx, python, etc.) are therefore never signaled to terminate, keeping the Node.js event loop alive and preventing clean process exit.

Impact

  • User Experience: Shell becomes unresponsive after every session end when MCPs are active.
  • Resource Leak: Orphaned Node.js/Python processes accumulate in the background.
  • Workaround Required: Users must wrap commandcode in a custom shell script that force-kills MCP processes after exit (see workaround below).

Workaround (for users until fixed)

Create a wrapper script that snapshots MCP processes before launch and kills orphans after exit:

#!/bin/bash
BEFORE=$(ps -eo pid,args | grep -i mcp | grep -v grep | awk '{print $1}')
commandcode "$@"
# After exit, kill any new MCP processes
for pid in $(ps -eo pid,args | grep -i mcp | grep -v grep | awk '{print $1}'); do
    [[ -n "$pid" ]] && ! echo "$BEFORE" | grep -q "^${pid}$" && kill -9 "$pid" 2>/dev/null
done

Suggested Fix

In the /exit handler (or in the React component's exit effect), iterate through McpConnectionManager.getConnectedServers() and call disconnect()/close() on each transport before allowing the process to terminate:

// Pseudocode for the exit flow
async function handleExit() {
    const mcpManager = getMcpConnectionManager();
    for (const server of mcpManager.getConnectedServers()) {
        if (server.transport?.close) {
            await server.transport.close();
        }
    }
    setShouldExit(true);
}

Alternatively, register an atexit/process.on('exit') handler in McpConnectionManager itself to ensure cleanup happens regardless of how the CLI shuts down.

Related Issues

  • None found — searched GitHub issues for mcp hang, exit freeze, process stuck, and mcp cleanup with no matching results. This appears to be a new, unreported bug.

Additional Context

The issue was discovered while comparing CommandCode behavior with OpenCode (another AI coding agent). OpenCode cleanly disconnects MCP servers before exit, while CommandCode v0.25.12 consistently reproduces the hang.


Labels: bug, mcp, process-management, macOS

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions