@berthojoris/mcp-mysql-server 1.40.6 → 1.42.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.
@@ -52,16 +52,59 @@ class DdlTools {
52
52
  // For other types, convert to string and escape
53
53
  return `'${String(defaultValue).replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
54
54
  }
55
+ validateColumnType(columnType) {
56
+ if (!columnType || typeof columnType !== "string") {
57
+ return { valid: false, error: "Column type must be a non-empty string" };
58
+ }
59
+ const normalizedType = columnType.trim().replace(/\s+/g, " ");
60
+ if (normalizedType.length > 128) {
61
+ return { valid: false, error: "Column type is too long" };
62
+ }
63
+ const safeTypePattern = /^(?:(?:TINY|SMALL|MEDIUM|BIG)?INT(?:EGER)?|DECIMAL|NUMERIC|FLOAT|DOUBLE(?: PRECISION)?|REAL|BIT|BOOL(?:EAN)?|CHAR|VARCHAR|BINARY|VARBINARY|TINYTEXT|TEXT|MEDIUMTEXT|LONGTEXT|TINYBLOB|BLOB|MEDIUMBLOB|LONGBLOB|DATE|DATETIME|TIMESTAMP|TIME|YEAR|JSON)(?:\s*\(\s*\d+(?:\s*,\s*\d+)?\s*\))?(?:\s+(?:UNSIGNED|ZEROFILL))*$/i;
64
+ if (!safeTypePattern.test(normalizedType)) {
65
+ return {
66
+ valid: false,
67
+ error: "Invalid or unsupported column type. Use a standard MySQL data type such as INT, VARCHAR(255), DECIMAL(10,2), TEXT, DATETIME, or JSON.",
68
+ };
69
+ }
70
+ return { valid: true };
71
+ }
72
+ validateIdentifier(identifier, label) {
73
+ const validation = this.security.validateIdentifier(identifier);
74
+ if (!validation.valid) {
75
+ return {
76
+ valid: false,
77
+ error: `Invalid ${label}: ${validation.error}`,
78
+ };
79
+ }
80
+ return { valid: true };
81
+ }
55
82
  /**
56
83
  * Create a new table
57
84
  */
58
85
  async createTable(params) {
59
86
  try {
60
87
  const { table_name, columns, indexes } = params;
88
+ const tableValidation = this.validateIdentifier(table_name, "table name");
89
+ if (!tableValidation.valid) {
90
+ return { status: "error", error: tableValidation.error };
91
+ }
92
+ if (!Array.isArray(columns) || columns.length === 0) {
93
+ return { status: "error", error: "At least one column is required" };
94
+ }
95
+ const escapedTableName = this.security.escapeIdentifier(table_name);
61
96
  // Build column definitions
62
97
  const columnDefs = columns
63
98
  .map((col) => {
64
- let def = `\`${col.name}\` ${col.type}`;
99
+ const columnValidation = this.validateIdentifier(col.name, "column name");
100
+ if (!columnValidation.valid) {
101
+ throw new Error(columnValidation.error);
102
+ }
103
+ const typeValidation = this.validateColumnType(col.type);
104
+ if (!typeValidation.valid) {
105
+ throw new Error(`Invalid column type for '${col.name}': ${typeValidation.error}`);
106
+ }
107
+ let def = `${this.security.escapeIdentifier(col.name)} ${col.type.trim()}`;
65
108
  if (col.nullable === false) {
66
109
  def += " NOT NULL";
67
110
  }
@@ -80,16 +123,29 @@ class DdlTools {
80
123
  })
81
124
  .join(", ");
82
125
  // Build the CREATE TABLE query
83
- let query = `CREATE TABLE \`${table_name}\` (${columnDefs})`;
126
+ let query = `CREATE TABLE ${escapedTableName} (${columnDefs})`;
84
127
  // Execute the query
85
128
  await this.db.query(query);
86
129
  // Create indexes if specified
87
130
  let queryCount = 1;
88
131
  if (indexes && indexes.length > 0) {
89
132
  for (const index of indexes) {
133
+ const indexValidation = this.validateIdentifier(index.name, "index name");
134
+ if (!indexValidation.valid) {
135
+ return { status: "error", error: indexValidation.error };
136
+ }
137
+ if (!Array.isArray(index.columns) || index.columns.length === 0) {
138
+ return { status: "error", error: "Index columns are required" };
139
+ }
90
140
  const indexType = index.unique ? "UNIQUE INDEX" : "INDEX";
91
- const indexColumns = index.columns.map((c) => `\`${c}\``).join(", ");
92
- const indexQuery = `CREATE ${indexType} \`${index.name}\` ON \`${table_name}\` (${indexColumns})`;
141
+ const indexColumns = index.columns.map((c) => {
142
+ const columnValidation = this.validateIdentifier(c, "index column name");
143
+ if (!columnValidation.valid) {
144
+ throw new Error(columnValidation.error);
145
+ }
146
+ return this.security.escapeIdentifier(c);
147
+ }).join(", ");
148
+ const indexQuery = `CREATE ${indexType} ${this.security.escapeIdentifier(index.name)} ON ${escapedTableName} (${indexColumns})`;
93
149
  await this.db.query(indexQuery);
94
150
  queryCount++;
95
151
  }
@@ -115,8 +171,16 @@ class DdlTools {
115
171
  async alterTable(params) {
116
172
  try {
117
173
  const { table_name, operations } = params;
174
+ const tableValidation = this.validateIdentifier(table_name, "table name");
175
+ if (!tableValidation.valid) {
176
+ return { status: "error", error: tableValidation.error };
177
+ }
178
+ if (!Array.isArray(operations) || operations.length === 0) {
179
+ return { status: "error", error: "At least one alter operation is required" };
180
+ }
181
+ const escapedTableName = this.security.escapeIdentifier(table_name);
118
182
  for (const op of operations) {
119
- let query = `ALTER TABLE \`${table_name}\``;
183
+ let query = `ALTER TABLE ${escapedTableName}`;
120
184
  switch (op.type) {
121
185
  case "add_column":
122
186
  if (!op.column_name || !op.column_type) {
@@ -125,7 +189,13 @@ class DdlTools {
125
189
  error: "column_name and column_type required for add_column",
126
190
  };
127
191
  }
128
- query += ` ADD COLUMN \`${op.column_name}\` ${op.column_type}`;
192
+ const addColumnValidation = this.validateIdentifier(op.column_name, "column name");
193
+ if (!addColumnValidation.valid)
194
+ return { status: "error", error: addColumnValidation.error };
195
+ const addTypeValidation = this.validateColumnType(op.column_type);
196
+ if (!addTypeValidation.valid)
197
+ return { status: "error", error: addTypeValidation.error };
198
+ query += ` ADD COLUMN ${this.security.escapeIdentifier(op.column_name)} ${op.column_type.trim()}`;
129
199
  if (op.nullable === false)
130
200
  query += " NOT NULL";
131
201
  if (op.default !== undefined) {
@@ -141,7 +211,10 @@ class DdlTools {
141
211
  error: "column_name required for drop_column",
142
212
  };
143
213
  }
144
- query += ` DROP COLUMN \`${op.column_name}\``;
214
+ const dropColumnValidation = this.validateIdentifier(op.column_name, "column name");
215
+ if (!dropColumnValidation.valid)
216
+ return { status: "error", error: dropColumnValidation.error };
217
+ query += ` DROP COLUMN ${this.security.escapeIdentifier(op.column_name)}`;
145
218
  break;
146
219
  case "modify_column":
147
220
  if (!op.column_name || !op.column_type) {
@@ -150,7 +223,13 @@ class DdlTools {
150
223
  error: "column_name and column_type required for modify_column",
151
224
  };
152
225
  }
153
- query += ` MODIFY COLUMN \`${op.column_name}\` ${op.column_type}`;
226
+ const modifyColumnValidation = this.validateIdentifier(op.column_name, "column name");
227
+ if (!modifyColumnValidation.valid)
228
+ return { status: "error", error: modifyColumnValidation.error };
229
+ const modifyTypeValidation = this.validateColumnType(op.column_type);
230
+ if (!modifyTypeValidation.valid)
231
+ return { status: "error", error: modifyTypeValidation.error };
232
+ query += ` MODIFY COLUMN ${this.security.escapeIdentifier(op.column_name)} ${op.column_type.trim()}`;
154
233
  if (op.nullable === false)
155
234
  query += " NOT NULL";
156
235
  if (op.default !== undefined) {
@@ -166,7 +245,16 @@ class DdlTools {
166
245
  error: "column_name, new_column_name, and column_type required for rename_column",
167
246
  };
168
247
  }
169
- query += ` CHANGE COLUMN \`${op.column_name}\` \`${op.new_column_name}\` ${op.column_type}`;
248
+ const oldColumnValidation = this.validateIdentifier(op.column_name, "column name");
249
+ if (!oldColumnValidation.valid)
250
+ return { status: "error", error: oldColumnValidation.error };
251
+ const newColumnValidation = this.validateIdentifier(op.new_column_name, "new column name");
252
+ if (!newColumnValidation.valid)
253
+ return { status: "error", error: newColumnValidation.error };
254
+ const renameTypeValidation = this.validateColumnType(op.column_type);
255
+ if (!renameTypeValidation.valid)
256
+ return { status: "error", error: renameTypeValidation.error };
257
+ query += ` CHANGE COLUMN ${this.security.escapeIdentifier(op.column_name)} ${this.security.escapeIdentifier(op.new_column_name)} ${op.column_type.trim()}`;
170
258
  break;
171
259
  case "add_index":
172
260
  if (!op.index_name || !op.index_columns) {
@@ -175,9 +263,18 @@ class DdlTools {
175
263
  error: "index_name and index_columns required for add_index",
176
264
  };
177
265
  }
266
+ const addIndexValidation = this.validateIdentifier(op.index_name, "index name");
267
+ if (!addIndexValidation.valid)
268
+ return { status: "error", error: addIndexValidation.error };
178
269
  const indexType = op.unique ? "UNIQUE INDEX" : "INDEX";
179
- const columns = op.index_columns.map((c) => `\`${c}\``).join(", ");
180
- query += ` ADD ${indexType} \`${op.index_name}\` (${columns})`;
270
+ const columns = op.index_columns.map((c) => {
271
+ const columnValidation = this.validateIdentifier(c, "index column name");
272
+ if (!columnValidation.valid) {
273
+ throw new Error(columnValidation.error);
274
+ }
275
+ return this.security.escapeIdentifier(c);
276
+ }).join(", ");
277
+ query += ` ADD ${indexType} ${this.security.escapeIdentifier(op.index_name)} (${columns})`;
181
278
  break;
182
279
  case "drop_index":
183
280
  if (!op.index_name) {
@@ -186,7 +283,10 @@ class DdlTools {
186
283
  error: "index_name required for drop_index",
187
284
  };
188
285
  }
189
- query += ` DROP INDEX \`${op.index_name}\``;
286
+ const dropIndexValidation = this.validateIdentifier(op.index_name, "index name");
287
+ if (!dropIndexValidation.valid)
288
+ return { status: "error", error: dropIndexValidation.error };
289
+ query += ` DROP INDEX ${this.security.escapeIdentifier(op.index_name)}`;
190
290
  break;
191
291
  default:
192
292
  return {
@@ -218,8 +318,12 @@ class DdlTools {
218
318
  async dropTable(params) {
219
319
  try {
220
320
  const { table_name, if_exists } = params;
321
+ const tableValidation = this.validateIdentifier(table_name, "table name");
322
+ if (!tableValidation.valid) {
323
+ return { status: "error", error: tableValidation.error };
324
+ }
221
325
  const ifExistsClause = if_exists ? "IF EXISTS " : "";
222
- const query = `DROP TABLE ${ifExistsClause}\`${table_name}\``;
326
+ const query = `DROP TABLE ${ifExistsClause}${this.security.escapeIdentifier(table_name)}`;
223
327
  await this.db.query(query);
224
328
  return {
225
329
  status: "success",
@@ -242,13 +346,14 @@ class DdlTools {
242
346
  async executeDdl(params) {
243
347
  try {
244
348
  const { query } = params;
245
- // Basic validation - ensure it's a DDL query
246
- const upperQuery = query.trim().toUpperCase();
247
- const isDdl = upperQuery.startsWith("CREATE") ||
248
- upperQuery.startsWith("ALTER") ||
249
- upperQuery.startsWith("DROP") ||
250
- upperQuery.startsWith("TRUNCATE") ||
251
- upperQuery.startsWith("RENAME");
349
+ const queryValidation = this.security.validateQuery(query);
350
+ if (!queryValidation.valid) {
351
+ return {
352
+ status: "error",
353
+ error: `DDL validation failed: ${queryValidation.error}`,
354
+ };
355
+ }
356
+ const isDdl = ["CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"].includes(queryValidation.queryType || "");
252
357
  if (!isDdl) {
253
358
  return {
254
359
  status: "error",
@@ -0,0 +1,207 @@
1
+ import SecurityLayer from "../security/securityLayer";
2
+ type RowsPerTable = number | Record<string, number>;
3
+ type SeedDomain = "auto" | "generic" | "ecommerce" | "pos" | "crm";
4
+ type SeedTemplate = "ecommerce" | "pos" | "crm";
5
+ type TemplateScale = "small" | "medium" | "large";
6
+ interface SeedRule {
7
+ generator?: string;
8
+ values?: any[];
9
+ value?: any;
10
+ min?: number;
11
+ max?: number;
12
+ start?: string;
13
+ end?: string;
14
+ prefix?: string;
15
+ pattern?: string;
16
+ domain?: string;
17
+ nullable?: boolean;
18
+ source?: string;
19
+ }
20
+ interface TableRequirement {
21
+ table: string;
22
+ rows_to_create: number;
23
+ reason: string;
24
+ existing_rows: number;
25
+ }
26
+ interface SeedPlan {
27
+ plan_id: string;
28
+ database: string;
29
+ dependency_order: string[];
30
+ tables_required: TableRequirement[];
31
+ constraints_detected: {
32
+ foreign_keys: string[];
33
+ unique: string[];
34
+ required_columns: string[];
35
+ };
36
+ seed_rules: Record<string, SeedRule>;
37
+ warnings: string[];
38
+ requires_confirmation: boolean;
39
+ confirm_token: string;
40
+ random_seed: number;
41
+ options: {
42
+ strategy: string;
43
+ respect_existing_data: boolean;
44
+ include_dependencies: boolean;
45
+ include_children: boolean;
46
+ child_rows_per_parent: number;
47
+ max_rows_per_table: number;
48
+ };
49
+ }
50
+ interface PlanSeedDataParams {
51
+ database?: string;
52
+ target_tables: string[];
53
+ rows_per_table?: RowsPerTable;
54
+ include_dependencies?: boolean;
55
+ include_children?: boolean;
56
+ child_rows_per_parent?: number;
57
+ respect_existing_data?: boolean;
58
+ strategy?: "append";
59
+ random_seed?: number;
60
+ max_rows_per_table?: number;
61
+ max_related_tables?: number;
62
+ seed_rules?: Record<string, SeedRule>;
63
+ require_confirmation?: boolean;
64
+ }
65
+ interface GenerateSeedPreviewParams {
66
+ plan_id: string;
67
+ locale?: string;
68
+ realistic?: boolean;
69
+ max_preview_rows_per_table?: number;
70
+ email_domain?: string;
71
+ }
72
+ interface ExecuteSeedPlanParams {
73
+ plan_id: string;
74
+ dry_run?: boolean;
75
+ use_transaction?: boolean;
76
+ batch_size?: number;
77
+ on_error?: "rollback" | "stop";
78
+ confirm_token?: string;
79
+ allow_production?: boolean;
80
+ email_domain?: string;
81
+ }
82
+ interface ValidateSeedIntegrityParams {
83
+ plan_id: string;
84
+ tables?: string[];
85
+ check_foreign_keys?: boolean;
86
+ check_orphans?: boolean;
87
+ check_required_columns?: boolean;
88
+ check_unique_collisions?: boolean;
89
+ check_row_counts?: boolean;
90
+ }
91
+ interface InferSeedRulesParams {
92
+ database?: string;
93
+ tables?: string[];
94
+ domain?: SeedDomain;
95
+ sample_size?: number;
96
+ max_tables?: number;
97
+ }
98
+ interface SeedFromTemplateParams {
99
+ database?: string;
100
+ template: SeedTemplate;
101
+ scale?: TemplateScale;
102
+ include?: string[];
103
+ exclude?: string[];
104
+ rows_per_table?: RowsPerTable;
105
+ include_dependencies?: boolean;
106
+ include_children?: boolean;
107
+ respect_existing_data?: boolean;
108
+ random_seed?: number;
109
+ require_confirmation?: boolean;
110
+ }
111
+ export declare class RelationalSeederTools {
112
+ private static planCounter;
113
+ private db;
114
+ private security;
115
+ private plans;
116
+ constructor(security: SecurityLayer);
117
+ planSeedData(params: PlanSeedDataParams): Promise<{
118
+ status: string;
119
+ data?: SeedPlan;
120
+ error?: string;
121
+ }>;
122
+ generateSeedPreview(params: GenerateSeedPreviewParams): Promise<{
123
+ status: string;
124
+ data?: any;
125
+ error?: string;
126
+ }>;
127
+ executeSeedPlan(params: ExecuteSeedPlanParams): Promise<{
128
+ status: string;
129
+ data?: any;
130
+ error?: string;
131
+ }>;
132
+ validateSeedIntegrity(params: ValidateSeedIntegrityParams): Promise<{
133
+ status: string;
134
+ data?: any;
135
+ error?: string;
136
+ }>;
137
+ inferSeedRules(params: InferSeedRulesParams): Promise<{
138
+ status: string;
139
+ data?: any;
140
+ error?: string;
141
+ }>;
142
+ seedFromTemplate(params: SeedFromTemplateParams): Promise<{
143
+ status: string;
144
+ data?: any;
145
+ error?: string;
146
+ }>;
147
+ private resolveDatabase;
148
+ private loadSchema;
149
+ private groupForeignKeys;
150
+ private topologicalSort;
151
+ private calculateRowsToCreate;
152
+ private buildInferredSeedRules;
153
+ private inferRuleForColumn;
154
+ private detectConstraints;
155
+ private buildPreview;
156
+ private buildRowsForTable;
157
+ private resolveForeignKeyTuple;
158
+ private generateValue;
159
+ private preloadExistingParentIds;
160
+ private ensureIdMapForTable;
161
+ private loadExistingKeyTuples;
162
+ private insertRow;
163
+ private trackInsertedKeys;
164
+ private countForeignKeyOrphans;
165
+ private countNullValues;
166
+ private findUniqueCollisions;
167
+ private getValidationRowCount;
168
+ private getRequiredColumns;
169
+ private shouldSkipColumn;
170
+ private isIntegerColumn;
171
+ private isNumberColumn;
172
+ private isBooleanColumn;
173
+ private extractEnumValues;
174
+ private isForeignKeyColumn;
175
+ private keySignature;
176
+ private formatForeignKey;
177
+ private tupleFromRow;
178
+ private storeTuples;
179
+ private buildTupleWhereClause;
180
+ private getSelectableTables;
181
+ private loadSampleRows;
182
+ private refineRulesFromSamples;
183
+ private isSensitiveColumn;
184
+ private canUseChoiceFromSamples;
185
+ private inferPatternFromSamples;
186
+ private detectDomainFromTables;
187
+ private getDomainRule;
188
+ private detectTemplateTables;
189
+ private getTemplateKeywords;
190
+ private getScaleRows;
191
+ private buildTemplateRules;
192
+ private requirementsToInsertMap;
193
+ private countResolvedForeignKeys;
194
+ private getExplicitOrDefaultRows;
195
+ private assertIdentifier;
196
+ private getStoredPlan;
197
+ private createPlanId;
198
+ private createConfirmToken;
199
+ private isProductionLikeDatabase;
200
+ private clampNumber;
201
+ private makeRng;
202
+ private hashString;
203
+ private truncate;
204
+ private pickPersonName;
205
+ private formatDate;
206
+ }
207
+ export {};