@berthojoris/mcp-mysql-server 1.4.5 → 1.4.7

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,6 +5,42 @@ 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.4.7] - 2025-11-21
9
+
10
+ ### Added
11
+ - **Query logging on output** - All query executions are now logged with detailed information including SQL, parameters, execution duration, and status
12
+ - `QueryLogger` utility class for tracking and formatting query logs
13
+ - Query logs are included in responses from query tools (runQuery, executeSql) and CRUD operations (create_record, read_records, update_record, delete_record)
14
+ - Query logs include: timestamp, SQL query, parameters used, execution time in milliseconds, and success/error status
15
+ - Production monitoring and configuration documentation for QueryLogger
16
+
17
+ ### Security & Performance Improvements
18
+ - **Memory leak prevention** - SQL queries truncated to 500 characters max (prevents megabyte-sized log entries)
19
+ - **Parameter limiting** - Only first 5 parameters logged to prevent memory bloat from bulk operations
20
+ - **Safe serialization** - Handles circular references, BigInt, and unstringifiable objects without crashes
21
+ - **Deep copy protection** - Parameters are deep copied to prevent reference mutations
22
+ - **Bounded storage** - Maximum 100 most recent queries retained (~100 KB total memory usage)
23
+ - **Output truncation** - Formatted output limited to prevent massive response payloads
24
+ - **Error handling** - All JSON.stringify operations wrapped in try-catch with safe fallbacks
25
+ - **Memory impact reduction** - 99.9% memory reduction for bulk operations (from ~1 GB to ~100 KB)
26
+
27
+ ### Technical Changes
28
+ - New `src/db/queryLogger.ts` module for query logging functionality with robust memory management
29
+ - Updated `src/db/connection.ts` to log all query executions with timing information
30
+ - Updated all query tool responses to include `queryLog` field with formatted log output
31
+ - Enhanced debugging capability by tracking the last 100 queries in memory
32
+ - Added configuration constants for tuning memory limits (MAX_LOGS, MAX_SQL_LENGTH, MAX_PARAM_LENGTH, MAX_PARAM_ITEMS)
33
+ - Implemented safeStringify method for type-safe value serialization
34
+
35
+ ## [1.4.6] - 2025-11-21
36
+
37
+ ### Changed
38
+ - Removed "Execute Permission & Advanced SQL Features" section from README.md
39
+ - Removed "Configuration" section from README.md
40
+ - Removed "REST API Mode" section from README.md
41
+ - Removed "Testing" section from README.md
42
+ - Streamlined documentation by removing outdated or less relevant sections
43
+
8
44
  ## [1.4.5] - 2025-01-08
9
45
 
10
46
  ### Changed
package/README.md CHANGED
@@ -21,39 +21,6 @@ A fully-featured **Model Context Protocol (MCP)** server for MySQL database inte
21
21
 
22
22
  ---
23
23
 
24
- ## 🆕 Recent Updates (v1.4.4)
25
-
26
- ### Bug Fixes
27
-
28
- #### ✅ Fixed: First Tool Call Failure Issue
29
- **Problem**: The first tool call would fail with "Connection closed" error (-32000), but subsequent calls would succeed.
30
-
31
- **Root Cause**: The MySQL MCP instance was initialized at module load time, before the MCP transport was fully connected, causing a race condition.
32
-
33
- **Solution**: Moved the initialization to occur after the MCP transport is connected, ensuring proper startup sequence.
34
-
35
- **Impact**: First tool calls now work reliably without needing retry.
36
-
37
- #### ✅ Fixed: Execute Permission Not Respected
38
- **Problem**: Users with `execute` permission would still get "Dangerous keyword detected" errors when using legitimate SQL functions like `LOAD_FILE()`, `UNION`, or accessing `INFORMATION_SCHEMA`.
39
-
40
- **Root Cause**: The security layer blocked certain SQL keywords unconditionally, regardless of granted permissions.
41
-
42
- **Solution**:
43
- - Modified security validation to respect the `execute` permission
44
- - Users with `execute` permission can now use:
45
- - SQL functions like `LOAD_FILE()`, `BENCHMARK()`, `SLEEP()`
46
- - Advanced SELECT features like `UNION` and subqueries
47
- - Access to `INFORMATION_SCHEMA` for metadata queries
48
- - Critical security operations remain blocked (GRANT, REVOKE, INTO OUTFILE, etc.)
49
-
50
- **Impact**: Users with full permissions can now use advanced SQL features as intended.
51
-
52
- ### Breaking Changes
53
- None - all changes are backward compatible.
54
-
55
- ---
56
-
57
24
  ## 📦 Installation
58
25
 
59
26
  ### Option 1: Quick Start (npx)
@@ -239,103 +206,6 @@ You can have different databases with different permissions in the same AI agent
239
206
 
240
207
  ---
241
208
 
242
- ## 🔓 Execute Permission & Advanced SQL Features
243
-
244
- The `execute` permission unlocks advanced SQL capabilities that are restricted by default for security reasons.
245
-
246
- ### What Execute Permission Enables
247
-
248
- When you grant the `execute` permission, users can:
249
-
250
- #### 1. **Advanced SQL Functions**
251
- - `LOAD_FILE()` - Read files from the server
252
- - `BENCHMARK()` - Performance testing
253
- - `SLEEP()` - Delay execution
254
- - And other MySQL built-in functions
255
-
256
- #### 2. **Advanced SELECT Features**
257
- - `UNION` queries - Combine multiple SELECT results
258
- - Subqueries in FROM clause - Complex nested queries
259
- - Access to `INFORMATION_SCHEMA` - Database metadata queries
260
-
261
- #### 3. **Execute Custom SQL**
262
- - Use `execute_sql` tool for INSERT, UPDATE, DELETE with advanced features
263
- - Use `run_query` tool for complex SELECT queries with UNION and subqueries
264
-
265
- ### Security Considerations
266
-
267
- **Without `execute` permission:**
268
- ```sql
269
- -- ❌ BLOCKED: UNION queries
270
- SELECT * FROM users WHERE id = 1 UNION SELECT * FROM admin;
271
-
272
- -- ❌ BLOCKED: Subqueries in FROM
273
- SELECT * FROM (SELECT * FROM users WHERE active = 1) AS active_users;
274
-
275
- -- ❌ BLOCKED: LOAD_FILE function
276
- SELECT LOAD_FILE('/etc/passwd');
277
-
278
- -- ❌ BLOCKED: INFORMATION_SCHEMA access
279
- SELECT * FROM INFORMATION_SCHEMA.TABLES;
280
- ```
281
-
282
- **With `execute` permission:**
283
- ```sql
284
- -- ✅ ALLOWED: All advanced SQL features
285
- SELECT * FROM users WHERE id = 1 UNION SELECT * FROM admin;
286
- SELECT * FROM (SELECT * FROM users WHERE active = 1) AS active_users;
287
- SELECT LOAD_FILE('/path/to/file.txt');
288
- SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'mydb';
289
- ```
290
-
291
- ### When to Grant Execute Permission
292
-
293
- **Grant `execute` when:**
294
- - Users need complex analytical queries with UNION or subqueries
295
- - Developers need access to INFORMATION_SCHEMA for metadata analysis
296
- - Advanced SQL functions are required for specific operations
297
- - You trust the user with broader database access
298
-
299
- **Don't grant `execute` when:**
300
- - Users only need basic CRUD operations
301
- - Working with untrusted AI agents
302
- - Production environments with strict security policies
303
- - Read-only access is sufficient
304
-
305
- ### Example: Analytics with Execute Permission
306
-
307
- ```json
308
- {
309
- "args": [
310
- "mysql://analyst:pass@localhost:3306/analytics_db",
311
- "list,read,execute,utility"
312
- ]
313
- }
314
- ```
315
-
316
- This allows the AI agent to run complex analytical queries:
317
-
318
- ```sql
319
- -- Complex query with UNION and subqueries
320
- SELECT 'Q1' as quarter, SUM(revenue) as total
321
- FROM (SELECT * FROM sales WHERE date BETWEEN '2024-01-01' AND '2024-03-31') AS q1_sales
322
- UNION
323
- SELECT 'Q2' as quarter, SUM(revenue) as total
324
- FROM (SELECT * FROM sales WHERE date BETWEEN '2024-04-01' AND '2024-06-30') AS q2_sales;
325
- ```
326
-
327
- ### Critical Security Keywords (Always Blocked)
328
-
329
- Even with `execute` permission, these operations are **always blocked** for security:
330
-
331
- - `GRANT` / `REVOKE` - User privilege management
332
- - `INTO OUTFILE` / `INTO DUMPFILE` - Writing files to server
333
- - `LOAD DATA` - Loading data from files
334
- - Direct access to `mysql`, `performance_schema`, `sys` databases
335
- - `USER` / `PASSWORD` manipulation
336
-
337
- ---
338
-
339
209
  ## 🚫 Permission Error Handling
340
210
 
341
211
  The MySQL MCP Server provides clear, user-friendly error messages when operations are attempted without proper permissions. This helps users understand exactly what permissions are needed and how to enable them.
@@ -1189,49 +1059,169 @@ END IF;
1189
1059
 
1190
1060
  ---
1191
1061
 
1192
- ## ⚙️ Configuration
1062
+ ## 📝 Query Logging
1193
1063
 
1194
- ### Connection String Format
1064
+ All queries executed through the MySQL MCP Server are automatically logged with detailed execution information. Query logs are included in the response output of all query and data manipulation operations.
1065
+
1066
+ ### Query Log Information
1067
+
1068
+ Each logged query includes:
1069
+ - **Timestamp** - ISO 8601 formatted execution time
1070
+ - **SQL Query** - The exact SQL statement executed
1071
+ - **Parameters** - Values passed to the query (if any)
1072
+ - **Execution Duration** - Time taken to execute in milliseconds
1073
+ - **Status** - Success or error indication
1074
+ - **Error Details** - Error message if the query failed (optional)
1075
+
1076
+ ### Example Query Log Output
1195
1077
 
1196
1078
  ```
1197
- mysql://username:password@host:port/database
1079
+ [1] 2025-11-21T10:30:45.123Z | SELECT * FROM users WHERE id = ? | Params: [5] | Duration: 12ms | Status: success
1198
1080
  ```
1199
1081
 
1200
- **Examples:**
1082
+ ### Viewing Query Logs in Responses
1083
+
1084
+ Query logs are automatically included in tool responses via the `queryLog` field:
1085
+
1086
+ **Query execution:**
1087
+ ```json
1088
+ {
1089
+ "status": "success",
1090
+ "data": [
1091
+ {"id": 1, "name": "John Doe", "email": "john@example.com"}
1092
+ ],
1093
+ "queryLog": "[1] 2025-11-21T10:30:45.123Z | SELECT * FROM users | Duration: 8ms | Status: success"
1094
+ }
1201
1095
  ```
1202
- mysql://root:pass@localhost:3306/myapp
1203
- mysql://user:pass@192.168.1.100:3306/production
1204
- mysql://admin:pass@db.example.com:3306/analytics
1096
+
1097
+ **Bulk operations with multiple queries:**
1098
+ ```json
1099
+ {
1100
+ "status": "success",
1101
+ "data": {
1102
+ "affectedRows": 100,
1103
+ "totalInserted": 100
1104
+ },
1105
+ "queryLog": "[1] 2025-11-21T10:30:45.123Z | INSERT INTO users ... | Duration: 45ms | Status: success\n[2] 2025-11-21T10:30:45.168Z | INSERT INTO users ... | Duration: 23ms | Status: success"
1106
+ }
1205
1107
  ```
1206
1108
 
1207
- **Database name is optional:**
1109
+ ### Query Logs for Debugging
1110
+
1111
+ Query logs are valuable for:
1112
+ - **Performance Analysis** - Track which queries are slow (high duration)
1113
+ - **Troubleshooting** - Review exact parameters sent to queries
1114
+ - **Auditing** - See what operations were performed and when
1115
+ - **Optimization** - Identify patterns in query execution
1116
+ - **Error Investigation** - Review failed queries and their errors
1117
+
1118
+ ### Query Log Limitations
1119
+
1120
+ - Logs are stored in memory (not persisted to disk)
1121
+ - Maximum of 100 most recent queries are retained
1122
+ - Logs are cleared when the MCP server restarts
1123
+ - For production audit trails, consider using MySQL's built-in query logging
1124
+
1125
+ ### Tools with Query Logging
1126
+
1127
+ All tools that execute queries include logs:
1128
+ - `run_query` - SELECT query execution
1129
+ - `executeSql` - Write operations (INSERT, UPDATE, DELETE)
1130
+ - `create_record` - Single record insertion
1131
+ - `read_records` - Record querying with filters
1132
+ - `update_record` - Record updates
1133
+ - `delete_record` - Record deletion
1134
+ - Bulk operations (`bulk_insert`, `bulk_update`, `bulk_delete`)
1135
+ - Stored procedure execution
1136
+ - Transaction operations
1137
+
1138
+ ### Query Logger Performance & Configuration
1139
+
1140
+ #### Memory Management
1141
+
1142
+ The QueryLogger is designed with robust memory safety:
1143
+
1144
+ **Built-in Protections:**
1145
+ - ✅ **SQL Truncation** - Queries truncated to 500 characters max
1146
+ - ✅ **Parameter Limiting** - Only first 5 parameters logged
1147
+ - ✅ **Value Truncation** - Individual parameter values limited to 50 characters
1148
+ - ✅ **Error Truncation** - Error messages limited to 200 characters
1149
+ - ✅ **Deep Copy** - Parameters are deep copied to prevent reference mutations
1150
+ - ✅ **Safe Serialization** - Handles circular references, BigInt, and unstringifiable objects
1151
+ - ✅ **Bounded Storage** - Maximum 100 most recent queries retained
1152
+
1153
+ **Memory Impact:**
1208
1154
  ```
1209
- mysql://user:pass@localhost:3306/
1155
+ Regular query: ~1 KB per log entry
1156
+ Bulk operations: ~1 KB per log entry (99.9% reduction vs unbounded)
1157
+ Total max memory: ~100 KB for all 100 log entries
1210
1158
  ```
1211
1159
 
1212
- When omitted, AI can switch between databases using SQL commands.
1160
+ #### Configuration Tuning
1213
1161
 
1214
- ### Environment Variables
1162
+ The QueryLogger limits are defined as constants and can be adjusted if needed by modifying `src/db/queryLogger.ts`:
1215
1163
 
1216
- Create `.env` file for local development:
1164
+ ```typescript
1165
+ private static readonly MAX_LOGS = 100; // Number of queries to retain
1166
+ private static readonly MAX_SQL_LENGTH = 500; // Max SQL string length
1167
+ private static readonly MAX_PARAM_LENGTH = 200; // Max params output length
1168
+ private static readonly MAX_PARAM_ITEMS = 5; // Max number of params to log
1169
+ ```
1217
1170
 
1218
- ```env
1219
- # MySQL Connection
1220
- DB_HOST=localhost
1221
- DB_PORT=3306
1222
- DB_USER=root
1223
- DB_PASSWORD=yourpassword
1224
- DB_NAME=yourdatabase
1171
+ **Tuning Recommendations:**
1172
+ - **High-traffic production**: Reduce `MAX_LOGS` to 50 to minimize memory
1173
+ - **Development/debugging**: Increase `MAX_SQL_LENGTH` to 1000 for fuller visibility
1174
+ - **Bulk operations heavy**: Keep defaults - they're optimized for bulk workloads
1225
1175
 
1226
- # Default Permissions (optional, can be overridden per-project)
1227
- MCP_CONFIG=list,read,utility
1176
+ #### Production Monitoring
1177
+
1178
+ When running in production, monitor these metrics:
1179
+
1180
+ 1. **Memory Usage** - QueryLogger should use <500 KB total
1181
+ 2. **Response Payload Size** - Query logs add minimal overhead (<1 KB per response)
1182
+ 3. **Performance Impact** - Logging overhead is <1ms per query
1228
1183
 
1229
- # REST API Mode (optional)
1230
- PORT=3000
1231
- JWT_SECRET=your_jwt_secret
1232
- NODE_ENV=development
1184
+ **Health Check:**
1185
+ ```javascript
1186
+ // Check log memory usage
1187
+ const logs = db.getQueryLogs();
1188
+ const estimatedMemory = logs.length * 1; // ~1 KB per log
1189
+ console.log(`Query log memory usage: ~${estimatedMemory} KB`);
1233
1190
  ```
1234
1191
 
1192
+ #### Persistent Logging for Production Auditing
1193
+
1194
+ **Important:** QueryLogger stores logs in memory only (not persisted to disk). For production audit trails and compliance, consider:
1195
+
1196
+ 1. **MySQL Query Log** (Recommended)
1197
+ ```sql
1198
+ -- Enable general query log
1199
+ SET GLOBAL general_log = 'ON';
1200
+ SET GLOBAL general_log_file = '/var/log/mysql/queries.log';
1201
+ ```
1202
+
1203
+ 2. **MySQL Slow Query Log**
1204
+ ```sql
1205
+ -- Log queries slower than 1 second
1206
+ SET GLOBAL slow_query_log = 'ON';
1207
+ SET GLOBAL long_query_time = 1;
1208
+ ```
1209
+
1210
+ 3. **Application-Level Logging**
1211
+ - Use Winston or similar logger to persist query logs to disk
1212
+ - Integrate with log aggregation services (ELK, Splunk, DataDog)
1213
+
1214
+ 4. **Database Audit Plugins**
1215
+ - MySQL Enterprise Audit
1216
+ - MariaDB Audit Plugin
1217
+ - Percona Audit Log Plugin
1218
+
1219
+ **Trade-offs:**
1220
+ - **In-Memory (QueryLogger)**: Fast, lightweight, for debugging & development
1221
+ - **MySQL Query Log**: Complete audit trail, slight performance impact
1222
+ - **Application Logging**: Flexible, can include business context
1223
+ - **Audit Plugins**: Enterprise-grade, compliance-ready, feature-rich
1224
+
1235
1225
  ---
1236
1226
 
1237
1227
  ## 🔒 Security Features
@@ -1281,92 +1271,6 @@ NODE_ENV=development
1281
1271
 
1282
1272
  ---
1283
1273
 
1284
- ## 🌐 REST API Mode
1285
-
1286
- In addition to MCP protocol, this server can run as a REST API.
1287
-
1288
- ### Start API Server
1289
-
1290
- ```bash
1291
- npm run start:api
1292
- ```
1293
-
1294
- Server runs on `http://localhost:3000` (or configured PORT).
1295
-
1296
- ### API Endpoints
1297
-
1298
- All endpoints require JWT authentication (except `/health`).
1299
-
1300
- **Health Check:**
1301
- ```
1302
- GET /health
1303
- ```
1304
-
1305
- **Database Operations:**
1306
- ```
1307
- GET /api/databases # List databases
1308
- GET /api/tables # List tables
1309
- GET /api/tables/:name/schema # Get table schema
1310
- GET /api/tables/:name/records # Read records
1311
- POST /api/tables/:name/records # Create record
1312
- PUT /api/tables/:name/records/:id # Update record
1313
- DELETE /api/tables/:name/records/:id # Delete record
1314
- ```
1315
-
1316
- **Query Operations:**
1317
- ```
1318
- POST /api/query # Execute SELECT query
1319
- POST /api/execute # Execute write query
1320
- ```
1321
-
1322
- **Utilities:**
1323
- ```
1324
- GET /api/connection # Connection info
1325
- GET /api/connection/test # Test connection
1326
- GET /api/tables/:name/relationships # Get foreign keys
1327
- ```
1328
-
1329
- ### Authentication
1330
-
1331
- Include JWT token in Authorization header:
1332
-
1333
- ```bash
1334
- curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
1335
- http://localhost:3000/api/tables
1336
- ```
1337
-
1338
- ---
1339
-
1340
- ## 🧪 Testing
1341
-
1342
- ### Test MCP Server Locally
1343
-
1344
- ```bash
1345
- # Test connection using npx (recommended)
1346
- npx -y @berthojoris/mcp-mysql-server mysql://user:pass@localhost:3306/test "list,read,utility"
1347
-
1348
- # Or if you cloned the repository locally
1349
- node bin/mcp-mysql.js mysql://user:pass@localhost:3306/test "list,read,utility"
1350
- ```
1351
-
1352
- ### Test with Claude Desktop
1353
-
1354
- 1. Configure `claude_desktop_config.json`
1355
- 2. Restart Claude Desktop
1356
- 3. Ask: *"What databases are available?"*
1357
-
1358
- ### Test REST API
1359
-
1360
- ```bash
1361
- # Start API server
1362
- npm run start:api
1363
-
1364
- # Test health endpoint
1365
- curl http://localhost:3000/health
1366
- ```
1367
-
1368
- ---
1369
-
1370
1274
  ## 🚀 Bulk Operations
1371
1275
 
1372
1276
  The MySQL MCP server includes powerful bulk operation tools designed for high-performance data processing. These tools are optimized for handling large datasets efficiently.
@@ -1542,6 +1446,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
1542
1446
  - ✅ **Transaction support (BEGIN, COMMIT, ROLLBACK)** - **COMPLETED!**
1543
1447
  - ✅ **Stored procedure execution** - **COMPLETED!**
1544
1448
  - ✅ **Bulk operations (batch insert/update/delete)** - **COMPLETED!**
1449
+ - ✅ **Add query log on output** - **COMPLETED!**
1545
1450
  - [ ] Query result caching
1546
1451
  - [ ] Advanced query optimization hints
1547
1452
 
@@ -17,6 +17,9 @@ declare class DatabaseConnection {
17
17
  rollbackTransaction(transactionId: string): Promise<void>;
18
18
  getActiveTransactionIds(): string[];
19
19
  hasActiveTransaction(transactionId: string): boolean;
20
+ getQueryLogs(): import("./queryLogger").QueryLog[];
21
+ getLastQueryLog(): import("./queryLogger").QueryLog | undefined;
22
+ getFormattedQueryLogs(count?: number): string;
20
23
  executeInTransaction<T>(transactionId: string, sql: string, params?: any[]): Promise<T>;
21
24
  }
22
25
  export default DatabaseConnection;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const promise_1 = __importDefault(require("mysql2/promise"));
7
7
  const config_1 = require("../config/config");
8
+ const queryLogger_1 = require("./queryLogger");
8
9
  class DatabaseConnection {
9
10
  constructor() {
10
11
  this.pool = promise_1.default.createPool({
@@ -34,11 +35,16 @@ class DatabaseConnection {
34
35
  }
35
36
  }
36
37
  async query(sql, params) {
38
+ const startTime = Date.now();
37
39
  try {
38
40
  const [results] = await this.pool.query(sql, params);
41
+ const duration = Date.now() - startTime;
42
+ queryLogger_1.QueryLogger.log(sql, params, duration, 'success');
39
43
  return results;
40
44
  }
41
45
  catch (error) {
46
+ const duration = Date.now() - startTime;
47
+ queryLogger_1.QueryLogger.log(sql, params, duration, 'error', error.message);
42
48
  throw new Error(`Query execution failed: ${error}`);
43
49
  }
44
50
  }
@@ -118,16 +124,30 @@ class DatabaseConnection {
118
124
  hasActiveTransaction(transactionId) {
119
125
  return this.activeTransactions.has(transactionId);
120
126
  }
127
+ getQueryLogs() {
128
+ return queryLogger_1.QueryLogger.getLogs();
129
+ }
130
+ getLastQueryLog() {
131
+ return queryLogger_1.QueryLogger.getLastLog();
132
+ }
133
+ getFormattedQueryLogs(count = 1) {
134
+ return queryLogger_1.QueryLogger.formatLogs(queryLogger_1.QueryLogger.getLastLogs(count));
135
+ }
121
136
  async executeInTransaction(transactionId, sql, params) {
122
137
  const connection = this.activeTransactions.get(transactionId);
123
138
  if (!connection) {
124
139
  throw new Error(`No active transaction found with ID: ${transactionId}`);
125
140
  }
141
+ const startTime = Date.now();
126
142
  try {
127
143
  const [results] = await connection.query(sql, params);
144
+ const duration = Date.now() - startTime;
145
+ queryLogger_1.QueryLogger.log(sql, params, duration, 'success');
128
146
  return results;
129
147
  }
130
148
  catch (error) {
149
+ const duration = Date.now() - startTime;
150
+ queryLogger_1.QueryLogger.log(sql, params, duration, 'error', error.message);
131
151
  throw new Error(`Query execution in transaction failed: ${error}`);
132
152
  }
133
153
  }
@@ -0,0 +1,51 @@
1
+ export interface QueryLog {
2
+ sql: string;
3
+ params?: any[];
4
+ duration: number;
5
+ timestamp: string;
6
+ status: 'success' | 'error';
7
+ error?: string;
8
+ }
9
+ export declare class QueryLogger {
10
+ private static logs;
11
+ private static readonly MAX_LOGS;
12
+ private static readonly MAX_SQL_LENGTH;
13
+ private static readonly MAX_PARAM_LENGTH;
14
+ private static readonly MAX_PARAM_ITEMS;
15
+ /**
16
+ * Safely stringify a value with truncation and error handling
17
+ */
18
+ private static safeStringify;
19
+ /**
20
+ * Truncate SQL string to prevent memory issues
21
+ */
22
+ private static truncateSQL;
23
+ /**
24
+ * Create a memory-safe copy of parameters
25
+ */
26
+ private static sanitizeParams;
27
+ /**
28
+ * Log a query execution
29
+ */
30
+ static log(sql: string, params: any[] | undefined, duration: number, status: 'success' | 'error', error?: string): void;
31
+ /**
32
+ * Get all logged queries (returns shallow copy of array)
33
+ */
34
+ static getLogs(): QueryLog[];
35
+ /**
36
+ * Get the last N logged queries
37
+ */
38
+ static getLastLogs(count?: number): QueryLog[];
39
+ /**
40
+ * Get logs for the current session (last query execution)
41
+ */
42
+ static getLastLog(): QueryLog | undefined;
43
+ /**
44
+ * Clear all logs
45
+ */
46
+ static clearLogs(): void;
47
+ /**
48
+ * Get logs as formatted string for output
49
+ */
50
+ static formatLogs(logs: QueryLog[]): string;
51
+ }
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryLogger = void 0;
4
+ class QueryLogger {
5
+ /**
6
+ * Safely stringify a value with truncation and error handling
7
+ */
8
+ static safeStringify(value, maxLength = 100) {
9
+ try {
10
+ if (value === null)
11
+ return 'null';
12
+ if (value === undefined)
13
+ return 'undefined';
14
+ if (typeof value === 'string') {
15
+ return value.length > maxLength ? value.substring(0, maxLength) + '...' : value;
16
+ }
17
+ if (typeof value === 'number' || typeof value === 'boolean') {
18
+ return String(value);
19
+ }
20
+ if (typeof value === 'bigint') {
21
+ return value.toString() + 'n';
22
+ }
23
+ if (Array.isArray(value)) {
24
+ if (value.length === 0)
25
+ return '[]';
26
+ const items = value.slice(0, 3).map(v => this.safeStringify(v, 30));
27
+ return value.length > 3
28
+ ? `[${items.join(', ')}, ... +${value.length - 3} more]`
29
+ : `[${items.join(', ')}]`;
30
+ }
31
+ if (typeof value === 'object') {
32
+ const str = JSON.stringify(value);
33
+ return str.length > maxLength ? str.substring(0, maxLength) + '...}' : str;
34
+ }
35
+ return String(value);
36
+ }
37
+ catch (error) {
38
+ return '[Unstringifiable]';
39
+ }
40
+ }
41
+ /**
42
+ * Truncate SQL string to prevent memory issues
43
+ */
44
+ static truncateSQL(sql) {
45
+ if (sql.length <= this.MAX_SQL_LENGTH)
46
+ return sql;
47
+ return sql.substring(0, this.MAX_SQL_LENGTH) + `... [truncated ${sql.length - this.MAX_SQL_LENGTH} chars]`;
48
+ }
49
+ /**
50
+ * Create a memory-safe copy of parameters
51
+ */
52
+ static sanitizeParams(params) {
53
+ if (!params || params.length === 0)
54
+ return undefined;
55
+ // Only keep first N params to prevent memory issues
56
+ const limitedParams = params.slice(0, this.MAX_PARAM_ITEMS);
57
+ // Create deep copy to prevent reference issues
58
+ try {
59
+ return JSON.parse(JSON.stringify(limitedParams));
60
+ }
61
+ catch (error) {
62
+ // If JSON serialization fails, create safe string representations
63
+ return limitedParams.map(p => this.safeStringify(p, 50));
64
+ }
65
+ }
66
+ /**
67
+ * Log a query execution
68
+ */
69
+ static log(sql, params, duration, status, error) {
70
+ const log = {
71
+ sql: this.truncateSQL(sql),
72
+ params: this.sanitizeParams(params),
73
+ duration,
74
+ timestamp: new Date().toISOString(),
75
+ status,
76
+ error: error ? (error.length > 200 ? error.substring(0, 200) + '...' : error) : undefined
77
+ };
78
+ this.logs.push(log);
79
+ // Keep only the last MAX_LOGS entries
80
+ if (this.logs.length > this.MAX_LOGS) {
81
+ this.logs.shift();
82
+ }
83
+ }
84
+ /**
85
+ * Get all logged queries (returns shallow copy of array)
86
+ */
87
+ static getLogs() {
88
+ return [...this.logs];
89
+ }
90
+ /**
91
+ * Get the last N logged queries
92
+ */
93
+ static getLastLogs(count = 10) {
94
+ // Ensure count doesn't exceed array length to prevent issues
95
+ const safeCount = Math.min(Math.max(1, count), this.logs.length);
96
+ return this.logs.slice(-safeCount);
97
+ }
98
+ /**
99
+ * Get logs for the current session (last query execution)
100
+ */
101
+ static getLastLog() {
102
+ return this.logs[this.logs.length - 1];
103
+ }
104
+ /**
105
+ * Clear all logs
106
+ */
107
+ static clearLogs() {
108
+ this.logs = [];
109
+ }
110
+ /**
111
+ * Get logs as formatted string for output
112
+ */
113
+ static formatLogs(logs) {
114
+ if (logs.length === 0)
115
+ return '';
116
+ return logs.map((log, index) => {
117
+ let paramStr = '';
118
+ if (log.params && log.params.length > 0) {
119
+ try {
120
+ const paramsJson = JSON.stringify(log.params);
121
+ paramStr = paramsJson.length > this.MAX_PARAM_LENGTH
122
+ ? ` | Params: ${paramsJson.substring(0, this.MAX_PARAM_LENGTH)}...`
123
+ : ` | Params: ${paramsJson}`;
124
+ }
125
+ catch (error) {
126
+ paramStr = ' | Params: [Error serializing]';
127
+ }
128
+ }
129
+ const errorStr = log.error ? ` | Error: ${log.error}` : '';
130
+ return `[${index + 1}] ${log.timestamp} | ${log.sql}${paramStr} | Duration: ${log.duration}ms | Status: ${log.status}${errorStr}`;
131
+ }).join('\n');
132
+ }
133
+ }
134
+ exports.QueryLogger = QueryLogger;
135
+ QueryLogger.logs = [];
136
+ QueryLogger.MAX_LOGS = 100;
137
+ QueryLogger.MAX_SQL_LENGTH = 500; // Truncate SQL beyond this
138
+ QueryLogger.MAX_PARAM_LENGTH = 200; // Truncate params beyond this
139
+ QueryLogger.MAX_PARAM_ITEMS = 5; // Only log first N params
package/dist/index.d.ts CHANGED
@@ -41,6 +41,7 @@ export declare class MySQLMCP {
41
41
  status: string;
42
42
  data?: any;
43
43
  error?: string;
44
+ queryLog?: string;
44
45
  }>;
45
46
  readRecords(params: {
46
47
  table_name: string;
@@ -58,6 +59,7 @@ export declare class MySQLMCP {
58
59
  data?: any[];
59
60
  total?: number;
60
61
  error?: string;
62
+ queryLog?: string;
61
63
  }>;
62
64
  updateRecord(params: {
63
65
  table_name: string;
@@ -69,6 +71,7 @@ export declare class MySQLMCP {
69
71
  affectedRows: number;
70
72
  };
71
73
  error?: string;
74
+ queryLog?: string;
72
75
  }>;
73
76
  deleteRecord(params: {
74
77
  table_name: string;
@@ -79,6 +82,7 @@ export declare class MySQLMCP {
79
82
  affectedRows: number;
80
83
  };
81
84
  error?: string;
85
+ queryLog?: string;
82
86
  }>;
83
87
  runQuery(params: {
84
88
  query: string;
@@ -87,6 +91,7 @@ export declare class MySQLMCP {
87
91
  status: string;
88
92
  data?: any[];
89
93
  error?: string;
94
+ queryLog?: string;
90
95
  }>;
91
96
  executeSql(params: {
92
97
  query: string;
@@ -95,6 +100,7 @@ export declare class MySQLMCP {
95
100
  status: string;
96
101
  data?: any;
97
102
  error?: string;
103
+ queryLog?: string;
98
104
  }>;
99
105
  createTable(params: any): Promise<{
100
106
  status: string;
@@ -14,6 +14,7 @@ export declare class CrudTools {
14
14
  status: string;
15
15
  data?: any;
16
16
  error?: string;
17
+ queryLog?: string;
17
18
  }>;
18
19
  /**
19
20
  * Read records from the specified table with optional filters, pagination, and sorting
@@ -28,6 +29,7 @@ export declare class CrudTools {
28
29
  data?: any[];
29
30
  total?: number;
30
31
  error?: string;
32
+ queryLog?: string;
31
33
  }>;
32
34
  /**
33
35
  * Update records in the specified table based on conditions
@@ -42,6 +44,7 @@ export declare class CrudTools {
42
44
  affectedRows: number;
43
45
  };
44
46
  error?: string;
47
+ queryLog?: string;
45
48
  }>;
46
49
  /**
47
50
  * Delete records from the specified table based on conditions
@@ -55,6 +58,7 @@ export declare class CrudTools {
55
58
  affectedRows: number;
56
59
  };
57
60
  error?: string;
61
+ queryLog?: string;
58
62
  }>;
59
63
  /**
60
64
  * Bulk insert multiple records into the specified table
@@ -64,13 +64,15 @@ class CrudTools {
64
64
  data: {
65
65
  insertId: result.insertId,
66
66
  affectedRows: result.affectedRows
67
- }
67
+ },
68
+ queryLog: this.db.getFormattedQueryLogs(1)
68
69
  };
69
70
  }
70
71
  catch (error) {
71
72
  return {
72
73
  status: 'error',
73
- error: error.message
74
+ error: error.message,
75
+ queryLog: this.db.getFormattedQueryLogs(1)
74
76
  };
75
77
  }
76
78
  }
@@ -188,7 +190,8 @@ class CrudTools {
188
190
  return {
189
191
  status: 'success',
190
192
  data: results,
191
- total
193
+ total,
194
+ queryLog: this.db.getFormattedQueryLogs(2)
192
195
  };
193
196
  }
194
197
  else {
@@ -198,14 +201,16 @@ class CrudTools {
198
201
  return {
199
202
  status: 'success',
200
203
  data: results,
201
- total: results.length
204
+ total: results.length,
205
+ queryLog: this.db.getFormattedQueryLogs(1)
202
206
  };
203
207
  }
204
208
  }
205
209
  catch (error) {
206
210
  return {
207
211
  status: 'error',
208
- error: error.message
212
+ error: error.message,
213
+ queryLog: this.db.getFormattedQueryLogs(1)
209
214
  };
210
215
  }
211
216
  }
@@ -320,13 +325,15 @@ class CrudTools {
320
325
  status: 'success',
321
326
  data: {
322
327
  affectedRows: result.affectedRows
323
- }
328
+ },
329
+ queryLog: this.db.getFormattedQueryLogs(1)
324
330
  };
325
331
  }
326
332
  catch (error) {
327
333
  return {
328
334
  status: 'error',
329
- error: error.message
335
+ error: error.message,
336
+ queryLog: this.db.getFormattedQueryLogs(1)
330
337
  };
331
338
  }
332
339
  }
@@ -429,13 +436,15 @@ class CrudTools {
429
436
  status: 'success',
430
437
  data: {
431
438
  affectedRows: result.affectedRows
432
- }
439
+ },
440
+ queryLog: this.db.getFormattedQueryLogs(1)
433
441
  };
434
442
  }
435
443
  catch (error) {
436
444
  return {
437
445
  status: 'error',
438
- error: error.message
446
+ error: error.message,
447
+ queryLog: this.db.getFormattedQueryLogs(1)
439
448
  };
440
449
  }
441
450
  }
@@ -13,6 +13,7 @@ export declare class QueryTools {
13
13
  status: string;
14
14
  data?: any[];
15
15
  error?: string;
16
+ queryLog?: string;
16
17
  }>;
17
18
  /**
18
19
  * Execute write operations (INSERT, UPDATE, DELETE) with validation
@@ -25,5 +26,6 @@ export declare class QueryTools {
25
26
  status: string;
26
27
  data?: any;
27
28
  error?: string;
29
+ queryLog?: string;
28
30
  }>;
29
31
  }
@@ -55,12 +55,14 @@ class QueryTools {
55
55
  return {
56
56
  status: "success",
57
57
  data: results,
58
+ queryLog: this.db.getFormattedQueryLogs(1),
58
59
  };
59
60
  }
60
61
  catch (error) {
61
62
  return {
62
63
  status: "error",
63
64
  error: error.message,
65
+ queryLog: this.db.getFormattedQueryLogs(1),
64
66
  };
65
67
  }
66
68
  }
@@ -110,12 +112,14 @@ class QueryTools {
110
112
  affectedRows: result.affectedRows || 0,
111
113
  insertId: result.insertId || null,
112
114
  },
115
+ queryLog: this.db.getFormattedQueryLogs(1),
113
116
  };
114
117
  }
115
118
  catch (error) {
116
119
  return {
117
120
  status: "error",
118
121
  error: error.message,
122
+ queryLog: this.db.getFormattedQueryLogs(1),
119
123
  };
120
124
  }
121
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@berthojoris/mcp-mysql-server",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "Model Context Protocol server for MySQL database integration with dynamic per-project permissions and data export capabilities",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",