Skip to content

claude_code.py MCP stdio adapter crashes on native Windows — connect_read_pipe() unimplemented for stdin #38

Description

@FPUHAL

Body:

Environment

OS: Windows 11 (native, not WSL)
Python: 3.12.10
Atlas: 0.1.0a1, installed via pip install -e ".[dev]"

Bug

_stdio_loop() in atlas_core/adapters/claude_code.py calls:

python
await loop.connect_read_pipe(lambda: transport_protocol, sys.stdin)

This raises NotImplementedError on native Windows Python, under both ProactorEventLoop and SelectorEventLoop. connect_read_pipe() for stdin has never been implemented on Windows — this is a long-standing CPython limitation (see bpo-26832, migrated to python/cpython#71019 and python/cpython#87694).

Repro

powershell
echo {"jsonrpc":"2.0","id":1,"method":"initialize","params":{}} | python -m atlas_core.adapters.claude_code
File "...\atlas_core\adapters\claude_code.py", line 137, in _stdio_loop
await loop.connect_read_pipe(lambda: transport_protocol, sys.stdin)
...
NotImplementedError

Practical effect: claude mcp add for this server succeeds, but Claude Code reports Failed to connect — MCP server "atlas" connection timed out after 30000ms, since the subprocess crashes on startup before completing the MCP handshake.

Fix

Replace the connect_read_pipe()-based reader with a background thread feeding an asyncio.Queue, which works identically on Windows, macOS, and Linux:

python
async def _stdio_loop() -> None:
"""Read JSON-RPC requests from stdin, dispatch, write responses to stdout.

Windows note: uses a background thread + asyncio.Queue instead of
loop.connect_read_pipe(), since that call is unimplemented on Windows
for stdin under both ProactorEventLoop and SelectorEventLoop.
Spec: 05 - Atlas Architecture & Schema § 2 (API Layer).
"""
server, driver = await _build_server()
loop = asyncio.get_running_loop()
queue: asyncio.Queue = asyncio.Queue()

def _read_stdin() -> None:
    try:
        for raw_line in sys.stdin.buffer:
            loop.call_soon_threadsafe(queue.put_nowait, raw_line)
    finally:
        loop.call_soon_threadsafe(queue.put_nowait, None)

reader_thread = threading.Thread(target=_read_stdin, daemon=True)
reader_thread.start()

try:
    while True:
        line = await queue.get()
        if line is None:
            break
        line_str = line.decode("utf-8").strip()
        if not line_str:
            continue
        try:
            req = json.loads(line_str)
        except json.JSONDecodeError as exc:
            sys.stdout.write(json.dumps(_err(None, -32700, str(exc))) + "\n")
            sys.stdout.flush()
            continue
        try:
            response = await _handle(server, req)
        except Exception as exc:
            log.exception("dispatch failed")
            response = _err(req.get("id"), -32603, f"{type(exc).__name__}: {exc}")
        if response is not None:
            sys.stdout.write(json.dumps(response) + "\n")
            sys.stdout.flush()
finally:
    await driver.close()

(needs import threading added at the top). Confirmed working on Windows after this change — claude mcp list shows atlas as connected, and manual initialize requests return a correct response. No behavior change on Unix, since the thread-based reader works there too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions