posit-dev / posit-dev/mcptools
stdio transport deadlocks on Windows: responses block in nanonext::write_stdout and child-process-spawning tools hang
Nobody has claimed this yet.
- Dominant language
- R
- Stars
- 196
- Forks
- 21
- Avg merge
- 1h 14m
- Merged PRs (30d)
- 1
Description
Title
stdio transport deadlocks on Windows: responses block in nanonext::write_stdout, and tool execution producing child processes hangs to the point where the server is unusable
Environment
- Windows 11 (10.0.2xxxx), x64
- R 4.6.1
- mcptools 1.0.1 (CRAN), btw 1.4.0, ellmer 0.4.2, S7 0.2.2, nanonext 1.10.2
- MCP client: MetaMCP (stdio spawn), but any stdio client should be affected
Summary
Running a server over the default stdio transport (type = "stdio") on Windows, every tools/call hangs: the client never receives a response, so it times out (typically -32001: Request timed out). I traced two distinct bugs, both in the nanonext-based stdio path. Switching to the HTTP transport (type = "http" using httpuv) makes the identical server work perfectly, which isolates the problem to the stdio transport.
Bug 1 — response writing blocks on the pipe
cat_json() sends responses via nanonext::write_stdout(). Short/empty payloads are fine, but a real response (a multi-line, ~6.7 kB help page) blocks indefinitely, so the client's timeout fires with no reply.
Supported by per-step instrumentation of handle_request():
append_tool_fn -> ok (list)
execute_tool_call -> ok (list) # tool ran, result correct
to_json(result) -> ok (~6.7 kB) # serialization is fine
logcat -> ok (file write)
cat_json(result) -> HANGS # nanonext::write_stdout on the pipe
cat_json(list(...)) with a small payload returns fine, and nanonext::write_stdout("plain") alone also returns fine — so the block is specific to writing the large/real string to an OS pipe. Overriding cat_json in the namespace to cat(to_json(x), "\n", sep = "") makes initialize and tools/call responses flow immediately — confirming the nanonext write path is the blocker, not the JSON or the tools.
Bug 2 — child-process spawning deadlocks once read_stdin() owns the stdio pipes
After the fix for Bug 1, initialize works, but tools/call then hangs inside execute_tool_call():
ETC start; tool class: ellmer::ToolDef
about to do.call
# never returns
The same do.call(tool, args) on the same registered ellmer::ToolDef returns in ~2.7 s in an isolated R process; it only hangs when called from inside the mcp_server_stdio() event loop after nanonext::read_stdin() has taken over the stdio pipes. The tool in question (btw_tool_docs_help_page) renders help via pandoc in a child process — once nanonext owns the stdio pipes, that child-process spawn deadlocks. (logcat() itself is fine: it writes to a log file, not stdout.)
Note discover_session_slot() returns NULL on Windows, so this hang is fully in-process, not in the session-forwarding path.
Working reproduction sketch
# Server
Rscript -e "options(repos=c(CRAN='https://cloud.r-project.org')); btw::btw_mcp_server()"
# Client: spawn the process, send line-delimited JSON-RPC over stdin:
# initialize -> responds fine
# tools/call btw_tool_docs_help_page {package: 'terra', topic: 'extract'} -> never responds
Fixing Bug 1 alone (cat_json override) reproduces Bug 2; fixing nothing, Bug 1 ships as the client-observed hang. With the HTTP transport (mcp_server(type = "http")), both initialize and the same tools/call return correct 200 responses with the full payload — no patches needed. That points at the nanonext stdio integration (read/write on OS pipes) as the component needing rework, consistent in spirit with #120's request for non-blocking stdio handling.
Workaround in use
Serve over HTTP and point the MCP client at http://127.0.0.1:<port> (Streamable HTTP, validated working: initialize + tools/call, also at / and /mcp). For a session_tools-free in-process server I call mcptools::mcp_server(tools = btw:::btw_mcp_tools() |> btw:::flatten_and_check_tools(), type = "http", session_tools = FALSE).
Happy to provide more detail, and thanks for a great package — the HTTP transport is rock solid.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with mcp_server_stdio(), read_stdin(), cat_json(), and the nanonext read/write calls described in the report. Reproduce the Windows stdio server with the supplied Rscript and line-delimited JSON-RPC sketch, then compare it with the working HTTP transport. Done means initialize and tools/call return complete responses without hangs, including tools that spawn child processes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- r
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100