Skip to content

fix(transport): handle SIGINT/SIGTERM in stdio transport and gracefully shut down LSP servers - #270

Merged
bug-ops merged 3 commits into
mainfrom
fix/241-stdio-sigterm-handling
Aug 4, 2026
Merged

fix(transport): handle SIGINT/SIGTERM in stdio transport and gracefully shut down LSP servers#270
bug-ops merged 3 commits into
mainfrom
fix/241-stdio-sigterm-handling

Conversation

@bug-ops

@bug-ops bug-ops commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • run_stdio, the default transport used by every stdio-based MCP client (Claude Desktop, Claude Code, etc.), installed no signal handler at all — an uncaught SIGINT/SIGTERM bypassed kill_on_drop and orphaned every spawned LSP child process. It now shares the same signal-handling logic run_http already had.
  • LspServer::shutdown() (the graceful LSP shutdown/exit handshake) was previously dead code outside tests. serve_with now calls a new Translator::shutdown_servers() after the transport future resolves — on signal, stdin EOF, or HTTP's own shutdown — which drains and gracefully shuts down every registered LspServer concurrently, with a bounded per-server grace period before falling back to kill_on_drop.
  • run_http's own graceful shutdown wait is now bounded too, so a stuck in-flight connection can't block LSP cleanup indefinitely.

Known limitation

Process termination via an uncaught panic under panic = "abort" ([profile.release]) still bypasses this cleanup, since no Drop runs on that path. A real fix needs process-group isolation, which is out of scope here and documented as a # Limitations note on Translator::shutdown_servers. Recommend filing a narrower follow-up issue for that vector before/independent of closing this one.

Test plan

  • cargo +nightly fmt --all -- --check
  • cargo clippy --all-targets --all-features --workspace -- -D warnings
  • cargo nextest run --workspace --all-features --lib --bins (489/489)
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features --workspace
  • New unit tests: empty-registry shutdown, concurrent drain via a fake LSP server, serve_with's shutdown path draining a registered server, run_stdio's prompt-return behavior, and a paused-clock regression test proving run_http no longer self-terminates on ordinary uptime
  • Manually verified against the built binary: stdio SIGTERM/SIGINT no longer orphans a spawned LSP server; HTTP transport stays up over multi-minute uptime with no signal, shuts down cleanly within ~0.02s on SIGTERM, and correctly force-times-out ~30s after a signal if a connection is stuck mid-drain

Closes #241

@github-actions github-actions Bot added documentation Improvements or additions to documentation rust Rust code changes mcpls-core mcpls-core crate changes labels Aug 4, 2026
@bug-ops
bug-ops requested a lite review from Copilot August 4, 2026 20:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses process hygiene in mcpls-core by ensuring both stdio and HTTP transports react to SIGINT/SIGTERM and trigger an orderly shutdown path that drains and shuts down spawned LSP servers instead of orphaning them.

Changes:

  • Added shared SIGINT/SIGTERM handling and applied it to both run_stdio and run_http, including a bounded HTTP graceful shutdown wait.
  • Introduced a post-transport shutdown sequence in serve_with that signals background tasks to stop and drains registered LSP servers via Translator::shutdown_servers().
  • Extended LspServer::shutdown() to attempt the LSP shutdown/exit handshake and then wait a bounded grace period for the child process to exit before relying on kill_on_drop.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/mcpls-core/src/transport.rs Adds shared shutdown-signal handling; stdio returns on signal; HTTP graceful shutdown is bounded post-signal.
crates/mcpls-core/src/lsp/mod.rs Re-exports a test-only fake LSP server helper for shutdown-path tests.
crates/mcpls-core/src/lsp/lifecycle.rs Adds bounded child-exit grace in LspServer::shutdown() and introduces a shared test helper for mock servers.
crates/mcpls-core/src/lib.rs Ensures transport completion triggers a centralized shutdown sequence that drains LSP servers.
crates/mcpls-core/src/bridge/translator.rs Implements shutdown_servers() to concurrently drain and shut down registered servers with per-server timeouts.
CHANGELOG.md Documents the fix and its limitations under Unreleased/Fixed.

Comment thread crates/mcpls-core/src/lsp/lifecycle.rs
Comment thread crates/mcpls-core/src/lsp/lifecycle.rs
bug-ops added a commit that referenced this pull request Aug 4, 2026
… comment

Copilot review on #270 correctly flagged the doc comment claiming
fake_lsp_server() was pub(crate) when the signature is pub — clarify that
pub is intentional (lifecycle is a private module, so pub already stays
crate-scoped, and clippy's redundant_pub_crate lint forbids the pub(crate)
form here).
@bug-ops
bug-ops enabled auto-merge (squash) August 4, 2026 20:31
bug-ops added a commit that referenced this pull request Aug 4, 2026
… comment

Copilot review on #270 correctly flagged the doc comment claiming
fake_lsp_server() was pub(crate) when the signature is pub — clarify that
pub is intentional (lifecycle is a private module, so pub already stays
crate-scoped, and clippy's redundant_pub_crate lint forbids the pub(crate)
form here).
@bug-ops
bug-ops force-pushed the fix/241-stdio-sigterm-handling branch from 9c124f6 to af72641 Compare August 4, 2026 20:38
bug-ops added 2 commits August 4, 2026 22:38
…ly shut down LSP servers

run_stdio, the default transport for stdio-based MCP clients, installed no
signal handler, so an uncaught SIGINT/SIGTERM bypassed kill_on_drop and
orphaned every spawned LSP child process. Separately, LspServer::shutdown()
was dead code outside tests, so even the clean stdin-EOF exit path never
gave LSP servers a graceful shutdown/exit handshake.

Both transports now share signal-handling logic, and serve_with calls a new
Translator::shutdown_servers() after the transport future resolves,
draining and gracefully shutting down every registered LspServer
concurrently with a bounded per-server grace period before falling back to
kill_on_drop. run_http's graceful shutdown wait is now itself bounded so a
stuck connection can't block LSP cleanup indefinitely.

Known limitation: process termination via an uncaught panic under
panic = "abort" still bypasses this cleanup, since no Drop runs on that
path; a real fix needs process-group isolation and is out of scope here.

Closes #241
… comment

Copilot review on #270 correctly flagged the doc comment claiming
fake_lsp_server() was pub(crate) when the signature is pub — clarify that
pub is intentional (lifecycle is a private module, so pub already stays
crate-scoped, and clippy's redundant_pub_crate lint forbids the pub(crate)
form here).
@bug-ops
bug-ops force-pushed the fix/241-stdio-sigterm-handling branch from af72641 to e47ffe2 Compare August 4, 2026 20:38
…io test

The rebase conflict fix for McplsServer::new's new fifth parameter
(added by #248 on main) was applied to the working tree but never
committed, so CI still built against the stale four-argument call.
@bug-ops
bug-ops merged commit ec67fa4 into main Aug 4, 2026
27 checks passed
@bug-ops
bug-ops deleted the fix/241-stdio-sigterm-handling branch August 4, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation mcpls-core mcpls-core crate changes rust Rust code changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants