Debugging with mcp-debugger
mcp-debugger exposes real language debuggers as MCP tools. Prefer it over print-debugging whenever you would otherwise need more than one edit-run cycle to see program state: a breakpoint plus evaluate_expression answers in one run what printf answers in three.
When to reach for the debugger
- A test fails and the assertion message doesn't explain why the value is wrong.
- Control flow surprises you (a branch that "can't happen", a loop that exits early).
- State mutates somewhere between two known-good points and you need to bisect.
- The bug lives in code you can't easily edit (third-party package, compiled artifact).
- You need ground truth about runtime types/values instead of inferring them from source.
Do NOT reach for it when a single glance at the code or one log line would answer the question — session setup costs a few seconds and the target must be runnable.
The golden path (launch)
1. create_debug_session {language: "python"} -> sessionId
2. set_breakpoint {sessionId, file: "<ABSOLUTE path>", statement: "<line text or distinctive substring>"} (or line: N + expectedContent)
3. start_debugging {sessionId, scriptPath: "<ABSOLUTE path>"}
4. get_stack_trace {sessionId} -> frames (use frame.id, never assume 0)
5. get_scopes {sessionId, frameId: <frame.id>} -> scope variablesReference
6. get_variables {sessionId, scope: <variablesReference>}
... or get_local_variables {sessionId} for the common case
7. evaluate_expression {sessionId, expression: "x + y"}
8. step_over / step_into / step_out / continue_execution
9. get_output {sessionId} -> captured debuggee stdout/stderr
10. close_debug_session {sessionId} -> ALWAYS, even on failure
Rules that prevent 90% of failed sessions:
- Absolute paths only for
file and scriptPath (relative paths are rejected in host mode).
- Use real frame IDs. Take
id from get_stack_trace frames; it is adapter-assigned and is not 0-indexed.
- Expand variable containers. If a variable entry carries a
variablesReference, call get_variables again with that reference to see children (Python's "special variables", object fields, array elements).
- Respect session state. Stepping, evaluation, and variable reads require
PAUSED. After continue_execution the session is RUNNING; after a step or breakpoint hit it returns to PAUSED with a persisted stop reason telling you why it stopped (breakpoint, step, entry, exception, ...).
- Breakpoints may verify late. Some adapters (debugpy, JDI) report breakpoints unverified until the module/class loads; that is normal, not an error.
<redacted:...> placeholders are masking, not program state. Credential-shaped values and values of sensitive variable names (password, api_key, ...) are masked by default in variable/evaluate/output results; a redaction field reports what was hidden. The real value is intact in the debuggee — don't "fix" it, and don't retry the read. The user can disable masking by restarting the server with DEBUG_MCP_NO_REDACT=1.
- If
get_variables demands names, the server is in least-privilege mode (DEBUG_MCP_VARIABLE_ACCESS=explicit): pass the exact variable names you need (names: ["user", "total"]; case-sensitive, misses reported in notFound) instead of dumping the scope. evaluate_expression still works for targeted reads.
- Always
close_debug_session when done — it tears down the debuggee process tree.
Root-cause discipline
- State a hypothesis about where reality diverges from expectation before setting breakpoints.
- Set at most two breakpoints: last-known-good and first-known-bad. Run, inspect, halve the interval. Bisection beats stepping line-by-line from the top. Move the window mid-session with
remove_breakpoint / clear_breakpoints; list_breakpoints shows what is currently set (with verified state and adapter ids).
- Prefer
statement: "<line text>" over line numbers: it matches like an Edit-tool old_string (whole line or a distinctive substring — whitespace-trimmed, trailing comments ignored, exact matches win), only lands on a line containing your text (inexact or multi-candidate matches are flagged in the response warning), lists every occurrence on ambiguity (add nearLine to pick one), and re-resolves across restart_debugging after you edit the file. When you do address by line, pass expectedContent: "<line text or distinctive substring>" (trailing comments ignored) so a stale or off-by-one line number fails immediately with the actual nearby lines. A response saying requested line N, bound to line M means the adapter moved the breakpoint — trust the bound line.
function: "name" breaks on entry to a symbol with no file or line at all — names survive edits best. Supported by Python/Go/Rust/.NET/Java/JavaScript and C/C++ (Java accepts bare method, Class.method, or fully-qualified names and binds every concrete overload; JavaScript names are dotted runtime paths like obj.method bound to the current function value — main-module function declarations bind at launch, functions in lazily-loaded modules bind at the next pause).
- When pausing is too disruptive (hot loops, live or attached processes), use a logpoint:
set_breakpoint with logMessage: "x={x}" streams interpolated values into get_output without stopping the program (Python/JS/Go/Rust and C/C++; Java, .NET and Ruby reject it with a clear error).
- At each pause, record what you learned (variable values, actual control flow), not just where you are.
- When the diverging line is found, inspect every input to that line before concluding — the bug is usually an operand, not the operator.
- Fix, then
restart_debugging {sessionId} — one call relaunches with the same configuration and re-applies every breakpoint (the output buffer resets; read get_output from since: 0). Confirm the observed state changed as predicted. Works even after the program exited; attach sessions are rejected (detach and re-attach instead).
Program output
get_output {sessionId} returns buffered debuggee stdout/stderr with a cursor: pass the returned nextSince back as since to read only new output. Each session also exposes the transcript as MCP resource debug://sessions/{id}/output with subscription support. Caveat: Ruby attach sessions capture no stdout (launch sessions stream it) — when attached to Ruby, verify behavior via evaluate_expression/breakpoints or have the program write a file.
Attach instead of launch
For an already-running process (including remote machines, containers, and Kubernetes pods via port-forward):
attach_to_process {sessionId, host: "localhost", port: 5678, sourcePaths: ["<local src>"], adapterConfig: {...}}
- Attach pauses the target by default (omitting
stopOnEntry means true — the opposite of start_debugging). Pass stopOnEntry: false for a live service you must not freeze. A response with pending: true means the pause lands when the target next runs code; continue_execution releases it.
- Python: target ran
python -m debugpy --listen <host>:<port> ...; to address breakpoints by local-checkout path, map it onto the debuggee tree with adapterConfig: {pathMappings: [{localRoot: "<abs local>", remoteRoot: "/app"}]}
- Ruby: target ran
rdbg --open --port <port> ... (works through kubectl port-forward); localfsMap: "/app:<abs local dir>" maps paths
- Java: target JVM has
-agentlib:jdwp=transport=dt_socket,server=y,address=*:<port>; breakpoints in not-yet-loaded classes are deferred automatically, and a fully-qualified class name as file needs no source files at all
- C/C++ (and other native): attach by PID instead of port —
attach_to_process {sessionId, processId: <pid>, adapterConfig: {program: "<path to binary>"}}; in a Kubernetes ephemeral debug container use processId: 1 with program: "/proc/1/root/<binary path>" (on Linux, mind kernel.yama.ptrace_scope)
Breakpoint paths on attach are sent verbatim and resolved against the target's filesystem (host-side existence checks are skipped). Without a mapping, use debuggee-side paths — get_stack_trace shows the paths the target uses — or address by symbol ({function: "name"}), which needs no paths. adapterConfig keys the adapter cannot forward into its attach request are named in the response's warning.
Direct-connect attaches (debugpy, rdbg) need no local language toolchain — the debug engine runs inside the target. list_supported_languages reports per-mode availability (modes.launch / modes.attach) with reasons.
detach_from_process leaves the target running; close_debug_session after detach cleans up the session.
For Kubernetes pods, don't re-derive attach configs: the Kubernetes recipe (pattern decision table, path rules) and the per-language attach presets have verified copy-paste calls.
IDE mirror (let a human look around)
When a human wants to inspect your live session in their IDE — CI flake parked at the failing state, a long-running attach session that hit an anomaly — expose it:
expose_session {sessionId} -> {host: "127.0.0.1", port, token}
Relay the endpoint with a ready-to-paste VS Code config: {"name": "Mirror", "type": "<language's debug type>", "request": "attach", "debugServer": <port>, "mirrorToken": "<token>"}. Their IDE attaches read-only and lands directly on the paused frame: stacks, scopes, variables, and evaluate all work; stepping, continuing, and breakpoint changes are rejected — execution control stays with you. unexpose_session {sessionId} disconnects IDE clients and closes the endpoint (it also closes on session close/restart/exit). Loopback-only; the token is required and should be treated as sensitive.
Crash diagnosis
- Launch sessions pause at uncaught exceptions by default (
breakOnExceptions: "uncaught") with the stack and locals live instead of losing the session — pass "none" to opt out, or "all" to also stop on caught raises (language-dependent). Ruby is the exception: rdbg has no uncaught-only filter, so Ruby crashes still run to termination unless you pass "all". Attach sessions apply no default — pass the mode explicitly.
- On an exception stop,
lastStop.description/lastStop.text carry the exception class and message; where the adapter supports it (Python, JS, Java, .NET), lastStop.exceptionInfo adds exceptionId, breakMode, and details (it lands a moment after the pause — re-query if absent). After termination, exitCode in list_debug_sessions distinguishes a crash (non-zero) from a clean exit.
Current limitations (be honest with yourself)
pause_execution support varies by adapter; prefer breakpoints over pausing a free-running program.
- Variable responses are size-guarded (values truncated past ~1KB, capped variable counts/response size, all env-tunable); a
truncation field says what was cut and suggests narrowing with names: [...] or a targeted evaluate_expression.
Language specifics
Read the matching reference before your first session in a language — each has load-bearing quirks:
| Language |
Reference |
Headline quirk |
| Python |
references/python.md |
expand "special variables" containers; late breakpoint verification |
| JavaScript/TS |
references/javascript.md |
child-session architecture; internals filtered from stacks |
| Ruby |
references/ruby.md |
entry pause auto-continued; attach captures no stdout |
| Rust |
references/rust.md |
GNU toolchain on Windows; scriptPath = source file, adapter finds Cargo project |
| Go |
references/go.md |
Delve native DAP; optimized-binary locals warning |
| Java |
references/java.md |
javac -g required; FQCN breakpoints; redefine_classes hot-swap |
| .NET/C# |
references/dotnet.md |
scriptPath = compiled .dll; Portable PDB required |
| C/C++ |
references/cpp.md |
scriptPath = binary (-gdwarf-4 -O0) or lone .c/.cpp (auto-compiled); attach by PID; MinGW/DWARF on Windows |
1---2name: mcp-debugger3description: Use when investigating a bug, failing test, or unexpected runtime behavior and the mcp-debugger MCP server is available — drives real step-through debuggers (breakpoints, stack traces, variable inspection, expression evaluation) for Python, JavaScript/TypeScript, Ruby, Rust, Go, Java, .NET/C#, and C/C++, locally or attached to remote processes.4---56# Debugging with mcp-debugger78mcp-debugger exposes real language debuggers as MCP tools. Prefer it over print-debugging whenever you would otherwise need more than one edit-run cycle to see program state: a breakpoint plus `evaluate_expression` answers in one run what printf answers in three.910## When to reach for the debugger1112- A test fails and the assertion message doesn't explain *why* the value is wrong.13- Control flow surprises you (a branch that "can't happen", a loop that exits early).14- State mutates somewhere between two known-good points and you need to bisect.15- The bug lives in code you can't easily edit (third-party package, compiled artifact).16- You need ground truth about runtime types/values instead of inferring them from source.1718Do NOT reach for it when a single glance at the code or one log line would answer the question — session setup costs a few seconds and the target must be runnable.1920## The golden path (launch)2122```text231. create_debug_session {language: "python"} -> sessionId242. set_breakpoint {sessionId, file: "<ABSOLUTE path>", statement: "<line text or distinctive substring>"} (or line: N + expectedContent)253. start_debugging {sessionId, scriptPath: "<ABSOLUTE path>"}264. get_stack_trace {sessionId} -> frames (use frame.id, never assume 0)275. get_scopes {sessionId, frameId: <frame.id>} -> scope variablesReference286. get_variables {sessionId, scope: <variablesReference>}29 ... or get_local_variables {sessionId} for the common case307. evaluate_expression {sessionId, expression: "x + y"}318. step_over / step_into / step_out / continue_execution329. get_output {sessionId} -> captured debuggee stdout/stderr3310. close_debug_session {sessionId} -> ALWAYS, even on failure34```3536Rules that prevent 90% of failed sessions:3738- **Absolute paths only** for `file` and `scriptPath` (relative paths are rejected in host mode).39- **Use real frame IDs.** Take `id` from `get_stack_trace` frames; it is adapter-assigned and is not 0-indexed.40- **Expand variable containers.** If a variable entry carries a `variablesReference`, call `get_variables` again with that reference to see children (Python's "special variables", object fields, array elements).41- **Respect session state.** Stepping, evaluation, and variable reads require `PAUSED`. After `continue_execution` the session is `RUNNING`; after a step or breakpoint hit it returns to `PAUSED` with a persisted stop reason telling you why it stopped (`breakpoint`, `step`, `entry`, `exception`, ...).42- **Breakpoints may verify late.** Some adapters (debugpy, JDI) report breakpoints unverified until the module/class loads; that is normal, not an error.43- **`<redacted:...>` placeholders are masking, not program state.** Credential-shaped values and values of sensitive variable names (`password`, `api_key`, ...) are masked by default in variable/evaluate/output results; a `redaction` field reports what was hidden. The real value is intact in the debuggee — don't "fix" it, and don't retry the read. The user can disable masking by restarting the server with `DEBUG_MCP_NO_REDACT=1`.44- **If `get_variables` demands `names`, the server is in least-privilege mode** (`DEBUG_MCP_VARIABLE_ACCESS=explicit`): pass the exact variable names you need (`names: ["user", "total"]`; case-sensitive, misses reported in `notFound`) instead of dumping the scope. `evaluate_expression` still works for targeted reads.45- **Always `close_debug_session`** when done — it tears down the debuggee process tree.4647## Root-cause discipline48491. State a hypothesis about where reality diverges from expectation *before* setting breakpoints.502. Set at most two breakpoints: last-known-good and first-known-bad. Run, inspect, halve the interval. Bisection beats stepping line-by-line from the top. Move the window mid-session with `remove_breakpoint` / `clear_breakpoints`; `list_breakpoints` shows what is currently set (with verified state and adapter ids).51 - Prefer `statement: "<line text>"` over line numbers: it matches like an Edit-tool `old_string` (whole line or a distinctive substring — whitespace-trimmed, trailing comments ignored, exact matches win), only lands on a line containing your text (inexact or multi-candidate matches are flagged in the response `warning`), lists every occurrence on ambiguity (add `nearLine` to pick one), and re-resolves across `restart_debugging` after you edit the file. When you do address by line, pass `expectedContent: "<line text or distinctive substring>"` (trailing comments ignored) so a stale or off-by-one line number fails immediately with the actual nearby lines. A response saying `requested line N, bound to line M` means the adapter moved the breakpoint — trust the bound line.52 - `function: "name"` breaks on entry to a symbol with no file or line at all — names survive edits best. Supported by Python/Go/Rust/.NET/Java/JavaScript and C/C++ (Java accepts bare `method`, `Class.method`, or fully-qualified names and binds every concrete overload; JavaScript names are dotted runtime paths like `obj.method` bound to the current function value — main-module function declarations bind at launch, functions in lazily-loaded modules bind at the next pause).533. When pausing is too disruptive (hot loops, live or attached processes), use a **logpoint**: `set_breakpoint` with `logMessage: "x={x}"` streams interpolated values into `get_output` without stopping the program (Python/JS/Go/Rust and C/C++; Java, .NET and Ruby reject it with a clear error).544. At each pause, record what you *learned* (variable values, actual control flow), not just where you are.555. When the diverging line is found, inspect every input to that line before concluding — the bug is usually an operand, not the operator.566. Fix, then `restart_debugging {sessionId}` — one call relaunches with the same configuration and re-applies every breakpoint (the output buffer resets; read `get_output` from `since: 0`). Confirm the observed state changed as predicted. Works even after the program exited; attach sessions are rejected (detach and re-attach instead).5758## Program output5960`get_output {sessionId}` returns buffered debuggee stdout/stderr with a cursor: pass the returned `nextSince` back as `since` to read only new output. Each session also exposes the transcript as MCP resource `debug://sessions/{id}/output` with subscription support. Caveat: Ruby **attach** sessions capture no stdout (launch sessions stream it) — when attached to Ruby, verify behavior via `evaluate_expression`/breakpoints or have the program write a file.6162## Attach instead of launch6364For an already-running process (including remote machines, containers, and Kubernetes pods via port-forward):6566```text67attach_to_process {sessionId, host: "localhost", port: 5678, sourcePaths: ["<local src>"], adapterConfig: {...}}68```6970- **Attach pauses the target by default** (omitting `stopOnEntry` means `true` — the opposite of `start_debugging`). Pass `stopOnEntry: false` for a live service you must not freeze. A response with `pending: true` means the pause lands when the target next runs code; `continue_execution` releases it.71- **Python**: target ran `python -m debugpy --listen <host>:<port> ...`; to address breakpoints by local-checkout path, map it onto the debuggee tree with `adapterConfig: {pathMappings: [{localRoot: "<abs local>", remoteRoot: "/app"}]}`72- **Ruby**: target ran `rdbg --open --port <port> ...` (works through `kubectl port-forward`); `localfsMap: "/app:<abs local dir>"` maps paths73- **Java**: target JVM has `-agentlib:jdwp=transport=dt_socket,server=y,address=*:<port>`; breakpoints in not-yet-loaded classes are deferred automatically, and a fully-qualified class name as `file` needs no source files at all74- **C/C++ (and other native)**: attach by PID instead of port — `attach_to_process {sessionId, processId: <pid>, adapterConfig: {program: "<path to binary>"}}`; in a Kubernetes ephemeral debug container use `processId: 1` with `program: "/proc/1/root/<binary path>"` (on Linux, mind `kernel.yama.ptrace_scope`)7576Breakpoint paths on attach are sent **verbatim** and resolved against the **target's** filesystem (host-side existence checks are skipped). Without a mapping, use debuggee-side paths — `get_stack_trace` shows the paths the target uses — or address by symbol (`{function: "name"}`), which needs no paths. `adapterConfig` keys the adapter cannot forward into its attach request are named in the response's `warning`.7778Direct-connect attaches (debugpy, rdbg) need no local language toolchain — the debug engine runs inside the target. `list_supported_languages` reports per-mode availability (`modes.launch` / `modes.attach`) with reasons.7980`detach_from_process` leaves the target running; `close_debug_session` after detach cleans up the session.8182For Kubernetes pods, don't re-derive attach configs: the [Kubernetes recipe](https://github.com/debugmcp/mcp-debugger/blob/main/docs/kubernetes.md#breakpoints-and-paths-on-attach-read-this-once) (pattern decision table, path rules) and the per-language [attach presets](https://github.com/debugmcp/mcp-debugger/blob/main/examples/kubernetes/attach-presets.md) have verified copy-paste calls.8384## IDE mirror (let a human look around)8586When a human wants to inspect your live session in their IDE — CI flake parked at the failing state, a long-running attach session that hit an anomaly — expose it:8788```text89expose_session {sessionId} -> {host: "127.0.0.1", port, token}90```9192Relay the endpoint with a ready-to-paste VS Code config: `{"name": "Mirror", "type": "<language's debug type>", "request": "attach", "debugServer": <port>, "mirrorToken": "<token>"}`. Their IDE attaches read-only and lands directly on the paused frame: stacks, scopes, variables, and evaluate all work; stepping, continuing, and breakpoint changes are rejected — execution control stays with you. `unexpose_session {sessionId}` disconnects IDE clients and closes the endpoint (it also closes on session close/restart/exit). Loopback-only; the token is required and should be treated as sensitive.9394## Crash diagnosis9596- Launch sessions pause at uncaught exceptions **by default** (`breakOnExceptions: "uncaught"`) with the stack and locals live instead of losing the session — pass `"none"` to opt out, or `"all"` to also stop on caught raises (language-dependent). Ruby is the exception: rdbg has no uncaught-only filter, so Ruby crashes still run to termination unless you pass `"all"`. Attach sessions apply no default — pass the mode explicitly.97- On an exception stop, `lastStop.description`/`lastStop.text` carry the exception class and message; where the adapter supports it (Python, JS, Java, .NET), `lastStop.exceptionInfo` adds `exceptionId`, `breakMode`, and details (it lands a moment after the pause — re-query if absent). After termination, `exitCode` in `list_debug_sessions` distinguishes a crash (non-zero) from a clean exit.9899## Current limitations (be honest with yourself)100101- `pause_execution` support varies by adapter; prefer breakpoints over pausing a free-running program.102- Variable responses are size-guarded (values truncated past ~1KB, capped variable counts/response size, all env-tunable); a `truncation` field says what was cut and suggests narrowing with `names: [...]` or a targeted `evaluate_expression`.103104## Language specifics105106Read the matching reference before your first session in a language — each has load-bearing quirks:107108| Language | Reference | Headline quirk |109|---|---|---|110| Python | references/python.md | expand "special variables" containers; late breakpoint verification |111| JavaScript/TS | references/javascript.md | child-session architecture; internals filtered from stacks |112| Ruby | references/ruby.md | entry pause auto-continued; attach captures no stdout |113| Rust | references/rust.md | GNU toolchain on Windows; scriptPath = source file, adapter finds Cargo project |114| Go | references/go.md | Delve native DAP; optimized-binary locals warning |115| Java | references/java.md | javac -g required; FQCN breakpoints; redefine_classes hot-swap |116| .NET/C# | references/dotnet.md | scriptPath = compiled .dll; Portable PDB required |117| C/C++ | references/cpp.md | scriptPath = binary (-gdwarf-4 -O0) or lone .c/.cpp (auto-compiled); attach by PID; MinGW/DWARF on Windows |