rdbg ships as three binaries — a daemon, a CLI, and a GUI — that all share one wire protocol. Here's what each layer brings.
The daemon (rdbgd64.exe for Win64 targets, rdbgd32.exe for Win32 targets) launches your target under DEBUG_PROCESS and pumps the debug event loop in-process. No middleman; the wire protocol is identical between the two flavours.
Every operation — launch, set_breakpoint, inspect_object, step_over — is one JSON object per line over TCP. Trivial to drive from any language.
Resolve source:line via the loaded MAP, write 0xCC, handle the hit with restore-byte / rewind-RIP / trap-flag single-step / re-patch. Re-arm survives stepping right after a hit.
Up to four DR-slot HW breakpoints alongside SW BPs. hw: prefix on a BP spec routes to the next free debug register. Address forms accepted: source:line, address, or module!symbol (resolved via PDB). Code BPs today; data watchpoints (read/write watches via DR slots) planned.
Hand-rolled MAP-file parser plus a cross-process x64 RBP-chain walker. Every frame resolves to unit.pas:line symbol+offset for the launched exe, and to module!Symbol+offset for any DLL the target loaded — including kernel32, ntdll, user32, etc. PDBs are auto-fetched from Microsoft's public symbol server on first use and cached at %LOCALAPPDATA%\rdbg\symbols. Pure-Delphi MSF parser; no dbghelp.dll redistribution.
Trap-flag single-stepping with line-aware step-over: loops the TF step until RIP's MAP resolution moves to a different source:line, so CALL instructions step through cleanly.
Given a target VA pointing at a Delphi object, walks the VMT chain returning class name + instance size for every ancestor up to TObject. Self-calibrates for the RTL build.
Walks the entire props list cross-process: ordinals (any width), Int64, all float flavours, every string flavour, tkClass (recurses), classref, pointers, interfaces, variants, dyn / static arrays.
One wire call: evaluate with expr:"Self.Caption" or "Form.Edit1.Text" or "0x<hex>.ComponentCount". Pascal-style path resolution — dotted member access, both field- and method-backed, chained any depth, with cross-process RPC for property getters carried through transparently. Same return shape as inspect_object's per-prop entries, so existing renderers work.
inspect_locals returns parameter and stack-local information for the procedure containing the paused frame's RIP. Reads the EXE's .debug PE section — Borland's CodeView 4 derivative (magic FB09) — that Delphi emits when you build with -V ("Place debug information in EXE"). Decodes both stack-spilled and register-allocated parameters; resolves names through the global names table; demangles procedure signatures via a tight Itanium-ABI-style parser (Borland's variant). Auto-walks past kernel32!DbgBreakPoint at DebugBreak() to land on user code.
eval_method_props:true hijacks the paused thread and calls each property getter on a 64 KB scratch stack so values come back live — TForm.Caption = 'MainWindow', TStrings.Count = 42, virtual getters dispatch through the VMT, managed-result strings/records decode out of a hidden out-param slot. Per-prop AV failures surface data_addr + op (read/write/execute) so an agent sees which pointer was bad without parsing EXCEPTION_RECORD. Methods array can be suppressed (include_methods:false) or scoped to the leaf class only (methods_from_leaf_only:true) to cut ~3 KB of noise on triage calls.
read_param)When the target wasn't built with -V, inspect_locals returns no_debug_section. read_param(index, kind?) is the escape hatch: reads the calling-convention argument register for the paused thread — RCX/RDX/R8/R9 on x64, EAX/EDX/ECX on x86 (Delphi register convention), with stack-slot fallback for index ≥ 4 (or ≥ 3). Optional kind:"string" decodes a Delphi string at the value via the same primitives read uses, so paused inside StrToInt('abc') → read_param(0, kind:"string") → {value:"0x...", decoded:{text:"abc",...}} in one call.
Every entry in an exception event's frames[] now carries a kind: user (target's main exe), system (Windows DLLs — kernelbase, ntdll, user32, gdi32, ucrtbase, etc.), rtl (Delphi runtime packages, *.bpl), or unknown. Agents filter for kind == "user" to find the user-code frame instantly without re-implementing module-name heuristics. Combined with the existing MAP+PDB resolution, the first user frame typically carries source + line — the file:line you'd put in a bug report.
hello returns both daemons' status in one call ({x64:{running, port, version, ...}, x86:{...}}) so an agent sees the wrong-bitness situation before launch would have failed. The new bridge_config tool exposes the bridge's effective config — ports, daemon paths, auto_start state, current target arch, interactive-session flag, and LSP-side state — so "why didn't auto_start fire?" is one tool call, not a stderr expedition. Stale paused events are scrubbed from the queue before continue/step_*/run_to/set_rip/detach/kill_session, eliminating the loader-BP-event-still-queued footgun.
The bridge embeds Embarcadero's DelphiLSP.exe as a child process and surfaces its standard LSP methods as MCP tools, capability-gated per Delphi version (11.3 → 13.1). Claude can navigate Delphi source the way an IDE does — without grepping — alongside the runtime-debug surface in the same plugin.
Navigation: lsp_definition, lsp_declaration (interface section), lsp_implementation (jumps from interface decl → body), lsp_hover, lsp_references (server-backed when advertised). Authoring: lsp_completion (with trigger-char metadata), lsp_signature_help. Outline: lsp_document_symbols (hierarchical), lsp_workspace_symbol (server-backed when available; per-file documentSymbol aggregation fallback otherwise with precision:"approximate"). Diagnostics: lsp_diagnostics (pull-style snapshot of cached publishDiagnostics; per-URI wait-event lets callers block briefly for first diagnostics after didOpen). Project lifecycle: lsp_open_project bootstraps a channel and sets it as the session's current default. Aggregators: lsp_explain (hover + definition + references in one call), lsp_outline_workspace.
.delphilsp.json autogen from .dprojDelphiLSP's project-config requirement is the biggest first-time-user cliff. The bridge auto-generates a .delphilsp.json from any .dproj by parsing dproj XML + reading registry library paths + walking $(BDS)\source for browsingPaths. No "tick the IDE checkbox, close, reopen" dance — point the bridge at a .dproj and the LSP child gets a working project model on the first call. If you'd rather use an IDE-generated config, drop one next to the dproj and the bridge prefers a fresh existing sibling over autogen. Disable autogen entirely with RDBG_LSP_NO_AUTOGEN=1.
One DelphiLSP.exe child per .delphilsp.json, keyed by canonical (lowercased absolute) project path. Per-call resolution priority: explicit project arg → walk-up from the tool's file arg looking for sibling .delphilsp.json or .dproj → current default set by lsp_open_project or RDBG_LSP_PROJECT. Idle channels are reaped after 5 minutes (configurable); the current default is exempt so the typical "one project at a time" workflow never pays a respawn. Group-project workspaces switch via lsp_open_project.
--lsp mode for non-Claude clientsLaunch rdbg-mcp.exe --lsp and the bridge speaks raw LSP over stdio instead of MCP — VS Code, Neovim, Zed, or any LSP client can connect. Initialize-time project resolution + autogen + multi-version DelphiLSP discovery all carry through; non-Claude users get the same zero-config Delphi LSP server Claude Code users get. The bridge runs as a transparent pump after initialize: client requests forward verbatim with their original IDs; server responses + notifications stream back via the channel's reader thread. License-error translation also applies, so a non-interactive session shows the actionable bridge-level message instead of bare "valid license not found."
VCL Win64 native. Three columns, tabbed source viewer, project tree, structured event list. Built with the same wire that everything else speaks.
Open a .dproj, hit Go: connect → launch → install breakpoints → continue past loader BP, all in one click. Project sidecar persists target host:port and default BPs.
Local spawns the daemon as a child. SSH tunnels through ssh -L and runs the daemon on the remote box. Attach connects to a daemon you started yourself.
On Open Project, ValidateDproj checks the filesystem for missing .exe, missing .map next to the exe, non-Win64 platform, non-Debug config. Blocking issues gate Go.
An inspect_object response renders as a flat row table with Inspect-> buttons on every tkClass row. Multiple windows coexist, F5 refreshes, breadcrumb back to parent.
Owner-drawn line-number gutter + marker column: open circle for MAP-resolvable lines, filled red for set BPs. Click to toggle. Auto-navigates to the top frame on every break / step.
Disconnected / connected / running / paused. Every menu and toolbar entry knows when it's relevant; only the verbs that make sense at any given moment are enabled.
The CLI is a one-shot driver. The wire is the real interface — agents, scripts, and CI all live there.
rdbgc ships with five canonical verbs that cover most workflows:
launch — spawn the target, stream events to stdout.debug — one-shot "set BPs, print every hit".inspect — auto inspect_object on every exception.list — enumerate windows, threads, breakpoints.send — cross-process input injection.The wire's pause-loop mental model maps cleanly to autonomous tools. The daemon blocks on every BP hit; an agent reads the event, walks the stack, decides a next step, and sends continue.
Multi-conn daemon since v0.19.19 — an agent can drop the connection between probes without killing the target, and multiple agents can drive the same daemon concurrently. Zombie sessions survive disconnect; a fresh client adopts a paused target via bind_session.
Five new wire methods that let agents and tooling click through a Delphi VCL UI without a human in the loop.
Enumerate top-level + child HWNDs in the target process. Class name, caption, rect, owner.
Fire WM_* into a target HWND, or synthesize raw key/mouse input through SendInput on the daemon side.
Type a string into the focused control, or replay an ordered sequence with optional dwell times.
Synthesize a mouse click at a window-relative coordinate, optionally focusing the HWND first.
Bump a counter on every major window event so clients know when to re-enumerate.
Synchronous-return wire method for messages whose return value matters (e.g. RM_GetObjectInstance).
rdbg is opinionated about scope. These are explicit non-goals for v1.x — awareness, not regrets.
rdbgd64 debugs Win64 targets, rdbgd32 debugs Win32 targets. Each daemon stays in its own bitness; debugging a Win32 target from a Win64 daemon is intentionally not supported.
ptrace; that's a different project.
evaluate tool covers Pascal-style dot-paths (Self.Caption, Form.Edit1.Text) including method-backed property calls. Indexing (Components[0]), arithmetic, type casts, and method calls with args still fail with parseable error reasons; agents branch on those rather than getting silent wrong values.
Pick up the binaries, or follow the 5-minute quickstart from a console window.