MCP and AI agents

An AI coding agent reading your source can tell you what the code should do. It cannot tell you what the program did. The MCP server closes that gap: the same debug engine the VS Code extension drives, exposed to an agent as around forty tools, so it can launch your program, stop it, and read the real state.

Why an agent wants this

Ask an agent why a value is wrong and, without a debugger, it reasons backwards from the source — plausibly, confidently, and sometimes incorrectly, because the answer depended on runtime state it could not see. Give it a debugger and the same question becomes a breakpoint, a stop, and a look at the actual variable.

This matters most exactly where reading the source is hardest: a value corrupted by something several call frames away, an exception raised inside a runtime package, a failure that only reproduces on the third iteration of a loop. Those are the cases where an agent guessing is worse than useless, and where stopping the program and looking takes one round trip.

How it fits together

DelphiDebuggerMcp.exe is a separate program from the debug adapter, but not a separate debugger. Both are thin frontends over the same TDebugSession facade and the same Windows Debug API engine — everything below that line is shared, so an agent and the editor are looking at the same debugger.

flowchart LR
  A["AI agent
(Claude Code, VS Code, …)"] -- "MCP tool call" --> M["DelphiDebuggerMcp.exe"] M --> S["TDebugSession
(the same engine VS Code drives)"] S -- "Windows Debug API" --> P[("Your Delphi program")] P -- "stop event" --> S S --> M M -- "structured result" --> A

Results come back as structured data, not as screen output an agent has to parse. DAP and MCP frontends covers the shared machinery in detail.

The shape of a session

The typical cycle, which is worth knowing because it explains the tool names:

  1. launch_debuggee or launch_from_config — the latter reads the same launch.json the VS Code debugger uses, so the agent starts with the project’s real symbol and source paths instead of a hand-built approximation. Both stop at entry.
  2. set_breakpoint, while parked at entry.
  3. continue_and_wait — runs, and returns when the program next stops.
  4. get_compact_debug_snapshot, or the individual get_call_stack / get_locals / evaluate_expression calls.
  5. Step, continue, or look somewhere else — then repeat.
  6. terminate_debuggee, or detach_debugger if you attached.
Set breakpoints before the first continue_and_wait, while the session is parked at entry. Once running, the session is event-driven — there is no mid-flight “pause and set” convenience, though pause_execution exists if you genuinely need to interrupt.

get_compact_debug_snapshot deserves its own mention: state, stop reason, current location, top frames, top-frame locals and exception information in a single call. After a stop that is usually everything the agent wanted, at a fraction of the tokens three separate round trips would cost.

Where it differs from the VS Code experience

The engine is shared, but the two frontends do not expose identical surfaces. What is available in VS Code and not over MCP:

  • Set Next Statement has no MCP tool.
  • There is no setVariable equivalent. Only write_memory and set_register mutate the debuggee’s state.
  • A hardware watchpoint on a bare local variable is refused over MCP, with a reason — watch the containing global or object field, or pass an address you already have. In VS Code, watching a local is supported and withdrawn automatically when the frame dies.
  • expand_variable covers class fields, records and dynamic arrays; getter-backed properties and Variant arrays are not expanded through that path.

One design difference rather than a gap: there is deliberately no get_scopes or get_arguments. Locals come back directly, with opaque handles for anything expandable, because arguments are not reliably separable from locals in the underlying debug information — so a scope-reference indirection would have promised a distinction the data cannot support.

Module and package loading over MCP is currently eager: every module carrying debug-info sidecars is loaded once breakpoints exist, rather than lazily as each package maps.

Next