jlink-mcp
Version:
MCP server for SEGGER J-Link debug probes — LLM-driven embedded debugging with RTT, GDB server, and Trice/Pigweed support
260 lines • 12.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.JLinkBackend = void 0;
const child_process_1 = require("child_process");
const backend_1 = require("./backend");
const logger_1 = require("../utils/logger");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const GDB_SERVER_PROCESS = "jlink-gdb-server";
// Lines that are JLink connection boilerplate
const BOILERPLATE_PATTERNS = [
/^SEGGER J-Link Commander/, /^DLL version/, /^J-Link Commander will now exit/,
/^Connecting to J-Link via USB/, /^Firmware: J-Link/, /^Hardware version:/,
/^J-Link uptime/, /^S\/N:/, /^License\(s\):/, /^USB speed mode:/, /^VTref=/,
/^Device ".*" selected/, /^Connecting to target via SWD/, /^Connecting to target via JTAG/,
/^ConfigTargetSettings\(\)/, /^InitTarget\(\)/, /^Found SW-DP with ID/, /^DPIDR:/,
/^CoreSight/, /^AP map detection/, /^AP\[\d+\]:/, /^CPUID register:/,
/^Feature set:/, /^Cache:/, /^Found Cortex-/, /^FPUnit:/,
/^Security extension: /, /^Secure debug:/, /^ROMTbl\[\d+\]/, /^\[\d+\]\[\d+\]:/,
/^Memory zones:/, /^\s+Zone:/, /^Cortex-M\d+ identified/, /^Type "connect"/,
/^Please specify/, /^Specify target/, /^$/, /^J-Link>/, /^J-Link\[\d+\]:/,
/^Syntax:/, /^Sleep\(\d+\)/, /^Script processing completed/,
];
function stripBoilerplate(raw) {
return raw.split("\n")
.filter((line) => {
const t = line.trim();
return t && !BOILERPLATE_PATTERNS.some((p) => p.test(t));
})
.join("\n").trim();
}
function findJLinkInstallDir() {
const candidates = [
"/opt/SEGGER/JLink", "/usr/local/SEGGER/JLink", "/Applications/SEGGER/JLink",
"C:\\Program Files\\SEGGER\\JLink", "C:\\Program Files (x86)\\SEGGER\\JLink",
];
for (const dir of candidates) {
if (fs.existsSync(dir))
return dir;
}
for (const base of ["/opt/SEGGER", "/Applications/SEGGER", "/usr/local/SEGGER"]) {
if (fs.existsSync(base)) {
try {
const entries = fs.readdirSync(base).filter((e) => e.startsWith("JLink"));
if (entries.length > 0)
return path.join(base, entries.sort().reverse()[0]);
}
catch { /* ignore */ }
}
}
return "";
}
class JLinkBackend extends backend_1.ProbeBackend {
type = "jlink";
displayName = "SEGGER J-Link";
config;
processManager;
gdbOutputBuffer = [];
constructor(config, processManager) {
super();
this.processManager = processManager;
this.config = {
installDir: config.installDir || findJLinkInstallDir(),
device: config.device || "Unspecified",
interface: config.interface || "SWD",
speed: config.speed || 4000,
serialNumber: config.serialNumber,
gdbPort: config.gdbPort || 2331,
rttTelnetPort: config.rttTelnetPort || 19021,
swoTelnetPort: config.swoTelnetPort || 2332,
};
}
get jlinkExe() {
const exe = process.platform === "win32" ? "JLink.exe" : "JLinkExe";
return this.config.installDir ? path.join(this.config.installDir, exe) : exe;
}
get gdbServerExe() {
const exe = process.platform === "win32" ? "JLinkGDBServerCL.exe" : "JLinkGDBServerCLExe";
return this.config.installDir ? path.join(this.config.installDir, exe) : exe;
}
/** Core execution: spawn JLinkExe with commands piped to stdin */
async exec(commands) {
const args = [
"-device", this.config.device,
"-if", this.config.interface,
"-speed", String(this.config.speed),
"-autoconnect", "1",
"-ExitOnError", "1",
"-NoGui", "1",
];
if (this.config.serialNumber) {
args.push("-SelectEmuBySN", this.config.serialNumber);
}
(0, logger_1.log)(`[J-Link] ${commands.join("; ")}`);
return new Promise((resolve) => {
const proc = (0, child_process_1.spawn)(this.jlinkExe, args, { stdio: ["pipe", "pipe", "pipe"] });
let stdout = "", stderr = "";
proc.stdout?.on("data", (d) => { stdout += d.toString(); });
proc.stderr?.on("data", (d) => { stderr += d.toString(); });
proc.stdin?.write(commands.concat(["exit"]).join("\n") + "\n");
proc.stdin?.end();
proc.on("error", (err) => {
(0, logger_1.logError)("J-Link spawn error", err);
resolve({ success: false, rawOutput: stdout, output: stdout, error: `Failed to spawn JLinkExe: ${err.message}` });
});
proc.on("exit", (code) => {
if (code !== 0)
(0, logger_1.logError)(`J-Link exited with code ${code}`);
resolve({ success: code === 0, rawOutput: stdout, output: stripBoilerplate(stdout), error: stderr || undefined });
});
setTimeout(() => {
proc.kill("SIGTERM");
resolve({ success: false, rawOutput: stdout, output: stripBoilerplate(stdout), error: "J-Link timed out after 30s" });
}, 30000);
});
}
// ── ProbeBackend implementation ──────────────────────────────────
async getDeviceInfo() { return this.exec(["halt", "regs"]); }
async halt() { return this.exec(["halt"]); }
async resume() { return this.exec(["go"]); }
async reset(halt = false) { return this.exec(halt ? ["r", "halt"] : ["r", "go"]); }
async step() { return this.exec(["halt", "s"]); }
async readMemory(address, length) {
return this.exec([`mem 0x${address.toString(16)}, ${length}`]);
}
async writeMemory(address, value) {
return this.exec([`w4 0x${address.toString(16)}, 0x${value.toString(16)}`]);
}
async readAllRegisters() { return this.exec(["halt", "regs"]); }
async readRegister(name) { return this.exec(["halt", `rreg ${name}`]); }
async flash(filePath, baseAddress) {
const addr = baseAddress !== undefined ? ` 0x${baseAddress.toString(16)}` : "";
return this.exec(["r", "halt", `loadfile ${filePath}${addr}`, "r", "go"]);
}
async erase() { return this.exec(["erase"]); }
async setBreakpoint(address) {
return this.exec([`SetBP 0x${address.toString(16)}`]);
}
async clearBreakpoints() { return this.exec(["ClrBP"]); }
async executeRaw(commands) { return this.exec(commands); }
// ── GDB Server ───────────────────────────────────────────────────
async startGDBServer() {
if (this.processManager.get(GDB_SERVER_PROCESS)) {
return { success: true, message: "GDB Server is already running" };
}
const args = [
"-device", this.config.device,
"-if", this.config.interface,
"-speed", String(this.config.speed),
"-port", String(this.config.gdbPort),
"-RTTTelnetPort", String(this.config.rttTelnetPort),
"-SWOPort", String(this.config.swoTelnetPort),
"-vd", "-noir", "-LocalhostOnly", "1", "-singlerun", "-NoGui", "1",
];
if (this.config.serialNumber)
args.push("-select", `USB=${this.config.serialNumber}`);
try {
const managed = this.processManager.spawn(GDB_SERVER_PROCESS, this.gdbServerExe, args);
managed.process.stdout?.on("data", (d) => {
for (const line of d.toString().split("\n").filter(Boolean)) {
(0, logger_1.log)(`[GDB Server] ${line}`);
this.gdbOutputBuffer.push(line);
if (this.gdbOutputBuffer.length > 1000)
this.gdbOutputBuffer.shift();
}
});
managed.process.stderr?.on("data", (d) => {
for (const line of d.toString().split("\n").filter(Boolean)) {
(0, logger_1.logError)(`[GDB Server] ${line}`);
this.gdbOutputBuffer.push(`[ERR] ${line}`);
}
});
return { success: true, message: `GDB Server started on port ${this.config.gdbPort}, RTT telnet on port ${this.config.rttTelnetPort}` };
}
catch (err) {
(0, logger_1.logError)("Failed to start GDB Server", err);
return { success: false, message: `Failed to start GDB Server: ${err instanceof Error ? err.message : String(err)}` };
}
}
stopGDBServer() {
const killed = this.processManager.kill(GDB_SERVER_PROCESS);
this.gdbOutputBuffer = [];
return { success: true, message: killed ? "GDB Server stopped" : "GDB Server was not running" };
}
isGDBServerRunning() { return !!this.processManager.get(GDB_SERVER_PROCESS); }
getGDBServerStatus() {
return { running: this.isGDBServerRunning(), gdbPort: this.config.gdbPort, rttTelnetPort: this.config.rttTelnetPort };
}
getGDBServerOutput(lines = 50) { return this.gdbOutputBuffer.slice(-lines); }
// ── Device configuration ─────────────────────────────────────────
isDeviceConfigured() {
return !!this.config.device && this.config.device !== "Unspecified";
}
getDeviceName() { return this.config.device; }
setDevice(device) {
(0, logger_1.log)(`[J-Link] Device set to: ${device}`);
this.config.device = device;
}
async listDevices() {
// Run ShowEmuList without specifying a device to see connected probes
const args = ["-NoGui", "1"];
return new Promise((resolve) => {
const proc = (0, child_process_1.spawn)(this.jlinkExe, args, { stdio: ["pipe", "pipe", "pipe"] });
let stdout = "", stderr = "";
proc.stdout?.on("data", (d) => { stdout += d.toString(); });
proc.stderr?.on("data", (d) => { stderr += d.toString(); });
proc.stdin?.write("ShowEmuList\nexit\n");
proc.stdin?.end();
proc.on("error", (err) => {
resolve({ success: false, rawOutput: stdout, output: stdout, error: `Failed to run JLinkExe: ${err.message}` });
});
proc.on("exit", (code) => {
resolve({ success: code === 0, rawOutput: stdout, output: stripBoilerplate(stdout), error: stderr || undefined });
});
setTimeout(() => { proc.kill("SIGTERM"); resolve({ success: false, rawOutput: stdout, output: stdout, error: "Timed out" }); }, 10000);
});
}
// ── RTT ──────────────────────────────────────────────────────────
supportsRTT() { return true; }
getRTTPort() { return this.config.rttTelnetPort; }
// ── Lifecycle ────────────────────────────────────────────────────
dispose() {
this.processManager.kill(GDB_SERVER_PROCESS);
}
}
exports.JLinkBackend = JLinkBackend;
//# sourceMappingURL=jlink.js.map