Skip to content

fix: ensure Stdio transport threads are daemons and closed properly (…#869

Open
noxymon wants to merge 1 commit intomodelcontextprotocol:mainfrom
noxymon:fix/gh-759-stdio-thread-leak
Open

fix: ensure Stdio transport threads are daemons and closed properly (…#869
noxymon wants to merge 1 commit intomodelcontextprotocol:mainfrom
noxymon:fix/gh-759-stdio-thread-leak

Conversation

@noxymon
Copy link

@noxymon noxymon commented Mar 14, 2026

Summary

This PR ensures that all threads created by StdioClientTransport and StdioServerTransportProvider are daemon threads and are properly unblocked during shutdown. It also improves the graceful closure logic for the server process.

Motivation and Context

Fixes #759.

In the previous implementation, StdioClientTransport used Executors.newSingleThreadExecutor() with default thread factories, resulting in non-daemon threads. These threads would linger indefinitely after the transport was closed—especially Windows where they might be blocked on native I/O—preventing the JVM from exiting naturally.

The changes solve this by:

  1. Using Schedulers.newSingle(name, true) to ensure all transport schedulers use daemon threads.
  2. Explicitly closing the process InputStream, ErrorStream, and OutputStream during shutdown to unblock any threads waiting on readLine() or write().

How Has This Been Tested?

Tested on Linux (Ubuntu) using Java 17:

  1. Created a reproduction test that monitors active threads in the JVM after closeGracefully().
  2. Verified that without the fix, threads named pool-x-thread-y would linger as non-daemons.
  3. Verified that with the fix, threads are correctly marked as daemons and do not block exit.
  4. Ran the full Stdio test suite (StdioMcpAsyncClientTests, StdioMcpAsyncServerTests, etc.) to ensure protocol integrity is maintained. (162 tests passed).

Breaking Changes

None. This is a behavioral fix for internal resource management.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

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
  • I have added or updated documentation as needed

Additional context

The use of Schedulers.newSingle(name, true) is more idiomatic for Reactor-based applications than manually wrapping executors via Schedulers.fromExecutorService, as it handles both naming and daemon status natively while ensuring proper cleanup when dispose() is called.

…odelcontextprotocol#759)

- Use Schedulers.newSingle with daemon=true for StdioClientTransport and StdioServerTransportProvider.
- Explicitly close process streams in StdioClientTransport#closeGracefully to unblock reading threads.
- Removes unused Executors imports.

This prevents Stdio transport threads from lingering and blocking JVM exit.

Closes modelcontextprotocol#759
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.

StdioClientTransport: Inbound Scheduler and Error Scheduler threads are locked on Windows

1 participant