mcp-db-analyzer 0.2.6 → 0.2.8

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
  }
@@ -172,7 +172,8 @@ async function analyzeMysqlConnections() {
172
172
  lines.push("| ID | User | Duration (s) | State | Query |");
173
173
  lines.push("|-----|------|-------------|-------|-------|");
174
174
  for (const row of longQueries.rows) {
175
- lines.push(`| ${row.id} | ${row.user} | ${row.time} | ${row.state} | ${row.info} |`);
175
+ const info = row.info ? row.info.replace(/\|/g, "\\|") : "-";
176
+ lines.push(`| ${row.id} | ${row.user} | ${row.time} | ${row.state} | ${info} |`);
176
177
  }
177
178
  lines.push("");
178
179
  }
@@ -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
  }
@@ -11,6 +11,9 @@ export function createSqliteAdapter() {
11
11
  }
12
12
  db = new Database(dbPath, { readonly: true });
13
13
  db.pragma("journal_mode = WAL");
14
+ // Enforce read-only at the SQL engine level, not just at the file level.
15
+ // This prevents any write attempt from succeeding even if the OS permits it.
16
+ db.pragma("query_only = ON");
14
17
  }
15
18
  return db;
16
19
  }
package/build/errors.js CHANGED
@@ -4,7 +4,11 @@
4
4
  */
5
5
  export function formatToolError(context, err) {
6
6
  const msg = err instanceof Error ? err.message : String(err);
7
- const sanitized = msg.replace(/\/\/[^@]+@/g, "//****:****@");
7
+ // Sanitize URL-style credentials (postgresql://user:pass@host) and
8
+ // key-value style passwords (password=secret) used by libpq and JDBC.
9
+ const sanitized = msg
10
+ .replace(/\/\/[^@]+@/g, "//****:****@")
11
+ .replace(/\bpassword\s*=\s*\S+/gi, "password=****");
8
12
  const isConnectionError = /ECONNREFUSED|ENOTFOUND|ETIMEDOUT|EHOSTUNREACH|getaddrinfo|connect ECONNRESET|password authentication failed|Access denied|no pg_hba\.conf|connection refused|Connection lost|SQLITE_CANTOPEN/i.test(msg);
9
13
  if (isConnectionError) {
10
14
  return `Error ${context}: ${sanitized}\n\nThis looks like a database connection issue. Check your configuration:\n- Set DATABASE_URL environment variable with a valid connection string\n- Or use driver-specific variables (PGHOST, MYSQL_HOST, SQLITE_PATH)\n- Ensure the database server is running and accessible`;
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.8",
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",