---
title: Architecture
description: How mcjtag connects LLM clients to hardware
sidebar:
  order: 2
---

mcjtag bridges the gap between MCP-compatible clients and physical debug hardware. It translates high-level tool calls into low-level debug probe operations through a multi-layer stack.

## The Full Stack

```
MCP Client (Claude Code, etc.)
    |  MCP protocol (JSON-RPC over stdio)
    v
mcjtag (FastMCP server)
    |  Python API calls
    v
openocd-python (Python bindings)
    |  TCL commands over TCP socket
    v
OpenOCD (debug server)
    |  CMSIS-DAP / SWD / JTAG protocol
    v
Debug Probe (DAP-Link, ST-Link, J-Link)
    |  Physical wires
    v
Target MCU (ARM Cortex-M, RISC-V, etc.)
```

### MCP Client

Any MCP-compatible client -- Claude Code, a custom agent, or a script using the MCP SDK -- sends tool calls to mcjtag over stdin/stdout using JSON-RPC. The client sees 17 tools, 7 resources, and 5 prompts. It does not need to know anything about OpenOCD, JTAG, or SWD.

### mcjtag

The FastMCP server that receives MCP messages and translates them into operations on the debug target. It handles:

- **Input validation**: hex parsing, alignment checks, range validation
- **Safety enforcement**: write address restrictions, raw command deny-list, file size limits
- **Response formatting**: hex dump generation, bitfield decoding, progress reporting
- **Lifecycle management**: spawning/connecting to OpenOCD, session cleanup

mcjtag is structured as a set of 9 mixins (see below) that register tools on a single FastMCP instance.

### openocd-python

A Python library that communicates with OpenOCD's TCL server over a TCP socket (default port 6666). It translates Python method calls into TCL command strings, sends them to OpenOCD, and parses the responses. For example:

- `session.memory.read_u32(0x08000000, 16)` sends `mdw 0x08000000 16`
- `session.target.halt()` sends `halt`
- `session.flash.write_image(path)` sends `program <path> verify reset`

### OpenOCD

The Open On-Chip Debugger -- a widely-used open-source debug server that speaks the native protocol of dozens of debug probes. OpenOCD:

- Manages the physical connection to the debug probe over USB
- Translates high-level commands (read memory, halt, flash) into the probe's wire protocol
- Handles transport-specific details (SWD framing, JTAG state machine, etc.) -- see [JTAG and SWD Protocols](/concepts/jtag-swd-protocols/) for the wire-level details
- Exposes a TCL RPC server on port 6666 for external tools to connect to

OpenOCD can be pre-started by the user or auto-spawned by mcjtag (see [Environment Variables](/reference/environment/)).

### Debug Probe

A USB device that converts between the host computer's USB interface and the target's debug port. Common probes:

| Probe | Protocol | Notes |
|-------|----------|-------|
| DAP-Link | CMSIS-DAP | Open-source firmware, cheap |
| ST-Link V2/V3 | Proprietary | Ships with STM32 Nucleo/Discovery boards |
| J-Link | Proprietary | High performance, many features |
| FTDI-based | Various | Flexible, used in custom designs |

The shipped mcjtag configs target CMSIS-DAP probes (like the Treedix DAP-Link V1). See [OpenOCD Configurations](/reference/openocd-configs/) for details.

### Target MCU

The microcontroller being debugged. Connected to the probe via:

- **SWD**: 2 wires (SWDIO + SWCLK) -- most common for ARM Cortex-M
- **JTAG**: 4+ wires (TDI, TDO, TMS, TCK) -- supports chain scanning, used for FPGA and multi-device setups

The debug interface provides access to the target's registers, memory bus, flash controller, and execution control (halt, step, breakpoints).

---

## Mixin Architecture

mcjtag's tools are organized into 9 mixin classes. Each mixin owns a specific domain of functionality. The `MCJTAGServer` class inherits from all 9 mixins and registers their tools with the FastMCP instance.

```python
class MCJTAGServer(
    ConnectionMixin,
    DiagnosticsMixin,
    TargetMixin,
    MemoryMixin,
    RegistersMixin,
    FlashMixin,
    JTAGMixin,
    SVDMixin,
    RawMixin,
):
    pass
```

### Mixin Responsibilities

| Mixin | Tools | Domain |
|-------|-------|--------|
| `ConnectionMixin` | `connect`, `start_openocd`, `disconnect` | Session lifecycle |
| `DiagnosticsMixin` | `probe_diagnostics` | Health checks and capability discovery |
| `TargetMixin` | `target_state`, `target_control` | Execution state and control |
| `MemoryMixin` | `read_memory`, `write_memory`, `search_memory` | Memory access with safety validation |
| `RegistersMixin` | `read_registers`, `write_register` | CPU register read/write |
| `FlashMixin` | `flash_info`, `flash_program` | Flash inspection and programming |
| `JTAGMixin` | `jtag_scan`, `jtag_shift` | Scan chain enumeration and raw shifts |
| `SVDMixin` | `svd_inspect` | SVD loading and peripheral decoding |
| `RawMixin` | `raw_command` | OpenOCD TCL escape hatch |

Each mixin accesses the shared `SessionState` through the FastMCP context:

```python
def _get_state(ctx: Context) -> SessionState:
    return ctx.request_context.lifespan_context
```

The `SessionState` dataclass holds the active OpenOCD session, SVD path, and a lock for serializing connection state changes.

### Why Mixins

Mixins keep each domain in its own file with its own validation logic, while allowing all tools to share the same session state. Adding a new tool category means creating a new mixin file and adding it to the `MCJTAGServer` inheritance list -- no changes to existing code.

---

## Session Lifecycle

The FastMCP lifespan context manager handles session setup and teardown:

```
Server start
  |
  v
lifespan(__aenter__)
  |-- Check OPENOCD_CONFIG -> auto-spawn OpenOCD
  |-- Check OPENOCD_HOST   -> auto-connect
  |-- Check OPENOCD_SVD    -> auto-load SVD
  |-- yield SessionState
  |
  ... server handles requests ...
  |
lifespan(__aexit__)
  |-- Close OpenOCD session
  |-- Stop managed process (if spawned)
  |-- Clear state
```

The lifespan context is shared with resource functions via a module-level reference (necessary because FastMCP resource functions don't receive `Context` injection).

---

## Safety Layers

mcjtag operates on real hardware where mistakes can have physical consequences. Several safety mechanisms are built into the tool layer:

### Safe Write Ranges

The `write_memory` tool restricts writes to configured address ranges. By default, only SRAM (`0x20000000`--`0x20100000`) is writable. This prevents accidental writes to:

- **Flash** (requires erase-before-write, can brick the device)
- **Peripheral registers** (can change hardware state -- motors, heaters, RF transmitters)
- **System registers** (can lock out the debugger)

See [Memory Layout](/concepts/memory-layout/) for the full rationale.

### Raw Command Deny-list

The `raw_command` tool blocks destructive OpenOCD TCL commands by pattern matching against a deny-list. Blocked categories include flash operations, memory writes, resets, shutdown, and TCL metaprogramming commands that could bypass the deny-list.

This is a best-effort measure, not a sandbox. The dedicated typed tools (`flash_program`, `write_memory`, `target_control`) have proper validation and should be preferred.

### File Validation

The `flash_program` tool validates firmware images before programming:

- File must exist
- Extension must be a recognized firmware format
- File must not be empty
- File must not exceed 16 MB

### Alignment Checks

Memory operations validate address alignment:

- `write_memory` rejects unaligned writes (they can corrupt hardware state)
- `read_memory` warns on unaligned reads (they fault on Cortex-M0 targets)
- `target_control` requires 2-byte aligned addresses for resume/step (ARM Thumb requirement)

### Value Range Validation

- `write_memory` validates that each value fits within the specified width (e.g., 0xFF max for 8-bit writes)
- `read_memory` bounds the count to 4096 elements maximum
- `search_memory` bounds the range to 1 MB (configurable)

---

## Data Flow Example

Here is what happens when an LLM calls `read_memory(address="0x20000000", count=4, width=32)`:

1. **MCP Client** sends a JSON-RPC `tools/call` message over stdio
2. **FastMCP** deserializes the request and routes it to `MemoryMixin.read_memory`
3. **mcjtag** parses `"0x20000000"` to integer, validates count and width
4. **openocd-python** sends `mdw 0x20000000 4` over the TCP socket to port 6666
5. **OpenOCD** translates this to SWD read transactions on the target's memory bus
6. **Debug Probe** clocks out the SWD frames and reads back the data
7. **Target MCU** responds with 4 words from SRAM
8. The response travels back up: probe to OpenOCD to openocd-python to mcjtag
9. **mcjtag** formats the values as hex strings and builds a hexdump
10. **FastMCP** serializes the `MemoryDump` model as JSON-RPC response
11. **MCP Client** receives the result and presents it to the LLM
