UNPKG

jlink-mcp

Version:

MCP server for SEGGER J-Link debug probes — LLM-driven embedded debugging with RTT, GDB server, and Trice/Pigweed support

485 lines (481 loc) 32.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JLinkMcpServer = void 0; const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const zod_1 = require("zod"); const factory_1 = require("../probe/factory"); const rtt_client_1 = require("../rtt/rtt-client"); const telnet_proxy_1 = require("../telnet/telnet-proxy"); const process_manager_1 = require("../utils/process-manager"); const logger_1 = require("../utils/logger"); class JLinkMcpServer { server; processManager; probe; rttClient; telnetProxy; constructor(probeConfig, rttPort, telnetConfig) { this.processManager = new process_manager_1.ProcessManager(); this.probe = (0, factory_1.createProbeBackend)(probeConfig || { type: "jlink" }, this.processManager); const effectiveRttPort = rttPort ?? this.probe.getRTTPort(); this.rttClient = new rtt_client_1.RTTClient("localhost", effectiveRttPort > 0 ? effectiveRttPort : 19021); this.telnetProxy = new telnet_proxy_1.TelnetProxy(telnetConfig?.listenPort ?? 19400, telnetConfig?.sourceHost ?? "localhost", telnetConfig?.sourcePort ?? (effectiveRttPort > 0 ? effectiveRttPort : 19021)); this.server = new mcp_js_1.McpServer({ name: "jlink-mcp", version: "0.1.3", }); this.registerTools(); this.registerResources(); this.registerPrompts(); } /** * Returns an MCP error response if device is not configured, or null if OK. * Call at the top of any tool handler that talks to hardware. */ requireDevice() { if (!this.probe.isDeviceConfigured()) { return { content: [{ type: "text", text: `ERROR: No target device configured for ${this.probe.displayName}.\n\nBefore using debugging tools, you must set the target device. Please:\n1. Call list_devices to scan for connected probes\n2. Call set_device with the correct device name (e.g., "nRF52840_XXAA", "STM32F407VG")\n\nCommon device names: nRF52840_XXAA, nRF5340_xxAA_APP, STM32F407VG, STM32L476RG, STM32H743ZI, RP2040_M0_0`, }], }; } return null; } registerTools() { const probe = this.probe; // ═══════════════════════════════════════════════════════════════ // DEVICE CONFIGURATION (always available, even without device set) // ═══════════════════════════════════════════════════════════════ this.server.tool("list_devices", "Scan for connected debug probes and show what hardware is attached. Use this first if you don't know what device is connected.", {}, async () => { const result = await probe.listDevices(); const lines = [ `Probe: ${probe.displayName}`, `Currently configured device: ${probe.getDeviceName()}`, `Device configured: ${probe.isDeviceConfigured() ? "Yes" : "NO - use set_device to configure"}`, "", "--- Scan Results ---", result.output || result.rawOutput || "(no output)", ]; return { content: [{ type: "text", text: lines.join("\n") }] }; }); this.server.tool("set_device", "Set the target device name at runtime. Required before any debugging commands will work. Examples: 'nRF52840_XXAA', 'nRF5340_xxAA_APP', 'STM32F407VG', 'STM32L476RG'.", { device: zod_1.z.string().describe("Target device name (e.g., 'nRF52840_XXAA', 'STM32F407VG')"), }, async ({ device }) => { probe.setDevice(device); return { content: [{ type: "text", text: `Device set to "${device}". You can now use all debugging tools.` }] }; }); // ═══════════════════════════════════════════════════════════════ // COMPOSITE / WORKFLOW TOOLS // ═══════════════════════════════════════════════════════════════ this.server.tool("start_debug_session", `One-call setup: starts GDB server via ${probe.displayName}, connects RTT (if supported), waits for initial output. This is the recommended first tool to call. If no device is configured, use list_devices and set_device first.`, {}, async () => { const guard = this.requireDevice(); if (guard) return guard; const steps = []; if (!probe.isGDBServerRunning()) { const gdbResult = await probe.startGDBServer(); steps.push(gdbResult.success ? `GDB Server: started (${probe.displayName})` : `GDB Server: ${gdbResult.message}`); if (!gdbResult.success) return { content: [{ type: "text", text: steps.join("\n") }] }; await sleep(2000); } else { steps.push("GDB Server: already running"); } if (probe.supportsRTT() && !this.rttClient.isConnected()) { try { await this.rttClient.connect(); steps.push(`RTT: connected (port ${probe.getRTTPort()})`); await sleep(1500); } catch (err) { steps.push(`RTT: failed - ${err instanceof Error ? err.message : String(err)}`); } } else if (!probe.supportsRTT()) { steps.push(`RTT: not supported by ${probe.displayName}`); } else { steps.push("RTT: already connected"); } const lines = this.rttClient.getLines(100); if (lines.length > 0) { steps.push(`\n--- Device Output (${lines.length} lines) ---`); steps.push(lines.join("\n")); } else { steps.push("\nNo RTT output yet."); } return { content: [{ type: "text", text: steps.join("\n") }] }; }); this.server.tool("snapshot", "Capture complete device state: CPU registers (compact), fault status, recent RTT output, and stack dump.", { rttLines: zod_1.z.number().min(0).max(200).optional().describe("RTT lines to include (default 30)") }, async ({ rttLines }) => { const guard = this.requireDevice(); if (guard) return guard; const sections = []; const regResult = await probe.readAllRegisters(); const regs = probe.parseRegisters(regResult.rawOutput); if (regs) { sections.push("## Registers"); sections.push(probe.formatRegistersCompact(regs)); } else { sections.push("## Registers\n" + (regResult.output || "Failed to read")); } const faultData = await probe.readFaultRegisters(); sections.push("\n## Fault Status"); sections.push(faultData.decoded); if (regs?.["SP"]) { const sp = parseInt(regs["SP"], 16); if (!isNaN(sp) && sp > 0) { const stackResult = await probe.readMemory(sp, 64); const stackDump = probe.parseMemoryDump(stackResult.rawOutput); if (stackDump.length > 0) { sections.push("\n## Stack (64 bytes from SP)"); sections.push(stackDump.map((d) => `${d.address}: ${d.hex} ${d.ascii}`).join("\n")); } } } const lines = this.rttClient.getLines(rttLines ?? 30); if (lines.length > 0) { sections.push(`\n## RTT Output (last ${lines.length} lines)`); sections.push(lines.join("\n")); } return { content: [{ type: "text", text: sections.join("\n") }] }; }); this.server.tool("diagnose_crash", "Auto-read and decode ARM Cortex-M fault registers (CFSR, HFSR, MMFAR, BFAR), exception stack frame, and recent errors.", {}, async () => { const guard = this.requireDevice(); if (guard) return guard; const sections = ["## Crash Diagnosis"]; const regResult = await probe.readAllRegisters(); const regs = probe.parseRegisters(regResult.rawOutput); if (regs) { sections.push("\n### CPU State"); sections.push(probe.formatRegistersCompact(regs)); const ipsr = regs["IPSR"]; if (ipsr && ipsr !== "0x000" && ipsr !== "0x00000000") { sections.push(`\n⚠ CPU is in exception handler (IPSR=${ipsr})`); } } const faultData = await probe.readFaultRegisters(); sections.push("\n### Fault Registers"); sections.push(`CFSR=0x${faultData.raw.cfsr.toString(16).padStart(8, "0")} HFSR=0x${faultData.raw.hfsr.toString(16).padStart(8, "0")} MMFAR=0x${faultData.raw.mmfar.toString(16).padStart(8, "0")} BFAR=0x${faultData.raw.bfar.toString(16).padStart(8, "0")}`); sections.push("\n### Decoded Faults"); sections.push(faultData.decoded); if (regs) { const spAddr = regs["PSP"] && regs["PSP"] !== "0x00000000" ? parseInt(regs["PSP"], 16) : parseInt(regs["MSP"] || "0", 16); if (spAddr > 0 && spAddr < 0xFFFFFFFF) { const frameResult = await probe.readMemory(spAddr, 32); const frameDump = probe.parseMemoryDump(frameResult.rawOutput); if (frameDump.length > 0) { sections.push("\n### Exception Stack Frame"); const allBytes = frameDump.map((d) => d.hex).join(" "); const bytes = allBytes.split(/\s+/).filter(Boolean); if (bytes.length >= 32) { const frameRegs = ["R0", "R1", "R2", "R3", "R12", "LR", "PC", "xPSR"]; for (let i = 0; i < frameRegs.length; i++) { const offset = i * 4; if (offset + 3 < bytes.length) { const val = [bytes[offset + 3], bytes[offset + 2], bytes[offset + 1], bytes[offset]].join(""); sections.push(` ${frameRegs[i].padEnd(5)} = 0x${val}`); } } if (bytes.length >= 28) { const faultPC = [bytes[27], bytes[26], bytes[25], bytes[24]].join(""); sections.push(`\n→ Faulting instruction at PC=0x${faultPC}`); } } else { sections.push(frameDump.map((d) => `${d.address}: ${d.hex}`).join("\n")); } } } } const errLines = this.rttClient.search({ level: "err", count: 10 }); const wrnLines = this.rttClient.search({ level: "wrn", count: 5 }); if (errLines.length > 0 || wrnLines.length > 0) { sections.push("\n### Recent Errors/Warnings from RTT"); for (const l of [...errLines, ...wrnLines]) { sections.push(` [${l.level === "err" ? "ERR" : "WRN"}] ${l.module || "?"}: ${l.message}`); } } return { content: [{ type: "text", text: sections.join("\n") }] }; }); // ═══════════════════════════════════════════════════════════════ // DEVICE CONTROL // ═══════════════════════════════════════════════════════════════ this.server.tool("device_info", `Get connected device info via ${probe.displayName}. Returns probe type, target CPU, and compact register summary.`, {}, async () => { const guard = this.requireDevice(); if (guard) return guard; const result = await probe.getDeviceInfo(); const regs = probe.parseRegisters(result.rawOutput); if (regs) { return { content: [{ type: "text", text: `Probe: ${probe.displayName}\n\n${probe.formatRegistersCompact(regs)}` }] }; } return { content: [{ type: "text", text: result.output || result.rawOutput }] }; }); this.server.tool("halt", "Halt the target CPU", {}, async () => { const g = this.requireDevice(); if (g) return g; const r = await probe.halt(); return { content: [{ type: "text", text: r.success ? "CPU halted" : `Failed: ${r.output}` }] }; }); this.server.tool("resume", "Resume the target CPU", {}, async () => { const g = this.requireDevice(); if (g) return g; const r = await probe.resume(); return { content: [{ type: "text", text: r.success ? "CPU resumed" : `Failed: ${r.output}` }] }; }); this.server.tool("reset", "Reset the target device", { halt: zod_1.z.boolean().optional().describe("Halt after reset (default: false)") }, async ({ halt }) => { const g = this.requireDevice(); if (g) return g; const r = await probe.reset(halt ?? false); return { content: [{ type: "text", text: r.success ? `Device reset${halt ? " (halted)" : " (running)"}` : `Failed: ${r.output}` }] }; }); this.server.tool("step", "Step one CPU instruction", {}, async () => { const g = this.requireDevice(); if (g) return g; const r = await probe.step(); const regs = probe.parseRegisters(r.rawOutput); if (regs) return { content: [{ type: "text", text: `Stepped. PC=${regs["PC"] || "?"} LR=${regs["LR"] || "?"} SP=${regs["SP"] || "?"}` }] }; return { content: [{ type: "text", text: r.output }] }; }); // ═══════════════════════════════════════════════════════════════ // MEMORY // ═══════════════════════════════════════════════════════════════ this.server.tool("read_memory", "Read memory from the target. Returns clean hex dump.", { address: zod_1.z.string().describe("Hex address (e.g., '0x20000000')"), length: zod_1.z.number().min(1).max(4096).describe("Bytes to read (max 4096)"), }, async ({ address, length }) => { const g = this.requireDevice(); if (g) return g; const addr = parseInt(address, 16); if (isNaN(addr)) return { content: [{ type: "text", text: "Error: invalid hex address" }] }; const r = await probe.readMemory(addr, length); const dump = probe.parseMemoryDump(r.rawOutput); if (dump.length > 0) return { content: [{ type: "text", text: dump.map((d) => `${d.address}: ${d.hex} ${d.ascii}`).join("\n") }] }; return { content: [{ type: "text", text: r.output || "Could not read memory" }] }; }); this.server.tool("write_memory", "Write a 32-bit value to memory", { address: zod_1.z.string().describe("Hex address"), value: zod_1.z.string().describe("Hex value (e.g., '0xDEADBEEF')"), }, async ({ address, value }) => { const g = this.requireDevice(); if (g) return g; const addr = parseInt(address, 16), val = parseInt(value, 16); if (isNaN(addr) || isNaN(val)) return { content: [{ type: "text", text: "Error: invalid hex" }] }; const r = await probe.writeMemory(addr, val); return { content: [{ type: "text", text: r.success ? `Wrote 0x${val.toString(16)} to 0x${addr.toString(16)}` : `Failed: ${r.output}` }] }; }); // ═══════════════════════════════════════════════════════════════ // REGISTERS // ═══════════════════════════════════════════════════════════════ this.server.tool("read_registers", "Read all CPU registers (compact format, FP only if non-zero).", {}, async () => { const g = this.requireDevice(); if (g) return g; const r = await probe.readAllRegisters(); const regs = probe.parseRegisters(r.rawOutput); if (regs) return { content: [{ type: "text", text: probe.formatRegistersCompact(regs) }] }; return { content: [{ type: "text", text: r.output }] }; }); this.server.tool("read_register", "Read a specific CPU register by name", { register: zod_1.z.string().describe("Register name (e.g., 'PC', 'SP', 'R0')") }, async ({ register }) => { const g = this.requireDevice(); if (g) return g; const r = await probe.readRegister(register); return { content: [{ type: "text", text: r.output || r.rawOutput }] }; }); // ═══════════════════════════════════════════════════════════════ // FLASH // ═══════════════════════════════════════════════════════════════ this.server.tool("flash", "Flash firmware to the target device", { filePath: zod_1.z.string().describe("Path to firmware file (.hex, .bin, .elf)"), baseAddress: zod_1.z.string().optional().describe("Base address for .bin files (hex)"), }, async ({ filePath, baseAddress }) => { const g = this.requireDevice(); if (g) return g; const addr = baseAddress ? parseInt(baseAddress, 16) : undefined; const r = await probe.flash(filePath, addr); return { content: [{ type: "text", text: r.success ? `Flashed ${filePath}` : `Flash failed: ${r.output}` }] }; }); this.server.tool("erase", "Erase target flash memory", {}, async () => { const g = this.requireDevice(); if (g) return g; const r = await probe.erase(); return { content: [{ type: "text", text: r.success ? "Chip erased" : `Erase failed: ${r.output}` }] }; }); // ═══════════════════════════════════════════════════════════════ // BREAKPOINTS // ═══════════════════════════════════════════════════════════════ this.server.tool("set_breakpoint", "Set a hardware breakpoint", { address: zod_1.z.string().describe("Hex address") }, async ({ address }) => { const addr = parseInt(address, 16); const g = this.requireDevice(); if (g) return g; const r = await probe.setBreakpoint(addr); return { content: [{ type: "text", text: r.success ? `Breakpoint set at 0x${addr.toString(16)}` : `Failed: ${r.output}` }] }; }); this.server.tool("clear_breakpoints", "Clear all breakpoints", {}, async () => { const g = this.requireDevice(); if (g) return g; await probe.clearBreakpoints(); return { content: [{ type: "text", text: "Breakpoints cleared" }] }; }); // ═══════════════════════════════════════════════════════════════ // GDB SERVER // ═══════════════════════════════════════════════════════════════ this.server.tool("gdb_server_start", `Start ${probe.displayName} GDB server`, {}, async () => { const g = this.requireDevice(); if (g) return g; const r = await probe.startGDBServer(); return { content: [{ type: "text", text: r.message }] }; }); this.server.tool("gdb_server_stop", `Stop ${probe.displayName} GDB server and disconnect RTT`, {}, async () => { this.rttClient.disconnect(); const r = probe.stopGDBServer(); return { content: [{ type: "text", text: r.message }] }; }); this.server.tool("gdb_server_status", "Get GDB server, RTT, and telnet proxy status", {}, async () => { const status = { probe: probe.displayName, gdbServer: probe.getGDBServerStatus(), rtt: this.rttClient.getStats(), telnetProxy: this.telnetProxy.getStatus() }; return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] }; }); // ═══════════════════════════════════════════════════════════════ // RTT // ═══════════════════════════════════════════════════════════════ this.server.tool("rtt_connect", `Connect to RTT${probe.supportsRTT() ? "" : " (not supported by " + probe.displayName + ")"}`, {}, async () => { if (!probe.supportsRTT()) return { content: [{ type: "text", text: `RTT is not supported by ${probe.displayName}` }] }; try { await this.rttClient.connect(); return { content: [{ type: "text", text: "Connected to RTT" }] }; } catch (err) { return { content: [{ type: "text", text: `Failed: ${err instanceof Error ? err.message : String(err)}` }] }; } }); this.server.tool("rtt_disconnect", "Disconnect from RTT", {}, async () => { this.rttClient.disconnect(); return { content: [{ type: "text", text: "Disconnected from RTT" }] }; }); this.server.tool("rtt_read", "Read recent RTT log lines (clean, parsed Zephyr format)", { count: zod_1.z.number().min(1).max(500).optional().describe("Lines to read (default 50)") }, async ({ count }) => { if (!this.rttClient.isConnected()) return { content: [{ type: "text", text: "RTT not connected. Use start_debug_session first." }] }; const lines = this.rttClient.getLines(count ?? 50); return { content: [{ type: "text", text: lines.length > 0 ? lines.join("\n") : "No RTT output yet." }] }; }); this.server.tool("rtt_search", "Search/filter RTT logs by level, module, or regex", { level: zod_1.z.string().optional().describe("Log level: 'err', 'wrn', 'inf', 'dbg'"), module: zod_1.z.string().optional().describe("Module name (partial match)"), pattern: zod_1.z.string().optional().describe("Regex or text pattern"), count: zod_1.z.number().min(1).max(500).optional().describe("Max results (default 50)"), }, async ({ level, module, pattern, count }) => { const results = this.rttClient.search({ level, module, pattern, count: count ?? 50 }); if (results.length === 0) return { content: [{ type: "text", text: "No matches found" }] }; return { content: [{ type: "text", text: `Found ${results.length} matches:\n${results.map(formatLogLine).join("\n")}` }] }; }); this.server.tool("rtt_send", "Send data to device via RTT down-channel", { data: zod_1.z.string().describe("Data to send") }, async ({ data }) => { const sent = this.rttClient.send(data); return { content: [{ type: "text", text: sent ? `Sent ${data.length} bytes` : "Failed: RTT not connected" }] }; }); this.server.tool("rtt_clear", "Clear RTT buffer", {}, async () => { this.rttClient.clearBuffer(); return { content: [{ type: "text", text: "RTT buffer cleared" }] }; }); // ═══════════════════════════════════════════════════════════════ // TELNET PROXY // ═══════════════════════════════════════════════════════════════ this.server.tool("telnet_proxy_start", "Start TCP proxy for Trice/Pigweed detokenizer", {}, async () => { const r = await this.telnetProxy.start(); return { content: [{ type: "text", text: r.message }] }; }); this.server.tool("telnet_proxy_stop", "Stop telnet proxy", {}, async () => { this.telnetProxy.stop(); return { content: [{ type: "text", text: "Telnet proxy stopped" }] }; }); this.server.tool("telnet_proxy_status", "Get telnet proxy status", {}, async () => { return { content: [{ type: "text", text: JSON.stringify(this.telnetProxy.getStatus(), null, 2) }] }; }); this.server.tool("telnet_proxy_read", "Read raw data from telnet proxy buffer", { lines: zod_1.z.number().min(1).max(500).optional().describe("Lines (default 100)") }, async ({ lines }) => { const data = this.telnetProxy.getBuffer(lines ?? 100); return { content: [{ type: "text", text: data.length > 0 ? data.join("\n") : "No data" }] }; }); // ═══════════════════════════════════════════════════════════════ // RAW / CONFIG // ═══════════════════════════════════════════════════════════════ this.server.tool("probe_command", `Execute raw ${probe.displayName} commands`, { commands: zod_1.z.array(zod_1.z.string()).describe("Commands to execute") }, async ({ commands }) => { const g = this.requireDevice(); if (g) return g; const r = await probe.executeRaw(commands); return { content: [{ type: "text", text: r.output || "(no output)" }] }; }); this.server.tool("get_config", "Get current probe and server configuration", {}, async () => { return { content: [{ type: "text", text: JSON.stringify({ probe: probe.type, displayName: probe.displayName, supportsRTT: probe.supportsRTT(), gdbServer: probe.getGDBServerStatus() }, null, 2) }] }; }); } registerResources() { this.server.resource("rtt-output", "rtt://output", { description: "Clean RTT output (ANSI stripped, Zephyr logs parsed)", mimeType: "text/plain" }, async () => ({ contents: [{ uri: "rtt://output", text: this.rttClient.getLines(200).join("\n"), mimeType: "text/plain" }] })); this.server.resource("gdb-server-log", "probe://gdb-server-log", { description: `Recent ${this.probe.displayName} GDB server output`, mimeType: "text/plain" }, async () => ({ contents: [{ uri: "probe://gdb-server-log", text: this.probe.getGDBServerOutput(200).join("\n"), mimeType: "text/plain" }] })); this.server.resource("system-status", "probe://status", { description: "Overall system status", mimeType: "application/json" }, async () => { const status = { probe: this.probe.type, displayName: this.probe.displayName, gdbServer: this.probe.getGDBServerStatus(), rtt: this.rttClient.getStats(), telnetProxy: this.telnetProxy.getStatus(), runningProcesses: this.processManager.listRunning() }; return { contents: [{ uri: "probe://status", text: JSON.stringify(status, null, 2), mimeType: "application/json" }] }; }); } registerPrompts() { const probeName = this.probe.displayName; this.server.prompt("debug-embedded", "Start an embedded debugging session.", {}, async () => ({ messages: [{ role: "user", content: { type: "text", text: `You are an embedded debugging assistant with a ${probeName} debug probe. ## Quick start Call start_debug_session first. ## Key tools: - **start_debug_session** - One-call setup, returns boot log - **snapshot** - Full device state in one call - **diagnose_crash** - Auto-decode fault registers - **rtt_read** / **rtt_search** - Device logs (${this.probe.supportsRTT() ? "supported" : "not supported by " + probeName}) - **read_memory** / **read_registers** - Inspect state - halt/resume/reset/step - CPU control - flash/erase - Firmware programming - probe_command - Raw ${probeName} commands ## ARM Cortex-M memory map: - 0x00000000: Vector table - 0x20000000: SRAM - 0xE000ED28: CFSR (fault status) Start with start_debug_session.` } }], })); this.server.prompt("crash-analysis", "Diagnose a crash. Use diagnose_crash tool.", {}, async () => ({ messages: [{ role: "user", content: { type: "text", text: "My device crashed. Use diagnose_crash first, then explain what happened." } }], })); this.server.prompt("analyze-rtt-output", "Analyze RTT output for errors and anomalies", {}, async () => { const lines = this.rttClient.getLines(200); const errs = this.rttClient.search({ level: "err", count: 20 }); const wrns = this.rttClient.search({ level: "wrn", count: 20 }); const sections = []; if (errs.length > 0) sections.push("## Errors:\n" + errs.map(formatLogLine).join("\n")); if (wrns.length > 0) sections.push("## Warnings:\n" + wrns.map(formatLogLine).join("\n")); sections.push("## Full log:\n" + (lines.length > 0 ? lines.join("\n") : "(No RTT data)")); return { messages: [{ role: "user", content: { type: "text", text: `Analyze this RTT output for faults, errors, anomalies:\n\n${sections.join("\n\n")}` } }] }; }); this.server.prompt("peripheral-inspect", "Inspect peripheral registers", { peripheral: zod_1.z.string().optional().describe("Peripheral name"), baseAddress: zod_1.z.string().optional().describe("Base address hex") }, async ({ peripheral, baseAddress }) => ({ messages: [{ role: "user", content: { type: "text", text: `Inspect ${peripheral || "peripheral"} registers.${baseAddress ? ` Base: ${baseAddress}.` : ""} Use read_memory to read the block and decode bit fields.` } }], })); } async startStdio() { const transport = new stdio_js_1.StdioServerTransport(); await this.server.connect(transport); (0, logger_1.log)("MCP Server started on stdio"); } dispose() { this.rttClient.disconnect(); this.telnetProxy.stop(); this.probe.dispose(); this.processManager.killAll(); } } exports.JLinkMcpServer = JLinkMcpServer; function formatLogLine(l) { if (l.deviceTime && l.level && l.module) return `[${l.deviceTime}] <${l.level}> ${l.module}: ${l.message}`; return l.message; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } //# sourceMappingURL=server.js.map