Skip to content

fix: catch BrokenPipeError in stdio stdout_writer#3097

Open
Lexus2016 wants to merge 1 commit into
modelcontextprotocol:mainfrom
Lexus2016:fix/stdio-broken-pipe
Open

fix: catch BrokenPipeError in stdio stdout_writer#3097
Lexus2016 wants to merge 1 commit into
modelcontextprotocol:mainfrom
Lexus2016:fix/stdio-broken-pipe

Conversation

@Lexus2016

Copy link
Copy Markdown

Problem

When the parent process (MCP client) closes the stdout pipe — during restart, kill, or graceful shutdown — the stdout_writer() coroutine in stdio_server() raises BrokenPipeError at await stdout.flush() (line 81).

The existing handler only catches anyio.ClosedResourceError, which does not cover BrokenPipeError. The unhandled exception propagates through anyio's TaskGroup as an ExceptionGroup, crashing the entire MCP server process.

Reproduction

  1. Start any MCP server using stdio transport (e.g. turbo-memory-mcp serve)
  2. Connect from a client, make a few tool calls
  3. Kill the client process (kill -9 or close stdin)
  4. Server crashes with:
ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  -> BrokenPipeError: [Errno 32] Broken pipe

Impact

This is a common production scenario — every MCP client restart (config reload, session reset, crash recovery) kills the server. Downstream servers like turbo-memory-mcp leave stale daemon locks and require manual cleanup (pkill, rm lockfile, reconnect).

Fix

Extend the except clause in stdout_writer() to also catch BrokenPipeError and ConnectionResetError, treating them as graceful shutdown signals:

# Before:
except anyio.ClosedResourceError:
    await anyio.lowlevel.checkpoint()

# After:
except (anyio.ClosedResourceError, BrokenPipeError, ConnectionResetError):
    await anyio.lowlevel.checkpoint()

Testing

Tested in production with Hermes Agent (Nous Research) as the MCP client and turbo-memory-mcp as the server. The fix has been running reliably — server now exits cleanly on client restart instead of crashing with ExceptionGroup.

When the parent process (MCP client) closes the stdout pipe during
restart, kill, or graceful shutdown, stdout_writer() raises
BrokenPipeError at 'await stdout.flush()'. The existing handler only
catches anyio.ClosedResourceError, so the BrokenPipeError propagates
through anyio's TaskGroup as an unhandled ExceptionGroup, crashing the
entire MCP server process.

This is a common production scenario: any MCP client restart (config
reload, session reset, crash recovery) kills the server, requiring
manual cleanup of stale daemon locks and process restarts.

Fix: extend the except clause to also catch BrokenPipeError and
ConnectionResetError, treating them as graceful shutdown signals.

Tested in production with Hermes Agent (Nous Research) as the MCP
client and turbo-memory-mcp as the server.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant