@berthojoris/mcp-mysql-server 1.0.2 → 1.2.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.
@@ -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;
@@ -3,7 +3,8 @@ export declare class DatabaseTools {
3
3
  private db;
4
4
  constructor();
5
5
  /**
6
- * List all available databases
6
+ * List only the connected database (security restriction)
7
+ * This prevents access to other databases on the MySQL server
7
8
  */
8
9
  listDatabases(): Promise<{
9
10
  status: string;
@@ -6,20 +6,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.DatabaseTools = void 0;
7
7
  const connection_1 = __importDefault(require("../db/connection"));
8
8
  const schemas_1 = require("../validation/schemas");
9
+ const config_1 = require("../config/config");
9
10
  class DatabaseTools {
10
11
  constructor() {
11
12
  this.db = connection_1.default.getInstance();
12
13
  }
13
14
  /**
14
- * List all available databases
15
+ * List only the connected database (security restriction)
16
+ * This prevents access to other databases on the MySQL server
15
17
  */
16
18
  async listDatabases() {
17
19
  try {
18
- const results = await this.db.query('SHOW DATABASES');
19
- const databases = results.map(row => row.Database);
20
+ // Only return the database specified in the connection string
21
+ // This is a security measure to prevent access to other databases
22
+ if (!config_1.dbConfig.database) {
23
+ return {
24
+ status: 'error',
25
+ error: 'No database specified in connection string. Please specify a database name in your MySQL connection URL.'
26
+ };
27
+ }
28
+ // Verify the database exists and is accessible
29
+ const results = await this.db.query('SELECT DATABASE() as current_database');
30
+ const currentDatabase = results[0]?.current_database;
31
+ if (!currentDatabase) {
32
+ return {
33
+ status: 'error',
34
+ error: 'No database selected. Please ensure your connection string includes a valid database name.'
35
+ };
36
+ }
20
37
  return {
21
38
  status: 'success',
22
- data: databases
39
+ data: [currentDatabase]
23
40
  };
24
41
  }
25
42
  catch (error) {
@@ -41,8 +58,23 @@ class DatabaseTools {
41
58
  };
42
59
  }
43
60
  try {
61
+ // Security validation: if database is specified, ensure it matches the connected database
62
+ if (params.database) {
63
+ if (!config_1.dbConfig.database) {
64
+ return {
65
+ status: 'error',
66
+ error: 'No database specified in connection string. Cannot access other databases.'
67
+ };
68
+ }
69
+ if (params.database !== config_1.dbConfig.database) {
70
+ return {
71
+ status: 'error',
72
+ error: `Access denied. You can only access the connected database '${config_1.dbConfig.database}'. Requested database '${params.database}' is not allowed.`
73
+ };
74
+ }
75
+ }
44
76
  let query = 'SHOW TABLES';
45
- // If database is specified, use it
77
+ // If database is specified and validated, use it
46
78
  if (params.database) {
47
79
  query = `SHOW TABLES FROM \`${params.database}\``;
48
80
  }
@@ -76,20 +108,20 @@ class DatabaseTools {
76
108
  };
77
109
  }
78
110
  try {
79
- const query = `
80
- SELECT
81
- COLUMN_NAME as column_name,
82
- DATA_TYPE as data_type,
83
- IS_NULLABLE as is_nullable,
84
- COLUMN_KEY as column_key,
85
- COLUMN_DEFAULT as column_default,
86
- EXTRA as extra
87
- FROM
88
- INFORMATION_SCHEMA.COLUMNS
89
- WHERE
90
- TABLE_NAME = ?
91
- ORDER BY
92
- 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
93
125
  `;
94
126
  const results = await this.db.query(query, [params.table_name]);
95
127
  return {
@@ -3,6 +3,10 @@ export declare class StoredProcedureTools {
3
3
  private db;
4
4
  private security;
5
5
  constructor(security: SecurityLayer);
6
+ /**
7
+ * Validate database access - ensures only the connected database can be accessed
8
+ */
9
+ private validateDatabaseAccess;
6
10
  /**
7
11
  * List all stored procedures in the current database
8
12
  */