jlink-mcp
Give AI hands to touch silicon.
An MCP server that lets LLMs debug embedded devices through SEGGER J-Link probes.
See it work
Real MCP tool calls against a real nRF52840-DK. Reproduce it yourself: npm run demo
Your firmware just crashed. One tool call:
> diagnose_crash
## Crash Diagnosis
### CPU State
Core: PC=0x000000B8 SP=0x2003FFA8 R0=0x0000000A R1=0x000007FF R2=0x00000001 ...
Status: XPSR=0x21000003 CONTROL=0x00000000 PRIMASK=0x00000000 ...
Stack: MSP=0x2003FFA8 PSP=0x00000000
⚠ CPU is in exception handler (IPSR=0x00000003)
### Fault Registers
CFSR=0x01000000 HFSR=0x40000000 DFSR=0x00000000 MMFAR=0xe000edf8 BFAR=0xe000edf8
### Decoded Faults
## UsageFault (UFSR):
- UNALIGNED: Unaligned memory access
## HardFault (HFSR):
- FORCED: Forced HardFault (escalated from configurable fault)
### Exception Stack Frame
R0 = 0x0000000A R1 = 0x000007FF
R12 = 0x00000000 LR = 0x000001F5
PC = 0x00000254 xPSR = 0x21000000
→ Faulting instruction at PC=0x00000254
### Recent Errors/Warnings from RTT
[WRN] sensor_drv: sample out of range, clamping seq=2
Fault decoded, exception frame unwound, faulting instruction named, and the
device's own log correlated — from one call, without a human reading a
datasheet to find out what bit 24 of CFSR means.
Verbatim output from an nRF52840-DK in this project's hardware test suite.
What is this?
jlink-mcp connects AI assistants (Claude, Copilot, etc.) to your embedded hardware via SEGGER J-Link debug probes using the Model Context Protocol.
Instead of manually typing J-Link commands, your AI assistant can:
- Read registers and memory to understand device state
- Flash firmware and reset devices
- Stream RTT logs and search them by level/module/regex
- Diagnose crashes by auto-decoding ARM Cortex-M fault registers
- Control execution — halt, step, resume, breakpoints
- Start GDB servers for full debugging sessions
Also supports OpenOCD (ST-Link, CMSIS-DAP, FTDI) and Black Magic Probe backends.
Quick Start
Claude Desktop
Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"jlink": {
"command": "node",
"args": ["/path/to/jlink-mcp/out/mcp/standalone.js"],
"env": {
"JLINK_DEVICE": "nRF52840_XXAA"
}
}
}
}
Claude Code
Add .mcp.json to your project root:
{
"mcpServers": {
"jlink": {
"command": "node",
"args": ["out/mcp/standalone.js"],
"cwd": "/path/to/jlink-mcp",
"env": {
"JLINK_DEVICE": "nRF52840_XXAA"
}
}
}
}
VSCode Extension
Install the extension (requires VSCode 1.110+, matching engines.vscode). It auto-registers the MCP server via the native vscode.lm API. Configure the device in settings:
jlinkMcp.jlink.device = "nRF52840_XXAA"
Copilot Chat and Claude in VSCode will automatically discover all 31 tools.
From Source
git clone https://github.com/Klievan/jlink-mcp.git
cd jlink-mcp
npm install
npm run compile
JLINK_DEVICE=nRF52840_XXAA node out/mcp/standalone.js
| Tool |
Description |
start_debug_session |
One-call setup. Starts GDB server + connects RTT + returns boot log. |
snapshot |
Captures full device state: registers, fault status, stack dump, RTT output. |
diagnose_crash |
Auto-reads and decodes ARM Cortex-M fault registers (CFSR, HFSR, MMFAR, BFAR) with exception stack frame. |
Device Setup
| Tool |
Description |
list_devices |
Scan for connected probes and show the configured target |
set_device |
Change the target device at runtime — no restart needed |
get_config |
Current probe, target device, and GDB server state |
Device Control
| Tool |
Description |
device_info |
Probe type, target CPU, compact register summary |
halt |
Halt CPU |
resume |
Resume CPU |
reset |
Reset device (optionally halt after reset) |
step |
Single-step one instruction |
Memory & Registers
| Tool |
Description |
read_memory |
Read memory at address (clean hex dump output) |
write_memory |
Write 32-bit value to address |
read_registers |
All CPU registers in compact format |
read_register |
Read specific register (PC, SP, R0-R12, etc.) |
Flash
| Tool |
Description |
flash |
Flash .hex/.bin/.elf firmware to device |
erase |
Erase entire flash |
Breakpoints
| Tool |
Description |
set_breakpoint |
Set hardware breakpoint at address |
clear_breakpoints |
Clear all breakpoints |
GDB Server
| Tool |
Description |
gdb_server_start |
Start probe's GDB server |
gdb_server_stop |
Stop GDB server + disconnect RTT |
gdb_server_status |
GDB server, RTT, and proxy status |
Source-Level Debugging
Attach a real GDB client for symbol-aware work — backtraces, variable
inspection, and stepping by source line rather than by instruction.
| Tool |
Description |
gdb_connect |
Attach a GDB client (auto-starts the server; optional ELF for symbols) |
gdb_load |
Load an ELF for debug symbols, optionally flashing it too |
gdb_backtrace |
Call stack, optionally with locals in each frame |
gdb_command |
Run any GDB command — print sensor_state, info threads, break main |
gdb_wait |
Wait for the target to stop (after a continue or a breakpoint) |
gdb_disconnect |
Detach the client, clearing breakpoints and debug hardware |
RTT (Real-Time Transfer)
| Tool |
Description |
rtt_connect |
Connect to RTT telnet port |
rtt_disconnect |
Disconnect from RTT |
rtt_read |
Read recent log lines (ANSI stripped, Zephyr format parsed) |
rtt_search |
Filter logs by level (err/wrn/inf/dbg), module, or regex |
rtt_send |
Send data to device via RTT down-channel |
rtt_clear |
Clear RTT buffer |
Telnet Proxy (Trice / Pigweed)
| Tool |
Description |
telnet_proxy_start |
Start TCP proxy that tees RTT for external detokenizers |
telnet_proxy_stop |
Stop proxy |
telnet_proxy_status |
Proxy connection status |
telnet_proxy_read |
Read raw proxy buffer |
Advanced
| Tool |
Description |
probe_command |
Execute raw probe commands |
get_config |
Current probe and server configuration |
Multi-Probe Support
jlink-mcp supports multiple debug probe backends through a common ProbeBackend abstraction:
| Backend |
Probe Hardware |
Status |
RTT Support |
| J-Link |
SEGGER J-Link, J-Link OB, J-Link EDU |
Production |
Yes |
| OpenOCD |
ST-Link, CMSIS-DAP, FTDI, J-Link (via OpenOCD) |
Beta |
No |
| Black Magic Probe |
BMP (built-in GDB server on serial) |
Beta |
No |
| probe-rs |
All probe-rs supported probes |
Planned |
Planned |
Selecting a Backend
# J-Link (default)
PROBE_TYPE=jlink JLINK_DEVICE=nRF52840_XXAA node out/mcp/standalone.js
# OpenOCD with ST-Link
PROBE_TYPE=openocd \
OPENOCD_INTERFACE=interface/stlink.cfg \
OPENOCD_TARGET=target/stm32f4x.cfg \
node out/mcp/standalone.js
# Black Magic Probe
PROBE_TYPE=blackmagic \
BMP_SERIAL_PORT=/dev/ttyACM0 \
node out/mcp/standalone.js
Architecture
┌─────────────────────────────────────────────────────┐
│ MCP Client │
│ (Claude, Copilot, any MCP client) │
└──────────────────────┬──────────────────────────────┘
│ JSON-RPC over stdio
┌──────────────────────▼──────────────────────────────┐
│ jlink-mcp │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ 31 Tools │ │4 Resources│ │ 4 Prompts │ │
│ └────┬─────┘ └────┬─────┘ └───────┬───────────┘ │
│ │ │ │ │
│ ┌────▼──────────────▼────────────────▼───────────┐ │
│ │ ProbeBackend │ │
│ │ ┌─────────┐ ┌─────────┐ ┌──────────────────┐ │ │
│ │ │ J-Link │ │ OpenOCD │ │ Black Magic Probe│ │ │
│ │ └────┬────┘ └────┬────┘ └────────┬─────────┘ │ │
│ └───────┼───────────┼───────────────┼─────────────┘ │
│ │ │ │ │
│ ┌───────▼───┐ ┌─────▼────┐ ┌───────▼──────────┐ │
│ │ RTTClient │ │TelnetProxy│ │ ProcessManager │ │
│ └───────────┘ └──────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────┘
│
┌────────────▼────────────┐
│ Debug Probe (USB) │
│ → Target MCU (SWD/JTAG)│
└─────────────────────────┘
Source Layout
src/
├── probe/
│ ├── backend.ts # ProbeBackend abstract class + shared utilities
│ ├── jlink.ts # SEGGER J-Link implementation
│ ├── openocd.ts # OpenOCD implementation
│ ├── blackmagic.ts # Black Magic Probe implementation
│ └── factory.ts # Probe creation from config
├── mcp/
│ ├── server.ts # MCP server (31 tools, 4 resources, 4 prompts)
│ └── standalone.ts # Standalone entry (stdio transport)
├── rtt/
│ └── rtt-client.ts # RTT client with ANSI stripping + Zephyr log parsing
├── telnet/
│ └── telnet-proxy.ts # TCP proxy for Trice/Pigweed detokenizer
├── utils/
│ ├── config.ts # VSCode settings / env var config
│ ├── logger.ts # Logging
│ └── process-manager.ts # Child process lifecycle
└── extension.ts # VSCode extension + MCP provider registration
Design Decisions (LLM-Optimized)
This server was built by having an AI use it against real hardware, then fixing every friction point.
What read_registers would give you
Raw JLinkExe output for halt; regs — 77 lines, of which about six carry
information. Every token here costs context, and the register values are buried
in the middle:
SEGGER J-Link Commander V9.70 (Compiled Aug 19 2026 12:16:13)
DLL version V9.70, compiled Aug 19 2026 12:15:25
Connecting to J-Link ...O.K.
Firmware: J-Link OB-nRF5340-NordicSemi compiled Jun 11 2026 13:12:28
Hardware version: V1.00
J-Link uptime (since boot): 0d 00h 34m 29s
S/N: 1050298247
License(s): RDI, FlashBP, FlashDL, JFlash, GDB
...30 more lines of connect banner...
PC = 00000044, CycleCnt = 00EB67E5
R0 = 20000000, R1 = 9D56C547, R2 = 00000000, R3 = 00000000
...
FPS0 = 00000000, FPS1 = 00000000, FPS2 = 00000000, FPS3 = 00000000
...28 more lines of zeroed FP registers...
What it actually gives you
Three lines. Same information, grouped by what you would ask for:
Core: PC=0x00000046 SP=0x20010000 R0=0x20000000 R1=0x01D43416 R2=0x00000000 ...
Status: XPSR=0x01000000 CONTROL=0x00000000 PRIMASK=0x00000000 BASEPRI=0x00000000
Stack: MSP=0x20010000 PSP=0x00000000
Both captured from the same board. The rest of the design follows the same rule
— return what was asked for, and nothing else:
- Output parsing strips the connection banner. Only data comes back.
- Registers are compact and grouped (core / status / stack).
- FP registers only shown if non-zero (they're usually all zeros).
- RTT output has ANSI escape codes stripped and Zephyr log format parsed into structured fields.
- Composite tools (
start_debug_session, snapshot, diagnose_crash) replace multi-step workflows with single calls.
- Fault decoding is automatic — reads CFSR/HFSR/MMFAR/BFAR and explains each bit.
rtt_search lets you find errors without reading the entire log.
Verified on real hardware
Most of what can go wrong between an LLM and a debug probe fails quietly: a
tool returns success with an empty payload, a parser drops half a line, a
session dies and the next command reports something plausible instead. None of
that is visible from reading the code, and a test suite that asserts "the call
did not error" passes on all of it.
So this project runs a hardware tier: 58 tests against a real nRF52840-DK
on a self-hosted runner, driving the actual MCP server over stdio exactly as a
client would. It covers probe discovery, flash and verify, halt/step/resume
under a live GDB session, memory and peripheral reads, RTT streaming and
filtering, and crash diagnosis against faults injected on demand.
It has caught bugs that had been shipping green, including:
diagnose_crash reporting "No faults detected" during real crashes — the
memory-dump parser was dropping half of every line
- every GDB-routed tool returning empty output while the server reported
itself healthy
- sessions leaving the target unbootable, with breakpoint comparators still
armed that no reset clears
Raw probe output captured during those runs is committed as golden transcripts,
so a fast unit tier replays real device bytes in seconds on any machine —
no probe required to catch a format regression.
npm test # ~190 tests, seconds, no hardware
npm run test:hil # hardware tier; needs HIL=1 and a probe
Environment Variables
J-Link
| Variable |
Default |
Description |
PROBE_TYPE |
jlink |
Probe backend: jlink, openocd, blackmagic |
JLINK_DEVICE |
Unspecified |
Target device (e.g., nRF52840_XXAA, STM32F407VG) |
JLINK_INSTALL_DIR |
Auto-detect |
Path to SEGGER J-Link installation |
JLINK_INTERFACE |
SWD |
Debug interface: SWD or JTAG |
JLINK_SPEED |
4000 |
Connection speed in kHz |
JLINK_SERIAL |
|
J-Link serial number (multi-probe) |
JLINK_GDB_PORT |
2331 |
GDB server port |
JLINK_RTT_PORT |
19021 |
RTT telnet port |
OpenOCD
| Variable |
Default |
Description |
OPENOCD_BINARY |
openocd |
Path to openocd binary |
OPENOCD_INTERFACE |
interface/stlink.cfg |
Interface config file |
OPENOCD_TARGET |
target/stm32f4x.cfg |
Target config file |
OPENOCD_GDB_PORT |
3333 |
GDB server port |
OPENOCD_TELNET_PORT |
4444 |
Telnet command port |
Black Magic Probe
| Variable |
Default |
Description |
BMP_GDB_PATH |
arm-none-eabi-gdb |
Path to GDB binary |
BMP_SERIAL_PORT |
/dev/ttyACM0 |
BMP serial port |
BMP_TARGET_INDEX |
1 |
Target index after scan |
Prerequisites
- SEGGER J-Link Software installed (JLinkExe, JLinkGDBServer)
- A J-Link debug probe connected to an ARM Cortex-M target
- Node.js 18+
For other backends: OpenOCD or arm-none-eabi-gdb as appropriate.
Contributing
Adding a new probe backend:
- Create
src/probe/yourprobe.ts implementing ProbeBackend
- Add a case to
src/probe/factory.ts
- That's it — all 31 MCP tools work automatically
License
MIT - see LICENSE
Built by The Sprk Factory