From 52acecacd9299bdede027d5e1b4e3de31872a35d Mon Sep 17 00:00:00 2001 From: Derek Parker Date: Wed, 11 Mar 2026 13:49:28 -0700 Subject: [PATCH 1/2] feat: add debugging workflow guidance via MCP prompts, skills, and docs Adds four complementary layers of workflow guidance for AI assistants using mcp-dap-server, covering source debug, attach, core dump, and binary/assembly scenarios: - prompts.go: 4 MCP prompt handlers (debug-source, debug-attach, debug-core-dump, debug-binary) registered via registerPrompts(), each returning a structured step-by-step tool invocation guide - docs/superpowers/skills/: 4 Claude Code skill files with trigger conditions, decision trees, and root-cause patterns per scenario - docs/debugging-workflows.md: human-readable reference with decision table, Mermaid workflow diagrams, and common gotchas - CLAUDE.md: documents prompts.go, skill files, and workflow reference - main.go: wires registerPrompts() into server startup Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 43 +- docs/debugging-workflows.md | 211 ++++++++ docs/superpowers/skills/debug-attach.md | 174 ++++++ docs/superpowers/skills/debug-binary.md | 235 ++++++++ docs/superpowers/skills/debug-core-dump.md | 202 +++++++ docs/superpowers/skills/debug-source.md | 160 ++++++ main.go | 2 + prompts.go | 588 +++++++++++++++++++++ 8 files changed, 1614 insertions(+), 1 deletion(-) create mode 100644 docs/debugging-workflows.md create mode 100644 docs/superpowers/skills/debug-attach.md create mode 100644 docs/superpowers/skills/debug-binary.md create mode 100644 docs/superpowers/skills/debug-core-dump.md create mode 100644 docs/superpowers/skills/debug-source.md create mode 100644 prompts.go diff --git a/CLAUDE.md b/CLAUDE.md index 715ac08..c3e6ef6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,13 @@ This is an MCP (Model Context Protocol) server that bridges MCP clients with DAP **main.go**: MCP server initialization - Creates the MCP server using the `go-sdk` - Registers all debugging tools via `registerTools()` -- Exposes SSE (Server-Sent Events) transport on port 8080 +- Registers workflow prompts via `registerPrompts()` +- Exposes stdio transport + +**prompts.go**: MCP prompt implementations +- 4 prompt handlers for guided debugging workflows (source, attach, core dump, binary) +- Registered via `server.AddPrompt()` — no session state, always available +- Each prompt returns a `GetPromptResult` with step-by-step tool invocation guidance **tools.go**: MCP tool implementations (~1200 lines) - All MCP tools are methods on `debuggerSession` struct @@ -199,3 +205,38 @@ func TestSomething(t *testing.T) { - `github.com/modelcontextprotocol/go-sdk` - MCP server framework - Requires `dlv` (Delve debugger) in `$PATH` for Go debugging - Optional: `OpenDebugAD7` (cpptools) for GDB debugging (set `MCP_DAP_CPPTOOLS_PATH` or install ms-vscode.cpptools) + +## Workflow Guidance + +### MCP Prompts + +The server exposes 4 prompts (via `prompts/list` and `prompts/get`) that return guided debugging workflows: + +| Prompt | Required Args | Use for | +|--------|--------------|---------| +| `debug-source` | `path` | Debugging Go/C/C++ from source | +| `debug-attach` | `pid` | Attaching to a running process | +| `debug-core-dump` | `binary_path`, `core_path` | Post-mortem crash analysis | +| `debug-binary` | `path` | Assembly-level binary debugging | + +Prompts are registered in `prompts.go` via `registerPrompts()`, called from `main.go`. + +### Claude Code Skills + +Four skills live in `docs/superpowers/skills/` for use with the Claude Code Superpowers plugin: + +| Skill file | Trigger | +|-----------|---------| +| `debug-source.md` | Debugging from source code | +| `debug-attach.md` | Attaching to a running process | +| `debug-core-dump.md` | Analyzing a core dump | +| `debug-binary.md` | Assembly-level binary debugging | + +To register skills with Claude Code, configure the `docs/superpowers/skills/` directory as a skills source in your Superpowers plugin settings. + +### Human Reference + +See `docs/debugging-workflows.md` for: +- Decision table: scenario → mode → which prompt/skill to use +- Mermaid workflow diagrams for each scenario +- Common gotchas and patterns per scenario diff --git a/docs/debugging-workflows.md b/docs/debugging-workflows.md new file mode 100644 index 0000000..c937016 --- /dev/null +++ b/docs/debugging-workflows.md @@ -0,0 +1,211 @@ +# Debugging Workflows + +This document provides guidance for choosing and executing the right debugging workflow with `mcp-dap-server`. + +## Quick Decision Table + +| Scenario | Mode | Debugger | Tool / Prompt | +|----------|------|----------|---------------| +| Debug Go source code | `source` | `delve` | `debug-source` prompt / skill | +| Debug C/C++ source code | `binary`* | `gdb` | `debug-source` prompt / skill | +| Attach to running process | `attach` | `delve` or `gdb` | `debug-attach` prompt / skill | +| Analyze a crash (core dump) | `core` | `delve` or `gdb` | `debug-core-dump` prompt / skill | +| Debug a compiled binary | `binary` | `delve` or `gdb` | `debug-binary` prompt / skill | + +*GDB does not support compiling from source — compile with `gcc -g -O0` first. + +--- + +## Scenario Workflows + +### 1. Live Source Debugging (Go) + +```mermaid +flowchart TD + A[Identify bug or behavior to investigate] --> B[debug(mode='source', path=..., debugger='delve')] + B --> C[Set breakpoints at suspected locations] + C --> D[continue()] + D --> E{Stopped at breakpoint?} + E -- yes --> F[context() — inspect location, stack, variables] + E -- no --> G[Program terminated — check for panic/error] + F --> H{Values correct?} + H -- yes --> I[step(mode='over') or continue() to next point] + H -- no --> J[evaluate() to inspect specific values] + I --> H + J --> K[Identify root cause] + K --> L[stop()] +``` + +**Key tools:** `debug`, `breakpoint`, `continue`, `step`, `context`, `evaluate`, `info`, `stop` + +**Typical sequence:** +1. `debug(mode="source", path="/path/to/main.go")` +2. `breakpoint(file="/path/to/file.go", line=42)` +3. `continue()` — runs to breakpoint +4. `context()` — inspect full state +5. `evaluate(expression="variableName")` — drill into specifics +6. `step(mode="in")` — follow execution into a suspicious function +7. `stop()` — when done + +--- + +### 2. Live Attach Debugging + +```mermaid +flowchart TD + A[Find PID: ps aux or pgrep] --> B[debug(mode='attach', processId=PID)] + B --> C[context() — what is the process doing right now?] + C --> D[info(kind='threads') — check all goroutines/threads] + D --> E{Multiple threads suspicious?} + E -- yes --> F[context(threadId=N) for each suspicious thread] + E -- no --> G[Set targeted breakpoints at suspicious functions] + F --> H{Deadlock pattern?} + H -- yes --> I[Document lock ordering issue] + H -- no --> G + G --> J[continue() — let process run to breakpoint] + J --> K[Inspect state at breakpoint] + K --> L[Conclude and stop()] +``` + +**Key tools:** `debug`, `pause`, `context`, `info`, `breakpoint`, `continue`, `evaluate`, `stop` + +**Typical sequence:** +1. `debug(mode="attach", processId=12345)` +2. `context()` — immediate state +3. `info(kind="threads")` — all threads +4. `context(threadId=)` — per suspicious thread +5. `pause()` then `context()` — sample execution multiple times for CPU diagnosis +6. `stop()` + +--- + +### 3. Post-Mortem Core Dump Analysis + +```mermaid +flowchart TD + A[Locate binary and core dump file] --> B[debug(mode='core', path=..., coreFilePath=...)] + B --> C[context() — crash location, signal, variables] + C --> D[Check signal type] + D -- SIGSEGV --> E[Look for nil pointers or OOB access] + D -- SIGABRT --> F[Look for panic message or assert failure] + D -- SIGFPE --> G[Look for division by zero or overflow] + E --> H[evaluate() to inspect suspicious variables] + F --> H + G --> H + H --> I[Walk up call stack with context(frameId=N)] + I --> J[Find where bad value originated] + J --> K[info(kind='threads') — check other goroutines] + K --> L[Formulate root cause] + L --> M[stop()] +``` + +**Key tools:** `debug`, `context`, `evaluate`, `info`, `stop` + +**Typical sequence:** +1. `debug(mode="core", path="/path/to/binary", coreFilePath="/path/to/core")` +2. `context()` — crash frame and signal +3. `evaluate(expression="suspiciousVar")` — inspect values +4. `context(frameId=1)`, `context(frameId=2)` — walk the stack +5. `info(kind="threads")` — check other goroutines +6. `stop()` + +> **Remember:** You cannot step forward in a core dump. All observations are read-only. + +--- + +### 4. Binary / Assembly-Level Debugging + +```mermaid +flowchart TD + A[Have compiled binary, no source] --> B[debug(mode='binary', path=..., stopOnEntry=true)] + B --> C[context() — get instruction pointer address] + C --> D[disassemble(address=..., count=40)] + D --> E[Identify interesting call or branch] + E --> F[breakpoint(function='*0xADDR')] + F --> G[continue()] + G --> H[context() + evaluate registers] + H --> I{Understand the logic?} + I -- yes --> J[Document findings] + I -- no --> D + J --> K[stop()] +``` + +**Key tools:** `debug`, `context`, `disassemble`, `breakpoint`, `continue`, `step`, `evaluate`, `stop` + +**Typical sequence:** +1. `debug(mode="binary", path="/path/to/binary", stopOnEntry=true)` +2. `context()` — get current instruction pointer +3. `disassemble(address="0x", count=40)` — read the code +4. `breakpoint(function="*0x")` — set address breakpoint +5. `continue()`, `context()`, `evaluate(expression="$rax")` — inspect state +6. `stop()` + +--- + +## Common Gotchas + +### GDB and C/C++ + +GDB does **not** support `source` mode. Compile with debug symbols first: +```bash +gcc -g -O0 -o myprogram myprogram.c +g++ -g -O0 -o myprogram myprogram.cpp +``` +Then use `binary` mode. + +### Core dump prerequisites + +- The binary must exactly match the one that crashed (same build) +- Core dumps must be enabled: `ulimit -c unlimited` +- On Linux, check `/proc/sys/kernel/core_pattern` for where dumps go +- For Go programs, set `GOTRACEBACK=crash` to generate core dumps on panic + +### Attach permissions + +On Linux, you may need: +```bash +echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope +``` +Or run with `sudo`. Some distros restrict attaching to non-child processes. + +### Capability-gated tools + +The following tools are only available when the debugger reports support: +- `set-variable` — modify a variable's value (Delve supports this) +- `disassemble` — requires disassembly capability +- `restart` — restart the debug session + +Check what's available after starting a session: the tool list updates automatically. + +### Single reader architecture + +Only one tool can read from the DAP connection at a time. Do not call multiple tools concurrently in the same session — call them sequentially. + +--- + +## MCP Prompts + +Use MCP prompts to get a guided workflow injected directly into your AI conversation: + +| Prompt | Args | Use when | +|--------|------|----------| +| `debug-source` | `path`, `language?`, `breakpoints?` | Debugging from source | +| `debug-attach` | `pid`, `program?` | Attaching to a running process | +| `debug-core-dump` | `binary_path`, `core_path`, `language?` | Analyzing a crash | +| `debug-binary` | `path` | Debugging a compiled binary | + +To use a prompt from an MCP client: +``` +prompts/get debug-core-dump {"binary_path": "/usr/bin/myapp", "core_path": "/tmp/core.12345"} +``` + +## Claude Code Skills + +If using Claude Code with the `mcp-dap-server` skills configured, invoke the appropriate skill: + +- `/debug-source` — live source debugging workflow +- `/debug-attach` — live process attach workflow +- `/debug-core-dump` — post-mortem core dump analysis +- `/debug-binary` — assembly-level binary debugging + +Skills are located in `docs/superpowers/skills/` and provide the same workflow guidance with additional AI-specific decision trees and interpretation hints. diff --git a/docs/superpowers/skills/debug-attach.md b/docs/superpowers/skills/debug-attach.md new file mode 100644 index 0000000..6a24182 --- /dev/null +++ b/docs/superpowers/skills/debug-attach.md @@ -0,0 +1,174 @@ +--- +name: debug-attach +description: | + Live debugging by attaching to a running process using mcp-dap-server. + TRIGGER when: user asks to debug a running process, diagnose a live process by PID, attach to an already-running program, or investigate live CPU/memory/deadlock issues. + DO NOT TRIGGER when: debugging from source (use debug-source), analyzing a crash dump (use debug-core-dump), or the process hasn't started yet (use debug-source or debug-binary). +--- + +# Live Attach Debug Workflow + +## Pre-flight checklist + +Before starting, gather: +1. **PID** of the target process (use `ps aux | grep ` or `pgrep `) +2. **What is the observed problem?** (high CPU, hang, wrong behavior, memory leak) +3. **Is this a production process?** Setting breakpoints will pause it for all users — be careful. +4. **Language/runtime** — Go processes use Delve; others may need GDB + +## Important warnings + +- Attaching pauses the process. In production, this affects real users. +- After `stop()`, the target process is **terminated** — plan accordingly. +- You may need `sudo` or `ptrace_scope=0` permissions. + +--- + +## Step-by-Step Workflow + +### 1. Attach to the process + +```json +debug(mode="attach", processId=) +``` + +Expected: The process pauses. You see the current execution location, stack trace, and variables. + +If attach fails: +- Verify PID is still running: `ps -p ` +- Check ptrace permissions: `cat /proc/sys/kernel/yama/ptrace_scope` (0 = unrestricted) +- Try with elevated permissions if needed +- Process may have already exited + +### 2. Understand what the process was doing + +Immediately call: +```json +context() +``` + +This shows the state at the moment of pause. Key questions: +- **Where is it?** What function and file? +- **Why is it there?** Does the stack trace make sense? +- **What are the local values?** Do they look reasonable? + +If the process was in a system call (I/O, sleep, mutex wait), the stack will show that explicitly. + +### 3. Check all threads / goroutines + +```json +info(kind="threads") +``` + +This is critical for concurrent programs. Look for: +- Threads blocked on the **same mutex or channel** → potential deadlock +- **More threads than expected** → goroutine leak +- Threads in **unexpected functions** → processing wrong data or stuck in error path + +For each suspicious thread: +```json +context(threadId=) +``` + +### 4. Scenario-specific investigation + +#### High CPU usage + +Pause the process several times and look for patterns: +```json +pause() // if it was resumed +context() +``` + +Do this 3-5 times. If the same function keeps appearing, that's the hot path. + +Look for: +- Tight loops with no I/O or sleep +- Repeated work that should be cached +- Unexpected recursion or redundant computation + +#### Deadlock / hang (process not progressing) + +After attach, all threads should be visible. Look for: +- Thread A blocked waiting for lock X +- Thread B holding lock X and waiting for lock Y +- Thread C holding lock Y and waiting for lock X + +Use `context(threadId=)` on each blocked thread to see what lock/channel it's waiting on. + +**Red flag:** `sync.(*Mutex).Lock` or `<-chan` in every goroutine's stack → classic deadlock. + +#### Memory growth / leak + +Check sizes of collections: +```json +evaluate(expression="len(cache)") +evaluate(expression="len(connections)") +evaluate(expression="cap(buffer)") +``` + +Look for: +- Maps/slices that grow but never shrink +- Connection pools that accumulate but don't close +- Goroutines accumulating in `info(kind="threads")` + +#### Unexpected behavior / wrong results + +Set a targeted breakpoint at the function that produces wrong output: +```json +breakpoint(function="packageName.FunctionName") +continue() +``` + +When it hits, inspect inputs and internal state to find where the logic diverges. + +### 5. Iterate + +Resume the process and let it run to your next observation point: +```json +continue() +``` + +Or manually pause again: +```json +pause() +context() +``` + +### 6. Conclude and detach + +State findings clearly: +> **The process is stuck in** `FunctionName` **at** `file.go:42` **because** `mutex.Lock()` **is blocked waiting for a lock held by goroutine** `threadId=3`. +> **Root cause:** goroutine 3 is holding lock A while waiting for lock B; goroutine 1 holds lock B while waiting for lock A — circular deadlock. + +Then clean up: +```json +stop() +``` + +--- + +## Decision Tree + +``` +Process attached + │ + ├─ Is every thread blocked? → Deadlock + │ → Check all threads for mutual lock dependencies + │ + ├─ Is one thread consuming CPU? → Hot path / infinite loop + │ → Pause multiple times, look for repeating call site + │ + ├─ Are threads growing over time? → Goroutine leak + │ → Find goroutines that never finish + │ + └─ Thread behavior looks normal → Behavioral bug + → Set breakpoints at the function producing wrong output +``` + +## How to present findings + +> **Diagnosis:** The process is experiencing a deadlock between goroutines 1 and 3. +> **Evidence:** Goroutine 1 is at `sync.Mutex.Lock` waiting for lock B (held by goroutine 3). Goroutine 3 is at `sync.Mutex.Lock` waiting for lock A (held by goroutine 1). +> **Root cause:** `ProcessRequest` acquires locks in order A→B, while `HandleCallback` acquires them B→A. This creates a lock-ordering inversion. +> **Fix:** Establish a consistent lock acquisition order across all code paths. diff --git a/docs/superpowers/skills/debug-binary.md b/docs/superpowers/skills/debug-binary.md new file mode 100644 index 0000000..67b385a --- /dev/null +++ b/docs/superpowers/skills/debug-binary.md @@ -0,0 +1,235 @@ +--- +name: debug-binary +description: | + Assembly-level debugging of a compiled binary (no source required) using mcp-dap-server. + TRIGGER when: user asks to debug a binary without source code, reverse-engineer a binary, analyze assembly, inspect machine code, work with registers and memory addresses, or debug a stripped binary. + DO NOT TRIGGER when: source code is available (use debug-source), analyzing a crash dump (use debug-core-dump), or attaching to a running process (use debug-attach). +--- + +# Binary / Assembly-Level Debug Workflow + +## Pre-flight checklist + +Before starting, confirm: +1. **Absolute path** to the compiled binary +2. **Architecture** (x86-64, ARM64, etc.) — affects register names and calling conventions +3. **Language/runtime** (Go → Delve; C/C++/Rust → GDB) +4. **Symbols available?** (`file binary` or `nm binary | head` to check — stripped binaries have no function names) +5. **What behavior are you trying to understand?** Form a hypothesis. + +> **Note:** Without source, you navigate by function names (if unstripped), memory addresses, and assembly instructions. This requires reading disassembly output. + +--- + +## Step-by-Step Workflow + +### 1. Start the session + +```json +debug(mode="binary", path="/abs/path/to/binary", stopOnEntry=true) +``` + +Using `stopOnEntry=true` pauses at the entry point before any code runs, giving you time to orient. + +**Choose debugger:** +- Go binary: `debugger="delve"` +- C/C++/Rust binary: `debugger="gdb"` + +If the binary crashes immediately, add breakpoints before continuing. + +### 2. Get initial context + +```json +context() +``` + +Even without source, this shows: +- **Current function name** (if symbols available, e.g., `main.main`) +- **Instruction pointer address** (e.g., `0x004012a0`) +- **Stack trace** with addresses +- Any debug info embedded in the binary + +Note the `instructionPointerReference` from the stack trace — you need it for disassembly. + +### 3. Disassemble the current function + +```json +disassemble(address="0x", count=40) +``` + +Read the output to understand what the function does: + +**Key instructions to recognize:** +``` +call → function call (note the target address/name) +ret → function return +mov dst, src → data movement +cmp a, b → comparison (sets flags, followed by conditional jump) +test a, a → null/zero check +je/jne/jl/jg → conditional branches +lea → compute effective address +push/pop → stack manipulation +``` + +### 4. Set breakpoints + +**By function name** (if symbols exist): +```json +breakpoint(function="main.processRequest") +breakpoint(function="runtime.panic") +``` + +**By address** (always works): +```json +breakpoint(function="*0x00401234") +``` + +Note: GDB uses `*0xADDR` syntax for address-based breakpoints. + +Then run to the breakpoint: +```json +continue() +``` + +### 5. Inspect registers and memory + +**x86-64 registers:** +```json +evaluate(expression="$rax") // return value / first result +evaluate(expression="$rdi") // first function argument +evaluate(expression="$rsi") // second function argument +evaluate(expression="$rdx") // third argument +evaluate(expression="$rsp") // stack pointer +evaluate(expression="$rip") // instruction pointer +evaluate(expression="$rbp") // frame pointer +``` + +**Memory at address:** +```json +evaluate(expression="*(long*)0x
") +evaluate(expression="*(char**)0x
") +``` + +**x86-64 System V calling convention** (Linux, macOS): +- Args: `rdi, rsi, rdx, rcx, r8, r9` (first 6 integer args) +- Return: `rax` (first), `rdx` (second) +- Preserved across calls: `rbx, rbp, r12-r15` +- Caller-saved (may change): `rax, rcx, rdx, rsi, rdi, r8-r11` + +**ARM64 calling convention:** +- Args: `x0-x7` +- Return: `x0` (first), `x1` (second) +- Stack pointer: `sp` +- Preserved: `x19-x28` + +### 6. Step through assembly + +```json +step(mode="in") // step one instruction +step(mode="over") // step over (skip function call body) +step(mode="out") // run until current function returns +``` + +After each step, call `context()` to see the new instruction pointer. + +**Track state changes:** +- Which registers change after each instruction? +- Does a `cmp` result in the expected branch? +- Does a `call` go to the expected address? + +### 7. Navigate the call graph + +When you see a `call ` you want to understand: + +1. Disassemble the target: `disassemble(address="0x", count=30)` +2. Set a breakpoint there: `breakpoint(function="*0x")` +3. Continue and inspect state inside that function + +Work your way through the call graph by disassembling each function of interest. + +### 8. Identify the logic + +Common patterns in disassembly: + +**Null check (Go/C):** +```asm +test rax, rax ; is rax zero? +je ; jump if zero (nil) +``` + +**Bounds check:** +```asm +cmp rbx, rcx ; index vs length +jae ; jump if above-or-equal (out of bounds) +``` + +**String length:** +```asm +lea rax, [rip+] ; load string address +mov rbx, ; load length +``` + +**Function prologue:** +```asm +push rbp +mov rbp, rsp +sub rsp, ; allocate stack frame +``` + +**Function epilogue:** +```asm +add rsp, ; deallocate stack frame +pop rbp +ret +``` + +### 9. Form and test hypotheses + +Based on the disassembly: +1. State your hypothesis: "I think this branch at 0x401234 is taken when X" +2. Set a breakpoint before the branch +3. Continue, inspect the comparison operands +4. Confirm whether the branch is taken as expected + +### 10. Conclude + +State findings: +> **Function** `main.processRequest` **at** `0x401280` **validates the input length at** `0x4012a4`. When the length exceeds 255, the branch at `0x4012b0` jumps to the error path at `0x401340`. The issue is that the check uses unsigned comparison (`jae`), but the length variable is signed — a negative length passes the check. + +### 11. Clean up + +```json +stop() +``` + +--- + +## Quick Reference + +### Disassembly workflow +``` +context() → get current address + ↓ +disassemble(address=, count=40) + ↓ +Identify interesting branch or call + ↓ +Set breakpoint at target + ↓ +continue() → arrive at target + ↓ +inspect registers + disassemble again +``` + +### When symbols are stripped + +Use `info(kind="modules")` to find loaded libraries and their base addresses. Map raw addresses to library offsets to identify what is being called. Look for recognizable patterns in disassembly (system call numbers, library function signatures). + +### When you see a crash/panic in the disassembly + +Look for calls to: +- Go: `runtime.panic`, `runtime.throw`, `runtime.sigpanic` +- C: `__stack_chk_fail`, `abort`, `raise` +- These are called immediately before the program terminates + +Disassemble backwards from the crash call to find what condition triggered it. diff --git a/docs/superpowers/skills/debug-core-dump.md b/docs/superpowers/skills/debug-core-dump.md new file mode 100644 index 0000000..bc0c243 --- /dev/null +++ b/docs/superpowers/skills/debug-core-dump.md @@ -0,0 +1,202 @@ +--- +name: debug-core-dump +description: | + Post-mortem analysis of a core dump file using mcp-dap-server. + TRIGGER when: user asks to analyze a core dump, investigate a crash, figure out why a program crashed, analyze a segfault, or examine a .core / core.* / core file. + DO NOT TRIGGER when: the program is still running (use debug-attach), debugging from source interactively (use debug-source), or no core file exists yet (run the program first with core dumps enabled). +--- + +# Post-Mortem Core Dump Analysis Workflow + +## Pre-flight checklist + +Before starting, confirm: +1. **Absolute path** to the binary that crashed (must be the exact same build) +2. **Absolute path** to the core dump file (usually `core`, `core.`, or `core.XXXX`) +3. **Language** (Go → Delve; C/C++ → GDB) +4. **Do the binary and core match?** A rebuilt binary won't match the core dump. + +> **Key insight:** Execution is frozen. You cannot step forward. You are reading a snapshot of memory and registers at the moment of crash. + +--- + +## Step-by-Step Workflow + +### 1. Start the core dump session + +**Go:** +```json +debug(mode="core", path="/abs/path/to/binary", coreFilePath="/abs/path/to/core", debugger="delve") +``` + +**C/C++:** +```json +debug(mode="core", path="/abs/path/to/binary", coreFilePath="/abs/path/to/core", debugger="gdb") +``` + +Expected: The debugger loads the core and positions at the crash frame. You see the crash location, stack trace, and local variables. + +If loading fails: +- Check paths are absolute and files are readable +- Confirm binary matches core (same build, not recompiled after crash) +- For Go: `dlv version` to confirm Delve is installed +- For C/C++: check cpptools adapter is available (`MCP_DAP_CPPTOOLS_PATH`) + +### 2. Get the full picture of the crash + +```json +context() +``` + +This is your most important call. Extract: +- **Crash function and line** — where exactly did the program die? +- **Signal** — what killed it? (see signal guide below) +- **Local variables at the crash frame** — any nil pointers? Invalid values? +- **Full stack trace** — what sequence of calls led to the crash? + +**Signal interpretation guide:** + +| Signal | Meaning | Common causes | +|--------|---------|--------------| +| `SIGSEGV` | Segmentation fault | Nil pointer deref, use-after-free, buffer overflow, stack overflow | +| `SIGABRT` | Abort | Go runtime panic, C assert(), double-free, explicit abort() | +| `SIGFPE` | Arithmetic error | Division by zero, integer overflow | +| `SIGBUS` | Bus error | Misaligned memory access, unmapped file region | +| `SIGILL` | Illegal instruction | Compiler bug, corrupted binary | +| `SIGPIPE` | Broken pipe | Write to closed socket/pipe | + +### 3. Examine crash frame variables + +Look at every variable shown in `context()`: +- Any **nil pointer** being dereferenced? → SIGSEGV +- Any **index** being used on a nil or zero-length slice? → SIGSEGV +- Any **value that looks corrupt** (e.g., negative size, astronomically large number)? + +Use `evaluate()` to drill into values not shown automatically: +```json +evaluate(expression="err.Error()") +evaluate(expression="request.Header") +evaluate(expression="items[0]") +evaluate(expression="p.next") +``` + +### 4. Walk the call stack + +Work backwards through the stack trace. For each frame of interest: +```json +context(frameId=) +``` + +Ask for each frame: +- What argument was passed to the function that crashed? +- Was that argument already nil/invalid when it was passed? +- Which caller is responsible for the bad value? + +This traces the bad value back to its origin. + +### 5. Check other threads / goroutines + +```json +info(kind="threads") +``` + +In multi-threaded/concurrent programs: +- Another thread may have corrupted shared memory before the crash +- A goroutine in an unexpected state may indicate a race condition +- Look for threads at suspicious locations + +Inspect interesting threads: +```json +context(threadId=) +``` + +### 6. Evaluate suspicious expressions + +Based on your hypothesis, test specific values: +```json +evaluate(expression="config.MaxRetries") +evaluate(expression="len(pool.connections)") +evaluate(expression="handler != nil") +``` + +For Go, you can evaluate method calls if the receiver is valid: +```json +evaluate(expression="user.String()") +``` + +For C/C++ with GDB: +```json +evaluate(expression="*(struct Node*)ptr") +evaluate(expression="str->length") +``` + +### 7. Pattern-based diagnosis + +Use the signal + stack + variables to match a pattern: + +**Nil pointer (SIGSEGV on field access):** +``` +→ Which struct pointer is nil? +→ Who created/returned that pointer? +→ Was nil return from a function call checked? +→ Fix: add nil check before dereference, or fix the function to not return nil +``` + +**Buffer overflow (SIGSEGV in memory ops):** +``` +→ Is there an index operation near the crash? +→ What is the length/capacity of the buffer? +→ Was bounds checking skipped? +→ Fix: add bounds check, use safe slice operations +``` + +**Runtime panic / abort (SIGABRT in Go):** +``` +→ Look for panic message in variables or stack +→ Common: index out of range, nil map write, concurrent map read/write +→ Fix: depends on the specific panic type +``` + +**Infinite recursion (stack overflow → SIGSEGV):** +``` +→ Very deep stack with the same function repeating +→ What is the base case? Is it reachable? +→ Fix: add/fix the base case, or convert to iterative +``` + +**Double-free / use-after-free (SIGABRT in C/C++):** +``` +→ malloc/free mismatch, or pointer used after free() +→ Look for shared ownership without reference counting +→ Fix: use RAII/smart pointers, or fix ownership model +``` + +### 8. Conclude + +Answer: +1. **What crashed?** (function, file, line number) +2. **What signal?** (and what it means for this case) +3. **What bad value caused it?** (which variable, what value) +4. **Where did that value come from?** (trace through the call stack) +5. **Root cause?** (the code defect that needs fixing) + +State it clearly: +> **Crash at** `server.go:142` **in** `handleRequest`. **Signal:** SIGSEGV. +> **Cause:** `conn.writer` is nil. `conn` is non-nil but was not fully initialized because `newConn()` returns early on timeout without setting `writer`. +> **Fix:** Either return an error from `newConn()` on timeout (rather than a partially initialized struct), or add a nil check before `conn.writer.Write()`. + +### 9. Clean up + +```json +stop() +``` + +--- + +## How to present findings + +Structure your response as: +1. **One-line summary** of what crashed and why +2. **Evidence** — the specific values/stack frames that prove it +3. **Root cause** — the code defect +4. **Suggested fix** — what change would prevent this diff --git a/docs/superpowers/skills/debug-source.md b/docs/superpowers/skills/debug-source.md new file mode 100644 index 0000000..e79ef8c --- /dev/null +++ b/docs/superpowers/skills/debug-source.md @@ -0,0 +1,160 @@ +--- +name: debug-source +description: | + Live source-level debugging of a Go or C/C++ program using mcp-dap-server. + TRIGGER when: user asks to debug a program from source, find a bug, step through code, or inspect runtime state of a running program. + DO NOT TRIGGER when: debugging a core dump (use debug-core-dump), attaching to an existing process (use debug-attach), or debugging a binary without source (use debug-binary). +--- + +# Live Source Debug Workflow + +## Pre-flight checklist + +Before starting, gather: +1. **Absolute path** to the source file or directory +2. **Language** (Go or C/C++) — determines which debugger to use +3. **What is the bug or behavior to investigate?** — forms your hypothesis +4. **Which function/file is most likely involved?** — where to set the first breakpoint + +## Quick reference + +| Language | Debugger | Mode | +|----------|----------|------| +| Go | `delve` | `source` | +| C/C++/Rust | `gdb` | `binary` (compile first: `gcc -g -O0`) | + +--- + +## Step-by-Step Workflow + +### 1. Start the session + +**Go:** +```json +debug(mode="source", path="/abs/path/to/main.go", debugger="delve") +``` + +**C/C++ (compile first, then binary mode):** +```json +debug(mode="binary", path="/abs/path/to/binary", debugger="gdb") +``` + +Expected: debugger starts, stops at entry or breakpoint. You receive stack trace + variables. + +If this fails: +- Go: check `dlv` is in `$PATH` (`dlv version`) +- C/C++: check cpptools adapter; compile with `-g -O0` +- Ensure path is absolute + +### 2. Set strategic breakpoints + +Set breakpoints _before_ continuing, at the function(s) you want to investigate: + +```json +breakpoint(file="/abs/path/to/file.go", line=42) +breakpoint(function="packageName.FunctionName") +``` + +Choose breakpoint locations based on your hypothesis: +- Entry to the suspicious function +- Just before the condition you think is wrong +- At error return paths + +### 3. Run to the first interesting point + +```json +continue() +``` + +Output includes: current file/line, stack trace, all local variables. + +**What to look for:** +- Is the current location where you expected? +- Are variable values what you expect at this point? +- Is the call stack expected, or is something surprising calling this function? + +### 4. Inspect state in depth + +Refresh context at any time: +```json +context() +``` + +Drill into specific values: +```json +evaluate(expression="user.Address.City") +evaluate(expression="items[0]") +evaluate(expression="len(queue)") +evaluate(expression="err.Error()") +``` + +**Decision guide:** +- Value is nil when it shouldn't be → trace back where it was set +- Value is wrong → find where it was assigned incorrectly +- Value is correct here → the bug is downstream; add a later breakpoint + +### 5. Step through logic + +```json +step(mode="over") // execute current line, stay in same function +step(mode="in") // step into the function being called +step(mode="out") // run until current function returns +``` + +**When to use each:** +- `over`: when you don't suspect the called function is the problem +- `in`: when the called function is suspicious +- `out`: when you've seen enough in the current function + +After each step, check if values changed as expected. + +### 6. Check threads (concurrent programs) + +```json +info(kind="threads") +``` + +Look for goroutines/threads in unexpected states. For each suspicious thread: +```json +context(threadId=) +``` + +**Red flags:** +- Multiple threads at the same mutex/channel → potential deadlock +- A goroutine stuck in an unexpected location +- Unexpectedly few or many goroutines + +### 7. Modify state to test hypotheses (optional) + +If the debugger supports it: +```json +set-variable(variablesReference=, name="count", value="0") +``` + +Then `continue()` to see if the fix works. This confirms your hypothesis before writing code. + +### 8. Clean up + +```json +stop() +``` + +--- + +## Common Patterns and Root Causes + +| Symptom | Likely cause | What to check | +|---------|-------------|---------------| +| `nil` pointer panic | Missing nil check | Where was the pointer created/returned? | +| Wrong value in calculation | Logic error or uninitialized var | Step through the computation | +| Function called with wrong args | Caller bug | Step out to the caller, inspect args | +| Infinite loop | Missing termination condition | Evaluate the loop condition, check iterator | +| Goroutine deadlock | Lock ordering or missing unlock | Check all goroutines with `info(kind="threads")` | + +## How to present findings + +State clearly: +> **Bug found at** `file.go:42` in `FunctionName`. +> **Variable** `x` **has value** `nil` **when it should be** `*User{...}`. +> **Root cause:** `getUserByID()` returns `nil` on cache miss without error, but the caller doesn't check for nil before dereferencing. +> **Fix:** Add nil check in caller or return an error from `getUserByID()` on cache miss. diff --git a/main.go b/main.go index e95612c..5ed444c 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,8 @@ func main() { ds := registerTools(server, logWriter) defer ds.cleanup() + registerPrompts(server) + if err := server.Run(context.Background(), mcp.NewStdioTransport()); err != nil { log.Fatalf("server error: %v", err) } diff --git a/prompts.go b/prompts.go new file mode 100644 index 0000000..8abf6e4 --- /dev/null +++ b/prompts.go @@ -0,0 +1,588 @@ +package main + +import ( + "context" + "fmt" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// registerPrompts registers all debugging workflow prompts with the MCP server. +func registerPrompts(server *mcp.Server) { + server.AddPrompt(&mcp.Prompt{ + Name: "debug-source", + Description: "Structured workflow for debugging a program from source code", + Arguments: []*mcp.PromptArgument{ + {Name: "path", Required: true, Description: "Path to the source file or directory to debug"}, + {Name: "language", Required: false, Description: "Language: 'go' (default) or 'c'/'cpp'"}, + {Name: "breakpoints", Required: false, Description: "Comma-separated file:line pairs, e.g. 'main.go:42,server.go:100'"}, + }, + }, promptDebugSource) + + server.AddPrompt(&mcp.Prompt{ + Name: "debug-attach", + Description: "Structured workflow for attaching to a running process", + Arguments: []*mcp.PromptArgument{ + {Name: "pid", Required: true, Description: "Process ID to attach to"}, + {Name: "program", Required: false, Description: "Description of what the program does (for context)"}, + }, + }, promptDebugAttach) + + server.AddPrompt(&mcp.Prompt{ + Name: "debug-core-dump", + Description: "Structured workflow for post-mortem analysis of a core dump", + Arguments: []*mcp.PromptArgument{ + {Name: "binary_path", Required: true, Description: "Path to the executable that crashed"}, + {Name: "core_path", Required: true, Description: "Path to the core dump file"}, + {Name: "language", Required: false, Description: "Language: 'go' (default) or 'c'/'cpp'"}, + }, + }, promptDebugCoreDump) + + server.AddPrompt(&mcp.Prompt{ + Name: "debug-binary", + Description: "Structured workflow for assembly-level debugging of a compiled binary", + Arguments: []*mcp.PromptArgument{ + {Name: "path", Required: true, Description: "Path to the compiled binary to debug"}, + }, + }, promptDebugBinary) +} + +func promptDebugSource(_ context.Context, _ *mcp.ServerSession, params *mcp.GetPromptParams) (*mcp.GetPromptResult, error) { + path := params.Arguments["path"] + language := params.Arguments["language"] + breakpoints := params.Arguments["breakpoints"] + + if language == "" { + language = "go" + } + + debugger := "delve" + mode := "source" + compileNote := "" + if language == "c" || language == "cpp" { + debugger = "gdb" + mode = "binary" + compileNote = fmt.Sprintf(` +> **Note:** GDB does not support 'source' mode. Compile first with debug symbols: +> - C: `+"`"+`gcc -g -O0 -o myprogram %s`+"`"+` +> - C++: `+"`"+`g++ -g -O0 -o myprogram %s`+"`"+` +> Then use 'binary' mode with the compiled output path.`, path, path) + } + + bpSection := "" + if breakpoints != "" { + bpSection = fmt.Sprintf(` +### Optional: Pre-set breakpoints +The following breakpoints were requested: `+"`"+`%s`+"`"+` + +Include them in the debug call: +`+"```"+`json +{ + "mode": "%s", + "path": "%s", + "debugger": "%s", + "breakpoints": [ + {"file": "/abs/path/to/file.go", "line": 42} + ] +} +`+"```", breakpoints, mode, path, debugger) + } + + content := fmt.Sprintf(`## Live Source Debug Session + +You are debugging a **%s** program from source.%s + +**Program path:** `+"`"+`%s`+"`"+` + +--- + +### Step 1: Start the debug session + +Call: `+"`"+`debug(mode="%s", path="%s", debugger="%s")`+"`"+` +%s +Expected output: The debugger starts and either stops at entry or waits at a breakpoint. You will see a stack trace and local variables. + +**If the debug tool fails:** +- Check that `+"`"+`%s`+"`"+` is installed and in `+"`"+`$PATH`+"`"+` +- For Go: ensure the path points to a .go file or directory with a `+"`"+`main`+"`"+` package +- For C/C++: compile with -g -O0 first, then use binary mode + +--- + +### Step 2: Set strategic breakpoints + +Before running, identify the most useful locations: +- Entry points to the function or logic area of interest +- Error handling paths +- State transitions + +Call: `+"`"+`breakpoint(file="/abs/path/to/file", line=N)`+"`"+` +Or by function: `+"`"+`breakpoint(function="packageName.FunctionName")`+"`"+` + +--- + +### Step 3: Run to the first interesting point + +Call: `+"`"+`continue()`+"`"+` + +Expected: Stops at your breakpoint. Output includes: +- **Location**: file, line, function name +- **Stack trace**: full call chain +- **Variables**: locals and their current values + +**What to look for:** +- Are variable values what you expect at this point? +- Is the call stack reasonable, or is something unexpected calling this function? + +--- + +### Step 4: Inspect state + +Call: `+"`"+`context()`+"`"+` at any time to refresh the current location, stack, and variables. + +Call: `+"`"+`evaluate(expression="variableName")`+"`"+` to inspect specific values: +- Struct fields: `+"`"+`evaluate(expression="user.Address.City")`+"`"+` +- Slice/array elements: `+"`"+`evaluate(expression="items[0]")`+"`"+` +- Computed expressions: `+"`"+`evaluate(expression="len(queue)")`+"`"+` + +--- + +### Step 5: Step through logic + +- `+"`"+`step(mode="over")`+"`"+` — execute current line, staying in the same function +- `+"`"+`step(mode="in")`+"`"+` — step into a function call +- `+"`"+`step(mode="out")`+"`"+` — run until the current function returns + +**Decision guide:** +- See an unexpected value? Step _in_ to the function that produced it +- At a line that doesn't matter? Step _over_ it +- Deep in a call you don't care about? Step _out_ + +--- + +### Step 6: Check threads (if concurrent program) + +Call: `+"`"+`info(kind="threads")`+"`"+` + +Look for: goroutines/threads in unexpected states, goroutines blocked on channels or mutexes. + +--- + +### Step 7: Identify the root cause + +By now you should be able to answer: +1. What is the actual (incorrect) value, and where did it come from? +2. What condition or code path led to this state? +3. Is this a logic error, a missing nil check, an off-by-one, a race condition, or something else? + +State your findings clearly: **"The bug is at [file:line]. [Variable] has value [X] when it should be [Y] because [reason]."** + +--- + +### Step 8: Clean up + +Call: `+"`"+`stop()`+"`"+` +`, + language, compileNote, + path, + mode, path, debugger, + bpSection, + func() string { + if debugger == "delve" { + return "dlv" + } + return "OpenDebugAD7 (cpptools)" + }(), + ) + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("Live source debugging workflow for %s", path), + Messages: []*mcp.PromptMessage{ + {Role: "user", Content: &mcp.TextContent{Text: content}}, + }, + }, nil +} + +func promptDebugAttach(_ context.Context, _ *mcp.ServerSession, params *mcp.GetPromptParams) (*mcp.GetPromptResult, error) { + pid := params.Arguments["pid"] + program := params.Arguments["program"] + + programDesc := "" + if program != "" { + programDesc = fmt.Sprintf("\n**Program description:** %s\n", program) + } + + content := fmt.Sprintf(`## Live Attach Debug Session + +You are attaching to a **running process** to diagnose its live behavior.%s + +**Target PID:** `+"`"+`%s`+"`"+` + +> **Important:** The process continues running after you attach. You are observing live state. +> Be careful: setting breakpoints in a production process will pause it for all users. + +--- + +### Step 1: Attach to the process + +Call: `+"`"+`debug(mode="attach", processId=%s)`+"`"+` + +Expected: The debugger attaches and the process pauses. You will see the current execution location. + +**If attach fails:** +- Verify the PID is correct: `+"`"+`ps aux | grep `+"`"+` +- Check permissions: you may need to run as root or set `+"`"+`ptrace_scope`+"`"+` +- The process may have already exited + +--- + +### Step 2: Understand what the process was doing + +Immediately after attach, call: `+"`"+`context()`+"`"+` + +Look for: +- **Current location**: What function/file is it in? +- **Stack trace**: What sequence of calls led here? +- **Variables**: What are the current local values? + +If the process was in a system call or blocking operation, the stack will show that. + +--- + +### Step 3: Check all threads + +Call: `+"`"+`info(kind="threads")`+"`"+` + +This is especially important for concurrent programs. Look for: +- Threads blocked on the same mutex (potential deadlock) +- Threads in unexpected functions +- Unexpectedly high thread counts + +For each suspicious thread, inspect it: +Call: `+"`"+`context(threadId=)`+"`"+` + +--- + +### Step 4: Set targeted breakpoints (carefully) + +Only set breakpoints if you have a specific hypothesis to test. + +Call: `+"`"+`breakpoint(function="packageName.FunctionName")`+"`"+` + +Then resume: `+"`"+`continue()`+"`"+` + +The process resumes and runs until your breakpoint is hit. Inspect state at that point. + +--- + +### Step 5: Diagnose the issue + +Common attach scenarios and what to look for: + +**High CPU usage:** +- Pause several times with `+"`"+`pause()`+"`"+` + `+"`"+`context()`+"`"+` +- Which function keeps appearing in the stack? That's likely the hot path. +- Look for tight loops, repeated work, or missing caches. + +**Memory leak:** +- Check variables for large collections, caches without eviction, or accumulating slices. +- Use `+"`"+`evaluate()`+"`"+` to check sizes: `+"`"+`evaluate(expression="len(cache)")`+"`"+` + +**Deadlock / hang:** +- All threads should show where they're blocked +- Look for goroutines stuck in channel operations or mutexes +- Check if any goroutine holds a lock that another is waiting for + +**Unexpected behavior:** +- Set a breakpoint at the function exhibiting the behavior +- Inspect inputs and internal state + +--- + +### Step 6: Conclude and detach + +State your findings: **"The process is [doing X] because [reason]. The issue is [description]."** + +Call: `+"`"+`stop()`+"`"+` + +> After stop, the target process is terminated. If you need to detach without killing, that is not currently supported — plan accordingly. +`, + programDesc, pid, pid, + ) + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("Live attach debugging workflow for PID %s", pid), + Messages: []*mcp.PromptMessage{ + {Role: "user", Content: &mcp.TextContent{Text: content}}, + }, + }, nil +} + +func promptDebugCoreDump(_ context.Context, _ *mcp.ServerSession, params *mcp.GetPromptParams) (*mcp.GetPromptResult, error) { + binaryPath := params.Arguments["binary_path"] + corePath := params.Arguments["core_path"] + language := params.Arguments["language"] + + if language == "" { + language = "go" + } + + debugger := "delve" + if language == "c" || language == "cpp" { + debugger = "gdb" + } + + signalGuide := ` +**Signal interpretation:** +- `+"`"+`SIGSEGV`+"`"+` (segfault) — nil pointer dereference, use-after-free, buffer overflow, stack overflow +- `+"`"+`SIGABRT`+"`"+` — explicit abort, assertion failure, double-free (C/C++), runtime panic (Go) +- `+"`"+`SIGFPE`+"`"+` — arithmetic error: division by zero, integer overflow +- `+"`"+`SIGBUS`+"`"+` — misaligned memory access, unmapped file region +- `+"`"+`SIGILL`+"`"+` — illegal CPU instruction (often compiler bug or corrupted binary) +- `+"`"+`SIGPIPE`+"`"+` — write to closed pipe/socket with no signal handler` + + content := fmt.Sprintf(`## Post-Mortem Core Dump Analysis + +You are analyzing a **crash captured in a core dump**. Execution is frozen at the moment of the crash — you cannot step forward, but you have full access to the crashed state. + +**Program:** `+"`"+`%s`+"`"+` +**Core dump:** `+"`"+`%s`+"`"+` +**Debugger:** %s + +> This is read-only analysis. You cannot change execution — only observe the crashed state. + +--- + +### Step 1: Start the core dump session + +Call: `+"`"+`debug(mode="core", path="%s", coreFilePath="%s", debugger="%s")`+"`"+` + +Expected: The debugger loads the core dump and positions at the crash frame. You will see the crash location, stack trace, and local variables. + +**If loading fails:** +- Ensure the binary matches the core dump exactly (same build, not rebuilt after crash) +- Check file paths are absolute and readable +- For Go: ensure Delve is installed (`+"`"+`dlv version`+"`"+`) +- For C/C++: ensure the cpptools adapter is available + +--- + +### Step 2: Understand the crash location + +Call: `+"`"+`context()`+"`"+` + +This is your most important call. Look for: +- **Crash function**: What function was executing when the crash occurred? +- **File and line**: Exact source location of the crash +- **Local variables**: What values were present at the crash frame? +- **Stack trace**: The full call chain leading to the crash +%s + +--- + +### Step 3: Examine the crash frame variables + +Look at every variable in the crash frame: +- Are any pointers nil? A nil dereference causes SIGSEGV. +- Are indices within bounds? Out-of-bounds access can cause SIGSEGV. +- Are any values nonsensical (e.g., negative size, huge number)? Suggests corruption. + +Use `+"`"+`evaluate(expression="varName")`+"`"+` to inspect variables not shown in context, or to drill into nested fields: +- `+"`"+`evaluate(expression="err.Error()")`+"`"+` +- `+"`"+`evaluate(expression="request.Body")`+"`"+` +- `+"`"+`evaluate(expression="items[idx]")`+"`"+` + +--- + +### Step 4: Walk up the call stack + +Each frame in the stack trace tells part of the story. For each suspicious frame: + +1. Use `+"`"+`context(frameId=)`+"`"+` to inspect variables in that frame +2. Look for: what argument was passed to the crashing function? Was it already invalid? +3. Ask: could the caller have passed a nil/invalid value? + +Work backwards from the crash until you find where the bad value originated. + +--- + +### Step 5: Check other threads / goroutines + +Call: `+"`"+`info(kind="threads")`+"`"+` + +In multi-threaded programs, the crash may be triggered by another thread's action (race condition). Look for: +- Other threads at suspicious locations +- Threads that may have corrupted shared state + +For each interesting thread: `+"`"+`context(threadId=)`+"`"+` + +--- + +### Step 6: Formulate your conclusion + +Answer these questions: +1. **What crashed?** (function, file, line) +2. **Why did it crash?** (nil pointer, invalid index, assertion failure, etc.) +3. **What bad value caused it?** (which variable, what value it had) +4. **Where did that bad value come from?** (trace back through the call stack) +5. **What is the root cause?** (missing nil check, logic error, race condition, etc.) + +**Pattern recognition:** +- `+"`"+`SIGSEGV`+"`"+` on field access → check if the containing struct pointer is nil +- `+"`"+`SIGSEGV`+"`"+` in memory allocation → heap corruption (often from earlier use-after-free) +- `+"`"+`SIGABRT`+"`"+` in Go → runtime panic (check the panic message in variables) +- `+"`"+`SIGABRT`+"`"+` in C → double-free, assert(), or explicit abort() +- Very deep stack → infinite recursion (check for missing base case) +- Stack frame count = 1 in a goroutine → goroutine was running C code or in a syscall + +--- + +### Step 7: Clean up + +Call: `+"`"+`stop()`+"`"+` +`, + binaryPath, corePath, debugger, + binaryPath, corePath, debugger, + signalGuide, + ) + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("Post-mortem core dump analysis for %s", binaryPath), + Messages: []*mcp.PromptMessage{ + {Role: "user", Content: &mcp.TextContent{Text: content}}, + }, + }, nil +} + +func promptDebugBinary(_ context.Context, _ *mcp.ServerSession, params *mcp.GetPromptParams) (*mcp.GetPromptResult, error) { + path := params.Arguments["path"] + + // Infer likely language/debugger from path or note that both are supported + debuggerNote := `Use 'delve' for Go binaries, 'gdb' for C/C++/Rust binaries. +> - Go binary: `+"`"+`debug(mode="binary", path="...", debugger="delve")`+"`"+` +> - C/C++ binary: `+"`"+`debug(mode="binary", path="...", debugger="gdb")`+"`"+`` + + content := fmt.Sprintf(`## Binary / Assembly-Level Debug Session + +You are debugging a **compiled binary without source context**. You will work primarily with disassembled instructions, registers, and memory addresses. + +**Binary:** `+"`"+`%s`+"`"+` + +> %s + +--- + +### Step 1: Start the binary debug session + +Call: `+"`"+`debug(mode="binary", path="%s", stopOnEntry=true)`+"`"+` + +Using `+"`"+`stopOnEntry=true`+"`"+` pauses immediately so you can orient yourself before execution proceeds. + +Expected: The debugger stops at the binary entry point or `+"`"+`main`+"`"+`. You will see the current instruction address and possibly a stack trace. + +--- + +### Step 2: Get your bearings + +Call: `+"`"+`context()`+"`"+` + +Even without source, this shows: +- **Current location**: function name and address (e.g., `+"`"+`main.main at 0x004012a0`+"`"+`) +- **Stack trace**: the call chain with instruction pointer addresses +- **Variables**: any debug info that exists in the binary + +Note the `+"`"+`instructionPointerReference`+"`"+` (memory address) from the stack trace — you will use this for disassembly. + +--- + +### Step 3: Disassemble around the current location + +Call: `+"`"+`disassemble(address="0x", count=30)`+"`"+` + +Read the assembly output: +- Identify the instruction sequence (what is the function doing?) +- Look for `+"`"+`call`+"`"+` instructions (function calls) +- Look for `+"`"+`cmp`+"`"+`/`+"`"+`test`+"`"+` + conditional jumps (branch logic) +- Look for `+"`"+`mov`+"`"+` with memory operands (data access) + +--- + +### Step 4: Set breakpoints by address or function + +By function name (if debug symbols exist): +Call: `+"`"+`breakpoint(function="main.processRequest")`+"`"+` + +By address (no symbols needed): +Call: `+"`"+`breakpoint(function="*0x00401234")`+"`"+` + +> For GDB: you can use `+"`"+`*0xADDR`+"`"+` syntax to break at an absolute address. + +--- + +### Step 5: Step through assembly + +Use `+"`"+`step(mode="in")`+"`"+` to step one instruction at a time (at assembly level, each step may be one machine instruction). + +After each step, call `+"`"+`context()`+"`"+` to see the new instruction pointer location. + +**What to track:** +- Register values (available via `+"`"+`evaluate(expression="$rax")`+"`"+` in GDB sessions) +- Memory at specific addresses: `+"`"+`evaluate(expression="*(int*)0xADDR")`+"`"+` +- Stack pointer and frame pointer movement + +--- + +### Step 6: Inspect memory and registers + +Call: `+"`"+`evaluate(expression="$rsp")`+"`"+` — current stack pointer +Call: `+"`"+`evaluate(expression="$rip")`+"`"+` — current instruction pointer +Call: `+"`"+`evaluate(expression="$rax")`+"`"+` — return value register (x86-64) + +For memory: `+"`"+`evaluate(expression="*(long*)0x
")`+"`"+` + +**Calling convention reminders (x86-64 System V):** +- Function args: rdi, rsi, rdx, rcx, r8, r9 (in order) +- Return value: rax +- Preserved: rbx, rbp, r12-r15 +- Scratch: rax, rcx, rdx, rsi, rdi, r8-r11 + +--- + +### Step 7: Use disassembly to understand logic + +When you see a branch or call you want to understand: +1. Disassemble the target: `+"`"+`disassemble(address="0x", count=40)`+"`"+` +2. Set a breakpoint there and `+"`"+`continue()`+"`"+` +3. Inspect register/memory state at that point + +**Common patterns:** +- `+"`"+`call malloc`+"`"+` → memory allocation; check return in rax for NULL +- `+"`"+`test rax, rax; je`+"`"+` → null check +- `+"`"+`cmp [rsp+N], 0; jle`+"`"+` → bounds or sign check +- `+"`"+`rep stos`+"`"+` → memset-like loop + +--- + +### Step 8: Formulate findings + +After analysis, state: +1. **What the binary does** at the function/address of interest +2. **Where the issue is** (address, instruction, logic) +3. **What the expected vs actual behavior is** + +--- + +### Step 9: Clean up + +Call: `+"`"+`stop()`+"`"+` +`, + path, debuggerNote, path, + ) + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("Assembly-level binary debugging workflow for %s", path), + Messages: []*mcp.PromptMessage{ + {Role: "user", Content: &mcp.TextContent{Text: content}}, + }, + }, nil +} From 65387a931e1a5702233045a18f7d7478c85f3bcc Mon Sep 17 00:00:00 2001 From: Derek Parker Date: Wed, 11 Mar 2026 15:18:56 -0700 Subject: [PATCH 2/2] fix: address PR review feedback on debugging workflow guidance - Move skills from docs/superpowers/skills/ to top-level skills/ - stop tool: add detach=true parameter to disconnect without terminating the debuggee (sends DAP disconnect with terminateDebuggee=false) - debug-attach skill: note that debug() already returns initial context; only call context() after resuming; add C/C++ thread/lock indicators and memory-leak examples alongside Go examples; document stop(detach=true) - debug-binary skill: note that step() returns context automatically - debug-source skill: add language-specific evaluate() expression syntax for Delve (Go) and GDB (C/C++); expand common patterns table to cover Go, C/C++, and Rust root causes - Update CLAUDE.md and docs/debugging-workflows.md for new skills/ path Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 4 +- docs/debugging-workflows.md | 2 +- prompts.go | 5 +- .../skills => skills}/debug-attach.md | 49 +++++++++++++------ .../skills => skills}/debug-binary.md | 2 +- .../skills => skills}/debug-core-dump.md | 0 .../skills => skills}/debug-source.md | 42 ++++++++++++++-- tools.go | 27 ++++++++-- 8 files changed, 102 insertions(+), 29 deletions(-) rename {docs/superpowers/skills => skills}/debug-attach.md (75%) rename {docs/superpowers/skills => skills}/debug-binary.md (97%) rename {docs/superpowers/skills => skills}/debug-core-dump.md (100%) rename {docs/superpowers/skills => skills}/debug-source.md (70%) diff --git a/CLAUDE.md b/CLAUDE.md index c3e6ef6..9f1b0ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -223,7 +223,7 @@ Prompts are registered in `prompts.go` via `registerPrompts()`, called from `mai ### Claude Code Skills -Four skills live in `docs/superpowers/skills/` for use with the Claude Code Superpowers plugin: +Four skills live in `skills/` for use with the Claude Code Superpowers plugin: | Skill file | Trigger | |-----------|---------| @@ -232,7 +232,7 @@ Four skills live in `docs/superpowers/skills/` for use with the Claude Code Supe | `debug-core-dump.md` | Analyzing a core dump | | `debug-binary.md` | Assembly-level binary debugging | -To register skills with Claude Code, configure the `docs/superpowers/skills/` directory as a skills source in your Superpowers plugin settings. +To register skills with Claude Code, configure the `skills/` directory as a skills source in your Superpowers plugin settings. ### Human Reference diff --git a/docs/debugging-workflows.md b/docs/debugging-workflows.md index c937016..98e6e72 100644 --- a/docs/debugging-workflows.md +++ b/docs/debugging-workflows.md @@ -208,4 +208,4 @@ If using Claude Code with the `mcp-dap-server` skills configured, invoke the app - `/debug-core-dump` — post-mortem core dump analysis - `/debug-binary` — assembly-level binary debugging -Skills are located in `docs/superpowers/skills/` and provide the same workflow guidance with additional AI-specific decision trees and interpretation hints. +Skills are located in `skills/` and provide the same workflow guidance with additional AI-specific decision trees and interpretation hints. diff --git a/prompts.go b/prompts.go index 8abf6e4..24d4ad1 100644 --- a/prompts.go +++ b/prompts.go @@ -303,9 +303,10 @@ Common attach scenarios and what to look for: State your findings: **"The process is [doing X] because [reason]. The issue is [description]."** -Call: `+"`"+`stop()`+"`"+` +To **terminate** the process: `+"`"+`stop()`+"`"+` +To **detach** (leave the process running): `+"`"+`stop(detach=true)`+"`"+` -> After stop, the target process is terminated. If you need to detach without killing, that is not currently supported — plan accordingly. +> Use detach when you want to observe without disrupting the process long-term. `, programDesc, pid, pid, ) diff --git a/docs/superpowers/skills/debug-attach.md b/skills/debug-attach.md similarity index 75% rename from docs/superpowers/skills/debug-attach.md rename to skills/debug-attach.md index 6a24182..a82d806 100644 --- a/docs/superpowers/skills/debug-attach.md +++ b/skills/debug-attach.md @@ -19,7 +19,7 @@ Before starting, gather: ## Important warnings - Attaching pauses the process. In production, this affects real users. -- After `stop()`, the target process is **terminated** — plan accordingly. +- `stop()` terminates the debuggee; use `stop(detach=true)` to leave it running. - You may need `sudo` or `ptrace_scope=0` permissions. --- @@ -42,18 +42,15 @@ If attach fails: ### 2. Understand what the process was doing -Immediately call: -```json -context() -``` - -This shows the state at the moment of pause. Key questions: +The `debug()` call already returns the initial context at the moment of attach — review it immediately. Key questions: - **Where is it?** What function and file? - **Why is it there?** Does the stack trace make sense? - **What are the local values?** Do they look reasonable? If the process was in a system call (I/O, sleep, mutex wait), the stack will show that explicitly. +Call `context()` again after resuming (e.g., after `continue()` or `pause()`) to refresh the current state. + ### 3. Check all threads / goroutines ```json @@ -61,8 +58,8 @@ info(kind="threads") ``` This is critical for concurrent programs. Look for: -- Threads blocked on the **same mutex or channel** → potential deadlock -- **More threads than expected** → goroutine leak +- Threads blocked on the **same lock or channel** → potential deadlock +- **More threads than expected** → goroutine/thread leak - Threads in **unexpected functions** → processing wrong data or stuck in error path For each suspicious thread: @@ -70,6 +67,15 @@ For each suspicious thread: context(threadId=) ``` +**Go-specific indicators:** +- `sync.(*Mutex).Lock` or `<-chan` in every goroutine's stack → classic deadlock +- Many goroutines in `runtime.park` → goroutines blocked on channel/select + +**C/C++-specific indicators:** +- `pthread_mutex_lock` or `futex` in every thread's stack → mutex deadlock +- Thread in `__GI___poll` or `epoll_wait` → waiting on I/O (usually expected) +- Thread in `malloc` / `free` with another in `malloc` → heap lock contention + ### 4. Scenario-specific investigation #### High CPU usage @@ -96,21 +102,29 @@ After attach, all threads should be visible. Look for: Use `context(threadId=)` on each blocked thread to see what lock/channel it's waiting on. -**Red flag:** `sync.(*Mutex).Lock` or `<-chan` in every goroutine's stack → classic deadlock. +**Go red flag:** `sync.(*Mutex).Lock` or `<-chan` in every goroutine's stack → classic deadlock. +**C/C++ red flag:** `pthread_mutex_lock` stacked below a function that also calls `pthread_mutex_lock` → lock ordering issue. #### Memory growth / leak -Check sizes of collections: +**Go — check collection sizes:** ```json evaluate(expression="len(cache)") evaluate(expression="len(connections)") evaluate(expression="cap(buffer)") ``` +**C/C++ — inspect pointer chains and reference counts:** +```json +evaluate(expression="list->size") +evaluate(expression="pool->count") +evaluate(expression="obj->refcount") +``` + Look for: -- Maps/slices that grow but never shrink -- Connection pools that accumulate but don't close -- Goroutines accumulating in `info(kind="threads")` +- Collections that grow but never shrink +- Connection/object pools that accumulate but don't release +- Goroutines / threads accumulating in `info(kind="threads")` #### Unexpected behavior / wrong results @@ -141,11 +155,16 @@ State findings clearly: > **The process is stuck in** `FunctionName` **at** `file.go:42` **because** `mutex.Lock()` **is blocked waiting for a lock held by goroutine** `threadId=3`. > **Root cause:** goroutine 3 is holding lock A while waiting for lock B; goroutine 1 holds lock B while waiting for lock A — circular deadlock. -Then clean up: +To **terminate** the debuggee: ```json stop() ``` +To **detach** and leave the process running: +```json +stop(detach=true) +``` + --- ## Decision Tree diff --git a/docs/superpowers/skills/debug-binary.md b/skills/debug-binary.md similarity index 97% rename from docs/superpowers/skills/debug-binary.md rename to skills/debug-binary.md index 67b385a..c5dd94c 100644 --- a/docs/superpowers/skills/debug-binary.md +++ b/skills/debug-binary.md @@ -130,7 +130,7 @@ step(mode="over") // step over (skip function call body) step(mode="out") // run until current function returns ``` -After each step, call `context()` to see the new instruction pointer. +`step` automatically returns the new location and context — no need to call `context()` separately unless you want to refresh after other operations. **Track state changes:** - Which registers change after each instruction? diff --git a/docs/superpowers/skills/debug-core-dump.md b/skills/debug-core-dump.md similarity index 100% rename from docs/superpowers/skills/debug-core-dump.md rename to skills/debug-core-dump.md diff --git a/docs/superpowers/skills/debug-source.md b/skills/debug-source.md similarity index 70% rename from docs/superpowers/skills/debug-source.md rename to skills/debug-source.md index e79ef8c..1cc481a 100644 --- a/docs/superpowers/skills/debug-source.md +++ b/skills/debug-source.md @@ -80,16 +80,28 @@ Refresh context at any time: context() ``` -Drill into specific values: +Drill into specific values. Expression syntax differs by debugger: + +**Delve (Go):** ```json evaluate(expression="user.Address.City") evaluate(expression="items[0]") evaluate(expression="len(queue)") evaluate(expression="err.Error()") +evaluate(expression="*ptr") +``` + +**GDB (C/C++):** +```json +evaluate(expression="user->address.city") +evaluate(expression="items[0]") +evaluate(expression="*(int*)ptr") +evaluate(expression="ptr == NULL ? \"nil\" : ptr->name") +evaluate(expression="(struct Node*)node->next") ``` **Decision guide:** -- Value is nil when it shouldn't be → trace back where it was set +- Value is nil/NULL when it shouldn't be → trace back where it was set or returned - Value is wrong → find where it was assigned incorrectly - Value is correct here → the bug is downstream; add a later breakpoint @@ -143,13 +155,35 @@ stop() ## Common Patterns and Root Causes +### Go (Delve) + | Symptom | Likely cause | What to check | |---------|-------------|---------------| | `nil` pointer panic | Missing nil check | Where was the pointer created/returned? | | Wrong value in calculation | Logic error or uninitialized var | Step through the computation | | Function called with wrong args | Caller bug | Step out to the caller, inspect args | -| Infinite loop | Missing termination condition | Evaluate the loop condition, check iterator | -| Goroutine deadlock | Lock ordering or missing unlock | Check all goroutines with `info(kind="threads")` | +| Infinite loop | Missing termination condition | `evaluate(expression="len(slice)")` or loop var | +| Goroutine deadlock | Lock ordering or missing unlock | `info(kind="threads")`, look for blocked goroutines | +| Index out of range | Off-by-one or empty slice | `evaluate(expression="len(s)")` before indexing | + +### C/C++ (GDB) + +| Symptom | Likely cause | What to check | +|---------|-------------|---------------| +| Segfault on pointer dereference | NULL pointer or dangling pointer | `evaluate(expression="ptr")` — is it `0x0`? | +| Segfault in memory function | Heap corruption, buffer overflow | Check array bounds, `malloc`/`free` pairing | +| Wrong value in struct field | Uninitialized memory or aliasing | `evaluate(expression="*(MyStruct*)ptr")` | +| Use-after-free | Accessing freed memory | Check ownership; use valgrind/ASAN for confirmation | +| Stack overflow | Deep recursion, large stack allocs | Look for repeated frames in the stack trace | +| Infinite loop | Off-by-one in termination condition | `evaluate(expression="i")`, `evaluate(expression="n")` in the loop | + +### Rust (GDB) + +| Symptom | Likely cause | What to check | +|---------|-------------|---------------| +| Panic: unwrap on None/Err | Missing error handling | Check what function returned None/Err | +| Panic: index out of bounds | Length check missing | `evaluate(expression="v.len")` | +| Segfault (rare, unsafe code) | Unsafe block bug | Examine unsafe blocks in the call stack | ## How to present findings diff --git a/tools.go b/tools.go index 19dcc3e..1785809 100644 --- a/tools.go +++ b/tools.go @@ -98,7 +98,7 @@ func (ds *debuggerSession) registerSessionTools() { // Always-available tools mcp.AddTool(ds.server, &mcp.Tool{ Name: "stop", - Description: "End the debugging session completely. Terminates the debuggee and cleans up the debugger process.", + Description: "End the debugging session. By default terminates the debuggee. Pass detach=true to detach without killing the process (leaves it running); detach requires adapter support (Delve supports it; GDB via cpptools may not).", }, ds.stop) mcp.AddTool(ds.server, &mcp.Tool{ Name: "breakpoint", @@ -292,7 +292,9 @@ type ClearBreakpointsParams struct { } // StopParams defines parameters for stopping the debug session. -type StopParams struct{} +type StopParams struct { + Detach bool `json:"detach,omitempty" mcp:"if true, detach from the process without terminating it (leaves the debuggee running); default false terminates the debuggee"` +} // clearBreakpoints removes breakpoints. func (ds *debuggerSession) clearBreakpoints(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[ClearBreakpointsParams]) (*mcp.CallToolResultFor[any], error) { @@ -640,8 +642,10 @@ func (ds *debuggerSession) disassembleCode(ctx context.Context, _ *mcp.ServerSes }, nil } -// stop ends the debugging session completely. -func (ds *debuggerSession) stop(ctx context.Context, _ *mcp.ServerSession, _ *mcp.CallToolParamsFor[StopParams]) (*mcp.CallToolResultFor[any], error) { +// stop ends the debugging session. +// If params.Detach is true, a DAP disconnect request is sent with terminateDebuggee=false +// so the debuggee keeps running after the adapter disconnects. +func (ds *debuggerSession) stop(ctx context.Context, _ *mcp.ServerSession, params *mcp.CallToolParamsFor[StopParams]) (*mcp.CallToolResultFor[any], error) { log.Printf("stop") if ds.cmd == nil && ds.client == nil { return &mcp.CallToolResultFor[any]{ @@ -649,6 +653,21 @@ func (ds *debuggerSession) stop(ctx context.Context, _ *mcp.ServerSession, _ *mc }, nil } + if params.Arguments.Detach && ds.client != nil { + // Send disconnect with terminateDebuggee=false so the debuggee keeps running. + if err := ds.client.DisconnectRequest(false); err != nil { + log.Printf("stop: disconnect request failed: %v", err) + } else { + if err := readAndValidateResponse(ds.client, "disconnect"); err != nil { + log.Printf("stop: disconnect response error: %v", err) + } + } + ds.cleanup() + return &mcp.CallToolResultFor[any]{ + Content: []mcp.Content{&mcp.TextContent{Text: "Detached from process (debuggee still running)"}}, + }, nil + } + ds.cleanup() return &mcp.CallToolResultFor[any]{