OpenOCD MCP Server
Debug microcontrollers directly from your AI assistant. This is an MCP
server that drives OpenOCD, letting any MCP-compatible AI
flash firmware, control execution, and inspect a running target โ and read your
variables and peripheral registers by name instead of raw addresses.
Description
Once connected to a target through a debug probe (ST-Link, J-Link, CMSIS-DAP, โฆ),
your AI assistant can:
- Flash firmware โ program and verify
.elf / .bin / .hex images
- Control execution โ halt, resume, single-step, reset
- Inspect state โ read/write CPU registers and memory
- Set breakpoints โ hardware & software, including conditional breakpoints
(halt only when an expression is true) and hit-count breakpoints
- Watch memory โ hardware watchpoints that halt on read/write/access to an address
- Read variables by name โ from your firmware's
.elf symbols (e.g. read_variable uart_rx_count)
- Live-watch variables โ a window that samples variables over time without halting the CPU, with expandable structs/arrays auto-typed from DWARF (signed/float/pointer/enum)
- Read peripheral registers by name โ from a CMSIS-SVD file, decoded into named bitfields (e.g.
RCC.CR, GPIOA.MODER)
- Safety gates โ permission layer (read-only mode, gated flash-erase, flash path/size limits) so the agent can't damage a target unexpectedly
The server is chip-agnostic โ it works with any target OpenOCD supports; you
point it at your chip's config and (optionally) SVD/ELF. It can also start
OpenOCD for you and download OpenOCD automatically for your platform, so
there's nothing else to install by hand.
Supported AI clients
Any MCP-compatible client works. Tested and known to work:
Installation
Prerequisites: Python 3.10+ and a debug probe connected to your target.
Clone and install the package into a virtual environment:
git clone https://github.com/microhenrio/openocd-mcp
cd openocd-mcp
python -m venv .venv
Install it (creates the openocd-mcp command):
.venv\Scripts\python -m pip install -e .
.venv/bin/python -m pip install -e .
Windows shortcut: run setup.bat โ it creates the environment, installs
the package, and registers it with Claude Code automatically.
OpenOCD is obtained automatically: a build for your OS/architecture is
downloaded and cached on first connect (checksum-verified). You can also fetch it
ahead of time with openocd-mcp install-openocd, or use an existing install by
setting the OPENOCD_BIN environment variable.
Registering with your AI client
The server executable is:
# Windows
<repo>\.venv\Scripts\openocd-mcp.exe
# macOS / Linux
<repo>/.venv/bin/openocd-mcp
Claude Code
claude mcp add --scope user openocd -- "%CD%\.venv\Scripts\openocd-mcp.exe"
claude mcp add --scope user openocd -- "$PWD/.venv/bin/openocd-mcp"
Restart Claude Code, then verify with claude mcp list.
Claude Desktop
Add to claude_desktop_config.json (Edit โ Settings โ Developer โ Edit Config):
{
"mcpServers": {
"openocd": {
"command": "/path/to/.venv/bin/openocd-mcp"
}
}
}
Restart Claude Desktop.
Cursor
Add to ~/.cursor/mcp.json (or .cursor/mcp.json in your project):
{
"mcpServers": {
"openocd": {
"command": "/path/to/.venv/bin/openocd-mcp"
}
}
}
Restart Cursor.
Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"openocd": {
"command": "/path/to/.venv/bin/openocd-mcp"
}
}
}
Restart Windsurf.
Cline (VS Code)
Open the Cline panel โ MCP Servers โ Add Server โ Manual, then enter:
{
"openocd": {
"command": "/path/to/.venv/bin/openocd-mcp"
}
}
Continue (VS Code / JetBrains)
Add to ~/.continue/config.json:
{
"mcpServers": [
{
"name": "openocd",
"command": "/path/to/.venv/bin/openocd-mcp"
}
]
}
VS Code + GitHub Copilot
Add to .vscode/mcp.json in your workspace (or user settings.json):
{
"servers": {
"openocd": {
"type": "stdio",
"command": "/path/to/.venv/bin/openocd-mcp"
}
}
}
Enable via Chat โ Agent mode in VS Code.
Updating
There's no self-update tool exposed over MCP โ updating means running commands in
a terminal, either yourself or by asking an AI that has shell access (e.g. Claude
Code). After updating, restart your AI client so it loads the new server;
if it was mid-session, it may need to stop the old openocd-mcp process first
(a file lock can block the reinstall while it's running).
PyPI install (pip install openocd-mcp or uvx):
pip install -U openocd-mcp
uvx --refresh openocd-mcp
Editable git-clone install (what setup.bat / this repo's instructions set up):
git pull
.venv/bin/python -m pip install -e .
Check the installed version with pip show openocd-mcp.
How to work with it
1. Point it at your chip
Each firmware project tells the server which target it's debugging. Create an
openocd-mcp.json in your project root (a template is in
openocd-mcp.example.json):
{
"target_cfg": "target/stm32g0x.cfg",
"svd_file": "path/to/STM32G0B0.svd",
"elf_file": "path/to/build/firmware.elf"
}
target_cfg / interface_cfg โ OpenOCD configs (relative to its scripts dir).
Defaults to an ST-Link probe; set target_cfg for your chip.
transport โ "swd" or "jtag". Set "swd" for a J-Link on Cortex-M
(with interface_cfg: "interface/jlink.cfg"); leave empty for ST-Link.
svd_file โ CMSIS-SVD file for the chip (enables peripheral registers by name).
elf_file โ your firmware build output (enables variables by name).
J-Link on Windows: OpenOCD reaches J-Links via libusb, so bind the J-Link's
debug interface to libusbK (or WinUSB) with Zadig
once (Interface 2 / MI_02). Newer SEGGER software (v7.x+) is compatible with
libusbK, so both OpenOCD and SEGGER tools can coexist. ST-Link works without
that step.
Or simply tell the AI the chip you're using and it will configure the session for
you. show_config reports the active settings at any time.
2. Describe what you want
With the board plugged in, describe what you want โ the AI picks the right tools:
| You sayโฆ | What happens |
|---|
| "connect and halt the target" | Starts OpenOCD if needed, attaches, halts the CPU |
| "what's the status?" | Reports running/halted and the current program counter |
"read the variable sensor_value" | Looks it up in the .elf and reads it off the chip |
"set motor_enabled to 1" | Writes the variable by name |
"watch tick_count live for 2 seconds" | Calls watch_variables โ samples it repeatedly without halting and returns a table of values over time, right in the chat |
"read GPIOA.MODER" | Reads the register and decodes its named bitfields |
"list the RCC registers" | Lists registers from the SVD |
"break at 0x08001234, then reset and run" | Sets a breakpoint and resets |
"break at 0x08001234 when r0 is 42" | Sets a conditional breakpoint (using [get_reg r0] == 42) |
"break at 0x08001234 after 5 hits" | Sets a hit-count breakpoint |
"watch for writes to 0x20000000" | Sets a hardware data watchpoint |
"flash build/firmware.elf and run it" | Programs, verifies, and restarts |
"dump 64 bytes of RAM at 0x20000000" | Reads memory |
You don't call tools by name โ describe the goal and the AI maps it to the
underlying tools.
3. Conditional breakpoints & watchpoints
Conditional breakpoints halt only when a condition holds โ useful for catching
one specific case in code that runs constantly. Conditions are TCL expressions and
can use two helpers:
get_reg <name> โ a CPU register value (e.g. get_reg r0, get_reg pc)
get_mem <addr> [width] โ a memory value (e.g. get_mem 0x20000000)
Just describe the intent; the AI builds the condition:
| You sayโฆ | Condition used |
|---|
"break at 0x08001234 only when r0 > 100" | expr {[get_reg r0] > 100} |
"break at parse_packet when the byte at 0x20000005 is 0xFF" | expr {[get_mem 0x20000005 8] == 0xFF} |
"stop at 0x08001234 on the 10th time it's hit" | incr ::hits; expr {$::hits >= 10} |
When the condition is false the server resumes automatically and keeps going until
it's true (or you stop it).
Watchpoints halt the CPU when it accesses a memory location โ ideal for finding
what corrupts a variable:
- "watch for writes to
0x20000000"
- "watch address
0x20000010 for any read or write"
Live-watching variables
There are two separate ways to watch a variable without halting the CPU โ
one the AI can trigger, one you run yourself:
| watch_variables (MCP tool) | openocd-watch (standalone GUI) |
|---|
| Triggered by | Asking the AI in chat | Running the command yourself in a terminal |
| Output | A text table of samples returned to the chat | A live-updating window with a tree view |
| Duration | One-shot: N samples, then it returns | Keeps running until you close it |
| Structs/arrays | Flat values only | Expandable, DWARF-typed |
Ask the AI for a quick, bounded look at a value over time โ "watch tick_count
for 10 samples", "sample sensor_value every 200ms for 2 seconds". This calls the
watch_variables tool directly; no extra setup needed beyond an ELF loaded.
Run the GUI yourself for an open-ended live view, especially of structs/arrays.
It's a standalone app that opens its own connection to OpenOCD, so it works fine
alongside any AI session driving the same target โ but it is not an MCP tool, so
the AI cannot open it for you; run it directly:
openocd-watch tick_count sensor_value --elf path/to/firmware.elf
openocd-watch tick_count sensor_value
It samples the variables without halting the CPU and refreshes a tree in a
window. Add entries with the box (press Enter or Add) and remove a selected
row with Remove (or the Delete key). Each entry is resolved automatically and
can be:
- a variable name (
uwTick, commsService) โ looked up in the ELF; if it's a
struct, union, or array it gets an expand triangle, and its members/elements
are shown auto-typed from DWARF (signed, float, pointer, enum, bitfields,
nested structs). Enums show the symbolic name (STATE_HEATING (1)), and
C bitfields show just their own bit(s) rather than the whole storage word;
- a hex address with optional size (
0x20000000, 0x20000000:2) โ read directly.
A Format dropdown switches how values are shown โ Auto (by C type) / Hex /
Decimal / Signed / Float (f32) / Binary โ and re-renders instantly. Use
--interval to change the poll rate, --format to set the initial format, or
--samples N for a headless printout. Requires Tkinter (ships with standard Python)
and a debug build (-g) for the type info.
If OpenOCD isn't already running, add --autostart and the window launches it
for you (and stops it on close) โ fully standalone, no AI client or .bat needed:
openocd-watch uwTick xTickCount --elf path/to/firmware.elf --autostart --target target/stm32g0x.cfg
CPU core registers (r0, pc, sp, โฆ) require the target to be halted โ
the AI halts first when needed. Memory, variables, and peripheral registers are
memory-mapped and can be read while the CPU is running (as the live-watch window
does). The first connect of a session starts OpenOCD automatically.
Safety / permissions
Mutating operations are gated so the agent can't damage a target unexpectedly.
Reads are always allowed; the gates apply to writes, flashing, erasing, and the
raw-command escape hatch.
| Permission | Default | Gates |
|---|
read_only | false | master switch โ blocks all writes/flash/erase/raw |
allow_memory_write | true | write_memory, write_variable, write_register, write_peripheral_register |
allow_flash | true | flash_write (program) |
allow_flash_erase | false | flash_erase_sector (destructive โ opt in) |
allow_raw_command | true | run_command (can bypass other limits) |
flash_allowed_paths | [] (any) | restrict flash_write to files under these dirs |
flash_max_bytes | 0 (no limit) | reject flashing files larger than this |
Set them three ways (later wins):
- A
permissions object in openocd-mcp.json (see openocd-mcp.example.json).
- The
set_permissions tool at runtime โ e.g. ask the AI to "make the target
read-only" or "allow flash erase for this session".
- The
OPENOCD_MCP_READONLY=1 environment variable (forces read-only).
A blocked call returns a clear BLOCKED: โฆ message explaining which permission to
enable. show_config lists the active permissions.