Skip to content

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.

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
  • Python 3.11+ with uv:
    Terminal window
    curl -LsSf https://astral.sh/uv/install.sh | sh
Terminal window
uvx mcjtag

This 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:

Terminal window
uv pip install mcjtag

Add mcjtag to Claude Code so it can call the tools during conversation.

With auto-spawned OpenOCD (recommended for getting started):

Terminal window
claude mcp add mcjtag -- env OPENOCD_CONFIG=/path/to/your/openocd.cfg uvx mcjtag

This 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:

Terminal window
claude mcp add mcjtag -- uvx mcjtag

With this approach, start OpenOCD manually first:

Terminal window
openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfg

Then, within a Claude Code session, the LLM can call connect() to attach to it.

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:

CheckWhat it verifies
connectionTCP link to OpenOCD’s TCL RPC interface
openocd_versionOpenOCD is responding and reports its version
adapter_speedDebug adapter clock speed is readable
transportSWD or JTAG transport is selected
targetAt least one target is defined in the OpenOCD config
target_stateTarget is reachable (halted, running, etc.)
memory_accessCan read the CPUID register at 0xE000ED00 (ARM Cortex-M SCB)
flashFlash bank topology is readable
svdWhether 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).

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).

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:

OffsetContents
+0x00Initial stack pointer (top of RAM)
+0x04Reset handler address (entry point)
+0x08NMI handler
+0x0CHardFault 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.

read_registers()

This returns all CPU registers. On ARM Cortex-M, the key ones are:

RegisterPurpose
r0-r12General-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
xPSRProgram status register — condition flags, ISR number, Thumb bit
mspMain stack pointer (used in handler mode)
pspProcess 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"])

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