mcp-jvm-diagnostics 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * GC log parser.
3
+ *
4
+ * Supports:
5
+ * - Unified JVM logging format (-Xlog:gc*) — Java 9+
6
+ * - G1, ZGC, Parallel, Serial GC
7
+ * - Legacy -verbose:gc format (basic support)
8
+ */
9
+ // Unified logging: [0.123s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 24M->8M(256M) 1.234ms
10
+ const UNIFIED_GC_RE = /\[(\d+[.,]\d+)s\].*?GC\(\d+\)\s+(Pause\s+\S+(?:\s+\([^)]*\))*)\s+(\d+)M->(\d+)M\((\d+)M\)\s+(\d+[.,]\d+)ms/;
11
+ // Unified concurrent: [0.123s][info][gc] GC(0) Concurrent Mark 1.234ms
12
+ const UNIFIED_CONCURRENT_RE = /\[(\d+[.,]\d+)s\].*?GC\(\d+\)\s+(Concurrent\s+\S+(?:\s+\S+)?)\s+(\d+[.,]\d+)ms/;
13
+ // Legacy format: [GC (Allocation Failure) 65536K->12345K(251392K), 0.0123456 secs]
14
+ const LEGACY_GC_RE = /\[(Full )?GC\s*\(([^)]+)\)\s+(\d+)K->(\d+)K\((\d+)K\),\s+(\d+[.,]\d+)\s+secs\]/;
15
+ // ZGC format: [0.123s][info][gc] GC(0) Garbage Collection (Warmup) 24M(1%)->8M(0%) 1.234ms
16
+ const ZGC_RE = /\[(\d+[.,]\d+)s\].*?GC\(\d+\)\s+Garbage Collection\s+\([^)]+\)\s+(\d+)M\(\d+%\)->(\d+)M\(\d+%\)\s+(\d+[.,]\d+)ms/;
17
+ export function parseGcLog(text) {
18
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
19
+ const events = [];
20
+ let algorithm = "Unknown";
21
+ // Detect algorithm from log content
22
+ if (text.includes("Using G1"))
23
+ algorithm = "G1";
24
+ else if (text.includes("Using ZGC"))
25
+ algorithm = "ZGC";
26
+ else if (text.includes("Using Parallel"))
27
+ algorithm = "Parallel";
28
+ else if (text.includes("Using Serial"))
29
+ algorithm = "Serial";
30
+ else if (text.includes("Using Shenandoah"))
31
+ algorithm = "Shenandoah";
32
+ else if (text.includes("G1 Evacuation") || text.includes("G1 Humongous"))
33
+ algorithm = "G1";
34
+ else if (text.includes("Garbage Collection ("))
35
+ algorithm = "ZGC";
36
+ else if (text.includes("PSYoungGen") || text.includes("ParOldGen"))
37
+ algorithm = "Parallel";
38
+ for (const line of lines) {
39
+ // Try unified format first
40
+ const unifiedMatch = line.match(UNIFIED_GC_RE);
41
+ if (unifiedMatch) {
42
+ events.push({
43
+ timestamp: parseFloat(unifiedMatch[1].replace(",", ".")),
44
+ type: unifiedMatch[2].trim(),
45
+ pauseMs: parseFloat(unifiedMatch[6].replace(",", ".")),
46
+ heapBeforeMb: parseInt(unifiedMatch[3], 10),
47
+ heapAfterMb: parseInt(unifiedMatch[4], 10),
48
+ heapTotalMb: parseInt(unifiedMatch[5], 10),
49
+ });
50
+ continue;
51
+ }
52
+ // Try ZGC format
53
+ const zgcMatch = line.match(ZGC_RE);
54
+ if (zgcMatch) {
55
+ events.push({
56
+ timestamp: parseFloat(zgcMatch[1].replace(",", ".")),
57
+ type: "Pause Young (ZGC)",
58
+ pauseMs: parseFloat(zgcMatch[4].replace(",", ".")),
59
+ heapBeforeMb: parseInt(zgcMatch[2], 10),
60
+ heapAfterMb: parseInt(zgcMatch[3], 10),
61
+ heapTotalMb: 0,
62
+ });
63
+ continue;
64
+ }
65
+ // Try unified concurrent
66
+ const concurrentMatch = line.match(UNIFIED_CONCURRENT_RE);
67
+ if (concurrentMatch) {
68
+ events.push({
69
+ timestamp: parseFloat(concurrentMatch[1].replace(",", ".")),
70
+ type: concurrentMatch[2].trim(),
71
+ pauseMs: 0, // concurrent phases don't pause
72
+ heapBeforeMb: 0,
73
+ heapAfterMb: 0,
74
+ heapTotalMb: 0,
75
+ });
76
+ continue;
77
+ }
78
+ // Try legacy format
79
+ const legacyMatch = line.match(LEGACY_GC_RE);
80
+ if (legacyMatch) {
81
+ const isFull = legacyMatch[1] === "Full ";
82
+ events.push({
83
+ timestamp: 0,
84
+ type: isFull ? "Pause Full" : "Pause Young",
85
+ pauseMs: parseFloat(legacyMatch[6].replace(",", ".")) * 1000,
86
+ heapBeforeMb: Math.round(parseInt(legacyMatch[3], 10) / 1024),
87
+ heapAfterMb: Math.round(parseInt(legacyMatch[4], 10) / 1024),
88
+ heapTotalMb: Math.round(parseInt(legacyMatch[5], 10) / 1024),
89
+ });
90
+ }
91
+ }
92
+ // Calculate time span
93
+ let timeSpanMs = 0;
94
+ if (events.length > 1) {
95
+ const firstTs = events[0].timestamp;
96
+ const lastTs = events[events.length - 1].timestamp;
97
+ timeSpanMs = (lastTs - firstTs) * 1000;
98
+ }
99
+ return { algorithm, events, timeSpanMs };
100
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Parser for `jmap -histo` output.
3
+ *
4
+ * Example format:
5
+ * num #instances #bytes class name (module)
6
+ * -------------------------------------------------------
7
+ * 1: 123456 12345678 [B (java.base)
8
+ * 2: 98765 9876543 java.lang.String (java.base)
9
+ * 3: 45678 4567800 java.lang.Object[] (java.base)
10
+ * ...
11
+ * Total 500000 50000000
12
+ */
13
+ // Common JDK internal classes that are expected to be large
14
+ const JDK_INTERNALS = new Set([
15
+ "[B", "[C", "[I", "[J", "[S", "[Z", "[D", "[F",
16
+ "java.lang.String", "java.lang.Object[]", "java.lang.Class",
17
+ "java.util.HashMap$Node", "java.util.concurrent.ConcurrentHashMap$Node",
18
+ "java.lang.reflect.Method", "java.lang.ref.Finalizer",
19
+ ]);
20
+ const HISTO_LINE_RE = /^\s*(\d+):\s+(\d+)\s+(\d+)\s+(.+?)(?:\s+\((.+?)\))?\s*$/;
21
+ const TOTAL_LINE_RE = /^Total\s+(\d+)\s+(\d+)/;
22
+ export function parseHeapHisto(text) {
23
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
24
+ const entries = [];
25
+ let totalInstances = 0;
26
+ let totalBytes = 0;
27
+ for (const line of lines) {
28
+ const totalMatch = TOTAL_LINE_RE.exec(line);
29
+ if (totalMatch) {
30
+ totalInstances = parseInt(totalMatch[1], 10);
31
+ totalBytes = parseInt(totalMatch[2], 10);
32
+ continue;
33
+ }
34
+ const match = HISTO_LINE_RE.exec(line);
35
+ if (match) {
36
+ entries.push({
37
+ rank: parseInt(match[1], 10),
38
+ instances: parseInt(match[2], 10),
39
+ bytes: parseInt(match[3], 10),
40
+ className: match[4].trim(),
41
+ module: match[5] || null,
42
+ });
43
+ }
44
+ }
45
+ if (totalInstances === 0 && entries.length > 0) {
46
+ totalInstances = entries.reduce((sum, e) => sum + e.instances, 0);
47
+ totalBytes = entries.reduce((sum, e) => sum + e.bytes, 0);
48
+ }
49
+ const issues = [];
50
+ const recommendations = [];
51
+ if (entries.length === 0) {
52
+ issues.push({
53
+ severity: "CRITICAL",
54
+ message: "No histogram entries found — input may not be a valid jmap -histo output",
55
+ className: "",
56
+ });
57
+ return { entries, totalInstances, totalBytes, issues, recommendations };
58
+ }
59
+ // Analyze top entries for suspicious patterns
60
+ for (const entry of entries.slice(0, 30)) {
61
+ const pctBytes = totalBytes > 0 ? (entry.bytes / totalBytes) * 100 : 0;
62
+ const isJdkInternal = JDK_INTERNALS.has(entry.className);
63
+ // Large non-JDK class consuming > 10% of heap
64
+ if (pctBytes > 10 && !isJdkInternal) {
65
+ issues.push({
66
+ severity: "CRITICAL",
67
+ message: `${entry.className} consumes ${pctBytes.toFixed(1)}% of heap (${formatBytes(entry.bytes)}, ${entry.instances} instances) — potential memory leak`,
68
+ className: entry.className,
69
+ });
70
+ recommendations.push(`Investigate ${entry.className} — use Eclipse MAT or VisualVM to trace retention paths. Check for unbounded caches or collections.`);
71
+ }
72
+ // Very high instance count for non-JDK class (> 100K)
73
+ if (entry.instances > 100_000 && !isJdkInternal && pctBytes <= 10) {
74
+ issues.push({
75
+ severity: "WARNING",
76
+ message: `${entry.className} has ${entry.instances.toLocaleString()} instances (${formatBytes(entry.bytes)}) — may indicate an object creation hotspot`,
77
+ className: entry.className,
78
+ });
79
+ }
80
+ // Finalizer objects indicate slow finalization
81
+ if (entry.className === "java.lang.ref.Finalizer" && entry.instances > 10_000) {
82
+ issues.push({
83
+ severity: "WARNING",
84
+ message: `${entry.instances.toLocaleString()} Finalizer objects — finalization queue may be backed up. Classes using finalize() are blocking GC.`,
85
+ className: entry.className,
86
+ });
87
+ recommendations.push("Replace finalize() with Cleaner or try-with-resources. Finalizers delay GC and can cause memory pressure.");
88
+ }
89
+ // Char arrays ([C) or byte arrays ([B) dominating — usually String-related
90
+ if ((entry.className === "[B" || entry.className === "[C") && pctBytes > 40) {
91
+ issues.push({
92
+ severity: "WARNING",
93
+ message: `${entry.className === "[B" ? "Byte" : "Char"} arrays consume ${pctBytes.toFixed(1)}% of heap — likely driven by String retention. Check for large string caches or log buffering.`,
94
+ className: entry.className,
95
+ });
96
+ }
97
+ }
98
+ // Check for class loader leak indicators
99
+ const classCount = entries.find(e => e.className === "java.lang.Class");
100
+ if (classCount && classCount.instances > 30_000) {
101
+ issues.push({
102
+ severity: "WARNING",
103
+ message: `${classCount.instances.toLocaleString()} loaded classes — possible classloader leak (hot redeploy, dynamic proxy generation)`,
104
+ className: "java.lang.Class",
105
+ });
106
+ recommendations.push("Check for classloader leaks if using hot deployment (Tomcat, Spring DevTools). Consider restarting instead of redeploying.");
107
+ }
108
+ if (issues.length === 0) {
109
+ recommendations.push("Heap histogram looks healthy. For deeper analysis, capture a full heap dump: jmap -dump:live,format=b,file=heap.hprof <pid>");
110
+ }
111
+ return { entries, totalInstances, totalBytes, issues, recommendations };
112
+ }
113
+ function formatBytes(bytes) {
114
+ if (bytes >= 1_073_741_824)
115
+ return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
116
+ if (bytes >= 1_048_576)
117
+ return `${(bytes / 1_048_576).toFixed(1)} MB`;
118
+ if (bytes >= 1024)
119
+ return `${(bytes / 1024).toFixed(0)} KB`;
120
+ return `${bytes} B`;
121
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Parser for `jfr summary <file>` output.
3
+ *
4
+ * The jfr summary command prints event statistics from a JDK Flight Recorder
5
+ * (.jfr) file: event types, counts, and sizes. This parser extracts that data
6
+ * and produces analytical insights.
7
+ */
8
+ /**
9
+ * Parse `jfr summary` output text.
10
+ *
11
+ * Handles both the tabular format:
12
+ * Event Type Count Size (bytes)
13
+ * ===========================================================
14
+ * jdk.ObjectAllocationInNewTLAB 542 28184
15
+ *
16
+ * And the summary lines at the top (start time, duration, etc.).
17
+ */
18
+ export function parseJfrSummary(text) {
19
+ if (!text || text.trim().length === 0) {
20
+ throw new Error("Empty JFR summary input");
21
+ }
22
+ const lines = text.split("\n");
23
+ const events = [];
24
+ let startTime;
25
+ let duration;
26
+ // Match event rows: event_name count size
27
+ // Format: " jdk.GCPhasePause 123 45678"
28
+ const eventLineRegex = /^\s*([\w.]+(?:\s+\([^)]+\))?)\s+(\d+)\s+(\d+)\s*$/;
29
+ // Alternative format without size column
30
+ const eventNoSizeRegex = /^\s*([\w.]+)\s+(\d+)\s*$/;
31
+ // Header metadata patterns
32
+ const startTimeRegex = /start\s*(?:time)?[:=]\s*(.+)/i;
33
+ const durationRegex = /duration[:=]\s*(.+)/i;
34
+ for (const line of lines) {
35
+ const trimmed = line.trim();
36
+ // Skip empty lines, headers, separators
37
+ if (!trimmed || /^[=\-]+$/.test(trimmed) || /^Event\s+Type/i.test(trimmed)) {
38
+ continue;
39
+ }
40
+ // Check for metadata lines
41
+ const startMatch = trimmed.match(startTimeRegex);
42
+ if (startMatch) {
43
+ startTime = startMatch[1].trim();
44
+ continue;
45
+ }
46
+ const durMatch = trimmed.match(durationRegex);
47
+ if (durMatch) {
48
+ duration = durMatch[1].trim();
49
+ continue;
50
+ }
51
+ // Parse event lines (with size)
52
+ const eventMatch = line.match(eventLineRegex);
53
+ if (eventMatch) {
54
+ events.push({
55
+ name: eventMatch[1].trim(),
56
+ count: parseInt(eventMatch[2], 10),
57
+ size: parseInt(eventMatch[3], 10),
58
+ });
59
+ continue;
60
+ }
61
+ // Parse event lines (without size — some JFR versions)
62
+ const noSizeMatch = line.match(eventNoSizeRegex);
63
+ if (noSizeMatch && !trimmed.startsWith("#") && !/^[A-Z][a-z]+:/.test(trimmed)) {
64
+ events.push({
65
+ name: noSizeMatch[1].trim(),
66
+ count: parseInt(noSizeMatch[2], 10),
67
+ size: 0,
68
+ });
69
+ }
70
+ }
71
+ if (events.length === 0) {
72
+ throw new Error("No JFR events found in summary. Ensure input is from `jfr summary <file>`.");
73
+ }
74
+ const totalEvents = events.reduce((sum, e) => sum + e.count, 0);
75
+ const totalSize = events.reduce((sum, e) => sum + e.size, 0);
76
+ const { issues, recommendations } = analyzeJfrEvents(events, totalEvents, totalSize);
77
+ return {
78
+ events,
79
+ totalEvents,
80
+ totalSize,
81
+ startTime,
82
+ duration,
83
+ issues,
84
+ recommendations,
85
+ };
86
+ }
87
+ function analyzeJfrEvents(events, totalEvents, totalSize) {
88
+ const issues = [];
89
+ const recommendations = [];
90
+ // Build a lookup by event name
91
+ const byName = new Map(events.map((e) => [e.name, e]));
92
+ // Check for excessive GC events
93
+ const gcEvents = events.filter((e) => e.name.startsWith("jdk.GC") || e.name.startsWith("jdk.G1") || e.name.startsWith("jdk.ZGC"));
94
+ const gcCount = gcEvents.reduce((sum, e) => sum + e.count, 0);
95
+ if (gcCount > 1000) {
96
+ issues.push(`High GC activity: ${gcCount.toLocaleString()} GC events recorded. Possible memory pressure.`);
97
+ recommendations.push("Analyze GC logs with `analyze_gc_log` for pause time breakdown and tuning recommendations.");
98
+ }
99
+ // Check for excessive allocation events
100
+ const allocTLAB = byName.get("jdk.ObjectAllocationInNewTLAB");
101
+ const allocOutside = byName.get("jdk.ObjectAllocationOutsideTLAB");
102
+ if (allocOutside && allocOutside.count > 100) {
103
+ issues.push(`${allocOutside.count.toLocaleString()} allocations outside TLAB — objects too large for thread-local allocation buffers.`);
104
+ recommendations.push("Review large object allocations. Consider increasing TLAB size with -XX:TLABSize or reducing object sizes.");
105
+ }
106
+ // Check for thread contention
107
+ const monitorEnter = byName.get("jdk.JavaMonitorEnter");
108
+ const monitorWait = byName.get("jdk.JavaMonitorWait");
109
+ if (monitorEnter && monitorEnter.count > 500) {
110
+ issues.push(`${monitorEnter.count.toLocaleString()} monitor enter events — significant lock contention.`);
111
+ recommendations.push("Use `analyze_thread_dump` to identify contention hotspots and consider lock-free alternatives.");
112
+ }
113
+ // Check for thread starts (churn)
114
+ const threadStart = byName.get("jdk.ThreadStart");
115
+ if (threadStart && threadStart.count > 200) {
116
+ issues.push(`${threadStart.count.toLocaleString()} threads started — possible thread churn. Consider thread pooling.`);
117
+ }
118
+ // Check for exception events
119
+ const exceptions = byName.get("jdk.JavaExceptionThrow");
120
+ if (exceptions && exceptions.count > 500) {
121
+ issues.push(`${exceptions.count.toLocaleString()} exceptions thrown — exceptions are expensive and may indicate control flow issues.`);
122
+ recommendations.push("Review exception handling patterns. Avoid using exceptions for control flow.");
123
+ }
124
+ // Check for class loading
125
+ const classLoad = byName.get("jdk.ClassLoad");
126
+ if (classLoad && classLoad.count > 1000) {
127
+ issues.push(`${classLoad.count.toLocaleString()} class loads — excessive class loading may indicate classloader leak or dynamic proxy overuse.`);
128
+ }
129
+ // Check for file/socket I/O
130
+ const fileRead = byName.get("jdk.FileRead");
131
+ const fileWrite = byName.get("jdk.FileWrite");
132
+ const socketRead = byName.get("jdk.SocketRead");
133
+ const socketWrite = byName.get("jdk.SocketWrite");
134
+ const ioCount = [fileRead, fileWrite, socketRead, socketWrite]
135
+ .filter(Boolean)
136
+ .reduce((sum, e) => sum + e.count, 0);
137
+ if (ioCount > 5000) {
138
+ issues.push(`High I/O activity: ${ioCount.toLocaleString()} file/socket events. Consider batching or buffering.`);
139
+ }
140
+ // Check for compilation events
141
+ const compilation = byName.get("jdk.Compilation");
142
+ if (compilation && compilation.count > 500) {
143
+ issues.push(`${compilation.count.toLocaleString()} JIT compilations — may indicate insufficient code cache or deoptimizations.`);
144
+ recommendations.push("Check -XX:ReservedCodeCacheSize and look for deoptimization events.");
145
+ }
146
+ // Recording size analysis
147
+ if (totalSize > 100_000_000) {
148
+ recommendations.push(`Recording is ${(totalSize / 1_048_576).toFixed(0)} MB. Consider narrowing event filters with jfc settings.`);
149
+ }
150
+ // If dominant event type uses >50% of total
151
+ if (events.length > 0) {
152
+ const sorted = [...events].sort((a, b) => b.count - a.count);
153
+ const topEvent = sorted[0];
154
+ const topPct = totalEvents > 0 ? (topEvent.count / totalEvents) * 100 : 0;
155
+ if (topPct > 50) {
156
+ recommendations.push(`Event "${topEvent.name}" dominates at ${topPct.toFixed(0)}% of all events. Focus analysis there.`);
157
+ }
158
+ }
159
+ return { issues, recommendations };
160
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * HotSpot thread dump parser.
3
+ *
4
+ * Parses jstack/kill -3 output into structured thread data including:
5
+ * - Thread name, state, daemon status
6
+ * - Stack traces
7
+ * - Lock information (waiting on, holding)
8
+ */
9
+ // Java 21+ thread dumps include [os_thread_id] after #id:
10
+ // "Finalizer" #11 [2523503] daemon prio=8 os_prio=0 ...
11
+ const THREAD_HEADER_RE = /^"(.+?)"\s*(#\d+)?\s*(?:\[\d+\])?\s*(daemon)?\s*(?:prio=(\d+))?\s*(?:os_prio=\d+)?\s*(?:cpu=[\d.]+ms)?\s*(?:elapsed=[\d.]+s)?\s*(?:tid=(0x[0-9a-f]+))?\s*(?:nid=(0x[0-9a-f]+|\d+))?\s*(.*)/;
12
+ const STATE_RE = /java\.lang\.Thread\.State:\s*(\S+)/;
13
+ const WAITING_ON_RE = /- waiting to lock\s+<(0x[0-9a-f]+)>/;
14
+ const LOCKED_RE = /- locked\s+<(0x[0-9a-f]+)>/;
15
+ const PARKING_RE = /- parking to wait for\s+<(0x[0-9a-f]+)>/;
16
+ const WAITING_OBJ_RE = /- waiting on\s+<(0x[0-9a-f]+)>/;
17
+ export function parseThreadDump(text) {
18
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
19
+ const threads = [];
20
+ let jvmInfo = "";
21
+ let timestamp = "";
22
+ // Extract JVM info and timestamp from header
23
+ for (const line of lines) {
24
+ if (line.startsWith("Full thread dump")) {
25
+ jvmInfo = line.replace("Full thread dump ", "").replace(":", "").trim();
26
+ break;
27
+ }
28
+ }
29
+ // Look for timestamp (common format: YYYY-MM-DD HH:MM:SS)
30
+ const tsMatch = text.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/);
31
+ if (tsMatch) {
32
+ timestamp = tsMatch[1];
33
+ }
34
+ let currentThread = null;
35
+ let inStack = false;
36
+ for (const line of lines) {
37
+ // Try matching thread header
38
+ const headerMatch = line.match(THREAD_HEADER_RE);
39
+ if (headerMatch && line.startsWith('"')) {
40
+ // Save previous thread
41
+ if (currentThread) {
42
+ threads.push(currentThread);
43
+ }
44
+ currentThread = {
45
+ name: headerMatch[1],
46
+ state: "UNKNOWN",
47
+ isDaemon: headerMatch[3] === "daemon",
48
+ priority: headerMatch[4] ? parseInt(headerMatch[4], 10) : 5,
49
+ tid: headerMatch[5] || "",
50
+ nid: headerMatch[6] || "",
51
+ stackTrace: [],
52
+ waitingOn: null,
53
+ holdsLocks: [],
54
+ blockedBy: null,
55
+ };
56
+ // Some thread headers include state inline (e.g., "runnable", "waiting on condition")
57
+ const inlineState = headerMatch[7]?.trim();
58
+ if (inlineState) {
59
+ if (inlineState.includes("runnable"))
60
+ currentThread.state = "RUNNABLE";
61
+ else if (inlineState.includes("waiting on condition"))
62
+ currentThread.state = "TIMED_WAITING";
63
+ else if (inlineState.includes("waiting for monitor"))
64
+ currentThread.state = "BLOCKED";
65
+ else if (inlineState.includes("in Object.wait"))
66
+ currentThread.state = "WAITING";
67
+ else if (inlineState.includes("sleeping"))
68
+ currentThread.state = "TIMED_WAITING";
69
+ }
70
+ inStack = true;
71
+ continue;
72
+ }
73
+ if (!currentThread || !inStack)
74
+ continue;
75
+ // Thread state line
76
+ const stateMatch = line.match(STATE_RE);
77
+ if (stateMatch) {
78
+ currentThread.state = stateMatch[1];
79
+ continue;
80
+ }
81
+ // Stack trace line
82
+ if (line.match(/^\s+at\s+/)) {
83
+ currentThread.stackTrace.push(line.trim());
84
+ continue;
85
+ }
86
+ // Lock information
87
+ const waitMatch = line.match(WAITING_ON_RE);
88
+ if (waitMatch) {
89
+ currentThread.waitingOn = waitMatch[1];
90
+ currentThread.blockedBy = waitMatch[1];
91
+ continue;
92
+ }
93
+ const lockMatch = line.match(LOCKED_RE);
94
+ if (lockMatch) {
95
+ currentThread.holdsLocks.push(lockMatch[1]);
96
+ continue;
97
+ }
98
+ const parkMatch = line.match(PARKING_RE);
99
+ if (parkMatch) {
100
+ currentThread.waitingOn = parkMatch[1];
101
+ continue;
102
+ }
103
+ const waitObjMatch = line.match(WAITING_OBJ_RE);
104
+ if (waitObjMatch) {
105
+ currentThread.waitingOn = waitObjMatch[1];
106
+ continue;
107
+ }
108
+ // Empty line ends the current thread's stack
109
+ if (line.trim() === "" && currentThread.stackTrace.length > 0) {
110
+ inStack = false;
111
+ }
112
+ }
113
+ // Push last thread
114
+ if (currentThread) {
115
+ threads.push(currentThread);
116
+ }
117
+ return { jvmInfo, timestamp, threads };
118
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "mcp-jvm-diagnostics",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for JVM diagnostics — analyze thread dumps, detect deadlocks, parse GC logs, and get JVM tuning recommendations",
5
+ "author": "Dmytro Lisnichenko",
6
+ "type": "module",
7
+ "main": "./build/index.js",
8
+ "bin": {
9
+ "mcp-jvm-diagnostics": "./build/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc && chmod 755 build/index.js",
13
+ "test": "vitest run",
14
+ "dev": "tsc --watch",
15
+ "start": "node build/index.js",
16
+ "prepublishOnly": "npm run build && npm test"
17
+ },
18
+ "files": [
19
+ "build/**/*.js",
20
+ "!build/__tests__",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "keywords": [
25
+ "mcp",
26
+ "model-context-protocol",
27
+ "jvm",
28
+ "java",
29
+ "thread-dump",
30
+ "deadlock",
31
+ "gc-log",
32
+ "garbage-collection",
33
+ "diagnostics",
34
+ "performance",
35
+ "ai",
36
+ "claude"
37
+ ],
38
+ "license": "MIT",
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/Dmitriusan/mcp-jvm-diagnostics"
45
+ },
46
+ "homepage": "https://github.com/Dmitriusan/mcp-jvm-diagnostics#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/Dmitriusan/mcp-jvm-diagnostics/issues"
49
+ },
50
+ "dependencies": {
51
+ "@modelcontextprotocol/sdk": "^1.27.1",
52
+ "zod": "^3.24.2"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.0.0",
56
+ "typescript": "^5.8.2",
57
+ "vitest": "^4.0.18"
58
+ }
59
+ }