@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.
@@ -17,6 +17,8 @@ const toolArgumentValidation_js_1 = require("./tools/toolArgumentValidation.js")
17
17
  // Layer 2 (Categories): MCP_CATEGORIES (optional, for fine-grained control)
18
18
  const permissions = process.env.MCP_PERMISSIONS || process.env.MCP_CONFIG || "";
19
19
  const categories = process.env.MCP_CATEGORIES || "";
20
+ const SERVER_NAME = "mysql-mcp-server";
21
+ const SERVER_VERSION = "1.42.0";
20
22
  // Declare the MySQL MCP instance (will be initialized in main())
21
23
  let mysqlMCP;
22
24
  // Define all available tools with their schemas
@@ -404,6 +406,293 @@ const TOOLS = [
404
406
  required: ["table_name", "condition_sets"],
405
407
  },
406
408
  },
409
+ {
410
+ name: "plan_seed_data",
411
+ description: "🌱 RELATIONAL SEEDER: Analyzes target tables, foreign keys, constraints, and row counts to build a safe parent-first seed plan. Use this before generating preview or inserting dummy relational data.",
412
+ inputSchema: {
413
+ type: "object",
414
+ properties: {
415
+ database: {
416
+ type: "string",
417
+ description: "Optional: specific connected database name",
418
+ },
419
+ target_tables: {
420
+ type: "array",
421
+ description: "Target tables to seed",
422
+ minItems: 1,
423
+ items: { type: "string" },
424
+ },
425
+ rows_per_table: {
426
+ oneOf: [
427
+ { type: "number", minimum: 0 },
428
+ { type: "object", additionalProperties: { type: "number", minimum: 0 } },
429
+ ],
430
+ description: "Rows to create for target tables, either a single number or table-to-count map (default: 10)",
431
+ },
432
+ include_dependencies: {
433
+ type: "boolean",
434
+ description: "Include parent tables required by foreign keys (default: true)",
435
+ },
436
+ include_children: {
437
+ type: "boolean",
438
+ description: "Include child tables that reference included tables for nested relational seeding (default: false)",
439
+ },
440
+ child_rows_per_parent: {
441
+ type: "number",
442
+ description: "Rows to create in child tables for each parent row (default: 2)",
443
+ minimum: 1,
444
+ maximum: 20,
445
+ },
446
+ respect_existing_data: {
447
+ type: "boolean",
448
+ description: "Reuse existing parent records when possible instead of creating new parent rows (default: true)",
449
+ },
450
+ strategy: {
451
+ type: "string",
452
+ enum: ["append"],
453
+ description: "Seed strategy (currently append)",
454
+ },
455
+ random_seed: {
456
+ type: "number",
457
+ description: "Deterministic seed for repeatable preview and execution (default: 42)",
458
+ },
459
+ max_rows_per_table: {
460
+ type: "number",
461
+ description: "Safety cap for generated rows per table (default: 1000)",
462
+ minimum: 1,
463
+ maximum: 10000,
464
+ },
465
+ max_related_tables: {
466
+ type: "number",
467
+ description: "Safety cap for dependency/child expansion (default: 25)",
468
+ minimum: 1,
469
+ maximum: 100,
470
+ },
471
+ seed_rules: {
472
+ type: "object",
473
+ description: "Optional generator overrides keyed by table.column or column name",
474
+ additionalProperties: true,
475
+ },
476
+ require_confirmation: {
477
+ type: "boolean",
478
+ description: "Require confirm_token for non-dry-run execution (default: true)",
479
+ },
480
+ },
481
+ required: ["target_tables"],
482
+ },
483
+ },
484
+ {
485
+ name: "generate_seed_preview",
486
+ description: "🌱 RELATIONAL SEEDER: Generates deterministic dummy row previews from a seed plan without writing to the database. Shows symbolic foreign-key placeholders for review before execution.",
487
+ inputSchema: {
488
+ type: "object",
489
+ properties: {
490
+ plan_id: {
491
+ type: "string",
492
+ description: "Seed plan ID returned by plan_seed_data",
493
+ },
494
+ locale: {
495
+ type: "string",
496
+ description: "Optional locale hint for generated values (default: en_US)",
497
+ },
498
+ realistic: {
499
+ type: "boolean",
500
+ description: "Whether to prefer realistic generated values (default: true)",
501
+ },
502
+ max_preview_rows_per_table: {
503
+ type: "number",
504
+ description: "Maximum preview rows per table (default: 3)",
505
+ minimum: 1,
506
+ maximum: 25,
507
+ },
508
+ email_domain: {
509
+ type: "string",
510
+ description: "Safe email domain for generated emails (default: example.test)",
511
+ },
512
+ },
513
+ required: ["plan_id"],
514
+ },
515
+ },
516
+ {
517
+ name: "execute_seed_plan",
518
+ description: "🌱 RELATIONAL SEEDER: Executes a confirmed seed plan with dry-run enabled by default, production-name guard, transaction rollback on errors, and foreign-key ID resolution.",
519
+ inputSchema: {
520
+ type: "object",
521
+ properties: {
522
+ plan_id: {
523
+ type: "string",
524
+ description: "Seed plan ID returned by plan_seed_data",
525
+ },
526
+ dry_run: {
527
+ type: "boolean",
528
+ description: "If true, returns execution preview without inserting rows (default: true)",
529
+ },
530
+ use_transaction: {
531
+ type: "boolean",
532
+ description: "Use a transaction and rollback on error (default: true)",
533
+ },
534
+ batch_size: {
535
+ type: "number",
536
+ description: "Reserved batch-size hint for large plans (default: 1 for ID-safe inserts)",
537
+ minimum: 1,
538
+ maximum: 10000,
539
+ },
540
+ on_error: {
541
+ type: "string",
542
+ enum: ["rollback", "stop"],
543
+ description: "Error behavior (default: rollback)",
544
+ },
545
+ confirm_token: {
546
+ type: "string",
547
+ description: "Confirmation token returned by plan_seed_data; required when dry_run is false",
548
+ },
549
+ allow_production: {
550
+ type: "boolean",
551
+ description: "Allow writes to production-like database names (default: false)",
552
+ },
553
+ email_domain: {
554
+ type: "string",
555
+ description: "Safe email domain for generated emails (default: example.test)",
556
+ },
557
+ },
558
+ required: ["plan_id"],
559
+ },
560
+ },
561
+ {
562
+ name: "validate_seed_integrity",
563
+ description: "🌱 RELATIONAL SEEDER: Validates seed results with FK orphan checks, required-column checks, unique-collision checks, and inserted row-count checks.",
564
+ inputSchema: {
565
+ type: "object",
566
+ properties: {
567
+ plan_id: {
568
+ type: "string",
569
+ description: "Seed plan ID returned by plan_seed_data",
570
+ },
571
+ tables: {
572
+ type: "array",
573
+ description: "Optional table subset to validate",
574
+ items: { type: "string" },
575
+ },
576
+ check_foreign_keys: {
577
+ type: "boolean",
578
+ description: "Check foreign key integrity (default: true)",
579
+ },
580
+ check_orphans: {
581
+ type: "boolean",
582
+ description: "Alias for check_foreign_keys",
583
+ },
584
+ check_required_columns: {
585
+ type: "boolean",
586
+ description: "Check NOT NULL required columns (default: true)",
587
+ },
588
+ check_unique_collisions: {
589
+ type: "boolean",
590
+ description: "Check unique index collisions (default: true)",
591
+ },
592
+ check_row_counts: {
593
+ type: "boolean",
594
+ description: "Check inserted row counts when execution metadata is available (default: true)",
595
+ },
596
+ },
597
+ required: ["plan_id"],
598
+ },
599
+ },
600
+ {
601
+ name: "infer_seed_rules",
602
+ description: "🌱 ADVANCED SEEDER: Infers safe seed generator rules from schema metadata, sample row patterns, unique constraints, and optional ecommerce/POS/CRM domain presets without returning raw PII samples.",
603
+ inputSchema: {
604
+ type: "object",
605
+ properties: {
606
+ database: {
607
+ type: "string",
608
+ description: "Optional: specific connected database name",
609
+ },
610
+ tables: {
611
+ type: "array",
612
+ description: "Optional table subset to analyze; defaults to schema tables up to max_tables",
613
+ items: { type: "string" },
614
+ },
615
+ domain: {
616
+ type: "string",
617
+ enum: ["auto", "generic", "ecommerce", "pos", "crm"],
618
+ description: "Optional domain preset for better business-like dummy data (default: auto)",
619
+ },
620
+ sample_size: {
621
+ type: "number",
622
+ description: "Number of sample rows per table used to infer ranges and enum-like choices (default: 25, max: 100)",
623
+ minimum: 0,
624
+ maximum: 100,
625
+ },
626
+ max_tables: {
627
+ type: "number",
628
+ description: "Maximum number of tables to analyze when tables is omitted (default: 50)",
629
+ minimum: 1,
630
+ maximum: 200,
631
+ },
632
+ },
633
+ },
634
+ },
635
+ {
636
+ name: "seed_from_template",
637
+ description: "🌱 TEMPLATE SEEDER: Creates a plan-first FK-aware seed workflow from ecommerce, POS, or CRM templates. It detects matching tables, applies domain rules, and returns a seed plan for preview/execution.",
638
+ inputSchema: {
639
+ type: "object",
640
+ properties: {
641
+ database: {
642
+ type: "string",
643
+ description: "Optional: specific connected database name",
644
+ },
645
+ template: {
646
+ type: "string",
647
+ enum: ["ecommerce", "pos", "crm"],
648
+ description: "Business seed template to apply",
649
+ },
650
+ scale: {
651
+ type: "string",
652
+ enum: ["small", "medium", "large"],
653
+ description: "Template size preset (default: small)",
654
+ },
655
+ include: {
656
+ type: "array",
657
+ description: "Optional concrete table list to seed instead of auto-detected template matches",
658
+ items: { type: "string" },
659
+ },
660
+ exclude: {
661
+ type: "array",
662
+ description: "Optional table names to ignore during template matching",
663
+ items: { type: "string" },
664
+ },
665
+ rows_per_table: {
666
+ oneOf: [
667
+ { type: "number", minimum: 0 },
668
+ { type: "object", additionalProperties: { type: "number", minimum: 0 } },
669
+ ],
670
+ description: "Optional row-count override for the generated plan",
671
+ },
672
+ include_dependencies: {
673
+ type: "boolean",
674
+ description: "Include parent tables required by foreign keys (default: true)",
675
+ },
676
+ include_children: {
677
+ type: "boolean",
678
+ description: "Include child tables that reference included template tables (default: false)",
679
+ },
680
+ respect_existing_data: {
681
+ type: "boolean",
682
+ description: "Reuse existing parent records when possible instead of creating new parent rows (default: true)",
683
+ },
684
+ random_seed: {
685
+ type: "number",
686
+ description: "Deterministic seed for repeatable preview and execution",
687
+ },
688
+ require_confirmation: {
689
+ type: "boolean",
690
+ description: "Require confirm_token for non-dry-run execution (default: true)",
691
+ },
692
+ },
693
+ required: ["template"],
694
+ },
695
+ },
407
696
  {
408
697
  name: "run_select_query",
409
698
  description: "⚡ PRIMARY TOOL FOR SELECT QUERIES. Executes read-only SELECT statements with parameterization, optimizer hints, query caching, and dry-run mode. Supports complex queries with JOINs, subqueries, and aggregations. ⚠️ ONLY for SELECT - use execute_write_query for INSERT/UPDATE/DELETE, use execute_ddl for CREATE/ALTER/DROP.",
@@ -1862,8 +2151,8 @@ const TOOLS = [
1862
2151
  ];
1863
2152
  // Create the MCP server
1864
2153
  const server = new index_js_1.Server({
1865
- name: "mysql-mcp-server",
1866
- version: "1.40.6",
2154
+ name: SERVER_NAME,
2155
+ version: SERVER_VERSION,
1867
2156
  }, {
1868
2157
  capabilities: {
1869
2158
  tools: {},
@@ -1884,6 +2173,20 @@ const TOOL_METHOD_OVERRIDES = {
1884
2173
  export_query_to_csv: "exportQueryToCSV",
1885
2174
  };
1886
2175
  const toCamelCase = (value) => value.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
2176
+ const getRuntimeToolCatalog = () => {
2177
+ const enabledTools = (0, toolRegistry_js_1.getEnabledTools)(mysqlMCP, TOOLS);
2178
+ return {
2179
+ tools: TOOLS.map((tool) => ({
2180
+ name: tool.name,
2181
+ description: tool.description,
2182
+ inputSchema: tool.inputSchema,
2183
+ })),
2184
+ enabledToolNames: enabledTools.map((tool) => tool.name),
2185
+ accessProfile: mysqlMCP.getAccessProfile(),
2186
+ serverName: SERVER_NAME,
2187
+ serverVersion: SERVER_VERSION,
2188
+ };
2189
+ };
1887
2190
  const getCursorRequestFilePath = () => {
1888
2191
  const configuredPath = process.env.MYSQL_MCP_CURSOR_REQUEST_FILE ||
1889
2192
  process.env.MCP_MYSQL_REQUEST_FILE ||
@@ -1919,6 +2222,13 @@ const executeToolByName = async (toolName, args = {}) => {
1919
2222
  if (toolName === "cursor_execute_request") {
1920
2223
  throw new Error("cursor_execute_request cannot dispatch to itself");
1921
2224
  }
2225
+ const validation = (0, toolArgumentValidation_js_1.validateToolArguments)(toolName, args);
2226
+ if (!validation.valid) {
2227
+ throw new Error(`Validation Error: ${validation.errors?.join(", ") || "Invalid arguments"}`);
2228
+ }
2229
+ if (toolName === "list_all_tools") {
2230
+ return await mysqlMCP.listAllTools(getRuntimeToolCatalog());
2231
+ }
1922
2232
  const methodName = TOOL_METHOD_OVERRIDES[toolName] || toCamelCase(toolName);
1923
2233
  const method = mysqlMCP[methodName];
1924
2234
  if (typeof method !== "function") {
@@ -2025,6 +2335,24 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2025
2335
  case "bulk_delete":
2026
2336
  result = await mysqlMCP.bulkDelete((args || {}));
2027
2337
  break;
2338
+ case "plan_seed_data":
2339
+ result = await mysqlMCP.planSeedData(args || {});
2340
+ break;
2341
+ case "generate_seed_preview":
2342
+ result = await mysqlMCP.generateSeedPreview(args || {});
2343
+ break;
2344
+ case "execute_seed_plan":
2345
+ result = await mysqlMCP.executeSeedPlan(args || {});
2346
+ break;
2347
+ case "validate_seed_integrity":
2348
+ result = await mysqlMCP.validateSeedIntegrity(args || {});
2349
+ break;
2350
+ case "infer_seed_rules":
2351
+ result = await mysqlMCP.inferSeedRules(args || {});
2352
+ break;
2353
+ case "seed_from_template":
2354
+ result = await mysqlMCP.seedFromTemplate(args || {});
2355
+ break;
2028
2356
  // Query Tools
2029
2357
  case "run_select_query":
2030
2358
  result = await mysqlMCP.runSelectQuery((args || {}));
@@ -2055,7 +2383,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2055
2383
  result = await mysqlMCP.testConnection();
2056
2384
  break;
2057
2385
  case "list_all_tools":
2058
- result = await mysqlMCP.listAllTools();
2386
+ result = await mysqlMCP.listAllTools(getRuntimeToolCatalog());
2059
2387
  break;
2060
2388
  case "read_changelog":
2061
2389
  result = await mysqlMCP.readChangelog((args || {}));
@@ -2102,6 +2430,9 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
2102
2430
  case "export_table_to_csv":
2103
2431
  result = await mysqlMCP.exportTableToCSV((args || {}));
2104
2432
  break;
2433
+ case "export_query_to_csv":
2434
+ result = await mysqlMCP.exportQueryToCSV((args || {}));
2435
+ break;
2105
2436
  // Query Optimization Tools
2106
2437
  case "analyze_query":
2107
2438
  result = mysqlMCP.analyzeQuery((args || {}));
@@ -71,7 +71,7 @@ export declare class SecurityLayer {
71
71
  /**
72
72
  * Check if a query is a read-only SELECT query or information query (SHOW, DESCRIBE, etc.)
73
73
  */
74
- isReadOnlyQuery(query: string): boolean;
74
+ isReadOnlyQuery(query: string, bypassDangerousCheck?: boolean): boolean;
75
75
  /**
76
76
  * Check if a query contains dangerous operations
77
77
  */
@@ -404,13 +404,13 @@ class SecurityLayer {
404
404
  /**
405
405
  * Check if a query is a read-only SELECT query or information query (SHOW, DESCRIBE, etc.)
406
406
  */
407
- isReadOnlyQuery(query) {
407
+ isReadOnlyQuery(query, bypassDangerousCheck = false) {
408
408
  // Check if it's an information query first (SHOW, DESCRIBE, EXPLAIN, etc.)
409
409
  if (this.isInformationQuery(query)) {
410
410
  return true;
411
411
  }
412
412
  // Check if it's a SELECT query
413
- const validation = this.validateQuery(query);
413
+ const validation = this.validateQuery(query, bypassDangerousCheck);
414
414
  return validation.valid && validation.queryType === "SELECT";
415
415
  }
416
416
  /**
@@ -12,6 +12,8 @@ export declare class DataExportTools {
12
12
  * Escape string value for SQL INSERT statements
13
13
  */
14
14
  private escapeValue;
15
+ private escapeCsvValue;
16
+ private rowsToCSV;
15
17
  /**
16
18
  * Export table data to CSV format
17
19
  */
@@ -26,4 +28,13 @@ export declare class DataExportTools {
26
28
  data?: any;
27
29
  error?: string;
28
30
  }>;
31
+ exportQueryToCSV(queryParams: {
32
+ query: string;
33
+ params?: any[];
34
+ include_headers?: boolean;
35
+ }): Promise<{
36
+ status: string;
37
+ data?: any;
38
+ error?: string;
39
+ }>;
29
40
  }
@@ -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
  */