jlink-mcp
Version:
MCP server for SEGGER J-Link debug probes — LLM-driven embedded debugging with RTT, GDB server, and Trice/Pigweed support
1,233 lines (1,223 loc) • 39.9 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/utils/config.ts
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 {
}
}
}
return "";
}
function getConfig() {
const cfg = vscode.workspace.getConfiguration("jlinkMcp");
return {
jlink: {
installDir: cfg.get("jlink.installDir") || findJLinkInstallDir(),
device: cfg.get("jlink.device") || "Unspecified",
interface: cfg.get("jlink.interface") || "SWD",
speed: cfg.get("jlink.speed") || 4e3,
serialNumber: cfg.get("jlink.serialNumber") || void 0,
gdbPort: cfg.get("jlink.gdbPort") || 2331,
rttTelnetPort: cfg.get("jlink.rttTelnetPort") || 19021,
swoTelnetPort: cfg.get("jlink.swoTelnetPort") || 2332
},
telnetProxy: {
listenPort: cfg.get("telnetProxy.listenPort") || 19400,
sourcePort: cfg.get("telnetProxy.sourcePort") || 19021,
sourceHost: cfg.get("telnetProxy.sourceHost") || "localhost"
},
trice: {
binaryPath: cfg.get("trice.binaryPath") || "trice",
idListPath: cfg.get("trice.idListPath") || "",
encoding: cfg.get("trice.encoding") || "TREX"
},
pigweed: {
tokenDatabase: cfg.get("pigweed.tokenDatabase") || "",
pythonPath: cfg.get("pigweed.pythonPath") || "python3"
}
};
}
function getJLinkExePath(config) {
const exe = process.platform === "win32" ? "JLink.exe" : "JLinkExe";
return config.installDir ? path.join(config.installDir, exe) : exe;
}
function getJLinkGDBServerPath(config) {
const exe = process.platform === "win32" ? "JLinkGDBServerCL.exe" : "JLinkGDBServerCLExe";
return config.installDir ? path.join(config.installDir, exe) : exe;
}
var vscode, path, fs;
var init_config = __esm({
"src/utils/config.ts"() {
"use strict";
vscode = __toESM(require("vscode"));
path = __toESM(require("path"));
fs = __toESM(require("fs"));
}
});
// src/utils/logger.ts
function initLogger(channel) {
outputChannel = channel;
}
function log(message) {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const line = `[${timestamp}] ${message}`;
outputChannel?.appendLine(line);
}
function logError(message, error) {
const errMsg = error instanceof Error ? error.message : String(error ?? "");
log(`ERROR: ${message}${errMsg ? ` - ${errMsg}` : ""}`);
}
var outputChannel;
var init_logger = __esm({
"src/utils/logger.ts"() {
"use strict";
}
});
// src/jlink/commander.ts
var commander_exports = {};
__export(commander_exports, {
clearBreakpoints: () => clearBreakpoints,
decodeFaultRegisters: () => decodeFaultRegisters,
eraseChip: () => eraseChip,
executeJLinkCommands: () => executeJLinkCommands,
executeRawCommands: () => executeRawCommands,
flashFirmware: () => flashFirmware,
formatRegistersCompact: () => formatRegistersCompact,
getDeviceInfo: () => getDeviceInfo,
haltDevice: () => haltDevice,
parseMemoryDump: () => parseMemoryDump,
parseRegisters: () => parseRegisters,
readAllRegisters: () => readAllRegisters,
readFaultRegisters: () => readFaultRegisters,
readMemory: () => readMemory,
readRegister: () => readRegister,
resetDevice: () => resetDevice,
resumeDevice: () => resumeDevice,
setBreakpoint: () => setBreakpoint,
stepInstruction: () => stepInstruction,
stripBoilerplate: () => stripBoilerplate,
writeMemory: () => writeMemory
});
function stripBoilerplate(raw) {
const lines = raw.split("\n");
const meaningful = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
const isBoilerplate = BOILERPLATE_PATTERNS.some((p) => p.test(trimmed));
if (!isBoilerplate) {
meaningful.push(line);
}
}
return meaningful.join("\n").trim();
}
function parseRegisters(raw) {
const regs = {};
const regPatterns = [
// "PC = 0000BF54, CycleCnt = 0000855D"
/(\w+)\s*=\s*([0-9A-Fa-f]{2,8})(?:,|\s|$)/g,
// "SP(R13)= 20062880"
/(\w+)\((\w+)\)\s*=\s*([0-9A-Fa-f]{2,8})/g
];
for (const line of raw.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("SEGGER") || trimmed.startsWith("Connecting")) continue;
let match;
const simple = /(\w[\w()]*)\s*=\s*([0-9A-Fa-f]{2,8})/g;
while ((match = simple.exec(trimmed)) !== null) {
let name = match[1];
const value = match[2];
const parenMatch = name.match(/^(\w+)\(\w+\)$/);
if (parenMatch) name = parenMatch[1];
regs[name] = `0x${value}`;
}
const xpsrMatch = trimmed.match(/APSR\s*=\s*(\w+)/);
if (xpsrMatch) regs["APSR"] = xpsrMatch[1];
}
return Object.keys(regs).length > 0 ? regs : null;
}
function formatRegistersCompact(regs) {
const core = ["PC", "SP", "LR", "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R11", "R12"];
const status = ["XPSR", "CONTROL", "PRIMASK", "BASEPRI", "FAULTMASK"];
const stack = ["MSP", "PSP", "MSPLIM", "PSPLIM"];
const lines = [];
const coreVals = core.filter((r) => regs[r]).map((r) => `${r}=${regs[r]}`);
lines.push("Core: " + coreVals.join(" "));
const statusVals = status.filter((r) => regs[r]).map((r) => `${r}=${regs[r]}`);
if (statusVals.length > 0) lines.push("Status: " + statusVals.join(" "));
const stackVals = stack.filter((r) => regs[r]).map((r) => `${r}=${regs[r]}`);
if (stackVals.length > 0) lines.push("Stack: " + stackVals.join(" "));
const fpNonZero = Object.entries(regs).filter(([k, v]) => k.startsWith("FPS") && v !== "0x00000000").map(([k, v]) => `${k}=${v}`);
if (fpNonZero.length > 0) lines.push("FP (non-zero): " + fpNonZero.join(" "));
return lines.join("\n");
}
function parseMemoryDump(raw) {
const results = [];
for (const line of raw.split("\n")) {
const match = line.match(/^([0-9A-Fa-f]{8})\s*=\s*(.+?)\s{2,}(.*)$/);
if (match) {
results.push({
address: `0x${match[1]}`,
hex: match[2].trim(),
ascii: match[3].trim()
});
}
}
return results;
}
function decodeFaultRegisters(cfsr, hfsr, mmfar, bfar) {
const lines = [];
const mmfsr = cfsr & 255;
const bfsr = cfsr >> 8 & 255;
const ufsr = cfsr >> 16 & 65535;
if (cfsr === 0 && hfsr === 0) {
lines.push("No faults detected (CFSR=0, HFSR=0)");
return lines.join("\n");
}
if (mmfsr) {
lines.push("## MemManage Fault (MMFSR):");
if (mmfsr & 1) lines.push(" - IACCVIOL: Instruction access violation");
if (mmfsr & 2) lines.push(" - DACCVIOL: Data access violation");
if (mmfsr & 8) lines.push(" - MUNSTKERR: MemManage on unstacking (exception return)");
if (mmfsr & 16) lines.push(" - MSTKERR: MemManage on stacking (exception entry)");
if (mmfsr & 32) lines.push(" - MLSPERR: MemManage during FP lazy state preservation");
if (mmfsr & 128) {
lines.push(` - MMARVALID: Faulting address = 0x${mmfar.toString(16).padStart(8, "0")}`);
}
}
if (bfsr) {
lines.push("## BusFault (BFSR):");
if (bfsr & 1) lines.push(" - IBUSERR: Instruction bus error");
if (bfsr & 2) lines.push(" - PRECISERR: Precise data bus error");
if (bfsr & 4) lines.push(" - IMPRECISERR: Imprecise data bus error");
if (bfsr & 8) lines.push(" - UNSTKERR: BusFault on unstacking");
if (bfsr & 16) lines.push(" - STKERR: BusFault on stacking");
if (bfsr & 32) lines.push(" - LSPERR: BusFault during FP lazy state preservation");
if (bfsr & 128) {
lines.push(` - BFARVALID: Faulting address = 0x${bfar.toString(16).padStart(8, "0")}`);
}
}
if (ufsr) {
lines.push("## UsageFault (UFSR):");
if (ufsr & 1) lines.push(" - UNDEFINSTR: Undefined instruction");
if (ufsr & 2) lines.push(" - INVSTATE: Invalid state (e.g., Thumb bit)");
if (ufsr & 4) lines.push(" - INVPC: Invalid PC load (bad EXC_RETURN)");
if (ufsr & 8) lines.push(" - NOCP: No coprocessor");
if (ufsr & 16) lines.push(" - STKOF: Stack overflow detected");
if (ufsr & 256) lines.push(" - UNALIGNED: Unaligned memory access");
if (ufsr & 512) lines.push(" - DIVBYZERO: Division by zero");
}
if (hfsr) {
lines.push("## HardFault (HFSR):");
if (hfsr & 2) lines.push(" - VECTTBL: Vector table read fault on exception processing");
if (hfsr & 1073741824) lines.push(" - FORCED: Forced HardFault (escalated from configurable fault)");
if (hfsr & 2147483648) lines.push(" - DEBUGEVT: Debug event triggered HardFault");
}
return lines.join("\n");
}
async function executeJLinkCommands(commands2, deviceOverride) {
const config = getConfig();
const jlinkExe = getJLinkExePath(config.jlink);
const device = deviceOverride || config.jlink.device;
const scriptLines = [...commands2, "exit"];
const args = [
"-device",
device,
"-if",
config.jlink.interface,
"-speed",
String(config.jlink.speed),
"-autoconnect",
"1",
"-ExitOnError",
"1",
"-NoGui",
"1"
];
if (config.jlink.serialNumber) {
args.push("-SelectEmuBySN", config.jlink.serialNumber);
}
log(`JLink Commander: ${commands2.join("; ")}`);
return new Promise((resolve) => {
const proc = (0, import_child_process2.spawn)(jlinkExe, args, {
stdio: ["pipe", "pipe", "pipe"]
});
let stdout = "";
let stderr = "";
proc.stdout?.on("data", (data) => {
stdout += data.toString();
});
proc.stderr?.on("data", (data) => {
stderr += data.toString();
});
const script = scriptLines.join("\n") + "\n";
proc.stdin?.write(script);
proc.stdin?.end();
proc.on("error", (err) => {
logError("JLink Commander spawn error", err);
resolve({
success: false,
rawOutput: stdout,
output: stdout,
error: `Failed to spawn JLinkExe: ${err.message}. Is J-Link installed at ${config.jlink.installDir}?`
});
});
proc.on("exit", (code) => {
const success = code === 0;
if (!success) {
logError(`JLink Commander exited with code ${code}`);
}
resolve({
success,
rawOutput: stdout,
output: stripBoilerplate(stdout),
error: stderr || void 0
});
});
setTimeout(() => {
proc.kill("SIGTERM");
resolve({
success: false,
rawOutput: stdout,
output: stripBoilerplate(stdout),
error: "JLink Commander timed out after 30 seconds"
});
}, 3e4);
});
}
async function getDeviceInfo() {
return executeJLinkCommands(["halt", "regs"]);
}
async function haltDevice() {
return executeJLinkCommands(["halt"]);
}
async function resumeDevice() {
return executeJLinkCommands(["go"]);
}
async function resetDevice(halt = false) {
return executeJLinkCommands(halt ? ["r", "halt"] : ["r", "go"]);
}
async function readMemory(address, numBytes) {
const addrHex = `0x${address.toString(16)}`;
return executeJLinkCommands([`mem ${addrHex}, ${numBytes}`]);
}
async function writeMemory(address, value) {
const addrHex = `0x${address.toString(16)}`;
const valHex = `0x${value.toString(16)}`;
return executeJLinkCommands([`w4 ${addrHex}, ${valHex}`]);
}
async function readRegister(register) {
return executeJLinkCommands(["halt", `rreg ${register}`]);
}
async function readAllRegisters() {
return executeJLinkCommands(["halt", "regs"]);
}
async function flashFirmware(filePath, baseAddress) {
const addr = baseAddress !== void 0 ? ` 0x${baseAddress.toString(16)}` : "";
return executeJLinkCommands([
"r",
"halt",
`loadfile ${filePath}${addr}`,
"r",
"go"
]);
}
async function eraseChip() {
return executeJLinkCommands(["erase"]);
}
async function setBreakpoint(address) {
return executeJLinkCommands([`SetBP 0x${address.toString(16)}`]);
}
async function clearBreakpoints() {
return executeJLinkCommands(["ClrBP"]);
}
async function stepInstruction() {
return executeJLinkCommands(["halt", "s"]);
}
async function executeRawCommands(rawCommands) {
return executeJLinkCommands(rawCommands);
}
async function readFaultRegisters() {
const result = await executeJLinkCommands(["halt", "mem 0xE000ED28, 20"]);
const dump = parseMemoryDump(result.rawOutput);
let cfsr = 0, hfsr = 0, mmfar = 0, bfar = 0;
if (dump.length > 0) {
const allHex = dump.map((d) => d.hex).join(" ");
const bytes = allHex.split(/\s+/).filter(Boolean);
if (bytes.length >= 16) {
cfsr = parseLittleEndian32(bytes, 0);
hfsr = parseLittleEndian32(bytes, 4);
mmfar = parseLittleEndian32(bytes, 12);
bfar = parseLittleEndian32(bytes, 16);
}
}
return {
result,
decoded: decodeFaultRegisters(cfsr, hfsr, mmfar, bfar),
raw: { cfsr, hfsr, mmfar, bfar }
};
}
function parseLittleEndian32(bytes, offset) {
if (offset + 3 >= bytes.length) return 0;
return (parseInt(bytes[offset], 16) | parseInt(bytes[offset + 1], 16) << 8 | parseInt(bytes[offset + 2], 16) << 16 | parseInt(bytes[offset + 3], 16) << 24) >>> 0;
}
var import_child_process2, BOILERPLATE_PATTERNS;
var init_commander = __esm({
"src/jlink/commander.ts"() {
"use strict";
import_child_process2 = require("child_process");
init_config();
init_logger();
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/,
/^$/,
// blank lines
/^J-Link>/,
// prompt
/^J-Link\[\d+\]:/,
/^Syntax:/,
/^Sleep\(\d+\)/,
/^Script processing completed/
];
}
});
// src/extension.ts
var extension_exports = {};
__export(extension_exports, {
activate: () => activate,
deactivate: () => deactivate
});
module.exports = __toCommonJS(extension_exports);
var vscode2 = __toESM(require("vscode"));
// src/jlink/gdb-server.ts
init_config();
init_logger();
var GDB_SERVER_PROCESS_NAME = "jlink-gdb-server";
var GDBServerManager = class {
processManager;
outputBuffer = [];
maxOutputLines = 1e3;
constructor(processManager2) {
this.processManager = processManager2;
}
/** Start JLinkGDBServer */
start() {
const existing = this.processManager.get(GDB_SERVER_PROCESS_NAME);
if (existing) {
return { success: true, message: "GDB Server is already running" };
}
const config = getConfig();
const gdbServerPath = getJLinkGDBServerPath(config.jlink);
const args = [
"-device",
config.jlink.device,
"-if",
config.jlink.interface,
"-speed",
String(config.jlink.speed),
"-port",
String(config.jlink.gdbPort),
"-RTTTelnetPort",
String(config.jlink.rttTelnetPort),
"-SWOPort",
String(config.jlink.swoTelnetPort),
"-vd",
"-noir",
"-LocalhostOnly",
"1",
"-singlerun"
];
if (config.jlink.serialNumber) {
args.push("-select", `USB=${config.jlink.serialNumber}`);
}
try {
const managed = this.processManager.spawn(
GDB_SERVER_PROCESS_NAME,
gdbServerPath,
args
);
managed.process.stdout?.on("data", (data) => {
const lines = data.toString().split("\n").filter(Boolean);
for (const line of lines) {
log(`[GDB Server] ${line}`);
this.outputBuffer.push(line);
if (this.outputBuffer.length > this.maxOutputLines) {
this.outputBuffer.shift();
}
}
});
managed.process.stderr?.on("data", (data) => {
const lines = data.toString().split("\n").filter(Boolean);
for (const line of lines) {
logError(`[GDB Server] ${line}`);
this.outputBuffer.push(`[ERR] ${line}`);
}
});
return {
success: true,
message: `GDB Server started on port ${config.jlink.gdbPort}, RTT telnet on port ${config.jlink.rttTelnetPort}`
};
} catch (err) {
logError("Failed to start GDB Server", err);
return {
success: false,
message: `Failed to start GDB Server: ${err instanceof Error ? err.message : String(err)}`
};
}
}
/** Stop the GDB Server */
stop() {
const killed = this.processManager.kill(GDB_SERVER_PROCESS_NAME);
this.outputBuffer = [];
return {
success: true,
message: killed ? "GDB Server stopped" : "GDB Server was not running"
};
}
/** Check if running */
isRunning() {
return !!this.processManager.get(GDB_SERVER_PROCESS_NAME);
}
/** Get recent output */
getRecentOutput(lines = 50) {
return this.outputBuffer.slice(-lines);
}
/** Get status info */
getStatus() {
const config = getConfig();
return {
running: this.isRunning(),
gdbPort: config.jlink.gdbPort,
rttTelnetPort: config.jlink.rttTelnetPort,
swoTelnetPort: config.jlink.swoTelnetPort
};
}
};
// src/rtt/rtt-client.ts
var net = __toESM(require("net"));
var import_events = require("events");
init_logger();
function stripAnsi(text) {
return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\[0m/g, "");
}
function isRttHeader(line) {
return line.startsWith("SEGGER J-Link") || line.startsWith("Process: JLink") || line.trim() === "";
}
function parseZephyrLog(line) {
const match = line.match(
/^\[(\d{2}:\d{2}:\d{2}\.\d{3},?\d{0,3})\]\s*<(\w+)>\s*(\w[\w._-]*):\s*(.*)$/
);
if (match) {
return {
deviceTime: match[1],
level: match[2],
module: match[3],
message: match[4],
raw: line
};
}
return { deviceTime: null, level: null, module: null, message: line, raw: line };
}
var RTTClient = class extends import_events.EventEmitter {
socket = null;
host;
port;
messages = [];
/** Flat buffer of all parsed log lines for searching */
allLines = [];
maxMessages = 5e3;
maxLines = 2e4;
reconnectTimer = null;
connected = false;
lineBuffer = "";
constructor(host = "localhost", port = 19021) {
super();
this.host = host;
this.port = port;
}
/** Connect to RTT telnet port */
connect() {
return new Promise((resolve, reject) => {
if (this.connected) {
resolve();
return;
}
this.socket = new net.Socket();
this.socket.on("connect", () => {
log(`RTT Client connected to ${this.host}:${this.port}`);
this.connected = true;
this.emit("connected");
resolve();
});
this.socket.on("data", (data) => {
const raw = data.toString();
const cleaned = stripAnsi(raw);
this.lineBuffer += cleaned;
const parts = this.lineBuffer.split("\n");
this.lineBuffer = parts.pop() || "";
const parsedLines = [];
for (const part of parts) {
const trimmed = part.trim();
if (!trimmed || isRttHeader(trimmed)) continue;
parsedLines.push(parseZephyrLog(trimmed));
}
if (parsedLines.length > 0) {
const msg = {
channel: 0,
timestamp: /* @__PURE__ */ new Date(),
rawData: raw,
lines: parsedLines
};
this.messages.push(msg);
if (this.messages.length > this.maxMessages) {
this.messages.shift();
}
for (const line of parsedLines) {
this.allLines.push(line);
}
while (this.allLines.length > this.maxLines) {
this.allLines.shift();
}
this.emit("data", msg);
}
});
this.socket.on("close", () => {
log("RTT Client disconnected");
this.connected = false;
this.emit("disconnected");
});
this.socket.on("error", (err) => {
logError("RTT Client error", err);
this.connected = false;
reject(err);
});
this.socket.connect(this.port, this.host);
});
}
/** Disconnect */
disconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.socket) {
this.socket.destroy();
this.socket = null;
}
this.connected = false;
this.lineBuffer = "";
}
/** Send data to RTT down-channel (host → device) */
send(data) {
if (!this.socket || !this.connected) return false;
this.socket.write(data);
return true;
}
/** Get recent log lines as formatted text */
getLines(count = 50) {
const lines = this.allLines.slice(-count);
return lines.map((l) => {
if (l.deviceTime && l.level && l.module) {
return `[${l.deviceTime}] <${l.level}> ${l.module}: ${l.message}`;
}
return l.message;
});
}
/** Search/filter log lines */
search(opts) {
let results = [...this.allLines];
if (opts.level) {
const lvl = opts.level.toLowerCase();
results = results.filter((l) => l.level?.toLowerCase() === lvl);
}
if (opts.module) {
const mod = opts.module.toLowerCase();
results = results.filter((l) => l.module?.toLowerCase().includes(mod));
}
if (opts.pattern) {
try {
const re = new RegExp(opts.pattern, "i");
results = results.filter((l) => re.test(l.message) || re.test(l.raw));
} catch {
const pat = opts.pattern.toLowerCase();
results = results.filter(
(l) => l.message.toLowerCase().includes(pat) || l.raw.toLowerCase().includes(pat)
);
}
}
if (opts.count) {
results = results.slice(-opts.count);
}
return results;
}
/** Get messages (for backward compat / resource access) */
getMessages(count) {
if (count) return this.messages.slice(-count);
return [...this.messages];
}
/** Clear all buffers */
clearBuffer() {
this.messages = [];
this.allLines = [];
this.lineBuffer = "";
}
/** Check connection status */
isConnected() {
return this.connected;
}
/** Get buffer stats */
getStats() {
return {
connected: this.connected,
lineCount: this.allLines.length,
messageCount: this.messages.length,
host: this.host,
port: this.port
};
}
};
// src/telnet/telnet-proxy.ts
var net2 = __toESM(require("net"));
var import_events2 = require("events");
init_logger();
var TelnetProxy = class extends import_events2.EventEmitter {
server = null;
sourceSocket = null;
clients = /* @__PURE__ */ new Set();
listenPort;
sourceHost;
sourcePort;
running = false;
dataBuffer = [];
maxBufferLines = 2e3;
constructor(listenPort = 19400, sourceHost = "localhost", sourcePort = 19021) {
super();
this.listenPort = listenPort;
this.sourceHost = sourceHost;
this.sourcePort = sourcePort;
}
/** Start the proxy: connect to source and listen for clients */
async start() {
if (this.running) {
return { success: true, message: `Telnet proxy already running on port ${this.listenPort}` };
}
try {
await this.connectToSource();
await this.startServer();
this.running = true;
return {
success: true,
message: `Telnet proxy listening on port ${this.listenPort}, connected to ${this.sourceHost}:${this.sourcePort}`
};
} catch (err) {
logError("Failed to start telnet proxy", err);
return {
success: false,
message: `Failed to start telnet proxy: ${err instanceof Error ? err.message : String(err)}`
};
}
}
connectToSource() {
return new Promise((resolve, reject) => {
this.sourceSocket = new net2.Socket();
this.sourceSocket.on("connect", () => {
log(`Telnet proxy connected to source ${this.sourceHost}:${this.sourcePort}`);
resolve();
});
this.sourceSocket.on("data", (data) => {
const text = data.toString();
const lines = text.split("\n");
for (const line of lines) {
if (line.trim()) {
this.dataBuffer.push(line);
if (this.dataBuffer.length > this.maxBufferLines) {
this.dataBuffer.shift();
}
}
}
for (const client of this.clients) {
try {
client.write(data);
} catch {
this.clients.delete(client);
}
}
this.emit("data", text);
});
this.sourceSocket.on("close", () => {
log("Telnet proxy: source connection closed");
this.emit("sourceDisconnected");
});
this.sourceSocket.on("error", (err) => {
logError("Telnet proxy source error", err);
reject(err);
});
this.sourceSocket.connect(this.sourcePort, this.sourceHost);
});
}
startServer() {
return new Promise((resolve, reject) => {
this.server = net2.createServer((client) => {
log(`Telnet proxy: client connected from ${client.remoteAddress}:${client.remotePort}`);
this.clients.add(client);
client.on("close", () => {
this.clients.delete(client);
log("Telnet proxy: client disconnected");
});
client.on("error", () => {
this.clients.delete(client);
});
client.on("data", (data) => {
if (this.sourceSocket && !this.sourceSocket.destroyed) {
this.sourceSocket.write(data);
}
});
});
this.server.on("error", (err) => {
logError("Telnet proxy server error", err);
reject(err);
});
this.server.listen(this.listenPort, "127.0.0.1", () => {
log(`Telnet proxy server listening on port ${this.listenPort}`);
resolve();
});
});
}
/** Stop the proxy */
stop() {
for (const client of this.clients) {
client.destroy();
}
this.clients.clear();
if (this.sourceSocket) {
this.sourceSocket.destroy();
this.sourceSocket = null;
}
if (this.server) {
this.server.close();
this.server = null;
}
this.running = false;
this.dataBuffer = [];
log("Telnet proxy stopped");
}
/** Get buffered data */
getBuffer(lines) {
if (lines) return this.dataBuffer.slice(-lines);
return [...this.dataBuffer];
}
/** Clear buffer */
clearBuffer() {
this.dataBuffer = [];
}
/** Write data to source (device) */
writeToSource(data) {
if (this.sourceSocket && !this.sourceSocket.destroyed) {
this.sourceSocket.write(data);
return true;
}
return false;
}
/** Get proxy status */
getStatus() {
return {
running: this.running,
listenPort: this.listenPort,
sourceConnected: !!this.sourceSocket && !this.sourceSocket.destroyed,
clientCount: this.clients.size,
bufferedLines: this.dataBuffer.length
};
}
};
// src/utils/process-manager.ts
var import_child_process = require("child_process");
init_logger();
var import_events3 = require("events");
var ProcessManager = class extends import_events3.EventEmitter {
processes = /* @__PURE__ */ new Map();
spawn(name, command, args, options) {
this.kill(name);
log(`Spawning process "${name}": ${command} ${args.join(" ")}`);
const proc = (0, import_child_process.spawn)(command, args, {
stdio: ["pipe", "pipe", "pipe"],
...options
});
const managed = {
process: proc,
name,
kill: () => this.kill(name)
};
proc.on("error", (err) => {
logError(`Process "${name}" error`, err);
this.processes.delete(name);
this.emit("processExit", name, null, err);
});
proc.on("exit", (code, signal) => {
log(`Process "${name}" exited (code=${code}, signal=${signal})`);
this.processes.delete(name);
this.emit("processExit", name, code, signal);
});
this.processes.set(name, managed);
return managed;
}
kill(name) {
const existing = this.processes.get(name);
if (existing) {
log(`Killing process "${name}" (pid=${existing.process.pid})`);
existing.process.kill("SIGTERM");
setTimeout(() => {
try {
existing.process.kill("SIGKILL");
} catch {
}
}, 3e3);
this.processes.delete(name);
return true;
}
return false;
}
get(name) {
return this.processes.get(name);
}
killAll() {
for (const [name] of this.processes) {
this.kill(name);
}
}
listRunning() {
return Array.from(this.processes.keys());
}
};
// src/extension.ts
init_logger();
init_config();
var mcpServer;
var processManager;
var gdbServer;
var rttClient;
var telnetProxy;
var outputChannel2;
var rttOutputChannel;
var statusBarItem;
function activate(context) {
outputChannel2 = vscode2.window.createOutputChannel("J-Link MCP");
rttOutputChannel = vscode2.window.createOutputChannel("J-Link RTT");
initLogger(outputChannel2);
log("J-Link MCP Extension activating...");
const mcpDidChange = new vscode2.EventEmitter();
const mcpProvider = vscode2.lm.registerMcpServerDefinitionProvider(
"jlinkMcp.mcpServer",
{
onDidChangeMcpServerDefinitions: mcpDidChange.event,
provideMcpServerDefinitions(_token) {
const cfg = vscode2.workspace.getConfiguration("jlinkMcp");
const serverScript = vscode2.Uri.joinPath(
context.extensionUri,
"out",
"mcp",
"standalone.js"
).fsPath;
const env = {};
const device = cfg.get("jlink.device");
if (device && device !== "Unspecified") env["JLINK_DEVICE"] = device;
const installDir = cfg.get("jlink.installDir");
if (installDir) env["JLINK_INSTALL_DIR"] = installDir;
const iface = cfg.get("jlink.interface");
if (iface) env["JLINK_INTERFACE"] = iface;
const speed = cfg.get("jlink.speed");
if (speed) env["JLINK_SPEED"] = speed;
const serial = cfg.get("jlink.serialNumber");
if (serial) env["JLINK_SERIAL"] = serial;
const gdbPort = cfg.get("jlink.gdbPort");
if (gdbPort) env["JLINK_GDB_PORT"] = gdbPort;
const rttPort = cfg.get("jlink.rttTelnetPort");
if (rttPort) env["JLINK_RTT_PORT"] = rttPort;
return [
new vscode2.McpStdioServerDefinition(
"J-Link Debug Probe",
process.execPath,
// Use VSCode's bundled Node.js
[serverScript],
env,
context.extension.packageJSON.version
)
];
},
resolveMcpServerDefinition(server, _token) {
return server;
}
}
);
context.subscriptions.push(mcpProvider, mcpDidChange);
context.subscriptions.push(
vscode2.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("jlinkMcp")) {
log("J-Link MCP settings changed, notifying VSCode MCP client");
mcpDidChange.fire();
}
})
);
log("MCP server definition provider registered");
processManager = new ProcessManager();
const config = getConfig();
gdbServer = new GDBServerManager(processManager);
rttClient = new RTTClient("localhost", config.jlink.rttTelnetPort);
telnetProxy = new TelnetProxy(
config.telnetProxy.listenPort,
config.telnetProxy.sourceHost,
config.telnetProxy.sourcePort
);
rttClient.on("data", (msg) => {
for (const line of msg.lines) {
if (line.deviceTime && line.level && line.module) {
rttOutputChannel?.appendLine(`[${line.deviceTime}] <${line.level}> ${line.module}: ${line.message}`);
} else {
rttOutputChannel?.appendLine(line.message);
}
}
});
statusBarItem = vscode2.window.createStatusBarItem(vscode2.StatusBarAlignment.Left, 100);
statusBarItem.text = "$(debug-disconnect) J-Link";
statusBarItem.tooltip = "J-Link MCP - Click for status";
statusBarItem.command = "jlinkMcp.showStatus";
statusBarItem.show();
context.subscriptions.push(statusBarItem);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.showStatus", async () => {
const gdbStatus = gdbServer.getStatus();
const rttStats = rttClient.getStats();
const proxyStatus = telnetProxy.getStatus();
const configInfo = getConfig();
const statusText = [
"# J-Link MCP Status",
"",
`**Device:** ${configInfo.jlink.device}`,
`**Interface:** ${configInfo.jlink.interface} @ ${configInfo.jlink.speed} kHz`,
`**J-Link Install Dir:** ${configInfo.jlink.installDir || "(auto-detect)"}`,
"",
"## GDB Server",
`- Running: ${gdbStatus.running ? "Yes" : "No"}`,
`- GDB Port: ${gdbStatus.gdbPort}`,
`- RTT Telnet Port: ${gdbStatus.rttTelnetPort}`,
"",
"## RTT",
`- Connected: ${rttStats.connected ? "Yes" : "No"}`,
`- Messages buffered: ${rttStats.messageCount}`,
"",
"## Telnet Proxy",
`- Running: ${proxyStatus.running ? "Yes" : "No"}`,
`- Listen Port: ${proxyStatus.listenPort}`,
`- Clients Connected: ${proxyStatus.clientCount}`,
`- Buffered Lines: ${proxyStatus.bufferedLines}`
].join("\n");
const doc = await vscode2.workspace.openTextDocument({
content: statusText,
language: "markdown"
});
await vscode2.window.showTextDocument(doc, { preview: true });
})
);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.startGdbServer", () => {
const result = gdbServer.start();
if (result.success) {
vscode2.window.showInformationMessage(result.message);
updateStatusBar(true);
} else {
vscode2.window.showErrorMessage(result.message);
}
})
);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.stopGdbServer", () => {
const result = gdbServer.stop();
vscode2.window.showInformationMessage(result.message);
updateStatusBar(false);
})
);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.connectRtt", async () => {
try {
await rttClient.connect();
vscode2.window.showInformationMessage("Connected to RTT");
rttOutputChannel.show();
} catch (err) {
vscode2.window.showErrorMessage(
`Failed to connect to RTT: ${err instanceof Error ? err.message : String(err)}`
);
}
})
);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.disconnectRtt", () => {
rttClient.disconnect();
vscode2.window.showInformationMessage("Disconnected from RTT");
})
);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.startTelnetProxy", async () => {
const result = await telnetProxy.start();
if (result.success) {
vscode2.window.showInformationMessage(result.message);
} else {
vscode2.window.showErrorMessage(result.message);
}
})
);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.stopTelnetProxy", () => {
telnetProxy.stop();
vscode2.window.showInformationMessage("Telnet proxy stopped");
})
);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.flashFirmware", async () => {
const uri = await vscode2.window.showOpenDialog({
canSelectMany: false,
filters: {
"Firmware Files": ["hex", "bin", "elf"],
"All Files": ["*"]
},
title: "Select firmware file to flash"
});
if (!uri || uri.length === 0) return;
const filePath = uri[0].fsPath;
const { flashFirmware: flashFirmware2 } = await Promise.resolve().then(() => (init_commander(), commander_exports));
vscode2.window.withProgress(
{ location: vscode2.ProgressLocation.Notification, title: "Flashing firmware..." },
async () => {
const result = await flashFirmware2(filePath);
if (result.success) {
vscode2.window.showInformationMessage(`Firmware flashed successfully: ${filePath}`);
} else {
vscode2.window.showErrorMessage(`Flash failed: ${result.error || result.output}`);
}
}
);
})
);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.showOutput", () => {
outputChannel2.show();
})
);
context.subscriptions.push(
vscode2.commands.registerCommand("jlinkMcp.showRttOutput", () => {
rttOutputChannel.show();
})
);
context.subscriptions.push({
dispose() {
rttClient?.disconnect();
telnetProxy?.stop();
processManager?.killAll();
mcpServer?.dispose();
}
});
log("J-Link MCP Extension activated");
outputChannel2.show(true);
}
function updateStatusBar(gdbRunning) {
if (!statusBarItem) return;
if (gdbRunning) {
statusBarItem.text = "$(debug) J-Link Connected";
statusBarItem.backgroundColor = void 0;
} else {
statusBarItem.text = "$(debug-disconnect) J-Link";
statusBarItem.backgroundColor = void 0;
}
}
function deactivate() {
log("J-Link MCP Extension deactivating...");
rttClient?.disconnect();
telnetProxy?.stop();
processManager?.killAll();
mcpServer?.dispose();
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
activate,
deactivate
});
//# sourceMappingURL=extension.js.map