DAP and MCP frontends

TDapServer and TMcpServer are protocol adapters, not debuggers. Everything that actually debugs lives below them, in TDebugSession and the engine.

What each one adds

DAP frontendMCP frontend
Client VS Code and any other DAP client Any MCP client — Claude Code, VS Code, others
Shape of the API The Debug Adapter Protocol’s requests and events, as specified Around forty semantic tools, each answering a question rather than fetching a resource
Who drives The editor, in response to a person clicking A model, in response to a person asking
Optimised for Interactive latency — many small requests, a UI that re-asks constantly Round trips and tokens — hence get_compact_debug_snapshot, which answers in one call what DAP would take three or four to assemble

That last row is the real design difference. DAP assumes a client that can afford to ask twenty cheap questions because a human is waiting a few milliseconds; an agent pays for every round trip in latency and context. So the MCP surface is coarser on purpose, and its tool descriptions carry the caveats a model needs in order not to misread an answer.

One cycle, both frontends

Setting a breakpoint and hitting it, from either client. Everything below TDebugSession is identical — the frontend only translates at the top and the bottom.

sequenceDiagram
  participant C as "Client
(VS Code or agent)" participant F as "Frontend
(TDapServer / TMcpServer)" participant S as TDebugSession participant E as "Debug event loop" participant D as Debuggee C->>F: set breakpoint (file, line) F->>S: resolve line to address S->>D: write INT3 at address S-->>F: verified / not verified F-->>C: breakpoint state C->>F: continue F->>S: resume S->>E: ContinueDebugEvent E->>D: run D-->>E: EXCEPTION_BREAKPOINT E->>S: stop event S->>S: identify thread, walk frames, read locals S-->>F: stopped (reason, thread, location) F-->>C: stopped event / snapshot
Every step below TDebugSession is shared. A DAP client and an MCP client issuing the equivalent request take the same path through the engine, hit the same symbol providers and get the same answer — the difference is only in how it is spelled on the way out.

Where the surfaces are not symmetric

One engine does not mean two identical APIs. What the DAP frontend exposes and the MCP one does not:

  • Set Next Statement.
  • setVariable — writing a variable by name. Over MCP only write_memory and set_register mutate state.
  • Hardware watchpoints on a bare local. DAP pins one to the frame invocation and withdraws it when the frame dies; the MCP tool refuses a local with a reason instead.
  • Lazy per-module symbol loading. Over MCP, modules carrying debug-info sidecars currently load eagerly once breakpoints exist.

And two things the DAP frontend deliberately does not implement, which are worth recording as decisions rather than gaps: the modules request (the same information is already on every stack frame, so it would be a second source of truth), and setExpression (Watch-panel inline editing).

The complete list is in the MCP tool reference and Known limitations.

Why two frontends rather than one

An MCP server that shelled out to a DAP adapter would have been less code. It would also have inherited DAP’s shape — scope references, variable reference indirection, an event stream designed for a UI — and handed a model an API built for a different kind of consumer.

The concrete example is scopes. DAP models variables as scopes you dereference in a second request. The MCP surface returns locals directly, with opaque handles for whatever is expandable, and has no get_scopes at all — because in Delphi debug information arguments are not reliably separable from locals, so a scope split would have promised a distinction the data cannot support. Going through DAP would have forced that fiction.