---
name: delphi-debugging-workflow
description: Use when the user wants to debug a Delphi application — anything mentioning .pas, .dpr, .dproj, "Delphi", "rdbg", "debug this exe", "step through", or attaching to a Delphi process. Covers the daemon handshake, attach vs launch decision, breakpoint setting (source, module!symbol, address), the pause-loop vs request-loop distinction, and the event-pump rhythm.
---

# Delphi debugging workflow with rdbg

You drive a Delphi program through the rdbg debugger via MCP tools.
A daemon — `rdbgd64.exe` for Win64 targets, `rdbgd32.exe` for Win32
targets — is doing the OS-level Win32 debug API work; you tell it
what to do over JSON-RPC. The MCP bridge auto-switches daemons per
`launch` based on the target's PE machine type, so you don't have
to pick. `hello` reports `target_arch: "x64"` or `"x86"` so you
know which ABI is in play. Most state-change tools require the
target to be **paused** — issuing them while the target is running
returns an error.

## Confirm the daemon is up

Before any debugging, call `hello`. It returns daemon version,
protocol version, `active_sessions`, AND a `daemons` object showing
both x64 and x86 daemons' status (`{running, port, version,
target_arch, active_sessions}`). The bridge probes both ports per
call so a single `hello` reveals when the user has rdbgd32 up but
not rdbgd64 (or vice versa) — useful before launching when the
target's bitness might not match the connected daemon.

If `hello` errors or shows the daemon you need is down, call
`bridge_config` to learn why: it returns the bridge's full effective
config (host, ports, daemon paths, `auto_start` state, current
target arch, interactive-session flag, plus the live cross-bitness
probe results). Three common diagnostic patterns:

- `auto_start: false` AND target daemon's `running: false` → user
  needs to start the daemon manually (or set `auto_start_daemon`
  in plugin config).
- `auto_start: true` BUT `daemon_paths.x64: ""` (or x86) → bridge
  knows it should auto-start but has no binary path; user needs
  to set the path in plugin config.
- `interactive_session: false` → bridge running outside an
  interactive desktop session; auto-spawn refuses (the daemon
  needs DirectDraw/GDI from the user's RDP). User must launch
  Claude Code from inside their RDP.

Don't repeatedly call `hello` if it failed once — call
`bridge_config` once to diagnose, then surface a clear message to
the user including which daemon is missing and what to do.

## Attach vs launch

- **`attach`** when the target is already running. Take `pid` from
  `list_processes` (filter by `name_match`).
- **`launch`** when starting fresh. Pass `exe` (full path) and
  optional `args`. The daemon waits at the loader breakpoint
  (`paused` event with `reason: initial_loader_breakpoint`); set
  any breakpoints you need, then `continue`.

Either way, an `attach` succeeds with a `paused` event. **Do not
issue another control command before draining that event** — the
target is paused waiting for you.

## Setting breakpoints

`set_breakpoint` takes a `kind`:

- `kind: "source"`, `source: "Unit1.pas", line: 42` — source-line
  breakpoint, resolved via the exe's `.map` file. Delphi compiles
  with `{$D+}` and `dcc64 -GD` produce a usable map.
- `kind: "symbol"`, `symbol: "kernelbase!RaiseException"` — a
  module!symbol breakpoint. PDBs are downloaded from
  msdl.microsoft.com on first use and cached at
  `%LOCALAPPDATA%\rdbg\symbols`. Use this to catch every Delphi
  raise *before* any handler runs.
- `kind: "address"`, `address: 0x...` — raw address. Useful when
  you've stepped to an address you want to revisit.

Add `kind: "hw"` (hardware breakpoint, DR0–DR3) when setting BPs
on read-only pages or system DLLs that can't be patched. Limited
to four hardware slots.

`list_breakpoints` shows what's set; `clear_breakpoint` removes by
`bp_id` or `address`.

## Stepping and the event pump

After every `continue`, `step_over`, `step_into`, or `run_to`,
**immediately call `events(timeout_ms=...)`** to drain the next
event. The target is now running and *will* push an event when it
stops (`paused`, `breakpoint_hit`, `exception`, `exit_process`).
Issuing another control command before draining the event causes
the request to block or fail.

Choose a timeout based on what you're waiting for:

- `300` ms for "next instruction" steps.
- `5000` ms for "continue until breakpoint."
- `30000` ms for "let the user demonstrate something."

If `events` times out without an event, the target is still
running. Either `interrupt` (forces a pause) or extend the
timeout.

## Reading state at a pause

Once paused:

- `get_registers` — GP register dump + `eflags`. Field names follow
  the target ABI: `rip`/`rsp`/`rbp`/`rax-r15` on x64, `eip`/`esp`/
  `ebp`/`eax-edi` on x86. The response includes `arch` so you can
  branch. The instruction pointer (rip/eip) combined with the map
  file lookup tells you the current source line.
- `get_self` — returns the `Self` object at the current frame. The
  daemon probes the ABI's first-arg register (RCX on x64, EAX on
  x86) plus a stack scan; you don't pick.
- `inspect_object(address, max_depth=3)` — walks any Delphi
  object's RTTI. Returns class hierarchy, fields, published props,
  methods, and pointer values for further walking. **For typical
  "what's in this exception" questions pass `include_methods=false`**
  to drop ~3KB of method listings the agent rarely needs;
  `methods_from_leaf_only=true` keeps just the leaf class's methods.
- `read_param(index, kind?)` — reads the calling-convention
  argument register for the paused thread without needing TD32.
  `index=0` is the first arg; on x64 that's `RCX`, on x86
  (Delphi register) that's `EAX`. When the question is "what was
  the bad input?" (e.g. paused inside `StrToInt('abc')`),
  `read_param(0, kind: "string")` returns the decoded string in
  one call — no manual `get_registers` + `read` chain needed.
- `inspect_locals(frame_idx=0)` — returns the parameters and
  stack-locals for the procedure containing the paused frame's RIP.
  Source comes from the EXE's `.debug` TD32/TDS section, so the
  target must be built with `-V` (Project > Options > Linking >
  "Place debug information in EXE"). Auto-walks past the
  `kernel32!DbgBreakPoint` frame at `DebugBreak()` to land on user
  code; `auto_walked_frames` in the response reports how many
  frames were skipped (positive = via RBP chain, negative = via
  RSP scan, zero = immediate frame was already user code).
  Pair with `evaluate("LocalName.Field")` to chase further.
- `read(kind: "string", address)` — materialize a Delphi `string`
  at an address surfaced by `inspect_object`.

### When `inspect_locals` returns an error

The response includes a `version_warning` field plus a
`fallback_hint` and `diagnostic_command` so you can tell the user
clearly what went wrong:

- `message: "no_debug_section"` — target wasn't built with `-V`.
  Tell the user to enable "Place debug information in EXE" and
  rebuild; pivot to `inspect_object` / `evaluate` for what you
  can still inspect.
- `message: "unsupported_tds_version"` with
  `version_warning: "unsupported_tds: FB0X"` — Embarcadero shipped
  a new TDS format and rdbg hasn't been taught it yet. Surface
  the warning verbatim and pivot to `inspect_object` /
  `evaluate`. The `diagnostic_command` (typically
  `rdbgd64.exe --dump-tds <exe>`) is the bug-report payload.
- `message: "tds_corrupt"` — section magic recognised but layout
  failed validation. Treat the same as `unsupported_tds_version`:
  surface and pivot.

## Common pitfalls

- **Don't issue a control command while a previous step is
  in-flight.** The pump blocks; surface the issue rather than
  waiting indefinitely.
- **Map file vs PDB.** Delphi exes use `.map`; system DLLs use
  PDB. Both are handled transparently for breakpoints, but
  only `.map`-backed code surfaces `source`/`line` in `paused`
  events. Frame walks through system DLLs show `module!symbol+0xNN`
  until they hit user code again.
- **Single-client daemon.** Only one connection at a time. If a
  second client is holding the daemon, your `attach` will sit on
  the accept queue. `/rdbg:status` flags this.
