Skip to content

Commit 5012672

Browse files
Make FallbackProcess cleanup more robust with try-except
- Keep process termination in __aexit__ but wrap in try-except - Use non-blocking wait with short timeout (0.5s) to prevent hangs - Catch ProcessLookupError and OSError for already-dead processes - Still clean up all streams regardless of termination success This approach is more conventional for a context manager while preventing failures when the process is already being handled by stdio_client's cleanup sequence or has already terminated. Reported-by: Felix Weinberger 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d6f1264 commit 5012672

File tree

2 files changed

+13
-6
lines changed

2 files changed

+13
-6
lines changed

src/mcp/client/stdio/win32.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,21 @@ async def __aexit__(
7373
exc_val: BaseException | None,
7474
exc_tb: object | None,
7575
) -> None:
76-
"""Clean up streams without terminating the process.
76+
"""Clean up process and streams.
7777
78-
The process termination is handled by stdio_client's cleanup sequence
79-
which implements the MCP spec-compliant shutdown flow.
78+
Attempts to terminate the process, but doesn't fail if termination
79+
is not possible (e.g., process already dead or being handled elsewhere).
8080
"""
81+
# Try to terminate the process, but don't fail if it's already being handled
82+
try:
83+
self.popen.terminate()
84+
# Use our non-blocking wait with a short timeout
85+
with anyio.move_on_after(0.5):
86+
await self.wait()
87+
except (ProcessLookupError, OSError):
88+
# Process already dead or being handled elsewhere
89+
pass
90+
8191
# Close the file handles to prevent ResourceWarning
8292
if self.stdin:
8393
await self.stdin.aclose()

tests/client/test_stdio.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,6 @@ async def test_stdio_client_universal_cleanup():
156156

157157
start_time = time.time()
158158

159-
# Use move_on_after which is more reliable for cleanup scenarios
160-
# Increased timeout to account for Windows process termination overhead
161159
with anyio.move_on_after(8.0) as cancel_scope:
162160
async with stdio_client(server_params) as (read_stream, write_stream):
163161
# Immediately exit - this triggers cleanup while process is still running
@@ -166,7 +164,6 @@ async def test_stdio_client_universal_cleanup():
166164
end_time = time.time()
167165
elapsed = end_time - start_time
168166

169-
# Key assertion: Should complete within reasonable time due to timeout mechanism
170167
# On Windows: 2s (stdin wait) + 2s (terminate wait) + overhead = ~5s expected
171168
assert elapsed < 6.0, (
172169
f"stdio_client cleanup took {elapsed:.1f} seconds, expected < 6.0 seconds. "

0 commit comments

Comments
 (0)