mcp-jvm-diagnostics 0.1.5 → 0.1.6
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 +13 -0
- package/build/analyzers/deadlock.js +14 -5
- package/build/index.js +59 -77
- package/build/parsers/gc-log.js +4 -2
- package/build/reporting.js +155 -0
- package/package.json +2 -1
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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),
|
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.
|
|
16
|
+
console.log(`mcp-jvm-diagnostics v0.1.6 — 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.
|
|
36
|
+
version: "0.1.6",
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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: [
|
|
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 | ${
|
|
126
|
+
sections.push(`| GC overhead | ${gcOverheadStr} |`);
|
|
148
127
|
}
|
|
149
128
|
// Heap sizing
|
|
150
129
|
if (pressure.heapBeforeMb > 0) {
|
|
@@ -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
|
-
|
|
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
|
package/build/parsers/gc-log.js
CHANGED
|
@@ -126,10 +126,12 @@ export function parseGcLog(text) {
|
|
|
126
126
|
}
|
|
127
127
|
// Calculate time span
|
|
128
128
|
let timeSpanMs = 0;
|
|
129
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.6",
|
|
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": "file:../../mcp-suite-license/mcp-suite-license",
|
|
56
57
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
57
58
|
"zod": "^3.24.2"
|
|
58
59
|
},
|