<< All versions

Skill v1.0.2

currentLLM-judged scan90/100
debugmcp/mcp-debugger/debugging

1 files

──Details
PublishedOctober 19, 2026 at 07:18 AM
Content Hashsha256:9f362f2658652611...
Git SHA1e857844730c
Bump Typepatch
Compare with v1.0.1
──Files
Files (1 file, 16.4 KB)
SKILL.md16.4 KBactive
SKILL.md · 126 lines · 16.4 KB

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)

text
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>"} -> 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 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
wait_for_stop {sessionId} -> blocks until the next stop or the exit (continue_execution does not wait)
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, ...). continue_execution returns at once — call wait_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 (answers pending), send the request, wait_for_stop. The same goes for any step, pause or attach that answers pending: true. wait_for_stop takes a timeout in ms (default 30000); when it runs out it answers pending: true again — 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 long timeout, 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; 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

  1. State a hypothesis about where reality diverges from expectation before setting breakpoints.
  2. 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, C/C++ and COBOL (paragraph, section or PROGRAM-ID names) — not Ruby or Dart/Flutter, which reject it up front (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).
  1. 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, C/C++, COBOL — where {WS-NAME} is a COBOL data reference — and Dart/Flutter, where a condition on the logpoint is ignored and every hit logs; Java, .NET and Ruby reject it with a clear error).
  2. At each pause, record what you learned (variable values, actual control flow), not just where you are.
  3. When the diverging line is found, inspect every input to that line before concluding — the bug is usually an operand, not the operator.
  4. 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):

text
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 (wait_for_stop blocks until it does); 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
  • JavaScript: target ran node --inspect=<host>:<port> ... → attach by host/port; a plain node app.js on the same host → attach by processId (mcp-debugger opens its inspector on 9229 — pass port too 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 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)
  • COBOL: attach by PID like C/C++, plus adapterConfig: {sources: ["<abs .cob>", …], dialect: "ibm"} (the manifest is regenerated by a translate-only cobc -C; pass the options the binary was built with) or adapterConfig: {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 a dart --enable-vm-service … run target prints, or flutter run --machine's app.debugPort event) or adapterConfig: {vmServiceInfoFile: "<file written by --write-service-info>"}; a bare host/port only works for a VM started with --disable-service-auth-codes. The dart-test/flutter-test runners refuse attach. An isolate idle in an await shows an empty stack at the attach pause — set a breakpoint and continue_execution, or attach with stopOnEntry: 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:

text
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:

LanguageReferenceHeadline quirk
Pythonreferences/python.mdexpand "special variables" containers; late breakpoint verification
JavaScript/TSreferences/javascript.mdchild-session architecture; internals filtered from stacks
Rubyreferences/ruby.mdstops at load only with stopOnEntry (rdbg continues by itself otherwise); attach captures no stdout
Rustreferences/rust.mdGNU toolchain on Windows; scriptPath = source file, adapter finds Cargo project
Goreferences/go.mdDelve native DAP; optimized-binary locals warning
Javareferences/java.mdjavac -g required; FQCN breakpoints; redefine_classes hot-swap
.NET/C#references/dotnet.mdscriptPath = compiled .dll; Portable PDB required
C/C++references/cpp.mdscriptPath = binary (-gdwarf-4 -O0) or lone .c/.cpp (auto-compiled); attach by PID; MinGW/DWARF on Windows
COBOLreferences/cobol.mdscriptPath = .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/Flutterreferences/dart.mdscriptPath = .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
← v1.0.1All versions