Architecture
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
Section titled “The Full Stack”MCP Client (Claude Code, etc.) | MCP protocol (JSON-RPC over stdio) vmcjtag (FastMCP server) | Python API calls vopenocd-python (Python bindings) | TCL commands over TCP socket vOpenOCD (debug server) | CMSIS-DAP / SWD / JTAG protocol vDebug Probe (DAP-Link, ST-Link, J-Link) | Physical wires vTarget MCU (ARM Cortex-M, RISC-V, etc.)MCP Client
Section titled “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
Section titled “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
Section titled “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)sendsmdw 0x08000000 16session.target.halt()sendshaltsession.flash.write_image(path)sendsprogram <path> verify reset
OpenOCD
Section titled “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.)
- 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).
Debug Probe
Section titled “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 for details.
Target MCU
Section titled “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
Section titled “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.
class MCJTAGServer( ConnectionMixin, DiagnosticsMixin, TargetMixin, MemoryMixin, RegistersMixin, FlashMixin, JTAGMixin, SVDMixin, RawMixin,): passMixin Responsibilities
Section titled “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:
def _get_state(ctx: Context) -> SessionState: return ctx.request_context.lifespan_contextThe SessionState dataclass holds the active OpenOCD session, SVD path, and a lock for serializing connection state changes.
Why Mixins
Section titled “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
Section titled “Session Lifecycle”The FastMCP lifespan context manager handles session setup and teardown:
Server start | vlifespan(__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 stateThe lifespan context is shared with resource functions via a module-level reference (necessary because FastMCP resource functions don’t receive Context injection).
Safety Layers
Section titled “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
Section titled “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 for the full rationale.
Raw Command Deny-list
Section titled “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
Section titled “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
Section titled “Alignment Checks”Memory operations validate address alignment:
write_memoryrejects unaligned writes (they can corrupt hardware state)read_memorywarns on unaligned reads (they fault on Cortex-M0 targets)target_controlrequires 2-byte aligned addresses for resume/step (ARM Thumb requirement)
Value Range Validation
Section titled “Value Range Validation”write_memoryvalidates that each value fits within the specified width (e.g., 0xFF max for 8-bit writes)read_memorybounds the count to 4096 elements maximumsearch_memorybounds the range to 1 MB (configurable)
Data Flow Example
Section titled “Data Flow Example”Here is what happens when an LLM calls read_memory(address="0x20000000", count=4, width=32):
- MCP Client sends a JSON-RPC
tools/callmessage over stdio - FastMCP deserializes the request and routes it to
MemoryMixin.read_memory - mcjtag parses
"0x20000000"to integer, validates count and width - openocd-python sends
mdw 0x20000000 4over the TCP socket to port 6666 - OpenOCD translates this to SWD read transactions on the target’s memory bus
- Debug Probe clocks out the SWD frames and reads back the data
- Target MCU responds with 4 words from SRAM
- The response travels back up: probe to OpenOCD to openocd-python to mcjtag
- mcjtag formats the values as hex strings and builds a hexdump
- FastMCP serializes the
MemoryDumpmodel as JSON-RPC response - MCP Client receives the result and presents it to the LLM