mcp-db-analyzer 0.2.9 → 0.2.11

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
@@ -30,7 +30,7 @@ Other analytical MCP servers (CrystalDBA, pg-dash, MCP-PostgreSQL-Ops) cover Pos
30
30
  - Priority support
31
31
 
32
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)
33
+ **$9.00/month** — [Get Pro License](https://buy.stripe.com/PLACEHOLDER)
34
34
 
35
35
  Pro license key activates the `generate_report` MCP tool in mcp-jvm-diagnostics.
36
36
 
@@ -162,6 +162,23 @@ async function analyzeMysqlConnections() {
162
162
  }
163
163
  lines.push(`| **Total** | **${total}** |`);
164
164
  lines.push("");
165
+ // 2. Max connections utilization
166
+ try {
167
+ const maxConn = await query(`SELECT @@max_connections AS max_connections`);
168
+ if (maxConn.rows.length > 0) {
169
+ const max = parseInt(maxConn.rows[0].max_connections, 10);
170
+ const utilization = total / max;
171
+ lines.push(`**Max connections**: ${max}`);
172
+ lines.push(`**Utilization**: ${(utilization * 100).toFixed(1)}%`);
173
+ if (utilization > 0.8) {
174
+ lines.push(`\n**WARNING**: Connection pool is ${(utilization * 100).toFixed(0)}% utilized. Consider increasing max_connections or using a connection pooler (e.g. ProxySQL).`);
175
+ }
176
+ lines.push("");
177
+ }
178
+ }
179
+ catch {
180
+ // Supplemental info — skip silently if unavailable
181
+ }
165
182
  const longQueries = await query(`SELECT ID AS id, USER AS user, TIME AS time, STATE AS state, LEFT(INFO, 100) AS info
166
183
  FROM information_schema.PROCESSLIST
167
184
  WHERE COMMAND = 'Query' AND TIME > 30
@@ -72,8 +72,9 @@ async function analyzeIndexUsageMysql(schema) {
72
72
  `, [schema]);
73
73
  return formatIndexUsage(result.rows, schema);
74
74
  }
75
- catch {
76
- return `## Index Usage Analysis schema '${schema}'\n\nUnable to query performance_schema. Ensure performance_schema is enabled (it is ON by default in MySQL 5.7+) and the user has SELECT privilege on performance_schema tables.`;
75
+ catch (err) {
76
+ const detail = err instanceof Error ? err.message : String(err);
77
+ return `## Index Usage Analysis — schema '${schema}'\n\nUnable to query performance_schema. Ensure performance_schema is enabled (it is ON by default in MySQL 5.7+) and the user has SELECT privilege on performance_schema tables.\n\nDetails: ${detail}`;
77
78
  }
78
79
  }
79
80
  function formatIndexUsage(rows, schema) {
@@ -226,8 +227,9 @@ async function findMissingIndexesMysql(schema) {
226
227
  }
227
228
  return lines.join("\n");
228
229
  }
229
- catch {
230
- return "## Missing Index Analysis\n\nUnable to query performance_schema. Ensure performance_schema is enabled (it is ON by default in MySQL 5.7+) and the user has SELECT privilege on performance_schema tables.";
230
+ catch (err) {
231
+ const detail = err instanceof Error ? err.message : String(err);
232
+ return `## Missing Index Analysis\n\nUnable to query performance_schema. Ensure performance_schema is enabled (it is ON by default in MySQL 5.7+) and the user has SELECT privilege on performance_schema tables.\n\nDetails: ${detail}`;
231
233
  }
232
234
  }
233
235
  function formatMissingIndexes(rows, schema) {
@@ -99,6 +99,59 @@ async function analyzeSqliteRelationships() {
99
99
  }
100
100
  return formatRelationshipReport(tablesResult.rows.map(r => r.name), fks);
101
101
  }
102
+ /**
103
+ * Detect cycles in the FK dependency graph using iterative DFS.
104
+ * Returns each unique cycle as an ordered list of table names with the
105
+ * first node repeated at the end to show closure (e.g. ["a","b","c","a"]).
106
+ */
107
+ function findFkCycles(allTables, graph) {
108
+ const visited = new Set();
109
+ const onStack = new Set();
110
+ const stackPath = [];
111
+ const cycles = [];
112
+ const seenCycleKeys = new Set();
113
+ function dfs(node) {
114
+ visited.add(node);
115
+ onStack.add(node);
116
+ stackPath.push(node);
117
+ const nodeData = graph.get(node);
118
+ if (nodeData) {
119
+ for (const fk of nodeData.outgoing) {
120
+ const neighbor = fk.target_table;
121
+ if (onStack.has(neighbor)) {
122
+ // Back edge found — extract the cycle
123
+ const cycleStartIdx = stackPath.indexOf(neighbor);
124
+ if (cycleStartIdx !== -1) {
125
+ const cycleNodes = stackPath.slice(cycleStartIdx);
126
+ // Normalize: rotate so lexicographically smallest node is first
127
+ const minNode = cycleNodes.reduce((a, b) => (a < b ? a : b));
128
+ const minIdx = cycleNodes.indexOf(minNode);
129
+ const normalized = [
130
+ ...cycleNodes.slice(minIdx),
131
+ ...cycleNodes.slice(0, minIdx),
132
+ ];
133
+ const key = normalized.join("\0");
134
+ if (!seenCycleKeys.has(key)) {
135
+ seenCycleKeys.add(key);
136
+ cycles.push([...normalized, normalized[0]]); // close the cycle
137
+ }
138
+ }
139
+ }
140
+ else if (!visited.has(neighbor)) {
141
+ dfs(neighbor);
142
+ }
143
+ }
144
+ }
145
+ stackPath.pop();
146
+ onStack.delete(node);
147
+ }
148
+ for (const table of allTables) {
149
+ if (!visited.has(table)) {
150
+ dfs(table);
151
+ }
152
+ }
153
+ return cycles;
154
+ }
102
155
  function formatRelationshipReport(allTables, foreignKeys) {
103
156
  const lines = [];
104
157
  lines.push("## Table Relationships\n");
@@ -161,30 +214,57 @@ function formatRelationshipReport(allTables, foreignKeys) {
161
214
  lines.push("*Orphan tables may be lookup tables, denormalized tables, or tables missing FK constraints.*");
162
215
  lines.push("");
163
216
  }
217
+ // Circular FK dependencies
218
+ const cycles = findFkCycles(allTables, graph);
219
+ if (cycles.length > 0) {
220
+ lines.push("### Circular FK Dependencies\n");
221
+ lines.push("These tables form circular foreign key references. Cycles complicate cascade operations, " +
222
+ "schema migrations, and may cause issues with certain ORMs and migration tools:\n");
223
+ for (const cycle of cycles) {
224
+ lines.push(`- ${cycle.join(" → ")}`);
225
+ }
226
+ lines.push("\n**Note**: Circular FKs are sometimes intentional (e.g., a 'current_record' pointer back to a parent). " +
227
+ "Review each cycle to confirm the design is correct.\n");
228
+ }
164
229
  // Cascading delete chains
165
230
  const cascadeDeletes = foreignKeys.filter(fk => fk.on_delete === "CASCADE");
166
231
  if (cascadeDeletes.length > 0) {
167
232
  lines.push("### Cascading Delete Chains\n");
168
233
  lines.push("Deleting a row from these parent tables will cascade-delete rows in child tables:\n");
169
- // Group by target (parent) table
170
- const cascadeByParent = new Map();
234
+ // Build parent→children map for cascade edges only
235
+ const cascadeChildren = new Map();
171
236
  for (const fk of cascadeDeletes) {
172
- const existing = cascadeByParent.get(fk.target_table) || [];
173
- existing.push(fk);
174
- cascadeByParent.set(fk.target_table, existing);
237
+ const existing = cascadeChildren.get(fk.target_table) || [];
238
+ if (!existing.includes(fk.source_table))
239
+ existing.push(fk.source_table);
240
+ cascadeChildren.set(fk.target_table, existing);
175
241
  }
176
- for (const [parent, fks] of cascadeByParent) {
177
- const children = fks.map(f => f.source_table).join(", ");
178
- lines.push(`- **${parent}** cascades to: ${children}`);
179
- // Check for deep chains (cascade through multiple levels)
180
- for (const fk of fks) {
181
- const grandchildren = cascadeDeletes.filter(gfk => gfk.target_table === fk.source_table);
182
- if (grandchildren.length > 0) {
183
- const gcNames = grandchildren.map(g => g.source_table).join(", ");
184
- lines.push(` - **${fk.source_table}** further cascades to: ${gcNames}`);
242
+ // Identify root cascade tables: have cascade children but are not cascade children themselves
243
+ const allCascadeChildTables = new Set(cascadeDeletes.map(fk => fk.source_table));
244
+ const cascadeRoots = [...cascadeChildren.keys()].filter(t => !allCascadeChildTables.has(t));
245
+ // Fall back to all parents if every parent is also a child (full cycle)
246
+ const parentsToShow = cascadeRoots.length > 0 ? cascadeRoots : [...cascadeChildren.keys()];
247
+ // Recursively render the cascade chain under a given parent
248
+ function renderCascadeChain(parent, indent, visited) {
249
+ const children = cascadeChildren.get(parent) || [];
250
+ const prefix = " ".repeat(indent);
251
+ for (const child of children) {
252
+ if (visited.has(child)) {
253
+ lines.push(`${prefix}- **${child}** *(circular)*`);
254
+ }
255
+ else if (cascadeChildren.has(child)) {
256
+ lines.push(`${prefix}- **${child}** →`);
257
+ renderCascadeChain(child, indent + 1, new Set([...visited, child]));
258
+ }
259
+ else {
260
+ lines.push(`${prefix}- **${child}**`);
185
261
  }
186
262
  }
187
263
  }
264
+ for (const root of parentsToShow) {
265
+ lines.push(`- **${root}** → cascades to:`);
266
+ renderCascadeChain(root, 1, new Set([root]));
267
+ }
188
268
  lines.push("");
189
269
  lines.push("**WARNING**: Cascading deletes can cause unexpected data loss. Ensure all CASCADE rules are intentional.\n");
190
270
  }
@@ -200,6 +280,9 @@ function formatRelationshipReport(allTables, foreignKeys) {
200
280
  if (hubs.length > 0) {
201
281
  issues.push(`Hub tables (${hubs.map(h => h.name).join(", ")}) have 5+ FK connections — changes to these tables affect many others`);
202
282
  }
283
+ if (cycles.length > 0) {
284
+ issues.push(`${cycles.length} circular FK reference(s) detected — review for unintended schema design`);
285
+ }
203
286
  if (issues.length > 0) {
204
287
  lines.push("### Recommendations\n");
205
288
  for (const issue of issues) {
@@ -127,8 +127,9 @@ async function suggestMissingIndexesMysql(schema) {
127
127
  `, [schema]);
128
128
  return formatSuggestions(needsIndex.rows, unused.rows, schema);
129
129
  }
130
- catch {
131
- return "## Index Suggestions\n\nUnable to query performance_schema. Ensure performance_schema is enabled (it is ON by default in MySQL 5.7+) and the user has SELECT privilege on performance_schema tables.";
130
+ catch (err) {
131
+ const detail = err instanceof Error ? err.message : String(err);
132
+ return `## Index Suggestions\n\nUnable to query performance_schema. Ensure performance_schema is enabled (it is ON by default in MySQL 5.7+) and the user has SELECT privilege on performance_schema tables.\n\nDetails: ${detail}`;
132
133
  }
133
134
  }
134
135
  function formatSuggestions(needsIndex, unused, schema) {
package/build/index.js CHANGED
@@ -248,7 +248,7 @@ server.tool("analyze_slow_queries", "Find the slowest queries using pg_stat_stat
248
248
  }
249
249
  });
250
250
  // Tool 7: analyze_connections
251
- server.tool("analyze_connections", "Analyze active database connections. Detects idle-in-transaction sessions, long-running queries, lock contention, and connection pool utilization. PostgreSQL and MySQL only.", {
251
+ server.tool("analyze_connections", "Analyze active database connections. Detects idle-in-transaction sessions (which hold locks and block other queries), long-running queries (flagged at >30 seconds), lock contention between sessions, and connection pool utilization. PostgreSQL and MySQL only.", {
252
252
  timeout_ms: timeoutParam,
253
253
  }, async ({ timeout_ms }) => {
254
254
  applyTimeout(timeout_ms);
@@ -268,7 +268,7 @@ server.tool("analyze_connections", "Analyze active database connections. Detects
268
268
  }
269
269
  });
270
270
  // Tool 8: analyze_table_relationships
271
- server.tool("analyze_table_relationships", "Analyze foreign key relationships between tables. Builds a dependency graph showing entity connectivity, orphan tables (no FKs), cascading delete chains, and hub entities. Useful for understanding schema design and impact analysis.", {
271
+ server.tool("analyze_table_relationships", "Analyze foreign key relationships between tables. Builds a dependency graph showing entity connectivity, orphan tables (no FKs), cascading delete chains (shown at full depth), hub entities (tables with 5+ FK connections), and circular FK dependencies. Useful for understanding schema design, planning migrations, and impact analysis.", {
272
272
  schema: z
273
273
  .string()
274
274
  .default("public")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-db-analyzer",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",