Content
# debug-mcp
An MCP (Model Context Protocol) server that gives AI agents interactive debugging
capabilities. This is the Rust port of `lldb-debug-mcp`, built on the official Rust MCP
SDK ([`rmcp`](https://crates.io/crates/rmcp)). It is **behaviorally feature-identical** to
the Go version — the same 21 tools, parameters, defaults, session state machine, DAP
handshake, response shapes, and error semantics — with three intentional, documented
deviations (see [Deviations from the Go server](#deviations-from-the-go-server)).
The motivation for the rewrite is a **pluggable debugger backend**: the tool and session
layers depend on a debugger-neutral `DebuggerBackend` trait, so a future backend (e.g.
WinDbg) can be added without touching the MCP tool layer. Today the only backend is
`lldb-dap`, driven over the Debug Adapter Protocol (DAP) on stdio.
> **Binary name `debug-mcp`, server name `debug`.** The published binary is `debug-mcp`
> and the advertised MCP server name is `debug` (the Go version used `lldb-debug-mcp` /
> `lldb-debug`). Backends are now pluggable, so the `lldb` prefix is reserved for the
> genuinely lldb-bound pieces (the lldb backend crate and lldb-dap detection). MCP clients
> that namespace tools by server name should use `debug`.
## Architecture
```mermaid
graph LR
Agent["AI Agent<br/>(Claude Code)"] -->|stdio / MCP (rmcp)| Server["Rust MCP Server<br/>(debug-mcp)"]
Server -->|stdio / DAP| LLDB["lldb-dap<br/>(LLVM)"]
LLDB -->|SB API| Target["Target<br/>Process"]
```
The server is a Cargo workspace of six crates, split along the `DebuggerBackend` seam so
the tool/session crates cannot reach DAP- or lldb-specific code:
| Crate | Role | Notes |
|-------|------|-------|
| `debugger-core` | contract | `DebuggerBackend` + `BackendFactory` traits, neutral types, `BackendEvent`, `BackendError`. Leaf crate — **no** `tokio`/`rmcp`/DAP dependency. |
| `dap-client` | generic DAP transport | Content-Length framing, sequence correlation, the pending-request map, the read loop, the stop waiter. |
| `lldb-backend` | lldb backend | `LldbBackend` (the launch/attach handshake, lldb-dap arg shapes, repl-mode/backtick) + `LldbFactory` (detect → spawn → connect). Built on `dap-client`. |
| `mcp-session` | session | `SessionManager`: state machine, breakpoint tracking, frame-map cache, output buffer. Depends only on `debugger-core`. |
| `mcp-tools` | tool layer | the 21 handlers, `Args` accessor, response builders, `flatten_variables`, hex-dump/output formatters, the rmcp `ServerHandler`. Depends only on `debugger-core` + `mcp-session` (+ `rmcp`). |
| `debug-mcp` | binary | `main`: wire the session + the `LldbFactory` into the `ToolServer`, serve over stdio via rmcp. |
**Seam guarantee.** `mcp-tools` and `mcp-session` depend on `debugger-core` only — they
cannot name a DAP or lldb type. Only the binary depends on a concrete backend crate, and
only to obtain a `dyn BackendFactory`. Adding a backend = a new backend crate implementing
the same two traits + one registration line in the binary, with zero changes above the
seam. (The `seam` Make target enforces this.)
### Session state machine
```mermaid
stateDiagram-v2
[*] --> idle
idle --> configuring : launch / attach
configuring --> stopped : StoppedEvent
configuring --> running : stop_on_entry=false
stopped --> running : continue / step
running --> stopped : StoppedEvent
running --> terminated : ExitedEvent
stopped --> terminated : ExitedEvent
terminated --> idle : disconnect
configuring --> idle : error
```
## Requirements
- Rust (stable) for building; a nightly toolchain + `rust-src` only for the optional
ThreadSanitizer run.
- `lldb-dap` (LLVM 18+) or `lldb-vscode` (older LLVM) at runtime.
- A C compiler (`gcc`/`clang`) only for building the integration-test fixtures.
### Installing lldb-dap
| Platform | Command |
|----------|---------|
| macOS | `xcode-select --install` |
| Ubuntu / Debian | `sudo apt install lldb` |
| Fedora | `sudo dnf install lldb` |
| Arch Linux | `sudo pacman -S lldb` |
The server auto-detects the binary using this fallback chain (matching the Go version):
1. `LLDB_DAP_PATH` environment variable
2. `lldb-dap` in PATH
3. `lldb-dap-{20..15}` in PATH (versioned, prefers higher)
4. `lldb-vscode` in PATH (older LLVM — `run_command` falls back to backtick-prefixing)
5. macOS only: `xcrun --find lldb-dap`
Set `LLDB_DAP_PATH` if auto-detection doesn't find it. The variable is read lazily at the
first `launch`/`attach`, never at startup.
## Build
```bash
cargo build --release -p debug-mcp
# binary at: <CARGO_TARGET_DIR or target>/release/debug-mcp
```
## How to use with Claude Code
### 1. Configure the MCP server
```bash
claude mcp add debug -- /path/to/debug-mcp
```
Or add it manually to your MCP settings (`.claude/settings.json` or project-level):
```json
{
"mcpServers": {
"debug": {
"command": "/path/to/debug-mcp"
}
}
}
```
If `lldb-dap` isn't on your PATH, pass the environment variable:
```json
{
"mcpServers": {
"debug": {
"command": "/path/to/debug-mcp",
"env": {
"LLDB_DAP_PATH": "/usr/lib/llvm-18/bin/lldb-dap"
}
}
}
}
```
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or
`%APPDATA%/Claude/claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"debug": {
"command": "/path/to/debug-mcp"
}
}
}
```
### 2. Compile your program with debug info
The target binary must be compiled with debug symbols. For C/C++:
```bash
gcc -g -O0 -o myprogram myprogram.c # or clang -g -O0 ...
```
For Rust, `cargo build` (debug profile) includes symbols by default.
### 3. Ask Claude to debug
Example prompts:
- *"Launch `./myprogram` and set a breakpoint at main.c line 42, then continue and show me the local variables when it hits"*
- *"Debug the segfault in `./crash_repro` — find where it crashes and inspect the state"*
- *"Attach to PID 12345 and get a backtrace of all threads"*
### Tips
- **Breakpoints before launch**: set breakpoints before `launch` — they are buffered and
flushed automatically during the DAP handshake.
- **`run_command` escape hatch**: `run_command` executes any LLDB command directly
(e.g. `run_command(command="watchpoint set variable x")`).
- **Concurrent pause**: while `continue` is blocking, a separate `pause` tool call can
interrupt execution.
- **Output capture**: program stdout/stderr is buffered and merged into `continue`/`step_*`
responses; `read_output` drains any additional output.
## Tools reference
### Session management
| Tool | Description | Parameters |
|------|-------------|------------|
| `launch` | Launch a program under the debugger | `program` (required), `args`, `cwd`, `env`, `stop_on_entry` |
| `attach` | Attach to a running process | `pid` or `wait_for` |
| `disconnect` | End the debug session | `terminate` (default true) |
### Breakpoints
| Tool | Description | Parameters |
|------|-------------|------------|
| `set_breakpoint` | Set a source-line breakpoint | `file` (required), `line` (required), `condition` |
| `set_function_breakpoint` | Break on function entry | `name` (required), `condition` |
| `remove_breakpoint` | Remove a breakpoint | `breakpoint_id` (required) |
| `list_breakpoints` | List all breakpoints | — |
### Execution control
| Tool | Description | Parameters |
|------|-------------|------------|
| `continue` | Resume execution (blocks until next stop) | `thread_id` |
| `step_over` | Step over current line | `thread_id`, `granularity` (line/instruction) |
| `step_into` | Step into function call | `thread_id`, `granularity` (line/instruction) |
| `step_out` | Step out of current function | `thread_id` |
| `pause` | Pause all threads | — |
### Inspection
| Tool | Description | Parameters |
|------|-------------|------------|
| `status` | Session state and stop info | — |
| `backtrace` | Call stack for a thread | `thread_id`, `levels` |
| `threads` | List all threads | — |
| `variables` | Variables in scope (recursive flattening) | `frame_index`, `scope` (local/global/register), `depth`, `filter` |
| `evaluate` | Evaluate an expression | `expression` (required), `frame_index` |
| `read_output` | Drain captured stdout/stderr | — |
### Advanced
| Tool | Description | Parameters |
|------|-------------|------------|
| `read_memory` | Read raw memory (hex dump) | `address` (required), `count` (required) |
| `disassemble` | Disassemble at address or PC | `address`, `instruction_count` (default 20) |
| `run_command` | Execute any LLDB command | `command` (required) |
## Deviations from the Go server
The Go implementation is the parity oracle. There are exactly three intentional, documented
deviations from it:
1. **Server identity.** The binary is `debug-mcp` (was `lldb-debug-mcp`) and the advertised
MCP server name is `debug` (was `lldb-debug`), reflecting that backends are now
pluggable. The DAP `clientID` sent to lldb-dap remains `lldb-debug-mcp` (an
lldb-dap-facing identifier below the seam, unchanged).
2. **`disassemble` default `instruction_count` = 20.** The design doc and README document
20; the Go *code* defaults to 10, treated as a latent bug. The Rust port aligns to the
documented intent (20). This is isolated to one default and its parity test.
3. **Numeric-validation policy (tool-boundary guards).** Go is permissive: it coerces
`float64 → int` and forwards clearly-invalid values straight to lldb-dap. Since this is a
debugger-control surface exposed to agents, the Rust port validates a *minimal* set of
clearly-invalid values at the tool boundary with predictable errors instead:
- `read_memory` `count` must be a positive integer → `'count' must be a positive integer`;
- an explicit, numeric `thread_id` (on `continue`/`step_*`/`backtrace`) must be positive →
`'thread_id' must be a positive integer` (an absent or non-numeric `thread_id` still
falls back to the last-stopped thread, then `1` — Go parity);
- `set_breakpoint` `line` must be a positive integer after truncation →
`'line' must be a positive integer`.
Valid values keep Go's `float64 → int` truncation (e.g. `line` `4.7 → 4`), and large
positive values are still forwarded unchanged (no caps are added). No other numeric
parameter is affected.
Relatedly (a robustness improvement on the *error* path, not a deviation on the success
path): the stopped-state breakpoint mutations (`set_breakpoint`,
`set_function_breakpoint`, `remove_breakpoint`) are now **transactional** — the session's
tracked breakpoint list is committed only after lldb-dap confirms the change, so a backend
rejection leaves the tracked state unchanged. The success-path output is identical to Go.
Everything else is byte-for-byte behavior parity at the level of observable MCP output
(field names, types, presence rules, values, and error strings). Object key order and
whitespace may differ (structural JSON parity).
## Development
There are two gates, and `make all` is **not** full coverage on its own:
- **`make all` — the hermetic gate.** Format check, build, `clippy -D warnings` (no
`#[allow]` — warnings are fixed at the source), unit tests, and the seam check. It needs
no `lldb-dap` and runs anywhere. Note it does **not** exercise the live runtime path: the
integration scenarios are behind the `integration` feature and compile as *zero tests*
under the default workspace test command, so a green `make all` does not prove launch /
debugging against real lldb-dap.
- **`make integration` — the live lldb-dap gate.** Builds `debug-mcp`, then runs the ported
integration scenarios + the golden cross-check + the differential lane against real
lldb-dap. This is the gate that proves the actual product path.
```bash
# Hermetic gate: format, build, lint, unit tests, seam.
make all
# or individually:
make fmt-check build clippy test seam
# Unit tests only (hermetic; integration scenarios compile as zero tests here).
cargo test --workspace
# Live integration + differential-parity suite (Phase 6).
# Requires lldb-dap + the compiled C fixtures. Each test SKIPS cleanly (logs + passes)
# when lldb-dap or a fixture is absent. Single-threaded (the suites share lldb-dap and
# the crash scenarios kill subprocesses by pid).
make -C testdata # build the C fixtures once
make integration
# ThreadSanitizer over the dap-client concurrency tests (nightly + rust-src).
make tsan
```
The differential-parity harness (`mcp-tools/tests/integration_differential.rs`) replays
identical MCP tool sequences against `debug-mcp` and the Go `lldb-debug-mcp` over stdio and
diffs the parsed JSON structurally, asserting the deviations above explicitly. It needs a
**Go oracle** — provided via `GO_DEBUG_MCP_BIN` (an explicit path) or `lldb-debug-mcp` on
PATH. Behavior when the oracle is absent:
- by default it **skips cleanly**, logging `SKIPPED (NOT compared)`, and the always-on
golden cross-check still validates the documented response shapes against `debug-mcp`;
- set `REQUIRE_GO_DIFFERENTIAL=1` to make the absence of the oracle (or of lldb-dap / the
Rust binary) a **hard failure** — so a CI/merge job can't pass while the strongest parity
lane silently no-ops. CI (`.github/workflows/ci.yml`) builds the Go oracle from the `main`
branch and runs this lane with the gate set.
## License
MIT
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
everything-claude-code
Complete Claude Code configuration collection - agents, skills, hooks,...
markitdown
MarkItDown-MCP is a lightweight server for converting URIs to Markdown.
cc-switch
All-in-One Assistant for Claude Code, Codex & Gemini CLI across platforms.
Train-in-Silence
The first Task-Aware MCP server and automated VRAM calculator for LLM...
stacklit
108,000 lines of code. 4,000 tokens of index. One command makes any repo...
AppClaw
AI-powered mobile automation agent — describe what you want in plain...