Skip to content

Conversation

elewis787
Copy link

@elewis787 elewis787 commented Oct 1, 2025

Description

This adds a fallback to help clean up a process that may not be closing. Currently, processes can be held open, causing the close function to not finish.

I believe this fixes #131 and #498.

Type of Change

  • [ x] Bug fix (non-breaking change that fixes an issue)

Checklist

  • [x ] My code follows the code style of this project
  • [x ] I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation accordingly

MCP Spec Compliance

  • [ x] This PR implements a feature defined in the MCP specification
  • Link to relevant spec section: Link text
  • Implementation follows the specification exactly

Additional Information

This is largely pulled from the official MCP-Go SDK https://github.com/modelcontextprotocol/go-sdk/blob/ab092510d20a23bed672a27164957e0ff1104bc7/mcp/cmd.go#L100

Summary by CodeRabbit

  • New Features

    • Added a configurable graceful shutdown timeout for stdio transport, with a default of 5 seconds.
  • Bug Fixes

    • Improved shutdown reliability: input streams are closed first, the process is given time to exit, and termination escalates if needed to prevent hangs or orphaned processes.
    • More informative errors when a subprocess fails to terminate within the allotted time.

Copy link
Contributor

coderabbitai bot commented Oct 1, 2025

Walkthrough

Adds a configurable graceful-termination mechanism to stdio transport. Introduces terminateDuration with a default of 5 seconds, option to override, and a multi-stage Close(): close stdin/stderr, wait up to duration, send SIGTERM, wait again, then SIGKILL if needed. Constructors updated; imports for time/syscall added.

Changes

Cohort / File(s) Summary of changes
Stdio termination control and shutdown sequence
client/transport/stdio.go
Added terminateDuration field and WithTerminateDuration option; defaulted to 5s in NewIO/NewStdioWithOptions; expanded Close() to staged shutdown (close pipes, timed wait, SIGTERM, timed wait, SIGKILL, error if still running); imported time and syscall; updated comments.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The title “feat: add additional process kill for sigterm and sigkill” succinctly captures the core change of introducing a fallback kill mechanism and specifically mentions SIGTERM and SIGKILL, which aligns directly with the primary enhancements in the PR.
Linked Issues Check ✅ Passed The changes implement a multi-stage shutdown on the stdio transport by closing stdin, waiting with a configurable terminateDuration, then sending SIGTERM and SIGKILL as fallbacks, which directly addresses the hanging-process behavior described in issue #131.
Out of Scope Changes Check ✅ Passed All modifications in stdio.go focus on adding termination timing parameters and signal-based shutdown logic, which align with the linked issue’s scope and no unrelated or extraneous changes are present.
Description Check ✅ Passed The pull request description follows the repository template by including a Description section with issue references, Type of Change markers, a Checklist, MCP Spec Compliance, and Additional Information. Although the “Fixes” line could explicitly use the “Fixes #131, #498” syntax and include a link to the spec section, all required template sections are present and populated.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61b5d9e and 275fa9c.

📒 Files selected for processing (1)
  • client/transport/stdio.go (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
client/transport/stdio.go (2)
client/transport/interface.go (2)
  • JSONRPCResponse (67-72)
  • RequestHandler (38-38)
util/logger.go (2)
  • Logger (8-11)
  • DefaultLogger (16-20)

Comment on lines +247 to +265
if err, ok := wait(); ok {
return err
}

// Note the condition here: if sending SIGTERM fails, don't wait and just
// move on to SIGKILL.
if err := c.cmd.Process.Signal(syscall.SIGTERM); err == nil {
if err, ok := wait(); ok {
return err
}
}
// Sending SIGKILL if the server does not exit within a reasonable time after SIGTERM
if err := c.cmd.Process.Kill(); err != nil {
return err
}
if err, ok := wait(); ok {
return err
}
return fmt.Errorf("unresponsive subprocess")
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid returning an error when the child already exited.

wait() can fall through when cmd.Wait() finishes just as the timeout fires. In that case resChan already holds the exit result, but we still hit the fallback path. Because the process is already gone, Process.Signal/Process.Kill return os.ErrProcessDone, which bubbles up and causes Close() to report an error even though the subprocess shut down cleanly. Please drain resChan (or treat os.ErrProcessDone as success) before signalling, so we don’t misreport a failure when the child already exited.

🤖 Prompt for AI Agents
In client/transport/stdio.go around lines 247 to 265, Close() can return an
error when the child already exited because we signal/kill without first
draining the wait result; Process.Signal/Process.Kill then return
os.ErrProcessDone and bubbles up. Fix by first draining the wait result channel
(or calling wait() and treating os.ErrProcessDone as success) before attempting
to Signal/Kill; if wait() indicates the process already exited, return that
result (or nil if successful) and do not call Signal/Kill, otherwise proceed
with SIGTERM/SIGKILL as now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

close server in stdio is not work as expected
1 participant