@berthojoris/mcp-mysql-server 1.10.5 → 1.13.0

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.
@@ -0,0 +1,327 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AnalysisTools = void 0;
7
+ const connection_1 = __importDefault(require("../db/connection"));
8
+ const config_1 = require("../config/config");
9
+ class AnalysisTools {
10
+ constructor(security) {
11
+ this.db = connection_1.default.getInstance();
12
+ this.security = security;
13
+ }
14
+ /**
15
+ * Validate database access - ensures only the connected database can be accessed
16
+ */
17
+ validateDatabaseAccess(requestedDatabase) {
18
+ const connectedDatabase = config_1.dbConfig.database;
19
+ if (!connectedDatabase) {
20
+ return {
21
+ valid: false,
22
+ database: "",
23
+ error: "No database specified in connection string. Cannot access any database.",
24
+ };
25
+ }
26
+ if (!requestedDatabase) {
27
+ return {
28
+ valid: true,
29
+ database: connectedDatabase,
30
+ };
31
+ }
32
+ if (requestedDatabase !== connectedDatabase) {
33
+ return {
34
+ valid: false,
35
+ database: "",
36
+ error: `Access denied. You can only access the connected database '${connectedDatabase}'. Requested database '${requestedDatabase}' is not allowed.`,
37
+ };
38
+ }
39
+ return {
40
+ valid: true,
41
+ database: connectedDatabase,
42
+ };
43
+ }
44
+ /**
45
+ * Get statistics for a specific column
46
+ */
47
+ async getColumnStatistics(params) {
48
+ try {
49
+ const dbValidation = this.validateDatabaseAccess(params?.database);
50
+ if (!dbValidation.valid) {
51
+ return { status: "error", error: dbValidation.error };
52
+ }
53
+ const { table_name, column_name } = params;
54
+ const database = dbValidation.database;
55
+ // Validate names
56
+ if (!this.security.validateIdentifier(table_name).valid) {
57
+ return { status: "error", error: "Invalid table name" };
58
+ }
59
+ if (!this.security.validateIdentifier(column_name).valid) {
60
+ return { status: "error", error: "Invalid column name" };
61
+ }
62
+ // Check if column exists and get its type
63
+ const colCheckQuery = `
64
+ SELECT DATA_TYPE
65
+ FROM INFORMATION_SCHEMA.COLUMNS
66
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?
67
+ `;
68
+ const colCheck = await this.db.query(colCheckQuery, [
69
+ database,
70
+ table_name,
71
+ column_name,
72
+ ]);
73
+ if (colCheck.length === 0) {
74
+ return {
75
+ status: "error",
76
+ error: `Column '${column_name}' not found in table '${table_name}'`,
77
+ };
78
+ }
79
+ const dataType = colCheck[0].DATA_TYPE;
80
+ const isNumeric = [
81
+ "int",
82
+ "tinyint",
83
+ "smallint",
84
+ "mediumint",
85
+ "bigint",
86
+ "float",
87
+ "double",
88
+ "decimal",
89
+ ].includes(dataType);
90
+ const isDate = [
91
+ "date",
92
+ "datetime",
93
+ "timestamp",
94
+ "time",
95
+ "year",
96
+ ].includes(dataType);
97
+ // Build statistics query
98
+ let query = `
99
+ SELECT
100
+ COUNT(*) as total_rows,
101
+ COUNT(\`${column_name}\`) as non_null_count,
102
+ COUNT(DISTINCT \`${column_name}\`) as distinct_count,
103
+ SUM(CASE WHEN \`${column_name}\` IS NULL THEN 1 ELSE 0 END) as null_count
104
+ `;
105
+ if (isNumeric || isDate) {
106
+ query += `,
107
+ MIN(\`${column_name}\`) as min_value,
108
+ MAX(\`${column_name}\`) as max_value
109
+ `;
110
+ }
111
+ if (isNumeric) {
112
+ query += `,
113
+ AVG(\`${column_name}\`) as avg_value
114
+ `;
115
+ }
116
+ query += ` FROM \`${database}\`.\`${table_name}\``;
117
+ const statsResult = await this.db.query(query);
118
+ const stats = statsResult[0];
119
+ // Get top frequent values
120
+ const topValuesQuery = `
121
+ SELECT \`${column_name}\` as value, COUNT(*) as count
122
+ FROM \`${database}\`.\`${table_name}\`
123
+ GROUP BY \`${column_name}\`
124
+ ORDER BY count DESC
125
+ LIMIT 5
126
+ `;
127
+ const topValues = await this.db.query(topValuesQuery);
128
+ return {
129
+ status: "success",
130
+ data: {
131
+ column_name,
132
+ data_type: dataType,
133
+ statistics: {
134
+ total_rows: stats.total_rows,
135
+ non_null_count: stats.non_null_count,
136
+ null_count: stats.null_count,
137
+ distinct_count: stats.distinct_count,
138
+ unique_ratio: stats.total_rows > 0
139
+ ? (stats.distinct_count / stats.total_rows).toFixed(4)
140
+ : 0,
141
+ ...(isNumeric || isDate
142
+ ? { min_value: stats.min_value, max_value: stats.max_value }
143
+ : {}),
144
+ ...(isNumeric ? { avg_value: stats.avg_value } : {}),
145
+ },
146
+ top_values: topValues,
147
+ },
148
+ };
149
+ }
150
+ catch (error) {
151
+ return {
152
+ status: "error",
153
+ error: error.message,
154
+ };
155
+ }
156
+ }
157
+ /**
158
+ * Build a compact, schema-aware context pack for RAG (tables, PK/FK, columns, row estimates)
159
+ */
160
+ async getSchemaRagContext(params = {}) {
161
+ try {
162
+ const dbValidation = this.validateDatabaseAccess(params?.database);
163
+ if (!dbValidation.valid) {
164
+ return { status: "error", error: dbValidation.error };
165
+ }
166
+ const database = dbValidation.database;
167
+ const maxTables = Math.min(Math.max(params.max_tables ?? 50, 1), 200);
168
+ const maxColumns = Math.min(Math.max(params.max_columns ?? 12, 1), 200);
169
+ const includeRelationships = params.include_relationships ?? true;
170
+ // Count total tables for truncation note
171
+ const totalTablesResult = await this.db.query(`SELECT COUNT(*) as total FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?`, [database]);
172
+ const totalTables = totalTablesResult[0]?.total ?? 0;
173
+ // Fetch tables limited for context pack
174
+ const tables = await this.db.query(`
175
+ SELECT TABLE_NAME, TABLE_ROWS
176
+ FROM INFORMATION_SCHEMA.TABLES
177
+ WHERE TABLE_SCHEMA = ?
178
+ ORDER BY TABLE_NAME
179
+ LIMIT ?
180
+ `, [database, maxTables]);
181
+ if (!tables.length) {
182
+ return {
183
+ status: "success",
184
+ data: {
185
+ database,
186
+ total_tables: 0,
187
+ tables: [],
188
+ relationships: [],
189
+ context_text: `Schema-Aware RAG Context Pack (${database}): no tables found.`,
190
+ },
191
+ };
192
+ }
193
+ const tableNames = tables.map((t) => t.TABLE_NAME);
194
+ const placeholders = tableNames.map(() => "?").join(",");
195
+ const columnParams = [database, ...tableNames];
196
+ const columns = await this.db.query(`
197
+ SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY, IS_NULLABLE
198
+ FROM INFORMATION_SCHEMA.COLUMNS
199
+ WHERE TABLE_SCHEMA = ?
200
+ AND TABLE_NAME IN (${placeholders})
201
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
202
+ `, columnParams);
203
+ let foreignKeys = [];
204
+ if (includeRelationships) {
205
+ const fkParams = [database, ...tableNames, ...tableNames];
206
+ foreignKeys = await this.db.query(`
207
+ SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
208
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
209
+ WHERE TABLE_SCHEMA = ?
210
+ AND TABLE_NAME IN (${placeholders})
211
+ AND REFERENCED_TABLE_NAME IN (${placeholders})
212
+ AND REFERENCED_TABLE_NAME IS NOT NULL
213
+ `, fkParams);
214
+ }
215
+ const fkLookup = new Map();
216
+ foreignKeys.forEach((fk) => {
217
+ fkLookup.set(`${fk.TABLE_NAME}.${fk.COLUMN_NAME}`, {
218
+ table: fk.REFERENCED_TABLE_NAME,
219
+ column: fk.REFERENCED_COLUMN_NAME,
220
+ });
221
+ });
222
+ const tableEntries = tables.map((table) => {
223
+ const tableColumns = columns.filter((c) => c.TABLE_NAME === table.TABLE_NAME);
224
+ const truncatedColumns = tableColumns.length > maxColumns ? tableColumns.length - maxColumns : 0;
225
+ const columnsForContext = tableColumns.slice(0, maxColumns).map((col) => {
226
+ const key = col.COLUMN_KEY === "PRI"
227
+ ? "PK"
228
+ : col.COLUMN_KEY === "UNI"
229
+ ? "UNI"
230
+ : undefined;
231
+ const fkRef = fkLookup.get(`${col.TABLE_NAME}.${col.COLUMN_NAME}`);
232
+ return {
233
+ name: col.COLUMN_NAME,
234
+ data_type: col.DATA_TYPE,
235
+ nullable: col.IS_NULLABLE === "YES",
236
+ key: fkRef ? "FK" : key,
237
+ references: fkRef
238
+ ? `${fkRef.table}.${fkRef.column}`
239
+ : undefined,
240
+ };
241
+ });
242
+ const primaryKeys = tableColumns
243
+ .filter((col) => col.COLUMN_KEY === "PRI")
244
+ .map((col) => col.COLUMN_NAME);
245
+ const foreignKeyList = foreignKeys
246
+ .filter((fk) => fk.TABLE_NAME === table.TABLE_NAME)
247
+ .map((fk) => ({
248
+ column: fk.COLUMN_NAME,
249
+ references: `${fk.REFERENCED_TABLE_NAME}.${fk.REFERENCED_COLUMN_NAME}`,
250
+ }));
251
+ return {
252
+ table_name: table.TABLE_NAME,
253
+ row_estimate: typeof table.TABLE_ROWS === "number"
254
+ ? table.TABLE_ROWS
255
+ : parseInt(table.TABLE_ROWS || "0", 10) || 0,
256
+ primary_keys: primaryKeys,
257
+ columns: columnsForContext,
258
+ foreign_keys: foreignKeyList,
259
+ truncated_columns: truncatedColumns > 0 ? truncatedColumns : 0,
260
+ };
261
+ });
262
+ const relationships = foreignKeys.map((fk) => ({
263
+ from_table: fk.TABLE_NAME,
264
+ from_column: fk.COLUMN_NAME,
265
+ to_table: fk.REFERENCED_TABLE_NAME,
266
+ to_column: fk.REFERENCED_COLUMN_NAME,
267
+ }));
268
+ const lines = [];
269
+ lines.push(`Schema-Aware RAG Context Pack (${database})`);
270
+ lines.push(`Tables shown: ${tableEntries.length}/${totalTables || tableEntries.length} (rows are approximate)`);
271
+ lines.push(`Per-table column limit: ${maxColumns}${tableEntries.some((t) => t.truncated_columns > 0)
272
+ ? " (additional columns truncated)"
273
+ : ""}`);
274
+ lines.push("");
275
+ tableEntries.forEach((t) => {
276
+ const approxRows = typeof t.row_estimate === "number" && t.row_estimate >= 0
277
+ ? `~${t.row_estimate}`
278
+ : "~0";
279
+ const columnSnippets = t.columns.map((c) => {
280
+ const tags = [];
281
+ if (c.key)
282
+ tags.push(c.key);
283
+ if (c.references)
284
+ tags.push(`-> ${c.references}`);
285
+ const nullability = c.nullable ? "null" : "not null";
286
+ return `${c.name} ${c.data_type} (${nullability})${tags.length ? ` [${tags.join(", ")}]` : ""}`;
287
+ });
288
+ lines.push(`- ${t.table_name} (${approxRows} rows) PK: ${t.primary_keys.length ? t.primary_keys.join(", ") : "none"}`);
289
+ lines.push(` Columns: ${columnSnippets.join("; ")}`);
290
+ if (t.truncated_columns) {
291
+ lines.push(` ...and ${t.truncated_columns} more columns not shown`);
292
+ }
293
+ });
294
+ if (includeRelationships && relationships.length) {
295
+ lines.push("");
296
+ lines.push("Relationships:");
297
+ relationships.forEach((rel) => {
298
+ lines.push(`- ${rel.from_table}.${rel.from_column} -> ${rel.to_table}.${rel.to_column}`);
299
+ });
300
+ }
301
+ if (totalTables > tableEntries.length) {
302
+ lines.push(`\nNote: ${totalTables - tableEntries.length} table(s) omitted (max_tables=${maxTables}).`);
303
+ }
304
+ return {
305
+ status: "success",
306
+ data: {
307
+ database,
308
+ total_tables: totalTables,
309
+ tables: tableEntries,
310
+ relationships: includeRelationships ? relationships : [],
311
+ context_text: lines.join("\n"),
312
+ limits: {
313
+ max_tables: maxTables,
314
+ max_columns: maxColumns,
315
+ },
316
+ },
317
+ };
318
+ }
319
+ catch (error) {
320
+ return {
321
+ status: "error",
322
+ error: error.message,
323
+ };
324
+ }
325
+ }
326
+ }
327
+ exports.AnalysisTools = AnalysisTools;
@@ -31,4 +31,25 @@ export declare class DatabaseTools {
31
31
  data?: ColumnInfo[];
32
32
  error?: string;
33
33
  }>;
34
+ /**
35
+ * Get a high-level summary of the database (tables, columns, row counts)
36
+ * Optimized for AI context window
37
+ */
38
+ getDatabaseSummary(params: {
39
+ database?: string;
40
+ }): Promise<{
41
+ status: string;
42
+ data?: string;
43
+ error?: string;
44
+ }>;
45
+ /**
46
+ * Get a Mermaid.js ER diagram for the database schema
47
+ */
48
+ getSchemaERD(params: {
49
+ database?: string;
50
+ }): Promise<{
51
+ status: string;
52
+ data?: string;
53
+ error?: string;
54
+ }>;
34
55
  }
@@ -139,5 +139,143 @@ class DatabaseTools {
139
139
  };
140
140
  }
141
141
  }
142
+ /**
143
+ * Get a high-level summary of the database (tables, columns, row counts)
144
+ * Optimized for AI context window
145
+ */
146
+ async getDatabaseSummary(params) {
147
+ try {
148
+ // Security validation
149
+ if (params.database && params.database !== config_1.dbConfig.database) {
150
+ return {
151
+ status: "error",
152
+ error: `Access denied. You can only access the connected database '${config_1.dbConfig.database}'.`,
153
+ };
154
+ }
155
+ const database = params.database || config_1.dbConfig.database;
156
+ if (!database) {
157
+ return {
158
+ status: "error",
159
+ error: "No database specified and none connected.",
160
+ };
161
+ }
162
+ // Get tables and row counts
163
+ const tablesQuery = `
164
+ SELECT TABLE_NAME, TABLE_ROWS
165
+ FROM INFORMATION_SCHEMA.TABLES
166
+ WHERE TABLE_SCHEMA = ?
167
+ `;
168
+ const tables = await this.db.query(tablesQuery, [database]);
169
+ // Get columns for all tables
170
+ const columnsQuery = `
171
+ SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY
172
+ FROM INFORMATION_SCHEMA.COLUMNS
173
+ WHERE TABLE_SCHEMA = ?
174
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
175
+ `;
176
+ const columns = await this.db.query(columnsQuery, [database]);
177
+ // Build text summary
178
+ let summary = `# Database Summary: ${database}\n\n`;
179
+ for (const table of tables) {
180
+ summary += `## Table: ${table.TABLE_NAME} (~${table.TABLE_ROWS || 0} rows)\n`;
181
+ const tableColumns = columns.filter(c => c.TABLE_NAME === table.TABLE_NAME);
182
+ const columnDefs = tableColumns.map(c => {
183
+ let def = `${c.COLUMN_NAME} (${c.DATA_TYPE})`;
184
+ if (c.COLUMN_KEY === 'PRI')
185
+ def += " [PK]";
186
+ if (c.COLUMN_KEY === 'MUL')
187
+ def += " [FK/Index]";
188
+ if (c.COLUMN_KEY === 'UNI')
189
+ def += " [Unique]";
190
+ return def;
191
+ });
192
+ summary += `Columns: ${columnDefs.join(", ")}\n\n`;
193
+ }
194
+ return {
195
+ status: "success",
196
+ data: summary
197
+ };
198
+ }
199
+ catch (error) {
200
+ return {
201
+ status: "error",
202
+ error: error.message
203
+ };
204
+ }
205
+ }
206
+ /**
207
+ * Get a Mermaid.js ER diagram for the database schema
208
+ */
209
+ async getSchemaERD(params) {
210
+ try {
211
+ // Security validation
212
+ if (params.database && params.database !== config_1.dbConfig.database) {
213
+ return {
214
+ status: "error",
215
+ error: `Access denied. You can only access the connected database '${config_1.dbConfig.database}'.`,
216
+ };
217
+ }
218
+ const database = params.database || config_1.dbConfig.database;
219
+ if (!database) {
220
+ return {
221
+ status: "error",
222
+ error: "No database specified and none connected.",
223
+ };
224
+ }
225
+ // Get tables and columns
226
+ const columnsQuery = `
227
+ SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY
228
+ FROM INFORMATION_SCHEMA.COLUMNS
229
+ WHERE TABLE_SCHEMA = ?
230
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
231
+ `;
232
+ const columns = await this.db.query(columnsQuery, [database]);
233
+ // Get foreign keys
234
+ const fkQuery = `
235
+ SELECT
236
+ TABLE_NAME, COLUMN_NAME,
237
+ REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
238
+ FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
239
+ WHERE TABLE_SCHEMA = ?
240
+ AND REFERENCED_TABLE_SCHEMA = ?
241
+ AND REFERENCED_TABLE_NAME IS NOT NULL
242
+ `;
243
+ const fks = await this.db.query(fkQuery, [database, database]);
244
+ // Build Mermaid ER diagram
245
+ let mermaid = "erDiagram\n";
246
+ // Add entities (tables)
247
+ const tables = [...new Set(columns.map(c => c.TABLE_NAME))];
248
+ for (const table of tables) {
249
+ mermaid += ` ${table} {\n`;
250
+ const tableColumns = columns.filter(c => c.TABLE_NAME === table);
251
+ for (const col of tableColumns) {
252
+ let type = col.DATA_TYPE;
253
+ let key = "";
254
+ if (col.COLUMN_KEY === 'PRI')
255
+ key = "PK";
256
+ else if (col.COLUMN_KEY === 'MUL')
257
+ key = "FK";
258
+ mermaid += ` ${type} ${col.COLUMN_NAME} ${key}\n`;
259
+ }
260
+ mermaid += ` }\n`;
261
+ }
262
+ // Add relationships
263
+ for (const fk of fks) {
264
+ // Relationship logic: referenced_table ||--o{ table
265
+ // This is a simplification, usually FK implies 1 to Many
266
+ mermaid += ` ${fk.REFERENCED_TABLE_NAME} ||--o{ ${fk.TABLE_NAME} : "${fk.COLUMN_NAME}"\n`;
267
+ }
268
+ return {
269
+ status: "success",
270
+ data: mermaid
271
+ };
272
+ }
273
+ catch (error) {
274
+ return {
275
+ status: "error",
276
+ error: error.message
277
+ };
278
+ }
279
+ }
142
280
  }
143
281
  exports.DatabaseTools = DatabaseTools;
@@ -13,11 +13,16 @@ export declare class QueryTools {
13
13
  params?: any[];
14
14
  hints?: QueryHints;
15
15
  useCache?: boolean;
16
+ dry_run?: boolean;
16
17
  }): Promise<{
17
18
  status: string;
18
19
  data?: any[];
19
20
  error?: string;
20
21
  optimizedQuery?: string;
22
+ dry_run?: boolean;
23
+ execution_plan?: any;
24
+ estimated_cost?: string;
25
+ message?: string;
21
26
  }>;
22
27
  /**
23
28
  * Analyze a query and get optimization suggestions
@@ -61,6 +61,33 @@ class QueryTools {
61
61
  optimizedQuery = finalQuery;
62
62
  }
63
63
  }
64
+ // Handle dry_run: return query plan and cost estimate without executing
65
+ if (queryParams.dry_run) {
66
+ const explainQuery = `EXPLAIN FORMAT=JSON ${finalQuery}`;
67
+ const explainResult = await this.db.query(explainQuery, paramValidation.sanitizedParams);
68
+ // Try to get cost from JSON format
69
+ let estimatedCost = "Unknown";
70
+ let executionPlan = explainResult;
71
+ if (explainResult[0] && explainResult[0].EXPLAIN) {
72
+ try {
73
+ const explainJson = JSON.parse(explainResult[0].EXPLAIN);
74
+ estimatedCost = explainJson.query_block?.cost_info?.query_cost || "Unknown";
75
+ executionPlan = explainJson;
76
+ }
77
+ catch (e) {
78
+ // Ignore parsing error
79
+ }
80
+ }
81
+ return {
82
+ status: "success",
83
+ data: [],
84
+ optimizedQuery,
85
+ dry_run: true,
86
+ execution_plan: executionPlan,
87
+ estimated_cost: estimatedCost,
88
+ message: "Dry run completed. Query was not executed."
89
+ };
90
+ }
64
91
  // Execute the query with sanitized parameters
65
92
  const results = await this.db.query(finalQuery, paramValidation.sanitizedParams, useCache);
66
93
  return {
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mysql-mcp",
3
3
  "description": "A Model Context Protocol for MySQL database interaction",
4
- "version": "1.4.4",
4
+ "version": "1.12.0",
5
5
  "tools": [
6
6
  {
7
7
  "name": "list_databases",
@@ -244,6 +244,27 @@
244
244
  }
245
245
  }
246
246
  },
247
+ {
248
+ "name": "get_schema_rag_context",
249
+ "description": "Returns a condensed schema-aware context pack with tables, PK/FK details, and row estimates for RAG prompts.",
250
+ "input_schema": {
251
+ "type": "object",
252
+ "properties": {
253
+ "database": { "type": "string", "description": "Optional specific database name" },
254
+ "max_tables": { "type": "number", "description": "Max tables to include (default 50, max 200)" },
255
+ "max_columns": { "type": "number", "description": "Max columns per table (default 12, max 200)" },
256
+ "include_relationships": { "type": "boolean", "description": "Whether to include FK relationships (default true)" }
257
+ }
258
+ },
259
+ "output_schema": {
260
+ "type": "object",
261
+ "properties": {
262
+ "database": { "type": "string" },
263
+ "total_tables": { "type": "number" },
264
+ "context_text": { "type": "string" }
265
+ }
266
+ }
267
+ },
247
268
  {
248
269
  "name": "get_table_size",
249
270
  "description": "Gets size information for one or all tables including data and index sizes.",