feat: forward debuggee stdout/stderr to the MCP client (#218) - #224
Open
debugmcpdev wants to merge 1 commit into
Open
feat: forward debuggee stdout/stderr to the MCP client (#218)#224debugmcpdev wants to merge 1 commit into
debugmcpdev wants to merge 1 commit into
Conversation
DAP 'output' events flowed intact from the adapter through the proxy to
ProxyManager, which re-emitted them on the generic 'dap-event' channel —
where they died, because SessionManager never subscribed. A debugged
program's print() output was invisible to the MCP client.
- Promote 'output' to a first-class typed ProxyManager event (dap-core
mirror switch updated to match); other unhandled events still ride
'dap-event'.
- Capture output per session into a bounded OutputRingBuffer (1000
entries, 8 KB per entry, telemetry filtered at write, fresh buffer per
launch); SessionManager emits 'output-captured' for observers.
- New get_output tool: cursor-based (since/nextSince), works while
running and after program exit until the session is closed.
- Expose each session's transcript as an MCP resource
(debug://sessions/{id}/output) with resources/subscribe support;
updated-pings are debounced (~150 ms) so notification volume is
independent of debuggee output volume. Dev proxy passes resource
requests and notifications through.
- Also register sinceTimestamp in TOOL_ARG_EXPECTED_TYPES (pre-existing
gap in the SSE arg-coercion map).
Verified end-to-end: Python smoke e2e asserts the printed marker arrives
via get_output (category stdout), the cursor drains, the subscription
ping fires, and the resource read contains the transcript.
Follow-ups filed: #222 (Ruby routes debuggee stdio to the adapter
process), #223 (Rust console/terminal launch-config key).
Fixes #218
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
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.
Fixes #218.
Problem
A debugged program's stdout/stderr never reached the MCP client. DAP
outputevents flowed intact from the adapter →minimal-dap→ proxy worker (IPCdapEvent) →ProxyManager.handleDapEvent, which re-emitted them on the generic'dap-event'channel — and died there:SessionManagerCore.setupProxyEventHandlersregisters handlers for 8 events, none of them'dap-event', so Node's EventEmitter silently discarded every output event. Python has emitted these events all along (redirectOutput: true); they were simply dropped on the last hop.What this PR does
1. Typed
outputevent (src/proxy/proxy-manager.ts, src/dap-core/handlers.ts)outputgets a first-class entry inProxyManagerEventsand its owncasein both switch statements (ProxyManager and the functional-core mirror), consistent withstopped/continued/terminated/exited. Other unhandled events still ride'dap-event'.2. Per-session capture (packages/shared, src/session/)
SessionOutputEntrymodel type (seq,category,output,timestamp,truncated?).OutputRingBuffer(src/session/output-buffer.ts): monotonic seq from 1, cap 1000 entries with eviction +droppedcount, 8 KB per-entry truncation,renderText()verbatim transcript.SessionManagerCore(now anEventEmitter) registers a ninth proxy-event handler that pushesoutputevents intosession.outputBuffer— created fresh per launch/attach, alongside thelastStopreset — and emits'output-captured'.telemetry(js-debug noise) is filtered at write time; a missing category defaults toconsoleper DAP. The handler rides the existing handlers map, so teardown is automatic.3.
get_outputtool (src/server.ts)Cursor-based read:
sessionId, optionalsince(passnextSincefrom the previous response) andlimit(default 100, max 1000). Returns{ entries, nextSince, hasMore, dropped }. Deliberately does notvalidateSession()— reading output after the program finished is the primary use case; the buffer stays readable untilclose_debug_session.since/limitare registered in the SSE arg-coercion map (andsinceTimestamp, a pre-existing gap, is registered while touching that map).4. Output resources + subscriptions (src/server.ts)
Each session exposes
debug://sessions/{id}/output(text/plain, verbatim interleaved transcript). Server capabilities now declareresources: { subscribe: true, listChanged: true };resources/list|read|subscribe|unsubscribeare implemented,list_changedfires on session create/close, andresources/updatedpings are debounced (~150 ms per URI) so notification volume is independent of how fast the debuggee prints. Timers and the SessionManager listener are torn down instop()(leak-guard safe). The dev proxy passes resource requests through and relays resource notifications (backend restarts drop subscriptions — documented inline).Adapter audit (from #218's "worth an audit" note)
redirectOutput: trueoutputCapture: 'std';telemetryfiltered)JdiDapServer.javapumps both streamsconsole: 'internalConsole'path, no traces in-repoconsole:but CodeLLDB wantsterminal:→ #223output; unit tests drive the mock ProxyManagerVerification
get_output(incl. TERMINATED-session regression guard), and resources (debounce burst → single ping under fake timers, unsubscribe/stop cleanup).mcp-server-smoke-python): the script'sFactorial of 5print arrives viaget_outputwithcategory: stdout; cursor round-trip drains; the subscription producedresources/updated;resources/readreturns the transcript. Comprehensive matrix gains a lenient per-languageget_outputrow.🤖 Generated with Claude Code