Skip to content

fix(stdio): cancel supervisor job on close to prevent process hang after EOF#735

Open
blackwell-systems wants to merge 2 commits intomodelcontextprotocol:mainfrom
blackwell-systems:fix-stdio-transport-close
Open

fix(stdio): cancel supervisor job on close to prevent process hang after EOF#735
blackwell-systems wants to merge 2 commits intomodelcontextprotocol:mainfrom
blackwell-systems:fix-stdio-transport-close

Conversation

@blackwell-systems
Copy link
Copy Markdown

Motivation and Context

Fixes #708

When a client closes stdin (e.g., Docker container shutdown, opencode disconnect), StdioServerTransport.close() cleans up all child jobs (reading, processing, sending), flushes and closes I/O streams, and invokes the onClose callback chain. However, the SupervisorJob backing the coroutine scope was never cancelled, keeping the scope alive indefinitely. The server process hangs even though all meaningful work has completed.

Fix

Extract the SupervisorJob into a named field and cancel it at the end of close(), after all cleanup is done. This allows the coroutine scope to complete and the process to exit.

How Has This Been Tested?

  • Existing StdioServerTransportTest passes (all tests green)
  • The fix is a one-line addition (supervisorJob.cancel()) at the end of the already-tested close() method
  • The workaround from the issue (wrapping stdin with EOF detection + exitProcess(0)) would no longer be necessary

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling

…ayloadResult

The content-based polymorphic deserializer matches GetTaskResult on the
presence of "taskId" alone. This incorrectly captures GetTaskPayloadResult
responses (tasks/result), which wrap arbitrary JSON and may also contain
a "taskId" key but not a "status" key.

Tighten the discriminator to require both "taskId" and "status", since
GetTaskResult always has both fields. GetTaskPayloadResult responses
(which lack "status") now correctly fall through to other deserializers
or the empty result handler.

Fixes modelcontextprotocol#601

Signed-off-by: Dayna Blackwell <dayna@blackwell-systems.com>
…ter EOF

When stdin reaches EOF, StdioServerTransport.close() cleans up all
child jobs and invokes the onClose callback. However, the supervisor
job backing the coroutine scope was never cancelled, keeping the scope
alive indefinitely. This caused the server process to hang after the
client disconnected.

Cancel the supervisor job at the end of close() so the coroutine scope
completes and the process can exit.

Fixes modelcontextprotocol#708

Signed-off-by: Dayna Blackwell <dayna@blackwell-systems.com>
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.

StdioServerTransport: onClose callback not called when stdin receives EOF

1 participant