mcp-db-analyzer 0.2.6 → 0.2.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
@@ -21,6 +21,19 @@ Other analytical MCP servers (CrystalDBA, pg-dash, MCP-PostgreSQL-Ops) cover Pos
21
21
  - **Markdown output** optimized for LLM consumption
22
22
  - **Zero configuration** — just set `DATABASE_URL`
23
23
 
24
+ ## Pro Tier
25
+
26
+ **Generate exportable diagnostic reports (HTML + PDF)** with a Pro license key.
27
+
28
+ - Full JVM thread dump analysis report with actionable recommendations
29
+ - PDF export for sharing with your team
30
+ - Priority support
31
+
32
+ <!-- TODO: replace placeholder Stripe Payment Link once STRIPE_SECRET_KEY is configured -->
33
+ **$9.99/month** — [Get Pro License](https://buy.stripe.com/PLACEHOLDER)
34
+
35
+ Pro license key activates the `generate_report` MCP tool in mcp-jvm-diagnostics.
36
+
24
37
  ## Installation
25
38
 
26
39
  ```bash
@@ -63,7 +63,7 @@ async function analyzePostgresConnections() {
63
63
  lines.push("| PID | User | Duration | Query |");
64
64
  lines.push("|-----|------|----------|-------|");
65
65
  for (const row of idleTxn.rows) {
66
- lines.push(`| ${row.pid} | ${row.usename} | ${row.duration} | ${row.query} |`);
66
+ lines.push(`| ${row.pid} | ${row.usename} | ${row.duration} | ${row.query.replace(/\|/g, "\\|")} |`);
67
67
  }
68
68
  lines.push("");
69
69
  }
@@ -83,7 +83,7 @@ async function analyzePostgresConnections() {
83
83
  lines.push("| PID | User | Duration | Wait | Query |");
84
84
  lines.push("|-----|------|----------|------|-------|");
85
85
  for (const row of longQueries.rows) {
86
- lines.push(`| ${row.pid} | ${row.usename} | ${row.duration} | ${row.wait_event_type || "-"} | ${row.query} |`);
86
+ lines.push(`| ${row.pid} | ${row.usename} | ${row.duration} | ${row.wait_event_type || "-"} | ${row.query.replace(/\|/g, "\\|")} |`);
87
87
  }
88
88
  lines.push("");
89
89
  }
@@ -112,7 +112,7 @@ async function analyzePostgresConnections() {
112
112
  lines.push("| Blocked PID | Blocking PID | Blocked Query | Blocking Query |");
113
113
  lines.push("|-------------|--------------|---------------|----------------|");
114
114
  for (const row of blocked.rows) {
115
- lines.push(`| ${row.blocked_pid} | ${row.blocking_pid} | ${row.blocked_query} | ${row.blocking_query} |`);
115
+ lines.push(`| ${row.blocked_pid} | ${row.blocking_pid} | ${row.blocked_query.replace(/\|/g, "\\|")} | ${row.blocking_query.replace(/\|/g, "\\|")} |`);
116
116
  }
117
117
  lines.push("");
118
118
  }
@@ -237,14 +237,19 @@ function collectWarnings(node) {
237
237
  if (node["Sort Method"] === "external merge") {
238
238
  warnings.push(`**Disk sort** detected. Increase \`work_mem\` or add an index to avoid sorting.`);
239
239
  }
240
- // Stale statistics: actual rows deviate significantly from planner estimate
241
- if (node["Actual Rows"] !== undefined &&
242
- node["Plan Rows"] > 0 &&
243
- node["Actual Rows"] > 0) {
244
- const ratio = node["Actual Rows"] / node["Plan Rows"];
245
- if (ratio > 10 || ratio < 0.1) {
246
- const relation = node["Relation Name"] ?? node["Node Type"];
247
- warnings.push(`**Stale statistics** on \`${relation}\`: planner estimated ${node["Plan Rows"]} rows but got ${node["Actual Rows"]} (${ratio > 1 ? ratio.toFixed(0) + "× over" : (1 / ratio).toFixed(0) + "× under"}estimate). Run \`ANALYZE ${node["Relation Name"] ?? ""}\` to refresh table statistics.`);
240
+ // Stale statistics: actual rows deviate significantly from planner estimate.
241
+ // Handle Plan Rows = 0 separately — the planner expected an empty result but
242
+ // got rows, which is the most misleading case for query planning.
243
+ if (node["Actual Rows"] !== undefined) {
244
+ const relation = node["Relation Name"] ?? node["Node Type"];
245
+ if (node["Plan Rows"] === 0 && node["Actual Rows"] > 0) {
246
+ warnings.push(`**Stale statistics** on \`${relation}\`: planner estimated 0 rows but got ${node["Actual Rows"]}. Run \`ANALYZE ${node["Relation Name"] ?? ""}\` to refresh table statistics.`);
247
+ }
248
+ else if (node["Plan Rows"] > 0 && node["Actual Rows"] > 0) {
249
+ const ratio = node["Actual Rows"] / node["Plan Rows"];
250
+ if (ratio > 10 || ratio < 0.1) {
251
+ warnings.push(`**Stale statistics** on \`${relation}\`: planner estimated ${node["Plan Rows"]} rows but got ${node["Actual Rows"]} (${ratio > 1 ? ratio.toFixed(0) + "× over" : (1 / ratio).toFixed(0) + "× under"}estimate). Run \`ANALYZE ${node["Relation Name"] ?? ""}\` to refresh table statistics.`);
252
+ }
248
253
  }
249
254
  }
250
255
  if (node.Plans) {
package/build/db-mysql.js CHANGED
@@ -49,7 +49,13 @@ export function createMysqlAdapter() {
49
49
  return { rows: rows };
50
50
  }
51
51
  catch (err) {
52
- await conn.rollback();
52
+ try {
53
+ await conn.rollback();
54
+ }
55
+ catch {
56
+ // Transaction may not have started (e.g. beginTransaction threw);
57
+ // suppress the secondary error so the original cause is preserved.
58
+ }
53
59
  throw err;
54
60
  }
55
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-db-analyzer",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "MCP server for PostgreSQL, MySQL, and SQLite schema analysis, index optimization, and query plan inspection",
5
5
  "mcpName": "io.github.dmitriusan/mcp-db-analyzer",
6
6
  "author": "Dmytro Lisnichenko",