Skip to content

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.

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

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.

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.

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

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

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

ProbeProtocolNotes
DAP-LinkCMSIS-DAPOpen-source firmware, cheap
ST-Link V2/V3ProprietaryShips with STM32 Nucleo/Discovery boards
J-LinkProprietaryHigh performance, many features
FTDI-basedVariousFlexible, used in custom designs

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

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


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,
):
pass
MixinToolsDomain
ConnectionMixinconnect, start_openocd, disconnectSession lifecycle
DiagnosticsMixinprobe_diagnosticsHealth checks and capability discovery
TargetMixintarget_state, target_controlExecution state and control
MemoryMixinread_memory, write_memory, search_memoryMemory access with safety validation
RegistersMixinread_registers, write_registerCPU register read/write
FlashMixinflash_info, flash_programFlash inspection and programming
JTAGMixinjtag_scan, jtag_shiftScan chain enumeration and raw shifts
SVDMixinsvd_inspectSVD loading and peripheral decoding
RawMixinraw_commandOpenOCD TCL escape hatch

Each mixin accesses the shared SessionState through the FastMCP context:

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.

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.


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


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

The write_memory tool restricts writes to configured address ranges. By default, only SRAM (0x200000000x20100000) 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.

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.

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

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

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