@berthojoris/mcp-mysql-server 1.18.0 → 1.19.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.
package/CHANGELOG.md CHANGED
@@ -7,10 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Security
11
+ - Fixed critical SQL execution bypass in transactions by adding comprehensive security validation
12
+ - Enhanced stored procedure creation with body content validation and injection prevention
13
+ - Improved DDL operations with proper default value sanitization to prevent SQL injection
14
+ - Added transaction timeout mechanism (30 minutes) with automatic cleanup to prevent resource exhaustion
15
+ - Integrated security layer across all transaction operations for complete coverage
16
+
17
+ ### Changed
18
+ - Updated TransactionTools to require SecurityLayer for proper validation
19
+ - Enhanced DdlTools with comprehensive input sanitization
20
+ - Improved database connection management with timeout and cleanup features
21
+
22
+ ### Fixed
23
+ - Resolved TypeScript compilation error in SecurityLayer access patterns
24
+ - Eliminated all security bypass paths through the system
25
+
10
26
  ### Removed
11
27
  - Preset-based access control configuration: CLI `--preset` flag and `MCP_PRESET` / `MCP_PERMISSION_PRESET` environment variables. Use `MCP_PERMISSIONS` and optionally `MCP_CATEGORIES`.
12
28
  - Global masking configuration via `MCP_MASKING_PROFILE`. If you need enforced masking for exports, use the `safe_export_table` macro's `masking_profile` argument.
13
29
 
30
+
31
+ ## [1.18.2] - 2025-12-13
32
+
33
+ ### Removed
34
+ - Removed outdated comparison docs under `docs/comparison` (and the related README section). The canonical documentation is `DOCUMENTATIONS.md`.
35
+
36
+
37
+ ## [1.18.1] - 2025-12-13
38
+
39
+ ### Changed
40
+ - Updated documentation files with current datetime stamps
41
+
14
42
  ## [1.17.0] - 2025-12-12
15
43
 
16
44
  ### Added
package/DOCUMENTATIONS.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # MySQL MCP Server - Detailed Documentation
2
2
 
3
+ **Last Updated:** 2025-12-13T03:05:29.673Z
4
+
3
5
  This file contains detailed documentation for all features of the MySQL MCP Server. For quick start and basic information, see [README.md](README.md).
4
6
 
5
7
  ---
package/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  **A production-ready Model Context Protocol (MCP) server for MySQL database integration with AI agents**
6
6
 
7
+ **Last Updated:** 2025-12-13T03:05:56.256Z
8
+
7
9
  [![npm version](https://img.shields.io/npm/v/@berthojoris/mcp-mysql-server)](https://www.npmjs.com/package/@berthojoris/mysql-mcp)
8
10
  [![npm downloads](https://img.shields.io/npm/dm/@berthojoris/mysql-mcp)](https://www.npmjs.com/package/@berthojoris/mysql-mcp)
9
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -333,17 +335,7 @@ For comprehensive documentation, see **[DOCUMENTATIONS.md](DOCUMENTATIONS.md)**:
333
335
 
334
336
  This MySQL MCP is a **powerful intermediary layer** between AI assistants and MySQL databases.
335
337
 
336
- | Comparison Topic | Documentation |
337
- |------------------|---------------|
338
- | Data Access & Querying | [docs/comparison/data-access-querying.md](docs/comparison/data-access-querying.md) |
339
- | Data Analysis | [docs/comparison/data-analysis.md](docs/comparison/data-analysis.md) |
340
- | Data Validation | [docs/comparison/data-validation.md](docs/comparison/data-validation.md) |
341
- | Schema Inspection | [docs/comparison/schema-inspection.md](docs/comparison/schema-inspection.md) |
342
- | Debugging & Diagnostics | [docs/comparison/debugging-diagnostics.md](docs/comparison/debugging-diagnostics.md) |
343
- | Advanced Operations | [docs/comparison/advanced-operations.md](docs/comparison/advanced-operations.md) |
344
- | Key Benefits | [docs/comparison/key-benefits.md](docs/comparison/key-benefits.md) |
345
- | Example Workflows | [docs/comparison/example-workflows.md](docs/comparison/example-workflows.md) |
346
- | When to Use | [docs/comparison/when-to-use.md](docs/comparison/when-to-use.md) |
338
+ For full feature coverage and usage examples, see **[DOCUMENTATIONS.md](DOCUMENTATIONS.md)**.
347
339
 
348
340
  ---
349
341
 
@@ -4,6 +4,7 @@ declare class DatabaseConnection {
4
4
  private pool;
5
5
  private activeTransactions;
6
6
  private queryCache;
7
+ private readonly TRANSACTION_TIMEOUT_MS;
7
8
  private constructor();
8
9
  static getInstance(): DatabaseConnection;
9
10
  getConnection(): Promise<mysql.PoolConnection>;
@@ -19,11 +20,32 @@ declare class DatabaseConnection {
19
20
  errorCode?: string;
20
21
  }>;
21
22
  closePool(): Promise<void>;
23
+ /**
24
+ * Clean up expired transactions to prevent resource exhaustion
25
+ */
26
+ private cleanupExpiredTransactions;
27
+ /**
28
+ * Force rollback a transaction (used for cleanup)
29
+ */
30
+ private forceRollbackTransaction;
31
+ /**
32
+ * Reset transaction timeout (call this when there's activity)
33
+ */
34
+ private resetTransactionTimeout;
22
35
  beginTransaction(transactionId: string): Promise<void>;
23
36
  commitTransaction(transactionId: string): Promise<void>;
24
37
  rollbackTransaction(transactionId: string): Promise<void>;
25
38
  getActiveTransactionIds(): string[];
26
39
  hasActiveTransaction(transactionId: string): boolean;
40
+ /**
41
+ * Get transaction information for debugging/monitoring
42
+ */
43
+ getTransactionInfo(transactionId: string): {
44
+ exists: boolean;
45
+ createdAt?: Date;
46
+ lastActivity?: Date;
47
+ ageMs?: number;
48
+ };
27
49
  getQueryLogs(): import("./queryLogger").QueryLog[];
28
50
  getLastQueryLog(): import("./queryLogger").QueryLog | undefined;
29
51
  getFormattedQueryLogs(count?: number): string;
@@ -9,6 +9,7 @@ const queryLogger_1 = require("./queryLogger");
9
9
  const queryCache_1 = require("../cache/queryCache");
10
10
  class DatabaseConnection {
11
11
  constructor() {
12
+ this.TRANSACTION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes default timeout
12
13
  this.pool = promise_1.default.createPool({
13
14
  host: config_1.dbConfig.host,
14
15
  port: config_1.dbConfig.port,
@@ -21,6 +22,10 @@ class DatabaseConnection {
21
22
  });
22
23
  this.activeTransactions = new Map();
23
24
  this.queryCache = queryCache_1.QueryCache.getInstance();
25
+ // Set up periodic cleanup of expired transactions
26
+ setInterval(() => {
27
+ this.cleanupExpiredTransactions();
28
+ }, 5 * 60 * 1000); // Check every 5 minutes
24
29
  }
25
30
  static getInstance() {
26
31
  if (!DatabaseConnection.instance) {
@@ -110,32 +115,108 @@ class DatabaseConnection {
110
115
  throw new Error(`Failed to close connection pool: ${error}`);
111
116
  }
112
117
  }
118
+ /**
119
+ * Clean up expired transactions to prevent resource exhaustion
120
+ */
121
+ cleanupExpiredTransactions() {
122
+ const now = new Date();
123
+ const expiredTransactions = [];
124
+ for (const [transactionId, transaction,] of this.activeTransactions.entries()) {
125
+ const timeSinceLastActivity = now.getTime() - transaction.lastActivity.getTime();
126
+ if (timeSinceLastActivity > this.TRANSACTION_TIMEOUT_MS) {
127
+ expiredTransactions.push(transactionId);
128
+ }
129
+ }
130
+ for (const transactionId of expiredTransactions) {
131
+ this.forceRollbackTransaction(transactionId, "Transaction timed out");
132
+ }
133
+ }
134
+ /**
135
+ * Force rollback a transaction (used for cleanup)
136
+ */
137
+ forceRollbackTransaction(transactionId, reason) {
138
+ const transaction = this.activeTransactions.get(transactionId);
139
+ if (!transaction)
140
+ return;
141
+ try {
142
+ // Clear timeout if exists
143
+ if (transaction.timeout) {
144
+ clearTimeout(transaction.timeout);
145
+ }
146
+ // Attempt to rollback
147
+ transaction.connection.rollback();
148
+ transaction.connection.release();
149
+ }
150
+ catch (error) {
151
+ console.error(`Failed to rollback expired transaction ${transactionId}:`, error);
152
+ try {
153
+ // Force release connection even if rollback fails
154
+ transaction.connection.release();
155
+ }
156
+ catch (releaseError) {
157
+ console.error(`Failed to release connection for expired transaction ${transactionId}:`, releaseError);
158
+ }
159
+ }
160
+ this.activeTransactions.delete(transactionId);
161
+ console.warn(`Transaction ${transactionId} force rolled back: ${reason}`);
162
+ }
163
+ /**
164
+ * Reset transaction timeout (call this when there's activity)
165
+ */
166
+ resetTransactionTimeout(transactionId) {
167
+ const transaction = this.activeTransactions.get(transactionId);
168
+ if (!transaction)
169
+ return;
170
+ // Clear existing timeout
171
+ if (transaction.timeout) {
172
+ clearTimeout(transaction.timeout);
173
+ }
174
+ // Update last activity
175
+ transaction.lastActivity = new Date();
176
+ // Set new timeout
177
+ transaction.timeout = setTimeout(() => {
178
+ this.forceRollbackTransaction(transactionId, "Transaction timeout");
179
+ }, this.TRANSACTION_TIMEOUT_MS);
180
+ }
113
181
  // Transaction Management Methods
114
182
  async beginTransaction(transactionId) {
115
183
  try {
116
184
  const connection = await this.getConnection();
117
185
  await connection.beginTransaction();
118
- this.activeTransactions.set(transactionId, connection);
186
+ const now = new Date();
187
+ const timeout = setTimeout(() => {
188
+ this.forceRollbackTransaction(transactionId, "Transaction timeout");
189
+ }, this.TRANSACTION_TIMEOUT_MS);
190
+ this.activeTransactions.set(transactionId, {
191
+ connection,
192
+ createdAt: now,
193
+ lastActivity: now,
194
+ timeout,
195
+ });
119
196
  }
120
197
  catch (error) {
121
198
  throw new Error(`Failed to begin transaction: ${error}`);
122
199
  }
123
200
  }
124
201
  async commitTransaction(transactionId) {
125
- const connection = this.activeTransactions.get(transactionId);
126
- if (!connection) {
202
+ const transaction = this.activeTransactions.get(transactionId);
203
+ if (!transaction) {
127
204
  throw new Error(`No active transaction found with ID: ${transactionId}`);
128
205
  }
129
206
  try {
130
- await connection.commit();
131
- connection.release();
207
+ // Clear timeout
208
+ if (transaction.timeout) {
209
+ clearTimeout(transaction.timeout);
210
+ }
211
+ await transaction.connection.commit();
212
+ transaction.connection.release();
132
213
  this.activeTransactions.delete(transactionId);
133
214
  }
134
215
  catch (error) {
135
216
  // If commit fails, rollback and release connection
136
217
  try {
137
- await connection.rollback();
138
- connection.release();
218
+ await transaction.connection.rollback();
219
+ transaction.connection.release();
139
220
  }
140
221
  catch (rollbackError) {
141
222
  console.error("Failed to rollback after commit error:", rollbackError);
@@ -145,17 +226,21 @@ class DatabaseConnection {
145
226
  }
146
227
  }
147
228
  async rollbackTransaction(transactionId) {
148
- const connection = this.activeTransactions.get(transactionId);
149
- if (!connection) {
229
+ const transaction = this.activeTransactions.get(transactionId);
230
+ if (!transaction) {
150
231
  throw new Error(`No active transaction found with ID: ${transactionId}`);
151
232
  }
152
233
  try {
153
- await connection.rollback();
154
- connection.release();
234
+ // Clear timeout
235
+ if (transaction.timeout) {
236
+ clearTimeout(transaction.timeout);
237
+ }
238
+ await transaction.connection.rollback();
239
+ transaction.connection.release();
155
240
  this.activeTransactions.delete(transactionId);
156
241
  }
157
242
  catch (error) {
158
- connection.release();
243
+ transaction.connection.release();
159
244
  this.activeTransactions.delete(transactionId);
160
245
  throw new Error(`Failed to rollback transaction: ${error}`);
161
246
  }
@@ -166,6 +251,22 @@ class DatabaseConnection {
166
251
  hasActiveTransaction(transactionId) {
167
252
  return this.activeTransactions.has(transactionId);
168
253
  }
254
+ /**
255
+ * Get transaction information for debugging/monitoring
256
+ */
257
+ getTransactionInfo(transactionId) {
258
+ const transaction = this.activeTransactions.get(transactionId);
259
+ if (!transaction) {
260
+ return { exists: false };
261
+ }
262
+ const now = new Date();
263
+ return {
264
+ exists: true,
265
+ createdAt: transaction.createdAt,
266
+ lastActivity: transaction.lastActivity,
267
+ ageMs: now.getTime() - transaction.createdAt.getTime(),
268
+ };
269
+ }
169
270
  getQueryLogs() {
170
271
  return queryLogger_1.QueryLogger.getLogs();
171
272
  }
@@ -206,15 +307,17 @@ class DatabaseConnection {
206
307
  this.queryCache.resetStats();
207
308
  }
208
309
  async executeInTransaction(transactionId, sql, params) {
209
- const connection = this.activeTransactions.get(transactionId);
210
- if (!connection) {
310
+ const transaction = this.activeTransactions.get(transactionId);
311
+ if (!transaction) {
211
312
  throw new Error(`No active transaction found with ID: ${transactionId}`);
212
313
  }
213
314
  const startTime = Date.now();
214
315
  try {
215
- const [results] = await connection.query(sql, params);
316
+ const [results] = await transaction.connection.query(sql, params);
216
317
  const duration = Date.now() - startTime;
217
318
  queryLogger_1.QueryLogger.log(sql, params, duration, "success");
319
+ // Reset timeout on successful activity
320
+ this.resetTransactionTimeout(transactionId);
218
321
  return results;
219
322
  }
220
323
  catch (error) {
package/dist/index.d.ts CHANGED
@@ -1309,9 +1309,7 @@ export declare class MySQLMCP {
1309
1309
  column_name: string;
1310
1310
  pattern_type: string;
1311
1311
  description: string;
1312
- metrics? /**
1313
- * Get current schema version
1314
- */: Record<string, any>;
1312
+ metrics?: Record<string, any>;
1315
1313
  recommendations?: string[];
1316
1314
  }>;
1317
1315
  summary: {
package/dist/index.js CHANGED
@@ -51,8 +51,8 @@ class MySQLMCP {
51
51
  this.crudTools = new crudTools_1.CrudTools(this.security);
52
52
  this.queryTools = new queryTools_1.QueryTools(this.security);
53
53
  this.utilityTools = new utilityTools_1.UtilityTools();
54
- this.ddlTools = new ddlTools_1.DdlTools();
55
- this.transactionTools = new transactionTools_1.TransactionTools();
54
+ this.ddlTools = new ddlTools_1.DdlTools(this.security);
55
+ this.transactionTools = new transactionTools_1.TransactionTools(this.security);
56
56
  this.storedProcedureTools = new storedProcedureTools_1.StoredProcedureTools(this.security);
57
57
  this.dataExportTools = new dataExportTools_1.DataExportTools(this.security);
58
58
  this.viewTools = new viewTools_1.ViewTools(this.security);
@@ -8,6 +8,10 @@ export declare class SecurityLayer {
8
8
  private featureConfig;
9
9
  masking: MaskingLayer;
10
10
  constructor(featureConfig?: FeatureConfig);
11
+ /**
12
+ * Check if a specific tool is enabled in the feature configuration
13
+ */
14
+ isToolEnabled(toolName: string): boolean;
11
15
  /**
12
16
  * Check if a query is a read-only information query (SHOW, DESCRIBE, EXPLAIN, etc.)
13
17
  */
@@ -37,6 +37,12 @@ class SecurityLayer {
37
37
  // Define DDL operations that require special permission
38
38
  this.ddlOperations = ["CREATE", "ALTER", "DROP", "TRUNCATE", "RENAME"];
39
39
  }
40
+ /**
41
+ * Check if a specific tool is enabled in the feature configuration
42
+ */
43
+ isToolEnabled(toolName) {
44
+ return this.featureConfig.isToolEnabled(toolName);
45
+ }
40
46
  /**
41
47
  * Check if a query is a read-only information query (SHOW, DESCRIBE, EXPLAIN, etc.)
42
48
  */
@@ -1,6 +1,12 @@
1
+ import { SecurityLayer } from "../security/securityLayer";
1
2
  export declare class DdlTools {
2
3
  private db;
3
- constructor();
4
+ private security;
5
+ constructor(security: SecurityLayer);
6
+ /**
7
+ * Sanitize default value for SQL safety
8
+ */
9
+ private sanitizeDefaultValue;
4
10
  /**
5
11
  * Create a new table
6
12
  */
@@ -6,8 +6,51 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.DdlTools = void 0;
7
7
  const connection_1 = __importDefault(require("../db/connection"));
8
8
  class DdlTools {
9
- constructor() {
9
+ constructor(security) {
10
10
  this.db = connection_1.default.getInstance();
11
+ this.security = security;
12
+ }
13
+ /**
14
+ * Sanitize default value for SQL safety
15
+ */
16
+ sanitizeDefaultValue(defaultValue) {
17
+ if (defaultValue === null || defaultValue === undefined) {
18
+ return "NULL";
19
+ }
20
+ if (typeof defaultValue === "number") {
21
+ return String(defaultValue);
22
+ }
23
+ if (typeof defaultValue === "boolean") {
24
+ return defaultValue ? "1" : "0";
25
+ }
26
+ if (typeof defaultValue === "string") {
27
+ // Check for dangerous SQL patterns in default values
28
+ const dangerousPatterns = [
29
+ /;/g, // Statement separators
30
+ /--/g, // SQL comments
31
+ /\/\*/g, // Block comment start
32
+ /\*\//g, // Block comment end
33
+ /\bUNION\b/gi, // UNION operations
34
+ /\bSELECT\b/gi, // SELECT statements
35
+ /\bINSERT\b/gi, // INSERT statements
36
+ /\bUPDATE\b/gi, // UPDATE statements
37
+ /\bDELETE\b/gi, // DELETE statements
38
+ /\bDROP\b/gi, // DROP statements
39
+ /\bCREATE\b/gi, // CREATE statements
40
+ /\bALTER\b/gi, // ALTER statements
41
+ ];
42
+ let sanitized = defaultValue;
43
+ for (const pattern of dangerousPatterns) {
44
+ if (pattern.test(sanitized)) {
45
+ throw new Error(`Dangerous SQL pattern detected in default value: ${pattern.source}`);
46
+ }
47
+ }
48
+ // Escape single quotes and backslashes
49
+ sanitized = sanitized.replace(/\\/g, "\\\\").replace(/'/g, "''");
50
+ return `'${sanitized}'`;
51
+ }
52
+ // For other types, convert to string and escape
53
+ return `'${String(defaultValue).replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
11
54
  }
12
55
  /**
13
56
  * Create a new table
@@ -26,7 +69,9 @@ class DdlTools {
26
69
  def += " AUTO_INCREMENT";
27
70
  }
28
71
  if (col.default !== undefined) {
29
- def += ` DEFAULT ${col.default}`;
72
+ // SECURITY: Properly sanitize default values to prevent SQL injection
73
+ const sanitizedDefault = this.sanitizeDefaultValue(col.default);
74
+ def += ` DEFAULT ${sanitizedDefault}`;
30
75
  }
31
76
  if (col.primary_key) {
32
77
  def += " PRIMARY KEY";
@@ -83,8 +128,11 @@ class DdlTools {
83
128
  query += ` ADD COLUMN \`${op.column_name}\` ${op.column_type}`;
84
129
  if (op.nullable === false)
85
130
  query += " NOT NULL";
86
- if (op.default !== undefined)
87
- query += ` DEFAULT ${op.default}`;
131
+ if (op.default !== undefined) {
132
+ // SECURITY: Properly sanitize default values to prevent SQL injection
133
+ const sanitizedDefault = this.sanitizeDefaultValue(op.default);
134
+ query += ` DEFAULT ${sanitizedDefault}`;
135
+ }
88
136
  break;
89
137
  case "drop_column":
90
138
  if (!op.column_name) {
@@ -105,8 +153,11 @@ class DdlTools {
105
153
  query += ` MODIFY COLUMN \`${op.column_name}\` ${op.column_type}`;
106
154
  if (op.nullable === false)
107
155
  query += " NOT NULL";
108
- if (op.default !== undefined)
109
- query += ` DEFAULT ${op.default}`;
156
+ if (op.default !== undefined) {
157
+ // SECURITY: Properly sanitize default values to prevent SQL injection
158
+ const sanitizedDefault = this.sanitizeDefaultValue(op.default);
159
+ query += ` DEFAULT ${sanitizedDefault}`;
160
+ }
110
161
  break;
111
162
  case "rename_column":
112
163
  if (!op.column_name || !op.new_column_name || !op.column_type) {
@@ -40,6 +40,10 @@ export declare class StoredProcedureTools {
40
40
  data?: any;
41
41
  error?: string;
42
42
  }>;
43
+ /**
44
+ * Validate stored procedure body content for security
45
+ */
46
+ private validateProcedureBody;
43
47
  /**
44
48
  * Create a new stored procedure
45
49
  */
@@ -294,6 +294,51 @@ class StoredProcedureTools {
294
294
  };
295
295
  }
296
296
  }
297
+ /**
298
+ * Validate stored procedure body content for security
299
+ */
300
+ validateProcedureBody(body) {
301
+ if (!body || typeof body !== "string") {
302
+ return {
303
+ valid: false,
304
+ error: "Procedure body must be a non-empty string",
305
+ };
306
+ }
307
+ const trimmedBody = body.trim();
308
+ // Check for dangerous SQL patterns in procedure body
309
+ const dangerousPatterns = [
310
+ /\bGRANT\b/i,
311
+ /\bREVOKE\b/i,
312
+ /\bDROP\s+USER\b/i,
313
+ /\bCREATE\s+USER\b/i,
314
+ /\bALTER\s+USER\b/i,
315
+ /\bSET\s+PASSWORD\b/i,
316
+ /\bINTO\s+OUTFILE\b/i,
317
+ /\bINTO\s+DUMPFILE\b/i,
318
+ /\bLOAD\s+DATA\b/i,
319
+ /\bLOAD_FILE\s*\(/i,
320
+ /\bSYSTEM\s*\(/i,
321
+ /\bEXEC\s*\(/i,
322
+ /\bEVAL\s*\(/i,
323
+ ];
324
+ for (const pattern of dangerousPatterns) {
325
+ if (pattern.test(trimmedBody)) {
326
+ return {
327
+ valid: false,
328
+ error: `Dangerous SQL pattern detected: ${pattern.source}`,
329
+ };
330
+ }
331
+ }
332
+ // Check for multiple statement separators that might indicate injection attempts
333
+ const semicolonCount = (trimmedBody.match(/;/g) || []).length;
334
+ if (semicolonCount > 50) {
335
+ return {
336
+ valid: false,
337
+ error: "Too many statements in procedure body",
338
+ };
339
+ }
340
+ return { valid: true };
341
+ }
297
342
  /**
298
343
  * Create a new stored procedure
299
344
  */
@@ -325,6 +370,18 @@ class StoredProcedureTools {
325
370
  error: identifierValidation.error || "Invalid procedure name",
326
371
  };
327
372
  }
373
+ // SECURITY VALIDATION: Validate procedure body content
374
+ const bodyValidation = this.validateProcedureBody(body);
375
+ if (!bodyValidation.valid) {
376
+ return {
377
+ status: "error",
378
+ error: bodyValidation.error || "Invalid procedure body",
379
+ };
380
+ }
381
+ // Sanitize comment to prevent SQL injection
382
+ const sanitizedComment = comment
383
+ ? comment.replace(/'/g, "''").replace(/\\/g, "\\\\")
384
+ : "";
328
385
  // Build parameter list
329
386
  const parameterList = parameters
330
387
  .map((param) => {
@@ -336,9 +393,8 @@ class StoredProcedureTools {
336
393
  .join(", ");
337
394
  // Build CREATE PROCEDURE statement
338
395
  let createQuery = `CREATE PROCEDURE \`${database}\`.\`${procedure_name}\`(${parameterList})\n`;
339
- if (comment) {
340
- createQuery += `COMMENT '${comment.replace(/'/g, "''")}'
341
- `;
396
+ if (sanitizedComment) {
397
+ createQuery += `COMMENT '${sanitizedComment}'\n`;
342
398
  }
343
399
  // Check if body already contains BEGIN/END, if not add them
344
400
  const trimmedBody = body.trim();
@@ -1,3 +1,4 @@
1
+ import { SecurityLayer } from "../security/securityLayer";
1
2
  export interface TransactionResult {
2
3
  status: "success" | "error";
3
4
  transactionId?: string;
@@ -7,7 +8,8 @@ export interface TransactionResult {
7
8
  }
8
9
  export declare class TransactionTools {
9
10
  private db;
10
- constructor();
11
+ private security;
12
+ constructor(security: SecurityLayer);
11
13
  /**
12
14
  * Begin a new transaction
13
15
  */
@@ -6,8 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TransactionTools = void 0;
7
7
  const connection_1 = __importDefault(require("../db/connection"));
8
8
  class TransactionTools {
9
- constructor() {
9
+ constructor(security) {
10
10
  this.db = connection_1.default.getInstance();
11
+ this.security = security;
11
12
  }
12
13
  /**
13
14
  * Begin a new transaction
@@ -114,7 +115,25 @@ class TransactionTools {
114
115
  error: "Query is required",
115
116
  };
116
117
  }
117
- const result = await this.db.executeInTransaction(params.transactionId, params.query, params.params);
118
+ // SECURITY VALIDATION: Check if user has execute permission
119
+ const hasExecutePermission = this.security.isToolEnabled("executeSql");
120
+ // SECURITY VALIDATION: Validate the query before execution
121
+ const queryValidation = this.security.validateQuery(params.query, hasExecutePermission);
122
+ if (!queryValidation.valid) {
123
+ return {
124
+ status: "error",
125
+ error: `Query validation failed: ${queryValidation.error}`,
126
+ };
127
+ }
128
+ // SECURITY VALIDATION: Validate and sanitize parameters
129
+ const paramValidation = this.security.validateParameters(params.params || []);
130
+ if (!paramValidation.valid) {
131
+ return {
132
+ status: "error",
133
+ error: `Parameter validation failed: ${paramValidation.error}`,
134
+ };
135
+ }
136
+ const result = await this.db.executeInTransaction(params.transactionId, params.query, paramValidation.sanitizedParams);
118
137
  return {
119
138
  status: "success",
120
139
  data: result,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@berthojoris/mcp-mysql-server",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "description": "Model Context Protocol server for MySQL database integration with dynamic per-project permissions, backup/restore, data import/export, and data migration capabilities",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",