@berthojoris/mcp-mysql-server 1.16.3 → 1.16.4

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,359 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SchemaDesignTools = void 0;
4
+ class SchemaDesignTools {
5
+ constructor(security) {
6
+ this.security = security;
7
+ }
8
+ async designSchemaFromRequirements(params) {
9
+ try {
10
+ const requirementsText = params?.requirements_text?.trim();
11
+ if (!requirementsText) {
12
+ return { status: "error", error: "requirements_text is required" };
13
+ }
14
+ const naming = params.naming_convention ?? "snake_case";
15
+ const includeAudit = params.include_audit_columns ?? true;
16
+ const idType = params.id_type ?? "BIGINT";
17
+ const engine = params.engine ?? "InnoDB";
18
+ const charset = params.charset ?? "utf8mb4";
19
+ const collation = params.collation ?? "utf8mb4_unicode_ci";
20
+ const notes = [
21
+ "This tool uses deterministic heuristics (no external LLM) and may require manual refinement.",
22
+ "Review data types, nullability, and indexes before applying DDL in production.",
23
+ ];
24
+ const extracted = this.extractEntitiesAndRelations(requirementsText);
25
+ const explicitEntities = (params.entities || [])
26
+ .map((e) => ({
27
+ name: e.name,
28
+ fields: e.fields || [],
29
+ }))
30
+ .filter((e) => !!e.name?.trim());
31
+ const entityNames = new Set();
32
+ for (const e of explicitEntities)
33
+ entityNames.add(e.name.trim());
34
+ for (const e of extracted.entities)
35
+ entityNames.add(e);
36
+ if (entityNames.size === 0) {
37
+ // Fallback: create a single generic table
38
+ entityNames.add("items");
39
+ notes.push("No entities could be inferred from the text; generated a single fallback table 'items'.");
40
+ }
41
+ // Build initial table specs
42
+ const tablesSpec = Array.from(entityNames).map((rawName, idx) => {
43
+ const tableName = this.normalizeIdentifier(rawName, naming, `entity_${idx + 1}`);
44
+ const idColumn = idType === "UUID"
45
+ ? { name: "id", type: "CHAR(36)", nullable: false, primary_key: true }
46
+ : {
47
+ name: "id",
48
+ type: "BIGINT UNSIGNED",
49
+ nullable: false,
50
+ primary_key: true,
51
+ };
52
+ const baseColumns = [
53
+ idType === "UUID"
54
+ ? idColumn
55
+ : { ...idColumn, type: "BIGINT UNSIGNED AUTO_INCREMENT" },
56
+ ];
57
+ if (includeAudit) {
58
+ baseColumns.push({ name: "created_at", type: "DATETIME", nullable: false }, { name: "updated_at", type: "DATETIME", nullable: false });
59
+ }
60
+ return {
61
+ table_name: tableName,
62
+ columns: baseColumns,
63
+ indexes: [],
64
+ _raw_name: rawName,
65
+ };
66
+ });
67
+ const rawNameToTable = new Map();
68
+ for (const t of tablesSpec) {
69
+ rawNameToTable.set(t._raw_name, t.table_name);
70
+ }
71
+ // Apply field hints from explicit entities and text patterns
72
+ for (const t of tablesSpec) {
73
+ const explicit = explicitEntities.find((e) => this.normalizeLoose(e.name) === this.normalizeLoose(t._raw_name));
74
+ const fieldsFromText = extracted.fieldsByEntity.get(t._raw_name) || [];
75
+ const hintedFields = [
76
+ ...(explicit?.fields || []),
77
+ ...fieldsFromText,
78
+ ].map((f) => f.trim());
79
+ for (const field of hintedFields) {
80
+ const colName = this.normalizeIdentifier(field, naming, undefined);
81
+ if (!colName || colName === "id" || colName === "created_at" || colName === "updated_at") {
82
+ continue;
83
+ }
84
+ if (t.columns.some((c) => c.name === colName))
85
+ continue;
86
+ const inferred = this.inferColumnType(colName);
87
+ t.columns.push(inferred.column);
88
+ if (inferred.uniqueIndex) {
89
+ t.indexes.push({
90
+ name: this.makeIndexName(t.table_name, [colName], true),
91
+ columns: [colName],
92
+ unique: true,
93
+ });
94
+ }
95
+ }
96
+ }
97
+ // Apply inferred relationships
98
+ const relationships = [];
99
+ for (const rel of extracted.relationships) {
100
+ const fromTableRaw = rel.from;
101
+ const toTableRaw = rel.to;
102
+ const fromTable = this.findTableNameForRaw(fromTableRaw, tablesSpec) ?? this.normalizeIdentifier(fromTableRaw, naming, undefined);
103
+ const toTable = this.findTableNameForRaw(toTableRaw, tablesSpec) ?? this.normalizeIdentifier(toTableRaw, naming, undefined);
104
+ if (!fromTable || !toTable)
105
+ continue;
106
+ const parentTable = rel.type === "many_to_one" ? toTable : fromTable;
107
+ const childTable = rel.type === "many_to_one" ? fromTable : toTable;
108
+ const child = tablesSpec.find((t) => t.table_name === childTable);
109
+ const parent = tablesSpec.find((t) => t.table_name === parentTable);
110
+ if (!child || !parent)
111
+ continue;
112
+ const fkColumn = this.normalizeIdentifier(`${parentTable}_id`, naming, `${parentTable}_id`);
113
+ if (!child.columns.some((c) => c.name === fkColumn)) {
114
+ child.columns.push({
115
+ name: fkColumn,
116
+ type: idType === "UUID" ? "CHAR(36)" : "BIGINT UNSIGNED",
117
+ nullable: false,
118
+ references: { table: parentTable, column: "id" },
119
+ });
120
+ child.indexes.push({
121
+ name: this.makeIndexName(childTable, [fkColumn], false),
122
+ columns: [fkColumn],
123
+ });
124
+ }
125
+ relationships.push({
126
+ from_table: childTable,
127
+ from_column: fkColumn,
128
+ to_table: parentTable,
129
+ to_column: "id",
130
+ type: "many_to_one",
131
+ });
132
+ }
133
+ // Generate DDL
134
+ const ddlStatements = [];
135
+ for (const t of tablesSpec) {
136
+ ddlStatements.push(this.generateCreateTableDDL(t, engine, charset, collation));
137
+ for (const idx of t.indexes) {
138
+ ddlStatements.push(this.generateCreateIndexDDL(t.table_name, idx));
139
+ }
140
+ }
141
+ // Basic sanity: validate generated identifiers
142
+ for (const t of tablesSpec) {
143
+ if (!this.security.validateIdentifier(t.table_name).valid) {
144
+ notes.push(`Generated table name '${t.table_name}' may be invalid. Consider renaming.`);
145
+ }
146
+ for (const c of t.columns) {
147
+ if (!this.security.validateIdentifier(c.name).valid) {
148
+ notes.push(`Generated column name '${t.table_name}.${c.name}' may be invalid. Consider renaming.`);
149
+ }
150
+ }
151
+ }
152
+ return {
153
+ status: "success",
154
+ data: {
155
+ input: {
156
+ requirements_text: requirementsText,
157
+ inferred_entities_count: tablesSpec.length,
158
+ },
159
+ tables: tablesSpec.map(({ _raw_name, ...t }) => t),
160
+ relationships,
161
+ ddl_statements: ddlStatements,
162
+ notes,
163
+ },
164
+ };
165
+ }
166
+ catch (error) {
167
+ return { status: "error", error: error.message };
168
+ }
169
+ }
170
+ normalizeLoose(value) {
171
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
172
+ }
173
+ toSnakeCase(value) {
174
+ return value
175
+ .trim()
176
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
177
+ .replace(/[^a-zA-Z0-9]+/g, "_")
178
+ .replace(/^_+|_+$/g, "")
179
+ .replace(/_+/g, "_")
180
+ .toLowerCase();
181
+ }
182
+ toCamelCase(value) {
183
+ const parts = value
184
+ .trim()
185
+ .replace(/[^a-zA-Z0-9]+/g, " ")
186
+ .split(/\s+/)
187
+ .filter(Boolean);
188
+ if (parts.length === 0)
189
+ return "";
190
+ return (parts[0].toLowerCase() +
191
+ parts
192
+ .slice(1)
193
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())
194
+ .join(""));
195
+ }
196
+ normalizeIdentifier(raw, naming, fallback) {
197
+ const base = naming === "camelCase" ? this.toCamelCase(raw) : this.toSnakeCase(raw);
198
+ let ident = base;
199
+ if (!ident)
200
+ ident = fallback || "";
201
+ if (!ident)
202
+ return "";
203
+ // Ensure valid start char
204
+ if (!/^[a-zA-Z_$]/.test(ident)) {
205
+ ident = `_${ident}`;
206
+ }
207
+ // Strip invalid chars (safety)
208
+ ident = ident.replace(/[^a-zA-Z0-9_$]/g, "_");
209
+ ident = ident.slice(0, 64);
210
+ // If still invalid, fallback
211
+ if (!this.security.validateIdentifier(ident).valid) {
212
+ const fb = fallback ? this.toSnakeCase(fallback) : "entity";
213
+ const safe = fb && this.security.validateIdentifier(fb).valid ? fb : "entity";
214
+ return safe;
215
+ }
216
+ return ident;
217
+ }
218
+ inferColumnType(colName) {
219
+ const name = colName.toLowerCase();
220
+ if (name === "email") {
221
+ return {
222
+ column: { name: colName, type: "VARCHAR(255)", nullable: false, unique: true },
223
+ uniqueIndex: true,
224
+ };
225
+ }
226
+ if (name.endsWith("_id")) {
227
+ return { column: { name: colName, type: "BIGINT UNSIGNED", nullable: false } };
228
+ }
229
+ if (name.includes("amount") ||
230
+ name.includes("price") ||
231
+ name.includes("total") ||
232
+ name.includes("cost")) {
233
+ return { column: { name: colName, type: "DECIMAL(10,2)", nullable: false } };
234
+ }
235
+ if (name.startsWith("is_") || name.startsWith("has_") || name.includes("enabled") || name.includes("active")) {
236
+ return { column: { name: colName, type: "BOOLEAN", nullable: false } };
237
+ }
238
+ if (name.endsWith("_at") || name.includes("date") || name.includes("time")) {
239
+ return { column: { name: colName, type: "DATETIME", nullable: true } };
240
+ }
241
+ if (name.includes("count") || name.includes("qty") || name.includes("quantity") || name.includes("number")) {
242
+ return { column: { name: colName, type: "INT", nullable: false } };
243
+ }
244
+ if (name.includes("description") || name.includes("notes") || name.includes("comment")) {
245
+ return { column: { name: colName, type: "TEXT", nullable: true } };
246
+ }
247
+ if (name.includes("name") || name.includes("title") || name.includes("status")) {
248
+ return { column: { name: colName, type: "VARCHAR(255)", nullable: false } };
249
+ }
250
+ return { column: { name: colName, type: "VARCHAR(255)", nullable: true } };
251
+ }
252
+ extractEntitiesAndRelations(text) {
253
+ const entities = new Set();
254
+ const relationships = [];
255
+ const fieldsByEntity = new Map();
256
+ const raw = text;
257
+ // Entity hints: "table for X", "entity X", "model X"
258
+ for (const match of raw.matchAll(/\b(?:table|entity|model)\s+(?:for\s+)?([a-zA-Z][a-zA-Z0-9 _-]{1,40})/gi)) {
259
+ const name = match[1].trim();
260
+ if (name)
261
+ entities.add(name);
262
+ }
263
+ // Relationship patterns: "A has many B" / "A belongs to B"
264
+ for (const match of raw.matchAll(/\b([a-zA-Z][a-zA-Z0-9_-]{1,40})\s+has\s+many\s+([a-zA-Z][a-zA-Z0-9_-]{1,40})\b/gi)) {
265
+ const from = match[1].trim();
266
+ const to = match[2].trim();
267
+ if (from && to) {
268
+ entities.add(from);
269
+ entities.add(to);
270
+ relationships.push({ from, to, type: "one_to_many" });
271
+ }
272
+ }
273
+ for (const match of raw.matchAll(/\b([a-zA-Z][a-zA-Z0-9_-]{1,40})\s+belongs\s+to\s+([a-zA-Z][a-zA-Z0-9_-]{1,40})\b/gi)) {
274
+ const from = match[1].trim();
275
+ const to = match[2].trim();
276
+ if (from && to) {
277
+ entities.add(from);
278
+ entities.add(to);
279
+ relationships.push({ from, to, type: "many_to_one" });
280
+ }
281
+ }
282
+ // Field list patterns: "X fields: a, b, c" or "X columns: ..."
283
+ for (const match of raw.matchAll(/\b([a-zA-Z][a-zA-Z0-9_-]{1,40})\s+(?:fields|columns|attributes)\s*:\s*([^\n\.]+)(?:\.|\n|$)/gi)) {
284
+ const entity = match[1].trim();
285
+ const list = match[2].trim();
286
+ if (!entity || !list)
287
+ continue;
288
+ entities.add(entity);
289
+ const fields = list
290
+ .split(",")
291
+ .map((s) => s.trim())
292
+ .filter(Boolean)
293
+ .map((s) => s.replace(/[^a-zA-Z0-9 _-]/g, "").trim())
294
+ .filter(Boolean);
295
+ if (fields.length > 0) {
296
+ fieldsByEntity.set(entity, fields);
297
+ }
298
+ }
299
+ return {
300
+ entities: Array.from(entities.values()),
301
+ relationships,
302
+ fieldsByEntity,
303
+ };
304
+ }
305
+ findTableNameForRaw(raw, tablesSpec) {
306
+ const target = this.normalizeLoose(raw);
307
+ const match = tablesSpec.find((t) => this.normalizeLoose(t._raw_name) === target);
308
+ return match?.table_name;
309
+ }
310
+ makeIndexName(table, columns, unique) {
311
+ const base = `${unique ? "uidx" : "idx"}_${table}_${columns.join("_")}`;
312
+ const name = base.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 64);
313
+ return name;
314
+ }
315
+ generateCreateTableDDL(table, engine, charset, collation) {
316
+ const colLines = [];
317
+ const pk = [];
318
+ const fkLines = [];
319
+ for (const c of table.columns) {
320
+ const nullSql = c.nullable ? "NULL" : "NOT NULL";
321
+ colLines.push(" `" + c.name + "` " + c.type + " " + nullSql);
322
+ if (c.primary_key)
323
+ pk.push(c.name);
324
+ if (c.references) {
325
+ const fkName = `fk_${table.table_name}_${c.name}`.slice(0, 64);
326
+ fkLines.push(" CONSTRAINT `" +
327
+ fkName +
328
+ "` FOREIGN KEY (`" +
329
+ c.name +
330
+ "`) REFERENCES `" +
331
+ c.references.table +
332
+ "`(`" +
333
+ c.references.column +
334
+ "`)");
335
+ }
336
+ }
337
+ if (pk.length > 0) {
338
+ colLines.push(" PRIMARY KEY (" + pk.map((c) => "`" + c + "`").join(", ") + ")");
339
+ }
340
+ const allLines = [...colLines, ...fkLines];
341
+ return ("CREATE TABLE `" +
342
+ table.table_name +
343
+ "` (\n" +
344
+ allLines.join(",\n") +
345
+ "\n) ENGINE=" +
346
+ engine +
347
+ " DEFAULT CHARSET=" +
348
+ charset +
349
+ " COLLATE=" +
350
+ collation +
351
+ ";");
352
+ }
353
+ generateCreateIndexDDL(tableName, idx) {
354
+ const uniq = idx.unique ? "UNIQUE " : "";
355
+ const cols = idx.columns.map((c) => "`" + c + "`").join(", ");
356
+ return "CREATE " + uniq + "INDEX `" + idx.name + "` ON `" + tableName + "` (" + cols + ");";
357
+ }
358
+ }
359
+ exports.SchemaDesignTools = SchemaDesignTools;
@@ -0,0 +1,39 @@
1
+ type Severity = "info" | "low" | "medium" | "high" | "critical";
2
+ export declare class SecurityAuditTools {
3
+ private db;
4
+ constructor();
5
+ private validateDatabaseAccess;
6
+ auditDatabaseSecurity(params?: {
7
+ database?: string;
8
+ include_user_account_checks?: boolean;
9
+ include_privilege_checks?: boolean;
10
+ }): Promise<{
11
+ status: string;
12
+ data?: {
13
+ database: string;
14
+ findings: Array<{
15
+ severity: Severity;
16
+ title: string;
17
+ evidence?: string;
18
+ recommendation: string;
19
+ }>;
20
+ summary: {
21
+ critical: number;
22
+ high: number;
23
+ medium: number;
24
+ low: number;
25
+ info: number;
26
+ };
27
+ notes: string[];
28
+ };
29
+ error?: string;
30
+ }>;
31
+ private severityRank;
32
+ private summarizeFindings;
33
+ private asOnOff;
34
+ private asInt;
35
+ private readVariables;
36
+ private tryReadUserAccounts;
37
+ private tryReadPrivilegeSummaries;
38
+ }
39
+ export {};