stm32-stlink-mcp
# stm32-stlink-mcp
MCP server for debugging STM32 microcontrollers over ST-LINK, built on STMicroelectronics' own
**STM32CubeCLT** toolset — `ST-LINK_gdbserver`, `STM32_Programmer_CLI`, and `arm-none-eabi-gdb`
(driven via GDB/MI2). No OpenOCD, J-Link, or probe-rs involved.
## Architecture
A debug session is a pair of child processes, exactly mirroring ST's own documented workflow
(UM2576, "STM32CubeIDE ST-LINK GDB server"):
```
arm-none-eabi-gdb --interpreter=mi2 --(TCP, target extended-remote)--> ST-LINK_gdbserver --(USB)--> ST-LINK --(SWD)--> STM32
```
`arm-none-eabi-gdb` is driven in MI2 mode so the server gets source-level stepping, symbolic
breakpoints, and symbol-aware expression evaluation for free, instead of hand-rolling the GDB
Remote Serial Protocol. `ST-LINK_gdbserver` owns the USB handle to the probe for the lifetime of
the session; flashing via gdb's `load` (MI: `-target-download`) is transparently delegated by the
server to `STM32CubeProgrammer`, so no session teardown is needed to reflash. A *standalone*
one-shot flash (`flash_standalone`, no session required) invokes `STM32_Programmer_CLI` directly
and therefore conflicts with an already-open session on the same probe — see the tool description.
## Setup
```bash
npm install
npm run build
```
Requires STM32CubeCLT to be installed and its `bin/` directories reachable — either already on
`PATH` (the CLT installer does this by default) or via `STMCP_CUBECLT_PATH` / per-tool overrides.
Run `npm run doctor` to check.
## Running
```bash
node dist/index.js serve # starts the MCP server on stdio (default mode)
node dist/index.js doctor # pre-flight check: tool resolution, connected probes, udev rules
node dist/index.js doctor --json
```
### Registering with an MCP client
```json
{
"mcpServers": {
"stm32-stlink": {
"command": "node",
"args": ["<path-to-this-repo>/stmcp/dist/index.js"]
}
}
}
```
## Configuration (environment variables)
| Variable | Default | Purpose |
|---|---|---|
| `STMCP_GDBSERVER_PATH` / `STMCP_PROGRAMMER_CLI_PATH` / `STMCP_ARM_GDB_PATH` | — | Per-binary override (highest priority) |
| `STMCP_CUBECLT_PATH` | — | CubeCLT install root; subpaths resolved via `STM32CubeCLT_metadata.sh -j` |
| `STMCP_STLINK_SERIAL` | — | Default probe serial (omit to auto-select if exactly one is attached) |
| `STMCP_DEFAULT_DEVICE` | `STM32G431CBTx` | Default MCU device string |
| `STMCP_DEFAULT_INTERFACE` | `swd` | `swd` or `jtag` |
| `STMCP_DEFAULT_FREQUENCY_KHZ` | `4000` | SWD/JTAG clock |
| `STMCP_MAX_SESSIONS` | `1` | Concurrent debug session cap |
| `STMCP_GDBSERVER_READY_TIMEOUT_MS` | `8000` | How long to wait for "Waiting for debugger connection..." |
| `STMCP_LOG_LEVEL` | `info` | `error` \| `warn` \| `info` \| `debug` |
| `STMCP_LOG_FILE` | — | Optional log file (stderr always used regardless — stdout is reserved for MCP framing) |
| `STMCP_ALLOW_FLASH_ERASE` | `false` | Enables the erase path |
| `STMCP_ALLOW_MEMORY_WRITE` | `true` | Enables `memory_write` |
| `STMCP_ALLOW_FLASH_ADDRESS_WRITE` | `false` | Allows `memory_write` to target the flash address window (normally blocked — use the flash tools instead) |
| `STMCP_ALLOWED_FILE_PATHS` | (unrestricted) | Comma-separated allowlist roots for ELF/bin file arguments |
| `STMCP_MAX_FILE_SIZE_BYTES` | `16777216` | Max size for file arguments |
| `STMCP_FLASH_RANGE_START` / `STMCP_FLASH_RANGE_END` | `0x08000000` / `0x08020000` | Flash address window for the write guard (default: 128KB, STM32G431CB) |
## Tools
| Domain | Tool | Purpose |
|---|---|---|
| Probe | `list_probes` | List connected ST-LINK probes |
| Session | `debug_connect` | Spawn gdbserver+gdb, load ELF symbols, connect |
| Session | `debug_disconnect` | Clean session teardown |
| Session | `debug_session_status` | Session info (one, or all) |
| Flash | `flash_standalone` | One-shot flash via STM32_Programmer_CLI, no session needed |
| Flash | `flash_load_in_session` | Reflash via gdb `load` inside an open session |
| Execution | `debug_run` | Resume/continue |
| Execution | `debug_halt` | Interrupt |
| Execution | `debug_reset` | Reset (monitor reset [halt]) |
| Execution | `debug_step` | Step over/into/out |
| Breakpoints | `breakpoint_set` / `breakpoint_clear` / `breakpoint_list` | By file:line, symbol, or `*addr` |
| Memory | `memory_read` / `memory_write` | Raw memory access (write is guarded) |
| Registers | `register_read` / `register_write` | Named core registers |
| Registers | `read_fault_registers` | One-call Cortex-M SCB fault register dump (CFSR/HFSR/... decoded) |
| Expressions | `evaluate_expression` | Symbol-aware evaluation via gdb MI |
### Deferred to v2
SVD peripheral register tools (memory_read/write + evaluate_expression already reach everything by
address), live/streaming memory polling, a plugin system, per-chip memory-region allowlists,
arbitrary gdb `monitor` passthrough, and option-bytes/RDP tools (bricking-capable, intentionally
out of scope).
## RTT
RTT (SEGGER Real Time Transfer — live, non-halting console/variable tracing) is intentionally
**not** implemented in this server. ST-LINK_gdbserver's GDB/MI stub has no non-stop mode, so
reading memory through this server's `debug_connect` session requires halting the core first —
which defeats RTT's purpose. The correct mechanism is direct AP memory access that never halts
the core (confirmed by reading ST's own STM32CubeMonitor source, which uses exactly this, and by
`STM32_Programmer_CLI`'s `-r32fast`).
That's what [`strtt`](https://github.com/phryniszak/strtt) already does, and
[`strtt-mcp`](https://github.com/phryniszak/strtt/tree/main/mcp) wraps it as its own MCP server
(`strtt_start`/`strtt_stop`/`strtt_status`/`strtt_read`/`strtt_write`). Register it **alongside**
this server rather than through it:
```json
{
"mcpServers": {
"stm32-stlink": { "command": "node", "args": ["<...>/mcp-server/dist/index.js"] },
"strtt": {
"command": "node",
"args": ["<path-to-strtt-repo>/mcp/dist/index.js"],
"env": { "STRTT_BIN": "<path-to-strtt-binary>" }
}
}
}
```
Start `strtt_start` with `tcp: true` to connect through the shared ST-LINK Server instead of
claiming the USB device directly — this lets it run concurrently with an open `debug_connect`
session here, since `GdbServerProcess` always passes `-t`/`--shared` to `ST-LINK_gdbserver`.
Without `tcp: true`, `strtt` and an open debug session will contend for the same probe.
## Hardware verification runbook
With an ST-LINK and target attached:
```bash
node dist/index.js doctor # confirm probe + tools resolve
npx @modelcontextprotocol/inspector node dist/index.js # interactive tool testing
```
Then, via the inspector or an MCP client:
1. `list_probes` → the probe's serial appears.
2. `debug_connect { elfPath, device, interface: "swd", serial }` → returns a `sessionId`.
3. `breakpoint_set { sessionId, location: "main" }` → returns a breakpoint number.
4. `debug_run { sessionId }` → halts with `reason: "breakpoint-hit"`.
5. `register_read { sessionId, registers: ["pc","sp","lr","r0"] }`.
6. `evaluate_expression { sessionId, expression: "<a known global>" }`.
7. `read_fault_registers { sessionId }` → benign/zero flags right after reset.
8. `debug_disconnect { sessionId }` → confirm no orphaned processes: `ps aux | grep -E 'ST-LINK_gdbserver|arm-none-eabi-gdb'`.
9. `flash_standalone { file, reset: "hard", run: true }` with no session open.
10. Negative test: open a session, then call `flash_standalone` on the same serial → expect `DEVICE_BUSY`.
**Note:** `debug_connect` halts the target's CPU. Don't attach to a board that's actively driving
actuators/outputs in a way where an unplanned halt would be unsafe, without first confirming that's OK.
TDQS
Scored across 19 tools
Each tool targets a distinct action: session lifecycle, run control, breakpoints, register/memory access, and flashing are clearly separated. The only similar pair, flash_standalone and flash_load_in_session, is explicitly differentiated by requiring or not requiring an active debug session.
All tool names are lowercase snake_case and mostly follow a predictable domain-prefixed verb pattern such as breakpoint_set, memory_read, and debug_run. Minor exceptions like debug_session_status and flash_standalone break the strict verb_noun pattern but remain easy to anticipate.
19 tools is on the heavier side, but the embedded debug workflow legitimately spans session management, execution control, breakpoints, register/memory inspection, fault diagnosis, and flashing. Each tool has a justifiable role, so the count feels slightly over ideal rather than bloated.
The toolset covers the full core debug loop: connect/disconnect, run/halt/reset/step, breakpoint management, register and memory access, expression evaluation, fault register dumps, and both standalone and in-session flashing. Missing conveniences like stack backtraces or watchpoints are minor gaps, not workflow-breaking omissions.