@berthojoris/mcp-mysql-server 1.4.15 → 1.5.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.
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const promise_1 = __importDefault(require("mysql2/promise"));
7
7
  const config_1 = require("../config/config");
8
8
  const queryLogger_1 = require("./queryLogger");
9
+ const queryCache_1 = require("../cache/queryCache");
9
10
  class DatabaseConnection {
10
11
  constructor() {
11
12
  this.pool = promise_1.default.createPool({
@@ -16,9 +17,10 @@ class DatabaseConnection {
16
17
  database: config_1.dbConfig.database,
17
18
  waitForConnections: true,
18
19
  connectionLimit: 10,
19
- queueLimit: 0
20
+ queueLimit: 0,
20
21
  });
21
22
  this.activeTransactions = new Map();
23
+ this.queryCache = queryCache_1.QueryCache.getInstance();
22
24
  }
23
25
  static getInstance() {
24
26
  if (!DatabaseConnection.instance) {
@@ -34,20 +36,54 @@ class DatabaseConnection {
34
36
  throw new Error(`Failed to get database connection: ${error}`);
35
37
  }
36
38
  }
37
- async query(sql, params) {
39
+ async query(sql, params, useCache = true) {
40
+ const normalizedSql = sql.trim().toUpperCase();
41
+ const isSelectQuery = normalizedSql.startsWith("SELECT");
42
+ // Check cache for SELECT queries
43
+ if (useCache && isSelectQuery) {
44
+ const cachedEntry = this.queryCache.get(sql, params);
45
+ if (cachedEntry) {
46
+ // Log cache hit
47
+ queryLogger_1.QueryLogger.log(sql, params, 0, "success", undefined, true);
48
+ return cachedEntry.data;
49
+ }
50
+ }
38
51
  const startTime = Date.now();
39
52
  try {
40
53
  const [results] = await this.pool.query(sql, params);
41
54
  const duration = Date.now() - startTime;
42
- queryLogger_1.QueryLogger.log(sql, params, duration, 'success');
55
+ queryLogger_1.QueryLogger.log(sql, params, duration, "success");
56
+ // Cache SELECT query results
57
+ if (useCache && isSelectQuery) {
58
+ this.queryCache.set(sql, params, results);
59
+ }
60
+ // Invalidate cache for write operations
61
+ if (!isSelectQuery) {
62
+ this.invalidateCacheForWriteOperation(sql);
63
+ }
43
64
  return results;
44
65
  }
45
66
  catch (error) {
46
67
  const duration = Date.now() - startTime;
47
- queryLogger_1.QueryLogger.log(sql, params, duration, 'error', error.message);
68
+ queryLogger_1.QueryLogger.log(sql, params, duration, "error", error.message);
48
69
  throw new Error(`Query execution failed: ${error}`);
49
70
  }
50
71
  }
72
+ /**
73
+ * Invalidate cache entries when write operations occur
74
+ */
75
+ invalidateCacheForWriteOperation(sql) {
76
+ // Extract table name from INSERT, UPDATE, DELETE statements
77
+ const insertMatch = sql.match(/INSERT\s+INTO\s+[\`"']?(\w+)[\`"']?/i);
78
+ const updateMatch = sql.match(/UPDATE\s+[\`"']?(\w+)[\`"']?/i);
79
+ const deleteMatch = sql.match(/DELETE\s+FROM\s+[\`"']?(\w+)[\`"']?/i);
80
+ const truncateMatch = sql.match(/TRUNCATE\s+(?:TABLE\s+)?[\`"']?(\w+)[\`"']?/i);
81
+ const dropMatch = sql.match(/DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?[\`"']?(\w+)[\`"']?/i);
82
+ const match = insertMatch || updateMatch || deleteMatch || truncateMatch || dropMatch;
83
+ if (match && match[1]) {
84
+ this.queryCache.invalidateTable(match[1]);
85
+ }
86
+ }
51
87
  async testConnection() {
52
88
  const startTime = Date.now();
53
89
  try {
@@ -96,7 +132,7 @@ class DatabaseConnection {
96
132
  connection.release();
97
133
  }
98
134
  catch (rollbackError) {
99
- console.error('Failed to rollback after commit error:', rollbackError);
135
+ console.error("Failed to rollback after commit error:", rollbackError);
100
136
  }
101
137
  this.activeTransactions.delete(transactionId);
102
138
  throw new Error(`Failed to commit transaction: ${error}`);
@@ -133,6 +169,36 @@ class DatabaseConnection {
133
169
  getFormattedQueryLogs(count = 1) {
134
170
  return queryLogger_1.QueryLogger.formatLogs(queryLogger_1.QueryLogger.getLastLogs(count));
135
171
  }
172
+ // Cache Management Methods
173
+ getCacheStats() {
174
+ return this.queryCache.getStats();
175
+ }
176
+ getCacheConfig() {
177
+ return this.queryCache.getConfig();
178
+ }
179
+ setCacheConfig(config) {
180
+ this.queryCache.setConfig(config);
181
+ }
182
+ clearCache() {
183
+ const previousSize = this.queryCache.getStats().currentSize;
184
+ this.queryCache.clear();
185
+ return previousSize;
186
+ }
187
+ invalidateCache(pattern) {
188
+ return this.queryCache.invalidate(pattern);
189
+ }
190
+ invalidateCacheForTable(tableName) {
191
+ return this.queryCache.invalidateTable(tableName);
192
+ }
193
+ enableCache() {
194
+ this.queryCache.enable();
195
+ }
196
+ disableCache() {
197
+ this.queryCache.disable();
198
+ }
199
+ resetCacheStats() {
200
+ this.queryCache.resetStats();
201
+ }
136
202
  async executeInTransaction(transactionId, sql, params) {
137
203
  const connection = this.activeTransactions.get(transactionId);
138
204
  if (!connection) {
@@ -142,12 +208,12 @@ class DatabaseConnection {
142
208
  try {
143
209
  const [results] = await connection.query(sql, params);
144
210
  const duration = Date.now() - startTime;
145
- queryLogger_1.QueryLogger.log(sql, params, duration, 'success');
211
+ queryLogger_1.QueryLogger.log(sql, params, duration, "success");
146
212
  return results;
147
213
  }
148
214
  catch (error) {
149
215
  const duration = Date.now() - startTime;
150
- queryLogger_1.QueryLogger.log(sql, params, duration, 'error', error.message);
216
+ queryLogger_1.QueryLogger.log(sql, params, duration, "error", error.message);
151
217
  throw new Error(`Query execution in transaction failed: ${error}`);
152
218
  }
153
219
  }
@@ -3,8 +3,9 @@ export interface QueryLog {
3
3
  params?: any[];
4
4
  duration: number;
5
5
  timestamp: string;
6
- status: 'success' | 'error';
6
+ status: "success" | "error";
7
7
  error?: string;
8
+ cacheHit?: boolean;
8
9
  }
9
10
  export declare class QueryLogger {
10
11
  private static logs;
@@ -27,7 +28,7 @@ export declare class QueryLogger {
27
28
  /**
28
29
  * Log a query execution
29
30
  */
30
- static log(sql: string, params: any[] | undefined, duration: number, status: 'success' | 'error', error?: string): void;
31
+ static log(sql: string, params: any[] | undefined, duration: number, status: "success" | "error", error?: string, cacheHit?: boolean): void;
31
32
  /**
32
33
  * Get all logged queries (returns shallow copy of array)
33
34
  */
@@ -8,34 +8,38 @@ class QueryLogger {
8
8
  static safeStringify(value, maxLength = 100) {
9
9
  try {
10
10
  if (value === null)
11
- return 'null';
11
+ return "null";
12
12
  if (value === undefined)
13
- return 'undefined';
14
- if (typeof value === 'string') {
15
- return value.length > maxLength ? value.substring(0, maxLength) + '...' : value;
13
+ return "undefined";
14
+ if (typeof value === "string") {
15
+ return value.length > maxLength
16
+ ? value.substring(0, maxLength) + "..."
17
+ : value;
16
18
  }
17
- if (typeof value === 'number' || typeof value === 'boolean') {
19
+ if (typeof value === "number" || typeof value === "boolean") {
18
20
  return String(value);
19
21
  }
20
- if (typeof value === 'bigint') {
21
- return value.toString() + 'n';
22
+ if (typeof value === "bigint") {
23
+ return value.toString() + "n";
22
24
  }
23
25
  if (Array.isArray(value)) {
24
26
  if (value.length === 0)
25
- return '[]';
26
- const items = value.slice(0, 3).map(v => this.safeStringify(v, 30));
27
+ return "[]";
28
+ const items = value.slice(0, 3).map((v) => this.safeStringify(v, 30));
27
29
  return value.length > 3
28
- ? `[${items.join(', ')}, ... +${value.length - 3} more]`
29
- : `[${items.join(', ')}]`;
30
+ ? `[${items.join(", ")}, ... +${value.length - 3} more]`
31
+ : `[${items.join(", ")}]`;
30
32
  }
31
- if (typeof value === 'object') {
33
+ if (typeof value === "object") {
32
34
  const str = JSON.stringify(value);
33
- return str.length > maxLength ? str.substring(0, maxLength) + '...}' : str;
35
+ return str.length > maxLength
36
+ ? str.substring(0, maxLength) + "...}"
37
+ : str;
34
38
  }
35
39
  return String(value);
36
40
  }
37
41
  catch (error) {
38
- return '[Unstringifiable]';
42
+ return "[Unstringifiable]";
39
43
  }
40
44
  }
41
45
  /**
@@ -44,7 +48,8 @@ class QueryLogger {
44
48
  static truncateSQL(sql) {
45
49
  if (sql.length <= this.MAX_SQL_LENGTH)
46
50
  return sql;
47
- return sql.substring(0, this.MAX_SQL_LENGTH) + `... [truncated ${sql.length - this.MAX_SQL_LENGTH} chars]`;
51
+ return (sql.substring(0, this.MAX_SQL_LENGTH) +
52
+ `... [truncated ${sql.length - this.MAX_SQL_LENGTH} chars]`);
48
53
  }
49
54
  /**
50
55
  * Create a memory-safe copy of parameters
@@ -60,20 +65,25 @@ class QueryLogger {
60
65
  }
61
66
  catch (error) {
62
67
  // If JSON serialization fails, create safe string representations
63
- return limitedParams.map(p => this.safeStringify(p, 50));
68
+ return limitedParams.map((p) => this.safeStringify(p, 50));
64
69
  }
65
70
  }
66
71
  /**
67
72
  * Log a query execution
68
73
  */
69
- static log(sql, params, duration, status, error) {
74
+ static log(sql, params, duration, status, error, cacheHit) {
70
75
  const log = {
71
76
  sql: this.truncateSQL(sql),
72
77
  params: this.sanitizeParams(params),
73
78
  duration,
74
79
  timestamp: new Date().toISOString(),
75
80
  status,
76
- error: error ? (error.length > 200 ? error.substring(0, 200) + '...' : error) : undefined
81
+ cacheHit: cacheHit,
82
+ error: error
83
+ ? error.length > 200
84
+ ? error.substring(0, 200) + "..."
85
+ : error
86
+ : undefined,
77
87
  };
78
88
  this.logs.push(log);
79
89
  // Keep only the last MAX_LOGS entries
@@ -113,9 +123,9 @@ class QueryLogger {
113
123
  static formatSQL(sql) {
114
124
  // Add line breaks for better readability
115
125
  return sql
116
- .replace(/\s+/g, ' ') // Normalize whitespace
117
- .replace(/\b(SELECT|FROM|WHERE|JOIN|LEFT JOIN|RIGHT JOIN|INNER JOIN|OUTER JOIN|GROUP BY|ORDER BY|HAVING|LIMIT|UNION|INSERT INTO|UPDATE|DELETE FROM|SET|VALUES|CREATE|ALTER|DROP|TRUNCATE|BEGIN|COMMIT|ROLLBACK|CALL)\b/gi, '\n$1')
118
- .replace(/,\s*/g, ',\n ') // Add line breaks after commas
126
+ .replace(/\s+/g, " ") // Normalize whitespace
127
+ .replace(/\b(SELECT|FROM|WHERE|JOIN|LEFT JOIN|RIGHT JOIN|INNER JOIN|OUTER JOIN|GROUP BY|ORDER BY|HAVING|LIMIT|UNION|INSERT INTO|UPDATE|DELETE FROM|SET|VALUES|CREATE|ALTER|DROP|TRUNCATE|BEGIN|COMMIT|ROLLBACK|CALL)\b/gi, "\n$1")
128
+ .replace(/,\s*/g, ",\n ") // Add line breaks after commas
119
129
  .trim();
120
130
  }
121
131
  /**
@@ -124,61 +134,72 @@ class QueryLogger {
124
134
  */
125
135
  static formatLogs(logs) {
126
136
  if (logs.length === 0)
127
- return '';
128
- return logs.map((log, index) => {
137
+ return "";
138
+ return logs
139
+ .map((log, index) => {
129
140
  // Format the SQL for better readability
130
141
  const formattedSQL = this.formatSQL(log.sql);
131
142
  // Format parameters
132
- let paramStr = '';
143
+ let paramStr = "";
133
144
  if (log.params && log.params.length > 0) {
134
145
  try {
135
146
  const paramsJson = JSON.stringify(log.params, null, 2);
136
- paramStr = paramsJson.length > this.MAX_PARAM_LENGTH
137
- ? `\n📋 Parameters:\n${paramsJson.substring(0, this.MAX_PARAM_LENGTH)}...`
138
- : `\n📋 Parameters:\n${paramsJson}`;
147
+ paramStr =
148
+ paramsJson.length > this.MAX_PARAM_LENGTH
149
+ ? `\n📋 Parameters:\n${paramsJson.substring(0, this.MAX_PARAM_LENGTH)}...`
150
+ : `\n📋 Parameters:\n${paramsJson}`;
139
151
  }
140
152
  catch (error) {
141
- paramStr = '\n📋 Parameters: [Error serializing]';
153
+ paramStr = "\n📋 Parameters: [Error serializing]";
142
154
  }
143
155
  }
144
156
  // Format error if present
145
- const errorStr = log.error ? `\n❌ Error: ${log.error}` : '';
157
+ const errorStr = log.error ? `\n❌ Error: ${log.error}` : "";
146
158
  // Format status with emoji for better visibility
147
- const statusEmoji = log.status === 'success' ? '' : '';
148
- const statusText = log.status === 'success' ? 'SUCCESS' : 'ERROR';
159
+ const statusEmoji = log.status === "success" ? "" : "";
160
+ const statusText = log.status === "success" ? "SUCCESS" : "ERROR";
161
+ // Format cache hit indicator
162
+ const cacheStr = log.cacheHit
163
+ ? "\n💾 Cache: HIT (served from cache)"
164
+ : "";
165
+ const cacheLabel = log.cacheHit ? " [CACHED]" : "";
149
166
  // Build the formatted log entry with clear visual hierarchy
150
167
  return `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
151
- ${statusEmoji} SQL Query #${index + 1} - ${statusText}
168
+ ${statusEmoji} SQL Query #${index + 1} - ${statusText}${cacheLabel}
152
169
  ⏱️ Execution Time: ${log.duration}ms
153
- 🕐 Timestamp: ${log.timestamp}
170
+ 🕐 Timestamp: ${log.timestamp}${cacheStr}
154
171
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
155
172
 
156
173
  📝 SQL Query:
157
174
  ${formattedSQL}${paramStr}${errorStr}`;
158
- }).join('\n\n');
175
+ })
176
+ .join("\n\n");
159
177
  }
160
178
  /**
161
179
  * Get logs as compact formatted string (for backward compatibility)
162
180
  */
163
181
  static formatLogsCompact(logs) {
164
182
  if (logs.length === 0)
165
- return '';
166
- return logs.map((log, index) => {
167
- let paramStr = '';
183
+ return "";
184
+ return logs
185
+ .map((log, index) => {
186
+ let paramStr = "";
168
187
  if (log.params && log.params.length > 0) {
169
188
  try {
170
189
  const paramsJson = JSON.stringify(log.params);
171
- paramStr = paramsJson.length > this.MAX_PARAM_LENGTH
172
- ? ` | Params: ${paramsJson.substring(0, this.MAX_PARAM_LENGTH)}...`
173
- : ` | Params: ${paramsJson}`;
190
+ paramStr =
191
+ paramsJson.length > this.MAX_PARAM_LENGTH
192
+ ? ` | Params: ${paramsJson.substring(0, this.MAX_PARAM_LENGTH)}...`
193
+ : ` | Params: ${paramsJson}`;
174
194
  }
175
195
  catch (error) {
176
- paramStr = ' | Params: [Error serializing]';
196
+ paramStr = " | Params: [Error serializing]";
177
197
  }
178
198
  }
179
- const errorStr = log.error ? ` | Error: ${log.error}` : '';
199
+ const errorStr = log.error ? ` | Error: ${log.error}` : "";
180
200
  return `[${index + 1}] ${log.timestamp} | ${log.sql}${paramStr} | Duration: ${log.duration}ms | Status: ${log.status}${errorStr}`;
181
- }).join('\n');
201
+ })
202
+ .join("\n");
182
203
  }
183
204
  }
184
205
  exports.QueryLogger = QueryLogger;
package/dist/index.d.ts CHANGED
@@ -55,7 +55,7 @@ export declare class MySQLMCP {
55
55
  };
56
56
  sorting?: {
57
57
  field: string;
58
- direction: 'asc' | 'desc';
58
+ direction: "asc" | "desc";
59
59
  };
60
60
  }): Promise<{
61
61
  status: string;
@@ -95,6 +95,7 @@ export declare class MySQLMCP {
95
95
  data?: any[];
96
96
  error?: string;
97
97
  queryLog?: string;
98
+ optimizedQuery?: string;
98
99
  }>;
99
100
  executeSql(params: {
100
101
  query: string;
@@ -214,7 +215,7 @@ export declare class MySQLMCP {
214
215
  procedure_name: string;
215
216
  parameters?: Array<{
216
217
  name: string;
217
- mode: 'IN' | 'OUT' | 'INOUT';
218
+ mode: "IN" | "OUT" | "INOUT";
218
219
  data_type: string;
219
220
  }>;
220
221
  body: string;
@@ -251,7 +252,7 @@ export declare class MySQLMCP {
251
252
  };
252
253
  sorting?: {
253
254
  field: string;
254
- direction: 'asc' | 'desc';
255
+ direction: "asc" | "desc";
255
256
  };
256
257
  include_headers?: boolean;
257
258
  }): Promise<{
@@ -317,5 +318,77 @@ export declare class MySQLMCP {
317
318
  error?: string;
318
319
  }>;
319
320
  close(): Promise<void>;
321
+ /**
322
+ * Get cache statistics
323
+ */
324
+ getCacheStats(): {
325
+ status: string;
326
+ data: import("./cache/queryCache").CacheStats;
327
+ };
328
+ /**
329
+ * Get cache configuration
330
+ */
331
+ getCacheConfig(): {
332
+ status: string;
333
+ data: import("./cache/queryCache").CacheConfig;
334
+ };
335
+ /**
336
+ * Configure cache settings
337
+ */
338
+ configureCacheSettings(params: {
339
+ enabled?: boolean;
340
+ ttlMs?: number;
341
+ maxSize?: number;
342
+ maxMemoryMB?: number;
343
+ }): {
344
+ status: string;
345
+ data: {
346
+ message: string;
347
+ config: import("./cache/queryCache").CacheConfig;
348
+ };
349
+ };
350
+ /**
351
+ * Clear the query cache
352
+ */
353
+ clearCache(): {
354
+ status: string;
355
+ data: {
356
+ message: string;
357
+ entriesCleared: number;
358
+ };
359
+ };
360
+ /**
361
+ * Invalidate cache for a specific table
362
+ */
363
+ invalidateCacheForTable(params: {
364
+ table_name: string;
365
+ }): {
366
+ status: string;
367
+ data: {
368
+ message: string;
369
+ entriesInvalidated: number;
370
+ };
371
+ };
372
+ /**
373
+ * Analyze a query and get optimization suggestions
374
+ */
375
+ analyzeQuery(params: {
376
+ query: string;
377
+ }): {
378
+ status: string;
379
+ data: import("./optimization/queryOptimizer").QueryAnalysis;
380
+ };
381
+ /**
382
+ * Get suggested optimizer hints for a specific optimization goal
383
+ */
384
+ getOptimizationHints(params: {
385
+ goal: "SPEED" | "MEMORY" | "STABILITY";
386
+ }): {
387
+ status: string;
388
+ data: {
389
+ goal: "SPEED" | "MEMORY" | "STABILITY";
390
+ hints: import("./optimization/queryOptimizer").QueryHints;
391
+ };
392
+ };
320
393
  }
321
394
  export default MySQLMCP;