@berthojoris/mcp-mysql-server 1.1.0 → 1.2.1

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.
@@ -439,5 +439,415 @@ class CrudTools {
439
439
  };
440
440
  }
441
441
  }
442
+ /**
443
+ * Bulk insert multiple records into the specified table
444
+ */
445
+ async bulkInsert(params) {
446
+ // Validate input schema
447
+ if (!(0, schemas_1.validateBulkInsert)(params)) {
448
+ return {
449
+ status: 'error',
450
+ error: 'Invalid parameters: ' + JSON.stringify(schemas_1.validateBulkInsert.errors)
451
+ };
452
+ }
453
+ try {
454
+ const { table_name, data, batch_size = 1000 } = params;
455
+ // Validate table name
456
+ const tableValidation = this.security.validateIdentifier(table_name);
457
+ if (!tableValidation.valid) {
458
+ return {
459
+ status: 'error',
460
+ error: `Invalid table name: ${tableValidation.error}`
461
+ };
462
+ }
463
+ // Ensure data is not empty
464
+ if (!data || data.length === 0) {
465
+ return {
466
+ status: 'error',
467
+ error: 'Data array cannot be empty'
468
+ };
469
+ }
470
+ // Validate that all records have the same columns
471
+ const firstRecord = data[0];
472
+ const columns = Object.keys(firstRecord);
473
+ for (let i = 1; i < data.length; i++) {
474
+ const recordColumns = Object.keys(data[i]);
475
+ if (recordColumns.length !== columns.length ||
476
+ !recordColumns.every(col => columns.includes(col))) {
477
+ return {
478
+ status: 'error',
479
+ error: `All records must have the same columns. Record ${i + 1} has different columns.`
480
+ };
481
+ }
482
+ }
483
+ // Validate column names
484
+ for (const column of columns) {
485
+ const columnValidation = this.security.validateIdentifier(column);
486
+ if (!columnValidation.valid) {
487
+ return {
488
+ status: 'error',
489
+ error: `Invalid column name '${column}': ${columnValidation.error}`
490
+ };
491
+ }
492
+ }
493
+ // Process in batches
494
+ const results = [];
495
+ let totalInserted = 0;
496
+ for (let i = 0; i < data.length; i += batch_size) {
497
+ const batch = data.slice(i, i + batch_size);
498
+ // Prepare batch values
499
+ const batchValues = [];
500
+ for (const record of batch) {
501
+ const values = columns.map(col => record[col]);
502
+ // Validate and sanitize parameter values for this record
503
+ const paramValidation = this.security.validateParameters(values);
504
+ if (!paramValidation.valid) {
505
+ return {
506
+ status: 'error',
507
+ error: `Invalid parameter values in record ${i + batchValues.length / columns.length + 1}: ${paramValidation.error}`
508
+ };
509
+ }
510
+ batchValues.push(...paramValidation.sanitizedParams);
511
+ }
512
+ // Build the query with escaped identifiers
513
+ const escapedTableName = this.security.escapeIdentifier(table_name);
514
+ const escapedColumns = columns.map(col => this.security.escapeIdentifier(col));
515
+ const valueGroups = batch.map(() => `(${columns.map(() => '?').join(', ')})`).join(', ');
516
+ const query = `INSERT INTO ${escapedTableName} (${escapedColumns.join(', ')}) VALUES ${valueGroups}`;
517
+ // Execute the batch query
518
+ const result = await this.db.query(query, batchValues);
519
+ results.push({
520
+ batchNumber: Math.floor(i / batch_size) + 1,
521
+ recordsInserted: batch.length,
522
+ firstInsertId: result.insertId,
523
+ affectedRows: result.affectedRows
524
+ });
525
+ totalInserted += result.affectedRows;
526
+ }
527
+ return {
528
+ status: 'success',
529
+ data: {
530
+ totalRecords: data.length,
531
+ totalInserted,
532
+ batches: results.length,
533
+ batchResults: results
534
+ }
535
+ };
536
+ }
537
+ catch (error) {
538
+ return {
539
+ status: 'error',
540
+ error: error.message
541
+ };
542
+ }
543
+ }
544
+ /**
545
+ * Bulk update multiple records with different conditions and data
546
+ */
547
+ async bulkUpdate(params) {
548
+ // Validate input schema
549
+ if (!(0, schemas_1.validateBulkUpdate)(params)) {
550
+ return {
551
+ status: 'error',
552
+ error: 'Invalid parameters: ' + JSON.stringify(schemas_1.validateBulkUpdate.errors)
553
+ };
554
+ }
555
+ try {
556
+ const { table_name, updates, batch_size = 100 } = params;
557
+ // Validate table name
558
+ const tableValidation = this.security.validateIdentifier(table_name);
559
+ if (!tableValidation.valid) {
560
+ return {
561
+ status: 'error',
562
+ error: `Invalid table name: ${tableValidation.error}`
563
+ };
564
+ }
565
+ // Ensure updates is not empty
566
+ if (!updates || updates.length === 0) {
567
+ return {
568
+ status: 'error',
569
+ error: 'Updates array cannot be empty'
570
+ };
571
+ }
572
+ // Validate each update operation
573
+ for (let i = 0; i < updates.length; i++) {
574
+ const update = updates[i];
575
+ // Validate column names in data
576
+ const columns = Object.keys(update.data);
577
+ for (const column of columns) {
578
+ const columnValidation = this.security.validateIdentifier(column);
579
+ if (!columnValidation.valid) {
580
+ return {
581
+ status: 'error',
582
+ error: `Invalid column name '${column}' in update ${i + 1}: ${columnValidation.error}`
583
+ };
584
+ }
585
+ }
586
+ // Validate condition fields
587
+ for (const condition of update.conditions) {
588
+ const fieldValidation = this.security.validateIdentifier(condition.field);
589
+ if (!fieldValidation.valid) {
590
+ return {
591
+ status: 'error',
592
+ error: `Invalid condition field '${condition.field}' in update ${i + 1}: ${fieldValidation.error}`
593
+ };
594
+ }
595
+ }
596
+ }
597
+ // Process in batches using transactions for consistency
598
+ const results = [];
599
+ let totalAffected = 0;
600
+ for (let i = 0; i < updates.length; i += batch_size) {
601
+ const batch = updates.slice(i, i + batch_size);
602
+ // Start a transaction for this batch
603
+ await this.db.query('START TRANSACTION');
604
+ try {
605
+ const batchResults = [];
606
+ for (const update of batch) {
607
+ // Build SET clause
608
+ const setClause = Object.entries(update.data)
609
+ .map(([column, _]) => `${this.security.escapeIdentifier(column)} = ?`)
610
+ .join(', ');
611
+ const setValues = Object.values(update.data);
612
+ // Build WHERE clause
613
+ const whereConditions = [];
614
+ const whereValues = [];
615
+ update.conditions.forEach(condition => {
616
+ const escapedField = this.security.escapeIdentifier(condition.field);
617
+ switch (condition.operator) {
618
+ case 'eq':
619
+ whereConditions.push(`${escapedField} = ?`);
620
+ whereValues.push(condition.value);
621
+ break;
622
+ case 'neq':
623
+ whereConditions.push(`${escapedField} != ?`);
624
+ whereValues.push(condition.value);
625
+ break;
626
+ case 'gt':
627
+ whereConditions.push(`${escapedField} > ?`);
628
+ whereValues.push(condition.value);
629
+ break;
630
+ case 'gte':
631
+ whereConditions.push(`${escapedField} >= ?`);
632
+ whereValues.push(condition.value);
633
+ break;
634
+ case 'lt':
635
+ whereConditions.push(`${escapedField} < ?`);
636
+ whereValues.push(condition.value);
637
+ break;
638
+ case 'lte':
639
+ whereConditions.push(`${escapedField} <= ?`);
640
+ whereValues.push(condition.value);
641
+ break;
642
+ case 'like':
643
+ whereConditions.push(`${escapedField} LIKE ?`);
644
+ whereValues.push(`%${condition.value}%`);
645
+ break;
646
+ case 'in':
647
+ if (Array.isArray(condition.value)) {
648
+ const placeholders = condition.value.map(() => '?').join(', ');
649
+ whereConditions.push(`${escapedField} IN (${placeholders})`);
650
+ whereValues.push(...condition.value);
651
+ }
652
+ break;
653
+ }
654
+ });
655
+ const whereClause = whereConditions.length > 0
656
+ ? 'WHERE ' + whereConditions.join(' AND ')
657
+ : '';
658
+ // Validate all parameters
659
+ const allParams = [...setValues, ...whereValues];
660
+ const paramValidation = this.security.validateParameters(allParams);
661
+ if (!paramValidation.valid) {
662
+ throw new Error(`Invalid parameters: ${paramValidation.error}`);
663
+ }
664
+ // Build and execute the query
665
+ const escapedTableName = this.security.escapeIdentifier(table_name);
666
+ const query = `UPDATE ${escapedTableName} SET ${setClause} ${whereClause}`;
667
+ const result = await this.db.query(query, paramValidation.sanitizedParams);
668
+ batchResults.push({
669
+ affectedRows: result.affectedRows
670
+ });
671
+ totalAffected += result.affectedRows;
672
+ }
673
+ // Commit the transaction
674
+ await this.db.query('COMMIT');
675
+ results.push({
676
+ batchNumber: Math.floor(i / batch_size) + 1,
677
+ updatesProcessed: batch.length,
678
+ results: batchResults
679
+ });
680
+ }
681
+ catch (error) {
682
+ // Rollback on error
683
+ await this.db.query('ROLLBACK');
684
+ throw error;
685
+ }
686
+ }
687
+ return {
688
+ status: 'success',
689
+ data: {
690
+ totalUpdates: updates.length,
691
+ totalAffectedRows: totalAffected,
692
+ batches: results.length,
693
+ batchResults: results
694
+ }
695
+ };
696
+ }
697
+ catch (error) {
698
+ return {
699
+ status: 'error',
700
+ error: error.message
701
+ };
702
+ }
703
+ }
704
+ /**
705
+ * Bulk delete records based on multiple condition sets
706
+ */
707
+ async bulkDelete(params) {
708
+ // Validate input schema
709
+ if (!(0, schemas_1.validateBulkDelete)(params)) {
710
+ return {
711
+ status: 'error',
712
+ error: 'Invalid parameters: ' + JSON.stringify(schemas_1.validateBulkDelete.errors)
713
+ };
714
+ }
715
+ try {
716
+ const { table_name, condition_sets, batch_size = 100 } = params;
717
+ // Validate table name
718
+ const tableValidation = this.security.validateIdentifier(table_name);
719
+ if (!tableValidation.valid) {
720
+ return {
721
+ status: 'error',
722
+ error: `Invalid table name: ${tableValidation.error}`
723
+ };
724
+ }
725
+ // Ensure condition_sets is not empty
726
+ if (!condition_sets || condition_sets.length === 0) {
727
+ return {
728
+ status: 'error',
729
+ error: 'Condition sets array cannot be empty'
730
+ };
731
+ }
732
+ // Validate each condition set
733
+ for (let i = 0; i < condition_sets.length; i++) {
734
+ const conditions = condition_sets[i];
735
+ // Ensure there are conditions for safety
736
+ if (!conditions || conditions.length === 0) {
737
+ return {
738
+ status: 'error',
739
+ error: `DELETE operations require at least one condition for safety. Condition set ${i + 1} is empty.`
740
+ };
741
+ }
742
+ // Validate condition fields
743
+ for (const condition of conditions) {
744
+ const fieldValidation = this.security.validateIdentifier(condition.field);
745
+ if (!fieldValidation.valid) {
746
+ return {
747
+ status: 'error',
748
+ error: `Invalid condition field '${condition.field}' in condition set ${i + 1}: ${fieldValidation.error}`
749
+ };
750
+ }
751
+ }
752
+ }
753
+ // Process in batches using transactions for consistency
754
+ const results = [];
755
+ let totalDeleted = 0;
756
+ for (let i = 0; i < condition_sets.length; i += batch_size) {
757
+ const batch = condition_sets.slice(i, i + batch_size);
758
+ // Start a transaction for this batch
759
+ await this.db.query('START TRANSACTION');
760
+ try {
761
+ const batchResults = [];
762
+ for (const conditions of batch) {
763
+ // Build WHERE clause
764
+ const whereConditions = [];
765
+ const whereValues = [];
766
+ conditions.forEach(condition => {
767
+ const escapedField = this.security.escapeIdentifier(condition.field);
768
+ switch (condition.operator) {
769
+ case 'eq':
770
+ whereConditions.push(`${escapedField} = ?`);
771
+ whereValues.push(condition.value);
772
+ break;
773
+ case 'neq':
774
+ whereConditions.push(`${escapedField} != ?`);
775
+ whereValues.push(condition.value);
776
+ break;
777
+ case 'gt':
778
+ whereConditions.push(`${escapedField} > ?`);
779
+ whereValues.push(condition.value);
780
+ break;
781
+ case 'gte':
782
+ whereConditions.push(`${escapedField} >= ?`);
783
+ whereValues.push(condition.value);
784
+ break;
785
+ case 'lt':
786
+ whereConditions.push(`${escapedField} < ?`);
787
+ whereValues.push(condition.value);
788
+ break;
789
+ case 'lte':
790
+ whereConditions.push(`${escapedField} <= ?`);
791
+ whereValues.push(condition.value);
792
+ break;
793
+ case 'like':
794
+ whereConditions.push(`${escapedField} LIKE ?`);
795
+ whereValues.push(`%${condition.value}%`);
796
+ break;
797
+ case 'in':
798
+ if (Array.isArray(condition.value)) {
799
+ const placeholders = condition.value.map(() => '?').join(', ');
800
+ whereConditions.push(`${escapedField} IN (${placeholders})`);
801
+ whereValues.push(...condition.value);
802
+ }
803
+ break;
804
+ }
805
+ });
806
+ const whereClause = 'WHERE ' + whereConditions.join(' AND ');
807
+ // Validate all parameters
808
+ const paramValidation = this.security.validateParameters(whereValues);
809
+ if (!paramValidation.valid) {
810
+ throw new Error(`Invalid parameters: ${paramValidation.error}`);
811
+ }
812
+ // Build and execute the query
813
+ const escapedTableName = this.security.escapeIdentifier(table_name);
814
+ const query = `DELETE FROM ${escapedTableName} ${whereClause}`;
815
+ const result = await this.db.query(query, paramValidation.sanitizedParams);
816
+ batchResults.push({
817
+ affectedRows: result.affectedRows
818
+ });
819
+ totalDeleted += result.affectedRows;
820
+ }
821
+ // Commit the transaction
822
+ await this.db.query('COMMIT');
823
+ results.push({
824
+ batchNumber: Math.floor(i / batch_size) + 1,
825
+ deletesProcessed: batch.length,
826
+ results: batchResults
827
+ });
828
+ }
829
+ catch (error) {
830
+ // Rollback on error
831
+ await this.db.query('ROLLBACK');
832
+ throw error;
833
+ }
834
+ }
835
+ return {
836
+ status: 'success',
837
+ data: {
838
+ totalDeletes: condition_sets.length,
839
+ totalDeletedRows: totalDeleted,
840
+ batches: results.length,
841
+ batchResults: results
842
+ }
843
+ };
844
+ }
845
+ catch (error) {
846
+ return {
847
+ status: 'error',
848
+ error: error.message
849
+ };
850
+ }
851
+ }
442
852
  }
443
853
  exports.CrudTools = CrudTools;
@@ -108,20 +108,20 @@ class DatabaseTools {
108
108
  };
109
109
  }
110
110
  try {
111
- const query = `
112
- SELECT
113
- COLUMN_NAME as column_name,
114
- DATA_TYPE as data_type,
115
- IS_NULLABLE as is_nullable,
116
- COLUMN_KEY as column_key,
117
- COLUMN_DEFAULT as column_default,
118
- EXTRA as extra
119
- FROM
120
- INFORMATION_SCHEMA.COLUMNS
121
- WHERE
122
- TABLE_NAME = ?
123
- ORDER BY
124
- ORDINAL_POSITION
111
+ const query = `
112
+ SELECT
113
+ COLUMN_NAME as column_name,
114
+ DATA_TYPE as data_type,
115
+ IS_NULLABLE as is_nullable,
116
+ COLUMN_KEY as column_key,
117
+ COLUMN_DEFAULT as column_default,
118
+ EXTRA as extra
119
+ FROM
120
+ INFORMATION_SCHEMA.COLUMNS
121
+ WHERE
122
+ TABLE_NAME = ?
123
+ ORDER BY
124
+ ORDINAL_POSITION
125
125
  `;
126
126
  const results = await this.db.query(query, [params.table_name]);
127
127
  return {
@@ -71,33 +71,33 @@ class UtilityTools {
71
71
  try {
72
72
  const { table_name } = params;
73
73
  // Query to get foreign keys where this table is the parent
74
- const parentQuery = `
75
- SELECT
76
- TABLE_NAME as child_table,
77
- COLUMN_NAME as child_column,
78
- REFERENCED_TABLE_NAME as parent_table,
79
- REFERENCED_COLUMN_NAME as parent_column,
80
- CONSTRAINT_NAME as constraint_name
81
- FROM
82
- INFORMATION_SCHEMA.KEY_COLUMN_USAGE
83
- WHERE
84
- REFERENCED_TABLE_NAME = ?
85
- AND REFERENCED_TABLE_SCHEMA = DATABASE()
74
+ const parentQuery = `
75
+ SELECT
76
+ TABLE_NAME as child_table,
77
+ COLUMN_NAME as child_column,
78
+ REFERENCED_TABLE_NAME as parent_table,
79
+ REFERENCED_COLUMN_NAME as parent_column,
80
+ CONSTRAINT_NAME as constraint_name
81
+ FROM
82
+ INFORMATION_SCHEMA.KEY_COLUMN_USAGE
83
+ WHERE
84
+ REFERENCED_TABLE_NAME = ?
85
+ AND REFERENCED_TABLE_SCHEMA = DATABASE()
86
86
  `;
87
87
  // Query to get foreign keys where this table is the child
88
- const childQuery = `
89
- SELECT
90
- TABLE_NAME as child_table,
91
- COLUMN_NAME as child_column,
92
- REFERENCED_TABLE_NAME as parent_table,
93
- REFERENCED_COLUMN_NAME as parent_column,
94
- CONSTRAINT_NAME as constraint_name
95
- FROM
96
- INFORMATION_SCHEMA.KEY_COLUMN_USAGE
97
- WHERE
98
- TABLE_NAME = ?
99
- AND REFERENCED_TABLE_NAME IS NOT NULL
100
- AND TABLE_SCHEMA = DATABASE()
88
+ const childQuery = `
89
+ SELECT
90
+ TABLE_NAME as child_table,
91
+ COLUMN_NAME as child_column,
92
+ REFERENCED_TABLE_NAME as parent_table,
93
+ REFERENCED_COLUMN_NAME as parent_column,
94
+ CONSTRAINT_NAME as constraint_name
95
+ FROM
96
+ INFORMATION_SCHEMA.KEY_COLUMN_USAGE
97
+ WHERE
98
+ TABLE_NAME = ?
99
+ AND REFERENCED_TABLE_NAME IS NOT NULL
100
+ AND TABLE_SCHEMA = DATABASE()
101
101
  `;
102
102
  // Execute both queries
103
103
  const parentRelationships = await this.db.query(parentQuery, [table_name]);