Your First Debug Session
This tutorial walks through a complete first session with mcjtag — from installation to reading memory and registers on a live target. By the end, you will have connected to a microcontroller over JTAG or SWD and inspected its internal state.
Prerequisites
Section titled “Prerequisites”You will need:
- A debug probe — DAP-Link, ST-Link, J-Link, or an FTDI-based adapter connected via USB
- A target microcontroller wired to the probe (e.g., an STM32 Blue Pill, Nucleo board, or the DAP-Link + Blue Pill kit used for mcjtag development)
- OpenOCD installed on your system:
- Debian/Ubuntu:
sudo apt install openocd - macOS:
brew install openocd - Arch Linux:
sudo pacman -S openocd
- Debian/Ubuntu:
- Python 3.11+ with uv:
Terminal window curl -LsSf https://astral.sh/uv/install.sh | sh
Step 1: Install mcjtag
Section titled “Step 1: Install mcjtag”uvx mcjtagThis downloads mcjtag from PyPI and runs it. It starts an MCP server on stdin/stdout that your MCP client (Claude Code, etc.) connects to.
You can also install it persistently:
uv pip install mcjtagStep 2: Configure your MCP client
Section titled “Step 2: Configure your MCP client”Add mcjtag to Claude Code so it can call the tools during conversation.
With auto-spawned OpenOCD (recommended for getting started):
claude mcp add mcjtag -- env OPENOCD_CONFIG=/path/to/your/openocd.cfg uvx mcjtagThis tells mcjtag to start OpenOCD itself using your config file when it launches. You do not need to start OpenOCD separately.
Connecting to an already-running OpenOCD:
claude mcp add mcjtag -- uvx mcjtagWith this approach, start OpenOCD manually first:
openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfgThen, within a Claude Code session, the LLM can call connect() to attach to it.
Step 3: Run diagnostics
Section titled “Step 3: Run diagnostics”The first thing to do after connecting is run the 9-point health check:
probe_diagnostics()This tests each layer of the debug stack and reports pass/fail for each:
| Check | What it verifies |
|---|---|
connection | TCP link to OpenOCD’s TCL RPC interface |
openocd_version | OpenOCD is responding and reports its version |
adapter_speed | Debug adapter clock speed is readable |
transport | SWD or JTAG transport is selected |
target | At least one target is defined in the OpenOCD config |
target_state | Target is reachable (halted, running, etc.) |
memory_access | Can read the CPUID register at 0xE000ED00 (ARM Cortex-M SCB) |
flash | Flash bank topology is readable |
svd | Whether an SVD file is loaded for peripheral decoding |
If a check fails, the detail field explains what to try. For example, if memory_access
fails, the target may need to be halted first, or the wiring may be wrong.
A healthy system shows 8/9 or 9/9 passing (SVD is optional).
Step 4: Scan the JTAG chain
Section titled “Step 4: Scan the JTAG chain”jtag_scan()This enumerates all TAPs (Test Access Ports) on the JTAG chain. Each TAP has:
- name — the OpenOCD-assigned name (e.g.,
stm32f1x.cpu) - IDCODE — a 32-bit identifier baked into the silicon
- ir_length — instruction register length in bits
- enabled — whether the TAP is active
The IDCODE identifies the chip. For example, 0x1ba01477 is the ARM CoreSight
DAP found in many Cortex-M devices. The upper bits encode the manufacturer
(ARM, STMicroelectronics, etc.) and part number. The LLM can look these up
for you from context, or you can check the ARM DPIDR documentation.
If you are using SWD transport, jtag_scan() returns a single TAP (SWD does not
support daisy-chaining multiple devices).
Step 5: Read memory
Section titled “Step 5: Read memory”With the target halted, read the vector table at the base of flash:
read_memory("0x08000000", count=4)On ARM Cortex-M, the first two words of flash are:
| Offset | Contents |
|---|---|
+0x00 | Initial stack pointer (top of RAM) |
+0x04 | Reset handler address (entry point) |
+0x08 | NMI handler |
+0x0C | HardFault handler |
The response includes both raw hex values and a formatted hexdump:
08000000: 00 50 00 20 A1 01 00 08 AB 01 00 08 AD 01 00 08 |.P. ............|In this example, 0x20005000 is the initial SP (stack grows downward from there)
and 0x080001A1 is the reset handler (bit 0 set indicates Thumb mode, so the
actual address is 0x080001A0).
The width parameter controls element size — use width=8 for byte-level reads,
width=16 for halfwords, or the default width=32 for words. Maximum count per
call is 4096 elements.
Step 6: Read registers
Section titled “Step 6: Read registers”read_registers()This returns all CPU registers. On ARM Cortex-M, the key ones are:
| Register | Purpose |
|---|---|
r0-r12 | General-purpose registers |
sp (r13) | Stack pointer — points to the current top of the stack |
lr (r14) | Link register — return address of the last function call |
pc (r15) | Program counter — address of the next instruction to execute |
xPSR | Program status register — condition flags, ISR number, Thumb bit |
msp | Main stack pointer (used in handler mode) |
psp | Process stack pointer (used in thread mode, if configured) |
The pc value tells you exactly where the CPU is sitting right now. If the target
was halted by the debugger, the pc points to the next instruction that would execute.
If it was halted by a fault, the pc (and the stacked frame) tell you what went wrong.
You can also read specific registers:
read_registers(names=["pc", "sp", "lr"])What’s next
Section titled “What’s next”Now that you have a working debug connection, you can:
- Debug a crash — diagnose a HardFault by reading registers and decoding the exception stack frame
- Decode peripherals with SVD — load an SVD file and inspect GPIOA, USART1, RCC, or any other peripheral at the bitfield level
- Set up hardware — detailed wiring, probe selection, and OpenOCD configuration for various targets
- Understand the safety rails — what’s protected by default and how to adjust it for your workflow