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 +13 -0
- package/build/analyzers/connections.js +5 -4
- package/build/analyzers/query.js +13 -8
- package/build/db-mysql.js +7 -1
- package/build/db-sqlite.js +3 -0
- package/build/errors.js +5 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
}
|
package/build/analyzers/query.js
CHANGED
|
@@ -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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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/build/db-sqlite.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|