Add MCP bridge for DAP debugging - #3937
Conversation
…scovery, time-based fixture - dap.rst: a definition-list term must sit on one line; the two-line term at the stepping tools entry failed the Sphinx build with "Unexpected indentation" - .gitignore: `!.codex/` un-ignored the whole directory, so a local .codex/config.toml or .codex/worktrees/ showed as untracked; `.codex/*` with the single un-ignore keeps only the example tracked - mcp_bridge.py / test_mcp_bridge.py: the Ninja and MinGW layouts on Windows put the binary at bin/daslang.exe, which neither candidate list carried; the harness override moves from `DASLANG` to `DASLANG_DAP_BIN`, matching `DASLANG_MCP_BIN` / `DASLANG_LSP_COMPILER` and no longer colliding with a shell variable that names an unrelated binary - _fixture.das: the keep-alive loop is time-based (300 ms) instead of 1000 x sleep(1u), which is ~1 s on Linux but ~16 s on Windows (15.6 ms timer granularity) and overran every post-disconnect exit wait in the harness Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…osed peer no longer spins forever Server::send_msg and Server::tick compared errno after send/recv, which Winsock never sets: a failed send read as "no error", so the retry loop spun forever with the context lock held, and the debugger tick that would have noticed the closed client never ran. The DAP debuggee therefore hung after a client disconnect on Windows. Both paths now go through last_socket_error() and socket_would_block(); the same helpers report init failures. REVIEW.md carries the ban. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5476efa to
6f2d669
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
It rewrites concurrency-critical debugger worker lifecycle and callback lock-ordering in the core C++ runtime, whose thread-interleaving correctness needs human verification.
Pull request overview
This PR adds a stateful Python MCP bridge (utils/dap/mcp_bridge.py) that fronts daslang's existing TCP Debug Adapter Protocol server, exposing 21 MCP tools for launch/attach, breakpoint and data-breakpoint management, stack/variable inspection, evaluation, execution control, event waiting, and idempotent cleanup. It also fixes debugger-side lifecycle and stepping races: debug-agent callbacks are serialized through Context::threadlock_context, the statement-debugger worker uses an explicit generation-based readiness/singleton handshake, and existing contexts get breakpoint instrumentation at configurationDone. Socket error handling in network.cpp is corrected for Winsock. Extensive Python and dastest fixtures, docs (RST + README + tutorial), CI wiring, and install/CMake plumbing accompany the feature.
Changes:
- New Python DAP↔MCP bridge plus an end-to-end test and
.dasregression fixtures covering callback locking, cancellation, duplicate/destroyed contexts, framing, and stepping. - C++ runtime fixes:
threadlock_context-serialized debug-agent callbacks (lazycontextMutex), generation-based debugger-worker lifecycle inmodule_builtin_debugger.cpp/module_builtin_jobque.cpp, and portable socket-error helpers innetwork.cpp. daslib/debug.dasbreakpoint-lookup refactor (release table borrow before pausing), explicit pinvoke readiness query,onSimulateContextworker release; matching ARCHITECTURE anchors and documentation/CI/install wiring.
File summaries
| File | Description |
|---|---|
| utils/dap/mcp_bridge.py | New stateful MCP bridge translating MCP JSON-RPC to DAP; DAP framing, session/process lifecycle, 21 tools. |
| utils/dap/test_mcp_bridge.py | End-to-end + unit tests for framing limits, command mapping, and full launch/attach/step flows. |
| utils/dap/README.md, doc/source/reference/utils/dap.rst, doc/.../utils.rst | New reference docs and TOC entry for the bridge. |
| doc/.../45_debug_agents.rst, doc/.../function-debugapi-...ready-....rst, doc/reflections/das2rst.das | Debugger-macro scope docs and stdlib doc registration for the new builtin. |
| utils/dap/_fixture*.das | Regression fixtures for cancel/callback/context-destroy/lifecycle/duplicate scenarios. |
| tests/debug_agent/test_callback_threadlock.das | Verifies callback/pinvoke share the context lock (no overlap). |
| src/builtin/module_builtin_debugger.cpp | Serializes callbacks via threadlock_context; generation-based worker readiness/singleton; lazy contextMutex. |
| src/builtin/module_builtin_jobque.cpp | Reworks new_debugger_thread to the readiness handshake and rejects duplicate workers. |
| src/misc/network.cpp, src/misc/REVIEW.md | Portable last_socket_error()/socket_would_block(); Winsock rule in REVIEW. |
| daslib/debug.das, daslib/ARCHITECTURE.md | Breakpoint-lookup refactor, readiness pinvoke, worker release; three new arch anchors. |
| CMakeLists.txt, install/CLAUDE.md, .github/workflows/extended_checks.yml, .codex/config.toml.example, .gitignore | Install/CI/config wiring for the bridge. |
I did not find a concrete, defensible defect: the ARCHITECTURE anchors all resolve to their [arch] citations, contextMutex is freed in the Context destructor (no leak), throw_error_at is [[noreturn]] (so the duplicate-worker path is safe), the DAP frame limits and command mapping are well-covered by tests, and the network.cpp change correctly fixes Winsock error reporting. However, the C++ changes introduce new process-wide synchronization (condition variables, generation counters, a debugger-worker singleton, and callback lock ordering between the agent registry and context mutex) whose correctness under all thread interleavings cannot be fully verified from static review alone.
Review details
- Files reviewed: 25/26 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The gate scans network.cpp with comments stripped, finds each helper's body by brace depth from its definition line, and fails a read of errno outside last_socket_error() (writes such as `errno = 0` pass), a would-block code (EAGAIN, EWOULDBLOCK, EINTR, WSAEWOULDBLOCK) named outside socket_would_block(), or either helper missing. Red on master's network.cpp with 11 findings, green after the fix. The checklist keeps only "weakening REVIEW.das is a defect"; the mechanism moves to ARCHITECTURE.md section 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e CI lane gets a watchdog A deserialized whole-suite run printed nothing per program, so a hang left the CI log ending mid-test with no way to tell which test was running. The run now logs `deser N/M: <file>` before each program and a finish line with the totals. The extended_checks ser/deser step passes --timeout 1200, so a hang fails the lane in twenty minutes with the last started program in the log instead of holding the runner for six hours. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A whole-suite run that trips --timeout printed only "Test timed out after Ns", so a hang on a CI lane whose runner buffers the test output (cmake's run_tests_* targets) left no clue which test it was. The per-file loop and the deserialized run record the file they are about to run, and the timeout report prints it: "Test timed out after 1800s while running <file>". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Behavior change on Windows: a daslang debuggee no longer hangs after the DAP client disconnects -
src/misc/network.cppchanged, rebuilddaslang.Adds a stateful Python MCP bridge over the existing daScript DAP server. It exposes launch and attach, breakpoint and data-breakpoint management, stack and variable inspection, evaluation, execution control, event waiting, and lifecycle cleanup, and keeps DAP events, process output, and exit information for agent clients. On the debugger side it fixes lifecycle and stepping races: debug-agent callbacks are serialized with the context lock, worker readiness is explicit, and contexts that already exist when configuration completes get their breakpoints instrumented. Documentation, install rules, and the Linux
extended_checkslane describe and run the bridge.Rebased onto current master. Building and testing the branch on Windows found five more defects, all fixed here. The
.gitignorechange un-ignored the whole.codex/directory. Neither the bridge nor its harness looked forbin/daslang.exe, the Ninja layout. The harness read a bareDASLANGenvironment variable, which collides with unrelated shells; it is nowDASLANG_DAP_BIN, matchingDASLANG_MCP_BIN. The fixture's keep-alive loop of 1000 xsleep(1u)takes 1 s on Linux but 16 s on Windows (15.6 ms timer granularity), so every post-disconnect exit wait timed out; it is time-based now. And a pre-existing runtime bug:Server::send_msgandServer::tickreaderrnoafter Winsock calls, which never set it. A send to a disconnected client read as "no error" and retried forever while holding the debug-agent context lock, so the tick that would have noticed the closed socket never ran and the debuggee hung - the VSCode Stop button hit this too. Both paths now go throughlast_socket_error()andsocket_would_block().src/misc/REVIEW.dasis the executable half of that folder's checklist. It strips comments fromnetwork.cpp, locates the body of each helper by brace depth from its definition line, and fails whenerrnois read outsidelast_socket_error()(a write such aserrno = 0passes), whenEAGAIN,EWOULDBLOCK, orWSAEWOULDBLOCKis named outsidesocket_would_block(), or when either helper is missing. Each finding carries the file, the line, and the helper to call. The checklist rule became "weakeningREVIEW.dasis a defect"; the mechanism lives insrc/misc/ARCHITECTURE.mdsection 5. The tree-wide walker inextended_checksruns it with the other gates.The darwin15
extended_checkslane hung once inside the deserialized whole-suite run with no per-program output to say where. The deserialized run now logsdeser N/M: <file>before each program and a finish line with the totals, the CI step passes--timeout 1200, and the dastest timeout report now names the file that was running ("Test timed out after Ns while running "), so a hang fails the lane in twenty minutes and says where, including under the cmakerun_tests_*targets whose output ninja buffers until the process exits.Where to look:
utils/dap/mcp_bridge.py,src/builtin/module_builtin_debugger.cpp,src/builtin/module_builtin_jobque.cpp,daslib/debug.das,src/misc/network.cpp,src/misc/REVIEW.das.Validation, claims, ledger
Validation
tests/debug_agent54/54;utils/dap/test_mcp_bridge.pypasses in both instrumentation andDAS_TEST_STEPPING=1modes (CI runs it on Linux only); a plugin-equivalent DAP session (--das-debug-port+--das-wait-debugger, then initialize, launch, setBreakpoints, configurationDone, stackTrace, scopes, variables, evaluate, next, stepOut, continue) hits and steps in both modes with Windows paths; adisconnectright afterconfigurationDonenow lets the debuggee run to exit, where master spins forever (confirmed by a cdb stack dump: main thread insendunderonCreateContext, tick thread blocked on the context mutex).src/misc/REVIEW.das: 11 findings against master'snetwork.cpp, green after the fix;utils/internal/review-md/all.dasdiscovers it (20 gates; the one local red is the absent tree-sitter CLI).dap.rstfix verified with docutils only; the Build doc lane re-proves it.tests/debug_agent54/54,review-md --base HEAD,fix_md_ascii.py --check, the twoexamples/debugapiscripts, and preflight's formatting, interpreted lint, hash-refs, review metadata, AST verification, C++ syntax, dasgen, CI das scripts, and documentation consistency rails.Claims - stated, not tested
socket_would_block()treatsEINTRas retry on POSIX; no test delivers a signal mid-send. A break would show asonError("can't send", EINTR)closing a healthy connection.build/daslang.execandidate (MinGW or Ninja inbuild/) is not exercised;bin/daslang.exeis.Not done
extended_checks (modules)hang on the previous tip is not explained: the log stops whiletest_flatten_foldcompilescells.shader, before any debug agent exists in that process, and the same step passed on the tip before it and on Windows locally. The progress lines and watchdog above are there to name the program if it recurs.compare_pathcompares strings; VSCode and the bridge send backslashes); in stepping mode a breakpoint insidedap_add_onenever stops;stepOutstops at the next line of the same function.🤖 Generated with Claude Code