Skip to content

stdio transport (default) has no SIGINT/SIGTERM handling, orphaning spawned LSP processes #241

Description

@bug-ops

Description

run_http (crates/mcpls-core/src/transport.rs:120-196) installs SIGTERM/SIGINT handlers and drives axum::serve(...).with_graceful_shutdown(...), explicitly calling cancel.cancel() on signal receipt so the HTTP transport shuts down cleanly. run_stdio (crates/mcpls-core/src/transport.rs:84-102), which is the default transport used by every stdio-based MCP client (Claude Desktop, Claude Code, etc.), installs no signal handler at all — it just awaits service.waiting().

Every spawned LSP child process (LspServer::spawn, crates/mcpls-core/src/lsp/lifecycle.rs:232-238) relies on tokio::process::Command::kill_on_drop(true) to be reaped when mcpls exits. kill_on_drop only fires when the Child's Drop runs, i.e. during normal unwinding as main() returns. On Unix, an uncaught SIGINT/SIGTERM terminates the process immediately without running destructors (this is the default signal disposition, bypassed only if a handler is registered — which run_http does and run_stdio does not). The workspace also sets panic = "abort" (Cargo.toml:60), so any reachable panic has the same effect.

Separately, LspServer::shutdown() (crates/mcpls-core/src/lsp/lifecycle.rs:445-459), which sends the graceful LSP shutdown request + exit notification, is never called from serve_with or any production path — only from unit tests. So even on the clean-exit path (stdin EOF), spawned LSP servers are only ever killed, never given the LSP-level chance to shut down gracefully.

Reproduction Steps

  1. Run mcpls directly in a terminal (stdio mode, no --listen) with a configured LSP server (e.g. rust-analyzer) in a workspace.
  2. Confirm the LSP server process is running (ps aux | grep rust-analyzer).
  3. Send Ctrl-C (SIGINT) or kill -TERM <mcpls-pid> to the mcpls process.
  4. Observe the LSP server process is still running (orphaned), reparented to init/launchd, with no equivalent SIGTERM/SIGINT handler in run_stdio to trigger cleanup.

Expected Behavior

run_stdio installs the same SIGINT/SIGTERM handling as run_http and, on receipt, drives an orderly shutdown: send LSP shutdown/exit to each running LspServer (via the currently-unused LspServer::shutdown()) before process exit, falling back to kill_on_drop only if a server doesn't respond in time.

Actual Behavior

No signal handler is registered for the stdio transport; abnormal termination (signal or panic) orphans every spawned LSP child process. On the normal exit path, LSP servers are killed rather than gracefully shut down, since LspServer::shutdown() is dead code in production.

Environment

  • Version: 0.3.7 (current main, commit 2c8ad7f)
  • Features: default (stdio transport)

Logs / Evidence

  • crates/mcpls-core/src/transport.rs:84-102 (run_stdio, no signal handling)
  • crates/mcpls-core/src/transport.rs:120-196 (run_http, has SIGTERM/SIGINT + graceful shutdown)
  • crates/mcpls-core/src/lsp/lifecycle.rs:232-238 (kill_on_drop(true))
  • crates/mcpls-core/src/lsp/lifecycle.rs:445-459 (LspServer::shutdown(), unreachable from production code — only referenced in bridge/state.rs test modules)
  • Cargo.toml:60 (panic = "abort" in [profile.release])

Metadata

Metadata

Assignees

Labels

P3Low: cosmetic, edge case unlikely in practicebugSomething isn't workingmcpls-coremcpls-core crate changes

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions