Skip to main content
Glama
phryniszak

stm32-stlink-mcp

by phryniszak
README.md
# 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

A3.7/5.0

Scored across 19 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count4/5

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.

Completeness4/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues