diff --git a/docs/run/index.md b/docs/run/index.md index b3cea554cc..6134ccc7f8 100644 --- a/docs/run/index.md +++ b/docs/run/index.md @@ -41,6 +41,30 @@ 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 +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)**. + ### Try it ```console diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 621b32c6cd..1279448359 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -141,6 +141,41 @@ 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 +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 +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.