Skill v1.0.2
currentLLM-judged scan90/1001 files
version: "1.0.2" name: mcp-debugger description: 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#, C/C++, COBOL, and Dart/Flutter, locally or attached to remote processes.
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"} -> sessionId2. set_breakpoint {sessionId, file: "<ABSOLUTE path>", statement: "<line text or distinctive substring>"} (or line: N + expectedContent)3. start_debugging {sessionId, scriptPath: "<ABSOLUTE path>"} -> paused at the breakpoint, or pending: true -> wait_for_stop {sessionId}4. get_stack_trace {sessionId} -> frames (use frame.id, never assume 0)5. get_scopes {sessionId, frameId: <frame.id>} -> scope variablesReference6. get_variables {sessionId, scope: <variablesReference>}... or get_local_variables {sessionId} for the common case7. evaluate_expression {sessionId, expression: "x + y"}8. step_over / step_into / step_out / continue_executionwait_for_stop {sessionId} -> blocks until the next stop or the exit (continue_execution does not wait)9. get_output {sessionId} -> captured debuggee stdout/stderr10. close_debug_session {sessionId} -> ALWAYS, even on failure
Rules that prevent 90% of failed sessions:
- Absolute paths only for
fileandscriptPath(relative paths are rejected in host mode). - Use real frame IDs. Take
idfromget_stack_traceframes; it is adapter-assigned and is not 0-indexed. - Expand variable containers. If a variable entry carries a
variablesReference, callget_variablesagain with that reference to see children (Python's "special variables", object fields, array elements). - Respect session state. Stepping, evaluation, and variable reads require
PAUSED. Aftercontinue_executionthe session isRUNNING; after a step or breakpoint hit it returns toPAUSEDwith a persisted stop reason telling you why it stopped (breakpoint,step,entry,exception, ...).continue_executionreturns at once — callwait_for_stop {sessionId}to block until the program stops again or ends; it answers with the stop (lastStop,location) or the exit code. - A `start_debugging` that answers `running` with `pending: true` is not stuck. The launch holds its answer only briefly — about a second — for the program's first stop, so a breakpoint reached as the program starts is answered in the one call, and anything later is collected with
wait_for_stop {sessionId}, which blocks until the program stops or ends. Nothing was cancelled: breakpoints stay armed for as long as the program runs, and the message says what is armed. For a server, this is the workflow: set the breakpoint in the handler,start_debugging(answerspending), send the request,wait_for_stop. The same goes for any step, pause or attach that answerspending: true.wait_for_stoptakes atimeoutin ms (default 30000); when it runs out it answerspending: trueagain — just call it again. Keep each call under your MCP client's own request cap (45 s is safe under the common 60 s cap): a longer wait is several calls, not one longtimeout, or the client times out first. - 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; aredactionfield 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 withDEBUG_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 innotFound) instead of dumping the scope.evaluate_expressionstill 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_breakpointsshows what is currently set (with verified state and adapter ids).
- Prefer
statement: "<line text>"over line numbers: it matches like an Edit-toolold_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 responsewarning), lists every occurrence on ambiguity (addnearLineto pick one), and re-resolves acrossrestart_debuggingafter you edit the file. When you do address by line, passexpectedContent: "<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 sayingrequested line N, bound to line Mmeans 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, C/C++ and COBOL (paragraph, section or PROGRAM-ID names) — not Ruby or Dart/Flutter, which reject it up front (Java accepts baremethod,Class.method, or fully-qualified names and binds every concrete overload; JavaScript names are dotted runtime paths likeobj.methodbound 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_breakpointwithlogMessage: "x={x}"streams interpolated values intoget_outputwithout stopping the program (Python/JS/Go/Rust, C/C++, COBOL — where{WS-NAME}is a COBOL data reference — and Dart/Flutter, where aconditionon the logpoint is ignored and every hit logs; 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; readget_outputfromsince: 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
stopOnEntrymeanstrue— the opposite ofstart_debugging). PassstopOnEntry: falsefor a live service you must not freeze. A response withpending: truemeans the pause lands when the target next runs code (wait_for_stopblocks until it does);continue_executionreleases it. - Python: target ran
python -m debugpy --listen <host>:<port> ...; to address breakpoints by local-checkout path, map it onto the debuggee tree withadapterConfig: {pathMappings: [{localRoot: "<abs local>", remoteRoot: "/app"}]} - Ruby: target ran
rdbg --open --port <port> ...(works throughkubectl port-forward);localfsMap: "/app:<abs local dir>"maps paths - JavaScript: target ran
node --inspect=<host>:<port> ...→ attach byhost/port; a plainnode app.json the same host → attach byprocessId(mcp-debugger opens its inspector on 9229 — passporttoo if the target set--inspect-port=<n>) - 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 asfileneeds 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 useprocessId: 1withprogram: "/proc/1/root/<binary path>"(on Linux, mindkernel.yama.ptrace_scope) - COBOL: attach by PID like C/C++, plus
adapterConfig: {sources: ["<abs .cob>", …], dialect: "ibm"}(the manifest is regenerated by a translate-onlycobc -C; pass the options the binary was built with) oradapterConfig: {manifestDirs: ["<dir holding *.cobol-symbols.json>"]}(an earlier source launch's.debug-mcp/cobol/<name>/<buildKey>/, needs no cobc) so variables are COBOL-shaped; without either only the engine's C view is available. A job paused inside libcob still shows its program's data division (scopes from the nearest COBOL frame up the stack) - Dart/Flutter: attach by VM-service URI, never by PID —
attach_to_process {sessionId, adapterConfig: {vmServiceUri: "ws://127.0.0.1:<port>/<token>=/ws"}}(the URI adart --enable-vm-service … runtarget prints, orflutter run --machine'sapp.debugPortevent) oradapterConfig: {vmServiceInfoFile: "<file written by --write-service-info>"}; a barehost/portonly works for a VM started with--disable-service-auth-codes. Thedart-test/flutter-testrunners refuse attach. An isolate idle in anawaitshows an empty stack at the attach pause — set a breakpoint andcontinue_execution, or attach withstopOnEntry: false
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.textcarry the exception class and message; where the adapter supports it (Python, JS, Java, .NET),lastStop.exceptionInfoaddsexceptionId,breakMode, and details (it lands a moment after the pause — re-query if absent). After termination,exitCodeinlist_debug_sessionsdistinguishes a crash (non-zero) from a clean exit.
Current limitations (be honest with yourself)
pause_executionsupport 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
truncationfield says what was cut and suggests narrowing withnames: [...]or a targetedevaluate_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 | stops at load only with stopOnEntry (rdbg continues by itself otherwise); 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 | |
| COBOL | references/cobol.md | scriptPath = .cob/.cbl source (auto-compiled with GnuCOBOL) or prebuilt exe; dapLaunchArgs {dialect, format, copybookDirs, runtimeChecks, stdinFile}; COBOL-shaped variables; breakpoints in copybooks; paragraph function breakpoints; {WS-NAME} logpoints; PERFORM-aware step_over/step_out | |
| Dart/Flutter | references/dart.md | scriptPath = .dart entrypoint; runner auto-detected from pubspec.yaml + path (dart / dart-test / flutter / flutter-test; runner overrides); Flutter device via deviceId; attach by VM-service URI, never PID; step_over stops twice per line; no function breakpoints; a logpoint's condition is ignored |