This document provides a complete reference for all tools available in mcp-debugger.
- Session Management
- Breakpoint Management
- Execution Control
- State Inspection
- Additional Tools
- IDE Mirror
- Language-Specific Tools
Creates a new debugging session.
Parameters:
language(string, required): The programming language to debug. Languages are discovered dynamically from installed adapters. The default fallback languages (when dynamic discovery is unavailable) are"python"and"mock". When all adapters are available, the full list is:"python","ruby","javascript","rust","go","java","dotnet","cpp","cobol","dart","mock". The actual list depends on which@debugmcp/adapter-*packages are discoverable at runtime. A language may be usable in one mode only — e.g. in the Docker container Ruby is attach-only (the adapter ships without a Ruby runtime; attach connects directly to a remote rdbg socket). Checklist_supported_languagesmodesfor per-mode availability; creating a session for an attach-only language is allowed, and onlystart_debuggingwill fail.name(string, optional): A descriptive name for the debug session. Defaults to"<language>-debug-<timestamp>"(e.g.,"python-debug-1711500000000"), built from the session language andDate.now().executablePath(string, optional): Path to the language interpreter/executable (e.g., Python interpreter path).host(string, optional): Attach mode only — host to attach to; the adapter defaults it (127.0.0.1for every adapter but Java, which useslocalhost). Does not trigger attach mode by itself.port(number, optional): Debug port to attach to. Passingport,processIdor anadapterConfigthat names a target — a debugger URI (vmServiceUri,websocketAddress,vmServiceInfoFile), or a FlutterdeviceIdfor an app running on an Android device — switches the call into attach mode — the session is created and immediately attached with the same requestattach_to_processwould send (see attach_to_process for the full attach contract; issue #905).hostalone does not trigger it.processId(number or string, optional): Attach mode only — same as attach_to_process'sprocessId; triggers attach mode.sourcePaths(string[], optional): Attach mode only — same asattach_to_process.justMyCode(boolean, optional): Attach mode only — same asattach_to_process.breakOnExceptions(string, optional): Attach mode only — same asattach_to_process(no language default on attach). An invalid value is refused before the session is created.stopOnEntry(boolean, optional): Attach mode only — same semantics as attach_to_process'sstopOnEntry: omitting it pauses the target after attach (the pause may land after the response, reported aspending: trueand named inmessage); passfalseto attach to a live service without stopping it.timeout(number, optional): Attach mode only — connection timeout in milliseconds, forwarded as-is: js-debug honours it (its own default is 10 s) and COBOL uses it as its manifest-build budget; the other adapters ignore it. The shortcut no longer forces30000, which on js-debug out-waited the verify window. A value longer thanverifyTimeoutmeans the verify deadline is what you hit.verifyTimeout(number, optional): Attach mode only — how long to wait (ms) for the debugger to report at least one thread after attaching before failing the attach (default:20000, max:600000).adapterConfig(object, optional): Attach mode only — adapter-specific attach configuration merged into the attach config, with the same semantics as attach_to_process'sadapterConfig.
Response:
{
"success": true,
"sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
"message": "Created python debug session: Test Debug Session"
}Example:
{
"language": "python",
"name": "My Debug Session"
}Notes:
- Session IDs are UUIDs in the format
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - Sessions start in
"created"state - One debugger core per process; the id is the capability (issue #1025). On Streamable HTTP every MCP client of a process sees the same debug sessions:
list_debug_sessionsis the whole process's list, and any client holding an id can drive or close that session — run onehttpprocess per agent where agents must not see each other's work. A debug session outlives the MCP session that created it, so a one-shot script that opens a fresh MCP session per call reaches the sessions its previous run created, and a client that reconnects can resume or close what it left behind. A session ends byclose_debug_session, by its program ending, or with the server: nothing expires on a timer by default. An operator can turn on the idle policy (MCP_DEBUG_SESSION_IDLE_MS, off by default on every transport): a session tree that no tool call has named for that long is then closed through the normal close cascade (a launched program is ended, an attached one released), unless a client is attending the process — while any client holds a stream open (a 2025 session's SSE stream, a 2026-07-28 client'ssubscriptions/listenstream), nothing expires; a tool call in flight holds the tree it names, whatever the tool, and the window starts over when it answers (issue #1127); children never expire on a clock of their own.list_debug_sessionsshows each session'sidleMsand, with a policy on,expiresInMs; a later call naming a session the policy closed answers what happened, withclosedBy: "idle-policy". The server is built to scope sessions per principal once it verifies callers itself (no authenticatedhttpmode ships yet; an authenticating reverse proxy does not identify callers to the server), so today every caller is in the one shared scope - When
port,processIdor anadapterConfigURI/info-file key is provided increate_debug_session, the server performs an inline attach (creating the session and immediately attaching). The session exists before the addressing gate can refuse the form (aporton C/C++, say), so such a refusal answerssuccess: falsewith a usablesessionId. The response then mirrorsattach_to_process: alongsidesessionIdit carriesstate, the attachdatapayload, an optionalwarning, and — when a requested post-attach pause has not landed yet —pending: true, withmessagesaying so (…; post-attach pause pending — the target stops when it next executes code; call wait_for_stop to block until it does (pass stopOnEntry: false to attach without pausing))
Lists all active debugging sessions — the whole process's list, whichever MCP client created them (see the note under create_debug_session).
Parameters: None (empty object {})
Response:
{
"success": true,
"sessions": [
{
"id": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
"name": "Test Debug Session",
"language": "python",
"state": "created",
"createdAt": "2025-06-11T04:53:14.762Z",
"updatedAt": "2025-06-11T04:53:14.762Z",
"idleMs": 4210
}
],
"count": 1
}Session States (from SessionState enum):
"created": Session created but not started"initializing": Debug session starting up"ready": Session initialized and ready to start debugging"running": Actively debugging (program executing)"paused": Paused at breakpoint or step"stopped": Session stopped (program terminated)"error": Session encountered an error
lastStop: present while the session is paused (the stop it is at: reason, threadId, timestamp, the adapter's description/text, and exceptionInfo for exception stops) and after it reaches stopped/error (the last stop before it ended). A running session never carries one, so a poller that calls this after continue_execution or a step sees state: "running" with no stop record until the next stop lands — the record of the stop it just left is not repeated as if the program were still paused.
debuggerDisabled: present (true) while the current launch runs with the debugger off — dapLaunchArgs.noDebug: true on an adapter that honours it (see start_debugging) — and the launch is live (initializing, running or paused). Cleared by the next launch or attach, and by a stopped event only a live debugger produces: a breakpoint the adapter itself reports (or names in hitBreakpointIds), an exception stop, or an entry stop — not a pause, a step, or a debugger; statement js-debug relabels as a breakpoint. Omitted otherwise (issue #749).
Errored sessions include optional diagnostics with the current launch attempt's server-host proxyLogPath and remote-safe proxyLogResource. The record is retained for proxy initialization failures and for proxy/adapter deaths after initialization, and is cleared when a new launch or attach attempt begins.
idleMs: milliseconds since the last tool call naming this session, or any session in its tree, answered (a child reports its root's); 0 while such a call is still running — what tells a forgotten session from a live one. expiresInMs: present only when the operator turned the idle policy on (MCP_DEBUG_SESSION_IDLE_MS): how long the tree has before the policy closes it; any tool call naming the session resets it and holds it for as long as the call runs (a slow launch or a long wait_for_stop is never closed underneath itself, and the window starts over when it answers), and nothing expires while a client is attending the process.
parentSessionId: present on child rows only. A child session is created by the server, never by create_debug_session, when the adapter reports a process the debuggee started: a Python launch or attach — debugpy's sub-process debugging is on by default; dapLaunchArgs: { subProcess: false } opts a launch out and an attach target decides for itself with --configure-subProcess — where debugpy reports each child, or a JavaScript launch or verified-local attach — js-debug's autoAttachChildProcesses is on by default for launch or an inspector target that can read both a temporary server-side probe file and the installed js-debug bootloader files; other attaches default it off with a warning; dapLaunchArgs: { autoAttachChildProcesses: false } opts a launch out, adapterConfig: { autoAttachChildProcesses: false } an attach — where js-debug reports every Node process the program forks or spawns, cluster workers and grandchildren included (see the session tree). The child inherits the parent's language, its name is the adapter's own label for the process (debugpy: Subprocess <pid>; js-debug: <script> [<pid>]), it has its own breakpoints, get_output and close_debug_session, and it carries no exitCode (it ends with the adapter's terminated event). The parent's wait_for_stop reports the child's stop with sessionId naming the child and children listing the tree; a child can still be waited on by its own id (that wait answers for the child and its own descendants only). start_debugging, restart_debugging, attach_to_process, detach_from_process, expose_session and unexpose_session refuse a child id by name. Closing the parent closes its children first (childrenClosed in the answer). DEBUG_MCP_MAX_CHILD_SESSIONS caps the live children of one tree (default 32; 0 turns adoption off): a child reported beyond the cap is released and runs undebugged, and the parent's next wait_for_stop, start_debugging or restart_debugging answer counts the run's released children in childrenReleased.
Closes an active debugging session.
Parameters:
sessionId(string, required): The ID of the debug session to close.
Response:
{
"success": true,
"message": "Closed debug session: a4d1acc8-84a8-44fe-a13e-28628c5b33c7"
}A session with child sessions, a child session or an attach root says what the close took with it (the session tree, issue #1081):
{ "success": true, "message": "Closed debug session: c7e2a9b4-5d1f-4e8a-9b3c-2f6d8a1e0b47", "processEnded": true }childrenClosed: nwhen the close took descendants with it: descendants are closed first, deepest first, then the named session. Closing a child leaves its ancestors, its siblings and the debug adapter they share alone.processEnded: truewhen the named session is a child whose process the close ended (debugpy: a child's close terminates it, exit 0 reported to its parent).released: truewhen the named session is an attach root: its descendants are detached with their targets kept running, deepest first, their rows removed, then the root is detached. Also when the named session is a JavaScript child: js-debug cannot end a target it adopted, so the close disconnects with the process kept running and it finishes undebugged (a root's close releases its JavaScript children the same way, counted inchildrenClosed).
Notes:
- Sessions may close automatically on errors
- Closing a non-existent session returns
success: false(message: "Failed to close debug session: <id>"); a session another MCP client of the same process created closes like any other — the id is the capability (issue #1025) - On a launch session the debuggee is terminated. Adapters that need a DAP
terminateahead of the disconnect to do so (the Dart and Flutter SDK adapters) receive one first (#885); an attached process is detached, not terminated
Sets a breakpoint in a source file.
Parameters:
In the shipped default addressing mode (content), sessionId is the only parameter the schema marks required: address the breakpoint by file + line, by file + statement, or by function alone. Restricting the mode with DEBUG_MCP_BP_ADDRESSING=line (or =assert) removes statement, nearLine and function from the schema, and file + line become required.
sessionId(string, required): The ID of the debug session.file(string): Path to the source file — absolute in host mode (a relative path is rejected withPath must be absolute. Received: "..."); in container mode paths are resolved against the/workspacemount, so a path relative to the mounted directory works there. For Java, a fully-qualified class name (e.g.com.example.MyClassorcom.example.Outer$Inner) is preferred — it works reliably with every classloader.line(number): Line number where to set breakpoint (1-indexed).statement(string, optional): Content addressing — instead ofline, pass the text of the target line, like an Edit-tool match — a distinctive substring is enough (leading/trailing whitespace and trailing///#comments are ignored; an exact whole-line match always wins over substring matches). Can only land on a line containing your stated text — an inexact or multi-candidate match still sets the breakpoint but adds awarningto the response; if the text appears on multiple lines the error lists every match; anchors re-resolve acrossrestart_debuggingafter file edits. ProvidestatementORline, not both. See Statement anchors.function(string, optional): Symbol addressing — break on entry to a function/method by name (DAP function breakpoint). Session-global: nofileorlineat all, and names survive edits better than both. Composes withconditiononly. Supported by Python, Go, Rust, C/C++, .NET, Java, JavaScript and COBOL (JavaScript names are dotted runtime paths delivered over the CDP bridge — see Function breakpoints; COBOL names are paragraphs, sections or a PROGRAM-ID, qualifiable as1000-INIT OF PAYROLL, resolved by the COBOL shim to the range's first statement); Dart/Flutter is rejected up front (the SDK debug adapters have nosetFunctionBreakpointsrequest, so the Dart policy pins the capability off); Ruby is rejected up front (rdbg advertises the capability but its handler ignores the request, so nothing would ever bind).nearLine(number, optional): Withstatementonly — when the statement text appears on multiple lines, bind to the match closest to this line (ties go to the lower line).expectedContent(string, optional): Withlineonly — assert the text of the target line before setting; a distinctive substring is enough (leading/trailing whitespace, trailing///#comments and the line's trailing;or.are ignored — a statement quoted without its terminator is a whole-line match, issue #1038). On a mismatch the breakpoint is not set and the error shows the actual content of that line and its neighbors — a fast, self-explanatory failure instead of a breakpoint that silently lands on the wrong line. A relaxed match (a partial substring, or one that only holds after ignoring text past a///#marker) still sets the breakpoint but adds awarningquoting the actual line. See Content assertions and loud snapping.condition(string, optional): Conditional expression — only break (or log) when it evaluates truthy.logMessage(string, optional): Create a logpoint instead of a pausing breakpoint — see Logpoints below.suspendPolicy(string, optional): Suspend policy when the breakpoint is hit —"all"suspends all threads (default),"thread"suspends only the event thread. Only supported by the Java/JDI adapter.
Response:
{
"success": true,
"breakpointId": "28e06119-619e-43c0-b029-339cec2615df",
"file": "C:\\path\\to\\debug-mcp-server\\examples\\python_simple_swap\\swap_vars.py",
"line": 10,
"verified": false,
"message": "Breakpoint set at C:\\path\\to\\debug-mcp-server\\examples\\python_simple_swap\\swap_vars.py:10",
"context": {
"lineContent": " a = b # Bug: 'a' loses its original value here",
"surrounding": [
{ "line": 8, "content": " # Or Python's tuple assignment: a, b = b, a" },
{ "line": 9, "content": " " },
{ "line": 10, "content": " a = b # Bug: 'a' loses its original value here" },
{ "line": 11, "content": " b = a # Bug: 'b' gets the new value of 'a' (which is original 'b')" },
{ "line": 12, "content": " " }
]
}
}Important Notes:
- Breakpoints show
"verified": falseuntil debugging starts - Works in every lifecycle state short of
close_debug_session: before the first launch (queued,verified: false), while running or paused (sent live, with the adapter's answer in the response), and after the program has exited (queued for the nextstart_debugging/restart_debugging). A session that was attached keeps attach path semantics until its next launch (issue #806): a remote attach passes paths through verbatim with nostatementanchors, while a local attach — byprocessIdor to a loopback endpoint — resolves a path that exists on this host the way a launch does (issue #899) - The response echoes the resolved path the server actually used — in container mode that is the path with the workspace prefix applied; for a remote attach session the path is passed through unchanged, and so is a local attach's path when the file is not on this host. On a remote attach the response carries no
content/context(the host file is not the debuggee's source) and anotesays so (issue #899) - Setting breakpoints on non-executable lines (comments, blank lines, declarations) may cause unexpected behavior
- Executable lines that work well: assignments, function calls, conditionals, returns
- The top-level
contentfield echoes the bound line's text (same ascontext.lineContent) content,contextandlinecome from the file you named. When the adapter binds that file through a compiled one and that compiled file is older than the source, the response adds awarning: "The compiled file the debugger binds this location through may be stale:dist/x.js(modified …) is older thansrc/x.ts(modified …)…" (issues #938, #950). Which compiled file depends on the language. A TypeScript source under js-debug binds through the generated.jsand its.js.map: found from the nearesttsconfig.jsonthat names anoutDir(with itsrootDir), or — when no tsconfig names one — the conventionaldist|build|out|libunder the nearestpackage.json, a sibling.jslast; only a candidate whose.js.mapnames the source counts, and a launch whose program is itself a.ts(tsx, ts-node, type stripping) or a runner undernode_modules(vitest, jest, mocha) is exempt. A Java source binds through its.classunder aclasspathdirectory of the launch (at the package path mirroring the source's directory, or beside the source); a .NET source through the launched DLL (whose PDB carries the lines); a Go source inexecmode, and a Rust, C/C++ (headers included) or COBOL (copybooks included) source, through the executable the launch runs. For those compiled languages the artefact is known only oncestart_debugginghas run: before that the echo stands alone; afterwards aset_breakpointcarries thewarning, and every answer that names a stop carries thenote— thestart_debugging(orrestart_debugging) answer itself when the program is already paused at a breakpoint when the launch answers,wait_for_stop,step_*,pause_execution, andget_stack_tracefor the file of its top frame (issue #951) — so a fast program that stops as it starts is told in the one call. A COBOLrunner: "cobcrun"launch, a COBOL source launch (cobc's build is keyed on the sources' content, so it is current by construction) and every attach session name no artefact, and a source under the toolchain (a system header, Rust's std, a registry crate) is not compared. When several class files could be the one (a leftover in-place compile beside the source and the build underout/), the newest existing one is compared — a stale leftover never outranks the build the engine loads. The check compares mtimes only, so it is a hedge, not a verdict (a file saved without changes reads as newer too, an incrementaltscdoes not re-emit for it, and a source beside the project that was never compiled into the artefact reads as stale). The same sentence appears asnoteonstart_debugging,restart_debugging,step_*,wait_for_stopandpause_executionanswers whose location is such a file, and onget_stack_tracewhen its top frame is in one
expectedContent is a checksum on intent: agents that compute line numbers from a code listing routinely land one line off, and a breakpoint on a blank line or brace produces confusing session behavior much later. With expectedContent, the mismatch fails at set time:
Breakpoint not set: line 12 of /abs/app.py does not match expectedContent.
Expected: "total = sum(prices)"
Actual: "return total"
Context:
10 | prices = load()
11 | total = sum(prices)
> 12 | return total
13 |
14 | def main():
The file may have changed since you last read it. Pick the correct line from the context above.
Relatedly, when a debug adapter accepts a breakpoint but binds it to a different line (adapters snap requests on non-executable lines to the nearest valid one), the response reports it prominently instead of silently mutating the line: message and warning carry "requested line 12, bound to line 13: \...`", and the response includes requestedLinealongside the boundline. Adapters that relocate breakpoints asynchronously (after the response) surface the move in list_breakpoints, where line≠requestedLine` marks a snapped breakpoint.
expectedContent requires a source file the server can read: it is rejected for Java FQCN breakpoints and for remote attach sessions (the debuggee's filesystem is the authority); a local attach — by processId or to a loopback endpoint — whose file exists on this host is accepted (issue #899). Both addressing aids can be restricted with the DEBUG_MCP_BP_ADDRESSING environment variable (line = pre-existing behavior, assert = + expectedContent/loud snapping, content = all features; default content) — useful for controlled comparisons of agent behavior.
statement addresses a breakpoint by content instead of line number — the single most practiced agent skill (Edit-tool old_string matching) instead of line arithmetic:
{ "sessionId": "...", "file": "/abs/app.py", "statement": "total = sum(prices)" }- Matching: whole-line equality after trimming leading/trailing whitespace; when no exact match exists anywhere, a substring match against comment-stripped lines is accepted (exact matches always win — the two populations never mix). Multi-line input is rejected — anchor on the first line of a multi-line construct.
- Ambiguity is an error, and the error is the disambiguation UI: every matching
line: contentpair is listed (capped at 20); addnearLineto bind to the closest match. AnearLinepick among multiple matches adds awarningnaming every candidate line — proximity is a heuristic, not a guarantee. - Blank/comment anchors are rejected (
#,//,/*prefixes) — debuggers cannot break there reliably. - The anchor is stored on the breakpoint record (visible in
list_breakpoints) and re-resolves onrestart_debugging: after you edit the file — the whole point of a debug session — the relaunch re-finds each anchored statement in the current file (the breakpoint's previous line breaks ties between duplicates). Moves are reported in the restart response'sdata.anchorResolution.moved— a move picked among multiple matching lines carries acandidateslist and a restartwarning; anchors that no longer match keep their previous line and warn (data.anchorResolution.stale) rather than failing the restart or dropping state. - Same readable-file requirement as
expectedContent(no Java FQCNs, no remote attach sessions — an attach byprocessIdor to a loopbackhost/portis content-addressable whenever the file exists on the mcp-debugger host, since it is the debuggee's own source there, issue #899); composes withcondition,logMessage, andsuspendPolicyunchanged. - A matching
expectedContentalongsidestatementis accepted as redundant (matching under the same relaxed rules: trailing comments ignored, either value may be a substring of the other); a genuinely different one is an error (contradictory intent).
set_breakpoint {sessionId, function: "process_order", condition?} breaks on entry to a symbol, with no file or line:
- Session-global, name-addressed — the adapter resolves the symbol across the whole program, and the name survives any file edit.
restart_debuggingre-applies them natively. - Composes with
conditiononly (logMessageandsuspendPolicyhave no DAP function-breakpoint form; file/line/statement/expectedContent are contradictory and rejected). - The response and
list_breakpointsreport the adapter's bound location asboundFile/boundLineonce verified.list_breakpointsreturns function breakpoints in a separatefunctionBreakpointsarray (excluded when filtering by file);remove_breakpointacceptsfunction: "name"or the breakpoint id; an unscopedclear_breakpointsremoves them, a file-scoped clear does not. - Support is adapter-gated: Python, Go, Rust, C/C++, .NET, Java, JavaScript and COBOL (paragraph, section and PROGRAM-ID names) work; Ruby is rejected up front — rdbg's initialize response advertises
supportsFunctionBreakpoints, but the debug gem's DAP handler acknowledges the request and ignores it (verified against debug 1.11.0), so the Ruby policy pins the capability off rather than trust the advertisement. Dart/Flutter is rejected up front too: the SDK debug adapters have nosetFunctionBreakpointsat all (the request answersUnknown command), so the Dart policy pins the capability off. - A name that never binds is reported, not silent: known-hazard names warn at set time (Go's bare identifiers need package qualification —
main.main; a baremainon Rust resolves to the C runtime's entry point, usemy_crate::main); function breakpoints still unbound after launch produce awarningin thestart_debuggingresponse (suppressed for JavaScript/Java, which bind late by design); and breakpoints that never bound by program exit get an explanatorymessageinlist_breakpointsplus a[mcp-debugger] Warning:entry inget_output. - Java names may be bare (
helper), class-qualified (Foo.helper,com.example.Foo.helper,Outer.Inner.helper), or constructors (Foo.<init>). Every concrete overload binds (the reportedboundLineis the first); classes not yet loaded bind on load and report through breakpoint events. Bare names skip JDK-internal classes (java.*,javax.*,sun.*,jdk.*,com.sun.*) — qualify the class to target those. - JavaScript semantics differ by design (js-debug implements no DAP function breakpoints upstream — vscode-js-debug#952 — so ours are delivered over js-debug's CDP proxy via V8's
Debugger.setBreakpointOnFunctionCall, the same primitive behind Chrome DevTools'debug(fn)). The name is a dotted runtime path (handler,obj.method,globalThis.tick), resolved side-effect-free against the top paused frame's scope (or global scope while running) and bound to the function value it resolves to at that moment — it is not a source-symbol search ("all functions named X" is not the contract), and reassigning the property later does not move the breakpoint. Top-levelfunctiondeclarations of the main module bind at launch;const fn = ...and functions in lazily-loaded modules stayverified: falsewith an explanatory message and bind automatically at the next pause. Names resolve against runtime names, not TypeScript/minified source names. Launch and attach modes both work (attach has no entry pause, so module-scoped names bind at the first pause after attach).
Passing logMessage turns the breakpoint into a DAP logpoint: when the line is hit the program does not pause — the message is logged and execution continues at full speed. Expressions in {curly braces} are interpolated with live values (e.g. "order={orderId} total={total}"), and the messages arrive in the session output, readable via get_output and the debug://sessions/{id}/output resource. condition may be combined with logMessage — the message is only logged when the condition holds.
This is the prod-safe just-in-time diagnostics primitive: attach to a live process, plant logpoints at suspect lines, read interpolated values from get_output — no pauses, no pre-instrumented logging.
JavaScript logpoints are compiled by mcp-debugger (issues #850, #861, #853). js-debug's own logpoints print through the debuggee's console.log, which a program that owns its stdout may have replaced (an MCP stdio server — mcp-debugger itself — no-ops it), and js-debug lets a condition take precedence over the message. So for a JavaScript session the logMessage is compiled into a breakpoint condition that renders the message, delivers it over js-debug's CDP proxy (a Runtime.addBinding global the proxy installs; console.log only as a fallback before the binding exists, e.g. in the first milliseconds after an attach) and never pauses; the entries arrive with category console. What that means in practice:
- Strings interpolate raw; everything else renders through
util.inspecton one line (obj={ k: 2 }); a throwing expression renders inline as<ReferenceError: x is not defined>; a throwingconditionlogs oneLogpoint condition error: …line and does not pause. - A
{expression}is one JavaScript expression, judged by V8 the way js-debug judges a condition: regex literals, comments, and brackets inside strings are all fine ({/;/.test(s)},{JSON.stringify({a: 1})}); a statement list ({a; b}) is refused. The only lexical limit is the brace scanner's: a}inside a regex literal ends the expression early — write that}in a string.{{foo}}is the object-literal shorthand({foo}); an unclosed{, an empty{}, a stray}and a brace group with an unbalanced quote ({it's}) are literal text, and the groups after them still interpolate. - A
logMessagewhose expression does not compile is refused byset_breakpointwith V8's message; a plainconditionjs-debug rejects is reported on the breakpoint (list_breakpoints→message, and the launch's unbound-breakpoint warning) in js-debug's own words, as well as in the output stream. - A logpoint set before
start_debuggingmakes js-debug stop on entry so the delivery binding is installed before the first hit; the session auto-continues that stop (it is not reported,wait_for_stopnever sees it), exactly as it does for a pre-launch function breakpoint.
Support is adapter-dependent:
| Adapters | Behavior |
|---|---|
| Python, JavaScript/TypeScript, Go, Rust, C/C++, COBOL, Dart/Flutter, mock | Supported — logs without pausing (COBOL: {WS-NAME} is a COBOL data reference evaluated by the shim; Dart/Flutter: the SDK adapter ignores a condition on a logpoint, so every hit logs) |
| Java, .NET, Ruby | Not supported — set_breakpoint with logMessage fails fast with a clear error (rdbg would silently convert a logpoint into a pausing breakpoint, issue #469) |
| Adapters whose support is genuinely unknown (dynamically loaded) | Accepted with a warning; validated against the adapter's live capabilities at launch — the start_debugging response names each logpoint downgraded to a pausing breakpoint |
Lists all breakpoints in a session with their current verified state and adapter-assigned ids. Works before launch (queued breakpoints, verified: false), while running or paused, and after the program exits.
Parameters:
sessionId(string, required): The ID of the debug session.file(string, optional): Only list breakpoints in this file.
Response:
{
"success": true,
"breakpoints": [
{
"id": "28e06119-619e-43c0-b029-339cec2615df",
"file": "C:\\path\\to\\project\\app.py",
"line": 10,
"verified": true,
"adapterId": 3
},
{
"id": "bbe96ee0-6c6d-47b1-a3ef-166ffbde3028",
"file": "C:\\path\\to\\project\\src\\cli\\http-command.ts",
"line": 398,
"verified": true,
"adapterId": 2,
"boundFile": "c:\\path\\to\\project\\dist\\cli\\http-command.js",
"boundLine": 305
}
],
"count": 2,
"functionBreakpoints": [],
"functionCount": 0
}Notes:
- The array is sorted by file, then line. Conditional breakpoints include their
condition; Java suspend policies appear assuspendPolicy. functionBreakpoints/functionCountare always present in the unfiltered response (empty arrays when none exist). When filtering byfilethey are omitted — function breakpoints are session-global, not file-scoped.adapterIdis the debug adapter's own numeric id for the breakpoint, captured from setBreakpoints responses and breakpoint events. It is absent until the adapter has seen the breakpoint.messageis the adapter's own words about the breakpoint: its validation message on a set, or its refusal when the adapter declined a live re-send of the file's breakpoints (aset_breakpoint,remove_breakpointorclear_breakpointson a live session, or the post-launch/post-attach re-send) — e.g. debugpy'sServer is not availableundernoDebug. A refusal is the adapter's answer to the request, not a verdict on any one breakpoint: it is stamped as themessageof every unverified breakpoint the re-send covered, markedmessageOrigin: "refusal"(a refused pre-launch set is echoed and marked the same way), while a breakpoint the adapter had verified before (or a stop proved bound) stands as it was — the adapter never said it was gone. The stamp is dropped the moment anything verifies the breakpoint, and it never displaces a curated note such as the capability-drift warning (issue #754).messageOriginnames who wrotemessagewhen it is not the adapter's verdict on the breakpoint:"refusal"as above, or"curated"for a note of the server's own (the capability-drift warning, a re-resolved anchor at restart, a function that never bound); absent, the words are the adapter's. A re-send that failed without the adapter answering — a timeout, the proxy exiting — leaves the breakpoints as they were, since the previous set may still be armed. Either failure is reported in that call'swarning, and instart_debugging/attach_to_process'swarningfor the post-handshake re-send (The debugger refused the re-send of the breakpoints for <files>: …/The re-send of … failed: …, one sentence per distinct answer).- Verification is eventually consistent: some adapters (js-debug, JDI, netcoredbg) bind breakpoints asynchronously and confirm via DAP breakpoint events shortly after launch or class load.
warning(top level) appears only while the session's current launch runs with the debugger off (noDebug: trueon an adapter that honours it, seestart_debugging) and at least one listed breakpoint is unverified: "the debugger is off for this launch (noDebug is true): breakpoints cannot bind and no stop is expected; drop noDebug and launch again to debug" — on a paused session (js-debug lands a pause under the flag) only the clause still true of it, "…: breakpoints cannot bind; drop noDebug and launch again to debug". The per-breakpoint records keep the adapter's own answers; a breakpoint the adapter did verify is proof this build debugs after all, and the warning (withdebuggerDisabledand every other debugger-off note) drops (issue #749).- A breakpoint the program has stopped on is reported
verified: truefrom that stop onward, even if the adapter never confirmed it (issue #673), for adapters whosestoppedevent names the breakpoints it hit (hitBreakpointIds: js-debug, debugpy, Delve, CodeLLDB — netcoredbg, the JDI bridge and rdbg omit the field). Such a record carriesverifiedBy: "hit"until the adapter itself confirms it ("adapter"); an adapter answer of "unbound" does not downgrade it. A provisional "Unbound breakpoint"messageis dropped by the hit; any other note is kept. - On entries of the
breakpointsarray,boundFile/boundLineappear when the adapter answers under a different file from the request. For a source-mapped.tsrequest on a JavaScript launch with maps on (the default) js-debug verifies the request under the.tspath,get_stack_traceframes show.ts, and the pair is absent; it appears when js-debug answers under the generateddist/*.jsinstead — measured withadapterLaunchConfig: { sourceMaps: false }(the second entry in the example above was captured that way, and the frames then show the generated file too), and possible whenever the.tssource cannot be resolved through the map (issues #673, #700).fileandlinekeep describing the request; the bound pair is where it landed. (Entries offunctionBreakpointsuse the same names for the bound location of the symbol, present whenever it is bound.)
Removes one breakpoint by id, every function breakpoint with a given name, or every breakpoint at a file + line location. Takes effect immediately while the program is running or paused (the file's remaining breakpoint set is re-sent to the adapter); also works after the program exits, so breakpoints can be adjusted before a relaunch.
Parameters:
sessionId(string, required): The ID of the debug session.breakpointId(string, optional): Breakpoint id fromset_breakpointorlist_breakpoints. Takes precedence overfunctionandfile+line.function(string, optional): Alternative addressing — remove all function breakpoints with this symbol name. The same per-language rewriteset_breakpointapplies is used here, so the name you set with is the name you remove with (a bare Gomainmatches the storedmain.main); the response reports the effective name asfunctionNameand, when it was rewritten, the original asrequestedName.file(string, optional): Alternative addressing — source file path (use together withline). Removes all breakpoints at that location.line(number, optional): Alternative addressing — line number (use together withfile).
Response:
{
"success": true,
"removed": [
{ "id": "28e06119-619e-43c0-b029-339cec2615df", "file": "C:\\path\\to\\project\\app.py", "line": 10, "verified": true }
],
"message": "Removed 1 breakpoint(s)"
}Notes:
- An unknown
breakpointId(or an empty location or function name) returnssuccess: falsewith an explanatory error; for a function name the error names the effective (rewritten) form, and a bare name with no rewrite gets the same package-qualification hintset_breakpointgives. - If the live re-send to the adapter fails, the breakpoint is still removed from the session (it will not be re-applied on the next launch) and the response carries a
warning.
Removes all breakpoints in a session, or all breakpoints in one file. Clearing zero breakpoints is success, not an error.
Parameters:
sessionId(string, required): The ID of the debug session.file(string, optional): Only clear breakpoints in this file.
Response:
{
"success": true,
"cleared": 2,
"files": ["C:\\path\\to\\project\\app.py"],
"message": "Cleared 2 breakpoint(s)"
}Every answer that reports the session paused — start_debugging, restart_debugging,
wait_for_stop, pause_execution, attach_to_process, the create_debug_session attach
shortcut, and the step tools (step_over/step_into/step_out, issue #983) — additionally
carries a bounded snapshot of the stopped program's state (issue #982), so reading the
crashing state costs zero extra tool calls:
"snapshot": {
"exception": { "type": "ZeroDivisionError", "message": "division by zero" },
"frames": [
{ "name": "compute", "file": "/abs/path/app.py", "line": 44 },
{ "name": "main", "file": "/abs/path/app.py", "line": 61 }
],
"locals": { "subtotal": "125.0", "user": "'ada'" },
"truncation": "3 variable(s) omitted, 1 value(s) cut — get_local_variables returns the full list",
"note": "…"
}exceptionappears only on an exception stop: the class/message the adapter reported (secret-redacted), topped up with whatever DAPexceptionInfohad merged by the time the answer was built — never awaited, and never its adapter-side stack trace (framescovers the stack). An adapter that reported nothing (e.g. a CodeLLDB panic relabel) gets noexceptionkey — the answer's stop reason already says it was one.wait_for_stopomits the key by design: its answer carries the full (redacted)lastStop, and the same message never ships twice.framesare the top ≤3 frames of the same policy-filtered stack the answer'slocationwas read from (no second stack fetch).localsis a flat name → value map of the top frame's locals through the same pipeline asget_local_variables(policy scope selection, the walk-down to the first frame with locals, secret redaction), under tighter caps — 20 locals / 200 chars per value by default (DEBUG_MCP_SNAPSHOT_MAX_LOCALS/DEBUG_MCP_SNAPSHOT_MAX_VALUE_CHARS);truncationsays what was cut. The read is bounded (~2 s): if it expires or fails inside the composer, the answer ships withoutlocalsandnotesays so. A read that comes back empty is shown as no locals — and when unprobed deeper frames exist (the snapshot probes the top 3),notesays that too; an adapter error the pipeline itself absorbs looks the same as a locals-free frame, andget_local_variableson the same stop is the deeper read. The snapshot is orientation, not navigation —get_local_variables/get_variablesremain the way to expand structures or fetch everything.changedVariablesreplaceslocalson a repeated stop in the same frame (issue #983) — stepping through a function, or acontinue_execution→wait_for_stoploop over one breakpoint: only what changed since the locals were last shown, as{ "name": { "before": "100.0", "after": "125.0" } }. A variable new in scope appears withbefore: "<undefined>"; one that left scope simply drops out; an empty{}means nothing visible changed — that is information. Frame identity is the locals anchor's name + file + visible stack depth: any mismatch (a different function, a recursion depth change) re-emits fulllocalsand rebases — never a wrong diff. The same stop answered twice (twowait_for_stopcalls without resuming) re-emits fulllocalsrather than diffing against itself. Two different secrets masked to the same<redacted:…>placeholder compare equal, so a changed secret may not be listed. The cache resets on relaunch/re-attach, so a new run's first stop always shows fulllocals.- Not carried on: entry stops (module scaffolding, not state), dry runs,
pending: trueanswers, thepause_execution"Already paused" fast path (callwait_for_stopfor the full snapshot of the current stop),continue_execution(answersrunning), and any stop whose landing read failed. UnderDEBUG_MCP_VARIABLE_ACCESS=explicitthelocalshalf is withheld (exception and frames stay).DEBUG_MCP_NO_SNAPSHOT=1turns the snapshot off entirely.
Starts debugging a script.
Parameters:
sessionId(string, required): The ID of the debug session.scriptPath(string, required): Path to the script to debug. Must be absolute in host mode (a relative path is rejected withPath must be absolute); in container mode it is re-rooted underMCP_WORKSPACE_ROOT.args(array of strings, optional): Command line arguments for the script.dapLaunchArgs(object, optional): Standard DAP launch arguments:stopOnEntry(boolean): Stop at first line (defaultfalse— the opposite of attach, which pauses unlessstopOnEntryisfalse)justMyCode(boolean): Debug only user code (defaulttrue). JavaScript launch:trueblackboxesnode_modulesthrough js-debug'sskipFilesand keeps js-debug's smart-stepper on, so a pause or step that lands in skipped code (Node internals,node_modules) is stepped through and may never land (pending: true, with an explanation);falsedropsnode_modulesfrom the skip list and turns the stepper off, so steps land inside dependencies andpause_executionlands as soon as any JavaScript runs (issue #678). A callerskipFilesreplaces the default list. Source maps are on for every JavaScript launch,.jsprograms included — stops reportsrc/*.tswhen maps and sources are present;adapterLaunchConfig: { sourceMaps: false }opts out (issue #684)noDebug(boolean): DAP's "launch without enabling debugging" flag (defaultfalse). What it does depends on the adapter, and the response says which case you are in. Honoured — the debugger is off: no breakpoint binds, no exception filter arms, no entry stop lands, and no breakpoint, exception or entry stop ever arrives, so a short script endsstoppedand a server staysrunning. That is a legitimate plain run and says nothing by itself; when the session has breakpoints (line or function), an explicitbreakOnExceptionsother than"none", orstopOnEntry, thewarningsays the debugger is off for this launch and names what will not fire — not that no stop of any kind can come — in place of the unbound-breakpoint diagnostics that would otherwise send you to check paths that are fine (issue #710). js-debug, debugpy, Delve and CodeLLDB honour it (debugpy and Delve open no configuration phase at all, so the launch is complete on the launch response; CodeLLDB refuses the configuration requests, and the refusal reaches thewarningandget_output— issue #746). The run endsstoppedwith the program's output and, where the adapter reports one, the exit code (Delve states it as a console line,Process N has exited with status S, which is read back asexitCode— issue #753). Under the flag Delve runs the program through Go'sexec, so on Windows the binary needs its.exeextension. Ignored — rdbg, netcoredbg and the Java bridge run the debugger regardless, and the rust launch transform never forwards the flag: thewarningsays the flag had no effect and everything works as usual. The same warning comes back fromrestart_debuggingand from adryRunSpawncheck. While such a launch runs, the session remembers the decision (list_debug_sessionsreportsdebuggerDisabled: true) and every later surface says why:set_breakpointandlist_breakpointskeep the adapter's own answer (still sent — e.g. js-debug's "Unbound breakpoint", or CodeLLDB's refusal echoed onto each pre-launch breakpoint) and add "the debugger is off for this launch (noDebug is true)…",pause_executionis still sent and the same sentence explains a pause that never lands or one the adapter refused, andstep_*/continue_execution/get_stack_trace/get_local_variables/evaluate_expressionappend it to their "not paused" answers. Astoppedevent only a live debugger produces — a breakpoint the adapter itself reports (or names inhitBreakpointIds), an exception stop, an entry stop — proves the debugger on for that adapter build and clears the decision, and so does a breakpoint the adapter verified (every honouring adapter refuses or unbinds one under the flag, so a bound one can only come from a build that ignores it); a pause, a step taken from one, or adebugger;statement do not (js-debug lands all three under the flag while its breakpoints stay unbound; an uncaught throw does not stop). On a paused session the sentence keeps only the clause still true of it — breakpoints cannot bind — and outside a live launch (initializing/running/paused) the decision is neither reported nor consulted (issue #749).- Additional DAP launch keys (
program,cwd,env, language-specific options) pass through to the adapter. For Python,module(python -m <module>) orcode(python -c) replacesprogram: debugpy takes exactly one target, so the launch goes out withoutprogramand the response'swarningsays so (dapLaunchArgs.module: launched as a module (python -m); program is not sent …);scriptPathstill has to name an existing file, andcwdis what-mputs onsys.path: for these launches it defaults to the directory above the top-most package containingscriptPath(thewarningnames it), and a module launch of a project sets it to the project root explicitly (issue #985).dryRunSpawnreports such a launch'sdata.scriptasmodule: <name>orcode: …. Top-level parameters do not belong here: a nestedbreakOnExceptionsis honored as an alias (the top-level value wins if both are given) and reported via awarningin the response; other misplaced top-level keys (dryRunSpawn,sessionId,scriptPath,adapterLaunchConfig) are stripped with a warning instead of silently riding into the launch config.
adapterLaunchConfig(object, optional): Adapter-specific launch configuration overrides. Use this for language-specific settings that go beyond standard DAP arguments (e.g.,mainClassandclasspathfor Java,buildCommandfor Rust). For Rust,_adapterSettingspasses through to CodeLLDB (issue #441) — e.g.{"_adapterSettings": {"scriptConfig": {"lang": {"rust": {"sysroot": "/path"}}}}}points the Rust formatter lookup at an explicit sysroot; theCODELLDB_RUST_SYSROOTenv var does the same without per-launch config (a user-supplied_adapterSettingsvalue wins over the env var).dryRunSpawn(boolean, optional): check the launch without starting anything (issue #812). The whole launch preparation runs — the toolchain gate, the adapter and its environment checks, the launch transform (which compiles a source-file program for C/C++, Rust and COBOL, as a launch would), executable resolution, the adapter command — and the answer carriesdata.dryRun: true,data.command(the adapter spawn command, or[connect] host:portfor a connect-mode adapter),data.script(the program the engine would run, after the transform) andwarning(the configuration notices a launch would return). The session is left exactly as it was: the answer'sstateis the session's current state, a running or paused program keeps running, and the previous run's output, exit code and last stop stay readable. No proxy is spawned and nothing is recorded, so a dry run is never a launch:restart_debuggingstill replays the last real launch, and the realstart_debuggingon the same session binds every breakpoint already set (issue #793). Not available for an attach-shaped configuration (request: "attach"): preparing an attach acts on the target process. One compile-side caveat: a dry run of a C/C++, Rust or COBOL source recompiles it exactly as a launch would (C/C++ into.debug-mcp/, beside a running binary when that one is locked), and the stale-build note compares the source with the newest artefact, so a note on a program paused from an older build clears after a dry run even though the paused program is still the old build.breakOnExceptions(string, optional):"uncaught"pauses at uncaught exceptions at the crash site (stack and locals inspectable) instead of terminating the session;"all"also pauses on caught/raised exceptions (language-dependent). Launch sessions default to"uncaught"(issue #244) — a crashing script pauses withlastStop.reason: "exception"instead of terminating; pass"none"to opt out and let it run to termination. Ruby is the exception: rdbg has no uncaught-only filter, so Ruby launches stay"none"by default (only explicit"all"is available) — an explicit"uncaught"on Ruby arms nothing, and the response'swarningsays so and names"all"(issue #1032). Attach sessions never apply a language default. The "still running" answer ofstart_debuggingandwait_for_stoppromises a crash stop only when one is actually armed: under"none", on an adapter with no uncaught-only filter whose engine does not stop on its own (Ruby; C/C++ on Windows), or on an attach that requested no mode, it says a crash runs to the program's exit (issues #1009, #1032). The abstract mode maps to per-language debugger filters (e.g. Pythonuncaught/raised— a pytest module launch, thetestRunner: "pytest"preset included, armsuserUnhandledbesideuncaughtunder the default, since pytest catches test failures anduncaughtalone never fires on one; JavaScriptuncaught/all, Javauncaught/caught, .NETuser-unhandled/all, Gounrecovered-panic+runtime-fatal-throw, Rustrust_panic, C/C++cpp_throwfor"all"— its"uncaught"default sets no filter since uncaught throws crash via SIGABRT, which pauses natively on Linux and macOS (on Windowsabort()exits with code 3 and nothing pauses, issue #1032); COBOLcobol_runtime_errorfor both modes — a function breakpoint on libcob'scob_runtime_error, which only fires when the program was compiled withruntimeChecks: true; Dart/FlutterUnhandledfor"uncaught"andAllfor"all"— the SDK adapters' own filters); an explicitly requested unsupported mode is skipped with a warning. Python edge: debugpy treatssys.exit(n)with a non-zero code as an unhandledSystemExitand pauses there (sys.exit(0)runs to completion normally) — pass"none"if a script legitimately exits non-zero viasys.exit.testRunner(string, optional): runscriptPathunder a test runner the language declares a preset for (issue #985) —list_supported_languagesliststestRunnersper language; Python declarespytest. The preset builds the launch: for pytest,python -m pytest <scriptPath>[::testTarget] -x …plus, per plugin,-p no:xdist/-p no:cov— or the plugin's inert switch (-n0/--no-cov) when the project's ownaddoptsturns it on, since-p no:would make pytest reject those options — left out when yourargsask for that plugin; run from pytest's rootdir — the nearest ancestor of the test file holdingpytest.ini, apyproject.tomlwith a[tool.pytest.ini_options]table, atox.iniwith[pytest]or asetup.cfgwith[tool:pytest], else the nearestsetup.py(pytest's own rule), else the top-mostconftest.pyancestor, else the file's directory;dapLaunchArgs.cwdoverrides it — with debugpy'suserUnhandledfilter armed besideuncaughtunder thebreakOnExceptionsdefault, because pytest catches every test failure anduncaughtalone never fires. The answer is the autopsy: paused at the failing assert, or at the raise site of an exception that escapes the test, withsnapshot.exception,snapshot.framesandsnapshot.locals; a passing test runs to completion with exit code 0. Measured shapes (docs/python/spike-notes-pytest.md): a raise inside a helper puts the helper frame first, the live test frame below it marked(Current frame); an exceptionpytest.raisesexpects never stops on Python 3.12+ and stops once on 3.11 and older (pydevd's frame-tracing path; continue);pytest.skip()stops once (continue); a fixture that raises stops in the fixture; a test'sprintreachesget_outputwith pytest's report after the run, not before the pause (passargs: ["-s"]for live prints).argsare appended as pytest arguments. An undeclared runner is refused by name before anything starts, and atestTargetwithouttestRunneris refused too. The preset travels insideadapterLaunchConfig(testRunner/testTarget), sorestart_debuggingreplays it and a dry run reportsdata.script: "module: pytest"with the expansion inwarning.testTarget(string, optional): withtestRunner, the test insidescriptPathto run —test_name,Class::test_name,test_name[param]— or a full node id of that file (tests/test_x.py::test_name; one naming another file is refused). Omit to run the whole file.
Response:
{
"success": true,
"state": "paused",
"message": "Debugging started for /abs/path/examples/python_simple_swap/swap_vars.py. Current state: paused",
"location": { "file": "/abs/path/examples/python_simple_swap/swap_vars.py", "line": 10, "column": 1 },
"context": {
"lineContent": " a = b # Bug: 'a' loses its original value here",
"surrounding": [
{ "line": 8, "content": " # Or Python's tuple assignment: a, b = b, a" },
{ "line": 9, "content": " " },
{ "line": 10, "content": " a = b # Bug: 'a' loses its original value here" },
{ "line": 11, "content": " b = a # Bug: 'b' gets the new value of 'a' (which is original 'b')" },
{ "line": 12, "content": " " }
]
},
"snapshot": {
"frames": [ { "name": "swap", "file": "/abs/path/examples/python_simple_swap/swap_vars.py", "line": 10 } ],
"locals": { "a": "10", "b": "20" }
},
"data": {
"message": "Debugging started for /abs/path/examples/python_simple_swap/swap_vars.py. Current state: paused",
"reason": "breakpoint",
"location": { "file": "/abs/path/examples/python_simple_swap/swap_vars.py", "line": 10, "column": 1 }
}
}When the launch answers: once the program is launched, start_debugging holds its answer for a short window and reports the session as it is when the first of three things happens — the same way in every language (issues #815, #823, #826):
- The program stops —
state: "paused"withreason, and where, the waywait_for_stopanswers a stop (issue #951):locationis the first visible frame,contextthe source line around it, andthreadIdandnoteappear when there is something to say — the answer anchored on a thread other than the reported one, no frame of the stack being program code (as forpause_execution), or the compiled file the debugger binds the location through being older than its source (the stale-build sentenceset_breakpointwarns with, issues #938/#950). The stop snapshot rides beside them — exception, top frames and bounded locals in the same answer (issue #982; not on an entry stop).locationandthreadIdsit at the top level and indata;context,snapshotand the fullnoteat the top level (data.noteis the session layer's part of the note, without the stale-build sentence the handler appends). A breakpoint reached as the program starts is answered here, in the one call (the example above). - The program ends —
state: "stopped"with the run-to-completion summary andexitCodebelow. - The hold elapses —
state: "running"withpending: true(top level and indata, likestep_over) and amessagethat says what is armed:…has not reached 2 breakpoint(s) yet. Nothing was cancelled: what is armed stays armed…; or, with nothing armed (the"uncaught"default does not count — a crash is not a stop you are waiting for),…nothing is armed to stop it (no breakpoints, no entry stop, no caught-exception filter)…; or, for anoDebuglaunch, that the debugger is off. Logpoints the adapter runs on never stop the program, so they are named apart:…1 logpoint(s) that log without stopping is armed (read get_output for the messages), and nothing is armed to stop it (no pausing breakpoints, …)…. What is armed is read when the answer is built, so a breakpoint set while the launch was in flight is named.
The hold is not a limit on the program. Nothing is cancelled when it elapses: breakpoints and exception filters stay armed for the life of the process, and the program pauses when it reaches one — in a second or in a day. The hold only decides whether this call reports the first stop itself. It is deliberately short (about a second, sized from the measured time each adapter takes from launch to a first breakpoint; the number is a tunable, not part of this contract), so that a launch whose breakpoint needs an outside trigger — a request handler in a server — hands control back at once. To wait for the stop, call wait_for_stop, which blocks until the program stops or ends for as long as you say; list_debug_sessions reports the state without waiting.
Two things are outside the hold. A stopOnEntry launch is answered at its entry stop however long the program takes to reach it, because that stop is certain to come. And whatever precedes the launch — compiling a source file, starting the adapter — happens before the hold begins.
A breakpoint set, removed or cleared while a launch is still starting is delivered to the adapter as soon as the program is launched (issue #851).
Sessions with children (the session tree, issues #1081 and #1082): when a child session was adopted during the hold (a Python launch — debugpy's sub-process debugging is on by default; dapLaunchArgs: { subProcess: false } opts the launch out — where debugpy reports each process the program starts; a JavaScript launch — js-debug's autoAttachChildProcesses is on by default; dapLaunchArgs: { autoAttachChildProcesses: false } opts the launch out — where js-debug reports each Node process it forks), the answer carries children: [{ sessionId, name, state, parentSessionId }] — every descendant, depth first in adoption order, each with its state and the session that spawned it. childrenReleased: n says how many children reported this run were released to run undebugged (beyond DEBUG_MCP_MAX_CHILD_SESSIONS, or an adoption that failed) — and the message of the same answer says what that means and why (issue #1137), e.g. "1 child process was released to run undebugged (child sessions are off: DEBUG_MCP_MAX_CHILD_SESSIONS=0): a breakpoint only it reaches will not hit.", so a pending answer's "has not reached 1 breakpoint(s) yet" is not read as a promise for a breakpoint only the released child could reach — and childrenClosed: n that the start tore down a previous live run and closed its children with it. All three sit at the top level beside pending and in data, and are absent when there is nothing to say, so a session with no children answers as before. The launch answer is about the launched session itself; a child's stop is collected by wait_for_stop, which answers with sessionId naming the child.
JavaScript launch configuration: Both launch option objects accept js-debug settings;
adapterLaunchConfig wins when a key appears in both. The effective stopOnEntry value controls
the entry pause and is replayed by restart_debugging. An honoured noDebug disables that pause.
Ignored or malformed options produce a warning naming the input object and key. Unrecognized
options remain forwarded, with a warning and a spelling suggestion when available. Warnings also
appear once in get_output after launch setup, are returned by dryRunSpawn, and are retained in
data.warning when preparation fails. A subsequent launch starts with fresh notices.
For JavaScript, envFile resolves relative to the effective cwd (absolute paths work too).
Its dotenv values override the inherited environment; explicit env entries override the file,
and null removes a variable. This also applies to NODE_ENV and NODE_OPTIONS; without a file
or explicit override, the existing NODE_ENV=development default applies. A missing file produces
a warning; another read error fails the launch. The server's own environment is unchanged.
See the JavaScript configuration guide.
Pause Reasons:
"breakpoint": Stopped at a breakpoint"step": Stopped after a step operation"entry": Stopped on entry (if configured)"exception": Stopped at an exception (the launch default for most languages; seebreakOnExceptions).lastStop.description/lastStop.textcarry the exception class and message where the adapter reports them — secret-redacted wherever an answer emits them (wait_for_stop,get_stack_trace,list_debug_sessions), like variable values (issue #982). Where the adapter supports the DAPexceptionInforequest (Python, JavaScript, Java, .NET, mock),lastStop.exceptionInfois additionally populated best-effort withexceptionId,breakMode, and optionaldetails(message, type names, adapter-side stack trace). The enrichment is requested asynchronously right after the pause, so it may appear inlist_debug_sessions/get_stack_tracea moment after the stop itself — re-query if it is absent immediately after pausing.
A launch that ends before it can report a pause answers state: "stopped" and says how the program ended (issue #701):
{
"success": true,
"state": "stopped",
"message": "Debugging started for /abs/path/app.js. Current state: stopped. The program ran to completion (exit code 0) without hitting any breakpoint. Verified but never hit: app.ts:91.",
"data": {
"message": "Debugging started for /abs/path/app.js. Current state: stopped. The program ran to completion (exit code 0) without hitting any breakpoint. Verified but never hit: app.ts:91.",
"exitCode": 0,
"unhitBreakpoints": [{ "file": "/abs/path/src/app.ts", "line": 91, "verified": true }],
"stopOnEntrySuccessful": false
}
}- The sentence distinguishes a clean run (
ran to completion (exit code 0)), a non-zero exit (exited with code N) and a debuggee that reported no code at all (ended without reporting an exit code— attach targets, signal-killed debuggees, adapters that send noexitedevent and state the code nowhere else). data.exitCodeis present only when the debuggee reported one.data.unhitBreakpointslists every line breakpoint the program ran past (file,lineas bound,requestedLinewhen the adapter moved it,verified); logpoints the adapter supports are left out, since they log and run on. Verified line and function breakpoints that were never hit are named in the sentence; breakpoints that never bound keep their #467warning.- When a stop and the exit arrived in the same tick (a breakpoint hit as the program ended), the breakpoints were not all missed: the list is withheld and the sentence names the stop (
after a stop the launch could not report (last stop: breakpoint)), whichlist_debug_sessionsalso shows aslastStop. - The same holds for the session's tree (issue #1128): when a child session stopped during the run, the parent can no longer say which breakpoints were missed — each child has its own copy of the breakpoints and its own stops. The list is withheld and the sentence counts the tree's stops instead, every stop the agent could see on a child, whatever its reason (
The program ran to completion (exit code 0). Its child sessions stopped 2 times during this run.), whether or not the child is still listed. What remains root-only: when children were adopted but none stopped,unhitBreakpoints[].verifiedis the launched process's own flag, so a breakpoint that bound only in a child readsverified: falsethere. restart_debuggingreturns the same fields for a relaunch that ends the same way.
Exit code: when the debuggee terminates, the exit code reported by the adapter is surfaced as exitCode in list_debug_sessions (and, for a launch that ended before reporting a pause, as data.exitCode above), so a crash (non-zero) is distinguishable from a clean exit. Adapters that never send exited still get one where the debugger states the code some other way: js-debug through the launch shim's recorded code (#247), rdbg through the adapter process's own exit status (#258), Delve before 1.27 through its Process N has exited with status S console line (#753 — in debug mode Delve prints it only in reply to disconnect, so the proxy sends the disconnect before forwarding terminated; Delve 1.27+ sends exited itself). No line, no code: nothing is guessed.
Between launches: a session whose program has exited is still present, with its breakpoints, its last real launch and, until the next launch, the previous run's output and exit code (a dry run leaves all of it in place). start_debugging (a new configuration), restart_debugging (the same one), set_breakpoint, list_breakpoints, remove_breakpoint, clear_breakpoints and get_output all work on it, so "ran to completion without hitting my breakpoint → set an earlier one → restart_debugging" needs no new session; each launch starts a fresh output buffer and re-applies the stored breakpoints with their verified state reset. Tools that need a live debuggee — stepping, continue_execution, pause_execution, list_threads, get_stack_trace/get_scopes/get_variables/get_local_variables/evaluate_expression, get_source_context, expose_session — answer Session is terminated: <id> until the next launch (issues #793, #806).
Restarts the debuggee in one call. When the adapter offers a native restart (DAP supportsRestartRequest — the Flutter adapter advertises it once the app has started), the DAP restart request is sent and the session goes on in place: for Flutter that is a hot restart, main() re-runs in the same process and the first breakpoint is reached again about half a second later (measured on Windows desktop and Chrome). Otherwise the current program is terminated (if still running) and relaunched with the same configuration as the last start_debugging. Either way the current breakpoints stay in force — the core edit-rerun loop (fix a line → restart → confirm at the same breakpoints) is a single tool call instead of close/create/re-set/start.
Parameters:
sessionId(string, required): The ID of the debug session.
Response: mirrors start_debugging, plus:
{
"success": true,
"state": "paused",
"message": "Debugging started for /path/app.py. Current state: paused",
"data": {
"reason": "breakpoint",
"breakpointsReapplied": 2,
"outputReset": true
}
}Notes:
- Works while the program is running, paused, or after it has exited (the primary use case — a finished session can be restarted without recreating it).
- Which path ran is in the answer:
data.hotRestart: truemeans the adapter's own restart was used (same process, output buffer kept, nooutputReset); otherwise the restart was terminate + relaunch (the DAP-spec-blessed emulation every launch-mode language gets) andoutputReset: trueis set. The native path is taken only for adapters measured for it (the policy'srestartViaDapRequestpin) that also advertisesupportsRestartRequestlive — today Flutter. js-debug advertisesrestarttoo but relaunches through a child session this proxy does not follow, so JavaScript, the Dart CLI adapter and every other language keep the emulation (issue #876). - A hot restart of a
stopOnEntry: trueFlutter launch stops at entry again: the re-runmain()hits the entry breakpoint the launch armed, reported asreason: "entry"like the first time. - The launch configuration is replayed verbatim (script, args,
dapLaunchArgs,adapterLaunchConfig— thetestRunnerpreset rides in it —breakOnExceptions); there are no per-restart overrides — callstart_debuggingfor a different configuration. - The output buffer starts fresh on a relaunch:
outputReset: truesignals thatget_outputcursors from the previous launch are stale — read fromsince: 0. A hot restart keeps the buffer (same process): cursors stay valid. - The hold and its
pending: trueanswer are the same asstart_debugging's ("When the launch answers" above): a relaunch that has not stopped when the hold elapses answersrunning, andwait_for_stopcollects its stop. A relaunch that stopped carries the same stop snapshot (issue #982). - Children belong to a run (the session tree, issues #1081 and #1082): a relaunch closes and removes the previous run's child sessions before it starts and says so with
childrenClosed: n(JavaScript children are released rather than ended, js-debug cannot end a target it adopted); a child adopted during the new run's hold is listed inchildren, and children released to run undebugged this run are counted inchildrenReleased, as forstart_debugging(top level besidepending, and indata). A hot restart keeps the process and its children: nochildrenClosed, the children listed. - Not available for attach sessions unless the adapter offers a native restart (a Flutter attach session hot-restarts like a launched one; any other attach session has no launch configuration to replay — detach and re-attach instead), nor for sessions that were never launched, including dry-run-only sessions — call
start_debuggingon the same session instead.
Steps over the current line, executing it without entering function calls.
Parameters:
sessionId(string, required): The ID of the debug session.
Response:
{
"success": true,
"state": "paused",
"message": "Stepped over",
"location": { "file": "/path/app.py", "line": 11, "column": 1 },
"context": {
"lineContent": " b = temp",
"surrounding": [
{ "line": 9, "content": " temp = a" },
{ "line": 10, "content": " a = b" },
{ "line": 11, "content": " b = temp" },
{ "line": 12, "content": " return a, b" },
{ "line": 13, "content": "" }
]
},
"snapshot": {
"frames": [ { "name": "swap", "file": "/path/app.py", "line": 11 } ],
"changedVariables": { "b": { "before": "20", "after": "10" } }
}
}location and context are best-effort: they appear when the post-step stack trace and the source file could both be read.
The stop snapshot rides on every landed step (issues #982/#983). Stepping within one
function reports changedVariables — only what the executed line changed — instead of re-sending the
full locals; stepping into a new frame (or any frame-identity change) shows that frame's full locals.
An empty changedVariables: {} says the line changed nothing visible. On an adapter that steps once per
token position (Dart), the snapshot is composed once, from the final landing of the repeated step.
When the stop that ended the step was recorded as something other than the step — a breakpoint on the landing
line, or a breakpoint or exception hit before the step completed — the response also carries stopReason (and
rawStopReason when the adapter's raw reason was normalized, as for pause_execution), and the
message reads Stepped over; stopped on 'breakpoint' rather than on the step itself (see stopReason). When that stop
is on the very line the step was issued from — a lost step whose breakpoint fired again, or a single-line loop — the
message adds The program is back at the line the step was issued from. A pause reason is reported in
stopReason with the plain wording (issue #678). location is the first visible frame, as everywhere else; threadId
and note appear only when the answer is anchored off the reported thread or no frame of the shown stack is program
code (as for pause_execution). A step stop is read plainly and never moves off its own thread; a step that was
ended by a pause from elsewhere is read as a pause and may.
step_over, step_into, and step_out share one contract. The DAP step request only acknowledges that the debugger accepted the command — where the program lands arrives later, as a stopped event — so each tool waits up to ~5s for it. If the step is still executing when that grace window elapses (e.g. stepping over a long-running call), the call still succeeds, but with state: "running", a top-level pending: true, and no location:
{
"success": true,
"state": "running",
"pending": true,
"message": "Step dispatched; the program is still executing after 5s (e.g. stepping over a long-running call). The step is still in effect: the session remains 'running' and will become 'paused' when it completes. Call wait_for_stop to block until then, or pause_execution to interrupt."
}The step is not cancelled: the session becomes "paused" on its own when it completes — call wait_for_stop to block until it does (or poll list_debug_sessions), or call pause_execution to interrupt it.
The adapter may append one sentence to that message explaining why the step may never land — the JavaScript adapter
does so for a step issued from a frame its skipFiles blackbox while smartStep is on (issue #678) — so match a
pending step by its pending: true flag, not by the message text.
Once a stop has been observed, the stop path owns the answer — a step that demonstrably landed is never reported as pending afterwards, at the cost of the response then being bounded by the stack-trace round trip rather than by the ~5s window. If the debuggee ends while the step is in flight the result is still a success, with a terminal state and a message such as "Step completed as session terminated.".
Stepping when the session is not paused returns success: false with state showing the session's actual state; stepping in a terminated session returns an application-level failure (see Error Handling).
Steps into function calls on the current line.
Parameters:
sessionId(string, required): The ID of the debug session.
Response:
{
"success": true,
"state": "paused",
"message": "Stepped into",
"location": { "file": "/path/app.py", "line": 4, "column": 1 }
}Same ~5s wait and pending: true contract as step_over — see Pending steps.
Steps out of the current function.
Parameters:
sessionId(string, required): The ID of the debug session.
Response:
{
"success": true,
"state": "paused",
"message": "Stepped out",
"location": { "file": "/path/app.py", "line": 18, "column": 1 }
}Same ~5s wait and pending: true contract as step_over — see Pending steps.
Continues execution until the next breakpoint or program end.
Parameters:
sessionId(string, required): The ID of the debug session.
Response:
{
"success": true,
"message": "Continued execution",
"state": "running"
}continue_execution returns as soon as the adapter acknowledges the resume; it does not wait for the next stop. state is the session state at that moment — normally "running", and "paused" when a breakpoint fired before the acknowledgement arrived. Call wait_for_stop to block until the program stops again or ends.
Error Response:
{
"code": -32603,
"message": "MCP error -32603: Failed to continue execution: Managed session not found: {sessionId}"
}Blocks until the session next pauses or ends, then says why and where. This is the explicit wait behind every pending: true answer and behind continue_execution, which returns as soon as the adapter acknowledges the resume — call it instead of polling list_debug_sessions.
Parameters:
sessionId(string, required): The ID of the debug session.timeout(number, optional): How long to wait, in milliseconds (default30000, max600000; larger values are clamped). Not a limit on the program: it keeps running under the debugger either way, and its breakpoints and exception filters stay armed. Your MCP client may enforce its own request timeout (60 s is a common default) — keeptimeoutbelow it (45 s slices are safe under a 60 s cap) and call again onpending: true. A wait that must outlast a slow build (a first Flutter Android build, say) is several slices, not one longtimeout: one long call fails on the client side first (-32001), and only that call — the program and its breakpoints are untouched, so the next call resumes the wait.
Response (the session is paused — it already was, or a stop arrived during the wait):
{
"success": true,
"state": "paused",
"sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
"message": "Paused (breakpoint) at /abs/path/app.py:42",
"lastStop": { "reason": "breakpoint", "threadId": 1, "timestamp": 1791213582583 },
"location": { "file": "/abs/path/app.py", "line": 42, "column": 1 },
"context": {
"lineContent": " total = price * qty",
"surrounding": [
{ "line": 41, "content": "def subtotal(price, qty):" },
{ "line": 42, "content": " total = price * qty" },
{ "line": 43, "content": " return total" }
]
},
"snapshot": {
"frames": [ { "name": "subtotal", "file": "/abs/path/app.py", "line": 42 } ],
"locals": { "price": "25.0", "qty": "5" }
}
}Response (the program ended):
{
"success": true,
"state": "stopped",
"sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
"message": "The program exited with code 3.",
"exitCode": 3
}When the program ends with no stop recorded in this launch and line breakpoints were set, the
answer carries the same run-to-completion summary as start_debugging (issues #701, #1033): the
message adds "without hitting any breakpoint. Verified but never hit: app.py:3." and
unhitBreakpoints lists every line breakpoint it ran past ({ file, line, verified }, plus
requestedLine when the adapter moved it). A program resumed past a stop keeps the plain message.
So does a session whose child sessions stopped during the run (issue #1128): its message counts
them — "The program ran to completion (exit code 0). Its child sessions stopped 2 times during this
run." — and lists no breakpoint as unhit: which breakpoints a child stopped on is the child's own
record, so the parent cannot say which were missed. The count is of every stop the agent could
see on a child (a breakpoint, an exception, a step), not of breakpoint hits alone.
Response (neither happened within timeout):
{
"success": true,
"state": "running",
"pending": true,
"sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
"message": "The program is still running after 30s without reaching 1 breakpoint(s). Nothing was cancelled: what is armed stays armed, and the session becomes 'paused' when the program gets there. Call wait_for_stop again to keep waiting, or pause_execution to interrupt it."
}Notes:
- The wait is on the session's state, not on the adapter's raw
stoppedevent, so a stop the server resumes by itself — the entry stop of astopOnEntry: falselaunch — is never reported. The answer is the first stoplist_debug_sessionswould also show. - A session that is already paused answers at once with its current stop, and asking twice without resuming returns the same stop twice. After
continue_execution(or a step that answeredpending: true) the session isrunning, so the next call waits for the next stop. lastStopis the recordlist_debug_sessionsreports:reason,rawReasonwhen the adapter's own reason was normalized,threadId, anddescription/textfor an exception — emitted as a secret-redacted copy, like variable values (issue #982).lastStop.exceptionInfois filled in a moment after an exception stop — re-querylist_debug_sessionsif it is absent.locationandcontextare best-effort: they appear when the stack could be read, andlocationis the first visible frame, as for the step tools;threadIdandnoteappear as forpause_execution(a pause collected by the wait is anchored the same way). The stop snapshot rides on every paused answer; here it carries noexceptionkey —lastStop.description/textalready are the exception.exitCodeis present only when the debuggee reported one. What the adapter read into the program's end joins the answer, its note inmessageand its data beside it under the adapter's own keys — a Flutterflutter runwhosedeviceIdmatched no device answersrequestedDeviceIdandavailableDevices(id,name,kind,targetPlatform, andsupportedByProject: falsewhen the project has no platform for the device) with a note that says to pass one of them, or flutter's own remedy (issue #877); a key the answer already has is never overwritten by the adapter's data;list_debug_sessionscarries the same note asendNotewith the data beside it. A session that ended in an error answersstate: "error"with the reason inmessage. A finished session keeps answering the same way until it is closed. When a stop landed during the wait and the program ended before it could be inspected — an uncaught exception and the exit arriving together — the ended answer carries that stop aslastStop; a stop from before the wait is not repeated.pending: truesits at the top level, as for the step tools. The message says what the wait is still for: with breakpoints (orbreakOnExceptions: "all") armed it names them and says they stay armed; with nothing armed it promises no pause — the program will stop only for an uncaught exception the debugger catches, or report its exit — so a caller is not sent round a loop that cannot end in a stop. When the launch runs with the debugger off (dapLaunchArgs.noDebug, honoured), the message says so and that the wait is for the program's exit; when the launch or attach itself has not completed, it says the session is still starting.- A session that was never started answers
success: false(Nothing to wait for: this session has not been started…). So does a session closed while the call was waiting, and atimeoutthat is not a positive number. - Several calls may wait on one session at once. A call whose request is cancelled, or whose client disconnects, releases its wait.
Sessions with children (the session tree, issue #1081): a wait on a session observes it and every descendant — present, or adopted while the wait runs — and answers for the one session a fixed priority names:
- the session itself, if it is paused or in error;
- else the first paused or errored descendant, depth first in adoption order (a child, then its own descendants, then the next child);
- else the session itself, if it has ended;
- else it keeps waiting — an ended descendant alone never completes an ancestor's wait.
The wait stays level-triggered: nothing is consumed, asking twice answers the same stop twice, two concurrent waiters agree, and the entry stop the server auto-continues is still never reported. One consequence: an errored descendant keeps being selected on every later wait until it is closed — close it to wait past it.
sessionIdis on every answer: the selected session's id, equal to the id asked for when the session itself is selected and on the pending answer. Everything else in the answer (lastStop,location,context,snapshot,threadId,note,exitCode, theprogramEnddata) is the ordinary stop answer for that session; usesessionIdfor inspection, stepping and continuing. An errored descendant answersstate: "error"with its id and the reason inmessage(Child session <id> ended in an error state…).children: [{ sessionId, name, state, parentSessionId }]whenever the asked session has descendants: every descendant, depth first in adoption order, each with its state and the session that spawned it — the pending answer included.childrenReleased: nwhen children reported on that session's connection during this run were released to run undebugged (beyondDEBUG_MCP_MAX_CHILD_SESSIONS, or an adoption that failed): a running total for the run, reset by a relaunch, never consumed. Every answer that carries it — paused, pending, ended or in error — ends itsmessagewith what the number means and why (issue #1137): "1 child process was released to run undebugged (child sessions are off: DEBUG_MCP_MAX_CHILD_SESSIONS=0): a breakpoint only it reaches will not hit." At the cap it reads "(the tree was at its child-session cap of 32, which DEBUG_MCP_MAX_CHILD_SESSIONS raises)". A pending answer's "what is armed stays armed" is therefore never the last word on a breakpoint that only a released child could reach.- A session with no children answers byte-for-byte as before plus
sessionId. A wait on a child answers for the child and its own descendants only.
A wait on the parent answering a child's stop (sessionId is the child's):
{
"success": true,
"state": "paused",
"sessionId": "c7e2a9b4-5d1f-4e8a-9b3c-2f6d8a1e0b47",
"message": "Paused (breakpoint) at /abs/path/child.py:12",
"lastStop": { "reason": "breakpoint", "threadId": 1, "timestamp": 1791213590211 },
"location": { "file": "/abs/path/child.py", "line": 12, "column": 1 },
"context": {
"lineContent": " result = work(item)",
"surrounding": [
{ "line": 11, "content": "def run(item):" },
{ "line": 12, "content": " result = work(item)" },
{ "line": 13, "content": " return result" }
]
},
"snapshot": {
"frames": [ { "name": "run", "file": "/abs/path/child.py", "line": 12 } ],
"locals": { "item": "3" }
},
"children": [
{ "sessionId": "c7e2a9b4-5d1f-4e8a-9b3c-2f6d8a1e0b47", "name": "Subprocess 48213", "state": "paused", "parentSessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7" }
]
}The pending answer lists the tree too, with sessionId naming the session asked for:
{ "success": true, "state": "running", "pending": true, "sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7", "message": "The program is still running after 30s …", "children": [ { "sessionId": "c7e2a9b4-5d1f-4e8a-9b3c-2f6d8a1e0b47", "name": "Subprocess 48213", "state": "running", "parentSessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7" } ] }Pauses a running program. The DAP pause request only acknowledges that the debugger accepted it — the stop arrives later, as an event — so the tool waits up to ~5s for that stop before answering.
Parameters:
sessionId(string, required): The ID of the debug session.threadId(number, optional): Thread to pause. Omitted or0means "all threads"; because some adapters (e.g. netcoredbg) rejectthreadId: 0, the server then asks the adapter for its thread list and pauses the first thread it reports, falling back to0when that request fails or returns nothing.
Response (the stop was observed inside the grace window):
{
"success": true,
"state": "paused",
"message": "Paused",
"stopReason": "pause",
"location": { "file": "/path/app.py", "line": 42, "column": 1 }, // the first visible frame; if the pause landed inside a hidden internal frame, get_stack_trace's pausedFrame names it
"context": { "lineContent": " total += price", "surrounding": ["…"] },
"snapshot": { "frames": [ { "name": "loop", "file": "/path/app.py", "line": 42 } ], "locals": { "total": "75.0", "price": "25.0" } },
"data": { "message": "Paused", "stopReason": "pause", "location": { "file": "/path/app.py", "line": 42, "column": 1 } }
}When the pause was reported on a thread with no program frame — on Windows, CodeLLDB reports it on the break-in thread the OS injects while the program's thread sits in main (issue #898) — the answer is anchored on the first thread whose stack reaches the program, and says so:
{
"success": true,
"state": "paused",
"message": "Paused",
"stopReason": "pause",
"rawStopReason": "exception",
"location": { "file": "C:\\proj\\examples\\cpp\\pause_test.cpp", "line": 20, "column": 36 },
"threadId": 45324,
"note": "The stop was reported on thread 31208 (5: tid=31208), whose 4 frames are all internal/runtime (top: 'DbgBreakPoint'); showing thread 45324 (1: tid=45324), which is in 'main' (…/pause_test.cpp:20)."
}When no thread has a program frame — an idle Node server's pause lands on a sourceless <anonymous> frame between event-loop dispatches (issue #900) — location is that frame and note reads No program frame is active on thread 0: the only frame is internal/runtime (top: '<anonymous>' <unknown_source>:1). The stop landed outside your code: continue_execution and trigger activity, or set a breakpoint; get_stack_trace with includeInternals: true lists the frames.
Response (the program had not stopped when the grace window elapsed):
{
"success": true,
"state": "running",
"pending": true,
"message": "Pause requested; no 'stopped' event within 5s (the program may be blocked in native code or a syscall). The pause is still in effect: the session will report 'paused' once the stop lands. Call wait_for_stop to block until then.",
"data": {
"message": "Pause requested; no 'stopped' event within 5s (the program may be blocked in native code or a syscall). The pause is still in effect: the session will report 'paused' once the stop lands. Call wait_for_stop to block until then.",
"pending": true
}
}The adapter may append one sentence explaining why the pause may never land — the JavaScript adapter does so for a
launch whose smartStep is on and whose skipFiles blackbox Node internals (issue #678) — so match a pending pause
by its pending: true flag, not by the message text.
Notes:
- The
"state"field is the session state at the moment the tool answers:"paused"once the stop has been observed, and"running"only on the pending path — the request was delivered but the program has not stopped yet. On that path, callwait_for_stopto block until the state flips to"paused"when the target next executes code (or polllist_debug_sessions). pending,message,stopReason/rawStopReason,location,threadId,noteandcontextsit at the top level, as for the step tools andwait_for_stop;datakeeps the controller's own copy (issue #906).locationis best-effort: it appears on the observed path when the post-stop stack trace could be read, and is the first visible frame.threadIdis present only when the answer is anchored on a thread other than the one the stop was reported on (and the stop named a thread), andnotesays why: apausereported on a thread with no program frame moves to the first sibling whose stack reaches the program (issue #898), or, when no thread has one, says that no program frame is active (issue #900). Breakpoint, step, exception and entry stops stay on the thread they were reported on.- When the stop is observed before the tool returns,
data.stopReasoncarries the (normalized) stop reason and — if the adapter reported a misleading raw reason that was normalized —data.rawStopReasoncarries the original. Example: CodeLLDB delivers an explicit pause via SIGSTOP and reports"exception"; the result isstopReason: "pause", rawStopReason: "exception". js-debug similarly reports pauses as"step". The same raw reason appears aslastStop.rawReasoninlist_debug_sessions. Stale stops from before the pause request are never echoed. - The observed path carries the stop snapshot beside the landing (issue #982).
- The session must be in a
"running"state; pausing an already-paused session returns success immediately with"Already paused"(plus the currentstopReason) — no landing read and no snapshot on that fast path;wait_for_stopanswers at once with both for the current stop. Any other state fails withCannot pause in state: <state>. - If the debuggee ends before the pause takes effect, the call still succeeds, reporting
"Session ended before pause took effect". - A session with no debuggable target to pause (e.g. a JavaScript attach whose target session was never adopted, or has ended) fails with an actionable error instead of hanging.
- After pausing, you can inspect variables, evaluate expressions, and step through code
Debuggers see everything in scope — including credentials — and when the debugging driver is an AI agent, variable values flow into model context and transcripts. By default, mcp-debugger masks credential-shaped values in every value-bearing surface: get_variables, get_local_variables, evaluate_expression results, captured output (get_output and the debug://sessions/{id}/output resource), the stop snapshot's locals and exception, and the stop record (lastStop as wait_for_stop, get_stack_trace and list_debug_sessions emit it — an exception message can carry a token too, issue #982).
Masking is per-token and labeled, so the rest of the value stays legible:
{ "name": "gh_token", "value": "<redacted:github-pat>", "type": "str", "redacted": true }Two detection layers apply:
- Value shapes — well-known token formats (GitHub/GitLab PATs, OpenAI/Anthropic-style
sk-keys, Slack, Stripe, AWS access key IDs, Google API keys, npm/PyPI/Hugging Face tokens, SendGrid, JWTs, PEM private-key blocks,Bearercredentials, connection-string passwords, URL userinfo passwords). Patterns are adapted from the MIT-licensed gitleaks corpus. - Sensitive names — a variable whose name exactly matches a known sensitive name (
password,api_key,client_secret, ...) has its whole value masked as<redacted:sensitive-name>, unless the value is trivial (None,'',0, ...) so "why is my token empty?" stays debuggable. Matching is exact after normalization, never substring —tokenCount,PATH, andpatienceare untouched.evaluate_expressiontreats the expression's final dot-segment as the name, soconfig.passwordis masked like the variablepassword.
Results that had values masked carry a redaction field ({ masked, notice } on variable/output tools; { rules, notice } on evaluate results) explaining that only the display is masked, not program state. Redacted variables and output entries are flagged redacted: true.
Opting out: start the server with DEBUG_MCP_NO_REDACT=1 to disable redaction entirely (e.g. when debugging credential-handling code itself). Adapter stderr sanitization (unconditional, whole-line) is unaffected by this flag.
Logs: the server's per-process log and the per-session proxy log (both under the temp directory, outliving the session) record a debuggee output event by its category and length only; the text itself is written only at --log-level debug, masked the same way get_output is. Other DAP event bodies are logged after the same sanitization as request payloads (environment blocks counted, token-shaped values masked).
Limitations: redaction is display-level protection against credentials leaking into transcripts, not a security boundary against a hostile agent — an agent can still compute over secrets via evaluate_expression side effects. Secrets split across separate output chunks, and generic high-entropy strings with no recognizable shape or name, are not detected. The expose_session IDE mirror shows raw, unredacted values — it serves a human's IDE, not the agent.
For security-sensitive deployments, DEBUG_MCP_VARIABLE_ACCESS=explicit disables bulk scope dumps: get_variables and get_local_variables require a names filter, so the agent must ask for specific variables instead of sweeping every value in scope into its context. An unfiltered call is rejected with a clear InvalidParams error that teaches the correct call shape; the tool schemas and the initialize instructions advertise the requirement.
The names parameter works in the default (open) mode too, as an ordinary filter:
- Exact-match and case-sensitive against variable names (DAP names are language identifiers).
- The response's
notFoundarray lists requested names absent from the scope, so "filtered out" is distinguishable from "doesn't exist". - Unrequested values are dropped in the session layer, before redaction and logging.
evaluate_expression is deliberately not restricted in explicit mode: least-privilege guards against indiscriminate bulk exposure, not against a determined agent — evaluate is already explicit-by-name, per-request, and audit-logged, which is exactly the access discipline the mode enforces, and removing the core debugging primitive would make the mode unusable. Secret redaction still applies to evaluate results.
Gets the current call stack.
Parameters:
sessionId(string, required): The ID of the debug session.threadId(number, optional): Inspect a specific thread fromlist_threads. A frame-bearing explicit thread becomes the anchor for follow-up scopes, locals, and evaluation calls.includeInternals(boolean, optional): Include runtime/framework frames. Defaults tofalse.
Response:
{
"success": true,
"threadId": 1,
"stackFrames": [
{
"id": 3,
"name": "swap_variables",
"file": "C:\\path\\to\\debug-mcp-server\\examples\\python_simple_swap\\swap_vars.py",
"line": 5,
"column": 1
},
{
"id": 4,
"name": "main",
"file": "C:\\path\\to\\debug-mcp-server\\examples\\python_simple_swap\\swap_vars.py",
"line": 21,
"column": 1
},
{
"id": 2,
"name": "<module>",
"file": "C:\\path\\to\\debug-mcp-server\\examples\\python_simple_swap\\swap_vars.py",
"line": 30,
"column": 1
}
],
"count": 3
}Notes:
- Stack frames are ordered from innermost (current) to outermost
threadIdidentifies the thread represented bystackFrames, or the explicitly queried thread when the stack is empty.lastStop.threadId, when present, remains the thread reported by the original stop event.stopReasonandlastStopdescribe the pause the frames were read at, so they are omitted whenever the session is not paused; they are taken from the state at the start of the call, so acontinue_executionracing the fetch does not strip them from an answer that really was read at a stop.- Frame IDs are used with
get_scopes - Internal/runtime frames (e.g. Node.js internals and
node_modulesdependencies, Go/runtime/,System.*) are filtered out by default; passincludeInternals: trueto see them. When any frames were hidden, the response additionally carrieshiddenFrames(count) and anoteexplaining how to reveal them. - When the top frame's file is a source the adapter binds through a compiled file that is older than it — a
.tsthrough its generated.js, a.javathrough its.class, a.csthrough the launched DLL, a native source through the executable the launch runs — thenoteends with the stale-build sentenceset_breakpointwarns with (issues #938, #950, #951): the line shown comes from the newer source and may not be the statement that runs. The top frame only, and not one whosefileis a<…>label or that carriesunresolvedSource: true; the same hedge applies as forset_breakpoint(mtimes only). - A frame with no openable source path on this host carries
unresolvedSource: true, and thenotesays itsfileis a label rather than a path — do not pass it toget_source_context— then adds what such a frame is under the session's adapter (issue #816): js-debug — a source-mapped.tsthe package did not ship, with the switch that shows the generated.jspaths instead (adapterConfig.sourceMaps: falseon attach,adapterLaunchConfig.sourceMaps: falseon launch); CodeLLDB (C/C++, Rust, COBOL) — a native frame without debug info (system libraries, CRT start-up) that the debugger names by symbol (@NtWaitForSingleObject), or a runtime built elsewhere whose relative path is not present here, where the program's own frames are the ones with a full path; Java — a class the JDI bridge names by package path (java/io/PrintStream.java) because no breakpoint has taught it the file's location. Adapters with nothing to add leave the neutral sentence alone. - The filtered stack is never empty when the adapter reported frames: if every frame is internal (e.g. a goroutine paused inside the Go runtime), the top internal frame is kept so
get_scopes/evaluate_expressionstill have a validframeId, and thenotesays so. - When the frame the debuggee is paused in is itself internal (a breakpoint or step that landed inside a
node_modulesdependency, say) while user frames survive further down, the response carriespausedFrame(id,name,file,line, andkept). On a breakpoint or step stop (adebugger;statement counts), or when an async boundary separates the paused frame from the first visible one (nothing below it can be evaluated), the paused frame is kept asstackFrames[0]so locals and evaluation anchor where the program actually stopped;hiddenFramescounts only the frames that stayed hidden. After apauseor an exception with a synchronous caller visible — stops that routinely land in runtime frames where the first user frame is the useful one — it stays hidden (kept: false) and thenotenames it; usepausedFrame.idas theframeIdto inspect it (issue #672). - When a
pauseis reported on a thread whose every frame is internal (on Windows, CodeLLDB reportspause_executionon the break-in thread the OS injects, an ntdll-only stack, while the program's thread sits inmain), the default stack is the first thread whose stack reaches the program instead, andnotesays which thread the stop was reported on and which is shown (issue #898). Breakpoint, step, exception and entry stops stay on the thread they were reported on. PassingthreadIdalways shows that thread and anchors the session there; the nextthreadId-less read on the same pause moves back and says so. Once the session is anchored off the reported thread (this, or an attach that moved off the break-in thread), everythreadId-less read notesThe stop was reported on thread X; inspection is anchored on thread Y. - When an explicit thread reports no frames, the response remains anchored to that thread and its
notesuggests a frame-bearing alternative when one is available. - When the implicit stopped thread is frameless, stack, locals, and default evaluation share one resolver. It scans siblings, prefers a thread whose frames the language policy recognizes as user code over runtime-only stacks, adopts it once, and discloses the switch in
note/anchorNote. - Failed or empty stack responses include the session's optional
diagnosticswhen the proxy failed, matchinglist_debug_sessions.
Gets variable scopes for a specific stack frame.
Parameters:
sessionId(string, required): The ID of the debug session.frameId(number, required): The ID of the stack frame fromget_stack_trace.
Response:
{
"success": true,
"scopes": [
{
"name": "Locals",
"variablesReference": 5,
"expensive": false,
"presentationHint": "locals",
"source": {}
},
{
"name": "Globals",
"variablesReference": 6,
"expensive": false,
"source": {}
}
]
}Important:
- The
variablesReferenceis what you pass toget_variablesas thescopeparameter - This is NOT the same as the frame ID!
Gets variables within a scope.
Parameters:
sessionId(string, required): The ID of the debug session.scope(number, required): ThevariablesReferencenumber from a scope or variable.names(string[], optional): Only return variables with these exact names (case-sensitive). Requested names missing from the scope are listed in the response'snotFound. Required in least-privilege mode.
Response:
{
"success": true,
"variables": [
{
"name": "a",
"value": "10",
"type": "int",
"variablesReference": 0,
"expandable": false
},
{
"name": "b",
"value": "20",
"type": "int",
"variablesReference": 0,
"expandable": false
}
],
"count": 2,
"variablesReference": 5
}Variable Properties:
variablesReference: 0 for primitive types, >0 for complex objects that can be expandedexpandable: Whether the variable has child properties- Values are always returned as strings
Size guards (issues #356/#359): responses are capped so an enormous scope (e.g. a JS internal frame reaching process/global) can't exceed an MCP client's per-result size limit. Per variable, values longer than 1024 chars are cut and flagged truncated: true; per call, at most 300 variables (256KB total) are returned. When anything was cut, the response carries a top-level truncation object — { omittedCount, valueTruncatedCount, notice } — and the names filter is the escape hatch to fetch specific variables in full. Limits are env-overridable: DEBUG_MCP_MAX_VARIABLE_VALUE_CHARS, DEBUG_MCP_MAX_VARIABLES, DEBUG_MCP_MAX_VARIABLES_TOTAL_CHARS.
Gets local variables for the session's shared inspection anchor. It walks down from the top frame and stops at the first frame that yields usable locals, then applies the language adapter's policy to that frame's scopes. Closures and outer-scope bindings come from fanning out across that one frame's scopes (Local, Block, Closure, Module), not from collecting every frame in the stack — walking past the answer is both wasteful and unsafe, since an unrelated lower-frame formatter can hang the whole inspection. When the anchor is not the top frame, frame names the one that was used. A convenience over calling stack→scopes→variables by hand.
Parameters:
sessionId(string, required): The ID of the debug session.includeSpecial(boolean, optional): Include special/internal variables likethis,__proto__,__builtins__, etc. Default: false.names(string[], optional): Only return variables with these exact names (case-sensitive). Requested names missing from the extracted locals are listed in the response'snotFound. Required in least-privilege mode.
Response:
{
"success": true,
"variables": [
{
"name": "x",
"value": "10",
"type": "int",
"variablesReference": 0,
"expandable": false
},
{
"name": "y",
"value": "20",
"type": "int",
"variablesReference": 0,
"expandable": false
}
],
"count": 2,
"frame": {
"name": "main",
"file": "C:\\path\\to\\script.py",
"line": 31
},
"scopeName": "Locals"
}Size guards: same caps and truncation advisory as get_variables; additionally, the multi-frame scope fan-out stops issuing DAP requests once the per-call variable budget is spent (truncation.scopesSkipped reports scopes never fetched). Top-frame scopes are fetched first, so the locals that matter are unaffected. The truncation counts describe only the returned payload — values cut while fetching fan-out scopes the policy then discarded (e.g. Global/Closure) are not reported (issue #438).
If the originally stopped thread has no frames, this tool uses the same user-frame-preferring adopted anchor as get_stack_trace. The response's anchorNote always names an automatic thread or lower-frame switch.
Example - Python:
// Request
{
"sessionId": "842ef9bb-037a-4d3c-960c-ad79a63ccfab",
"includeSpecial": false
}
// Response
{
"success": true,
"variables": [
{"name": "x", "value": "10", "type": "int", "variablesReference": 0, "expandable": false},
{"name": "y", "value": "20", "type": "int", "variablesReference": 0, "expandable": false}
],
"count": 2,
"frame": {
"name": "main",
"file": "C:\\path\\to\\test-scripts\\python_test_comprehensive.py",
"line": 31
},
"scopeName": "Locals"
}Example - JavaScript:
// Request
{
"sessionId": "ec46719a-68d9-4755-9c28-70478e0cde7d",
"includeSpecial": false
}
// Response
{
"success": true,
"variables": [
{"name": "x", "value": "10", "type": "number", "variablesReference": 0, "expandable": false}
],
"count": 1,
"frame": {
"name": "main",
"file": "c:\\path\\to\\test-scripts\\javascript_test_comprehensive.js",
"line": 40
},
"scopeName": "Local"
}Edge Cases:
// Empty locals
{
"success": true,
"variables": [],
"count": 0,
"frame": {"name": "<module>", "file": "script.py", "line": 2},
"scopeName": "Locals",
"message": "The Locals scope is empty."
}
// Session not paused
{
"success": false,
"error": "Session is not paused",
"message": "Cannot get local variables. The session must be paused at a breakpoint."
}Key Advantages:
- Single Call: Get local variables with one tool call instead of three (stack_trace → scopes → variables)
- Language-Aware Filtering: Automatically filters out internal/special variables based on language
- Consistent Format: Returns a consistent structure across Python and JavaScript
- Smart Defaults: By default, excludes noise like
__proto__,this,__builtins__unless explicitly requested
Language-Specific Behavior:
- Python: Looks for "Locals" scope, filters out
__builtins__, special variables, and internal debugger variables - JavaScript: Merges every block scope on the frame — js-debug names them
Block,Catch Block,With Block(or the legacyBlock:<label>) — ahead of the frame'sLocalscope, innermost block first, so aletdeclared in aforbody or acatch (e)binding is returned alongside the function's own locals and shadows them in the order JavaScript does (#558).scopeNamestaysLocalwith nonotewhenever the frame has a Local scope and any block or local scope produced variables, even ifLocalitself contributed nothing. On an ESM top-level frame (blocks but noLocal), the frame'sModule/Scriptscope joins the merge as its base andscopeNameis the block. An empty Local/block frame may fall through to Closure or Module on that same frame. A frame with no Local or block scope returns empty variables and ananchorNotedirecting you toget_scopes/get_variables; Global is never mislabeled as locals. Filters outthis,__proto__, and V8 internals. - Ruby: Reads rdbg's "Local variables" scope, hiding the
%selfpseudo-variable unlessincludeSpecial: true; a frame whose only local is%self(a native[C]frame) counts as empty - Other Languages: Falls back to generic behavior (first non-global scope)
Notes:
- Session must be paused at a breakpoint for this tool to work
- The tool walks down from the top frame only until a frame yields usable locals, then uses the adapter policy to extract them from that frame's scopes
- When the top frame has no usable locals (a runtime/stdlib frame, a
sleep), the response anchors to the first lower frame that does,framenames that frame, andnoteexplains the switch;notealso reports a same-frame scope fallback (e.g. JavaScript Local → Module). Passnamesto disable the frame walk-down - At a breakpoint or step inside a frame the stack display hides as internal (a dependency under
node_modules), the anchor is that frame — the one the program actually stopped in — andnotesays it was kept. After apauseinside such a frame the anchor stays on the first visible frame andnotenames the hidden paused frame and itsframeId(issue #672) - When
includeSpecialis true, all variables including internals are returned - This is especially useful for AI agents that need quick access to current local state
Evaluates an expression in the context of the current debug session.
Parameters:
sessionId(string, required): The ID of the debug session.expression(string, required): The expression to evaluate.frameId(number, optional): Authoritative stack frame ID for context. If omitted, evaluation uses the same adopted top frame asget_stack_traceandget_local_variables— at a breakpoint or step that is the frame the program stopped in even when the default stack display would hide it as internal (issue #672).anchorNoteexplains an automatic thread switch and any paused-frame decision (kept or hidden), andframenames the frame that was actually used — on failure too, so anUnable to evaluate on async stack frameerror says which frame it tried. Compareframewithget_stack_trace'spausedFrameto tell whether the evaluation ran in the frame the program stopped in.timeout(number, optional): Maximum time in milliseconds to wait for the evaluation to complete (default: 30000, max: 600000). On expiry the request fails but the expression may keep executing in the debuggee.
Response:
{
"success": true,
"result": "10",
"type": "int",
"variablesReference": 0,
"presentationHint": {},
"frame": { "name": "swap_variables", "file": "C:\\path\\to\\swap_vars.py", "line": 5 }
}frame is present when frameId was omitted and names the frame the expression ran in (the same shape get_local_variables reports).
Example - Simple Variable:
// Request (no frameId needed!)
{
"sessionId": "d507d6fb-45fc-4295-9dc0-4f44b423c103",
"expression": "x"
}
// Response
{
"success": true,
"result": "10",
"type": "int",
"variablesReference": 0
}Example - Arithmetic Expression:
// Request
{
"sessionId": "d507d6fb-45fc-4295-9dc0-4f44b423c103",
"expression": "x + y"
}
// Response
{
"success": true,
"result": "30",
"type": "int",
"variablesReference": 0
}Example - Complex Expression:
// Request
{
"sessionId": "d507d6fb-45fc-4295-9dc0-4f44b423c103",
"expression": "[i*2 for i in range(5)]"
}
// Response
{
"success": true,
"result": "[0, 2, 4, 6, 8]",
"type": "list",
"variablesReference": 4 // Can be expanded to see elements
}Error Handling:
// Request - undefined variable
{
"sessionId": "d507d6fb-45fc-4295-9dc0-4f44b423c103",
"expression": "undefined_variable"
}
// Response
{
"success": false,
"error": "Name not found: Traceback (most recent call last):\n File \"<string>\", line 1, in <module>\nNameError: name 'undefined_variable' is not defined\n"
}Important Notes:
- Automatic Frame Detection: When
frameIdis not provided, the tool automatically gets the current frame from the stack trace - Side Effects Are Allowed: Expressions CAN modify program state (e.g.,
x = 100). This is intentional and useful for debugging - Session Must Be Paused: The debugger must be stopped at a breakpoint for evaluation to work
- Results Are Strings: All results are returned as strings, even for numeric types
- Python Truncation: Python/debugpy automatically truncates collections at 300 items for performance
Gets source code context around a specific line in a file.
Parameters:
sessionId(string, required): The ID of the debug session.file(string, required): Path to the source file — absolute in host mode (a relative path is rejected); in container mode, relative to the/workspacemount.line(number, required): Line number to get context for (1-indexed).linesContext(number, optional): Number of lines before and after to include (default: 5).
Response:
{
"success": true,
"file": "C:\\path\\to\\script.py",
"line": 15,
"lineContent": " result = calculate_sum(x, y)",
"surrounding": [
{ "line": 12, "content": "def main():" },
{ "line": 13, "content": " x = 10" },
{ "line": 14, "content": " y = 20" },
{ "line": 15, "content": " result = calculate_sum(x, y)" },
{ "line": 16, "content": " print(f\"Result: {result}\")" },
{ "line": 17, "content": " return result" },
{ "line": 18, "content": "" }
],
"contextLines": 3
}Example:
{
"sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
"file": "C:\\path\\to\\test_script.py",
"line": 25,
"linesContext": 3
}Notes:
- Useful for AI agents to understand code structure without reading entire files
- Returns the requested line content and surrounding context
- Handles file boundaries gracefully (won't return lines before 1 or after EOF)
- Uses efficient line reading with LRU caching for performance
Gets the debuggee's output (stdout/stderr/console) captured for a session. Output is delivered by the debug adapter as DAP output events and buffered per launch (issue #218).
Parameters:
sessionId(string, required): The ID of the debug session.since(number, optional): Sequence cursor — only entries withseqgreater than this are returned. PassnextSincefrom the previous response to fetch only new output. Default:0(start of the buffer).limit(number, optional): Maximum entries to return (default: 100, max: 1000).
Response:
{
"success": true,
"sessionId": "a4d1acc8-84a8-44fe-a13e-28628c5b33c7",
"entries": [
{ "seq": 1, "category": "stdout", "output": "Factorial of 5: 120\n", "timestamp": 1754140800123 },
{ "seq": 2, "category": "stderr", "output": "warning: deprecated\n", "timestamp": 1754140800345 }
],
"nextSince": 2,
"hasMore": false,
"dropped": 0
}Notes:
- The buffer holds the last 1000 entries per launch; older entries are evicted and counted in
dropped. Individual entries longer than 8192 characters are cut and flagged"truncated": true. - Adapter-internal
telemetryevents are filtered out at capture time, as are output events an adapter policy declares to be pure adapter noise — e.g. LLDB's harmless DWARF-parser error spew on MinGW-built rust/cpp binaries (issue #361); suppressed lines remain visible in debug logs. All other categories (stdout,stderr,console,important, ...) are kept. Adapters that omit a category default toconsole. - Works while the program is running and after it finishes — output stays readable until
close_debug_session. Re-launching a session starts a fresh buffer (seq restarts at 1). - A child session of a JavaScript launch (the session tree, issue #1082) answers
outputOn: <launch session id>with anote: js-debug delivers the whole tree's program output on the launch session's connection, so the launch session's buffer is where its lines are; the child's ownentriescan still contain diagnostics (its resource reads the same way, a pointer line first). A debugpy child and a child of a JavaScript attach carry their own lines (an attach owns no pipes; js-debug's console capture then lands on each process's own connection) and answer nooutputOn. hasMore: truemeans more entries matched thanlimitallowed; call again withsince: nextSince.- Incremental polling recipe: call once, remember
nextSince, and pass it assinceon the next call — you'll only ever see new output. - Adapter support: Python (
redirectOutput), JavaScript (outputCapture: 'std'), Go (outputMode: 'remote'), and Java forward debuggee stdio as output events; .NET typically does as well. Ruby launch mode and Rust on Windows route debuggee stdio to the adapter process; the proxy forwards those lines as synthesizedstdout/stderrevents (#222/#223), excluding rdbg'sDEBUGGER:banners. Ruby attach captures nothing — the target's stdio stays wherever the process was started.
Each session also exposes its captured output as an MCP resource:
- URI:
debug://sessions/{sessionId}/output(text/plain) — the verbatim console transcript (all categories interleaved in arrival order). resources/listenumerates one output resource per session; the list changes on session create/close (notifications/resources/list_changed).resources/subscribeto a session's URI to receivenotifications/resources/updatedpings as output arrives. Pings are coalesced (~150 ms), so notification volume is independent of how fast the debuggee prints — on a ping, re-read the resource or callget_outputwith your cursor.- Subscriptions are tracked per MCP connection and cleaned up when the connection closes or the session is removed; the coalesced pings are fanned out to every connection that subscribed.
After a launch or attach creates its run directory, resources/list also includes debug://sessions/{sessionId}/proxy-log. Reading it returns at most the final 64 KiB, sanitized and trimmed to 80 lines. It is a point-in-time diagnostic resource and is intentionally not subscribable; output remains the only resource that emits update notifications.
Lists all supported debugging languages with metadata.
Parameters: None (empty object {})
Response:
{
"success": true,
"installed": ["python", "ruby", "mock"],
"available": [
{
"language": "ruby",
"package": "@debugmcp/adapter-ruby",
"installed": true,
"modes": {
"launch": { "supported": true, "available": false, "reason": "Ruby executable not found..." },
"attach": { "supported": true, "available": true, "by": ["port"] }
}
}
],
"languages": [
{
"id": "python",
"displayName": "Python",
"version": "1.0.0",
"requiresExecutable": true,
"defaultExecutable": "python"
}
],
"count": 3
}Notes:
installed[]keeps its historical meaning: adapter package present on disk (the metadata probe) and not disabled.countis its length. Whether the package can actually be used is whatmodessays.available[]carries one entry per known adapter, with per-mode availability (issue #331) and adescriptionwhen the adapter registry supplies one.supportedsays whether the adapter implements the mode at all;availablesays whether it is usable in this runtime right now (with areasonwhen it isn't).- When the adapter package is present but importing it failed — a dependency it imports is missing from the distribution, say (issue #795) — both modes read
available: falsewith the loader's own words as thereason:Failed to load adapter for '<lang>' from package '@debugmcp/adapter-<lang>'. The package is installed but importing it failed: Cannot find package '<dep>' imported from … — its dependency '<dep>' is missing or broken. Reinstall it (npm install @debugmcp/adapter-<lang>) or rebuild it.create_debug_sessionandstart_debuggingrefuse with the same reason, andmcp-debugger doctorreports the language as broken. A genuinely absent package readsAdapter package @debugmcp/adapter-<lang> is not installed.on the modes, and the loader'sAdapter not installed. Install with: npm install @debugmcp/adapter-<lang>when a launch is attempted anyway. - Attach for Python and Ruby is a direct connection to a debugpy/rdbg DAP socket, so it stays available even when the local toolchain is missing — the container image uses exactly this to offer Ruby attach without a Ruby runtime.
- Disabled languages (
DEBUG_MCP_DISABLE_LANGUAGES) stay listed with a disabled reason on both modes. modes.attach.bylists theattach_to_processaddressing forms the language accepts —"processId","port","adapterConfig"(a debugger URI underadapterConfig) — as its adapter declares them (the attach-addressing contract, issue #903); present on every attach-capable language, installed or not. A request using any other form is refused by name before the session changes state.languages[]is the backward-compatible metadata shape:id,displayName,version,requiresExecutable, anddefaultExecutablefor the languages that have one (mockdoes not).
Attaches the debugger to a running process. The target is verified after the handshake by polling for threads, whatever stopOnEntry says; if none are reported within verifyTimeout the attach fails and the proxy is torn down. With stopOnEntry: false the post-attach pause is skipped and the session is reported as running.
Parameters:
sessionId(string, required): The ID of the debug session.port(number, optional): Debug port to attach to. Refused by name on a PID-only language (C/C++, COBOL, .NET) — see Addressing by language.host(string, optional): Host to attach to; the adapter defaults it (127.0.0.1for every adapter but Java, which useslocalhost). A non-loopback host on a PID-only language is refused the same way.processId(number or string, optional): The PID of a process on this host, for the languages whose attach is local — see Addressing by language. For JavaScript (issue #871) the inspector of the running Node.js process is activated for you (SIGUSR1/process._debugProcess), confirmed to belong to that PID, and attached onport—9229, Node's default, unless the target was started with--inspect-port=<n>(then pass that port); a PID that is not a Node.js process, or a port held by another inspector, is refused by name. See the JavaScript guide.timeout(number, optional): Connection timeout in milliseconds, forwarded as-is: js-debug honours it (its own default is 10 s) and COBOL uses it as its manifest-build budget; the other adapters ignore it. A value longer thanverifyTimeoutmeans the verify deadline is what you hit.verifyTimeout(number, optional): How long to wait (ms) for the debugger to report at least one thread after attaching before failing the attach (default:20000, max:600000). Decrease for fast failure-by-design probes; increase for targets that are exceptionally slow to become debuggable.sourcePaths(string[], optional): Source paths for code mapping.stopOnEntry(boolean, optional): Request a pause immediately after attaching. Anything butfalse— including omitting it — pauses the target once the thread verification above succeeds;falseskips the post-attach pause (never the verification), and the attach returnsstate: "running". Passfalsewhen attaching to a live service you still need to use — this is the opposite ofstart_debugging, whosestopOnEntrydefaults tofalse. A pause that lands after the response is reported aspending: trueand named inmessage.justMyCode(boolean, optional): Only debug user code (skip library code).breakOnExceptions(string, optional):"uncaught","all", or"none"— same mode semantics as onstart_debugging, but attach never applies a language default: it stays"none"unless requested.adapterConfig(object, optional): Adapter-specific attach extras, merged into the attach config before the adapter transforms it, mirroringstart_debugging'sadapterLaunchConfig(C/C++/LLDB example:{"program": "/proc/1/root/pricer"}for symbol resolution from a kubectl-debug ephemeral container, orinitCommands; Python example:{"pathMappings": [{"localRoot": "/home/user/checkout/src", "remoteRoot": "/app"}]}so breakpoints at local-checkout paths bind against a remote debugpy, issue #450). Reserved keysrequest/__attachModeare ignored with a warning; setstopOnEntryvia the top-level parameter.
Addressing by language:
Which forms a language accepts is declared by its adapter (the attach-addressing contract, issue #903) and checked before anything starts: a form the language does not take is refused by name — with the adapter's remedy and the forms it does take — and the session stays usable. A request that names neither processId nor port on a language whose forms are only those two is refused as "needs port" / "needs processId"; a language that also takes a URI under adapterConfig leaves that case to its adapter. list_supported_languages shows each language's forms as modes.attach.by.
| Language | Attaches by | A processId |
A port (or a remote host) |
|---|---|---|---|
| C/C++, COBOL, .NET | processId only |
used | refused: CodeLLDB and netcoredbg attach to a local PID (a gdbserver-style remote target goes through C/C++ adapterConfig.processCreateCommands) |
| JavaScript | processId, port, or adapterConfig.websocketAddress (the inspector URL from /json/list; js-debug dials it directly, no port needed — issue #896) |
the running Node.js process's inspector is activated and attached on port (issue #871) |
used |
| Python, Ruby, Java | port only |
refused, with the recipe for opening a port (python -m debugpy --listen …, rdbg --open --port …, -agentlib:jdwp=…) |
used |
| Dart | port, adapterConfig.vmServiceUri / vmServiceInfoFile, or adapterConfig.deviceId alone for a Flutter app on an Android device or emulator (flutter attach discovers it, issue #882) |
refused, with the VM-service recipe | used — host/port becomes ws://host:port/ws, which needs --disable-service-auth-codes (the response warning says so) |
| Go, Rust, Mock | no attach | refused (Attach mode is not implemented) |
refused |
The same pass records where the target lives (processId → this host; port → loopback or remote by its host; a URI → by its hostname), which is what lets set_breakpoint content-address a local attach (see set_breakpoint).
Response:
{
"success": true,
"state": "paused",
"message": "Attached to process at 127.0.0.1:5678",
"location": { "file": "/abs/path/app.py", "line": 42, "column": 1 },
"context": { "lineContent": " total += price", "surrounding": ["…"] },
"snapshot": { "frames": [ { "name": "loop", "file": "/abs/path/app.py", "line": 42 } ], "locals": { "total": "75.0" } },
"data": {
"message": "Attached to process at 127.0.0.1:5678",
"location": { "file": "/abs/path/app.py", "line": 42, "column": 1 }
}
}A paused attach answer carries the same landing every stop answer carries (issue #982): location, threadId/note when there is something to say, the source line as context, the stale-build sentence, and the stop snapshot — the same shape as pause_execution, and create_debug_session's attach shortcut answers identically.
When the requested pause has not landed by the time the tool answers (an idle Node server, say — js-debug's pause lands on the next event-loop dispatch), the response says so:
{
"success": true,
"state": "running",
"pending": true,
"message": "Attached to process at 127.0.0.1:9229; post-attach pause pending — the target stops when it next executes code; call wait_for_stop to block until it does (pass stopOnEntry: false to attach without pausing)",
"data": {
"message": "Attached to process at 127.0.0.1:9229; post-attach pause pending — the target stops when it next executes code; call wait_for_stop to block until it does (pass stopOnEntry: false to attach without pausing)",
"pending": true
}
}Notes:
stateis"paused"only once a stopped event has actually been observed; otherwise the attach reports"running". When a requested post-attach pause is accepted but its stopped event has not arrived within the bounded wait, the response is successful withstate: "running"andpending: true(at the top level and indata) and themessagenames the pending pause; the late stopped event is the only transition topaused—wait_for_stopblocks until it arrives — and every paused session has alastStop.- An idle Node server may instead answer
pausedat once: js-debug lands the pause on a sourceless<anonymous>frame between event-loop dispatches. The attach answer then carries (at the top level and underdata)note: "No program frame is active on thread 0: the only frame is internal/runtime (top: '<anonymous>' <unknown_source>:1). The stop landed outside your code: continue_execution and trigger activity, or set a breakpoint; …",get_stack_traceshows that one frame withnoProgramFrame: trueand the same note, andget_local_variablesis empty (issue #900). Either way, attach to a live server you still need to use withstopOnEntry: false. - A
pausedattach answer reads where its stop landed through the same resolver read asget_stack_trace(issue #904): onestackTraceround trip, and on a thread whose stack has not materialised yet (a .NET post-attach pause) the bounded readiness waitget_stack_traceused to pay, now on the attach's own clock. On Windows, where CodeLLDB reported the attach stop on the injected break-in thread and the attach anchored on the program's thread instead, the answer'snotesaysThe stop was reported on thread X; inspection is anchored on thread Y. - The initial stop of an attach is reported as
lastStop.reason: "pause"(stopReason: "pause"inget_stack_trace) by every adapter, whether the debugger paused the target on request or, as CodeLLDB does, stopped it itself. On Windows, CodeLLDB reports that stop as theDebugBreakProcessbreak-in — an0x80000003exception on a helper thread — so the adapter's own words stay beside the pause aslastStop.rawReason: "exception"anddescription: "Exception 0x80000003 encountered at address …"(issue #817). Only the first stop of an attach that did not passstopOnEntry: falseis read this way: a break-in met later, or first by an attach that opted out of the initial stop, is a real__debugbreak()and keepsreason: "exception". - When
processIdwas used, the message readsAttached to process PID <pid>instead. - The response
warningreports two distinctadapterConfigkey outcomes (issues #450/#466): keys the adapter's attach transform genuinely drops (e.g. Python's ptvsd-eralocalRoot/remoteRoot— usepathMappings) are named as ignored, while keys mcp-debugger doesn't recognize are forwarded to the adapter as-is and named with an edit-distance suggestion for near-misses (pathMapping (did you mean pathMappings?)) — "ignored" means dropped, "forwarded as-is" means the adapter still sees them. The same field also carries the launch-style warning for function breakpoints still unverified at attach (issue #308), and the adapter's own attach-transform notes (issue #908): Dart's note that ahost/portattach built a no-authws://…/wsURI (and that adeviceIdis ignored on a Dart runner), COBOL's build options given withoutsourcesand its C-view-only note. - js-debug attach honors
adapterConfigtoo:localRoot/remoteRoot/sourceMaps/skipFiles/continueOnAttachand other js-debug attach options reach the debugger (issue #466). Two keys are defaulted when absent (issue #655):resolveSourceMapLocations: ["**", "!**/node_modules/**"](js-debug's own attach default collapses to "resolve everywhere", which applied every dependency's.js.mapand produced phantom../src/*.tsframes) andcwd(the server's working directory, or the workspace root in container mode — js-debug needs a base path before it resolves any source map's relativesources, even ones next to the generated file). Pass your own value, includingresolveSourceMapLocations: null, to override.skipFilesis deliberately left at js-debug's default: blackboxingnode_moduleswould turn apause_executionthat lands in framework code on an idle server into the endless step-chase of issue #513 —get_stack_tracehides those frames instead. - js-debug child auto-attach defaults on only for a target that can read both a temporary probe file created by the server and the installed js-debug bootloader files. Every attach form, including
processId, is checked: a sidecar can share PIDs without sharing files, and an SSH orkubectlport forward can use a loopback address without sharing the server's filesystem. When locality cannot be verified, including a failed probe,autoAttachChildProcessesdefaults tofalseand the response warns, preventing the injected server-local bootloader path from breaking later forks. An explicit boolean inadapterConfig.autoAttachChildProcessesis preserved; usetrueonly when the target and its children can load the server's js-debug bootloader and reach its debugger endpoint. justMyCodehas no effect on a JavaScript attach: js-debug has no such key, and the attach transform derives nothing from it (a launch derivesskipFilesandsmartStepfrom it — see start_debugging). SetadapterConfig.skipFiles/adapterConfig.smartStepyourself; attach defaultssmartSteptofalseand leavesskipFilesunset (issue #513), so pauses and steps land where the program is (issue #678).- Breakpoints set before the attach are re-sent once the debuggee-owning session is provably live, so their verified state in
list_breakpointsis authoritative. - Languages whose adapter has no attach implementation (
rust,go,mock) fail fast with a clear error. - A failed attach reports
success: falsewith the reason inmessage(Failed to attach: ...) and tears the proxy down, so no live proxy is left behind;stateis then"error"(or"stopped"if the session was already gone). When the adapter answers the handshake's attach request with its own error response, the attach fails at once in its words (issue #896):Failed to attach: The javascript debug adapter rejected the attach request: …. When the adapter itself ended the session while the attach was being verified — it exited, or dropped its DAP connection as js-debug does after failing to reach the inspector — the message says which and quotes the adapter's last stderr line (issue #897):Failed to attach: The debug adapter ended the session during attach verification: it closed its DAP connection; its last stderr line: "Error: Could not connect to debug target at http://localhost:5678: Could not find any debuggable target". When the failure happened during proxy initialization,datacarriesproxyLogPath,proxyLogResource, andinitProgressdiagnostics. Session-lifecycle failures (unknown or terminated session) return the standard application-levelerrorpayload instead — see Error Handling.
Detaches the debugger from an attached process, leaving the process running.
Parameters:
sessionId(string, required): The ID of the debug session.terminateProcess(boolean, optional): Terminate the process on detach (default:false).
Response:
{
"success": true,
"state": "stopped",
"message": "Detached from process (process still running)",
"data": {
"message": "Detached from process (process still running)"
}
}Notes:
- With
terminateProcess: truethe whole session is closed and the message readsDetached and terminated process. - Detaching sends DAP
disconnectwithterminateDebuggee: false, then stops the proxy; the session ends in"stopped"either way, so a later re-attach needs a new session. - CodeLLDB sessions on Windows (C/C++ and COBOL attach sessions, and any C/C++, Rust or COBOL launch detached this way): while the target is stopped, a DAP
continuegoes out ahead of the detachingdisconnect— on this path, onclose_debug_sessionand on shutdown alike — anddata.resumedBeforeDetachistrue; if the debugger refuses the continue, the detach still proceeds withresumedBeforeDetach: falseanddata.warningnames the risk. Why: CodeLLDB's Windows detach otherwise kills a target whose last resume was a step (issue #763; the mechanism is in the C/C++ guide's attach section). - Detaching a session with no active debug process reports
success: falsewithNo active debug session to detach frominmessage. - An attach root with child sessions (the session tree, issues #1081 and #1082; Python and JavaScript): the children are detached first, deepest first, each with
disconnectandterminateDebuggee: falseso it runs on undebugged, then the parent. The child rows stay listed asstoppedwithprogramEnd: { note: "Released: detached with its parent; the process runs on undebugged.", data: { released: true } }, and the answer carrieschildrenReleased: n— n children now run undebugged (top level and indata). WithterminateProcess: truethe whole subtree is closed and the answer carrieschildrenClosed: ninstead.
Lists all threads in the debugged process. Thread ids from here are what get_stack_trace's and pause_execution's threadId parameters accept.
Parameters:
sessionId(string, required): The ID of the debug session.
Response:
{
"success": true,
"threads": [
{ "id": 1, "name": "MainThread" },
{ "id": 2, "name": "worker-0" }
]
}Notes:
- Requires a live debug process: an unknown or terminated session, or one whose proxy is not running, returns an application-level failure (see Error Handling).
- A failed DAP
threadsresponse is propagated as an error rather than flattened into an empty-but-successful list (issue #124).
The DAP mirror (issue #217) lets a human attach an IDE — VS Code, nvim-dap, any DAP client — to an agent-owned debug session as a read-only second client. The agent debugs a process no IDE launched (CI, a container, a terminal-driven run), parks it at an interesting point, and hands over host/port/token; the human lands on the live paused frame and inspects real state. Execution control stays with the MCP session.
Starts a per-session DAP server endpoint on 127.0.0.1 (ephemeral port) inside the session's debug proxy. Requires an active session (launched or attached); works while running or paused. Idempotent — calling it again returns the same endpoint and token.
Parameters:
sessionId(string, required): The debug session ID
Response:
{
"success": true,
"state": "paused",
"host": "127.0.0.1",
"port": 52341,
"token": "kx3P…32-char-random…",
"message": "Session exposed for IDE attach at 127.0.0.1:52341. VS Code: add a launch.json config … and start it. …"
}The endpoint closes on unexpose_session, close_debug_session, restart_debugging, or debuggee exit. list_debug_sessions shows exposure: {host, port} for exposed sessions (never the token).
Note: the mirror serves a human's IDE and proxies DAP responses directly, so it shows raw variable values — secret redaction does not apply to this surface.
Connecting VS Code:
debugServer points VS Code directly at a running DAP server; extra properties like mirrorToken are passed through in the attach request, where the mirror validates them.
| Session language | VS Code type |
|---|---|
| python | debugpy (python is the extension's deprecated alias) |
| javascript | node |
| java | java |
| go | go |
| rust | lldb |
| dotnet | coreclr |
| ruby | rdbg |
Other DAP clients (nvim-dap, etc.): connect a TCP DAP client to the host/port and include mirrorToken in the attach request arguments.
Measured (2026-11-02, VS Code 1.116.0 with ms-python.debugpy 2026.6.0, a Python launch paused at a breakpoint): F5 on the config above — type: debugpy, no connect block, nothing else — lands the IDE on the paused frame with the call stack, Locals and the Debug Console's evaluate served live from the adapter; the IDE follows every stop the MCP session drives (step_over, continue_execution to the next breakpoint); pressing Continue or Step in VS Code shows the mirror's refusal as a warning (The mcp-debugger mirror is read-only; 'continue' is controlled by the primary session) and the IDE stays attached; unexpose_session ends the IDE's debug session cleanly, with no error dialog.
What the mirror serves:
- Answered locally:
initialize(from the adapter's real capabilities, with control affordances masked off),attach/launch(token check),configurationDone(also the trigger for the late-join stopped replay below),disconnect(that client only),cancel. - Forwarded to the live adapter:
threads,stackTrace,scopes,variables,source,evaluate,exceptionInfo,loadedSources,modules. - Soft-succeeded so IDE attach flows survive:
setBreakpoints/setFunctionBreakpoints(reported unverified — breakpoints stay agent-owned),setExceptionBreakpoints. - Rejected with a quiet error:
continue,next,stepIn,stepOut,pause,setVariable,restart,terminate, and every other control or mutation request.
On attach while the session is paused, the mirror replays the last stop as a stopped event, so the IDE lands directly on the paused frame. The replay is delivered when the client sends configurationDone (standard DAP handshake order); clients that skip configurationDone receive it after a short (~200ms) fallback. Live events, by contrast, are broadcast to every attached client immediately.
Security: the endpoint binds loopback only and every client must present the per-expose token. Treat the token as a debuggee-execution capability, not a view-only credential — evaluate is forwarded, and DAP evaluate can run arbitrary code in the debuggee. The token appears only in the expose_session result and is redacted from logs. "Read-only" means execution control and breakpoint changes are rejected, not that the debuggee is immutable.
Container note: when the server runs inside a container, the mirror listens on the container's loopback — a host IDE cannot reach it without extra networking (docker run --network host on Linux, or a socat/ssh forward into the container). The same applies to any deployment where the MCP server host is not the IDE host.
Closes the mirror endpoint and disconnects any attached IDE clients (they receive a terminated event). A no-op success when the session is not exposed.
Parameters:
sessionId(string, required): The debug session ID
Response:
{
"success": true,
"state": "paused",
"wasExposed": true,
"message": "Mirror endpoint closed (1 client disconnected)"
}Apply changed code to the running program and keep its state — the adapter's own live update. Two adapters have one today; the tool is refused by name on the others.
- Flutter (a
flutter run/flutter attachsession): the SDK adapter's hot reload. The reassemble runsbuild(), so a breakpoint there is reported by this call as the stop, with itslocationandsnapshot(measured: ~150 ms after the request on desktop; on web the adapter holds its response until the paused isolate resumes, which is why the call answers the stop rather than waiting for the response). Widget state, counters and timers survive. A reload sent to a paused program answers at once and lands when the program resumes (notesays so). Before the app has reportedappStarted, and on a plain Dart program, the call is refused by name. - Java: class redefinition (JDI
VirtualMachine.redefineClasses(), HotSwap) — the formerredefine_classestool, whose answer is unchanged.
redefine_classes is a deprecated alias of this tool: still accepted, answered with a warning, not advertised by tools/list, removed at 1.0.
Parameters:
sessionId(string, required): The debug session ID.classesDir(string, Java only): Absolute path to the compiled classes directory (e.g.,build/classes/java/main/). Required on Java; refused on Flutter.sinceTimestamp(number, Java only, optional): Unix timestamp in milliseconds. Only redefine.classfiles modified after this time.0or omitted = scan all files.timeout(number, optional): Maximum time in milliseconds to wait for the update to complete (default: 30000, max: 600000). Increase when hot-swapping many classes at once.
Response (Flutter, the reassemble reached a breakpoint):
{
"success": true,
"state": "paused",
"reason": "breakpoint",
"location": { "file": "/app/lib/main.dart", "line": 30, "function": "_ProbeAppState.build" },
"snapshot": { "frames": ["…"], "locals": ["…"] }
}A reload whose program is still running when the hold elapses answers state: "running" with pending: true — call wait_for_stop for a stop it may land on.
Response (Java):
{
"success": true,
"state": "paused",
"redefined": ["com.example.Foo", "com.example.Bar"],
"redefinedCount": 2,
"skippedNotLoaded": 3,
"failedCount": 1,
"failed": [
{ "fqcn": "com.example.Baz", "error": "UnsupportedOperationException: class redefinition failed: attempted to add a method" }
],
"scannedFiles": 6,
"newestTimestamp": 1711500000000
}Example — Flutter:
{ "sessionId": "abc-123" }Example — Java, full scan:
{
"sessionId": "abc-123",
"classesDir": "/project/build/classes/java/main"
}Example — Java, incremental scan (pass newestTimestamp from the previous call):
{
"sessionId": "abc-123",
"classesDir": "/project/build/classes/java/main",
"sinceTimestamp": 1711500000000
}Notes:
- Statement-anchored breakpoints are re-resolved against the new source after the update (
anchorResolutionreports moved/stale, asrestart_debuggingdoes); moved ones are re-sent so they bind against the new line table. - Java: classes must already be loaded in the target JVM — unloaded classes are skipped (
skippedNotLoaded); schema changes (adding/removing methods or fields) fail for individual classes without blocking others; thenewestTimestampin the response enables incremental workflows. - Java: the session can be paused or running when calling this tool. Flutter: the app must have started.
- Hot restart (re-run
main()in the same process) isrestart_debuggingon a Flutter session.
Tools can return errors in two formats:
- MCP transport errors: Standard JSON-RPC error responses with numeric error codes. These indicate protocol-level failures.
- Application-level failures: JSON payloads with
{ "success": false, "error": "..." }. Most tool failures use this format, where the HTTP/transport layer succeeds but the operation itself failed.
-32603: Internal error (feature not implemented, unexpected failures)-32602: Invalid parameters (e.g., missingsessionId)
Session-lifecycle failures (unknown/terminated session, proxy not running) are application-level failures: they return { "success": false, "error": "..." } rather than an MCP transport error. Classification is based on typed lifecycle errors, never on words in an error message; an invalid path, expression, or breakpoint containing text such as closed or not found remains its original MCP error.
{
"code": -32603,
"name": "McpError",
"message": "MCP error -32603: {specific error message}",
"stack": "{stack trace}"
}{
"success": false,
"error": "Session is not paused",
"message": "Cannot get local variables. The session must be paused at a breakpoint."
}- Session not found: Occurs when a session terminates unexpectedly or was closed. A session the idle policy closed (
MCP_DEBUG_SESSION_IDLE_MS, seecreate_debug_session) says so instead:{ "success": false, "error": "Session … was closed by the idle policy at … Its program was ended. Create a new session to continue.", "closedBy": "idle-policy" }. The sentence about the program is one of three: "Its program was ended." (a launched program the policy ended), "The process it was attached to was released and keeps running.", or "It had no running program by then." (the session never launched, or its program was already over). An id created by another MCP client of the same process is found like any other; one outside the caller's scope (a future authenticatedhttpmode; today every caller shares one scope) does not exist for that caller — every tool answers it exactly as it answers an id nobody holds (issue #1025) - Invalid language: Language must be one of the supported languages (discovered dynamically from installed adapters)
- File not found: When setting breakpoints in non-existent files
- Invalid scope: When passing wrong variablesReference to get_variables
start_debugging reports success: false when the proxy or adapter enters an error state during readiness and includes available proxy-log diagnostics. A program that exits cleanly before the debugger pauses is still a successful result with state: "stopped".
- Always check session state before performing operations
- Use absolute paths for files to avoid ambiguity
- Get scopes before variables - you need the variablesReference
- Handle session termination gracefully - sessions can end unexpectedly
- Set breakpoints on executable lines - avoid comments and declarations