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.
package/build/index.js ADDED
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { parseThreadDump } from "./parsers/thread-dump.js";
6
+ import { detectDeadlocks } from "./analyzers/deadlock.js";
7
+ import { analyzeContention } from "./analyzers/contention.js";
8
+ import { parseGcLog } from "./parsers/gc-log.js";
9
+ import { analyzeGcPressure } from "./analyzers/gc-pressure.js";
10
+ import { parseHeapHisto } from "./parsers/heap-histo.js";
11
+ import { compareHeapHistos } from "./analyzers/heap-diff.js";
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
+ // Handle --help
17
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
18
+ console.log(`mcp-jvm-diagnostics v0.1.0 — MCP server for JVM diagnostics
19
+
20
+ Usage:
21
+ mcp-jvm-diagnostics [options]
22
+
23
+ Options:
24
+ --help, -h Show this help message
25
+
26
+ Tools provided:
27
+ analyze_thread_dump Parse thread dump, detect deadlocks and contention
28
+ analyze_gc_log Parse GC log, detect pressure and tuning opportunities
29
+ analyze_heap_histo Parse jmap -histo output, detect memory leak candidates
30
+ compare_heap_histos Compare two jmap histos to detect memory growth
31
+ analyze_jfr Parse JFR summary output, detect performance hotspots
32
+ diagnose_jvm Unified diagnosis from thread dump + GC log`);
33
+ process.exit(0);
34
+ }
35
+ const server = new McpServer({
36
+ name: "mcp-jvm-diagnostics",
37
+ version: "0.1.0",
38
+ });
39
+ // --- Tool: analyze_thread_dump ---
40
+ 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.", {
41
+ thread_dump: z
42
+ .string()
43
+ .describe("The full thread dump text (from jstack, kill -3, or VisualVM)"),
44
+ }, async ({ thread_dump }) => {
45
+ try {
46
+ const parsed = parseThreadDump(thread_dump);
47
+ const deadlocks = detectDeadlocks(parsed.threads);
48
+ const contention = analyzeContention(parsed.threads);
49
+ const sections = [];
50
+ // Summary
51
+ sections.push(`## Thread Dump Analysis`);
52
+ sections.push(`\n- **JVM**: ${parsed.jvmInfo || "Unknown"}`);
53
+ sections.push(`- **Timestamp**: ${parsed.timestamp || "Unknown"}`);
54
+ sections.push(`- **Total threads**: ${parsed.threads.length}`);
55
+ // Thread state breakdown
56
+ const stateCounts = new Map();
57
+ for (const t of parsed.threads) {
58
+ stateCounts.set(t.state, (stateCounts.get(t.state) || 0) + 1);
59
+ }
60
+ sections.push(`\n### Thread States`);
61
+ sections.push(`| State | Count |`);
62
+ sections.push(`|-------|-------|`);
63
+ for (const [state, count] of [...stateCounts.entries()].sort((a, b) => b[1] - a[1])) {
64
+ sections.push(`| ${state} | ${count} |`);
65
+ }
66
+ // Deadlocks
67
+ if (deadlocks.length > 0) {
68
+ sections.push(`\n### Deadlocks Detected (${deadlocks.length})`);
69
+ for (const dl of deadlocks) {
70
+ sections.push(`\n**Deadlock cycle** (${dl.threads.length} threads):`);
71
+ for (const t of dl.threads) {
72
+ sections.push(`- **${t.name}** holds \`${t.holdsLock}\`, waiting for \`${t.waitingOn}\``);
73
+ }
74
+ sections.push(`\n**Resolution**: ${dl.recommendation}`);
75
+ }
76
+ }
77
+ else {
78
+ sections.push(`\n### Deadlocks: None detected`);
79
+ }
80
+ // Contention
81
+ if (contention.hotspots.length > 0) {
82
+ sections.push(`\n### Lock Contention Hotspots`);
83
+ sections.push(`| Lock | Blocked Threads | Holder |`);
84
+ sections.push(`|------|----------------|--------|`);
85
+ for (const h of contention.hotspots) {
86
+ sections.push(`| \`${h.lock}\` | ${h.blockedCount} | ${h.holderThread} |`);
87
+ }
88
+ if (contention.recommendations.length > 0) {
89
+ sections.push(`\n### Recommendations`);
90
+ for (const rec of contention.recommendations) {
91
+ sections.push(`- ${rec}`);
92
+ }
93
+ }
94
+ }
95
+ // Daemon vs non-daemon
96
+ const daemonCount = parsed.threads.filter(t => t.isDaemon).length;
97
+ sections.push(`\n### Thread Classification`);
98
+ sections.push(`- Daemon threads: ${daemonCount}`);
99
+ sections.push(`- Non-daemon threads: ${parsed.threads.length - daemonCount}`);
100
+ return { content: [{ type: "text", text: sections.join("\n") }] };
101
+ }
102
+ catch (err) {
103
+ return {
104
+ content: [{ type: "text", text: `Error analyzing thread dump: ${err instanceof Error ? err.message : String(err)}` }],
105
+ };
106
+ }
107
+ });
108
+ // --- Tool: analyze_gc_log ---
109
+ server.tool("analyze_gc_log", "Parse a JVM GC log and analyze garbage collection patterns, pause times, allocation rates, and memory pressure. Supports G1, ZGC, and Parallel GC formats.", {
110
+ gc_log: z
111
+ .string()
112
+ .describe("The GC log text (from -Xlog:gc* or -verbose:gc)"),
113
+ }, async ({ gc_log }) => {
114
+ try {
115
+ const parsed = parseGcLog(gc_log);
116
+ const pressure = analyzeGcPressure(parsed);
117
+ const sections = [];
118
+ sections.push(`## GC Log Analysis`);
119
+ sections.push(`\n- **GC Algorithm**: ${parsed.algorithm}`);
120
+ sections.push(`- **Total GC events**: ${parsed.events.length}`);
121
+ sections.push(`- **Time span**: ${parsed.timeSpanMs > 0 ? (parsed.timeSpanMs / 1000).toFixed(1) + "s" : "N/A"}`);
122
+ // Pause time stats
123
+ if (parsed.events.length > 0) {
124
+ sections.push(`\n### Pause Time Statistics`);
125
+ sections.push(`| Metric | Value |`);
126
+ sections.push(`|--------|-------|`);
127
+ sections.push(`| Min pause | ${pressure.minPauseMs.toFixed(1)} ms |`);
128
+ sections.push(`| Max pause | ${pressure.maxPauseMs.toFixed(1)} ms |`);
129
+ sections.push(`| Avg pause | ${pressure.avgPauseMs.toFixed(1)} ms |`);
130
+ sections.push(`| P95 pause | ${pressure.p95PauseMs.toFixed(1)} ms |`);
131
+ sections.push(`| Total pause time | ${pressure.totalPauseMs.toFixed(0)} ms |`);
132
+ sections.push(`| GC overhead | ${pressure.gcOverheadPct.toFixed(1)}% |`);
133
+ }
134
+ // Heap sizing
135
+ if (pressure.heapBeforeMb > 0) {
136
+ sections.push(`\n### Heap Usage`);
137
+ sections.push(`| Metric | Value |`);
138
+ sections.push(`|--------|-------|`);
139
+ sections.push(`| Heap before GC (avg) | ${pressure.heapBeforeMb.toFixed(0)} MB |`);
140
+ sections.push(`| Heap after GC (avg) | ${pressure.heapAfterMb.toFixed(0)} MB |`);
141
+ sections.push(`| Reclaim per GC (avg) | ${(pressure.heapBeforeMb - pressure.heapAfterMb).toFixed(0)} MB |`);
142
+ }
143
+ // Event breakdown
144
+ const typeCounts = new Map();
145
+ for (const e of parsed.events) {
146
+ typeCounts.set(e.type, (typeCounts.get(e.type) || 0) + 1);
147
+ }
148
+ sections.push(`\n### GC Event Types`);
149
+ sections.push(`| Type | Count |`);
150
+ sections.push(`|------|-------|`);
151
+ for (const [type, count] of [...typeCounts.entries()].sort((a, b) => b[1] - a[1])) {
152
+ sections.push(`| ${type} | ${count} |`);
153
+ }
154
+ // Issues and recommendations
155
+ if (pressure.issues.length > 0) {
156
+ sections.push(`\n### Issues Detected`);
157
+ for (const issue of pressure.issues) {
158
+ sections.push(`- ${issue}`);
159
+ }
160
+ }
161
+ if (pressure.recommendations.length > 0) {
162
+ sections.push(`\n### Recommendations`);
163
+ for (const rec of pressure.recommendations) {
164
+ sections.push(`- ${rec}`);
165
+ }
166
+ }
167
+ return { content: [{ type: "text", text: sections.join("\n") }] };
168
+ }
169
+ catch (err) {
170
+ return {
171
+ content: [{ type: "text", text: `Error analyzing GC log: ${err instanceof Error ? err.message : String(err)}` }],
172
+ };
173
+ }
174
+ });
175
+ // --- Tool: analyze_heap_histo ---
176
+ server.tool("analyze_heap_histo", "Parse jmap -histo output and detect memory leak candidates, object creation hotspots, classloader leaks, and heap composition issues.", {
177
+ histo: z
178
+ .string()
179
+ .describe("The jmap -histo output text (from jmap -histo <pid> or jmap -histo:live <pid>)"),
180
+ }, async ({ histo }) => {
181
+ try {
182
+ const report = parseHeapHisto(histo);
183
+ const sections = [];
184
+ sections.push(`## Heap Histogram Analysis`);
185
+ sections.push(`\n- **Total instances**: ${report.totalInstances.toLocaleString()}`);
186
+ sections.push(`- **Total bytes**: ${formatBytes(report.totalBytes)}`);
187
+ sections.push(`- **Classes**: ${report.entries.length}`);
188
+ // Top 15 by bytes
189
+ sections.push(`\n### Top 15 Classes by Memory`);
190
+ sections.push(`| Rank | Instances | Bytes | % Heap | Class |`);
191
+ sections.push(`|------|-----------|-------|--------|-------|`);
192
+ for (const entry of report.entries.slice(0, 15)) {
193
+ const pct = report.totalBytes > 0 ? ((entry.bytes / report.totalBytes) * 100).toFixed(1) : "0.0";
194
+ sections.push(`| ${entry.rank} | ${entry.instances.toLocaleString()} | ${formatBytes(entry.bytes)} | ${pct}% | ${entry.className} |`);
195
+ }
196
+ if (report.issues.length > 0) {
197
+ sections.push(`\n### Issues Detected`);
198
+ for (const issue of report.issues) {
199
+ sections.push(`\n**${issue.severity}**: ${issue.message}`);
200
+ }
201
+ }
202
+ if (report.recommendations.length > 0) {
203
+ sections.push(`\n### Recommendations`);
204
+ for (const rec of report.recommendations) {
205
+ sections.push(`- ${rec}`);
206
+ }
207
+ }
208
+ return { content: [{ type: "text", text: sections.join("\n") }] };
209
+ }
210
+ catch (err) {
211
+ return {
212
+ content: [{ type: "text", text: `Error analyzing heap histogram: ${err instanceof Error ? err.message : String(err)}` }],
213
+ };
214
+ }
215
+ });
216
+ // --- Tool: compare_heap_histos ---
217
+ server.tool("compare_heap_histos", "Compare two jmap -histo snapshots taken at different times to detect memory growth patterns, leak candidates, and new allocations. Captures what is growing between snapshots.", {
218
+ before: z
219
+ .string()
220
+ .describe("The FIRST (earlier) jmap -histo output"),
221
+ after: z
222
+ .string()
223
+ .describe("The SECOND (later) jmap -histo output"),
224
+ }, async ({ before, after }) => {
225
+ if (!license.isPro) {
226
+ return {
227
+ content: [{
228
+ type: "text",
229
+ text: formatUpgradePrompt("compare_heap_histos", "Heap histogram comparison with:\n" +
230
+ "- Memory growth pattern detection between snapshots\n" +
231
+ "- Leak candidate identification\n" +
232
+ "- New class allocation tracking\n" +
233
+ "- Shrinking class analysis"),
234
+ }],
235
+ };
236
+ }
237
+ try {
238
+ const report = compareHeapHistos(before, after);
239
+ const sections = [];
240
+ sections.push(`## Heap Histogram Comparison`);
241
+ sections.push(`\n- **Before**: ${report.totalInstancesBefore.toLocaleString()} instances, ${formatBytes(report.totalBytesBefore)}`);
242
+ sections.push(`- **After**: ${report.totalInstancesAfter.toLocaleString()} instances, ${formatBytes(report.totalBytesAfter)}`);
243
+ sections.push(`- **Delta**: ${report.totalBytesDelta >= 0 ? "+" : ""}${formatBytes(report.totalBytesDelta)}`);
244
+ // Top growing classes
245
+ if (report.growing.length > 0) {
246
+ sections.push(`\n### Top Growing Classes (${report.growing.length} total)`);
247
+ sections.push(`| Class | Before | After | Delta | Growth |`);
248
+ sections.push(`|-------|--------|-------|-------|--------|`);
249
+ for (const e of report.growing.slice(0, 15)) {
250
+ sections.push(`| ${e.className} | ${formatBytes(e.bytesBefore)} | ${formatBytes(e.bytesAfter)} | +${formatBytes(e.bytesDelta)} | +${e.growthPct.toFixed(0)}% |`);
251
+ }
252
+ }
253
+ // New classes
254
+ if (report.newClasses.length > 0) {
255
+ sections.push(`\n### New Classes (${report.newClasses.length} appeared)`);
256
+ sections.push(`| Class | Instances | Bytes |`);
257
+ sections.push(`|-------|-----------|-------|`);
258
+ for (const e of report.newClasses.slice(0, 10)) {
259
+ sections.push(`| ${e.className} | ${e.instancesAfter.toLocaleString()} | ${formatBytes(e.bytesAfter)} |`);
260
+ }
261
+ }
262
+ // Top shrinking
263
+ if (report.shrinking.length > 0) {
264
+ sections.push(`\n### Top Shrinking Classes (${report.shrinking.length} total)`);
265
+ sections.push(`| Class | Before | After | Delta |`);
266
+ sections.push(`|-------|--------|-------|-------|`);
267
+ for (const e of report.shrinking.slice(0, 10)) {
268
+ sections.push(`| ${e.className} | ${formatBytes(e.bytesBefore)} | ${formatBytes(e.bytesAfter)} | ${formatBytes(e.bytesDelta)} |`);
269
+ }
270
+ }
271
+ if (report.issues.length > 0) {
272
+ sections.push(`\n### Issues Detected`);
273
+ for (const issue of report.issues) {
274
+ sections.push(`- ${issue}`);
275
+ }
276
+ }
277
+ if (report.recommendations.length > 0) {
278
+ sections.push(`\n### Recommendations`);
279
+ for (const rec of report.recommendations) {
280
+ sections.push(`- ${rec}`);
281
+ }
282
+ }
283
+ return { content: [{ type: "text", text: sections.join("\n") }] };
284
+ }
285
+ catch (err) {
286
+ return {
287
+ content: [{ type: "text", text: `Error comparing histograms: ${err instanceof Error ? err.message : String(err)}` }],
288
+ };
289
+ }
290
+ });
291
+ function formatBytes(bytes) {
292
+ const abs = Math.abs(bytes);
293
+ const sign = bytes < 0 ? "-" : "";
294
+ if (abs >= 1_073_741_824)
295
+ return `${sign}${(abs / 1_073_741_824).toFixed(1)} GB`;
296
+ if (abs >= 1_048_576)
297
+ return `${sign}${(abs / 1_048_576).toFixed(1)} MB`;
298
+ if (abs >= 1024)
299
+ return `${sign}${(abs / 1024).toFixed(0)} KB`;
300
+ return `${sign}${abs} B`;
301
+ }
302
+ // --- Tool: analyze_jfr ---
303
+ server.tool("analyze_jfr", "Parse JDK Flight Recorder summary output (from `jfr summary <file>`) and analyze event distribution, detect performance hotspots, GC pressure, lock contention, I/O patterns, and excessive allocations.", {
304
+ jfr_summary: z
305
+ .string()
306
+ .describe("The output text from `jfr summary <recording.jfr>`"),
307
+ }, async ({ jfr_summary }) => {
308
+ try {
309
+ const summary = parseJfrSummary(jfr_summary);
310
+ const sections = [];
311
+ sections.push(`## JFR Recording Summary`);
312
+ if (summary.startTime)
313
+ sections.push(`\n- **Start time**: ${summary.startTime}`);
314
+ if (summary.duration)
315
+ sections.push(`- **Duration**: ${summary.duration}`);
316
+ sections.push(`- **Total events**: ${summary.totalEvents.toLocaleString()}`);
317
+ sections.push(`- **Total size**: ${formatBytes(summary.totalSize)}`);
318
+ sections.push(`- **Event types**: ${summary.events.length}`);
319
+ // Top 15 events by count
320
+ const sorted = [...summary.events].sort((a, b) => b.count - a.count);
321
+ sections.push(`\n### Top Events by Count`);
322
+ sections.push(`| Event Type | Count | Size | % Total |`);
323
+ sections.push(`|------------|-------|------|---------|`);
324
+ for (const e of sorted.slice(0, 15)) {
325
+ const pct = summary.totalEvents > 0 ? ((e.count / summary.totalEvents) * 100).toFixed(1) : "0.0";
326
+ sections.push(`| ${e.name} | ${e.count.toLocaleString()} | ${formatBytes(e.size)} | ${pct}% |`);
327
+ }
328
+ // Category breakdown
329
+ const categories = new Map();
330
+ for (const e of summary.events) {
331
+ const parts = e.name.split(".");
332
+ const category = parts.length >= 2 ? parts.slice(0, 2).join(".") : e.name;
333
+ const existing = categories.get(category) || { count: 0, size: 0 };
334
+ existing.count += e.count;
335
+ existing.size += e.size;
336
+ categories.set(category, existing);
337
+ }
338
+ const sortedCategories = [...categories.entries()].sort((a, b) => b[1].count - a[1].count);
339
+ sections.push(`\n### Event Categories`);
340
+ sections.push(`| Category | Events | Size |`);
341
+ sections.push(`|----------|--------|------|`);
342
+ for (const [cat, stats] of sortedCategories.slice(0, 10)) {
343
+ sections.push(`| ${cat} | ${stats.count.toLocaleString()} | ${formatBytes(stats.size)} |`);
344
+ }
345
+ if (summary.issues.length > 0) {
346
+ sections.push(`\n### Issues Detected`);
347
+ for (const issue of summary.issues) {
348
+ sections.push(`- ${issue}`);
349
+ }
350
+ }
351
+ if (summary.recommendations.length > 0) {
352
+ sections.push(`\n### Recommendations`);
353
+ for (const rec of summary.recommendations) {
354
+ sections.push(`- ${rec}`);
355
+ }
356
+ }
357
+ return { content: [{ type: "text", text: sections.join("\n") }] };
358
+ }
359
+ catch (err) {
360
+ return {
361
+ content: [{ type: "text", text: `Error analyzing JFR summary: ${err instanceof Error ? err.message : String(err)}` }],
362
+ };
363
+ }
364
+ });
365
+ // --- Tool: diagnose_jvm ---
366
+ server.tool("diagnose_jvm", "Unified JVM diagnosis combining thread dump and GC log analysis. Provide one or both inputs for comprehensive root cause analysis.", {
367
+ thread_dump: z
368
+ .string()
369
+ .optional()
370
+ .describe("Thread dump text (from jstack)"),
371
+ gc_log: z
372
+ .string()
373
+ .optional()
374
+ .describe("GC log text (from -Xlog:gc*)"),
375
+ }, async ({ thread_dump, gc_log }) => {
376
+ if (!license.isPro) {
377
+ return {
378
+ content: [{
379
+ type: "text",
380
+ text: formatUpgradePrompt("diagnose_jvm", "Unified JVM diagnosis with:\n" +
381
+ "- Combined thread dump + GC log analysis\n" +
382
+ "- Cross-correlation of GC pauses and thread contention\n" +
383
+ "- Root cause identification\n" +
384
+ "- Prioritized remediation plan"),
385
+ }],
386
+ };
387
+ }
388
+ try {
389
+ if (!thread_dump && !gc_log) {
390
+ return {
391
+ content: [{ type: "text", text: "Please provide at least one of: thread_dump or gc_log" }],
392
+ };
393
+ }
394
+ const sections = [];
395
+ sections.push(`## JVM Diagnostic Report`);
396
+ let threadAnalysis = null;
397
+ let gcAnalysis = null;
398
+ if (thread_dump) {
399
+ const parsed = parseThreadDump(thread_dump);
400
+ const deadlocks = detectDeadlocks(parsed.threads);
401
+ const contention = analyzeContention(parsed.threads);
402
+ threadAnalysis = { parsed, deadlocks, contention };
403
+ sections.push(`\n### Thread Analysis`);
404
+ sections.push(`- Total threads: ${parsed.threads.length}`);
405
+ sections.push(`- Deadlocks: ${deadlocks.length > 0 ? `**${deadlocks.length} DETECTED**` : "None"}`);
406
+ sections.push(`- Contention hotspots: ${contention.hotspots.length}`);
407
+ const blockedCount = parsed.threads.filter(t => t.state === "BLOCKED").length;
408
+ const waitingCount = parsed.threads.filter(t => t.state === "WAITING" || t.state === "TIMED_WAITING").length;
409
+ sections.push(`- Blocked threads: ${blockedCount}`);
410
+ sections.push(`- Waiting threads: ${waitingCount}`);
411
+ }
412
+ if (gc_log) {
413
+ const parsed = parseGcLog(gc_log);
414
+ const pressure = analyzeGcPressure(parsed);
415
+ gcAnalysis = { parsed, pressure };
416
+ sections.push(`\n### GC Analysis`);
417
+ sections.push(`- Algorithm: ${parsed.algorithm}`);
418
+ sections.push(`- Events: ${parsed.events.length}`);
419
+ sections.push(`- Max pause: ${pressure.maxPauseMs.toFixed(1)} ms`);
420
+ sections.push(`- GC overhead: ${pressure.gcOverheadPct.toFixed(1)}%`);
421
+ sections.push(`- Issues: ${pressure.issues.length}`);
422
+ }
423
+ // Cross-correlation
424
+ if (threadAnalysis && gcAnalysis) {
425
+ sections.push(`\n### Cross-Correlation`);
426
+ const gcThreads = threadAnalysis.parsed.threads.filter(t => t.name.includes("GC") || t.name.includes("G1") || t.name.includes("ZGC"));
427
+ if (gcThreads.length > 0) {
428
+ sections.push(`- GC-related threads: ${gcThreads.length}`);
429
+ }
430
+ if (gcAnalysis.pressure.gcOverheadPct > 10 && threadAnalysis.contention.hotspots.length > 0) {
431
+ sections.push(`- **Warning**: High GC overhead (${gcAnalysis.pressure.gcOverheadPct.toFixed(1)}%) combined with lock contention — threads may be blocked during GC pauses`);
432
+ }
433
+ if (gcAnalysis.pressure.maxPauseMs > 500 && threadAnalysis.parsed.threads.filter(t => t.state === "BLOCKED").length > 5) {
434
+ sections.push(`- **Warning**: Long GC pauses (${gcAnalysis.pressure.maxPauseMs.toFixed(0)}ms) with many blocked threads — GC may be causing cascading blocks`);
435
+ }
436
+ }
437
+ // Overall assessment
438
+ sections.push(`\n### Overall Assessment`);
439
+ const issues = [];
440
+ if (threadAnalysis?.deadlocks.length)
441
+ issues.push(`${threadAnalysis.deadlocks.length} deadlock(s) — application may be hung`);
442
+ if (threadAnalysis && threadAnalysis.contention.hotspots.length > 3)
443
+ issues.push("Significant lock contention detected");
444
+ if (gcAnalysis?.pressure.gcOverheadPct && gcAnalysis.pressure.gcOverheadPct > 15)
445
+ issues.push("High GC overhead — consider heap tuning");
446
+ if (gcAnalysis?.pressure.maxPauseMs && gcAnalysis.pressure.maxPauseMs > 1000)
447
+ issues.push("GC pauses exceed 1 second — latency impact");
448
+ if (issues.length === 0) {
449
+ sections.push(`JVM appears healthy based on provided diagnostics.`);
450
+ }
451
+ else {
452
+ sections.push(`**${issues.length} issue(s) found:**`);
453
+ for (const issue of issues) {
454
+ sections.push(`- ${issue}`);
455
+ }
456
+ }
457
+ return { content: [{ type: "text", text: sections.join("\n") }] };
458
+ }
459
+ catch (err) {
460
+ return {
461
+ content: [{ type: "text", text: `Error diagnosing JVM: ${err instanceof Error ? err.message : String(err)}` }],
462
+ };
463
+ }
464
+ });
465
+ // --- Start server ---
466
+ async function main() {
467
+ console.error("MCP JVM Diagnostics running on stdio");
468
+ const transport = new StdioServerTransport();
469
+ await server.connect(transport);
470
+ }
471
+ main().catch((error) => {
472
+ console.error("Fatal error:", error);
473
+ process.exit(1);
474
+ });
@@ -0,0 +1,114 @@
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
+ }