Repository navigation
Windows stdio MCP handshake hangs — stdout only flushes on stdin EOF (v0.8.1) #513
Description
Version
codebase-memory-mcp 0.8.1
Platform
Windows (x64)
Install channel
GitHub release archive / install.sh / install.ps1
Binary variant
standard
What happened, and what did you expect?
Windows stdio MCP handshake hangs — stdout only flushes on stdin EOF (v0.8.1)
Version
codebase-memory-mcp 0.8.1 (latest release at time of writing)
Binary SHA-256 (matches official v0.8.1 release checksums.txt):
12375E6A39A31F003D776E54EF487640F9F59DB7DBEEE96973A9EE9EB18BC7BB
Platform
Windows x64
OS: Microsoft Windows 10.0.19045 (also reproduced on Windows 11)
Arch: X64
Install Channel
GitHub release archive / install.ps1
Binary path: C:\Users\<user>\AppData\Local\Programs\codebase-memory-mcp\codebase-memory-mcp.exe
Binary Variant
standard (stdio server)
codebase-memory-mcp Run MCP server on stdio
What Happened
On Windows, the stdio MCP server does not flush its JSON-RPC responses to stdout until stdin is closed (EOF). This makes the initialize handshake hang indefinitely for any MCP client that keeps stdin open — which is every conformant MCP stdio client (AstrBot, Codex CLI, OpenCode, Claude Code, etc.).
The client sends initialize, keeps stdin open waiting for the response, and the server's response sits in the stdout buffer unflushed. After the client-side timeout (typically 30s), the handshake fails.
I expected the server to write the initialize response to stdout and flush immediately on every request, per the MCP stdio transport contract.
AstrBot log (the MCP client I discovered this with)
[2026-06-19 10:39:40.220] [INFO] [MCPServer-codebase-memory] level=info msg=mem.init budget_mb=8127 total_ram_mb=16255
# ... no "Connected to MCP server codebase-memory" ever appears.
# After 30s the client gives up and the stdio child process is torn down.
For comparison, the two npx-based MCP servers in the same config (brave-search, exa-search) connect fine in the same session:
[2026-06-19 10:39:44.045] [INFO] Connected to MCP server brave-search, Tools: ['brave_web_search', 'brave_local_search']
[2026-06-19 10:39:49.114] [INFO] Connected to MCP server exa-search, Tools: ['web_search_exa', 'web_fetch_exa']
Diagnosis
This looks like the classic C-stdio full-buffering behaviour on Windows: when stdout is a pipe (not a TTY), the C runtime fully buffers stdout and only flushes on buffer-full or process exit. The MCP server writes its JSON-RPC responses to stdout but does not force a fflush(stdout) after each response, so responses stay buffered while the process keeps running (stdin open).
This is the same class of bug as the one PR #101 targets (raw-fd polling vs buffered stdio reads causing OpenCode handshake stalls), but on the output side: the response write is buffered and never flushed mid-stream. PR #101 is currently unmerged and appears to address the read side, not this output-flush side.
A minimal fix would be to fflush(stdout) (or fflush(NULL)) after every JSON-RPC response written to stdout in the MCP stdio server loop, or to set stdout to line-unbuffered (setvbuf(stdout, NULL, _IONBF, 0) / _IOLBF) when running as a stdio MCP server.
Related
- Fix OpenCode MCP handshake timeout #101 (closed PR, unmerged) — "Fix OpenCode MCP handshake timeout" — read-side stdio stall
- constant timeouts #474 — "constant timeouts" on Windows x64, v0.8.1
- task: Windows platform issues (8 bugs) #394 — "task: Windows platform issues"
Environment
- codebase-memory-mcp 0.8.1, windows-amd64, standard variant
- Windows 10.0.19045 / Windows 11
- MCP clients reproducing: AstrBot v4.25.5 (mcp Python lib 1.27.x), Codex CLI, direct
mcpPython client - The UI variant binary at
Z:\CODE MEMORY\codebase-memory-mcp-ui-windows-amd64\codebase-memory-mcp.exe(different hash,B67A67CF...) exhibits the same stdout-no-flush behaviour.
Confirmations
- I searched existing issues and this is not a duplicate (closest: Fix OpenCode MCP handshake timeout #101, constant timeouts #474 — this pins the output-flush root cause with a direct reproduction).
- My reproduction uses shareable code (the PowerShell/.NET and Python snippets above run against the public v0.8.1 release binary; no proprietary code involved).
Reproduction
Reproduction
Repro 1 — stdin kept open (what every MCP client does) → HANGS
PowerShell, using .NET Process with redirected stdio. Write initialize to stdin, flush, keep stdin open, wait for stdout:
$bin = "$env:LOCALAPPDATA\Programs\codebase-memory-mcp\codebase-memory-mcp.exe"
$psi = [System.Diagnostics.ProcessStartInfo]::new($bin)
$psi.UseShellExecute = $false
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.CreateNoWindow = $true
$p = [System.Diagnostics.Process]::new(); $p.StartInfo = $psi
$out = [System.Text.StringBuilder]::new()
$p.add_OutputDataReceived({ if ($EventArgs.Data) { [void]$out.AppendLine($EventArgs.Data) } })
$p.Start() | Out-Null
$p.BeginOutputReadLine()
$init = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' + "`n"
$p.StandardInput.Write($init)
$p.StandardInput.Flush()
# stdin stays OPEN. Wait 12s.
Start-Sleep -Seconds 12
$out.ToString() # -> empty. No response ever appears on stdout.Result: stdout is empty after 12s. The initialize response is never flushed while stdin is open. Only mem.init shows up on stderr.
Repro 2 — stdin closed immediately after writing (NOT what MCP clients do) → WORKS
$bin = "$env:LOCALAPPDATA\Programs\codebase-memory-mcp\codebase-memory-mcp.exe"
$init = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
$notif = '{"jsonrpc":"2.0","method":"notifications/initialized"}'
$tools = '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
"$init`n$notif`n$tools`n" | & $bin 2>$nullResult: both initialize (id:1) and tools/list (id:2) responses appear immediately, because piping through PowerShell closes stdin (EOF) after writing, which triggers the stdout flush on process exit.
This contrast is the smoking gun: the server only emits responses when stdin reaches EOF, not when a request arrives.
Repro 3 — via the official Python mcp client library (same path AstrBot/Codex use) → TIMEOUT
import asyncio
from mcp.client.stdio import stdio_client, StdioServerParameters
from mcp import ClientSession
from datetime import timedelta
async def main():
params = StdioServerParameters(
command=r"C:\Users\<user>\AppData\Local\Programs\codebase-memory-mcp\codebase-memory-mcp.exe",
args=[],
env=None,
)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=30)) as session:
r = await asyncio.wait_for(session.initialize(), timeout=30) # -> TimeoutError
asyncio.run(main())Result: TimeoutError after 30s. (Stack trace below.)
Logs
## Logs
### Server stderr on startup (always appears, on stderr — not stdout)
level=info msg=mem.init budget_mb=8127 total_ram_mb=16255
### Python `mcp` client stack trace on the timeout
TimeoutError
During handling of the above exception, another exception occurred:
anyio.WouldBlock
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File ".../asyncio/tasks.py", line 520, in wait_for
return await fut
File ".../mcp/client/session.py", line 171, in initialize
result = await self.send_request(...)
File ".../mcp/shared/session.py", line 292, in send_request
response_or_error = await response_stream_reader.receive()
File ".../anyio/streams/memory.py", line 125, in receive
await receive_event.wait()
...
asyncio.exceptions.CancelledError
Project scale (if relevant)
No response
Confirmations
- I searched existing issues and this is not a duplicate.
- My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.
Activity
DeusData commentedon Jun 21, 2026
Hey, thx for reporting. Will check ASAP
test(repro): memory-budget #363 + bounded RSS-soak #581
test(smoke): binary-doesn't-fail invariants across all GitHub platforms
test(windows): add red regressions for hook augmenter and UI drive pi…
DeusData commentedon Jun 28, 2026
This issue now has a first-class, durable reproduction in the cumulative bug-reproduction suite landing in #667 — a test that pins the exact failing behaviour, so it's tracked and guarded against silent regression. Where #667 also lands the root-cause fix, that's called out in the PR; otherwise the reproduction makes the eventual fix straightforward and verifiable.
The reproduction goes live once #667 merges. (Posted from the release branch's QA pass.)
test(windows): add red regressions for hook augmenter and UI drive pi…
github-actions commentedon Jul 8, 2026
Thanks for the report! To move this forward we need a bit more so we can reproduce it ourselves:
codebase-memory-mcp --versionyou're onOnce that's here we'll pick it straight back up. Heads-up: issues left
awaiting-reporterare automatically closed after a few weeks of silence, but a comment reopens the door anytime.DeusData commentedon Jul 8, 2026
v0.9.0 includes Windows runtime and stdio reliability fixes, so this stdout/initialize hang needs a current check. Could you retest on v0.9.0 and confirm whether stdout is flushed during normal MCP startup? Marking awaiting-reporter.
x1051445024 commentedon Jul 8, 2026
Retested on v0.9.0 as requested. Here are the results:
Test environment: Windows 10, Python 3.12, codebase-memory-mcp v0.9.0
✅ Binary-level stdio works correctly
When piping input via shell (PowerShell / cmd.exe
echo ... | binary), the binary correctly flushes stdout and responds to MCP JSON-RPC requests:Full MCP handshake (initialize + initialized notification + tools/list) completes successfully. No hangs.
❌ Python asyncio subprocess still hangs
Both
asyncio.ProactorEventLoopandSelectorEventLoopon Windows still cannot read the binary stdout:asyncio.create_subprocess_exec()+proc.stdout.readline()hangs indefinitelyproc.stdout.read(1)also hangsbufsize=0Regular
subprocess.Popen(blocking, non-asyncio) works for a single call but can also hang on subsequent reads.Verdict
The v0.9.0 binary itself is fixed — stdout IS flushed during normal MCP startup. The remaining hang is a Python asyncio-on-Windows pipe I/O limitation. For Hermes (which uses Python asyncio), the Node.js stdio wrapper is still needed, but it works correctly with v0.9.0.
Suggest closing this issue as the binary-level fix is confirmed, and the asyncio issue is a separate Python/Windows limitation beyond this projects scope.
DeusData commentedon Jul 9, 2026
Thank you for the careful split verification — binary-level stdio confirmed fixed on v0.9.0 (handshake + tools/list flush correctly), with the residual hang isolated to Python asyncio's Windows pipe I/O rather than the server. Agreed with your verdict: closing on the binary-level fix. For asyncio-based clients on Windows (Hermes et al.), the Node.js stdio wrapper remains the recommended transport — we'll keep that documented. Thanks again for the depth here, it made this diagnosable.