---
title: Your First Debug Session
description: Connect to a microcontroller and explore it with mcjtag
sidebar:
  order: 1
---

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

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](https://www.amazon.com/dp/B0D93ZFVPY) 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:
  ```bash
  curl -LsSf https://astral.sh/uv/install.sh | sh
  ```

## Step 1: Install mcjtag

```bash
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:

```bash
uv pip install mcjtag
```

## 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):

```bash
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:**

```bash
claude mcp add mcjtag -- uvx mcjtag
```

With this approach, start OpenOCD manually first:

```bash
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.

## 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

```
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](https://developer.arm.com/documentation/ihi0031/latest/).

If you are using SWD transport, `jtag_scan()` returns a single TAP (SWD does not
support daisy-chaining multiple devices).

## 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

```
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

Now that you have a working debug connection, you can:

- **[Debug a crash](/tutorials/crash-debugging/)** — diagnose a HardFault by reading
  registers and decoding the exception stack frame
- **[Decode peripherals with SVD](/guides/svd-register-decoding/)** — load an SVD file
  and inspect GPIOA, USART1, RCC, or any other peripheral at the bitfield level
- **[Set up hardware](/guides/hardware-setup/)** — detailed wiring, probe selection,
  and OpenOCD configuration for various targets
- **[Understand the safety rails](/guides/safety-configuration/)** — what's protected
  by default and how to adjust it for your workflow
- **[Identify an unknown target](/guides/target-identification/)** — systematic
  workflow for figuring out what chip is on an unfamiliar board
