@berthojoris/mcp-mysql-server 1.16.4 → 1.17.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.
package/dist/index.js CHANGED
@@ -32,6 +32,10 @@ const documentationGeneratorTools_1 = require("./tools/documentationGeneratorToo
32
32
  const schemaDesignTools_1 = require("./tools/schemaDesignTools");
33
33
  const securityAuditTools_1 = require("./tools/securityAuditTools");
34
34
  const indexRecommendationTools_1 = require("./tools/indexRecommendationTools");
35
+ const testDataTools_1 = require("./tools/testDataTools");
36
+ const schemaPatternTools_1 = require("./tools/schemaPatternTools");
37
+ const queryVisualizationTools_1 = require("./tools/queryVisualizationTools");
38
+ const forecastingTools_1 = require("./tools/forecastingTools");
35
39
  const securityLayer_1 = __importDefault(require("./security/securityLayer"));
36
40
  const connection_1 = __importDefault(require("./db/connection"));
37
41
  const featureConfig_1 = require("./config/featureConfig");
@@ -72,6 +76,10 @@ class MySQLMCP {
72
76
  this.schemaDesignTools = new schemaDesignTools_1.SchemaDesignTools(this.security);
73
77
  this.securityAuditTools = new securityAuditTools_1.SecurityAuditTools();
74
78
  this.indexRecommendationTools = new indexRecommendationTools_1.IndexRecommendationTools(this.security);
79
+ this.testDataTools = new testDataTools_1.TestDataTools(this.security);
80
+ this.schemaPatternTools = new schemaPatternTools_1.SchemaPatternTools(this.security);
81
+ this.queryVisualizationTools = new queryVisualizationTools_1.QueryVisualizationTools(this.security);
82
+ this.forecastingTools = new forecastingTools_1.ForecastingTools(this.security);
75
83
  }
76
84
  // Helper method to check if tool is enabled
77
85
  checkToolEnabled(toolName) {
@@ -1155,6 +1163,39 @@ class MySQLMCP {
1155
1163
  return { status: "error", error: check.error };
1156
1164
  return await this.indexRecommendationTools.recommendIndexes(params);
1157
1165
  }
1166
+ // ==========================================
1167
+ // PHASE 3: AI Enhancement Tools (Data Gen + Patterns + Visualization + Forecasting)
1168
+ // ==========================================
1169
+ async generateTestData(params) {
1170
+ const check = this.checkToolEnabled("generateTestData");
1171
+ if (!check.enabled)
1172
+ return { status: "error", error: check.error };
1173
+ return await this.testDataTools.generateTestData(params);
1174
+ }
1175
+ async analyzeSchemaPatterns(params) {
1176
+ const check = this.checkToolEnabled("analyzeSchemaPatterns");
1177
+ if (!check.enabled)
1178
+ return { status: "error", error: check.error };
1179
+ return await this.schemaPatternTools.analyzeSchemaPatterns(params);
1180
+ }
1181
+ async visualizeQuery(params) {
1182
+ const check = this.checkToolEnabled("visualizeQuery");
1183
+ if (!check.enabled)
1184
+ return { status: "error", error: check.error };
1185
+ return await this.queryVisualizationTools.visualizeQuery(params);
1186
+ }
1187
+ async predictQueryPerformance(params) {
1188
+ const check = this.checkToolEnabled("predictQueryPerformance");
1189
+ if (!check.enabled)
1190
+ return { status: "error", error: check.error };
1191
+ return await this.forecastingTools.predictQueryPerformance(params);
1192
+ }
1193
+ async forecastDatabaseGrowth(params) {
1194
+ const check = this.checkToolEnabled("forecastDatabaseGrowth");
1195
+ if (!check.enabled)
1196
+ return { status: "error", error: check.error };
1197
+ return await this.forecastingTools.forecastDatabaseGrowth(params);
1198
+ }
1158
1199
  }
1159
1200
  exports.MySQLMCP = MySQLMCP;
1160
1201
  exports.default = MySQLMCP;
@@ -2992,6 +2992,141 @@ const TOOLS = [
2992
2992
  },
2993
2993
  },
2994
2994
  },
2995
+ // ==========================================
2996
+ // PHASE 3: AI Enhancement Tools (Data Gen + Patterns + Visualization + Forecasting)
2997
+ // ==========================================
2998
+ {
2999
+ name: "generate_test_data",
3000
+ description: "Generates synthetic test data as SQL INSERT statements for a given table (does not execute). Attempts FK-aware value selection for referential integrity.",
3001
+ inputSchema: {
3002
+ type: "object",
3003
+ properties: {
3004
+ table_name: {
3005
+ type: "string",
3006
+ description: "Target table to generate INSERT statements for",
3007
+ },
3008
+ row_count: {
3009
+ type: "number",
3010
+ description: "Number of rows to generate (max 5000)",
3011
+ },
3012
+ batch_size: {
3013
+ type: "number",
3014
+ description: "Rows per INSERT statement (default 100, max 1000)",
3015
+ },
3016
+ include_nulls: {
3017
+ type: "boolean",
3018
+ description: "Whether generated values may include NULLs when columns are nullable (default true)",
3019
+ },
3020
+ database: {
3021
+ type: "string",
3022
+ description: "Optional: specific database name",
3023
+ },
3024
+ },
3025
+ required: ["table_name", "row_count"],
3026
+ },
3027
+ },
3028
+ {
3029
+ name: "analyze_schema_patterns",
3030
+ description: "Analyzes the schema for common patterns and anti-patterns (missing PKs, wide tables, unindexed FKs, EAV-like tables, etc.) and returns recommendations.",
3031
+ inputSchema: {
3032
+ type: "object",
3033
+ properties: {
3034
+ scope: {
3035
+ type: "string",
3036
+ enum: ["database", "table"],
3037
+ description: "Analysis scope (default: database unless table_name provided)",
3038
+ },
3039
+ table_name: {
3040
+ type: "string",
3041
+ description: "Optional: specific table to analyze (implies table scope)",
3042
+ },
3043
+ database: {
3044
+ type: "string",
3045
+ description: "Optional: specific database name",
3046
+ },
3047
+ },
3048
+ },
3049
+ },
3050
+ {
3051
+ name: "visualize_query",
3052
+ description: "Creates a visual representation of a read-only SQL query as a Mermaid flowchart, based on EXPLAIN FORMAT=JSON and lightweight SQL parsing.",
3053
+ inputSchema: {
3054
+ type: "object",
3055
+ properties: {
3056
+ query: {
3057
+ type: "string",
3058
+ description: "Read-only SQL query to visualize",
3059
+ },
3060
+ include_explain_json: {
3061
+ type: "boolean",
3062
+ description: "Include full EXPLAIN JSON in the response (default true)",
3063
+ },
3064
+ format: {
3065
+ type: "string",
3066
+ enum: ["mermaid", "json", "both"],
3067
+ description: "Output format (default: both)",
3068
+ },
3069
+ },
3070
+ required: ["query"],
3071
+ },
3072
+ },
3073
+ {
3074
+ name: "predict_query_performance",
3075
+ description: "Predicts how EXPLAIN-estimated scan volume/cost could change under table growth assumptions (heuristic).",
3076
+ inputSchema: {
3077
+ type: "object",
3078
+ properties: {
3079
+ query: {
3080
+ type: "string",
3081
+ description: "Read-only SQL query to analyze",
3082
+ },
3083
+ row_growth_multiplier: {
3084
+ type: "number",
3085
+ description: "Default multiplicative growth factor to apply to table row estimates (default 2)",
3086
+ },
3087
+ per_table_row_growth: {
3088
+ type: "object",
3089
+ description: "Optional per-table growth overrides: { tableName: factor }",
3090
+ additionalProperties: { type: "number" },
3091
+ },
3092
+ include_explain_json: {
3093
+ type: "boolean",
3094
+ description: "Include full EXPLAIN JSON in the response (default false)",
3095
+ },
3096
+ },
3097
+ required: ["query"],
3098
+ },
3099
+ },
3100
+ {
3101
+ name: "forecast_database_growth",
3102
+ description: "Forecasts database/table growth based on current INFORMATION_SCHEMA sizes and user-supplied growth rate assumptions.",
3103
+ inputSchema: {
3104
+ type: "object",
3105
+ properties: {
3106
+ horizon_days: {
3107
+ type: "number",
3108
+ description: "Forecast horizon in days (default 30, max 3650)",
3109
+ },
3110
+ growth_rate_percent_per_day: {
3111
+ type: "number",
3112
+ description: "Base daily growth rate percent applied to all tables unless overridden (e.g., 0.5)",
3113
+ },
3114
+ growth_rate_percent_per_month: {
3115
+ type: "number",
3116
+ description: "Base monthly growth rate percent (converted to daily compound rate)",
3117
+ },
3118
+ per_table_growth_rate_percent_per_day: {
3119
+ type: "object",
3120
+ description: "Optional per-table daily growth rate percent overrides: { tableName: percentPerDay }",
3121
+ additionalProperties: { type: "number" },
3122
+ },
3123
+ database: {
3124
+ type: "string",
3125
+ description: "Optional: specific database name",
3126
+ },
3127
+ },
3128
+ },
3129
+ },
2995
3130
  // Smart Data Discovery
2996
3131
  {
2997
3132
  name: "smart_search",
@@ -3179,7 +3314,7 @@ const TOOLS = [
3179
3314
  // Create the MCP server
3180
3315
  const server = new index_js_1.Server({
3181
3316
  name: "mysql-mcp-server",
3182
- version: "1.12.0",
3317
+ version: "1.17.0",
3183
3318
  }, {
3184
3319
  capabilities: {
3185
3320
  tools: {},
@@ -3652,6 +3787,24 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
3652
3787
  case "recommend_indexes":
3653
3788
  result = await mysqlMCP.recommendIndexes((args || {}));
3654
3789
  break;
3790
+ // ==========================================
3791
+ // PHASE 3: AI Enhancement Tools (Data Gen + Patterns + Visualization + Forecasting)
3792
+ // ==========================================
3793
+ case "generate_test_data":
3794
+ result = await mysqlMCP.generateTestData((args || {}));
3795
+ break;
3796
+ case "analyze_schema_patterns":
3797
+ result = await mysqlMCP.analyzeSchemaPatterns((args || {}));
3798
+ break;
3799
+ case "visualize_query":
3800
+ result = await mysqlMCP.visualizeQuery((args || {}));
3801
+ break;
3802
+ case "predict_query_performance":
3803
+ result = await mysqlMCP.predictQueryPerformance((args || {}));
3804
+ break;
3805
+ case "forecast_database_growth":
3806
+ result = await mysqlMCP.forecastDatabaseGrowth((args || {}));
3807
+ break;
3655
3808
  default:
3656
3809
  throw new Error(`Unknown tool: ${name}`);
3657
3810
  }
@@ -0,0 +1,36 @@
1
+ import { SecurityLayer } from "../security/securityLayer";
2
+ export declare class ForecastingTools {
3
+ private db;
4
+ private security;
5
+ constructor(security: SecurityLayer);
6
+ private validateDatabaseAccess;
7
+ private extractExplainNodes;
8
+ /**
9
+ * Predict how query cost/scan volume might change under table growth assumptions.
10
+ * This is heuristic-based and uses EXPLAIN FORMAT=JSON estimates.
11
+ */
12
+ predictQueryPerformance(params: {
13
+ query: string;
14
+ row_growth_multiplier?: number;
15
+ per_table_row_growth?: Record<string, number>;
16
+ include_explain_json?: boolean;
17
+ }): Promise<{
18
+ status: string;
19
+ data?: any;
20
+ error?: string;
21
+ }>;
22
+ /**
23
+ * Forecast database/table growth based on current sizes and user-supplied growth rate assumptions.
24
+ */
25
+ forecastDatabaseGrowth(params?: {
26
+ horizon_days?: number;
27
+ growth_rate_percent_per_day?: number;
28
+ growth_rate_percent_per_month?: number;
29
+ per_table_growth_rate_percent_per_day?: Record<string, number>;
30
+ database?: string;
31
+ }): Promise<{
32
+ status: string;
33
+ data?: any;
34
+ error?: string;
35
+ }>;
36
+ }
@@ -0,0 +1,256 @@
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.ForecastingTools = void 0;
7
+ const connection_1 = __importDefault(require("../db/connection"));
8
+ const config_1 = require("../config/config");
9
+ class ForecastingTools {
10
+ constructor(security) {
11
+ this.db = connection_1.default.getInstance();
12
+ this.security = security;
13
+ }
14
+ validateDatabaseAccess(requestedDatabase) {
15
+ const connectedDatabase = config_1.dbConfig.database;
16
+ if (!connectedDatabase) {
17
+ return {
18
+ valid: false,
19
+ database: "",
20
+ error: "No database configured. Please specify a database in your connection settings.",
21
+ };
22
+ }
23
+ if (requestedDatabase && requestedDatabase !== connectedDatabase) {
24
+ return {
25
+ valid: false,
26
+ database: "",
27
+ error: `Access denied: You are connected to '${connectedDatabase}' but requested '${requestedDatabase}'. Cross-database access is not permitted.`,
28
+ };
29
+ }
30
+ return { valid: true, database: connectedDatabase };
31
+ }
32
+ extractExplainNodes(explainJson) {
33
+ const nodes = [];
34
+ const visit = (obj) => {
35
+ if (!obj || typeof obj !== "object")
36
+ return;
37
+ if (obj.table && typeof obj.table === "object") {
38
+ const t = obj.table;
39
+ nodes.push({
40
+ table_name: t.table_name,
41
+ access_type: t.access_type,
42
+ key: t.key,
43
+ rows_examined_per_scan: t.rows_examined_per_scan,
44
+ rows_produced_per_join: t.rows_produced_per_join,
45
+ filtered: t.filtered,
46
+ });
47
+ }
48
+ for (const v of Object.values(obj)) {
49
+ if (Array.isArray(v))
50
+ v.forEach(visit);
51
+ else if (v && typeof v === "object")
52
+ visit(v);
53
+ }
54
+ };
55
+ visit(explainJson);
56
+ return nodes;
57
+ }
58
+ /**
59
+ * Predict how query cost/scan volume might change under table growth assumptions.
60
+ * This is heuristic-based and uses EXPLAIN FORMAT=JSON estimates.
61
+ */
62
+ async predictQueryPerformance(params) {
63
+ try {
64
+ const query = params.query;
65
+ if (!query || typeof query !== "string") {
66
+ return { status: "error", error: "query is required" };
67
+ }
68
+ if (!this.security.isReadOnlyQuery(query)) {
69
+ return {
70
+ status: "error",
71
+ error: "Only read-only queries (SELECT/SHOW/DESCRIBE/EXPLAIN) are supported for prediction.",
72
+ };
73
+ }
74
+ const growth = params.row_growth_multiplier ?? 2;
75
+ if (!Number.isFinite(growth) || growth <= 0) {
76
+ return { status: "error", error: "row_growth_multiplier must be > 0" };
77
+ }
78
+ const explainRows = await this.db.query(`EXPLAIN FORMAT=JSON ${query}`);
79
+ let explainJson = null;
80
+ let queryCost = null;
81
+ if (explainRows[0] && explainRows[0].EXPLAIN) {
82
+ try {
83
+ explainJson = JSON.parse(explainRows[0].EXPLAIN);
84
+ const qc = explainJson?.query_block?.cost_info?.query_cost;
85
+ queryCost = qc !== undefined ? parseFloat(String(qc)) : null;
86
+ }
87
+ catch {
88
+ explainJson = explainRows;
89
+ }
90
+ }
91
+ const nodes = explainJson ? this.extractExplainNodes(explainJson) : [];
92
+ const perTable = params.per_table_row_growth || {};
93
+ const tablePredictions = nodes
94
+ .filter((n) => !!n.table_name)
95
+ .map((n) => {
96
+ const t = n.table_name;
97
+ const factor = typeof perTable[t] === "number" && perTable[t] > 0
98
+ ? perTable[t]
99
+ : growth;
100
+ const baseRows = n.rows_examined_per_scan ?? n.rows_produced_per_join ?? null;
101
+ const predictedRows = typeof baseRows === "number" ? Math.round(baseRows * factor) : null;
102
+ return {
103
+ table_name: t,
104
+ access_type: n.access_type,
105
+ key: n.key,
106
+ base_rows_estimate: baseRows,
107
+ growth_factor: factor,
108
+ predicted_rows_estimate: predictedRows,
109
+ };
110
+ });
111
+ const avgFactor = tablePredictions.length > 0
112
+ ? tablePredictions.reduce((acc, t) => acc + t.growth_factor, 0) /
113
+ tablePredictions.length
114
+ : growth;
115
+ const predictedCost = typeof queryCost === "number" && Number.isFinite(queryCost)
116
+ ? parseFloat((queryCost * avgFactor).toFixed(4))
117
+ : null;
118
+ const worstScan = tablePredictions.reduce((max, t) => Math.max(max, t.predicted_rows_estimate || 0), 0);
119
+ let risk = "low";
120
+ if (worstScan > 1000000)
121
+ risk = "high";
122
+ else if (worstScan > 100000)
123
+ risk = "medium";
124
+ const recommendations = [];
125
+ for (const t of tablePredictions) {
126
+ if ((t.access_type || "").toUpperCase() === "ALL") {
127
+ recommendations.push(`Table '${t.table_name}' is using a full scan (access_type=ALL). Consider adding an index aligned with WHERE/JOIN predicates.`);
128
+ }
129
+ if (!t.key && (t.predicted_rows_estimate || 0) > 100000) {
130
+ recommendations.push(`Table '${t.table_name}' has no chosen index in EXPLAIN and predicted scan is large; review indexing and query predicates.`);
131
+ }
132
+ }
133
+ const data = {
134
+ current_estimate: {
135
+ query_cost: queryCost,
136
+ },
137
+ growth_assumptions: {
138
+ row_growth_multiplier: growth,
139
+ per_table_row_growth: perTable,
140
+ cost_scaling_model: "cost ~ linear in average growth factor (heuristic)",
141
+ },
142
+ predicted_estimate: {
143
+ query_cost: predictedCost,
144
+ },
145
+ table_estimates: tablePredictions,
146
+ risk,
147
+ recommendations,
148
+ notes: [
149
+ "This is an estimate based on MySQL EXPLAIN and simple scaling assumptions.",
150
+ "Validate with production-like data volumes and real timings.",
151
+ ],
152
+ };
153
+ if (params.include_explain_json ?? false) {
154
+ data.explain_json = explainJson;
155
+ }
156
+ return { status: "success", data };
157
+ }
158
+ catch (error) {
159
+ return { status: "error", error: error.message };
160
+ }
161
+ }
162
+ /**
163
+ * Forecast database/table growth based on current sizes and user-supplied growth rate assumptions.
164
+ */
165
+ async forecastDatabaseGrowth(params = {}) {
166
+ try {
167
+ const dbValidation = this.validateDatabaseAccess(params?.database);
168
+ if (!dbValidation.valid) {
169
+ return { status: "error", error: dbValidation.error };
170
+ }
171
+ const database = dbValidation.database;
172
+ const horizonDays = Math.min(Math.max(params.horizon_days ?? 30, 1), 3650);
173
+ let baseDailyRate = null;
174
+ if (typeof params.growth_rate_percent_per_day === "number") {
175
+ baseDailyRate = params.growth_rate_percent_per_day / 100;
176
+ }
177
+ else if (typeof params.growth_rate_percent_per_month === "number") {
178
+ const monthly = params.growth_rate_percent_per_month / 100;
179
+ // Convert to approx daily compound rate assuming 30-day month
180
+ baseDailyRate = Math.pow(1 + monthly, 1 / 30) - 1;
181
+ }
182
+ const perTableRates = params.per_table_growth_rate_percent_per_day || {};
183
+ if (baseDailyRate === null && Object.keys(perTableRates).length === 0) {
184
+ return {
185
+ status: "error",
186
+ error: "Provide growth_rate_percent_per_day or growth_rate_percent_per_month, or per_table_growth_rate_percent_per_day.",
187
+ };
188
+ }
189
+ const rows = await this.db.query(`
190
+ SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH
191
+ FROM INFORMATION_SCHEMA.TABLES
192
+ WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'
193
+ ORDER BY TABLE_NAME
194
+ `, [database]);
195
+ const tableForecasts = rows.map((r) => {
196
+ const table = r.TABLE_NAME;
197
+ const currentRows = typeof r.TABLE_ROWS === "number"
198
+ ? r.TABLE_ROWS
199
+ : parseInt(String(r.TABLE_ROWS || "0"), 10) || 0;
200
+ const dataBytes = typeof r.DATA_LENGTH === "number"
201
+ ? r.DATA_LENGTH
202
+ : parseInt(String(r.DATA_LENGTH || "0"), 10) || 0;
203
+ const indexBytes = typeof r.INDEX_LENGTH === "number"
204
+ ? r.INDEX_LENGTH
205
+ : parseInt(String(r.INDEX_LENGTH || "0"), 10) || 0;
206
+ const totalBytes = dataBytes + indexBytes;
207
+ const dailyRate = typeof perTableRates[table] === "number"
208
+ ? perTableRates[table] / 100
209
+ : baseDailyRate || 0;
210
+ const growthFactor = Math.pow(1 + dailyRate, horizonDays);
211
+ const forecastRows = Math.round(currentRows * growthFactor);
212
+ const forecastTotalBytes = Math.round(totalBytes * growthFactor);
213
+ return {
214
+ table_name: table,
215
+ current: {
216
+ row_estimate: currentRows,
217
+ total_size_bytes: totalBytes,
218
+ data_size_bytes: dataBytes,
219
+ index_size_bytes: indexBytes,
220
+ },
221
+ assumptions: {
222
+ daily_growth_rate_percent: dailyRate * 100,
223
+ horizon_days: horizonDays,
224
+ },
225
+ forecast: {
226
+ row_estimate: forecastRows,
227
+ total_size_bytes: forecastTotalBytes,
228
+ },
229
+ };
230
+ });
231
+ const totals = {
232
+ current_total_bytes: tableForecasts.reduce((acc, t) => acc + t.current.total_size_bytes, 0),
233
+ forecast_total_bytes: tableForecasts.reduce((acc, t) => acc + t.forecast.total_size_bytes, 0),
234
+ };
235
+ return {
236
+ status: "success",
237
+ data: {
238
+ database,
239
+ horizon_days: horizonDays,
240
+ base_growth_rate_percent_per_day: baseDailyRate === null ? null : baseDailyRate * 100,
241
+ per_table_growth_rate_percent_per_day: perTableRates,
242
+ totals,
243
+ tables: tableForecasts,
244
+ notes: [
245
+ "Forecast uses simple exponential growth from current INFORMATION_SCHEMA sizes.",
246
+ "Row counts and sizes are estimates and may be stale depending on storage engine/statistics.",
247
+ ],
248
+ },
249
+ };
250
+ }
251
+ catch (error) {
252
+ return { status: "error", error: error.message };
253
+ }
254
+ }
255
+ }
256
+ exports.ForecastingTools = ForecastingTools;
@@ -0,0 +1,22 @@
1
+ import { SecurityLayer } from "../security/securityLayer";
2
+ export declare class QueryVisualizationTools {
3
+ private db;
4
+ private security;
5
+ constructor(security: SecurityLayer);
6
+ private extractExplainNodes;
7
+ private sqlJoinEdges;
8
+ private buildMermaid;
9
+ /**
10
+ * Create a lightweight visual representation of a SQL query.
11
+ * Returns Mermaid flowchart + EXPLAIN FORMAT=JSON summary.
12
+ */
13
+ visualizeQuery(params: {
14
+ query: string;
15
+ include_explain_json?: boolean;
16
+ format?: "mermaid" | "json" | "both";
17
+ }): Promise<{
18
+ status: string;
19
+ data?: any;
20
+ error?: string;
21
+ }>;
22
+ }