@berthojoris/mcp-mysql-server 1.18.0 → 1.20.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
@@ -5,12 +5,45 @@ All notable changes to the MySQL MCP Server will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.20.0] - 2025-12-17
9
+
10
+ ### Changed
11
+ - Version increment for implemented features and improvements
12
+
8
13
  ## [Unreleased]
9
14
 
15
+ ### Security
16
+ - Fixed critical SQL execution bypass in transactions by adding comprehensive security validation
17
+ - Enhanced stored procedure creation with body content validation and injection prevention
18
+ - Improved DDL operations with proper default value sanitization to prevent SQL injection
19
+ - Added transaction timeout mechanism (30 minutes) with automatic cleanup to prevent resource exhaustion
20
+ - Integrated security layer across all transaction operations for complete coverage
21
+
22
+ ### Changed
23
+ - Updated TransactionTools to require SecurityLayer for proper validation
24
+ - Enhanced DdlTools with comprehensive input sanitization
25
+ - Improved database connection management with timeout and cleanup features
26
+
27
+ ### Fixed
28
+ - Resolved TypeScript compilation error in SecurityLayer access patterns
29
+ - Eliminated all security bypass paths through the system
30
+
10
31
  ### Removed
11
32
  - Preset-based access control configuration: CLI `--preset` flag and `MCP_PRESET` / `MCP_PERMISSION_PRESET` environment variables. Use `MCP_PERMISSIONS` and optionally `MCP_CATEGORIES`.
12
33
  - 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
34
 
35
+
36
+ ## [1.18.2] - 2025-12-13
37
+
38
+ ### Removed
39
+ - Removed outdated comparison docs under `docs/comparison` (and the related README section). The canonical documentation is `DOCUMENTATIONS.md`.
40
+
41
+
42
+ ## [1.18.1] - 2025-12-13
43
+
44
+ ### Changed
45
+ - Updated documentation files with current datetime stamps
46
+
14
47
  ## [1.17.0] - 2025-12-12
15
48
 
16
49
  ### Added
package/DOCUMENTATIONS.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # MySQL MCP Server - Detailed Documentation
2
2
 
3
+ **Last Updated:** 2025-12-17 00:00:00
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-17 00:00:00
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,10 @@ declare class DatabaseConnection {
4
4
  private pool;
5
5
  private activeTransactions;
6
6
  private queryCache;
7
+ private readonly TRANSACTION_TIMEOUT_MS;
8
+ private readonly TRANSACTION_MONITOR_INTERVAL_MS;
9
+ private readonly MAX_TRANSACTION_DURATION_MS;
10
+ private transactionMonitorInterval?;
7
11
  private constructor();
8
12
  static getInstance(): DatabaseConnection;
9
13
  getConnection(): Promise<mysql.PoolConnection>;
@@ -19,11 +23,44 @@ declare class DatabaseConnection {
19
23
  errorCode?: string;
20
24
  }>;
21
25
  closePool(): Promise<void>;
26
+ /**
27
+ * Start transaction monitoring system
28
+ */
29
+ private startTransactionMonitor;
30
+ /**
31
+ * Monitor all active transactions for timeout and security violations
32
+ */
33
+ private monitorTransactions;
34
+ /**
35
+ * Detect suspicious transaction activity patterns
36
+ */
37
+ private detectSuspiciousActivity;
38
+ /**
39
+ * Enhanced cleanup of expired transactions with security checks
40
+ */
41
+ private cleanupExpiredTransactions;
42
+ /**
43
+ * Force rollback a transaction (used for timeout handling)
44
+ */
45
+ private forceRollbackTransaction;
46
+ /**
47
+ * Reset transaction timeout (call this when there's activity)
48
+ */
49
+ private resetTransactionTimeout;
22
50
  beginTransaction(transactionId: string): Promise<void>;
23
51
  commitTransaction(transactionId: string): Promise<void>;
24
52
  rollbackTransaction(transactionId: string): Promise<void>;
25
53
  getActiveTransactionIds(): string[];
26
54
  hasActiveTransaction(transactionId: string): boolean;
55
+ /**
56
+ * Get transaction information for debugging/monitoring
57
+ */
58
+ getTransactionInfo(transactionId: string): {
59
+ exists: boolean;
60
+ createdAt?: Date;
61
+ lastActivity?: Date;
62
+ ageMs?: number;
63
+ };
27
64
  getQueryLogs(): import("./queryLogger").QueryLog[];
28
65
  getLastQueryLog(): import("./queryLogger").QueryLog | undefined;
29
66
  getFormattedQueryLogs(count?: number): string;
@@ -9,6 +9,9 @@ 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
13
+ this.TRANSACTION_MONITOR_INTERVAL_MS = 60 * 1000; // 1 minute monitoring interval
14
+ this.MAX_TRANSACTION_DURATION_MS = 60 * 60 * 1000; // 1 hour maximum duration
12
15
  this.pool = promise_1.default.createPool({
13
16
  host: config_1.dbConfig.host,
14
17
  port: config_1.dbConfig.port,
@@ -21,6 +24,12 @@ class DatabaseConnection {
21
24
  });
22
25
  this.activeTransactions = new Map();
23
26
  this.queryCache = queryCache_1.QueryCache.getInstance();
27
+ // Start transaction monitoring
28
+ this.startTransactionMonitor();
29
+ // Set up periodic cleanup of expired transactions
30
+ setInterval(() => {
31
+ this.cleanupExpiredTransactions();
32
+ }, 5 * 60 * 1000); // Check every 5 minutes
24
33
  }
25
34
  static getInstance() {
26
35
  if (!DatabaseConnection.instance) {
@@ -110,32 +119,179 @@ class DatabaseConnection {
110
119
  throw new Error(`Failed to close connection pool: ${error}`);
111
120
  }
112
121
  }
122
+ /**
123
+ * Start transaction monitoring system
124
+ */
125
+ startTransactionMonitor() {
126
+ this.transactionMonitorInterval = setInterval(() => {
127
+ this.monitorTransactions();
128
+ }, this.TRANSACTION_MONITOR_INTERVAL_MS);
129
+ }
130
+ /**
131
+ * Monitor all active transactions for timeout and security violations
132
+ */
133
+ monitorTransactions() {
134
+ const now = Date.now();
135
+ const transactionsToCleanup = [];
136
+ for (const [transactionId, transaction] of this.activeTransactions) {
137
+ const age = now - transaction.createdAt.getTime();
138
+ const inactivity = now - transaction.lastActivity.getTime();
139
+ // Check for maximum duration violation
140
+ if (age > this.MAX_TRANSACTION_DURATION_MS) {
141
+ console.warn(`Transaction ${transactionId} exceeded maximum duration, forcing rollback`);
142
+ transactionsToCleanup.push(transactionId);
143
+ continue;
144
+ }
145
+ // Check for inactivity timeout (server-side verification)
146
+ if (inactivity > this.TRANSACTION_TIMEOUT_MS) {
147
+ console.warn(`Transaction ${transactionId} timed out due to inactivity, forcing rollback`);
148
+ transactionsToCleanup.push(transactionId);
149
+ continue;
150
+ }
151
+ // Check for suspicious activity patterns
152
+ if (this.detectSuspiciousActivity(transaction)) {
153
+ console.warn(`Transaction ${transactionId} shows suspicious activity patterns, forcing rollback`);
154
+ transactionsToCleanup.push(transactionId);
155
+ continue;
156
+ }
157
+ }
158
+ // Clean up flagged transactions
159
+ for (const transactionId of transactionsToCleanup) {
160
+ this.forceRollbackTransaction(transactionId, "Security violation or timeout");
161
+ }
162
+ }
163
+ /**
164
+ * Detect suspicious transaction activity patterns
165
+ */
166
+ detectSuspiciousActivity(transaction) {
167
+ const now = Date.now();
168
+ const age = now - transaction.createdAt.getTime();
169
+ // Very new transactions with excessive activity could indicate automated attacks
170
+ if (age < 60000 && transaction.queryCount > 100) { // More than 100 queries in first minute
171
+ return true;
172
+ }
173
+ // Transactions with extremely high query count could indicate resource exhaustion attacks
174
+ if (transaction.queryCount > 10000) {
175
+ return true;
176
+ }
177
+ // Check for rapid-fire queries (potential DoS)
178
+ if (transaction.lastQueryTime && (now - transaction.lastQueryTime.getTime()) < 10) { // Less than 10ms between queries
179
+ transaction.rapidQueryCount = (transaction.rapidQueryCount || 0) + 1;
180
+ if (transaction.rapidQueryCount > 50) { // More than 50 rapid queries
181
+ return true;
182
+ }
183
+ }
184
+ return false;
185
+ }
186
+ /**
187
+ * Enhanced cleanup of expired transactions with security checks
188
+ */
189
+ cleanupExpiredTransactions() {
190
+ const now = Date.now();
191
+ const transactionsToCleanup = [];
192
+ for (const [transactionId, transaction] of this.activeTransactions) {
193
+ const age = now - transaction.createdAt.getTime();
194
+ const inactivity = now - transaction.lastActivity.getTime();
195
+ // Multiple cleanup criteria for security
196
+ if (inactivity > this.TRANSACTION_TIMEOUT_MS ||
197
+ age > this.MAX_TRANSACTION_DURATION_MS ||
198
+ this.detectSuspiciousActivity(transaction)) {
199
+ transactionsToCleanup.push(transactionId);
200
+ }
201
+ }
202
+ for (const transactionId of transactionsToCleanup) {
203
+ this.forceRollbackTransaction(transactionId, "Automatic cleanup");
204
+ }
205
+ }
206
+ /**
207
+ * Force rollback a transaction (used for timeout handling)
208
+ */
209
+ forceRollbackTransaction(transactionId, reason) {
210
+ const transaction = this.activeTransactions.get(transactionId);
211
+ if (!transaction)
212
+ return;
213
+ try {
214
+ // Clear timeout if exists
215
+ if (transaction.timeout) {
216
+ clearTimeout(transaction.timeout);
217
+ }
218
+ // Attempt to rollback
219
+ transaction.connection.rollback();
220
+ transaction.connection.release();
221
+ }
222
+ catch (error) {
223
+ console.error(`Failed to rollback expired transaction ${transactionId}:`, error);
224
+ try {
225
+ // Force release connection even if rollback fails
226
+ transaction.connection.release();
227
+ }
228
+ catch (releaseError) {
229
+ console.error(`Failed to release connection for expired transaction ${transactionId}:`, releaseError);
230
+ }
231
+ }
232
+ this.activeTransactions.delete(transactionId);
233
+ console.warn(`Transaction ${transactionId} force rolled back: ${reason}`);
234
+ }
235
+ /**
236
+ * Reset transaction timeout (call this when there's activity)
237
+ */
238
+ resetTransactionTimeout(transactionId) {
239
+ const transaction = this.activeTransactions.get(transactionId);
240
+ if (!transaction)
241
+ return;
242
+ // Clear existing timeout
243
+ if (transaction.timeout) {
244
+ clearTimeout(transaction.timeout);
245
+ }
246
+ // Update last activity
247
+ transaction.lastActivity = new Date();
248
+ // Set new timeout
249
+ transaction.timeout = setTimeout(() => {
250
+ this.forceRollbackTransaction(transactionId, "Transaction timeout");
251
+ }, this.TRANSACTION_TIMEOUT_MS);
252
+ }
113
253
  // Transaction Management Methods
114
254
  async beginTransaction(transactionId) {
115
255
  try {
116
256
  const connection = await this.getConnection();
117
257
  await connection.beginTransaction();
118
- this.activeTransactions.set(transactionId, connection);
258
+ const now = new Date();
259
+ const timeout = setTimeout(() => {
260
+ this.forceRollbackTransaction(transactionId, "Transaction timeout");
261
+ }, this.TRANSACTION_TIMEOUT_MS);
262
+ this.activeTransactions.set(transactionId, {
263
+ connection,
264
+ createdAt: now,
265
+ lastActivity: now,
266
+ lastQueryTime: now,
267
+ timeout,
268
+ queryCount: 0,
269
+ rapidQueryCount: 0,
270
+ });
119
271
  }
120
272
  catch (error) {
121
273
  throw new Error(`Failed to begin transaction: ${error}`);
122
274
  }
123
275
  }
124
276
  async commitTransaction(transactionId) {
125
- const connection = this.activeTransactions.get(transactionId);
126
- if (!connection) {
277
+ const transaction = this.activeTransactions.get(transactionId);
278
+ if (!transaction) {
127
279
  throw new Error(`No active transaction found with ID: ${transactionId}`);
128
280
  }
129
281
  try {
130
- await connection.commit();
131
- connection.release();
282
+ // Clear timeout
283
+ if (transaction.timeout) {
284
+ clearTimeout(transaction.timeout);
285
+ }
286
+ await transaction.connection.commit();
287
+ transaction.connection.release();
132
288
  this.activeTransactions.delete(transactionId);
133
289
  }
134
290
  catch (error) {
135
291
  // If commit fails, rollback and release connection
136
292
  try {
137
- await connection.rollback();
138
- connection.release();
293
+ await transaction.connection.rollback();
294
+ transaction.connection.release();
139
295
  }
140
296
  catch (rollbackError) {
141
297
  console.error("Failed to rollback after commit error:", rollbackError);
@@ -145,17 +301,21 @@ class DatabaseConnection {
145
301
  }
146
302
  }
147
303
  async rollbackTransaction(transactionId) {
148
- const connection = this.activeTransactions.get(transactionId);
149
- if (!connection) {
304
+ const transaction = this.activeTransactions.get(transactionId);
305
+ if (!transaction) {
150
306
  throw new Error(`No active transaction found with ID: ${transactionId}`);
151
307
  }
152
308
  try {
153
- await connection.rollback();
154
- connection.release();
309
+ // Clear timeout
310
+ if (transaction.timeout) {
311
+ clearTimeout(transaction.timeout);
312
+ }
313
+ await transaction.connection.rollback();
314
+ transaction.connection.release();
155
315
  this.activeTransactions.delete(transactionId);
156
316
  }
157
317
  catch (error) {
158
- connection.release();
318
+ transaction.connection.release();
159
319
  this.activeTransactions.delete(transactionId);
160
320
  throw new Error(`Failed to rollback transaction: ${error}`);
161
321
  }
@@ -166,6 +326,22 @@ class DatabaseConnection {
166
326
  hasActiveTransaction(transactionId) {
167
327
  return this.activeTransactions.has(transactionId);
168
328
  }
329
+ /**
330
+ * Get transaction information for debugging/monitoring
331
+ */
332
+ getTransactionInfo(transactionId) {
333
+ const transaction = this.activeTransactions.get(transactionId);
334
+ if (!transaction) {
335
+ return { exists: false };
336
+ }
337
+ const now = new Date();
338
+ return {
339
+ exists: true,
340
+ createdAt: transaction.createdAt,
341
+ lastActivity: transaction.lastActivity,
342
+ ageMs: now.getTime() - transaction.createdAt.getTime(),
343
+ };
344
+ }
169
345
  getQueryLogs() {
170
346
  return queryLogger_1.QueryLogger.getLogs();
171
347
  }
@@ -206,15 +382,22 @@ class DatabaseConnection {
206
382
  this.queryCache.resetStats();
207
383
  }
208
384
  async executeInTransaction(transactionId, sql, params) {
209
- const connection = this.activeTransactions.get(transactionId);
210
- if (!connection) {
385
+ const transaction = this.activeTransactions.get(transactionId);
386
+ if (!transaction) {
211
387
  throw new Error(`No active transaction found with ID: ${transactionId}`);
212
388
  }
389
+ // Update query tracking before execution
390
+ const now = new Date();
391
+ transaction.lastQueryTime = now;
392
+ transaction.queryCount++;
213
393
  const startTime = Date.now();
214
394
  try {
215
- const [results] = await connection.query(sql, params);
395
+ const [results] = await transaction.connection.query(sql, params);
216
396
  const duration = Date.now() - startTime;
217
397
  queryLogger_1.QueryLogger.log(sql, params, duration, "success");
398
+ // Reset timeout on successful activity and update last activity
399
+ this.resetTransactionTimeout(transactionId);
400
+ transaction.lastActivity = now;
218
401
  return results;
219
402
  }
220
403
  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
  */
@@ -27,7 +31,7 @@ export declare class SecurityLayer {
27
31
  error?: string;
28
32
  };
29
33
  /**
30
- * Validate SQL query for security issues
34
+ * Enhanced SQL query validation for security issues
31
35
  * @param query - The SQL query to validate
32
36
  * @param bypassDangerousCheck - If true, skips dangerous keyword check (for users with 'execute' permission)
33
37
  */
@@ -36,6 +40,26 @@ export declare class SecurityLayer {
36
40
  error?: string;
37
41
  queryType?: string;
38
42
  };
43
+ /**
44
+ * Enhanced comment detection to prevent bypass techniques
45
+ */
46
+ private detectComments;
47
+ /**
48
+ * Enhanced multiple statement detection
49
+ */
50
+ private detectMultipleStatements;
51
+ /**
52
+ * Enhanced query type detection
53
+ */
54
+ private detectQueryType;
55
+ /**
56
+ * Enhanced dangerous keyword detection
57
+ */
58
+ private detectDangerousKeywords;
59
+ /**
60
+ * Enhanced SELECT query validation
61
+ */
62
+ private validateSelectQuery;
39
63
  /**
40
64
  * Validate parameter values to prevent injection
41
65
  */