Skip to content

fix(mcp): swallow broken/closed-pipe on dg mcp startup notifications and error path#88

Open
dg-coreylweathers wants to merge 2 commits into
mainfrom
fix/dg-mcp-broken-pipe-DX-CLI-P
Open

fix(mcp): swallow broken/closed-pipe on dg mcp startup notifications and error path#88
dg-coreylweathers wants to merge 2 commits into
mainfrom
fix/dg-mcp-broken-pipe-DX-CLI-P

Conversation

@dg-coreylweathers

Copy link
Copy Markdown

Summary

Fixes DX-CLI-Pdg mcp crashes with ValueError: I/O operation on closed file on a broken stderr pipe.

When dg runs as an MCP server and the host closes stdio (a normal condition on client disconnect), the crash was a two-stage cascade:

  1. Primary — after CLI execution, print_pending_notification() does a raw sys.stderr.write(...). On a closed pipe this raises BrokenPipeError.
  2. Secondary — the except Exception handler in main() then called console.print(...), but the console's underlying stream is also closed, so rich raised ValueError: I/O operation on closed file — an unhandled exception that escaped to the excepthook, crashing the process and generating a Sentry event.

Changes

  • Guard all three startup notification writers (update, plugin, skills) against (BrokenPipeError, OSError, ValueError), matching the existing closed-stream pattern in base_command.py.
  • Make main()'s error path stream-safe via a new _safe_console_print() helper, so a closed console no longer crashes the process on exit. Both the KeyboardInterrupt and generic Exception handlers route through it.
  • Defense-in-depth: drop broken-pipe / closed-file events in the Sentry before_send hook (_is_broken_pipe) so residual pipe-close on dg mcp produces no noise.
  • Adds unit coverage for the notification guard and the Sentry filter.

Testing

  • test_telemetry.py + test_startup_check.py: 42 passed (incl. 6 new)
  • plugin-update + skills packages: 33 passed
  • ruff check / ruff format --check / mypy on changed files: clean
  • End-to-end: reproduced the closed-stream scenario against print_pending_notification() (survives without raising) and ran a real dg mcp process with a broken output pipe (exit 0, no traceback leaked).

Sentry: https://deepgram.sentry.io/issues/DX-CLI-P

🤖 Generated with Claude Code

…and error path

When `dg mcp` runs as an MCP server and the host closes stdio, the startup
update-notification write to stderr raised BrokenPipeError, which then
cascaded into an unhandled ValueError from rich while the error handler
tried to print to the already-closed console — crashing through the
excepthook and generating a Sentry event (DX-CLI-P).

- Guard all three startup notification writers (update, plugin, skills)
  against (BrokenPipeError, OSError, ValueError), matching the existing
  closed-stream pattern in base_command.py.
- Route main()'s error/interrupt handlers through a new
  _safe_console_print() helper so a closed console no longer crashes the
  process on exit.
- Defense-in-depth: drop broken-pipe / closed-file events in the Sentry
  before_send hook (_is_broken_pipe) so residual pipe-close on dg mcp
  produces no noise.

Adds unit coverage for the notification guard and the Sentry filter.

Sentry: https://deepgram.sentry.io/issues/DX-CLI-P

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dg-coreylweathers dg-coreylweathers requested a review from a team July 16, 2026 19:27
Adds the missing regression tests for the three DX-CLI-P guards that shipped
without coverage:

- _safe_console_print (main.py): swallows BrokenPipeError / OSError / rich's
  closed-file ValueError, plus an end-to-end test that main() still exits(2)
  when the cli raises AND the console is closed (the exact crash cascade).
- skills print_pending_notification: tolerates a broken stderr.
- plugin print_pending_plugin_notifications: tolerates a broken stderr.

Each guard test fails if its try/except is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dg-coreylweathers

Copy link
Copy Markdown
Author

Reviewer context — DX-CLI-P

What broke: when dg runs as an MCP server and the host closes stdio (a normal disconnect), the startup update-notification write to stderr raised BrokenPipeError. The except Exception handler in main() then tried console.print(...), but that stream was also closed, so rich raised ValueError: I/O operation on closed file — an unhandled exception that escaped to the excepthook, crashing the process and generating a Sentry event.

The fix (commit 6091c6a):

  • Guarded all three startup notification writers (update, plugin, skills) against (BrokenPipeError, OSError, ValueError), matching the existing closed-stream pattern in base_command.py.
  • Routed main()'s error/interrupt handlers through a new _safe_console_print() so a closed console no longer crashes the process on exit — this is what stops the secondary cascade.
  • Defense-in-depth: drop broken-pipe / closed-file events in the Sentry before_send hook (_is_broken_pipe) so residual pipe-close on dg mcp produces no noise. (Note: this suppresses broken-pipe events globally, not just for dg mcp — a deliberate choice, since a broken pipe is environmental rather than an actionable CLI bug.)

Coverage (commit 5b9ea16): added regression tests for all three guards, including an end-to-end test that reproduces the exact cascade — cli raises and the console is closed — and asserts main() still exits(2) instead of crashing.

Verification:

  • 137 tests pass across the main, skills, update, and telemetry packages.
  • Mutation-checked: removing the _safe_console_print guard makes all 4 of its tests fail; restored, they pass — so the coverage genuinely guards the fix.
  • ruff / mypy clean on all changed source files.
  • Also exercised a real dg mcp process against a broken output pipe: exit 0, no traceback leaked.

One earlier review nit (bare ValueError vs the narrower "closed file" match in base_command.py) was left as-is intentionally — stderr.write() only raises ValueError for a closed file, so the broader catch is safe today.

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