mcp-jvm-diagnostics 0.1.2 → 0.1.4
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.
- package/README.md +8 -1
- package/build/index.js +2 -29
- package/build/parsers/gc-log.js +16 -0
- package/build/parsers/jfr-summary.js +5 -0
- package/package.json +2 -1
- package/build/license.js +0 -114
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ A Model Context Protocol (MCP) server that gives AI assistants the ability to an
|
|
|
9
9
|
|
|
10
10
|
JVM diagnostic MCP servers exist (TDA, jfr-mcp, Arthas) — but they're all **Java-based**, requiring a JVM runtime just to diagnose JVM problems. This tool runs on **Node.js** via `npx` — no JVM, no Docker, no SSH.
|
|
11
11
|
|
|
12
|
-
It analyzes **offline** artifacts (thread dumps, GC logs, heap histograms) rather than requiring a running JVM. Paste a thread dump or GC log and get instant analysis.
|
|
12
|
+
It analyzes **offline** artifacts (thread dumps, GC logs, heap histograms) rather than requiring a running JVM. Paste a thread dump or GC log and get instant analysis.
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
@@ -160,6 +160,13 @@ Add these JVM flags to your application:
|
|
|
160
160
|
- **HotSpot/OpenJDK only**: Parser targets HotSpot/OpenJDK thread dump format. GraalVM native-image or Eclipse OpenJ9 dumps may parse incompletely.
|
|
161
161
|
- **Classloader leak detection**: Heap analysis flags growing ClassLoader instances but cannot definitively prove a leak without memory profiler data.
|
|
162
162
|
|
|
163
|
+
## Part of the MCP Java Backend Suite
|
|
164
|
+
|
|
165
|
+
- [mcp-db-analyzer](https://www.npmjs.com/package/mcp-db-analyzer) — PostgreSQL/MySQL/SQLite schema analysis
|
|
166
|
+
- [mcp-spring-boot-actuator](https://www.npmjs.com/package/mcp-spring-boot-actuator) — Spring Boot health, metrics, and bean analysis
|
|
167
|
+
- [mcp-redis-diagnostics](https://www.npmjs.com/package/mcp-redis-diagnostics) — Redis memory, slowlog, and client diagnostics
|
|
168
|
+
- [mcp-migration-advisor](https://www.npmjs.com/package/mcp-migration-advisor) — Flyway/Liquibase migration risk analysis
|
|
169
|
+
|
|
163
170
|
## License
|
|
164
171
|
|
|
165
172
|
MIT
|
package/build/index.js
CHANGED
|
@@ -10,12 +10,9 @@ import { analyzeGcPressure } from "./analyzers/gc-pressure.js";
|
|
|
10
10
|
import { parseHeapHisto } from "./parsers/heap-histo.js";
|
|
11
11
|
import { compareHeapHistos } from "./analyzers/heap-diff.js";
|
|
12
12
|
import { parseJfrSummary } from "./parsers/jfr-summary.js";
|
|
13
|
-
import { validateLicense, formatUpgradePrompt } from "./license.js";
|
|
14
|
-
// License check (reads MCP_LICENSE_KEY env var once at startup)
|
|
15
|
-
const license = validateLicense(process.env.MCP_LICENSE_KEY, "jvm-diagnostics");
|
|
16
13
|
// Handle --help
|
|
17
14
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
18
|
-
console.log(`mcp-jvm-diagnostics v0.1.
|
|
15
|
+
console.log(`mcp-jvm-diagnostics v0.1.4 — MCP server for JVM diagnostics
|
|
19
16
|
|
|
20
17
|
Usage:
|
|
21
18
|
mcp-jvm-diagnostics [options]
|
|
@@ -34,7 +31,7 @@ Tools provided:
|
|
|
34
31
|
}
|
|
35
32
|
const server = new McpServer({
|
|
36
33
|
name: "mcp-jvm-diagnostics",
|
|
37
|
-
version: "0.1.
|
|
34
|
+
version: "0.1.4",
|
|
38
35
|
});
|
|
39
36
|
// --- Tool: analyze_thread_dump ---
|
|
40
37
|
server.tool("analyze_thread_dump", "Parse a JVM thread dump (jstack output) and analyze thread states, detect deadlocks, identify lock contention hotspots, and find thread starvation patterns.", {
|
|
@@ -240,18 +237,6 @@ server.tool("compare_heap_histos", "Compare two jmap -histo snapshots taken at d
|
|
|
240
237
|
.string()
|
|
241
238
|
.describe("The SECOND (later) jmap -histo output"),
|
|
242
239
|
}, async ({ before, after }) => {
|
|
243
|
-
if (!license.isPro) {
|
|
244
|
-
return {
|
|
245
|
-
content: [{
|
|
246
|
-
type: "text",
|
|
247
|
-
text: formatUpgradePrompt("compare_heap_histos", "Heap histogram comparison with:\n" +
|
|
248
|
-
"- Memory growth pattern detection between snapshots\n" +
|
|
249
|
-
"- Leak candidate identification\n" +
|
|
250
|
-
"- New class allocation tracking\n" +
|
|
251
|
-
"- Shrinking class analysis"),
|
|
252
|
-
}],
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
240
|
try {
|
|
256
241
|
const report = compareHeapHistos(before, after);
|
|
257
242
|
const sections = [];
|
|
@@ -391,18 +376,6 @@ server.tool("diagnose_jvm", "Unified JVM diagnosis combining thread dump and GC
|
|
|
391
376
|
.optional()
|
|
392
377
|
.describe("GC log text (from -Xlog:gc*)"),
|
|
393
378
|
}, async ({ thread_dump, gc_log }) => {
|
|
394
|
-
if (!license.isPro) {
|
|
395
|
-
return {
|
|
396
|
-
content: [{
|
|
397
|
-
type: "text",
|
|
398
|
-
text: formatUpgradePrompt("diagnose_jvm", "Unified JVM diagnosis with:\n" +
|
|
399
|
-
"- Combined thread dump + GC log analysis\n" +
|
|
400
|
-
"- Cross-correlation of GC pauses and thread contention\n" +
|
|
401
|
-
"- Root cause identification\n" +
|
|
402
|
-
"- Prioritized remediation plan"),
|
|
403
|
-
}],
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
379
|
try {
|
|
407
380
|
if (!thread_dump && !gc_log) {
|
|
408
381
|
return {
|
package/build/parsers/gc-log.js
CHANGED
|
@@ -14,6 +14,9 @@ const UNIFIED_CONCURRENT_RE = /\[(\d+[.,]\d+)s\].*?GC\(\d+\)\s+(Concurrent\s+\S+
|
|
|
14
14
|
const LEGACY_GC_RE = /\[(Full )?GC\s*\(([^)]+)\)\s+(\d+)K->(\d+)K\((\d+)K\),\s+(\d+[.,]\d+)\s+secs\]/;
|
|
15
15
|
// ZGC format: [0.123s][info][gc] GC(0) Garbage Collection (Warmup) 24M(1%)->8M(0%) 1.234ms
|
|
16
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
|
+
// Shenandoah pause format: [0.521s][info][gc] GC(0) Pause Init Mark 2.606ms
|
|
18
|
+
// Pauses include: Pause Init Mark, Pause Final Mark, Pause Init Update Refs, Pause Final Update Refs, Pause Full
|
|
19
|
+
const SHENANDOAH_PAUSE_RE = /\[(\d+[.,]\d+)s\].*?GC\(\d+\)\s+(Pause\s+(?:Init|Final)\s+\S+(?:\s+\S+)?|Pause Full(?:\s+\([^)]*\))?)\s+(\d+[.,]\d+)ms/;
|
|
17
20
|
export function parseGcLog(text) {
|
|
18
21
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
19
22
|
const events = [];
|
|
@@ -75,6 +78,19 @@ export function parseGcLog(text) {
|
|
|
75
78
|
});
|
|
76
79
|
continue;
|
|
77
80
|
}
|
|
81
|
+
// Try Shenandoah pause format (no heap sizes in pause events)
|
|
82
|
+
const shenandoahMatch = line.match(SHENANDOAH_PAUSE_RE);
|
|
83
|
+
if (shenandoahMatch) {
|
|
84
|
+
events.push({
|
|
85
|
+
timestamp: parseFloat(shenandoahMatch[1].replace(",", ".")),
|
|
86
|
+
type: shenandoahMatch[2].trim(),
|
|
87
|
+
pauseMs: parseFloat(shenandoahMatch[3].replace(",", ".")),
|
|
88
|
+
heapBeforeMb: 0,
|
|
89
|
+
heapAfterMb: 0,
|
|
90
|
+
heapTotalMb: 0,
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
78
94
|
// Try legacy format
|
|
79
95
|
const legacyMatch = line.match(LEGACY_GC_RE);
|
|
80
96
|
if (legacyMatch) {
|
|
@@ -110,6 +110,11 @@ function analyzeJfrEvents(events, totalEvents, totalSize) {
|
|
|
110
110
|
issues.push(`${monitorEnter.count.toLocaleString()} monitor enter events — significant lock contention.`);
|
|
111
111
|
recommendations.push("Use `analyze_thread_dump` to identify contention hotspots and consider lock-free alternatives.");
|
|
112
112
|
}
|
|
113
|
+
// Check for threads spending time in Object.wait() — indicates producer-consumer imbalance
|
|
114
|
+
if (monitorWait && monitorWait.count > 2000) {
|
|
115
|
+
issues.push(`${monitorWait.count.toLocaleString()} Object.wait() events — threads frequently waiting for notifications. Producers may be slower than consumers.`);
|
|
116
|
+
recommendations.push("Profile the producer side of producer-consumer queues. Consider bounded queues with backpressure or increasing producer thread count.");
|
|
117
|
+
}
|
|
113
118
|
// Check for thread starts (churn)
|
|
114
119
|
const threadStart = byName.get("jdk.ThreadStart");
|
|
115
120
|
if (threadStart && threadStart.count > 200) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-jvm-diagnostics",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "MCP server for JVM diagnostics — analyze thread dumps, detect deadlocks, parse GC logs, and get JVM tuning recommendations",
|
|
5
|
+
"mcpName": "io.github.dmitriusan/mcp-jvm-diagnostics",
|
|
5
6
|
"author": "Dmytro Lisnichenko",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"main": "./build/index.js",
|
package/build/license.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* License validation for MCP Migration Advisor (Pro features).
|
|
3
|
-
*
|
|
4
|
-
* Validates license keys offline using HMAC-SHA256.
|
|
5
|
-
* Missing or invalid keys gracefully degrade to free mode — never errors.
|
|
6
|
-
*
|
|
7
|
-
* Key format: MCPJBS-XXXXX-XXXXX-XXXXX-XXXXX
|
|
8
|
-
* Payload (12 bytes = 20 base32 chars):
|
|
9
|
-
* [0] product mask (8 bits)
|
|
10
|
-
* [1-2] expiry days since 2026-01-01 (16 bits)
|
|
11
|
-
* [3-5] customer ID (24 bits)
|
|
12
|
-
* [6-11] HMAC-SHA256 truncated (48 bits)
|
|
13
|
-
*/
|
|
14
|
-
import { createHmac } from "node:crypto";
|
|
15
|
-
const KEY_PREFIX = "MCPJBS-";
|
|
16
|
-
const EPOCH = new Date("2026-01-01T00:00:00Z");
|
|
17
|
-
const BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
18
|
-
const HMAC_SECRET = "mcp-java-backend-suite-license-v1";
|
|
19
|
-
const PRODUCTS = {
|
|
20
|
-
"db-analyzer": 0,
|
|
21
|
-
"jvm-diagnostics": 1,
|
|
22
|
-
"migration-advisor": 2,
|
|
23
|
-
"spring-boot-actuator": 3,
|
|
24
|
-
"redis-diagnostics": 4,
|
|
25
|
-
};
|
|
26
|
-
export function validateLicense(key, product) {
|
|
27
|
-
const FREE = {
|
|
28
|
-
isPro: false,
|
|
29
|
-
expiresAt: null,
|
|
30
|
-
customerId: null,
|
|
31
|
-
reason: "No license key provided",
|
|
32
|
-
};
|
|
33
|
-
if (!key || key.trim().length === 0)
|
|
34
|
-
return FREE;
|
|
35
|
-
const trimmed = key.trim().toUpperCase();
|
|
36
|
-
if (!trimmed.startsWith(KEY_PREFIX)) {
|
|
37
|
-
return { ...FREE, reason: "Invalid key format: missing MCPJBS- prefix" };
|
|
38
|
-
}
|
|
39
|
-
const body = trimmed.slice(KEY_PREFIX.length).replace(/-/g, "");
|
|
40
|
-
if (body.length < 20) {
|
|
41
|
-
return { ...FREE, reason: "Invalid key format: too short" };
|
|
42
|
-
}
|
|
43
|
-
let decoded;
|
|
44
|
-
try {
|
|
45
|
-
decoded = base32Decode(body.slice(0, 20));
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return { ...FREE, reason: "Invalid key format: bad base32 encoding" };
|
|
49
|
-
}
|
|
50
|
-
if (decoded.length < 12) {
|
|
51
|
-
return { ...FREE, reason: "Invalid key format: decoded data too short" };
|
|
52
|
-
}
|
|
53
|
-
const payload = decoded.subarray(0, 6);
|
|
54
|
-
const providedSignature = decoded.subarray(6, 12);
|
|
55
|
-
const expectedHmac = createHmac("sha256", HMAC_SECRET)
|
|
56
|
-
.update(payload)
|
|
57
|
-
.digest();
|
|
58
|
-
const expectedSignature = expectedHmac.subarray(0, 6);
|
|
59
|
-
if (!providedSignature.equals(expectedSignature)) {
|
|
60
|
-
return { ...FREE, reason: "Invalid license key: signature mismatch" };
|
|
61
|
-
}
|
|
62
|
-
const productMask = payload[0];
|
|
63
|
-
const daysSinceEpoch = (payload[1] << 8) | payload[2];
|
|
64
|
-
const customerId = (payload[3] << 16) | (payload[4] << 8) | payload[5];
|
|
65
|
-
const productBit = PRODUCTS[product];
|
|
66
|
-
if (productBit === undefined) {
|
|
67
|
-
return { ...FREE, reason: `Unknown product: ${product}` };
|
|
68
|
-
}
|
|
69
|
-
if ((productMask & (1 << productBit)) === 0) {
|
|
70
|
-
return { ...FREE, customerId, reason: `License does not include ${product}` };
|
|
71
|
-
}
|
|
72
|
-
const expiresAt = new Date(EPOCH.getTime() + daysSinceEpoch * 24 * 60 * 60 * 1000);
|
|
73
|
-
if (new Date() > expiresAt) {
|
|
74
|
-
return {
|
|
75
|
-
isPro: false,
|
|
76
|
-
expiresAt,
|
|
77
|
-
customerId,
|
|
78
|
-
reason: `License expired on ${expiresAt.toISOString().slice(0, 10)}`,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
return { isPro: true, expiresAt, customerId, reason: "Valid Pro license" };
|
|
82
|
-
}
|
|
83
|
-
export function formatUpgradePrompt(toolName, featureDescription) {
|
|
84
|
-
return [
|
|
85
|
-
`## ${toolName} (Pro Feature)`,
|
|
86
|
-
"",
|
|
87
|
-
"This analysis is available with MCP Java Backend Suite Pro.",
|
|
88
|
-
"",
|
|
89
|
-
`**What you'll get:**`,
|
|
90
|
-
featureDescription,
|
|
91
|
-
"",
|
|
92
|
-
"**Upgrade**: https://mcpjbs.dev/pricing",
|
|
93
|
-
"**Price**: $19/month or $190/year",
|
|
94
|
-
"",
|
|
95
|
-
"> Already have a key? Set `MCP_LICENSE_KEY` in your Claude Desktop config.",
|
|
96
|
-
].join("\n");
|
|
97
|
-
}
|
|
98
|
-
function base32Decode(encoded) {
|
|
99
|
-
const bytes = [];
|
|
100
|
-
let bits = 0;
|
|
101
|
-
let value = 0;
|
|
102
|
-
for (const char of encoded) {
|
|
103
|
-
const idx = BASE32_CHARS.indexOf(char);
|
|
104
|
-
if (idx === -1)
|
|
105
|
-
throw new Error(`Invalid base32 character: ${char}`);
|
|
106
|
-
value = (value << 5) | idx;
|
|
107
|
-
bits += 5;
|
|
108
|
-
if (bits >= 8) {
|
|
109
|
-
bits -= 8;
|
|
110
|
-
bytes.push((value >> bits) & 0xff);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return Buffer.from(bytes);
|
|
114
|
-
}
|