fix: ensure Stdio transport threads are daemons and closed properly (… by noxymon · Pull Request #869 · modelcontextprotocol/java-sdk
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:
- Using
Schedulers.newSingle(name, true)to ensure all transport schedulers use daemon threads. - Explicitly closing the process
InputStream,ErrorStream, andOutputStreamduring shutdown to unblock any threads waiting onreadLine()orwrite().
How Has This Been Tested?
Tested on Linux (Ubuntu) using Java 17:
- Created a reproduction test that monitors active threads in the JVM after
closeGracefully(). - Verified that without the fix, threads named
pool-x-thread-ywould linger as non-daemons. - Verified that with the fix, threads are correctly marked as daemons and do not block exit.
- 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.