From 593a91e2f81a337095b6b98a74ceea0511d5c535 Mon Sep 17 00:00:00 2001 From: andrejoe630 Date: Wed, 8 Jul 2026 15:17:45 -0700 Subject: [PATCH 1/2] docs: document Windows stdio subprocess stdin --- docs/run/index.md | 17 +++++++++++++++++ docs/troubleshooting.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/docs/run/index.md b/docs/run/index.md index b3cea554cc..5637610e86 100644 --- a/docs/run/index.md +++ b/docs/run/index.md @@ -41,6 +41,23 @@ Nothing prints, and it doesn't return. It is waiting on stdin for a host to spea That also means stdout **is the wire**. A stray `print()` corrupts the stream; the `logging` module writes to stderr and is the right tool. That story is in **[Logging](../handlers/logging.md)**. +On Windows, the same rule applies to child processes your tools start. A child +that inherits the stdio server's stdin can block behind the server's protocol +reader. If your tool starts a subprocess and you do not intend to feed it input, +redirect the child's stdin: + +```python +process = await asyncio.create_subprocess_exec( + sys.executable, + "script.py", + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, +) +``` + +The matching troubleshooting entry is **[My stdio tool hangs when it starts a subprocess on Windows](../troubleshooting.md#my-stdio-tool-hangs-when-it-starts-a-subprocess-on-windows)**. + ### Try it ```console diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 621b32c6cd..d3be20937f 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -141,6 +141,34 @@ There is no error string for this, which is exactly why it is hard to search. Th An "invalid" tool name is *not* on that list: a non-conforming name logs a warning but the tool is registered and listed anyway. +## My stdio tool hangs when it starts a subprocess on Windows + +Your server is running over `stdio`, and a tool starts another process with +`asyncio.create_subprocess_exec`, `asyncio.create_subprocess_shell`, or +`subprocess.Popen`. The tool call never returns on Windows, while the same code +works over an HTTP transport. + +The child inherited the server's stdin. In a stdio server, stdin is the protocol +pipe and the server is already waiting on it for the next JSON-RPC message. A +Python child process on Windows can block during startup when it inherits that +same pipe. + +If you do not intend to send input to the child, redirect its stdin: + +```python +process = await asyncio.create_subprocess_exec( + sys.executable, + "script.py", + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, +) +``` + +Use the same idea with `subprocess.Popen(..., stdin=subprocess.DEVNULL)`. Also +capture or redirect the child's stdout. The stdio server's stdout is the MCP +wire, so a child that writes there can corrupt the connection. + ## `MCPError: Server returned an error response` The server refused the HTTP request outright, with a body that is not JSON-RPC, so the python `Client` has nothing better to show you than this stand-in. From 631f7d74764e7b0616920b8f54ab769a8ac568ae Mon Sep 17 00:00:00 2001 From: andrejoe630 Date: Wed, 8 Jul 2026 15:23:15 -0700 Subject: [PATCH 2/2] docs: keep subprocess snippets lintable --- docs/run/index.md | 21 ++++++++++++++------- docs/troubleshooting.md | 21 ++++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/docs/run/index.md b/docs/run/index.md index 5637610e86..6134ccc7f8 100644 --- a/docs/run/index.md +++ b/docs/run/index.md @@ -47,13 +47,20 @@ reader. If your tool starts a subprocess and you do not intend to feed it input, redirect the child's stdin: ```python -process = await asyncio.create_subprocess_exec( - sys.executable, - "script.py", - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, -) +import asyncio +import subprocess +import sys + + +async def run_script() -> tuple[bytes, bytes]: + process = await asyncio.create_subprocess_exec( + sys.executable, + "script.py", + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return await process.communicate() ``` The matching troubleshooting entry is **[My stdio tool hangs when it starts a subprocess on Windows](../troubleshooting.md#my-stdio-tool-hangs-when-it-starts-a-subprocess-on-windows)**. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d3be20937f..1279448359 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -156,13 +156,20 @@ same pipe. If you do not intend to send input to the child, redirect its stdin: ```python -process = await asyncio.create_subprocess_exec( - sys.executable, - "script.py", - stdin=subprocess.DEVNULL, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, -) +import asyncio +import subprocess +import sys + + +async def run_script() -> tuple[bytes, bytes]: + process = await asyncio.create_subprocess_exec( + sys.executable, + "script.py", + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return await process.communicate() ``` Use the same idea with `subprocess.Popen(..., stdin=subprocess.DEVNULL)`. Also