mcp-jvm-diagnostics 0.1.5 → 0.1.7

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 CHANGED
@@ -22,6 +22,19 @@ It analyzes **offline** artifacts (thread dumps, GC logs, heap histograms) rathe
22
22
  - **Supports** G1, ZGC, Parallel, Serial, and Shenandoah GC formats
23
23
  - **No external dependencies** — works on local text input, no API keys needed
24
24
 
25
+ ## Pro Tier
26
+
27
+ **Generate exportable diagnostic reports (HTML + PDF)** with a Pro license key.
28
+
29
+ - Full JVM thread dump analysis report with actionable recommendations
30
+ - PDF export for sharing with your team
31
+ - Priority support
32
+
33
+ <!-- TODO: replace placeholder Stripe Payment Link once STRIPE_SECRET_KEY is configured -->
34
+ **$9.99/month** — [Get Pro License](https://buy.stripe.com/PLACEHOLDER)
35
+
36
+ Pro license key activates the `generate_report` MCP tool in mcp-jvm-diagnostics.
37
+
25
38
  ## Installation
26
39
 
27
40
  ```bash
@@ -63,11 +63,20 @@ export function detectDeadlocks(threads) {
63
63
  for (const t of cycleThreads) {
64
64
  visited.add(t.name);
65
65
  }
66
- const dlThreads = cycleThreads.map(t => ({
67
- name: t.name,
68
- holdsLock: t.holdsLocks[0] || "unknown",
69
- waitingOn: t.waitingOn || "unknown",
70
- }));
66
+ const dlThreads = cycleThreads.map((t, i) => {
67
+ // Use the lock this thread holds that the next thread in the cycle is waiting on.
68
+ // This ensures we report the lock that actually forms the deadlock cycle, not
69
+ // an arbitrary first lock (threads may hold multiple locks).
70
+ const nextThread = cycleThreads[(i + 1) % cycleThreads.length];
71
+ const cycleLock = t.holdsLocks.find(lock => lock === nextThread.waitingOn) ??
72
+ t.holdsLocks[0] ??
73
+ "unknown";
74
+ return {
75
+ name: t.name,
76
+ holdsLock: cycleLock,
77
+ waitingOn: t.waitingOn || "unknown",
78
+ };
79
+ });
71
80
  deadlocks.push({
72
81
  threads: dlThreads,
73
82
  recommendation: generateRecommendation(cycleThreads),
@@ -26,7 +26,16 @@ export function analyzeGcPressure(log) {
26
26
  result.maxPauseMs = pauses[pauses.length - 1];
27
27
  result.totalPauseMs = pauses.reduce((a, b) => a + b, 0);
28
28
  result.avgPauseMs = result.totalPauseMs / pauses.length;
29
- result.p95PauseMs = pauses[Math.floor(pauses.length * 0.95)] || result.maxPauseMs;
29
+ // Linear interpolation for P95: Math.floor(n * 0.95) returns index n-1 whenever
30
+ // n <= 20, making P95 equal the max. Using (n-1) * 0.95 as the fractional index
31
+ // and interpolating between adjacent elements gives accurate results at all sizes.
32
+ const p95Idx = (pauses.length - 1) * 0.95;
33
+ const p95Lo = Math.floor(p95Idx);
34
+ const p95Hi = Math.ceil(p95Idx);
35
+ result.p95PauseMs =
36
+ p95Lo === p95Hi
37
+ ? pauses[p95Lo]
38
+ : pauses[p95Lo] + (pauses[p95Hi] - pauses[p95Lo]) * (p95Idx - p95Lo);
30
39
  // GC overhead
31
40
  if (log.timeSpanMs > 0) {
32
41
  result.gcOverheadPct = (result.totalPauseMs / log.timeSpanMs) * 100;
@@ -70,8 +79,10 @@ function detectIssues(log, result) {
70
79
  result.issues.push(`Heap after GC is growing over time (${avgFirst.toFixed(0)}MB → ${avgSecond.toFixed(0)}MB) — possible memory leak.`);
71
80
  }
72
81
  }
73
- // Low reclaim ratio
74
- if (result.heapBeforeMb > 0 && result.heapAfterMb > 0) {
82
+ // Low reclaim ratio — only meaningful when heap actually shrank after GC.
83
+ // Guard heapAfterMb < heapBeforeMb to avoid a negative reclaim percentage when
84
+ // averaged heap-after exceeds heap-before (e.g. growing heap events skewing averages).
85
+ if (result.heapBeforeMb > 0 && result.heapAfterMb > 0 && result.heapAfterMb < result.heapBeforeMb) {
75
86
  const reclaimPct = ((result.heapBeforeMb - result.heapAfterMb) / result.heapBeforeMb) * 100;
76
87
  if (reclaimPct < 10) {
77
88
  result.issues.push(`GC reclaims only ${reclaimPct.toFixed(0)}% of heap per collection — most objects survive. Heap may be too small or there's a memory leak.`);
@@ -97,8 +108,8 @@ function generateRecommendations(log, result) {
97
108
  if (fullGcCount > 3) {
98
109
  result.recommendations.push("Frequent Full GCs indicate heap pressure. Increase -Xmx or tune -XX:InitiatingHeapOccupancyPercent (G1) to start concurrent marking earlier.");
99
110
  }
100
- // Low reclaim → check for leaks
101
- if (result.heapBeforeMb > 0) {
111
+ // Low reclaim → check for leaks (same guard as detectIssues: skip when averages are inverted)
112
+ if (result.heapBeforeMb > 0 && result.heapAfterMb < result.heapBeforeMb) {
102
113
  const reclaimPct = ((result.heapBeforeMb - result.heapAfterMb) / result.heapBeforeMb) * 100;
103
114
  if (reclaimPct < 10) {
104
115
  result.recommendations.push("Very low reclaim ratio suggests most objects are long-lived. Take a heap dump and analyze with Eclipse MAT or jmap -histo to identify memory leaks.");
package/build/index.js CHANGED
@@ -10,9 +10,10 @@ 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 { generateReportFromThreadDump, analyzeThreadDumpMarkdown, } from "./reporting.js";
13
14
  // Handle --help
14
15
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
15
- console.log(`mcp-jvm-diagnostics v0.1.5 — MCP server for JVM diagnostics
16
+ console.log(`mcp-jvm-diagnostics v0.1.7 — MCP server for JVM diagnostics
16
17
 
17
18
  Usage:
18
19
  mcp-jvm-diagnostics [options]
@@ -26,12 +27,13 @@ Tools provided:
26
27
  analyze_heap_histo Parse jmap -histo output, detect memory leak candidates
27
28
  compare_heap_histos Compare two jmap histos to detect memory growth
28
29
  analyze_jfr Parse JFR summary output, detect performance hotspots
29
- diagnose_jvm Unified diagnosis from thread dump + GC log`);
30
+ diagnose_jvm Unified diagnosis from thread dump + GC log
31
+ generate_report Generate an exportable HTML + PDF diagnostic report (Pro)`);
30
32
  process.exit(0);
31
33
  }
32
34
  const server = new McpServer({
33
35
  name: "mcp-jvm-diagnostics",
34
- version: "0.1.4",
36
+ version: "0.1.7",
35
37
  });
36
38
  // --- Tool: analyze_thread_dump ---
37
39
  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. Handles both platform threads and virtual threads (Java 21+).", {
@@ -40,83 +42,57 @@ server.tool("analyze_thread_dump", "Parse a JVM thread dump (jstack output) and
40
42
  .describe("The full thread dump text (from jstack, kill -3, or VisualVM)"),
41
43
  }, async ({ thread_dump }) => {
42
44
  try {
43
- const parsed = parseThreadDump(thread_dump);
44
- const deadlocks = detectDeadlocks(parsed.threads);
45
- const contention = analyzeContention(parsed.threads);
46
- const sections = [];
47
- // Summary
48
- sections.push(`## Thread Dump Analysis`);
49
- sections.push(`\n- **JVM**: ${parsed.jvmInfo || "Unknown"}`);
50
- sections.push(`- **Timestamp**: ${parsed.timestamp || "Unknown"}`);
51
- sections.push(`- **Total threads**: ${parsed.threads.length}`);
52
- // Thread state breakdown
53
- const stateCounts = new Map();
54
- for (const t of parsed.threads) {
55
- stateCounts.set(t.state, (stateCounts.get(t.state) || 0) + 1);
56
- }
57
- // Ensure all canonical Java thread states appear
58
- const canonicalStates = ["RUNNABLE", "WAITING", "TIMED_WAITING", "BLOCKED", "NEW", "TERMINATED"];
59
- for (const s of canonicalStates) {
60
- if (!stateCounts.has(s))
61
- stateCounts.set(s, 0);
62
- }
63
- const total = parsed.threads.length;
64
- const maxCount = Math.max(...stateCounts.values(), 1);
65
- const barMaxWidth = 20;
66
- sections.push(`\n### Thread State Summary`);
67
- sections.push(`| State | Count | % | Histogram |`);
68
- sections.push(`|-------|------:|--:|-----------|`);
69
- // Sort: non-zero descending by count, then zero-count states in canonical order
70
- const sorted = [...stateCounts.entries()].sort((a, b) => {
71
- if (a[1] !== b[1])
72
- return b[1] - a[1];
73
- return canonicalStates.indexOf(a[0]) - canonicalStates.indexOf(b[0]);
45
+ return {
46
+ content: [{ type: "text", text: analyzeThreadDumpMarkdown(thread_dump) }],
47
+ };
48
+ }
49
+ catch (err) {
50
+ return {
51
+ content: [{ type: "text", text: `Error analyzing thread dump: ${err instanceof Error ? err.message : String(err)}` }],
52
+ };
53
+ }
54
+ });
55
+ // --- Tool: generate_report ---
56
+ server.tool("generate_report", "Generate an exportable diagnostic report (HTML + PDF) from JVM diagnostic data. Requires a valid Pro license key.", {
57
+ license_key: z
58
+ .string()
59
+ .describe("A valid MCP JVM Diagnostics Pro license key"),
60
+ thread_dump: z
61
+ .string()
62
+ .describe("The full thread dump text to analyze and export"),
63
+ }, async ({ license_key, thread_dump }) => {
64
+ try {
65
+ const result = await generateReportFromThreadDump({
66
+ licenseKey: license_key,
67
+ threadDump: thread_dump,
74
68
  });
75
- for (const [state, count] of sorted) {
76
- const pct = total > 0 ? ((count / total) * 100).toFixed(1) : "0.0";
77
- const barLen = Math.round((count / maxCount) * barMaxWidth);
78
- const bar = "\u2588".repeat(barLen);
79
- sections.push(`| ${state} | ${count} | ${pct} | \`${bar}\` |`);
80
- }
81
- // Deadlocks
82
- if (deadlocks.length > 0) {
83
- sections.push(`\n### Deadlocks Detected (${deadlocks.length})`);
84
- for (const dl of deadlocks) {
85
- sections.push(`\n**Deadlock cycle** (${dl.threads.length} threads):`);
86
- for (const t of dl.threads) {
87
- sections.push(`- **${t.name}** holds \`${t.holdsLock}\`, waiting for \`${t.waitingOn}\``);
88
- }
89
- sections.push(`\n**Resolution**: ${dl.recommendation}`);
90
- }
91
- }
92
- else {
93
- sections.push(`\n### Deadlocks: None detected`);
94
- }
95
- // Contention
96
- if (contention.hotspots.length > 0) {
97
- sections.push(`\n### Lock Contention Hotspots`);
98
- sections.push(`| Lock | Blocked Threads | Holder |`);
99
- sections.push(`|------|----------------|--------|`);
100
- for (const h of contention.hotspots) {
101
- sections.push(`| \`${h.lock}\` | ${h.blockedCount} | ${h.holderThread} |`);
102
- }
103
- if (contention.recommendations.length > 0) {
104
- sections.push(`\n### Recommendations`);
105
- for (const rec of contention.recommendations) {
106
- sections.push(`- ${rec}`);
107
- }
108
- }
69
+ if (!result.ok) {
70
+ return {
71
+ content: [{ type: "text", text: result.error }],
72
+ };
109
73
  }
110
- // Daemon vs non-daemon
111
- const daemonCount = parsed.threads.filter(t => t.isDaemon).length;
112
- sections.push(`\n### Thread Classification`);
113
- sections.push(`- Daemon threads: ${daemonCount}`);
114
- sections.push(`- Non-daemon threads: ${parsed.threads.length - daemonCount}`);
115
- return { content: [{ type: "text", text: sections.join("\n") }] };
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: [
79
+ "Exportable diagnostic report generated successfully.",
80
+ `HTML: ${result.htmlPath}`,
81
+ `PDF: ${result.pdfPath}`,
82
+ `Customer ID: ${result.customerId ?? "Unknown"}`,
83
+ ].join("\n"),
84
+ },
85
+ ],
86
+ };
116
87
  }
117
88
  catch (err) {
118
89
  return {
119
- content: [{ type: "text", text: `Error analyzing thread dump: ${err instanceof Error ? err.message : String(err)}` }],
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: `Error generating report: ${err instanceof Error ? err.message : String(err)}`,
94
+ },
95
+ ],
120
96
  };
121
97
  }
122
98
  });
@@ -136,6 +112,9 @@ server.tool("analyze_gc_log", "Parse a JVM GC log and analyze garbage collection
136
112
  sections.push(`- **Time span**: ${parsed.timeSpanMs > 0 ? (parsed.timeSpanMs / 1000).toFixed(1) + "s" : "N/A"}`);
137
113
  // Pause time stats
138
114
  if (parsed.events.length > 0) {
115
+ const gcOverheadStr = parsed.hasTimestamps
116
+ ? `${pressure.gcOverheadPct.toFixed(1)}%`
117
+ : "N/A (legacy format has no timestamps)";
139
118
  sections.push(`\n### Pause Time Statistics`);
140
119
  sections.push(`| Metric | Value |`);
141
120
  sections.push(`|--------|-------|`);
@@ -144,7 +123,7 @@ server.tool("analyze_gc_log", "Parse a JVM GC log and analyze garbage collection
144
123
  sections.push(`| Avg pause | ${pressure.avgPauseMs.toFixed(1)} ms |`);
145
124
  sections.push(`| P95 pause | ${pressure.p95PauseMs.toFixed(1)} ms |`);
146
125
  sections.push(`| Total pause time | ${pressure.totalPauseMs.toFixed(0)} ms |`);
147
- sections.push(`| GC overhead | ${pressure.gcOverheadPct.toFixed(1)}% |`);
126
+ sections.push(`| GC overhead | ${gcOverheadStr} |`);
148
127
  }
149
128
  // Heap sizing
150
129
  if (pressure.heapBeforeMb > 0) {
@@ -303,10 +282,10 @@ function formatBytes(bytes) {
303
282
  return `${sign}${abs} B`;
304
283
  }
305
284
  // --- Tool: analyze_jfr ---
306
- 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.", {
285
+ server.tool("analyze_jfr", "Parse JDK Flight Recorder summary output and analyze event distribution, detect performance hotspots, GC pressure, lock contention, I/O patterns, and excessive allocations. Input must be the text printed to stdout by `jfr summary <recording.jfr>` — not the binary .jfr file itself.", {
307
286
  jfr_summary: z
308
287
  .string()
309
- .describe("The output text from `jfr summary <recording.jfr>`"),
288
+ .describe("The stdout text from running `jfr summary <recording.jfr>` (a text table of event types, counts, and sizes — not the binary .jfr file)"),
310
289
  }, async ({ jfr_summary }) => {
311
290
  try {
312
291
  const summary = parseJfrSummary(jfr_summary);
@@ -408,7 +387,10 @@ server.tool("diagnose_jvm", "Unified JVM diagnosis combining thread dump and GC
408
387
  sections.push(`- Algorithm: ${parsed.algorithm}`);
409
388
  sections.push(`- Events: ${parsed.events.length}`);
410
389
  sections.push(`- Max pause: ${pressure.maxPauseMs.toFixed(1)} ms`);
411
- sections.push(`- GC overhead: ${pressure.gcOverheadPct.toFixed(1)}%`);
390
+ const overheadLabel = parsed.hasTimestamps
391
+ ? `${pressure.gcOverheadPct.toFixed(1)}%`
392
+ : "N/A (no timestamps in legacy format)";
393
+ sections.push(`- GC overhead: ${overheadLabel}`);
412
394
  sections.push(`- Issues: ${pressure.issues.length}`);
413
395
  }
414
396
  // Cross-correlation
@@ -126,10 +126,12 @@ export function parseGcLog(text) {
126
126
  }
127
127
  // Calculate time span
128
128
  let timeSpanMs = 0;
129
- if (events.length > 1) {
129
+ // Legacy -verbose:gc events have timestamp=0; unified logging events have real timestamps.
130
+ const hasTimestamps = events.length > 0 && events.some(e => e.timestamp > 0);
131
+ if (hasTimestamps && events.length > 1) {
130
132
  const firstTs = events[0].timestamp;
131
133
  const lastTs = events[events.length - 1].timestamp;
132
134
  timeSpanMs = (lastTs - firstTs) * 1000;
133
135
  }
134
- return { algorithm, events, timeSpanMs };
136
+ return { algorithm, events, timeSpanMs, hasTimestamps };
135
137
  }
@@ -0,0 +1,155 @@
1
+ import { mkdtemp, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { generateDiagnosticPdf, maskLicenseKey, renderDiagnosticReport, validateLicense, } from "@mcp-java-suite/license";
5
+ import { analyzeContention } from "./analyzers/contention.js";
6
+ import { detectDeadlocks } from "./analyzers/deadlock.js";
7
+ import { parseThreadDump } from "./parsers/thread-dump.js";
8
+ const PRODUCT_NAME = "jvm-diagnostics";
9
+ const TOOL_VERSION = "0.1.6";
10
+ export const MISSING_LICENSE_INPUT_ERROR = "Missing required input: license_key";
11
+ export const MISSING_THREAD_DUMP_ERROR = "Missing required input: thread_dump";
12
+ export const MISSING_ENV_LICENSE_ERROR = "MCP_LICENSE_KEY environment variable is not set. generate_report requires a valid Pro license key. Free-tier analysis tools remain available without it.";
13
+ function isGenerateReportFailure(result) {
14
+ return "ok" in result && result.ok === false;
15
+ }
16
+ export function analyzeThreadDumpMarkdown(threadDump) {
17
+ const parsed = parseThreadDump(threadDump);
18
+ const deadlocks = detectDeadlocks(parsed.threads);
19
+ const contention = analyzeContention(parsed.threads);
20
+ const sections = [];
21
+ sections.push("## Thread Dump Analysis");
22
+ sections.push(`\n- **JVM**: ${parsed.jvmInfo || "Unknown"}`);
23
+ sections.push(`- **Timestamp**: ${parsed.timestamp || "Unknown"}`);
24
+ sections.push(`- **Total threads**: ${parsed.threads.length}`);
25
+ const stateCounts = new Map();
26
+ for (const thread of parsed.threads) {
27
+ stateCounts.set(thread.state, (stateCounts.get(thread.state) || 0) + 1);
28
+ }
29
+ const canonicalStates = [
30
+ "RUNNABLE",
31
+ "WAITING",
32
+ "TIMED_WAITING",
33
+ "BLOCKED",
34
+ "NEW",
35
+ "TERMINATED",
36
+ ];
37
+ for (const state of canonicalStates) {
38
+ if (!stateCounts.has(state)) {
39
+ stateCounts.set(state, 0);
40
+ }
41
+ }
42
+ const total = parsed.threads.length;
43
+ const maxCount = Math.max(...stateCounts.values(), 1);
44
+ const barMaxWidth = 20;
45
+ sections.push("\n### Thread State Summary");
46
+ sections.push("| State | Count | % | Histogram |");
47
+ sections.push("|-------|------:|--:|-----------|");
48
+ const sortedStates = [...stateCounts.entries()].sort((a, b) => {
49
+ if (a[1] !== b[1]) {
50
+ return b[1] - a[1];
51
+ }
52
+ return canonicalStates.indexOf(a[0]) - canonicalStates.indexOf(b[0]);
53
+ });
54
+ for (const [state, count] of sortedStates) {
55
+ const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : "0.0";
56
+ const barLength = Math.round((count / maxCount) * barMaxWidth);
57
+ const bar = "\u2588".repeat(barLength);
58
+ sections.push(`| ${state} | ${count} | ${percentage} | \`${bar}\` |`);
59
+ }
60
+ if (deadlocks.length > 0) {
61
+ sections.push(`\n### Deadlocks Detected (${deadlocks.length})`);
62
+ for (const deadlock of deadlocks) {
63
+ sections.push(`\n**Deadlock cycle** (${deadlock.threads.length} threads):`);
64
+ for (const thread of deadlock.threads) {
65
+ sections.push(`- **${thread.name}** holds \`${thread.holdsLock}\`, waiting for \`${thread.waitingOn}\``);
66
+ }
67
+ sections.push(`\n**Resolution**: ${deadlock.recommendation}`);
68
+ }
69
+ }
70
+ else {
71
+ sections.push("\n### Deadlocks: None detected");
72
+ }
73
+ if (contention.hotspots.length > 0) {
74
+ sections.push("\n### Lock Contention Hotspots");
75
+ sections.push("| Lock | Blocked Threads | Holder |");
76
+ sections.push("|------|----------------|--------|");
77
+ for (const hotspot of contention.hotspots) {
78
+ sections.push(`| \`${hotspot.lock}\` | ${hotspot.blockedCount} | ${hotspot.holderThread} |`);
79
+ }
80
+ if (contention.recommendations.length > 0) {
81
+ sections.push("\n### Recommendations");
82
+ for (const recommendation of contention.recommendations) {
83
+ sections.push(`- ${recommendation}`);
84
+ }
85
+ }
86
+ }
87
+ const daemonCount = parsed.threads.filter((thread) => thread.isDaemon).length;
88
+ sections.push("\n### Thread Classification");
89
+ sections.push(`- Daemon threads: ${daemonCount}`);
90
+ sections.push(`- Non-daemon threads: ${parsed.threads.length - daemonCount}`);
91
+ return sections.join("\n");
92
+ }
93
+ function validateConfiguredLicense() {
94
+ const configuredKey = process.env.MCP_LICENSE_KEY;
95
+ if (!configuredKey || configuredKey.trim().length === 0) {
96
+ return { ok: false, error: MISSING_ENV_LICENSE_ERROR };
97
+ }
98
+ return validateProLicense(configuredKey, "Configured MCP_LICENSE_KEY");
99
+ }
100
+ function validateProLicense(key, label) {
101
+ try {
102
+ const license = validateLicense(key, PRODUCT_NAME);
103
+ if (!license.isPro) {
104
+ return {
105
+ ok: false,
106
+ error: `${label} is invalid: ${license.reason}`,
107
+ };
108
+ }
109
+ return license;
110
+ }
111
+ catch (error) {
112
+ return {
113
+ ok: false,
114
+ error: `License validation unavailable: ${error instanceof Error ? error.message : String(error)}`,
115
+ };
116
+ }
117
+ }
118
+ export async function generateReportFromThreadDump(params) {
119
+ if (!params.licenseKey || params.licenseKey.trim().length === 0) {
120
+ return { ok: false, error: MISSING_LICENSE_INPUT_ERROR };
121
+ }
122
+ if (!params.threadDump || params.threadDump.trim().length === 0) {
123
+ return { ok: false, error: MISSING_THREAD_DUMP_ERROR };
124
+ }
125
+ const configuredLicense = validateConfiguredLicense();
126
+ if (isGenerateReportFailure(configuredLicense)) {
127
+ return configuredLicense;
128
+ }
129
+ const providedLicense = validateProLicense(params.licenseKey, "Provided license_key");
130
+ if (isGenerateReportFailure(providedLicense)) {
131
+ return providedLicense;
132
+ }
133
+ const reportMarkdown = analyzeThreadDumpMarkdown(params.threadDump);
134
+ const reportParams = {
135
+ customerName: `Pro Customer #${providedLicense.customerId ?? "Unknown"}`,
136
+ licenseKeyMasked: maskLicenseKey(params.licenseKey),
137
+ reportDate: new Date().toISOString().slice(0, 10),
138
+ toolVersion: TOOL_VERSION,
139
+ reportMarkdown,
140
+ };
141
+ const outputDir = params.outputDir ??
142
+ (await mkdtemp(path.join(tmpdir(), "mcp-jvm-diagnostics-report-")));
143
+ const htmlPath = path.join(outputDir, "diagnostic-report.html");
144
+ const pdfPath = path.join(outputDir, "diagnostic-report.pdf");
145
+ const html = renderDiagnosticReport(reportParams);
146
+ await writeFile(htmlPath, html, "utf8");
147
+ const pdf = await generateDiagnosticPdf(reportParams);
148
+ await writeFile(pdfPath, pdf);
149
+ return {
150
+ ok: true,
151
+ htmlPath,
152
+ pdfPath,
153
+ customerId: providedLicense.customerId,
154
+ };
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-jvm-diagnostics",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "MCP server for JVM diagnostics — analyze thread dumps, detect deadlocks, parse GC logs, and get JVM tuning recommendations",
5
5
  "mcpName": "io.github.dmitriusan/mcp-jvm-diagnostics",
6
6
  "author": "Dmytro Lisnichenko",
@@ -53,6 +53,7 @@
53
53
  "url": "https://github.com/Dmitriusan/mcp-jvm-diagnostics/issues"
54
54
  },
55
55
  "dependencies": {
56
+ "@mcp-java-suite/license": "npm:mcp-java-suite-license@^0.1.0",
56
57
  "@modelcontextprotocol/sdk": "^1.27.1",
57
58
  "zod": "^3.24.2"
58
59
  },