Skip to content

Windows stdio server: subprocess spawned inside a tool handler hangs forever when it inherits the server stdin (stdin=DEVNULL fixes) #3788

Open

Description

@dadawalissonsilva

What happens

A subprocess spawned from INSIDE a FastMCP tool handler (stdio server, Windows) hangs forever when the child inherits the server stdin — which is the default in subprocess.run/create_subprocess_exec if stdin is not set.

Repro (Windows, minimal)

FastMCP server (stdio) with a tool that spawns a trivial child (e.g. python -c "print(1)"):

  • subprocess.run([...]) with inherited stdin → child never completes; tool call hangs (abort only via MCP idle-timeout — 30 min by default in Claude Code)
  • same call with stdin=subprocess.DEVNULL → completes in ~0.6 s

A/B tested with FastMCP pure (zero user imports) to rule out user code; also reproduced via asyncio.create_subprocess_exec on the event loop. The moment the child stops sharing the server jsonrpc stdin, everything works.

Suggested guard

Document/force stdin=DEVNULL for subprocesses spawned in handlers, or detach the child stdin by default in any subprocess helper the SDK provides — inheriting the protocol pipe is never what a tool wants.

Environment: Windows 11 (26200), Python 3.14, mcp/fastmcp stdio transport, Claude Code 2.1.310 host.

Activity

  1. added
    bugSomething isn't working
    v1Affects the v1.x maintenance line
    on Oct 30, 2026
  2. Ben-Home commented on Oct 30, 2026

    @Ben-Home

    The suggested guard is right, and the reason it hangs is worth stating explicitly, because it changes what the SDK should promise.

    In a stdio server, process stdin is the JSON-RPC transport. A child spawned with inherited stdin shares that same handle, so there are now two readers on one pipe. What the child's runtime does with it varies (the Windows launcher, node, or a shell may read ahead to buffer), and any read it performs consumes bytes the server transport was waiting for. The tool call then waits for a response the transport can no longer assemble, and the client waits behind it. DEVNULL works because it removes the second reader, not because the child was waiting for input.

    Two things worth adding to the issue, both cheap to confirm:

    1. This is not Windows-specific in principle, only in visibility. On POSIX the child inherits the same descriptor, and if it reads, frames go missing the same way. Windows is where it surfaces first because process creation defaults differ and the hang is total rather than partial.
    2. The inverse case has the same root cause and a different symptom: a child that holds the inherited stdin open without reading it keeps the transport from ever seeing EOF. That blocks clean shutdown on client disconnect, and it usually shows up as a leaked process rather than a hang.

    On where the fix belongs, forcing DEVNULL inside the SDK is the safer default but it is a behaviour change, and anyone whose child legitimately reads the parent's stdin as data would be surprised. A middle path costs little:

    • a documented subprocess helper in the SDK that sets stdin=DEVNULL by default, with the parameter named so opting out is explicit;
    • a debug-level warning when a handler spawns a child without setting stdin at all, since that is the case that bites.

    The second one is what turns a 30 minute hang into a log line, and it needs no behavioural change to ship.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv1Affects the v1.x maintenance line

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions