Skip to content

Add MCP bridge for DAP debugging - #3937

Merged
borisbat merged 7 commits into
GaijinEntertainment:masterfrom
lookibed:codex/dap-mcp-bridge
Sep 5, 2026
Merged

Add MCP bridge for DAP debugging#3937
borisbat merged 7 commits into
GaijinEntertainment:masterfrom
lookibed:codex/dap-mcp-bridge

Conversation

@lookibed

@lookibed lookibed commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Behavior change on Windows: a daslang debuggee no longer hangs after the DAP client disconnects - src/misc/network.cpp changed, rebuild daslang.

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_checks lane describe and run the bridge.

Rebased onto current master. Building and testing the branch on Windows found five more defects, all fixed here. The .gitignore change un-ignored the whole .codex/ directory. Neither the bridge nor its harness looked for bin/daslang.exe, the Ninja layout. The harness read a bare DASLANG environment variable, which collides with unrelated shells; it is now DASLANG_DAP_BIN, matching DASLANG_MCP_BIN. The fixture's keep-alive loop of 1000 x sleep(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_msg and Server::tick read errno after 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 through last_socket_error() and socket_would_block().

src/misc/REVIEW.das is the executable half of that folder's checklist. It strips comments from network.cpp, locates the body of each helper by brace depth from its definition line, and fails when errno is read outside last_socket_error() (a write such as errno = 0 passes), when EAGAIN, EWOULDBLOCK, or WSAEWOULDBLOCK is named outside socket_would_block(), or when either helper is missing. Each finding carries the file, the line, and the helper to call. The checklist rule became "weakening REVIEW.das is a defect"; the mechanism lives in src/misc/ARCHITECTURE.md section 5. The tree-wide walker in extended_checks runs it with the other gates.

The darwin15 extended_checks lane hung once inside the deserialized whole-suite run with no per-program output to say where. The deserialized run now logs deser 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 cmake run_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

  • Windows, MSVC 2026 + Ninja, Release: tests/debug_agent 54/54; utils/dap/test_mcp_bridge.py passes in both instrumentation and DAS_TEST_STEPPING=1 modes (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; a disconnect right after configurationDone now lets the debuggee run to exit, where master spins forever (confirmed by a cdb stack dump: main thread in send under onCreateContext, tick thread blocked on the context mutex).
  • src/misc/REVIEW.das: 11 findings against master's network.cpp, green after the fix; utils/internal/review-md/all.das discovers it (20 gates; the one local red is the absent tree-sitter CLI).
  • dap.rst fix verified with docutils only; the Build doc lane re-proves it.
  • Author's Linux run before the rebase: both harness modes, tests/debug_agent 54/54, review-md --base HEAD, fix_md_ascii.py --check, the two examples/debugapi scripts, 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() treats EINTR as retry on POSIX; no test delivers a signal mid-send. A break would show as onError("can't send", EINTR) closing a healthy connection.
  • The build/daslang.exe candidate (MinGW or Ninja in build/) is not exercised; bin/daslang.exe is.

Not done

  • The darwin15 extended_checks (modules) hang on the previous tip is not explained: the log stops while test_flatten_fold compiles cells.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.
  • Pre-existing and unchanged, master identical: a breakpoint sent with forward-slash paths never matches on Windows (compare_path compares strings; VSCode and the bridge send backslashes); in stepping mode a breakpoint inside dap_add_one never stops; stepOut stops at the next line of the same function.
  • Executable lint and JIT lanes, the small C++ test lane, and the AOT matrix are left to GitHub Actions.

🤖 Generated with Claude Code

lookibed and others added 4 commits September 5, 2026 06:04
…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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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 .das regression fixtures covering callback locking, cancellation, duplicate/destroyed contexts, framing, and stepping.
  • C++ runtime fixes: threadlock_context-serialized debug-agent callbacks (lazy contextMutex), generation-based debugger-worker lifecycle in module_builtin_debugger.cpp/module_builtin_jobque.cpp, and portable socket-error helpers in network.cpp.
  • daslib/debug.das breakpoint-lookup refactor (release table borrow before pausing), explicit pinvoke readiness query, onSimulateContext worker 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.

borisbat and others added 3 commits September 5, 2026 07:39
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>
@borisbat
borisbat merged commit c18bfab into GaijinEntertainment:master Sep 5, 2026
32 checks passed
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.

3 participants