fix(mcp): survive stdio host disconnect and silence handled MCP noise#79
Merged
Conversation
`dg mcp --transport stdio` runs under an MCP host (Claude Desktop, Codex, etc.) which owns stdout for the JSON-RPC protocol and typically closes it on disconnect. After that, anything that writes to the global rich Console raises "ValueError: I/O operation on closed file" and the CLI exits with an unhandled crash (DX-CLI-4, DX-CLI-5; 88 events in the last 14 days, still firing). Three layered fixes: 1. dg mcp now uses stderr_console as its module console and returns None for stdio transport so output_result is never invoked with content destined for the closed protocol channel. 2. BaseCommand.execute traps BrokenPipeError, OSError, and the specific closed-file ValueError around output_result so any other stdio-mode command in the future inherits the same safety. Unrelated ValueErrors still propagate as ClickException so real bugs aren't hidden. 3. deepctl-telemetry's _scrub_event now drops handled log events from the upstream mcp SDK (mcp.client.streamable_http, mcp.server.lowlevel.server). These are recovered internally by the SDK when an upstream MCP server returns 5xx or a stdio peer closes mid-message. They were ending up as Sentry issues (DX-CLI-1, DX-CLI-3) with no actionable signal for the DX team. Unhandled exceptions from those loggers are still kept. Fixes DX-CLI-1, DX-CLI-3, DX-CLI-4, DX-CLI-5 in the dx-cli Sentry project. DX-CLI-2 lives in the upstream deepgram-mcp package and needs a separate fix there.
Merged
lukeocodes
pushed a commit
that referenced
this pull request
May 12, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.2.25</summary> ## [0.2.25](v0.2.24...v0.2.25) (2026-05-12) ### Bug Fixes * **mcp:** survive stdio host disconnect and silence handled MCP noise ([#79](#79)) ([e95d511](e95d511)) </details> <details><summary>deepctl-core: 0.2.13</summary> ## [0.2.13](deepctl-core-v0.2.12...deepctl-core-v0.2.13) (2026-05-12) ### Bug Fixes * **mcp:** survive stdio host disconnect and silence handled MCP noise ([#79](#79)) ([e95d511](e95d511)) </details> <details><summary>deepctl-telemetry: 0.0.5</summary> ## [0.0.5](deepctl-telemetry-v0.0.4...deepctl-telemetry-v0.0.5) (2026-05-12) ### Bug Fixes * **mcp:** survive stdio host disconnect and silence handled MCP noise ([#79](#79)) ([e95d511](e95d511)) </details> <details><summary>deepctl-cmd-mcp: 0.1.14</summary> ## [0.1.14](deepctl-cmd-mcp-v0.1.13...deepctl-cmd-mcp-v0.1.14) (2026-05-12) ### Bug Fixes * **mcp:** survive stdio host disconnect and silence handled MCP noise ([#79](#79)) ([e95d511](e95d511)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dg mcp --transport stdiowas crashing when the AI host (Claude Desktop, Codex, etc.) closed the JSON-RPC stdio channel on disconnect. Anything that wrote to the global richConsoleafter that raisedValueError: I/O operation on closed fileand surfaced as an unhandled exception (DX-CLI-4, DX-CLI-5 — 88 events in 14d, 27 in the last hour alone, still firing as of writing).This PR adds three layered fixes:
1.
dg mcpwrites to stderr and returns None for stdiopackages/deepctl-cmd-mcp/src/deepctl_cmd_mcp/command.pyconsoleis nowstderr_consolefromdeepctl_core.output. Writing anything to stdout while the MCP host owns it would corrupt the JSON-RPC stream anyway.transport=stdiothe handler returnsNoneinstead ofMCPServerResult. That skipsBaseCommand.output_resultentirely so there is nothing to write to the closed protocol channel after the proxy exits.MCPServerResult. Those have a real terminal to print to.2.
BaseCommand.executetraps closed-stream writes aroundoutput_resultpackages/deepctl-core/src/deepctl_core/base_command.pyDefense in depth so any future stdio-mode command inherits the same safety. Catches
BrokenPipeError,OSError, andValueError("...closed file..."). UnrelatedValueErrors still propagate asClickExceptionso real bugs aren't hidden.3.
deepctl-telemetrydrops handled MCP SDK log noisepackages/deepctl-telemetry/src/deepctl_telemetry/client.py_scrub_eventnow drops events whoseloggeris in:mcp.client.streamable_http(the upstream MCP returned 5xx, recovered by the client — DX-CLI-1)mcp.server.lowlevel.server(stdio peer closed mid-message, recovered by the server — DX-CLI-3)…provided no unhandled exception is attached. Unhandled crashes from those loggers (e.g. a real bug in the embedded SDK) are still surfaced.
Sentry issues addressed
ValueError: I/O operation on closed file.(rich/console)ValueError: I/O operation on closed file.(base_command:124)HTTPStatusError: 503 from /kapa/mcp_scrub_event(handled by MCP SDK)Received exception from stream: 1 validation error for JSONRPCMessage_scrub_event(peer-closed noise)AttributeError: 'Request' object has no attribute 'send'deepgram-mcppackage'sproxy.py:handle_sse. Needs a separate fix there.Companion PR
This is the client side of deepgram/dx-api#67, which fixed the same architectural family (handle upstream/downstream stream disconnects cleanly) on the server end.
Test plan
make test(full suite): 965 passed, 0 failedmake lint: cleanmake format-check: 115 files already formattedNew tests:
TestExecuteStreamSafetyinpackages/deepctl-core/tests/unit/test_base.pyValueError("I/O operation on closed file")BrokenPipeErrorValueErrors (negative invariant guard)TestMcpNoiseFilterinpackages/deepctl-telemetry/tests/unit/test_telemetry.pymcp.client.streamable_httpmcp.server.lowlevel.server_scrub_event: returns None for noise, still scrubs headers/cookies/user data for normal eventstest_handle_stdio_transport_returns_noneandtest_handle_api_key_from_auth_managerto expect None for stdio (the new contract)Backwards compatibility
McpCommand.handlefor stdio mode now returnsNoneinstead ofMCPServerResult(status="success", ...). Anything programmatically callinghandle()for stdio and reading.statuswill need to treatNoneas success. In practice this is internal toBaseCommand.execute, which already handlesresult is None.Related