Are you the author? Sign in to claim
A debugger for delphi in visual studio code, and also a MCP server to give claude/copilot/etc authonomous delphi debuggi
Debug Delphi Win64 applications outside the Embarcadero IDE — a real debugger built on the Windows Debug API, written in Delphi.
The repository contains three programs that share one debugger engine:
| What it is | Where it lives | |
|---|---|---|
| Debug adapter | A Debug Adapter Protocol (DAP) server. This is the debugger itself: breakpoints, stepping, call stacks, variables, expression evaluation. Any DAP client can drive it. | VisualStudioCodeDelphiDebugger\ |
| VS Code extension | The client that makes it usable in the editor: the delphi-win64 debug type, the process picker for attaching, status-bar progress, and an editor for the exception rules. | install\local.delphi-win64-debug\ |
| MCP server | The same engine exposed to an AI agent over the Model Context Protocol — 33 tools (set_breakpoint, step_into, get_locals, evaluate_expression, get_call_stack, read_memory, …). It lets an agent run a program, stop it, and read its actual state instead of guessing from the source. | MCPDebugger\ |
The engine is shared: DebuggerCore\ holds the Windows Debug API loop, the
symbol readers (.rsm, TD32, .map, .dcp, JCL) and the expression evaluator.
The three front ends are thin.
You will almost certainly also want the Delphi IDE plugin. It adds an Open in VS Code command to the Delphi IDE that generates the workspace and the launch configuration for the current project: output paths, source root, unit search paths, and the package list for a BPL project. A real Delphi project carries a couple of hundred search paths, and nobody wants to write that by hand. The debugger works without it if you write
launch.jsonyourself, but the plugin is what makes it practical — install it first.
⚠️ Compile the program you want to debug with full debug information, or most of this will not work. A debugger can only show what the compiler emitted. In the Delphi project options, for the Win64 Debug configuration:
- Compiling → Optimization off, Debug information on, Local symbols on;
- Linking → Debug information on, Include remote debug symbols on (this is the
.rsm), and Map file: Detailed.On the command line that is
-$O- -V -VN -VR, which is whatDebugme.cfgin this repository uses. Measured on that project: those four flags alone already emit the.exewith its TD32 section, the.mapand the.rsm—-GD(Map file: Detailed) turned out not to be required for a.mapto appear, though it is what the IDE sets and it does no harm. Keep the.mapand.rsmnext to the.exe.To step into the RTL and VCL rather than over them, also enable Compiling → Use debug .dcus.
What each one buys you:
Artefact Without it TD32 section, .map, or JCL data — any oneno source lines: no breakpoints, no stepping .rsmbreakpoints and stepping still work, but local variables, types and expression evaluation are severely limited optimizations off breakpoints land on the wrong line and locals read as garbage, because the code no longer matches the source The same applies to every runtime package you want to step into: a BPL compiled without debug information stays a black box even when the host has full symbols.
Status: working and feature-rich. Launch and attach, breakpoints (line / conditional / hit-count / log-points), stepping (over/into/out) and set-next-statement, call stack with function names, variable inspection (locals, registers, globals, nested object / record / dynamic-array expansion) with type-aware formatting, a full Pascal expression evaluator for watch/hover/REPL,
setVariable(primitives, strings, enums, sets,varparameters), and a configurable exception engine (filters + per-exception rules) are all functional. See What works for the full list.
| Tool | Purpose |
|---|---|
Delphi 10.3 Rio or later (dcc64 in PATH after rsvars.bat) | Compiling the adapter and the debug target |
| VS Code | The editor the extension plugs into |
| Delphi IDE plugin | Generates the workspace and launch configuration from a Delphi project. Not strictly required, but see the note above |
DelphiLSP extension (embarcaderotechnologies.delphilsp) | Delphi language support in VS Code (syntax, autocomplete). A separate Embarcadero extension; this one only debugs |
| JCL sources (optional) | Only for the default JCL debug-info support; build with JCL_DEBUG_OFF to omit it — see Optional: JCL debug-info support |
Just want to use it? Do not build anything. Grab
delphi-win64-debugger-setup-*.zipfrom the latest release, extract it anywhere and runSetup.exe. The debug adapter, the MCP server and the VS Code extension are already compiled inside it, so no Delphi toolchain and no build step are needed on that machine. Windows will warn that the executables are unsigned — see the notes in the release.The rest of this section is for working on the debugger from a source checkout.
Assumes Delphi (Athens or later) is installed and dcc64 is on PATH after
rsvars.bat, plus VS Code.
This is the path for working on the debugger from a source checkout — it installs the extension in development mode, where VS Code launches the adapter straight from the compiler output, so rebuilding is enough to pick up changes (no copy step). To install the debugger only to use it, or to ship it to a machine without the sources, see the alternatives right after.
:: 1. Get the repository
git clone <repository-url> Win64Debugger
cd Win64Debugger
:: 2. Build the adapter and install the extension in DEVELOPMENT mode. It writes
:: a manifest whose "program" points directly at the build-output exe, so a
:: rebuild is reflected on the next debug session with no copy. Run once.
call install-dev.bat
:: 3. Build the bundled sample so there is something to debug. A fresh clone
:: has no .exe/.map/.rsm (they are gitignored), so build them once.
call rsvars.bat
dcc64 Debugme.dpr
Now open the project and start a session:
embarcaderotechnologies.delphilsp, from .vscode/extensions.json), accept.Win64DebuggerProj.code-workspace — open the workspace file, not
the folder; it wires up DelphiLSP and the compiler settings.Ctrl+Shift+P → Developer: Reload Window).Ctrl+Shift+D) pick "Debug Debugme (Delphi DAP)"
and press F5. You should hit the entry point and be able to step, set
breakpoints, and inspect variables.After a change to the adapter source, the edit/test loop is just:
Shift+F5) — the exe is locked while it runs.call build_dap.batF5 — VS Code relaunches the freshly built adapter. No re-install, no copy.To debug your own application instead, compile it with debug info
(-V -VN -VR, optimizations off) so it emits .map and .rsm next to the
.exe, then add a launch configuration — see
Launch configuration.
Install to use, not develop — to copy the adapter into the extensions folder (so it no longer depends on the checkout) run
build_installer.battheninstall\Install.exe. It is interactive: builds the adapter if needed, detects VS Code / VS Code Insiders, updates an existing install in place after confirmation. See VS Code extension setup.
Ship to another machine — to install on a machine without the repository or a Delphi toolchain, build a self-contained zip with
build_setup_zip.batand ship it — see Distributable zip.
call build_debug.bat
This initializes the Delphi compiler environment (rsvars.bat), compiles Debugme.exe (emitting its .map and .rsm), and compiles VisualStudioCodeDelphiDebugger.exe.
call build_dap.bat
Key flags that must be active when compiling the program you want to debug:
| Flag | Purpose |
|---|---|
-$O- | Disable optimizations |
-V -VN -VR | Generate debug info and .map file |
-DDEBUG | Optional conditional define |
-E.\Win64\Debug | Output directory |
For Debugme these are set in Debugme.cfg (one per line, without the leading -).
The adapter can additionally read JCL (JEDI Code Library) debug data — the
MAP-derived symbol table JCL stores either as a linked JCLDEBUG PE section or a
sidecar .jdbg file — as an address→location and procedure-name fallback for
modules that ship JCL data but no embedded TD32 / no .map. It is implemented in
DebuggerCore\JclDebugReader.pas and gated by the JCL_DEBUG conditional.
JCL_DEBUG is ON by default, which requires the JCL sources to be present.
The build configurations point at the default install location
C:\Athens\jcl\jcl\source (subdirectories windows, common, include); adjust
those paths if your JCL lives elsewhere. The affected configs are the adapter
VisualStudioCodeDelphiDebugger\VisualStudioCodeDelphiDebugger.cfg, the test
runner DebuggerTests\RunTests.cfg, and build_mcp.bat.
To build without JCL (e.g. JCL is not installed): compile with the
JCL_DEBUG_OFF conditional defined. This removes the uses JclDebug entirely, so
JCL need not be installed and the JCL search paths are ignored (they are harmless
when the define is off). Add the define to the relevant .cfg files, e.g.:
-DJCL_DEBUG_OFF
into VisualStudioCodeDelphiDebugger.cfg, DebuggerTests\RunTests.cfg, and pass
-DJCL_DEBUG_OFF on the dcc64 command line in build_mcp.bat. With the define
off, JclDebugReader compiles to a no-op factory (it never registers a provider);
everything else — TD32, .map, .rsm/.dcp symbol resolution — is unaffected.
If you build the adapter, MCP server, or test runner from the IDE (msbuild via the
.dproj) rather than these command-line scripts, apply the same choice there: add the JCL search paths (to keep it on) or theJCL_DEBUG_OFFdefine (to turn it off) in the project options, since an IDE build regenerates the.cfgfrom the.dproj.
The first time the debugger reads a module's .rsm or .dcp, it scans the whole
container to build a lookup index, then caches that index in a <file>.idx
sidecar next to it. The scan costs roughly half a second for a 45 MB package;
every later session reloads the sidecar in a few milliseconds instead.
The catch is when that scan happens: lazily, at a stop, on the thread the debugger answers requests on. Stopping somewhere that first touches several runtime packages therefore pauses the session while they are indexed.
The RTL and third-party packages never change between your builds, so their sidecars can be built once, offline:
cmd /c "C:\Athens\GitHub\Win64Debugger\DevTools\build_all.bat"
rem the Delphi DCP corpus - a few hundred files, one pass, a few minutes
DevTools\Win64\Debug\PrebuildIdx.exe "C:\Users\Public\Documents\Embarcadero\Studio\23.0\Dcp\Win64" -j 2
Add -force when sidecars already exist but were written by an older build of
this project: a change to the index format or to the parser leaves them stale,
and a stale sidecar is ignored, so the cold scan is paid again in every session
until they are rebuilt.
This does not help your own packages — you recompile those constantly, which invalidates their sidecars by design. If it is worth automating, run the tool over your output directory as a post-build step.
Source is DevTools\PrebuildIdx.dpr; DevTools\README.md documents the full
flag list, including -verify, which rebuilds every sidecar and compares it by
SHA-256 against the one already on disk (useful after touching a parser).
An automated integration suite lives in DebuggerTests\. It builds a dedicated
test target, launches the real DAP adapter against it, and asserts correctness of
breakpoints, stepping, locals, and the expression evaluator. Run the full suite
after any change to the adapter or the RSM/TD32 parsers.
:: build the target + the runner, then run everything
call DebuggerTests\build_and_run.bat
The scripts use cd /d %~dp0 internally, so they can be called from any working
directory. Finer-grained entry points:
| Script | Purpose |
|---|---|
DebuggerTests\build_and_run.bat | Build the test target and runner, then run the suite |
DebuggerTests\build_target.bat | Build only the debuggee (TestTarget.exe) |
DebuggerTests\build_runner.bat | Build only the test runner (RunTests.exe) |
DebuggerTests\run_tests.bat | Run the already-built tests |
Separate diagnostic tools (RSM/TD32 inspectors, PE dumpers) live in DevTools\;
see DevTools\README.md.
The extension lives in a local directory under VS Code's extension folder. It is not published to the marketplace.
After building the adapter (build_dap.bat), run:
call build_installer.bat
install\Install.exe
Install.exe stages the freshly built adapter next to the extension
manifest, detects your VS Code installation(s), and copies the extension
into %USERPROFILE%\.vscode\extensions\local.delphi-win64-debug\. If a
previous version is already installed it updates it in place (after
confirmation). Reload VS Code afterwards.
:: build the adapter and stage it into install\local.delphi-win64-debug\
call update-install.bat
:: copy the staged folder into your VS Code extensions directory
call install\install.bat
build_dap.bat).%USERPROFILE%\.vscode\extensions\local.delphi-win64-debug\.install\local.delphi-win64-debug\package.json and the built
VisualStudioCodeDelphiDebugger.exe
(VisualStudioCodeDelphiDebugger\Win64\Debug\) into it.Ctrl+Shift+P → Developer: Reload Window).The bundled package.json registers the delphi-win64 debug type and
points at the adapter via the relative path ./VisualStudioCodeDelphiDebugger.exe,
so no path editing is needed as long as the executable sits beside it.
The build, the staging step and the install each hold a copy:
| Stage | Location |
|---|---|
| Build output | VisualStudioCodeDelphiDebugger\Win64\Debug\VisualStudioCodeDelphiDebugger.exe |
| Staging (gitignored, inside the repo) | install\local.delphi-win64-debug\VisualStudioCodeDelphiDebugger.exe |
| Installed (what VS Code launches) | %USERPROFILE%\.vscode\extensions\local.delphi-win64-debug\VisualStudioCodeDelphiDebugger.exe |
To install the debugger on another machine that has neither the repository nor a Delphi toolchain, build a self-contained zip on a development machine:
call build_setup_zip.bat
It builds the adapter, builds the portable installer, and bundles everything into
dist\delphi-win64-debugger-setup-v<version>.zip:
Setup.exe ← portable installer / updater
local.delphi-win64-debug\ ← the extension (manifest + prebuilt adapter exe)
INSTALL_INSTRUCTIONS.md
On the target machine: extract the zip anywhere and run Setup.exe. It detects
the local VS Code installation(s) and copies the extension; if a previous
version is already installed it updates it in place. No build step is needed —
the adapter is already compiled inside the zip.
Setup.exe is the same program as install\Install.exe. It auto-detects its
context: with the adapter already staged beside it (the zip layout) it runs in
portable mode and installs the bundled files directly; inside the repository
it runs in repository mode and builds/stages from the build output first.
The installs above copy the adapter, which is right for distribution but
awkward while changing the adapter: every fix would need a rebuild plus a
re-copy. For development use install-dev.bat instead — it builds the adapter
(build_dap.bat) and then writes a package.json into the extensions folder
whose program points directly at the build-output executable (no copy).
On a fresh clone this single command is enough (no separate build step):
call install-dev.bat :: builds + installs; run once
After that, the edit/test loop for the adapter is just:
Shift+F5) — the executable is locked while it runs.call build_dap.batF5No re-install, no copy: VS Code relaunches the freshly built adapter.
The extension is a different matter, because VS Code loads its code from the
installed folder rather than from the repository. install-dev.bat mirrors the
staged extension files (extension.js, the webview assets, the manifest) into
that folder, so after changing any of them the loop is:
call install-dev.batA change to the extension's version is the one thing this cannot deliver. VS
Code registers an extension by id and version — the folder name and its own
registry both carry the version it was installed with, and only VS Code's
installer writes those. So the synced manifest deliberately keeps the registered
version and everything else in it (new commands, new launch properties) still
arrives; the script says so when the two differ. To move to a new version, run
install\Install.exe once, which installs the .vsix through VS Code and lets
it retire the previous folder itself.
Both modes use the same local.delphi-win64-debug folder, so switching is just a
matter of re-running install-dev.bat (dev) or install\Install.exe
(distribution copy).
| Mode | program points at | After a fix |
|---|---|---|
Install.exe (local distribution copy) | a copy in the extensions folder | rebuild + re-run the installer + reload |
build_setup_zip.bat (ship to another PC) | a copy from the extracted zip | rebuild the zip + re-run Setup.exe on the target |
install-dev.bat (development) | the build output directly | stop session + rebuild + F5 |
.vscode/launch.json for the project being debugged:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Delphi Win64",
"type": "delphi-win64",
"request": "launch",
"program": "${workspaceFolder}/Win64/Debug/MyApp.exe",
"mapFile": "${workspaceFolder}/Win64/Debug/MyApp.map",
"sourceRoot": "${workspaceFolder}",
"stopAtEntry": true
}
]
}
| Property | Default | Description |
|---|---|---|
program | (required) | Path to the .exe to debug |
mapFile | same path as program with .map extension | Delphi MAP file |
sourceRoot | (empty) | Root directory for source file lookup |
stopAtEntry | false | Break at the process entry point before any user code runs |
exceptionRules | (empty) | Per-exception rule table — see Exception handling |
useGlobalExceptionRules | true | Also load the shared machine-wide rules file — see Shared rules across projects |
globalExceptionRulesPath | %USERPROFILE%\.DelphiWinDebugger\exceptionRules.json | Custom location for the shared rules file |
Source files are resolved by searching sourceRoot and one level of subdirectories. If a source file appears in multiple locations, the first match wins.
The BREAKPOINTS view exposes four exception filters:
| Filter | Meaning |
|---|---|
delphi | First-chance Delphi-raised exceptions ($0EEDFADE). Accepts an optional condition: a comma-separated class-name list. |
av | First-chance access violations |
all | Every other first-chance exception (noisy) |
unhandled | Unhandled / second-chance exceptions (always on) |
When the debugger stops on an exception it reports both the class name and the
message (read from Exception.FMessage), e.g. EOracleError: ORA-00942: table or view does not exist. VS Code shows this in the exception widget and, via the
exceptionInfo request, in the details panel.
$exception)While stopped on a Delphi exception, the live exception object is available as a
synthetic $exception pseudo-variable:
Class: Message; click the chevron to expand it and inspect the class, Message,
and any fields/properties, exactly like a normal object.$exception (drag the Locals row into Watch, or type it),
then expand it. For a specific member, expand the object — e.g. open $exception
and read its Message child.$exception in the editor shows the same value.It is present only on an exception stop (it disappears on the next breakpoint/step
stop) and reflects the Delphi exception object; access violations have no Delphi
object, so $exception is not shown for them (the class/message summary still is).
exceptionRules)The four filters are coarse. For fine-grained control add an exceptionRules
array to the launch config. Each rule matches an exception on any combination of
criteria and selects an action. Rules are evaluated top-down, first match
wins; if no rule matches, the filter selection above decides (so existing setups
keep working).
Match criteria (all specified criteria must hold; omitted = wildcard):
| Field | Matches |
|---|---|
class | Exact match on the runtime class name, or an array of names (any-of), case-insensitive. Leaf-only — does not match descendants |
classIs | Matches the runtime class or any ancestor (Delphi is semantics), single name or array, case-insensitive — e.g. "classIs": "Exception" catches every Exception descendant |
code | The Win32 exception code, or an array of codes (any-of). Hexadecimal ("0xC0000005", "$C0000005") or decimal (3221225477, or the signed form -1073741819), as a JSON string or number |
message | Case-insensitive substring of the exception message |
messageRegex | Regular expression against the message (ignore-case) |
unit | Source unit where the exception was raised (OracleData or OracleData.pas). The special value "*unknown*" matches raises with no resolvable source |
line | Exact source line where raised |
lineFrom / lineTo | Inclusive source-line range |
Unit/line refer to the raise site — the first stack frame with known source. For a Delphi
raisethe OS exception address is inside the RTL, so the debugger walks to the first user frame; for access violations it is the faulting line.
codeis the only criterion that can match a native Windows exception. A native exception — one raised throughRaiseExceptionrather than by a Delphiraise— carries no exception object, so it has no class and no message: the Delphi criteria (class,classIs,message,messageRegex) can never match it, and withoutcodethe only way to silence one was to turn off the wholeallfilter. Typical cases are0xE06D7363(a C++ exception thrown inside a C++ DLL),0xC0000094(integer divide by zero) and any customer-defined code a third-party library raises. Delphi's own raise is0x0EEDFADEand an access violation is0xC0000005, socodeworks for those too.One native code needs no rule: the thread-name announcement
0x406D1388thatTThread.NameThreadForDebuggingraises is debugger protocol traffic, and the adapter consumes it before the rules run — it never surfaces as a stop, not even with theallfilter on.
Actions:
| Action | Effect |
|---|---|
ignore | Resume immediately — no stop, no log |
log | Write class: message to the debug console, then resume |
logStack | Like log, plus the formatted call stack |
break | Pause the debuggee in the debugger (the default behaviour) |
Examples:
{
"type": "delphi-win64",
"request": "launch",
"program": "${workspaceFolder}/Win64/Debug/MyApp.exe",
"exceptionRules": [
// EAbort is control-flow noise: never break on it.
{ "class": "EAbort", "action": "ignore" },
// Log every Oracle error (with stack) but keep running.
{ "messageRegex": "ORA-\\d+", "action": "logStack" },
// Break only for AVs raised inside a hot method's line range.
{ "class": "EAccessViolation", "unit": "OracleData",
"lineFrom": 2700, "lineTo": 2800, "action": "break" },
// Quietly log anything raised from code with no source mapping.
{ "unit": "*unknown*", "action": "log" },
// Catch-all: break on everything else.
{ "action": "break" }
]
}
// "Allow-list" style: ignore everything, break only on what you list.
"exceptionRules": [
{ "class": ["EMyDomainError", "EValidationError"], "action": "break" },
{ "action": "ignore" }
]
// classIs (inheritance): ignore EDatabaseError and every descendant.
"exceptionRules": [
{ "classIs": "EDatabaseError", "action": "ignore" }
]
// code: keep the "all first-chance" filter on while silencing native exceptions
// that are normal traffic for a third-party library. They have no class and no
// message, so only `code` can express this.
"exceptionRules": [
{ "code": "0xE06D7363", "action": "ignore" }, // C++ exception inside a C++ DLL
{ "code": ["0xC0000094", "0xC0000095"], "action": "logStack" } // int divide by zero / overflow
]
A machine-wide rules file lets you define a baseline once and have it apply to every project without per-project config. By default the adapter reads:
%USERPROFILE%\.DelphiWinDebugger\exceptionRules.json
It is a JSON object with an exceptionRules array (a bare array is also
accepted), using the same rule schema as above:
// %USERPROFILE%\.DelphiWinDebugger\exceptionRules.json
{
"exceptionRules": [
{ "class": "EAbort", "action": "ignore" },
{ "messageRegex": "ORA-\\d+", "action": "logStack" }
]
}
Precedence: a project's exceptionRules (in launch.json) are evaluated
first, then the shared file's rules — so a project can override the shared
baseline, and anything it doesn't address falls back to the shared rules (and
finally to the exception filters). Control it from launch.json:
| Property | Default | Description |
|---|---|---|
useGlobalExceptionRules | true | Load the shared rules file |
globalExceptionRulesPath | (the path above) | Use a custom shared-rules file location |
A missing or malformed shared file is ignored (it never breaks debugging).
The shared file is hot-reloaded: edit it while stopped on a breakpoint or
exception, then resume (continue / step) and the new rules apply to subsequent
exceptions — no need to restart the session. The adapter re-reads the file on
resume only when its timestamp changed, and logs a line to the debug console
when it does. (Project exceptionRules come from launch.json and are not
hot-reloaded.)
VS Code's native exception UI offers only on/off checkboxes for the four filters, so the extension ships a dedicated editor for the rule table:
Command Palette (Ctrl+Shift+P) → Delphi Win64: Edit Exception Rules...
This is the route that always works: it is contributed unconditionally, so it is there with no debug session, no open Delphi file and no launch configuration.
The same command also sits as a numbered-list icon in the title bar of the BREAKPOINTS section of the Run and Debug view — the section's own header, which is where its icons appear on hover, not the panel header above it — and in the CALL STACK section header during a session. Breakpoints is the primary home: VS Code removes the Call Stack section when no session is running and no launch configuration is selected, whereas Breakpoints stays as soon as one breakpoint exists or the workspace has been debugged once.
While stopped on an exception, Delphi Win64: Create a Rule for This Exception... also appears as a button in the floating debug toolbar, pre-filled
from the exception in front of you.
Pick a delphi-win64 launch configuration and the editor opens with one card
per rule, numbered in evaluation order. Each card separates the match
criteria from the action, and the up/down buttons reorder rules — order is
semantic, because the first match wins. Rules are validated as you type
(unknown fields, invalid actions, regular expressions that do not compile,
inverted line ranges); saving is blocked until the table is valid.
Save to launch.json replaces only the exceptionRules value of the selected
configuration; every other byte of the file — comments included — is left
untouched. The file is left open and unsaved so the change can be reviewed or
undone before it hits disk. If the workspace is not trusted the editor is
read-only and offers Copy JSON instead.
The same engine is exposed over the Model Context Protocol, so an agent can debug a Delphi program the way a developer does — run it, stop it, and read the real state — instead of inferring behaviour from the source.
cmd /c build_mcp.bat
powershell -ExecutionPolicy Bypass -File register-mcp.ps1 MCPDebugger\Win64\Debug\DelphiDebuggerMcp.exe
The script registers the server as delphi-win64-debugger with Claude Code (via
the claude CLI, user scope) and with VS Code by merging the user mcp.json.
It is idempotent, and -Unregister removes it. install\Install.exe offers the
same registration at the end of an install.
The 33 tools cover the debugging cycle:
| Group | Tools |
|---|---|
| Session | launch_debuggee, launch_from_config, attach_to_process, attach_from_config, list_debuggable_processes, detach_debugger, terminate_debuggee, stop_debugging, get_debug_session_status |
| Breakpoints | set_breakpoint, set_breakpoints, list_breakpoints, remove_all_breakpoints, set_exception_filters |
| Execution | continue_and_wait, step_over, step_into, step_out, pause_execution, wait_until_stopped |
| State | get_call_stack, get_threads, get_locals, get_variable, expand_variable, evaluate_expression, get_current_source_location, get_exception_details, get_compact_debug_snapshot |
| Memory | read_memory, write_memory |
| Output | get_debuggee_output, get_debugger_output |
launch_from_config and attach_from_config read the launch.json the IDE
plugin generated, so an agent starts a session with the project's real symbol
and source paths rather than a hand-built approximation.
get_compact_debug_snapshot returns location, stack and locals in one call,
which is usually what an agent wants after a stop and costs far fewer tokens
than three round trips.
VS Code AI agent (Claude Code, …)
│ DAP (JSON over stdin/stdout) │ MCP (JSON-RPC over stdin/stdout)
▼ ▼
VisualStudioCodeDelphiDebugger.exe DelphiDebuggerMcp.exe
│ │
└────────────────┬───────────────────────┘
▼
DebuggerCore\ ← the engine: debug loop, symbols, evaluator
│ Windows Debug API (CreateProcess, WaitForDebugEvent, INT3)
▼
the process being debugged
Two front ends, one engine. Neither owns any debugging logic of its own: they translate a protocol into calls on the same core, so a fix to stepping or to symbol resolution reaches both.
| Component | Description |
|---|---|
DebuggerCore\ | The debugger proper: Windows debug loop, symbol readers (.rsm, TD32, .map, .dcp, JCL), expression evaluator, exception rules |
VisualStudioCodeDelphiDebugger\ | DAP server: reads DAP from stdin, drives the engine |
MCPDebugger\ | MCP server: the same engine as 33 tools an agent can call |
install\local.delphi-win64-debug\ | VS Code extension: registers the delphi-win64 debug type, the attach picker and the exception-rules editor, and bundles the adapter |
Debugme.dpr | A small program used as a debug target while developing the debugger |
| File | Responsibility |
|---|---|
DebuggerCore\DapProtocol.pas | DAP framing: Content-Length header, JSON send/receive, logging |
VisualStudioCodeDelphiDebugger\DapServer.pas | DAP request dispatcher: launch, setBreakpoints, stackTrace, next, continue… |
DebuggerCore\DebugTarget.pas | IDebugTarget abstraction shared by the DAP layer and the evaluator |
DebuggerCore\Win64Debugger.pas | Windows x64 debug loop: WaitForDebugEvent, INT3 plant/remove, single-step, synthetic calls |
DebuggerCore\TD32FileReader.pas | Borland TD32 reader for the .debug PE section: source lines, symbols, types |
DebuggerCore\RsmFileReader.pas | Reverse-engineered Delphi .rsm reader for type / local / global metadata |
DebuggerCore\MapFileReader.pas | Parses Delphi .map files, translates RVA ↔ source line (nested-proc linkage, fallback) |
DebuggerCore\ExprEval.pas | Pascal expression grammar for watch / hover / REPL and conditional breakpoints |
JSON messages over stdin/stdout with HTTP-style Content-Length framing. VS Code sends requests; the adapter sends responses and events. The full spec is at https://microsoft.github.io/debug-adapter-protocol/.
CreateProcess with DEBUG_ONLY_THIS_PROCESS. The adapter loops on WaitForDebugEvent / ContinueDebugEvent. Breakpoints are planted as INT3 bytes; the original byte is restored on first hit and re-planted after single-step.
The Delphi compiler emits a .map file with segment base addresses and line-number tables. The MAP format uses segment-relative offsets (not RVAs from ImageBase), so this adapter parses the Start section to obtain each segment's base RVA, then adds the per-line offset to get the correct RVA.
At runtime the adapter compares the debuggee's actual ImageBase (from CREATE_PROCESS_DEBUG_INFO) against the PE preferred base to compute the correct virtual address for each symbol.
DEBUG_ONLY_THIS_PROCESS)request: attach, requires SeDebugPrivilege; auto-elevates when possible)stopAtEntry: true)F10), step into (F11), step out (Shift+F11)F5), pause (injected DebugBreakProcess)gotoTargets / goto)allThreadsStopped).map files (segment-relative offsets handled correctly).rsm files$ActRec fields expand like a normal object)begin..end blocktypeId)TDateTime, chars, strings, Variants, C-string pointers, dynamic arrays in [...] notation)TList<T> and TDictionary<...> expand and enumerate their elementsevaluateForHovers)is / as, Length / SizeOf / Ord / Low / High, enum / set literals, method and property calls (executed in the debuggee, raises aborted cleanly)setVariable for primitives, floats, dates, chars, sized integer writessetVariable for enums (by name / ordinal) and sets (by bitmask) at the correct storage widthsetVariable for strings via in-process @UStrAsg / @LStrAsg (no refcount leak)setVariable writes through the pointer for var parametersread_memory / write_memory)exceptionInfo details panel)$exception pseudo-variable: live exception object inspectable in Locals and Watchdelphi / av / all / unhandled, with per-class condition)exceptionRules): match on class / message / regex / unit / line, act with ignore / log / logStack / break%USERPROFILE%\.DelphiWinDebugger\exceptionRules.json) applied across projectssourceSearchPaths and the BDS env varDebugger features:
DEBUG_ONLY_THIS_PROCESS only)disassemble request)Variable / type system:
Project / packaging:
.map-free build the documented default (the .map is already optional at load time and a PE import-table reader exists; this is about dropping the dependency end to end).rsm + TD32, not .map: Delphi .map files
carry only line numbers and symbol addresses. Full variable/type inspection
is driven by the reverse-engineered .rsm file and the TD32 .debug PE
section, so a target must be compiled with -V -VN -VR to emit them. Without
the .rsm, breakpoints and stepping still work but variable inspection is
limited.DEBUG_ONLY_THIS_PROCESS is used. Child processes launched by the debuggee are not tracked.The adapter can write a timestamped log to %TEMP%\dap_adapter.log. It is
off by default; enable it per session with "diagnosticLog": true in the
launch configuration (or the DAP_LOG=1 environment variable). The log is
verbose — every DAP request/response and debug event — so use it only when
diagnosing adapter behavior.
MIT — see LICENSE.
The debugger depends on, but does not redistribute, third-party components:
JCLDEBUG / .jdbg line
information. Mozilla Public License 1.1 / GPL. Set JCL_ROOT (see
setpaths.bat) to point at your own checkout.DUNITX_ROOT.The .rsm and TD32 format notes in this repository are the result of
independent analysis of files produced by the Delphi compiler on the author's
own machine.
Run Claude Code as an MCP server so any agent can delegate coding tasks to it
Browser automation using accessibility snapshots instead of screenshots
Google's universal MCP server supporting PostgreSQL, MySQL, MongoDB, Redis, and 10+ databases
Official GitHub integration for repos, issues, PRs, and CI/CD workflows