Skip to content

Run queued commands with stdin detached from the MCP transport - #38

Open
BitPhoenix wants to merge 1 commit into
block:mainfrom
pina-colada-crew:fix/detach-command-stdin-from-mcp-transport
Open

BitPhoenix wants to merge 1 commit into
block:mainfrom
pina-colada-crew:fix/detach-command-stdin-from-mcp-transport

Conversation

@BitPhoenix

Copy link
Copy Markdown

Problem

_execute_command starts each command with stdout and stderr piped, but doesn't set stdin. So the command inherits the server's stdin, which is the MCP stdio transport.

Node (vitest, tsc, npm) marks any stdin it touches as non-blocking. That flag belongs to the pipe itself, so the server's copy changes too. Once such a command exits, the server's next read of stdin fails and the server shuts down cleanly (exit code 0). The client then reports Connection closed on whichever call is open, usually task_status while it waits for a long test or build run. A command could also read the client's requests off the pipe.

We reproduced this without an MCP client in the loop. We drove the server over stdio and ran node -e "process.stdin; setTimeout(() => {}, 45000)", and the server exited as the command did, 46 s in. The same command with </dev/null left it running. In our Claude Code sessions it dropped the connection on nearly every vitest or tsc run.

Fix

Run commands with stdin=asyncio.subprocess.DEVNULL. tq is unchanged: it deliberately passes the terminal through.

Test

test_command_cannot_disturb_the_stdio_transport runs the server over a real stdio transport (PythonStdioTransport with a temporary --data-dir). The in-memory client that the other tests use has no stdin to share. The test's command calls os.set_blocking(0, False), which has the same effect as Node without needing Node. It then calls task_status.

  • Without the fix: fails with McpError: Connection closed.
  • With the fix: passes. The full suite passes too (91 passed), and ruff check . is clean.

Note: pyproject.toml allows any fastmcp>=2.14.4, and fastmcp 3+ no longer has fastmcp.tools.tool. So a fresh uv sync currently resolves a fastmcp where tests/test_queue.py fails to import. I ran the tests with fastmcp>=2.14.4,<3. That's independent of this change.

🤖 Generated with Claude Code

A queued command inherited the server's stdin, which is the MCP stdio
transport. Node (vitest, tsc, npm) marks any stdin it touches as
non-blocking, and that flag lives on the pipe the server shares. After such
a command exited, the server's next read failed, the server shut down
cleanly, and the client saw "Connection closed" on whichever call was in
flight, usually task_status. A command could also read the client's
requests off the pipe.

Commands now get /dev/null as stdin. The new test sets the flag from a
Python child, so it needs no Node, and drives the server over a real stdio
transport because the in-memory client has no stdin to share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant