mcp-db-analyzer 0.2.4 → 0.2.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 CHANGED
@@ -376,6 +376,13 @@ Analyze PostgreSQL VACUUM maintenance status. Checks dead tuple ratios, vacuum s
376
376
  - **Connection analysis**: `analyze_connections` is PostgreSQL/MySQL only. Not available for SQLite databases.
377
377
  - **Vacuum analysis**: `analyze_vacuum` is PostgreSQL only. For MySQL, use `OPTIMIZE TABLE` or `analyze_table_bloat`.
378
378
 
379
+ ## Part of the MCP Java Backend Suite
380
+
381
+ - [mcp-spring-boot-actuator](https://www.npmjs.com/package/mcp-spring-boot-actuator) — Spring Boot health, metrics, and bean analysis
382
+ - [mcp-jvm-diagnostics](https://www.npmjs.com/package/mcp-jvm-diagnostics) — Thread dump and GC log analysis
383
+ - [mcp-redis-diagnostics](https://www.npmjs.com/package/mcp-redis-diagnostics) — Redis memory, slowlog, and client diagnostics
384
+ - [mcp-migration-advisor](https://www.npmjs.com/package/mcp-migration-advisor) — Flyway/Liquibase migration risk analysis
385
+
379
386
  ## License
380
387
 
381
388
  MIT
@@ -10,7 +10,9 @@ export async function explainQuery(sql, analyze = false) {
10
10
  // EXPLAIN ANALYZE actually executes the query, so we must reject anything
11
11
  // that could modify data — including CTEs with write operations.
12
12
  if (analyze) {
13
- const upperSql = sql.trim().toUpperCase();
13
+ // Strip single-quoted string literals before scanning for DML keywords so that
14
+ // a query like `SELECT ... WHERE status = 'DELETE me'` is not falsely rejected.
15
+ const upperSql = sql.trim().toUpperCase().replace(/'[^']*'/g, "''");
14
16
  const DML_KEYWORDS = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE", "CREATE", "GRANT", "REVOKE", "COPY"];
15
17
  const containsDml = DML_KEYWORDS.some((kw) => upperSql.includes(kw + " ") || upperSql.includes(kw + "\n") || upperSql.includes(kw + "\t") || upperSql.endsWith(kw));
16
18
  if (containsDml) {
@@ -235,6 +237,16 @@ function collectWarnings(node) {
235
237
  if (node["Sort Method"] === "external merge") {
236
238
  warnings.push(`**Disk sort** detected. Increase \`work_mem\` or add an index to avoid sorting.`);
237
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.`);
248
+ }
249
+ }
238
250
  if (node.Plans) {
239
251
  for (const child of node.Plans) {
240
252
  warnings.push(...collectWarnings(child));
@@ -187,13 +187,16 @@ export async function inspectTable(tableName, schema = "public") {
187
187
  return lines.join("\n");
188
188
  }
189
189
  async function inspectTableSqlite(tableName) {
190
- const cols = await query(`PRAGMA table_info("${tableName}")`);
190
+ // Escape double-quote characters so a table name containing `"` (e.g. `weird"table`)
191
+ // does not break the PRAGMA queries or the row-count SELECT.
192
+ const escaped = tableName.replace(/"/g, '""');
193
+ const cols = await query(`PRAGMA table_info("${escaped}")`);
191
194
  if (cols.rows.length === 0) {
192
195
  return `Table '${tableName}' not found.`;
193
196
  }
194
197
  const lines = [`## Table: main.${tableName}\n`];
195
198
  // Row count
196
- const countResult = await query(`SELECT count(*) as cnt FROM "${tableName}"`);
199
+ const countResult = await query(`SELECT count(*) as cnt FROM "${escaped}"`);
197
200
  lines.push(`- **Rows**: ${countResult.rows[0]?.cnt ?? 0}`);
198
201
  lines.push("");
199
202
  lines.push("### Columns\n");
@@ -203,7 +206,7 @@ async function inspectTableSqlite(tableName) {
203
206
  lines.push(`| ${col.cid + 1} | ${col.name} | ${col.type || 'ANY'} | ${col.notnull ? 'NO' : 'YES'} | ${col.dflt_value ?? '-'} | ${col.pk ? 'YES' : '-'} |`);
204
207
  }
205
208
  // Foreign keys
206
- const fks = await query(`PRAGMA foreign_key_list("${tableName}")`);
209
+ const fks = await query(`PRAGMA foreign_key_list("${escaped}")`);
207
210
  if (fks.rows.length > 0) {
208
211
  lines.push("\n### Foreign Keys\n");
209
212
  lines.push("| Column | References |");
@@ -213,13 +216,13 @@ async function inspectTableSqlite(tableName) {
213
216
  }
214
217
  }
215
218
  // Indexes
216
- const indexes = await query(`PRAGMA index_list("${tableName}")`);
219
+ const indexes = await query(`PRAGMA index_list("${escaped}")`);
217
220
  if (indexes.rows.length > 0) {
218
221
  lines.push("\n### Indexes\n");
219
222
  lines.push("| Name | Unique | Columns |");
220
223
  lines.push("|------|--------|---------|");
221
224
  for (const idx of indexes.rows) {
222
- const idxCols = await query(`PRAGMA index_info("${idx.name}")`);
225
+ const idxCols = await query(`PRAGMA index_info("${idx.name.replace(/"/g, '""')}")`);
223
226
  const colNames = idxCols.rows.map(c => c.name).join(", ");
224
227
  lines.push(`| ${idx.name} | ${idx.unique ? 'YES' : 'NO'} | ${colNames} |`);
225
228
  }
@@ -119,6 +119,20 @@ async function analyzeMysqlSlowQueries(limit) {
119
119
  const truncated = r.DIGEST_TEXT.length > 80 ? r.DIGEST_TEXT.slice(0, 77) + "..." : r.DIGEST_TEXT;
120
120
  sections.push(`| ${i + 1} | ${r.AVG_TIMER_WAIT.toFixed(1)}ms | ${r.SUM_TIMER_WAIT.toFixed(0)}ms | ${r.COUNT_STAR} | \`${truncated.replace(/\|/g, "\\|")}\` |`);
121
121
  }
122
+ // Recommendations
123
+ sections.push("");
124
+ sections.push("### Recommendations");
125
+ const highCallSlow = result.rows.filter((r) => r.COUNT_STAR > 100 && r.AVG_TIMER_WAIT > 100);
126
+ if (highCallSlow.length > 0) {
127
+ sections.push(`- **${highCallSlow.length} high-impact queries** — called >100 times with >100ms avg. Prioritize these for optimization.`);
128
+ }
129
+ const fewRowsSlow = result.rows.filter((r) => r.AVG_TIMER_WAIT > 50 && r.SUM_ROWS_SENT / Math.max(r.COUNT_STAR, 1) < 10);
130
+ if (fewRowsSlow.length > 0) {
131
+ sections.push(`- **${fewRowsSlow.length} queries returning few rows but slow** — likely missing indexes. Use \`explain_query\` to check.`);
132
+ }
133
+ if (highCallSlow.length === 0 && fewRowsSlow.length === 0) {
134
+ sections.push("- No critical patterns detected. Monitor trends over time.");
135
+ }
122
136
  return sections.join("\n");
123
137
  }
124
138
  catch {
package/build/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "module";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5
  import { z } from "zod";
6
+ const require = createRequire(import.meta.url);
7
+ const { version: PKG_VERSION } = require("../package.json");
5
8
  import { listTables, inspectTable } from "./analyzers/schema.js";
6
9
  import { analyzeIndexUsage, findMissingIndexes } from "./analyzers/indexes.js";
7
10
  import { explainQuery } from "./analyzers/query.js";
@@ -15,7 +18,7 @@ import { closePool, initDriver, setConnectionTimeoutMs } from "./db.js";
15
18
  import { formatToolError } from "./errors.js";
16
19
  // Handle --help
17
20
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
18
- console.log(`mcp-db-analyzer v0.1.0 — MCP server for database analysis
21
+ console.log(`mcp-db-analyzer v${PKG_VERSION} — MCP server for database analysis
19
22
 
20
23
  Usage:
21
24
  mcp-db-analyzer [options]
@@ -68,7 +71,7 @@ function detectDriver() {
68
71
  }
69
72
  const server = new McpServer({
70
73
  name: "mcp-db-analyzer",
71
- version: "0.1.0",
74
+ version: PKG_VERSION,
72
75
  });
73
76
  // Shared Zod parameter for connection timeout
74
77
  const timeoutParam = z
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "mcp-db-analyzer",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "MCP server for PostgreSQL, MySQL, and SQLite schema analysis, index optimization, and query plan inspection",
5
+ "mcpName": "io.github.dmitriusan/mcp-db-analyzer",
5
6
  "author": "Dmytro Lisnichenko",
6
7
  "type": "module",
7
8
  "main": "./build/index.js",