Skip to content

[LOW] state-mcp launch wrappers and duplicate declarations amplify lingering host processes #2300

Closed

Description

@tamirdresher

Summary

state-mcp server processes accumulate heavily on a long-running Copilot development host. One inventory found 422 matching processes arranged under 107 launcher roots. Each bridge used a four-process wrapper chain:

cmd.exe → node npx-cli.js → cmd.exe "squad state-mcp" → node cli-entry.js

The global Copilot MCP config declared duplicate hashed bridge entries, so a single lingering Copilot session could retain roughly eight Squad-related processes. The roots still had live copilot.exe / copilot-runtime.exe parents, so they were not killed as orphans; however, Squad can substantially reduce the amplification and should terminate promptly when its transport closes.

Environment

  • Squad CLI 2.0.0-preview.1, installed from the exact published release tarball
  • Windows 11
  • Node.js 22
  • CFS-only npm host (https://packagefeedproxy.microsoft.io/npm/)
  • Long-running Copilot development host with both global hashed MCP entries and a repo .mcp.json

Repro

$all = Get-CimInstance Win32_Process
$stateMcp = $all | Where-Object { $_.CommandLine -match 'state-mcp' }
$stateMcp | Measure-Object
$stateMcp | Select-Object ProcessId, ParentProcessId, Name, CommandLine

Open and close multiple Copilot sessions in repositories that have both a repo squad_state declaration and hashed global declarations, then repeat the inventory.

Observed

422 processes matching state-mcp
107 launcher roots
63 distinct live copilot.exe / copilot-runtime.exe parents

The repeated launch shape was:

cmd.exe → node npx-cli.js → cmd.exe → node cli-entry.js

There were no parent-gone roots in the captured snapshot; the process retention begins with lingering host sessions, but duplicate declarations and wrapper processes multiply the footprint.

Expected

  • One effective Squad state bridge declaration per project/session.
  • Direct launch of the installed CLI entry point without npx/cmd wrapper layers.
  • The MCP server exits when stdin/transport closes or its parent session is gone.
  • Upgrade removes obsolete duplicate hashed declarations.

Impact / Severity

LOW (host/resource issue). The primary session-lifetime leak is upstream, but Squad currently amplifies every retained session into several extra processes. On a dev box this reached hundreds of Node/cmd processes.

Proposed fix

  1. Generate a single canonical bridge declaration and remove stale duplicates during upgrade.
  2. Launch node <installed-cli-entry> state-mcp directly.
  3. Explicitly handle stdin EOF/transport close and terminate the server process tree.
  4. Add a lifecycle test that starts the stdio server, closes input, and asserts timely exit with no wrapper descendants.

Activity

  1. johannschiffer commented on Oct 24, 2026

    @johannschiffer

    This process tree explosion (cmd.exe -> node npx-cli.js -> cmd.exe squad.cmd -> node cli-entry.js) is a direct consequence of how Windows command wrappers (.cmd batch files) interact with Node.js and MCP stdio lifecycles.

    Root Cause Analysis

    1. Double Batch Wrapper Amplification:
      On Windows, invoking npx resolves to npx.cmd. In Windows, .cmd files cannot be executed directly by CreateProcessW without spawning an intermediate cmd.exe shell. That cmd.exe spawns Node to run npx-cli.js. In turn, npx resolves @bradygaster/squad-cli and executes its bin entry, which is another .cmd file (squad.cmd), causing Node to spawn another cmd.exe, which spawns the final node cli-entry.js.

    2. Missing JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE:
      When Copilot or Claude Desktop disconnects from the stdio pipe, only the top-level process receives EOF. Windows cmd.exe does not propagate process termination signals downstream to grandchildren unless wrapped in a Win32 Job Object configured with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. As a result, the grandchildren survive as orphaned background processes, holding ports and memory.

    Recommended Fixes

    1. Bypass the Batch Shell Wrappers (Collapse 4 processes to 1):
      Instead of registering npx -y @bradygaster/squad-cli state-mcp, have the launcher resolve the entry JavaScript file and launch node directly:

      {
        "command": "node",
        "args": [
          "C:\\Users\\...\\AppData\\Local\\npm-cache\\...\\cli.js",
          "state-mcp"
        ]
      }

      Direct execution of node.exe eliminates both intermediate cmd.exe shells, collapsing the entire chain into a single process that terminates immediately when stdin closes.

    2. Deduplicate Bridge Registration via Normalized Keys:
      Copilot MCP config hashing should canonicalize paths (lowercasing drive letters and resolving symlinks/subst drives) so that multiple active editor workspaces do not spawn redundant parallel instances of state-mcp.

    3. Single-Process Paradigm in Modern Toolchains:
      For comparison, in Python MCP toolchains (e.g. uvx pgsafe-mcp, uvx browserbridge-mcp, or uvx crawlengine-mcp), uvx is a compiled Rust binary that executes the target entrypoint directly without shell wrapping, avoiding the multi-tiered batch wrapper leakage observed with npm on Windows.


    Technical Reference & Native Stdio Architecture

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions