Skip to content

Windows stdio MCP handshake hangs — stdout only flushes on stdin EOF (v0.8.1) #513

Closed

Description

@x1051445024

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

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 mcp Python 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>$null

Result: 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

  1. DeusData commented on Jun 21, 2026

    @DeusData
    Owner

    Hey, thx for reporting. Will check ASAP

  2. DeusData commented on Jun 28, 2026

    @DeusData
    Owner

    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.)

  3. added
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    on Jun 30, 2026
  4. added
    awaiting-reporterWaiting on the reporter for info/repro; stale bot will warn then close
    on Jul 8, 2026
  5. github-actions commented on Jul 8, 2026

    @github-actions

    Thanks for the report! To move this forward we need a bit more so we can reproduce it ourselves:

    • the codebase-memory-mcp --version you're on
    • the exact steps or command you ran
    • a public repo (or a small dummy snippet) that shows the problem — please don't paste proprietary code

    Once that's here we'll pick it straight back up. Heads-up: issues left awaiting-reporter are automatically closed after a few weeks of silence, but a comment reopens the door anytime.

  6. DeusData commented on Jul 8, 2026

    @DeusData
    Owner

    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.

  7. added this to the 0.9.1-rc milestone on Jul 8, 2026
  8. x1051445024 commented on Jul 8, 2026

    @x1051445024
    Author

    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:

    → initialize → {"jsonrpc":"2.0","id":1,"result":{...}}
    → tools/list → returns all 8 tools correctly
    

    Full MCP handshake (initialize + initialized notification + tools/list) completes successfully. No hangs.

    ❌ Python asyncio subprocess still hangs

    Both asyncio.ProactorEventLoop and SelectorEventLoop on Windows still cannot read the binary stdout:

    • asyncio.create_subprocess_exec() + proc.stdout.readline() hangs indefinitely
    • proc.stdout.read(1) also hangs
    • This happens regardless of bufsize=0

    Regular 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.

  9. removed
    awaiting-reporterWaiting on the reporter for info/repro; stale bot will warn then close
    on Jul 9, 2026
  10. DeusData commented on Jul 9, 2026

    @DeusData
    Owner

    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.

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 workingpriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions