Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Delphi Win64 Debugger

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.json yourself, 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:

  • CompilingOptimization off, Debug information on, Local symbols on;
  • LinkingDebug 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 what Debugme.cfg in this repository uses. Measured on that project: those four flags alone already emit the .exe with its TD32 section, the .map and the .rsm-GD (Map file: Detailed) turned out not to be required for a .map to appear, though it is what the IDE sets and it does no harm. Keep the .map and .rsm next to the .exe.

To step into the RTL and VCL rather than over them, also enable CompilingUse debug .dcus.

What each one buys you:

Artefact Without it
TD32 section, .map, or JCL data — any one no source lines: no breakpoints, no stepping
.rsm breakpoints 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, var parameters), and a configurable exception engine (filters + per-exception rules) are all functional. See What works for the full list.


Prerequisites

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

Quick start

Just want to use it? Do not build anything. Grab delphi-win64-debugger-setup-*.zip from the latest release, extract it anywhere and run Setup.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:

  1. If VS Code prompts to install the recommended DelphiLSP extension (embarcaderotechnologies.delphilsp, from .vscode/extensions.json), accept.
  2. Open Win64DebuggerProj.code-workspace — open the workspace file, not the folder; it wires up DelphiLSP and the compiler settings.
  3. Reload the window (Ctrl+Shift+PDeveloper: Reload Window).
  4. In Run and Debug (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:

  1. Stop the running debug session (Shift+F5) — the exe is locked while it runs.
  2. call build_dap.bat
  3. F5 — 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.bat then install\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.bat and ship it — see Distributable zip.


Build

One-shot: build everything

call build_debug.bat

This initializes the Delphi compiler environment (rsvars.bat), compiles Debugme.exe (emitting its .map and .rsm), and compiles VisualStudioCodeDelphiDebugger.exe.

Adapter only (faster iteration)

call build_dap.bat

Compiler flags for the debug target

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 -).

Optional: JCL debug-info support

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 the JCL_DEBUG_OFF define (to turn it off) in the project options, since an IDE build regenerates the .cfg from the .dproj.

Optional: pre-build the symbol-index sidecars

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).


Tests

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.


VS Code extension setup

The extension lives in a local directory under VS Code's extension folder. It is not published to the marketplace.

Recommended: the installer

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.

Scripted alternative

:: 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

Manual

  1. Build the adapter (build_dap.bat).
  2. Create the folder %USERPROFILE%\.vscode\extensions\local.delphi-win64-debug\.
  3. Copy install\local.delphi-win64-debug\package.json and the built VisualStudioCodeDelphiDebugger.exe (VisualStudioCodeDelphiDebugger\Win64\Debug\) into it.
  4. Reload VS Code (Ctrl+Shift+PDeveloper: 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.

Where the adapter executable ends up

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

Distributable zip (no repo, no Delphi on the target)

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.

Development install (fast iteration)

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:

  1. Stop the running debug session in VS Code (Shift+F5) — the executable is locked while it runs.
  2. call build_dap.bat
  3. F5

No 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:

  1. call install-dev.bat
  2. Developer: Reload Window in VS Code

A 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

Launch configuration

.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.


Exception handling

Filters and what is shown

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.

Inspecting the current exception ($exception)

While stopped on a Delphi exception, the live exception object is available as a synthetic $exception pseudo-variable:

  • Variables panel — it appears at the top of the Locals scope. Its value is Class: Message; click the chevron to expand it and inspect the class, Message, and any fields/properties, exactly like a normal object.
  • Watch — add an entry $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.
  • Hover — hovering $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).

Per-exception rules (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 raise the OS exception address is inside the RTL, so the debugger walks to the first user frame; for access violations it is the faulting line.

code is the only criterion that can match a native Windows exception. A native exception — one raised through RaiseException rather than by a Delphi raise — carries no exception object, so it has no class and no message: the Delphi criteria (class, classIs, message, messageRegex) can never match it, and without code the only way to silence one was to turn off the whole all filter. Typical cases are 0xE06D7363 (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 is 0x0EEDFADE and an access violation is 0xC0000005, so code works for those too.

One native code needs no rule: the thread-name announcement 0x406D1388 that TThread.NameThreadForDebugging raises is debugger protocol traffic, and the adapter consumes it before the rules run — it never surfaces as a stop, not even with the all filter 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
]

Shared rules across projects

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.)

Editing rules from the UI

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.


MCP server: debugging from an AI agent

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.


Architecture

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

Key source files

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

Technology notes

DAP protocol

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/.

Windows Debug API

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.

Delphi MAP files

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.


What works

  • Launch (Windows Debug API, DEBUG_ONLY_THIS_PROCESS)
  • Attach to a running process (request: attach, requires SeDebugPrivilege; auto-elevates when possible)
  • Stop at entry point (stopAtEntry: true)
  • Breakpoints in source files (set from VS Code gutter)
  • Conditional breakpoints, hit-count breakpoints, and log-points
  • Step over (F10), step into (F11), step out (Shift+F11)
  • Continue (F5), pause (injected DebugBreakProcess)
  • Set next statement (DAP gotoTargets / goto)
  • Current source line highlighted in editor
  • Multi-module debugging: DLLs and runtime-loaded BPLs (lazy per-module symbol loading)
  • All threads enumerated (with names); the whole process stops together (allThreadsStopped)
  • Per-thread inspection: selecting any thread shows that thread's own call stack, and its frames' locals/watches are inspectable (read-only)
  • RVA/address resolution from Delphi .map files (segment-relative offsets handled correctly)
  • ASLR: actual ImageBase vs. preferred base handled at runtime
  • Call stack with function names (frame unwinding beyond the current frame)
  • Local variable inspection from reverse-engineered Delphi .rsm files
  • Parent-procedure locals visible from inside nested procedures
  • Anonymous-method parameters and closure-captured variables inspectable (the captured $ActRec fields expand like a normal object)
  • Locals declared in the program's main begin..end block
  • Global variable inspection (with decoded typeId)
  • Registers scope in the Variables panel
  • Type-aware value formatting (integers, floats, TDateTime, chars, strings, Variants, C-string pointers, dynamic arrays in [...] notation)
  • Object / record / dynamic-array expansion in the Variables tree (all fields, all ancestor classes, nested)
  • Generic collection inspection: TList<T> and TDictionary<...> expand and enumerate their elements
  • Hover data tips in the editor (evaluateForHovers)
  • Full Pascal expression evaluator: qualified identifiers, arithmetic / comparison / boolean / set ops, string concat, indexing, type casts, 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 writes
  • setVariable for enums (by name / ordinal) and sets (by bitmask) at the correct storage width
  • setVariable for strings via in-process @UStrAsg / @LStrAsg (no refcount leak)
  • setVariable writes through the pointer for var parameters
  • Read and write raw debuggee process memory (MCP read_memory / write_memory)
  • First-chance exception break (continue past handled exceptions)
  • Exception class name and message shown on stop (exceptionInfo details panel)
  • $exception pseudo-variable: live exception object inspectable in Locals and Watch
  • Exception filter UI (delphi / av / all / unhandled, with per-class condition)
  • Per-exception rule engine (exceptionRules): match on class / message / regex / unit / line, act with ignore / log / logStack / break
  • Shared machine-wide exception rules (%USERPROFILE%\.DelphiWinDebugger\exceptionRules.json) applied across projects
  • Accurate breakpoint verification state reported back to VS Code
  • Delphi RTL/VCL source resolution via sourceSearchPaths and the BDS env var

Roadmap / not yet implemented

Debugger features:

  • Child process tracking (currently DEBUG_ONLY_THIS_PROCESS only)
  • Disassembly view (DAP disassemble request)
  • 32-bit (Win32) targets

Variable / type system:

  • Broader generic type coverage beyond the common collection classes
  • Pretty-printing for more RTL types beyond what's already handled

Project / packaging:

  • Make a .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)
  • Publish the VS Code extension to the marketplace instead of the local install

Known limitations

  • Type information comes from .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.
  • Single process: only DEBUG_ONLY_THIS_PROCESS is used. Child processes launched by the debuggee are not tracked.
  • Whole-process stops: the process stops as a unit — there is no "freeze one thread and let the others run". Stepping does target the thread you selected (the others are suspended for the duration of the step), and any thread's call stack and variables can be inspected.
  • x64 only: the Win64 calling convention and stack layout are assumed throughout.

Diagnostic logging

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.


License

MIT — see LICENSE.

The debugger depends on, but does not redistribute, third-party components:

  • JEDI Code Library (JCL) — used to read JCLDEBUG / .jdbg line information. Mozilla Public License 1.1 / GPL. Set JCL_ROOT (see setpaths.bat) to point at your own checkout.
  • DUnitX — the integration test suite only. Apache License 2.0. Set DUNITX_ROOT.
  • Embarcadero Delphi RTL/VCL — required to build; covered by your own Delphi licence.

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.

About

A debugger for delphi in visual studio code, and also a MCP server to give claude/copilot/etc authonomous delphi debugging capabilities

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages