Skip to content

Windows: MCP servers with heavy stderr output fail with "Transport closed" error #7155

Open

Description

@FBakkensen

What version of Codex is running?

codex-cli 0.63.0

What subscription do you have?

Pro

Which model were you using?

gpt-5.1-codex-max

What platform is your computer?

Microsoft Windows NT 10.0.26200.0 x64

What issue are you seeing?

MCP servers that produce heavy stderr output during tool execution fail with "Transport closed" errors on Windows. This affects servers like bc-code-intelligence-mcp but not servers with minimal stderr like @modelcontextprotocol/server-filesystem.

Error Message:

[WARN] MCP tool call error: "tool call error: tool call failed for `bc-code-intel/set_workspace_info`
    Caused by: tools/call failed: Transport closed"

Key Evidence:

  • bc-code-intel works perfectly when tested manually via stdio
  • bc-code-intel works in Claude Code, GitHub Copilot, and Cursor
  • filesystem MCP works perfectly in Codex (minimal stderr)
  • bc-code-intel ONLY fails in Codex on Windows with heavy stderr (189-237ms of output)

What steps can reproduce the bug?

  1. Configure bc-code-intelligence-mcp in config.toml:
[mcp_servers.bc-code-intel]
command = "node"
args = ["C:\\path\\to\\bc-code-intelligence-mcp\\dist\\index.js"]
startup_timeout_sec = 60
  1. Start Codex and call set_workspace_info tool
  2. Server processes successfully (logs show completion: "📊 Total: 123 topics, 16 specialists")
  3. Error occurs ~7ms after server completes: "Transport closed"

Manual Test (WORKS):

echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"set_workspace_info","arguments":{"workspace_root":"C:/test","available_mcps":["bc-code-intel"]}}}' | node dist/index.js
# Returns valid JSON-RPC response successfully

What is the expected behavior?

MCP servers should work regardless of stderr output volume. The server should complete tool execution and return results without "Transport closed" errors.

Root Cause Analysis

Location: codex-rs/rmcp-client/src/rmcp_client.rs lines 133-149

The Bug: The stderr reading task blocks indefinitely on lines().next_line().await without timeout handling. When MCP servers produce heavy stderr output (like bc-code-intel's 189-237ms of loading logs), this interferes with the stdio transport on Windows.

Code:

if let Some(stderr) = stderr {
    tokio::spawn(async move {
        let mut reader = BufReader::new(stderr).lines();
        loop {
            match reader.next_line().await {  // ← NO TIMEOUT
                Ok(Some(line)) => {
                    info!("MCP server stderr ({program_name}): {line}");
                }
                Ok(None) => break,
                Err(error) => {
                    warn!("Failed to read MCP server stderr ({program_name}): {error}");
                    break;
                }
            }
        }
    });
}

Smoking Gun: This exact pattern was already fixed in commit 73ed30d7e ("Avoid hang when tool's process spawns grandchild that shares stderr/stdout") for shell tool execution in codex-rs/core/src/exec.rs lines 584-625, but was never applied to MCP client code.

Proposed Fix (UNTESTED)

Note: This fix is based on code analysis and the existing pattern in exec.rs, but has NOT been tested locally due to build environment requirements. The Codex team should validate this approach.

Apply the same timeout pattern from exec.rs to prevent stderr blocking:

if let Some(stderr) = stderr {
    tokio::spawn(async move {
        use tokio::time::{timeout, Duration};
        let mut reader = BufReader::new(stderr).lines();
        loop {
            match timeout(Duration::from_secs(5), reader.next_line()).await {
                Ok(Ok(Some(line))) => {
                    info!("MCP server stderr ({program_name}): {line}");
                }
                Ok(Ok(None)) => break,
                Ok(Err(error)) => {
                    warn!("Failed to read MCP server stderr ({program_name}): {error}");
                    break;
                }
                Err(_) => {
                    warn!("MCP server stderr ({program_name}): read timeout after 5s, continuing...");
                }
            }
        }
    });
}

Additional Information

Testing Done:

  • ✅ Verified bc-code-intel works with manual stdio testing
  • ✅ Tested with SDK 0.4.0 and 1.22.0 - same issue
  • ✅ Confirmed filesystem MCP works in Codex (low stderr)
  • ✅ Isolated issue to Codex's stderr handling, not bc-code-intel
  • ❌ Proposed fix NOT tested locally (build environment setup incomplete)

Files Involved:

  • codex-rs/rmcp-client/src/rmcp_client.rs - needs fix
  • codex-rs/core/src/exec.rs - reference implementation

Related Commits:

  • 73ed30d7e - Fixed similar issue for shell tools
  • f828cd289 - Windows MCP server execution fix

This is a Windows-specific race condition in MCP client stderr handling that should be fixed to support MCP servers with verbose diagnostic output.

Activity

  1. added
    bugSomething isn't working
    windows-osIssues related to Codex on Windows systems
    mcpIssues related to the use of model context protocol (MCP) servers
    on Nov 22, 2025
  2. AyinMostima commented on Dec 23, 2025

    @AyinMostima

    I've also encountered this problem. Codex is truly disappointing, a bunch of idiots!

  3. vincentchau0709-lang commented on Jan 15, 2026

    @vincentchau0709-lang

    Hopefully Codex will fix this problem ASAP, this is truely disappointing.

  4. gitown commented on Feb 7, 2026

    @gitown

    The problem still exist in Codex VS Extension v0.4.71 and Codex CLI v0.98.0
    cc @gpeal

  5. sstklen commented on Feb 26, 2026

    @sstklen
  6. alario-tang commented on Apr 22, 2026

    @alario-tang

    Related, but I do not think this is a duplicate of #7155.

    I just filed #18977 for a different Transport closed failure mode on macOS:

    • underlying OMX operation is healthy when invoked through CLI parity
    • the downstream MCP server is using stdio transport and exits on stdin_end / stdin_close / transport_close
    • the failure appears when the host-side MCP transport is dropped/replaced/closed, not specifically when stderr is noisy

    So the overlap is the user-visible symptom (Transport closed), but the suspected trigger is different:

    Linking the two in case the maintainers see a shared host-side weakness around MCP process / transport management:
    #18977

  7. HaoYue1027 commented on Oct 13, 2026

    @HaoYue1027

    I isolated a stderr-encoding failure that may explain a subset of Windows MCP transport failures reported here. It reproduces with two tiny diagnostic writes; high stderr volume is not required. I have not established that it explains the original report.

    Environment and observed failure

    • Windows build 26300.9457; Python standard streams defaulted to GBK in the MCP child.
    • Actual desktop-bundled Codex app-server: 0.162.0-alpha.17.2.
    • Basic Memory 0.23.2, FastMCP 4.0.0b1, MCP Python SDK 2.3.0, Python 3.12.13, uv 0.12.9.
    • Server command: uvx --prerelease=allow basic-memory mcp.

    Basic Memory's FastMCP startup banner runs before the stdio protocol loop. The client logs stream did not contain valid UTF-8, then the server fails while flushing subsequent banner output:

    log_server_banner -> Rich console -> legacy_windows_render
    -> write_styled -> flush -> OSError: [Errno 22] Invalid argument
    

    The MCP initialization then fails with connection closed: initialize response, and no tools are registered. BrokenPipeError was not observed on this Windows machine.

    Controlled isolation

    The dependency-free Python MCP fixture below can be saved as stderr-pipe-fixture.py. It first writes b"invalid UTF-8 byte: \xff\n" to stderr, waits 350 ms, and then writes ASCII diagnostics. It never accesses knowledge files or account data; it writes a small result JSON beside itself.

    Register it with an isolated MCP configuration, substituting absolute paths to Python and the script below:

    [mcp_servers.stderr-fixture]
    command = "C:/path/to/python.exe"
    args = ["C:/path/to/stderr-pipe-fixture.py", "exit"]

    Start Codex app-server and query mcpServerStatus/list. Repeat in a fresh process with the last argument changed to survive.

    Fixture mode Second stderr write Initialization and tools
    exit Failed with errno 22; fixture exits Handshake fails; 0 tools
    survive Same write error, caught Handshake succeeds; 1 synthetic tool

    This separates stderr reader lifetime from directly terminating the server on decode failure. The stderr read end closes; a later unhandled server write error causes the process to exit.

    For the real Basic Memory server, setting only service-level PYTHONIOENCODING=utf-8 changes standard stream encoding while leaving sys.flags.utf8_mode == 0. The exact command above then initializes and exposes 21 tools. A real read-only note call also succeeded. This acceptance used no --offline argument or auto-update environment override. Disabling only the banner also worked with GBK stderr, identifying the banner as the current trigger.

    Code path and suggested regression

    The current main source uses BufReader.lines()/next_line() for stderr and breaks the reader loop on decoding error:

    https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/stdio_server_launcher.rs

    Main is explanatory source evidence, not a claim about the tested binary's exact commit. The reader-close effect was independently observed with the fixture above.

    Would you consider retaining and draining the diagnostic pipe after malformed UTF-8, using bounded byte reads and lossy display? Stdout MCP JSON should remain strictly validated. A regression can write an invalid byte to stderr followed by ASCII stderr and then a valid initialize response; initialization should survive and the second write should not fail because the client dropped the reader.

    The MCP specification permits UTF-8 strings on stderr. This report concerns compatibility and robustness when a server emits malformed diagnostic bytes; it does not allege a normative protocol violation by Codex:

    https://modelcontextprotocol.io/specification/2025-11-25/basic/transports

    Related work and limits

    FastMCP #5641 reports a similar Windows GBK/filtered-environment problem. Its linked PR #5642 changes the FastMCP client, so it does not directly fix Codex's reader:

    PrefectHQ/fastmcp#5641

    PrefectHQ/fastmcp#5642

    Checked on 2026-10-13: #7155 remains open and the main reader still has the break-on-error branch. The latest stable release found was rust-v0.162.1; that binary was not tested. Please confirm whether this belongs here or should be split into a focused stderr-decoding issue. Expected: malformed diagnostics do not tear down an otherwise valid stdio MCP connection. Actual: the reader closes and a subsequent server diagnostic write can abort initialization.

    Complete minimal stderr fixture (Python standard library only)
    """Synthetic MCP: only tests stderr reader lifetime; never touches knowledge files."""
    import json
    from pathlib import Path
    import sys
    import time
    
    mode = sys.argv[1]
    if mode not in {"survive", "exit"}:
        raise ValueError("Unsupported fixture mode")
    record = {"mode": mode}
    try:
        sys.stderr.buffer.write(b"invalid UTF-8 byte: \xff\n")
        sys.stderr.buffer.flush()
        time.sleep(0.35)
        sys.stderr.buffer.write(b"valid ASCII diagnostic after invalid line\n")
        sys.stderr.buffer.flush()
        record["second_write"] = "success"
    except OSError as exc:
        record.update(second_write="failed", errno=exc.errno, error=str(exc))
    Path(__file__).with_name(f"fixture-{mode}.json").write_text(json.dumps(record), encoding="utf-8")
    if mode == "exit" and record.get("second_write") == "failed":
        raise SystemExit(1)
    
    for raw in sys.stdin.buffer:
        try:
            request = json.loads(raw.decode("utf-8"))
        except (UnicodeError, json.JSONDecodeError):
            continue
        if "id" not in request:
            continue
        method = request.get("method")
        if method == "initialize":
            result = {"protocolVersion": request["params"]["protocolVersion"],
                      "capabilities": {"tools": {}}, "serverInfo": {"name": "stderr fixture", "version": "1"}}
        elif method == "tools/list":
            result = {"tools": [{"name": "fixture_readonly", "description": "Synthetic read-only diagnostic", "inputSchema": {"type": "object", "properties": {}}}]}
        elif method in {"resources/list", "resources/templates/list"}:
            result = {"resources" if method == "resources/list" else "resourceTemplates": []}
        elif method == "ping":
            result = {}
        else:
            result = {}
        reply = {"jsonrpc": "2.0", "id": request["id"], "result": result}
        sys.stdout.buffer.write((json.dumps(reply) + "\n").encode("utf-8"))
        sys.stdout.buffer.flush()
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 workingmcpIssues related to the use of model context protocol (MCP) serverswindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions