@berthojoris/mcp-mysql-server 1.40.5 → 1.40.7

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.
@@ -62,6 +62,33 @@ class DataExportTools {
62
62
  .replace(/\0/g, "\\0");
63
63
  return `'${escaped}'`;
64
64
  }
65
+ escapeCsvValue(value) {
66
+ if (value === null || value === undefined)
67
+ return "";
68
+ const normalizedValue = value instanceof Date
69
+ ? value.toISOString()
70
+ : Buffer.isBuffer(value)
71
+ ? value.toString("base64")
72
+ : String(value);
73
+ if (/[",\r\n]/.test(normalizedValue)) {
74
+ return `"${normalizedValue.replace(/"/g, '""')}"`;
75
+ }
76
+ return normalizedValue;
77
+ }
78
+ rowsToCSV(rows, includeHeaders) {
79
+ if (rows.length === 0) {
80
+ return "";
81
+ }
82
+ const columns = Object.keys(rows[0]);
83
+ const csvRows = [];
84
+ if (includeHeaders) {
85
+ csvRows.push(columns.map((column) => this.escapeCsvValue(column)).join(","));
86
+ }
87
+ for (const row of rows) {
88
+ csvRows.push(columns.map((column) => this.escapeCsvValue(row[column])).join(","));
89
+ }
90
+ return `${csvRows.join("\n")}\n`;
91
+ }
65
92
  /**
66
93
  * Export table data to CSV format
67
94
  */
@@ -179,33 +206,7 @@ class DataExportTools {
179
206
  },
180
207
  };
181
208
  }
182
- // Generate CSV
183
- let csv = "";
184
- // Add headers if requested
185
- if (include_headers) {
186
- const headers = Object.keys(results[0]).join(",");
187
- csv += headers + "\n";
188
- }
189
- // Add data rows
190
- for (const row of results) {
191
- const values = Object.values(row)
192
- .map((value) => {
193
- if (value === null)
194
- return "";
195
- if (typeof value === "string") {
196
- // Escape quotes and wrap in quotes if contains comma or newline
197
- if (value.includes(",") ||
198
- value.includes("\n") ||
199
- value.includes('"')) {
200
- return `"${value.replace(/"/g, '""')}"`;
201
- }
202
- return value;
203
- }
204
- return String(value);
205
- })
206
- .join(",");
207
- csv += values + "\n";
208
- }
209
+ const csv = this.rowsToCSV(results, include_headers);
209
210
  return {
210
211
  status: "success",
211
212
  data: {
@@ -221,5 +222,45 @@ class DataExportTools {
221
222
  };
222
223
  }
223
224
  }
225
+ async exportQueryToCSV(queryParams) {
226
+ try {
227
+ const { query, params = [], include_headers = true, } = queryParams;
228
+ const queryValidation = this.security.validateQuery(query, this.security.hasExecutePermission());
229
+ if (!queryValidation.valid) {
230
+ return {
231
+ status: "error",
232
+ error: `Query validation failed: ${queryValidation.error}`,
233
+ };
234
+ }
235
+ if (queryValidation.queryType !== "SELECT") {
236
+ return {
237
+ status: "error",
238
+ error: "export_query_to_csv only accepts SELECT queries.",
239
+ };
240
+ }
241
+ const paramValidation = this.security.validateParameters(params);
242
+ if (!paramValidation.valid) {
243
+ return {
244
+ status: "error",
245
+ error: `Parameter validation failed: ${paramValidation.error}`,
246
+ };
247
+ }
248
+ const results = await this.db.query(query, paramValidation.sanitizedParams, false);
249
+ const maskedResults = this.security.masking.processResults(results);
250
+ return {
251
+ status: "success",
252
+ data: {
253
+ csv: this.rowsToCSV(maskedResults, include_headers),
254
+ row_count: maskedResults.length,
255
+ },
256
+ };
257
+ }
258
+ catch (error) {
259
+ return {
260
+ status: "error",
261
+ error: error.message,
262
+ };
263
+ }
264
+ }
224
265
  }
225
266
  exports.DataExportTools = DataExportTools;
@@ -7,6 +7,8 @@ export declare class DdlTools {
7
7
  * Sanitize default value for SQL safety
8
8
  */
9
9
  private sanitizeDefaultValue;
10
+ private validateColumnType;
11
+ private validateIdentifier;
10
12
  /**
11
13
  * Create a new table
12
14
  */
@@ -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",
@@ -19,6 +19,11 @@ function validateToolArguments(name, args) {
19
19
  case "execute_write_query":
20
20
  case "execute_ddl":
21
21
  return (0, inputValidation_js_1.validateQuery)({ query: args?.query || "" });
22
+ case "export_query_to_csv":
23
+ return (0, inputValidation_js_1.validateQuery)({
24
+ query: args?.query || "",
25
+ params: args?.params,
26
+ });
22
27
  case "bulk_insert":
23
28
  return (0, inputValidation_js_1.validateBulkInsert)(args);
24
29
  case "list_tables":
@@ -1,3 +1,17 @@
1
+ interface RuntimeToolDefinition {
2
+ name: string;
3
+ description?: string;
4
+ inputSchema?: any;
5
+ input_schema?: any;
6
+ output_schema?: any;
7
+ }
8
+ interface ListAllToolsOptions {
9
+ tools?: RuntimeToolDefinition[];
10
+ enabledToolNames?: string[];
11
+ accessProfile?: any;
12
+ serverName?: string;
13
+ serverVersion?: string;
14
+ }
1
15
  export declare class UtilityTools {
2
16
  private db;
3
17
  constructor();
@@ -41,7 +55,7 @@ export declare class UtilityTools {
41
55
  /**
42
56
  * Lists all available tools in this MySQL MCP server
43
57
  */
44
- listAllTools(): Promise<{
58
+ listAllTools(params?: ListAllToolsOptions): Promise<{
45
59
  status: string;
46
60
  data?: any;
47
61
  error?: string;
@@ -58,3 +72,4 @@ export declare class UtilityTools {
58
72
  error?: string;
59
73
  }>;
60
74
  }
75
+ export {};
@@ -295,33 +295,88 @@ class UtilityTools {
295
295
  /**
296
296
  * Lists all available tools in this MySQL MCP server
297
297
  */
298
- async listAllTools() {
298
+ async listAllTools(params) {
299
299
  try {
300
- // Read manifest.json to get tool definitions
301
- const manifestPath = path_1.default.resolve(__dirname, "..", "..", "manifest.json");
302
- if (!fs_1.default.existsSync(manifestPath)) {
303
- return {
304
- status: "error",
305
- error: "manifest.json not found in project root.",
306
- };
300
+ let source = "runtime";
301
+ let serverName = params?.serverName || "mysql-mcp-server";
302
+ let serverVersion = params?.serverVersion || "unknown";
303
+ let toolDefinitions = params?.tools || [];
304
+ if (toolDefinitions.length === 0) {
305
+ const manifestPath = path_1.default.resolve(__dirname, "..", "..", "manifest.json");
306
+ if (!fs_1.default.existsSync(manifestPath)) {
307
+ return {
308
+ status: "error",
309
+ error: "Runtime tool catalog was not supplied and manifest.json was not found.",
310
+ };
311
+ }
312
+ const manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, "utf-8"));
313
+ source = "manifest_fallback";
314
+ serverName = manifest.name || serverName;
315
+ serverVersion = manifest.version || serverVersion;
316
+ toolDefinitions = manifest.tools || [];
307
317
  }
308
- const manifestContent = fs_1.default.readFileSync(manifestPath, "utf-8");
309
- const manifest = JSON.parse(manifestContent);
310
- const tools = manifest.tools.map((tool) => ({
318
+ const enabledToolNames = new Set(params?.enabledToolNames || toolDefinitions.map((tool) => tool.name));
319
+ const tools = toolDefinitions.map((tool) => ({
311
320
  name: tool.name,
312
321
  description: tool.description,
313
- input_schema: tool.input_schema,
314
- output_schema: tool.output_schema
322
+ enabled: enabledToolNames.has(tool.name),
323
+ input_schema: tool.inputSchema || tool.input_schema || {},
324
+ output_schema: tool.output_schema || { type: "object" },
315
325
  }));
316
326
  return {
317
327
  status: "success",
318
328
  data: {
329
+ source,
319
330
  total_tools: tools.length,
320
- server_name: manifest.name,
321
- server_version: manifest.version,
322
- server_description: manifest.description,
323
- tools: tools
324
- }
331
+ enabled_tools: tools.filter((tool) => tool.enabled).length,
332
+ disabled_tools: tools.filter((tool) => !tool.enabled).length,
333
+ server_name: serverName,
334
+ server_version: serverVersion,
335
+ access_profile: params?.accessProfile,
336
+ agent_guidance: {
337
+ recommended_first_calls: [
338
+ "describe_connection",
339
+ "list_databases",
340
+ "list_tables",
341
+ "get_schema_rag_context",
342
+ ],
343
+ workflows: {
344
+ explore_schema: [
345
+ "describe_connection",
346
+ "list_tables",
347
+ "get_database_summary",
348
+ "get_schema_rag_context",
349
+ ],
350
+ inspect_table: [
351
+ "read_table_schema",
352
+ "get_column_statistics",
353
+ "read_records",
354
+ ],
355
+ run_safe_query: [
356
+ "get_schema_rag_context",
357
+ "run_select_query with dry_run=true",
358
+ "run_select_query",
359
+ ],
360
+ export_data: [
361
+ "export_table_to_csv for simple table exports",
362
+ "export_query_to_csv for SELECT query exports",
363
+ ],
364
+ modify_data: [
365
+ "begin_transaction",
366
+ "execute_in_transaction",
367
+ "commit_transaction or rollback_transaction",
368
+ ],
369
+ },
370
+ selection_rules: [
371
+ "Use get_schema_rag_context before generating SQL to reduce token usage.",
372
+ "Use run_select_query only for SELECT statements.",
373
+ "Use execute_write_query for INSERT, UPDATE, and DELETE.",
374
+ "Use execute_ddl only for CREATE, ALTER, DROP, TRUNCATE, and RENAME.",
375
+ "Prefer structured tools over raw SQL when possible.",
376
+ ],
377
+ },
378
+ tools,
379
+ },
325
380
  };
326
381
  }
327
382
  catch (error) {