Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Changelog

## 0.3.0

Dual debug backend and AI-facing crash diagnosis.

### Added
- **Dual backend** behind a single `DebugBackend` trait, selected at `connect`:
- `probe-rs` (default) — native probe-rs; full flash and RTT support.
- `openocd` (experimental) — talks to an already-running `openocd` over the
GDB Remote Serial Protocol (`openocd_address`, default `127.0.0.1:3333`).
The AI uses the same tool set for both; only `connect` differs.
- `diagnose_fault` — reads the Cortex-M SCB fault registers
(CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID) plus PC/SP/LR in one call and returns a
compact structured evidence bundle with the set fault bits. Reports raw
evidence; it does not assert a root cause.
- `unwind_exception` — maps a crash to source lines. On the probe-rs backend it
returns a full DWARF backtrace via probe-rs's own unwinder; on the OpenOCD
backend it reads the Cortex-M exception stack frame and maps the faulting
PC/caller LR. Requires an `elf_path` built with debug info.
- 24 MCP tools total (added `diagnose_fault`, `unwind_exception`).

### Validated on real hardware
- OpenOCD backend verified end-to-end on a real **ESP32-S3** (openocd-esp32):
connect, memory read/write, halt/run/step/reset, and register reads
(PC/SP/LR) are correct.

### Notes / limitations
- OpenOCD register reads use `monitor reg <name>` (openocd resolves names per
architecture), so PC/SP/LR work across ARM/Xtensa/RISC-V. Start openocd with
`gdb_memory_map disable` or it may reject the GDB connection.
- `diagnose_fault` and `unwind_exception` are Cortex-M specific and do not apply
to Xtensa (ESP32) targets. Their probe-rs paths delegate to probe-rs's tested
DWARF/register code; they are unit-tested but not yet exercised on a physical
Cortex-M crash.

## 0.2.0

- Initial MCP server for embedded debugging via probe-rs (probe discovery,
target sessions, memory, breakpoints, flash, RTT), CLI, and bundled skill.
</content>
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[package]
name = "embedded-debugger-mcp"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
authors = ["adancurusul <adancurusul@gmail.com>"]
description = "A Model Context Protocol server for embedded debugging with probe-rs - supports ARM Cortex-M, RISC-V debugging via J-Link, ST-Link, and more"
description = "A Model Context Protocol server for embedded debugging via probe-rs or OpenOCD - ARM Cortex-M, RISC-V, and Xtensa (ESP32), with memory/flash/RTT and AI crash diagnosis"
license = "MIT"
readme = "README.md"
repository = "https://github.com/adancurusul/embedded-debugger-mcp"
keywords = ["mcp", "debugging", "embedded", "probe-rs", "rtt"]
keywords = ["mcp", "debugging", "embedded", "probe-rs", "openocd"]
categories = ["development-tools", "embedded"]

[dependencies]
Expand Down
68 changes: 60 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@
[![RMCP](https://img.shields.io/badge/RMCP-0.3.2-blue.svg)](https://github.com/modelcontextprotocol/rust-sdk)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

Embedded Debugger MCP is a Rust server for embedded debugging through
probe-rs. It exposes MCP tools for AI assistants and also includes a small CLI
and bundled skill for users who want a command-driven workflow without setting
up an MCP client first.
Embedded Debugger MCP is a Rust server for embedded debugging through either
probe-rs (native) or OpenOCD (via the GDB Remote Serial Protocol). It exposes a
single MCP tool set for AI assistants — probe discovery, target control, memory,
breakpoints, flash, RTT, and AI-facing crash diagnosis — plus a small CLI and
bundled skill for a command-driven workflow without setting up an MCP client
first.

Language versions: [English](README.md) | [中文](README_zh.md)

## What It Provides

- One MCP tool set over two interchangeable backends (probe-rs native, or
OpenOCD via GDB RSP), chosen at connect. Validated on real ESP32-S3.
- MCP tools for probe discovery, target connection, core control, memory access,
breakpoints, flash programming, and RTT communication.
breakpoints, flash programming, RTT communication, and crash diagnosis
(`diagnose_fault`, `unwind_exception`).
- CLI commands for environment checks, configuration inspection, probe listing,
MCP serving, and skill prompt handoff.
- A Codex/Claude Code compatible skill at `skills/embedded-debugger`.
Expand All @@ -27,17 +32,22 @@ Language versions: [English](README.md) | [中文](README_zh.md)
MCP client or CLI
|
v
embedded-debugger-mcp
embedded-debugger-mcp (one MCP tool set)
|
v
probe-rs -> debug probe -> target MCU
DebugBackend: probe-rs (native) | OpenOCD (GDB RSP)
|
v
debug probe / openocd -> target MCU
```

## Requirements

- Rust stable toolchain.
- A probe-rs compatible debug probe such as ST-Link, J-Link, DAPLink, Black
Magic Probe, or a supported FTDI-based probe.
Magic Probe, or a supported FTDI-based probe. Alternatively, a running
`openocd` exposing its GDB port (e.g. openocd-esp32 for ESP32) to use the
`openocd` backend.
- A supported target chip and working SWD/JTAG wiring for hardware operations.
- Nightly Rust plus `rust-src` for the bundled STM32 demo firmware check.

Expand Down Expand Up @@ -154,6 +164,24 @@ The first command validates the repository skill metadata, the second validates
the standard `SKILL.md` layout when the Codex skill creator validator is
installed, and the third validates the Claude Code plugin manifest.

## Backends

The tools run over one of two interchangeable engines, selected at `connect`:

- `backend: "probe-rs"` (default) — native probe-rs; full flash and RTT support.
- `backend: "openocd"` (experimental) — connects to an already-running `openocd`
over its GDB port (`openocd_address`, default `127.0.0.1:3333`) using the GDB
Remote Serial Protocol. Intended for targets probe-rs does not cover (e.g.
Xtensa ESP32 via openocd-esp32). Memory access and halt/run/step/reset are
validated on real ESP32-S3; flash and RTT are probe-rs only. Register reads
currently use ARM gdb register numbers, so PC/SP are wrong on Xtensa (known
limitation); `diagnose_fault` and `unwind_exception` are Cortex-M specific.
Start openocd with `gdb_memory_map disable`, otherwise it probes flash on the
GDB connect, fails, and rejects the connection — e.g.
`openocd -f board/esp32s3-builtin.cfg -c "gdb_memory_map disable"`.

The AI sees the same tool set for both; only `connect` arguments change.

## MCP Tool Set

Probe management:
Expand Down Expand Up @@ -184,6 +212,13 @@ Memory and breakpoints:
| `set_breakpoint` | Set a hardware breakpoint. |
| `clear_breakpoint` | Clear a hardware breakpoint. |

Diagnostics:

| Tool | Purpose |
|------|---------|
| `diagnose_fault` | Read the Cortex-M SCB fault registers (CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID) plus PC/SP/LR in one call and return a compact structured evidence bundle with the set fault bits. Reports raw evidence; it does not assert a root cause. Halt the target first. |
| `unwind_exception` | Unwind the stack after a crash and map each frame to a source line. On the probe-rs backend, returns a full DWARF backtrace (function + file:line per frame) via probe-rs's own unwinder; on the OpenOCD backend, reads the Cortex-M exception stack frame and maps the faulting PC / caller LR to source lines. Needs `elf_path` to firmware built with debug info (`.debug_line`). |

Flash:

| Tool | Purpose |
Expand All @@ -203,6 +238,23 @@ RTT:
| `rtt_read` | Read from an up channel with max byte and timeout limits. |
| `rtt_write` | Write to a down channel. |

## Crash Diagnosis

After a crash, halt the core and let the model reason over the evidence:

1. `halt`, then `diagnose_fault` — reads the Cortex-M SCB fault registers
(CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID) plus PC/SP/LR in one call and returns a
compact structured bundle with the set fault bits. It reports evidence; it
does not assert a root cause.
2. `unwind_exception` with `elf_path` — maps the crash to source lines. On the
probe-rs backend this is a full DWARF backtrace (function + `file:line` per
frame); on the OpenOCD backend it reads the exception stack frame and maps
the faulting PC / caller LR.

Both are Cortex-M specific (SCB registers, ARM exception frame) and do not apply
to Xtensa (ESP32) targets. The firmware must be built with debug info
(`.debug_line`) for source mapping.

## Safety Notes

- Flash erase is disabled unless `security.allow_flash_erase` or
Expand Down
61 changes: 53 additions & 8 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
[![RMCP](https://img.shields.io/badge/RMCP-0.3.2-blue.svg)](https://github.com/modelcontextprotocol/rust-sdk)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

Embedded Debugger MCP 是一个基于 probe-rs 的 Rust 嵌入式调试服务器。它为
AI 助手提供 MCP 工具,同时也提供小型 CLI 和内置 skill,让用户即使不安装
MCP 客户端,也可以先用命令行工作流完成检查和引导。
Embedded Debugger MCP 是一个 Rust 嵌入式调试服务器,可通过 probe-rs(原生)
或 OpenOCD(走 GDB Remote Serial Protocol)两种后端调试。它为 AI 助手提供同一套
MCP 工具——探针发现、目标控制、内存、断点、Flash、RTT,以及面向 AI 的崩溃诊断
——同时也提供小型 CLI 和内置 skill,让用户即使不安装 MCP 客户端也能先用命令行
工作流。

语言版本: [English](README.md) | [中文](README_zh.md)

## 功能

- MCP 工具覆盖探针发现、目标连接、核心控制、内存访问、断点、Flash 编程和
RTT 通信。
- 同一套 MCP 工具运行在两种可切换后端上(probe-rs 原生 / OpenOCD 走 GDB RSP),
在 connect 时选择。已在真实 ESP32-S3 上验证。
- MCP 工具覆盖探针发现、目标连接、核心控制、内存访问、断点、Flash 编程、RTT 通信
和崩溃诊断(`diagnose_fault`、`unwind_exception`)。
- CLI 命令覆盖环境检查、配置查看、探针列表、MCP 启动和 skill prompt 输出。
- 内置 Codex / Claude Code 兼容 skill: `skills/embedded-debugger`。
- 发布检查覆盖 rustfmt、clippy、测试、文档、打包和 STM32 demo 构建。
Expand All @@ -24,17 +28,21 @@ MCP 客户端,也可以先用命令行工作流完成检查和引导。
MCP client or CLI
|
v
embedded-debugger-mcp
embedded-debugger-mcp (one MCP tool set)
|
v
probe-rs -> debug probe -> target MCU
DebugBackend: probe-rs (native) | OpenOCD (GDB RSP)
|
v
debug probe / openocd -> target MCU
```

## 要求

- Rust stable 工具链。
- probe-rs 兼容调试探针,例如 ST-Link、J-Link、DAPLink、Black Magic Probe
或受支持的 FTDI 探针。
或受支持的 FTDI 探针。或者一个已运行、暴露了 GDB 端口的 `openocd`(例如 ESP32
用 openocd-esp32),以使用 `openocd` 后端。
- 目标芯片和可工作的 SWD/JTAG 连线。
- STM32 demo 固件检查需要 nightly Rust 和 `rust-src`。

Expand Down Expand Up @@ -149,6 +157,22 @@ embedded-debugger-mcp skill install --target both --home /tmp/embedded-debugger-
第一个命令验证仓库内 skill 元数据,第二个命令在已安装 Codex skill creator
validator 时验证标准 `SKILL.md` 布局,第三个命令验证 Claude Code 插件 manifest。

## 后端

同一套工具运行在两个可互换的引擎之上,在 `connect` 时选择:

- `backend: "probe-rs"`(默认)——原生 probe-rs,完整支持 flash 与 RTT。
- `backend: "openocd"`(实验性)——通过 GDB Remote Serial Protocol 连接一个已运行
的 `openocd`(`openocd_address`,默认 `127.0.0.1:3333`),用于 probe-rs 覆盖不佳的
芯片(如 Xtensa ESP32,走 openocd-esp32)。内存读写与 halt/run/step/reset 已在真实
ESP32-S3 上验证;flash 与 RTT 暂仅 probe-rs。读寄存器目前用 ARM 的 gdb 寄存器号,
在 Xtensa 上 PC/SP 会不对(已知局限);`diagnose_fault` 与 `unwind_exception` 是
Cortex-M 专属。启动 openocd 时须加 `gdb_memory_map disable`,否则它会在 GDB 连接时
探 flash 失败并拒绝连接,例如
`openocd -f board/esp32s3-builtin.cfg -c "gdb_memory_map disable"`。

对 AI 而言两者是同一套工具,只有 `connect` 参数不同。

## MCP 工具集

探针管理:
Expand Down Expand Up @@ -179,6 +203,13 @@ validator 时验证标准 `SKILL.md` 布局,第三个命令验证 Claude Code
| `set_breakpoint` | 设置硬件断点。 |
| `clear_breakpoint` | 清除硬件断点。 |

诊断:

| 工具 | 用途 |
|------|------|
| `diagnose_fault` | 一次调用读取 Cortex-M SCB 故障寄存器(CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID)及 PC/SP/LR,返回含已置位故障标志的紧凑结构化证据。只给原始证据、不下根因结论。请先 halt 目标。 |
| `unwind_exception` | 崩溃后回溯栈并把每帧映射到源码行。probe-rs 后端用 probe-rs 自带回溯器给出完整 DWARF 调用栈(每帧函数名+file:line);OpenOCD 后端读取 Cortex-M 异常栈帧,把出错 PC/调用者 LR 映射到源码行。需 `elf_path` 指向带调试信息(`.debug_line`)的固件。 |

Flash:

| 工具 | 用途 |
Expand All @@ -198,6 +229,20 @@ RTT:
| `rtt_read` | 从上行通道读取,并遵守最大字节数和超时限制。 |
| `rtt_write` | 向下行通道写入。 |

## 崩溃诊断

芯片崩溃后,先 halt,再让模型基于证据推理:

1. `halt`,然后 `diagnose_fault`——一次调用读取 Cortex-M SCB 故障寄存器
(CFSR/HFSR/MMFAR/BFAR/SHCSR/CPUID)及 PC/SP/LR,返回含已置位故障标志的紧凑
结构化证据;只给证据、不下根因结论。
2. `unwind_exception`(带 `elf_path`)——把崩溃映射到源码行。probe-rs 后端给出
完整 DWARF 调用栈(每帧函数名 + `file:line`);OpenOCD 后端读取异常栈帧并映射
出错 PC / 调用者 LR。

两者都是 Cortex-M 专属(SCB 寄存器、ARM 异常帧),不适用于 Xtensa(ESP32)。源码
映射需要固件带调试信息(`.debug_line`)。

## 安全说明

- 除非启用 `security.allow_flash_erase` 或 `flash.allow_erase`,否则 Flash
Expand Down
71 changes: 71 additions & 0 deletions examples/openocd_smoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//! Real-hardware smoke test for the OpenOCD backend.
//!
//! Drives `OpenOcdBackend` against a running `openocd` GDB server (default
//! 127.0.0.1:3333) and prints each operation's result. Intended to be run
//! against real hardware (e.g. ESP32-S3 via openocd-esp32) to confirm the RSP
//! protocol wiring end-to-end.
//!
//! Usage:
//! cargo run --release --example openocd_smoke -- [addr] [mem_hex]
//!
//! Notes: memory read/write and halt/run are architecture-neutral. Registers
//! are read by name via openocd's `monitor reg`, so PC/SP/LR read correctly on
//! both ARM and Xtensa (verified on real ESP32-S3).

use embedded_debugger_mcp::backend::{CoreRegId, DebugBackend, OpenOcdBackend};

#[tokio::main]
async fn main() {
let addr = std::env::args()
.nth(1)
.unwrap_or_else(|| "127.0.0.1:3333".to_string());
// Default read address: ESP32-S3 internal SRAM1.
let mem_addr = std::env::args()
.nth(2)
.and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok())
.unwrap_or(0x3FC8_8000);

println!("== openocd smoke test ==");
println!("connecting to {addr} ...");
let mut be = match OpenOcdBackend::connect(&addr).await {
Ok(b) => b,
Err(e) => {
eprintln!("CONNECT FAILED: {e}");
std::process::exit(1);
}
};
println!("connected (backend = {})", be.kind());

match be.halt().await {
Ok(()) => println!("halt : OK"),
Err(e) => println!("halt : ERR {e}"),
}
match be.status().await {
Ok(s) => println!("status : {s}"),
Err(e) => println!("status : ERR {e}"),
}

for a in [mem_addr, 0x4000_0000] {
match be.read_bytes(a, 16).await {
Ok(d) => {
let hex: String = d.iter().map(|b| format!("{b:02x}")).collect();
println!("read 0x{a:08X}: {hex}");
}
Err(e) => println!("read 0x{a:08X}: ERR {e}"),
}
}

// Read by name via 'monitor reg' — correct on ARM and Xtensa.
for reg in [CoreRegId::Pc, CoreRegId::Sp, CoreRegId::Lr] {
match be.core_reg(reg).await {
Ok(v) => println!("{reg:<10?}: 0x{v:08X}"),
Err(e) => println!("{reg:<10?}: ERR {e}"),
}
}

match be.run().await {
Ok(()) => println!("run : OK"),
Err(e) => println!("run : ERR {e}"),
}
println!("== done ==");
}
Loading
Loading