Stdio Transport: client.call_tool() hangs forever when the subprocess dies unexpectedly
Description
When a FastMCP server started via PythonStdioTransport (or any other stdio-based transport) crashes after the MCP handshake, the client keeps awaiting the response indefinitely. No exception is propagated, which makes upstream HTTP/CLI requests appear āstuckā.
Example Code
# server.py
import os, signal
from fastmcp.server import FastMCP
mcp = FastMCP()
@mcp.tool()
def bye():
# Simulate a fatal crash *after* the call_tool request was received
os.kill(os.getpid(), signal.SIGKILL)
if __name__ == "__main__":
mcp.run()
# client.py
import asyncio
from fastmcp import Client
from fastmcp.client.transports import PythonStdioTransport
async def main():
transport = PythonStdioTransport(
script_path="server.py",
python_cmd="python", # Adjust if needed
args=["--transport", "stdio"] # FastMCP server uses stdio transport
)
async with Client(transport) as client:
print("Calling tool⦠this will hang ā³")
await client.call_tool("bye", {}) # ā never returns
if __name__ == "__main__":
asyncio.run(main())
Version Information
v2.3.4
Additional Context
Thanks for the great project! This edge-case fix would help services that embed FastMCP inside long-running HTTP endpoints survive user script crashes gracefully.
Thanks for raising!
Hi @Cherwayway! I ran the example you provided to reproduce the issue:
# server.py
import os, signal
from fastmcp.server import FastMCP
mcp = FastMCP()
@mcp.tool()
def bye():
# Simulate a fatal crash *after* the call_tool request was received
os.kill(os.getpid(), signal.SIGKILL)
if __name__ == "__main__":
mcp.run()
#client.py
import asyncio
from fastmcp import Client
from fastmcp.client.transports import PythonStdioTransport
async def main():
transport = PythonStdioTransport(
script_path="server.py",
python_cmd="python",
args=["--transport", "stdio"]
)
async with Client(transport) as client:
print("Calling tool⦠this will hang ā³")
await client.call_tool("bye", {}, timeout=5) # NOTE THE TIMEOUT
if __name__ == "__main__":
asyncio.run(main())
With a timeout set, the client no longer hangsāit raises an error. The only side effect is a ProcessLookupError during cleanup, since the server process is already gone.
Maybe FastMCP should catch this error internally to ensure a clean shutdown?
Yes, setting a timeout can solve this problem, but this always requires waiting until the timeout, rather than throwing an exception when an error occurs, which will cause confusion and waste of resources. What I am doing is allowing users to customize the deployment of any MCP protocol, so it is difficult to add a fixed timeout for them.
This appears to be a common issue in the low-level protocol (we are re-exposing the client connector from the low-level SDK)