STM32 GDB MCP Server / STM32 GDB MCP 服务器
An MCP server that lets an AI agent debug STM32 firmware on real hardware — drive GDB + OpenOCD/ST-Link/J-Link to flash, breakpoint, inspect memory/registers/RTOS, triage HardFaults, and profile — and get back decoded, structured evidence instead of raw GDB text.
让 AI 智能体在真实硬件上调试 STM32:通过 GDB + OpenOCD/ST-Link/J-Link 烧录、打断点、查 内存/寄存器/RTOS、定位 HardFault、做性能采样,返回已解码的结构化证据而非原始 GDB 文本。
What you get / 能力一览
| Bring-up & flash / 启动与烧录 | detect_probe, suggest_server_args, build_firmware, flash_and_run, self_check, reset_target |
| Execution / 执行控制 | run_and_wait(结构化停止事件), run_for_duration(运行/采样后暂停并采集), breakpoint, step, halt/continue, debug_until |
| Inspect / 状态检查(需暂停) | capture_state, read_memory/read_variable, read_registers, frame, read_peripheral_register |
| Fault triage / 故障诊断 | reconstruct_fault_context(故障 PC → 源码), diagnose_fault, analyze_stack |
| RTOS / 实时系统 | detect_rtos, read_freertos, snapshot(scope=rtos) |
| Observe / 可观测性 | logging(RTT/SWO/UART), setup_swo(无需额外解码器的 printf), sample_pc(符号化采样器) |
| Determinism / 可复现性 | run_scenario(回放), batch, get_session 日志/指标, export_debug_report |
| Multi-board / 多板卡 | 任意工具传 session="name";使用 list_sessions/close_session 管理 |
Two cross-cutting goals: low comprehension cost (decoded outputs, suggested_next_actions)
and minimal repro steps (composites like flash_and_run / debug_until collapse 5–15 calls
into one). Compact mode keeps the visible surface small; every tool remains reachable through
call(tool, args), and tool_help exposes hidden schemas.
两个贯穿目标是:通过解码结果和 suggested_next_actions 降低理解成本,并通过
flash_and_run、debug_until 等组合工具把多次调用压缩为一次,缩短复现路径。
compact 模式只展示核心工具;所有隐藏工具仍可由 call(tool, args) 调用,并可用
tool_help 查询完整说明与 schema。
Install / 安装
| Client | One command |
|---|---|
| Claude Code(工具 + skills + 常驻规则) | /plugin marketplace add Zeraissh/stm32-gdb-mcp,然后 /plugin install stm32-debug-kit@zeraissh-stm32 |
| Cursor / VSCode / Codex / Windsurf / Trae | python scripts/deploy.py --project "<firmware dir>" --ide vscode,cursor |
| Manual / 手动配置(任意 MCP 客户端) | pip install -e .,再将客户端指向 stm32-gdb-mcp |
deploy.py installs the server, writes the IDE's MCP config, and drops a project-aware rules
file into your firmware project. Per-client config snippets + the rules template are in
docs/install-ides.md. Compact mode is on by default.
PyPI/console installs also expose stm32-gdb-mcp-check-env, stm32-gdb-mcp-install,
and stm32-gdb-mcp-deploy.
deploy.py 会安装服务器、写入 IDE 的 MCP 配置,并在固件项目中生成带项目上下文的规则文件。
各客户端配置片段和规则模板见 docs/install-ides.md。默认启用 compact
模式;PyPI/console 安装还会提供上述三个辅助命令。
stm32-gdb-mcp-check-env --json also reports the imported module version/path, installed
distribution version, and all four console scripts. Use stm32-gdb-mcp-deploy --upgrade
when those installation fields drift. / stm32-gdb-mcp-check-env --json 还会报告当前导入
模块的版本/路径、已安装发行版本和四个 console script;这些安装字段发生漂移时,使用
stm32-gdb-mcp-deploy --upgrade 修复。
Requirements / 环境要求:PATH 中需要 arm-none-eabi-gdb,以及至少一种 GDB Server
(openocd / JLinkGDBServerCL / st-util)。运行 python setup_env.py 检查。
30-second quickstart / 快速上手
detect_probe() # USB evidence; auto-select only one probe
debug_profile(action=set, mcu="STM32L431", probe="stlink", elf_path="build/app.elf", svd_path="STM32L4.svd")
suggest_server_args(mcu="STM32L431") # probe omitted -> use profile probe
start_debug_session(server_type="openocd") # server_args omitted -> infer from profile mcu/probe
self_check() # ALWAYS first: byte order, core, family
flash_and_run(file_path="build/app.elf", run_to="main")
breakpoint(action=set, location="my_func", condition="state == BAD")
run_and_wait() # structured stop event + next actions
run_for_duration(duration_sec=30, capture={"expressions": ["rx_count"]})
run_for_duration(duration_sec=60, sample={"interval_ms": 500, "expressions": ["rx_count", "state"]})
reconstruct_fault_context() # on a crash: faulting PC → file:line
detect_probe reads physical USB devices, keeps serials for identical probes, and never
chooses between multiple connected probes. / detect_probe 读取真实 USB 设备,同型号探针按
序列号分别保留;连接多个探针时绝不会擅自选择。
The full tool reference (lean families with action=/what=) is
skills/stm32-debug/reference/tool-map.md. The server
also ships always-on instructions, so any MCP client gets the debug loop without setup.
完整工具索引见 skills/stm32-debug/reference/tool-map.md,
其中按 action= / what= 归并工具族。服务器还会发送常驻 instructions,因此任意 MCP
客户端连接后都能获得同一套调试闭环。
Key rules (the target must cooperate) / 关键规则
- Reads need a HALTED core. If a read says
target_unresponsive,halt_executionfirst. / 读取要求内核已暂停。 若返回target_unresponsive,先执行halt_execution。 run_for_duration(sample=...)is best-effort low-rate debugger polling. It does not halt the target itself, but running-target expression reads may fail on some probes/MCUs; use SWO/ring-buffer firmware telemetry for higher-rate or guaranteed capture. /run_for_duration(sample=...)是尽力而为的低速调试器轮询;某些探针或 MCU 无法在运行中读取 表达式。需要更高采样率或可靠采集时,请使用 SWO 或固件环形缓冲。- A breakpoint TIMEOUT means the path was NOT reached — don't just retry. Halt,
capture_state,breakpoint(action=list)(hit_count=0 confirms), read the gating flag, set an earlier breakpoint or drive the precondition. / 断点超时表示路径没有到达。 不要原样重试;暂停后采集状态、确认 命中次数、读取门控条件,再前移断点或驱动前置条件。 - Writes are guarded (option bytes/IWDG/WWDG blocked) —
write_guard(action=policy)to allow. / 写操作受保护。 option bytes、IWDG、WWDG 默认禁止,需用write_guard(action=policy)显式放行。 - Never hard-kill OpenOCD (wedges the ST-Link USB) — use
recover_session. SWD is exclusive. / 不要强杀 OpenOCD。 使用recover_session;同一探针的 SWD 调试连接是独占的。 - A wedged probe needs a physical re-enumeration. With the optional
[hub]extra and a SmartUSBHub,hub(action=power)cuts VBUS andhub(action=data)drops the USB data lines, so that re-plug becomes a tool call. Without a hub it is still a human with a cable. / 卡死的探针必须物理重新枚举。 装上可选的[hub]extra 并接可编程 USB Hub 后,hub(action=power)可断 VBUS、hub(action=data)可断数据线,那次插拔就变成一次工具调用; 没有 Hub 时仍然只能靠人手拔插。
Programmable USB hub (optional) / 可编程 USB Hub(可选)
pip install 'stm32-gdb-mcp[hub]'
Then point a session at its port (1-based, matching the hub's silkscreen):
{"action": "set", "hub": {"channel": 2, "guard": "confirm"}}
hub(action=describe) is read-only and also reports per-port voltage/current on models
with an ADC. hub(action=power/data/cycle) requires confirm=true while the guard is in
its default confirm mode, and — in every mode, guard: allow included — whenever the
calling session's own GDB server is live on that port: cutting power mid-flash is how a
board becomes a brick. Note the scope: in local mode the check sees only the session
making the call, so it will not notice that a different session is mid-flash on that
channel. With the shared hub daemon (the default; see below) it sees every registered
session on every server.
Three entry points confirm on their own behalf, deliberately, because naming them is the
confirmation — and their decisions land in the same audit trail hub(action=describe)
returns. reset_target(strategy="cold") and recover_session's escalation ladder stop the
GDB session before the cut instead of yanking VBUS out from under it.
hub(action=discover) refuses outright while a session is live — except with
force=true, which bypasses the interlock entirely, so that flag is the one place you
are genuinely on your own. Set guard: allow for scripted CI. /
hub(action=describe) 是只读的,带 ADC 的型号还会返回每端口电压/电流。默认 confirm
模式下,hub(action=power/data/cycle) 必须显式传 confirm=true;调用方自己的会话在该端口
上有活跃 GDB 服务时,任何模式(含 guard: allow)下都强制要求——烧录中途断电正是把板子变砖的
方式。注意这条检查的范围:local 模式下它只看发起调用的那个会话,不会发现另一个会话正在该
通道上烧录;使用共享 Hub 守护进程时(默认,见下文)则能看到所有服务器登记的全部会话。
有三处入口会自行确认,这是刻意设计的——点名调用它们本身就是确认,且其决定同样进入
hub(action=describe) 返回的审计日志。reset_target(strategy="cold") 与 recover_session
的恢复阶梯会先停掉 GDB 会话再断电,而不是从活跃会话脚下拔掉 VBUS;hub(action=discover)
在有活跃会话时直接拒绝运行——但 force=true 会完全绕过该联锁,那是唯一真正需要你自己
负责的地方。CI 脚本可设 guard: allow。
Selecting a channel on a multi-board rack / 多板机架下的通道选择
hub: {channel: N} above pins one session to one port. With several boards, map the
whole rack once instead and let each session pick its own channel: /
上面的 hub: {channel: N} 把一个会话钉在一个端口上。多块板时改为一次性描述整个机架,
让每个会话各自选中自己的通道:
# bench_hub.yaml
hub:
port: COM7 # omit to auto-scan / 省略则自动扫描
guard: confirm
map:
3: {label: TC}
4: {label: AGS}
{"action": "load", "path": "bench_hub.yaml"}
Three things about this that are easy to get wrong: / 其中有三点很容易搞错:
- A
labelis matched against the debug session name, exactly. It is not a free-form nickname: the channel is selected only when the session was started assession="TC". This is the only way to select a channel for a serial-less probe —hub(action=discover, apply=true)keys those by USB port, which identifies a channel but cannot select one. /label是与调试会话名精确比对的,不是自由昵称:只有会话以session="TC"启动时才会 选中该通道。对于无序列号的探针这是唯一可行的选择方式——hub(action=discover, apply=true)只能按 USB 端口给它们建 key,那能认出通道,却无法选中通道。 - The debug profile is per-session. Loading the config into one session does not make
the map visible to another; the hub call then fails with
hub_unavailable: hub channel unmapped. Load it into each session:debug_config(action=load, path="bench_hub.yaml", session="TC"). The profile is also in-memory only, so this is re-run after every server restart. / 调试 profile 是按会话隔离的。 在一个会话里加载配置,另一个会话看不到该映射,此时 hub 调用会以hub_unavailable: hub channel unmapped失败。需要给每个会话都加载一次:debug_config(action=load, path="bench_hub.yaml", session="TC")。profile 还是纯内存的, 因此服务器每次重启后都要重新加载。 setreplaces a nested block; it does not merge into it.debug_profile(action=set, hub={...})keeps every OTHER profile field, but thehubblock you pass replaces the stored one whole — so writinghub={map: {3: {label: TC}}}afterhub(action=discover, apply=true)throws away the probe identity discovery had written there, and a serial-less probe has no other identity. Passmerge=trueto deep-merge into the stored block instead; leave it off when you mean to drop a stale channel, because replacing is the only way to remove one. /set对嵌套块是整块替换,不是合并。 顶层其他字段会保留,但传入的hub块会整块覆盖已 存的内容——在hub(action=discover, apply=true)之后再写hub={map: {3: {label: TC}}}, 会把 discovery 写入的探针身份一并丢掉(无序列号探针没有别的身份可用)。需要合并请加merge=true;要删除过期通道则保持默认的替换语义,因为那是唯一的删除方式。
When it works, the result carries channel_source: "map_label" — check that field rather
than assuming the right port was chosen. /
生效时结果会带 channel_source: "map_label"——请以该字段为准,不要想当然认为选对了端口。
Several MCP servers on one rack / 多个 MCP 服务器共用一个 Hub
The hub's control port opens in one process at a time, so by default the first hub call
spawns a small hub daemon that owns it, and every MCP server talks to that instead.
Nothing changes in how you call hub(...). /
Hub 控制口同一时间只能被一个进程打开。因此默认情况下,第一次调用 hub 会拉起一个Hub 守护进程
独占它,所有 MCP 服务器都改为与守护进程通信;hub(...) 的用法完全不变。
- Reads are shared. Writes lock only the channels they touch: a power cycle on CH1 never
interleaves with another switch on CH1, but a switch on CH4 runs alongside it. Exclusive
isolation locks the whole rack, and
hub(action=discover)holds a rack-wide lease for its whole run, so no other server's switch can corrupt the map. Waiting is FIFO among conflicting requests, so a rack-wide request is never starved. / 读取共享;写操作只锁住 涉及的通道:CH1 的电源循环不会与 CH1 上的其他开关交错,但 CH4 的操作可以同时进行。exclusive 隔离锁住整个机架;hub(action=discover)在整个运行期间持有全机架租约,其他服务器的开关无法 插入。冲突请求按先来后到排队,整机架请求不会被饿死。 - Each server's guard mode stays its own (
guard: allowon a CI server does not relax anyone else), and the audit trail records which server did what. / 每个服务器的 guard 模式各自独立,审计日志记录是哪台服务器执行的操作。 - What a server switched off is restored when it exits — or when the daemon finds its PID gone. / 某服务器断开的电源/数据线,在它退出(或守护进程发现其进程已消失)时自动恢复。
- Cross-server interlock. Every server registers its sessions that have a
hubblock (after each tool call, plus a 2 s heartbeat). Cutting power or data on a channel where any server has a live GDB server needsconfirm=true, whatever the guard mode, and the refusal names that session and server.hub(action=discover)refuses while any server on the rack is live. A session with nohubblock cannot be mapped to a channel and so is not protected. / 跨服务器互锁:每个服务器都会登记带hub配置的会话(每次工具调用后 同步登记,另有 2 秒心跳)。只要任一服务器在该通道上有活跃 GDB,断电/断数据就必须confirm=true(与 guard 模式无关),拒绝信息会指出是哪个服务器的哪个会话;任一服务器有活跃 会话时hub(action=discover)拒绝运行。没有hub配置的会话无法映射到通道,因此不受保护。 - A write waits at most 30 s behind another server's write, then is refused (not run late)
with the name of what held the hub; one that did wait reports
queued_ms. Every switch also invalidates every server'sdetect_probecache. / 写操作最多排队 30 秒,超时则拒绝执行 (而不是延后执行)并指出占用者;排过队的结果带queued_ms。每次切换都会让所有服务器的detect_probe缓存失效。 - The daemon exits after 10 idle minutes unless it still owes a channel. / 空闲 10 分钟且无待恢复通道时,守护进程自动退出。
python -m mcp_server.hub_daemon status # URL, version, connected servers
python -m mcp_server.hub_daemon stop # restores what clients switched off
python -m mcp_server.hub_daemon serve --config bench_hub.yaml # run it yourself, foreground
STM32_GDB_MCP_HUB_MODE=auto (default) uses the daemon and falls back to in-process when
the [hub] extra is absent or the daemon cannot start; daemon never falls back; local
is the old single-process behaviour. Everything binds to 127.0.0.1; writes need the token
in the per-user state file. /
STM32_GDB_MCP_HUB_MODE=auto(默认)使用守护进程,未安装 [hub] extra 或守护进程无法启动时
退回进程内模式;daemon 不回退;local 为原来的单进程行为。全部仅监听 127.0.0.1,写操作需
要用户私有状态文件中的 token。
Live dashboard / 实时看板
hub(action=dashboard) # -> data.url, e.g. http://127.0.0.1:8765/
A read-only page: per-port power/data state, live V/I/P with a 1/5/15-minute history
(power-off spans shaded), and a timeline of hub actions. With the daemon it is the
daemon's page, showing every connected MCP server and what each still owes; in local
mode it runs inside this server (state=off stops it) and lists its debug sessions. /
只读页面:各端口电源/数据线状态、电压电流功率及 1/5/15 分钟历史(断电区间灰色)、hub 操作时间线。
使用守护进程时就是守护进程的页面,显示所有已连接的 MCP 服务器及其待恢复通道;local 模式下
在本服务器内运行(state=off 停止),并列出本服务器的调试会话。
Response shape / 响应结构
Every tool returns a stable JSON envelope inside the MCP TextContent transport:
{ "ok": true, "data": {}, "error": null, "raw_response": null, "suggested_next_actions": [] }
Human-readable text lives in data.message / error.message; raw GDB output stays in
raw_response when it aids diagnosis. Errors carry a code (e.g. target_unresponsive).
可读消息位于 data.message / error.message;仅在有助于诊断时保留原始 GDB 输出到
raw_response。错误包含稳定的 code,例如 target_unresponsive。现代 MCP 客户端还会收到
内容相同的原生 structuredContent,错误结果设置 isError=true。
Agent guidance — three layers / 三层引导
- Inline / 内联 — most results carry
suggested_next_actions(the next loop step) / 大多数结果直接给出下一步动作。 - Always-on / 常驻 — the server's
instructionsinject automatically / 服务器自动注入调试闭环与关键规则。 - On-demand — skills:
stm32-debug(bring-up, HardFault, hang, minimal repro, replayable scenarios) andstm32-instrument(write-time SWO/ITM trace). In other IDEs the same guidance travels as a rules file (AGENTS.md). / 按需加载两项 skill;其他 IDE 通过AGENTS.md等规则文件获得同样指导。
Repeatable config / 可复现配置
Load a YAML debug profile so sessions reproduce across clients:
debug_config(action=load, path="mcp/board.yaml").
加载 YAML 调试 profile 可让不同客户端复现同一会话:
debug_config(action=load, path="mcp/board.yaml")。
mcu: STM32L431CCUx
probe: stlink
server_type: openocd
server_args: ["-f", "interface/stlink.cfg", "-f", "target/stm32l4x.cfg"]
elf_path: build/app.elf
svd_path: STM32L4.svd
Relative elf_path, svd_path, project_root, and swo.file values are resolved from the
YAML file's directory. Session start, logging, reset, and flash tools then use the loaded
profile when their corresponding arguments are omitted.
相对的 elf_path、svd_path、project_root 和 swo.file 均以 YAML 所在目录为基准解析。
随后启动会话、日志、复位和烧录工具会在省略对应参数时自动使用该 profile。
Load the profile, connect, and run the mandatory identity check in one MCP round trip:
batch(steps=[
{"tool": "debug_config", "args": {"action": "load", "path": "mcp/board.yaml"}},
{"tool": "start_debug_session", "args": {}},
{"tool": "self_check", "args": {}}
], stop_on_error=true)
以上配方在一次 MCP 往返中完成“加载配置 -> 连接 -> 自检”,任一步失败即停止。
See examples/configs/ for J-Link, OpenOCD, and non-flashing L151/L431/U535 HIL profiles. /
J-Link、OpenOCD 及不烧录的 L151/L431/U535 HIL 配置见 examples/configs/。
Develop / 开发
pip install -e ".[dev]"
python -m ruff check . && python -m pytest && python -m compileall src tests
python -m build && python scripts/check_dist_contents.py dist/*
docs/install-ides.md— per-IDE install + rules template / 各 IDE 安装与规则模板docs/TROUBLESHOOTING.md— common failures + recovery / 常见故障与恢复docs/hil-validation.md— HIL validation / 硬件在环验证(STM32_GDB_MCP_HIL=1)CONTRIBUTING.md,SECURITY.md,docs/release.md
Hardware validation runs on a self-hosted runner labeled stm32; normal CI is hardware-free
(lint, tests, compile, packaging).
硬件验证运行在带 stm32 标签的自托管 runner;普通 CI 不依赖硬件,只执行 lint、测试、
编译检查和发行包验证。
Metadata
Release files for stm32-gdb-mcp 0.15.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| stm32_gdb_mcp-0.15.0.tar.gz | 717.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stm32_gdb_mcp-0.15.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.1 MB
Release files / stm32_gdb_mcp-0.15.0.tar.gz
| Download URL | stm32_gdb_mcp-0.15.0.tar.gz |
|---|---|
| Size | 717.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5167700a1ecdfaf8a7a761e76e03dd9efe5aa3b4e485c0798e18c1100237a38a
|
|
BLAKE2b-256 checksum How to use checksums |
16dc888fa24f2b65c099771af0196760ce787a9c60646613dc13da14febdded8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / stm32_gdb_mcp-0.15.0-py3-none-any.whl
| Download URL | stm32_gdb_mcp-0.15.0-py3-none-any.whl |
|---|---|
| Size | 348.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1a72fae60e6f56892851bcfa2326a49f2057566618de9ed2aa958927ecfc47f5
|
|
BLAKE2b-256 checksum How to use checksums |
424d08dc4d7dc226362fd6ab952a41dfe3400d23eab90388ef290cdd5f9df641
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log