@berthojoris/mcp-mysql-server 1.5.0 → 1.6.1

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.
@@ -0,0 +1,345 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.IndexTools = void 0;
7
+ const connection_1 = __importDefault(require("../db/connection"));
8
+ const config_1 = require("../config/config");
9
+ class IndexTools {
10
+ constructor(security) {
11
+ this.db = connection_1.default.getInstance();
12
+ this.security = security;
13
+ }
14
+ /**
15
+ * Validate database access - ensures only the connected database can be accessed
16
+ */
17
+ validateDatabaseAccess(requestedDatabase) {
18
+ const connectedDatabase = config_1.dbConfig.database;
19
+ if (!connectedDatabase) {
20
+ return {
21
+ valid: false,
22
+ database: '',
23
+ error: 'No database specified in connection string. Cannot access any database.'
24
+ };
25
+ }
26
+ if (!requestedDatabase) {
27
+ return {
28
+ valid: true,
29
+ database: connectedDatabase
30
+ };
31
+ }
32
+ if (requestedDatabase !== connectedDatabase) {
33
+ return {
34
+ valid: false,
35
+ database: '',
36
+ error: `Access denied. You can only access the connected database '${connectedDatabase}'. Requested database '${requestedDatabase}' is not allowed.`
37
+ };
38
+ }
39
+ return {
40
+ valid: true,
41
+ database: connectedDatabase
42
+ };
43
+ }
44
+ /**
45
+ * List all indexes for a table
46
+ */
47
+ async listIndexes(params) {
48
+ try {
49
+ const dbValidation = this.validateDatabaseAccess(params?.database);
50
+ if (!dbValidation.valid) {
51
+ return { status: 'error', error: dbValidation.error };
52
+ }
53
+ const { table_name } = params;
54
+ const database = dbValidation.database;
55
+ // Validate table name
56
+ const identifierValidation = this.security.validateIdentifier(table_name);
57
+ if (!identifierValidation.valid) {
58
+ return { status: 'error', error: identifierValidation.error || 'Invalid table name' };
59
+ }
60
+ const query = `SHOW INDEX FROM \`${database}\`.\`${table_name}\``;
61
+ const results = await this.db.query(query);
62
+ // Group indexes by name for better readability
63
+ const indexMap = new Map();
64
+ for (const row of results) {
65
+ const indexName = row.Key_name;
66
+ if (!indexMap.has(indexName)) {
67
+ indexMap.set(indexName, {
68
+ index_name: indexName,
69
+ table_name: row.Table,
70
+ is_unique: !row.Non_unique,
71
+ is_primary: indexName === 'PRIMARY',
72
+ index_type: row.Index_type,
73
+ columns: [],
74
+ cardinality: row.Cardinality,
75
+ nullable: row.Null === 'YES',
76
+ comment: row.Index_comment || null,
77
+ visible: row.Visible === 'YES'
78
+ });
79
+ }
80
+ indexMap.get(indexName).columns.push({
81
+ column_name: row.Column_name,
82
+ seq_in_index: row.Seq_in_index,
83
+ collation: row.Collation,
84
+ sub_part: row.Sub_part,
85
+ expression: row.Expression || null
86
+ });
87
+ }
88
+ return {
89
+ status: 'success',
90
+ data: Array.from(indexMap.values()),
91
+ queryLog: this.db.getFormattedQueryLogs(1)
92
+ };
93
+ }
94
+ catch (error) {
95
+ return {
96
+ status: 'error',
97
+ error: error.message,
98
+ queryLog: this.db.getFormattedQueryLogs(1)
99
+ };
100
+ }
101
+ }
102
+ /**
103
+ * Get detailed information about a specific index
104
+ */
105
+ async getIndexInfo(params) {
106
+ try {
107
+ const dbValidation = this.validateDatabaseAccess(params?.database);
108
+ if (!dbValidation.valid) {
109
+ return { status: 'error', error: dbValidation.error };
110
+ }
111
+ const { table_name, index_name } = params;
112
+ const database = dbValidation.database;
113
+ // Validate names
114
+ if (!this.security.validateIdentifier(table_name).valid) {
115
+ return { status: 'error', error: 'Invalid table name' };
116
+ }
117
+ if (!this.security.validateIdentifier(index_name).valid) {
118
+ return { status: 'error', error: 'Invalid index name' };
119
+ }
120
+ const query = `
121
+ SELECT
122
+ s.INDEX_NAME as index_name,
123
+ s.TABLE_NAME as table_name,
124
+ s.NON_UNIQUE as non_unique,
125
+ s.SEQ_IN_INDEX as seq_in_index,
126
+ s.COLUMN_NAME as column_name,
127
+ s.COLLATION as collation,
128
+ s.CARDINALITY as cardinality,
129
+ s.SUB_PART as sub_part,
130
+ s.PACKED as packed,
131
+ s.NULLABLE as nullable,
132
+ s.INDEX_TYPE as index_type,
133
+ s.COMMENT as comment,
134
+ s.INDEX_COMMENT as index_comment
135
+ FROM INFORMATION_SCHEMA.STATISTICS s
136
+ WHERE s.TABLE_SCHEMA = ? AND s.TABLE_NAME = ? AND s.INDEX_NAME = ?
137
+ ORDER BY s.SEQ_IN_INDEX
138
+ `;
139
+ const results = await this.db.query(query, [database, table_name, index_name]);
140
+ if (results.length === 0) {
141
+ return {
142
+ status: 'error',
143
+ error: `Index '${index_name}' not found on table '${table_name}'`,
144
+ queryLog: this.db.getFormattedQueryLogs(1)
145
+ };
146
+ }
147
+ // Compile index info
148
+ const firstRow = results[0];
149
+ const indexInfo = {
150
+ index_name: firstRow.index_name,
151
+ table_name: firstRow.table_name,
152
+ is_unique: !firstRow.non_unique,
153
+ is_primary: firstRow.index_name === 'PRIMARY',
154
+ index_type: firstRow.index_type,
155
+ comment: firstRow.index_comment || null,
156
+ columns: results.map(r => ({
157
+ column_name: r.column_name,
158
+ seq_in_index: r.seq_in_index,
159
+ collation: r.collation,
160
+ cardinality: r.cardinality,
161
+ sub_part: r.sub_part,
162
+ nullable: r.nullable === 'YES'
163
+ }))
164
+ };
165
+ return {
166
+ status: 'success',
167
+ data: indexInfo,
168
+ queryLog: this.db.getFormattedQueryLogs(1)
169
+ };
170
+ }
171
+ catch (error) {
172
+ return {
173
+ status: 'error',
174
+ error: error.message,
175
+ queryLog: this.db.getFormattedQueryLogs(1)
176
+ };
177
+ }
178
+ }
179
+ /**
180
+ * Create a new index
181
+ */
182
+ async createIndex(params) {
183
+ try {
184
+ const dbValidation = this.validateDatabaseAccess(params?.database);
185
+ if (!dbValidation.valid) {
186
+ return { status: 'error', error: dbValidation.error };
187
+ }
188
+ const { table_name, index_name, columns, unique = false, index_type, comment } = params;
189
+ const database = dbValidation.database;
190
+ // Validate names
191
+ if (!this.security.validateIdentifier(table_name).valid) {
192
+ return { status: 'error', error: 'Invalid table name' };
193
+ }
194
+ if (!this.security.validateIdentifier(index_name).valid) {
195
+ return { status: 'error', error: 'Invalid index name' };
196
+ }
197
+ // Build column list
198
+ const columnList = columns.map(col => {
199
+ if (typeof col === 'string') {
200
+ if (!this.security.validateIdentifier(col).valid) {
201
+ throw new Error(`Invalid column name: ${col}`);
202
+ }
203
+ return `\`${col}\``;
204
+ }
205
+ else {
206
+ if (!this.security.validateIdentifier(col.column).valid) {
207
+ throw new Error(`Invalid column name: ${col.column}`);
208
+ }
209
+ let colDef = `\`${col.column}\``;
210
+ if (col.length) {
211
+ colDef += `(${col.length})`;
212
+ }
213
+ if (col.order) {
214
+ colDef += ` ${col.order}`;
215
+ }
216
+ return colDef;
217
+ }
218
+ }).join(', ');
219
+ // Build CREATE INDEX statement
220
+ let createQuery = 'CREATE ';
221
+ if (index_type === 'FULLTEXT') {
222
+ createQuery += 'FULLTEXT ';
223
+ }
224
+ else if (index_type === 'SPATIAL') {
225
+ createQuery += 'SPATIAL ';
226
+ }
227
+ else if (unique) {
228
+ createQuery += 'UNIQUE ';
229
+ }
230
+ createQuery += `INDEX \`${index_name}\` ON \`${database}\`.\`${table_name}\` (${columnList})`;
231
+ if (index_type && index_type !== 'FULLTEXT' && index_type !== 'SPATIAL') {
232
+ createQuery += ` USING ${index_type}`;
233
+ }
234
+ if (comment) {
235
+ createQuery += ` COMMENT '${comment.replace(/'/g, "''")}'`;
236
+ }
237
+ await this.db.query(createQuery);
238
+ return {
239
+ status: 'success',
240
+ data: {
241
+ message: `Index '${index_name}' created successfully on table '${table_name}'`,
242
+ index_name,
243
+ table_name,
244
+ database
245
+ },
246
+ queryLog: this.db.getFormattedQueryLogs(1)
247
+ };
248
+ }
249
+ catch (error) {
250
+ return {
251
+ status: 'error',
252
+ error: error.message,
253
+ queryLog: this.db.getFormattedQueryLogs(1)
254
+ };
255
+ }
256
+ }
257
+ /**
258
+ * Drop an index
259
+ */
260
+ async dropIndex(params) {
261
+ try {
262
+ const dbValidation = this.validateDatabaseAccess(params?.database);
263
+ if (!dbValidation.valid) {
264
+ return { status: 'error', error: dbValidation.error };
265
+ }
266
+ const { table_name, index_name } = params;
267
+ const database = dbValidation.database;
268
+ // Validate names
269
+ if (!this.security.validateIdentifier(table_name).valid) {
270
+ return { status: 'error', error: 'Invalid table name' };
271
+ }
272
+ if (!this.security.validateIdentifier(index_name).valid) {
273
+ return { status: 'error', error: 'Invalid index name' };
274
+ }
275
+ // Cannot drop PRIMARY KEY with DROP INDEX
276
+ if (index_name.toUpperCase() === 'PRIMARY') {
277
+ return { status: 'error', error: 'Cannot drop PRIMARY KEY using drop_index. Use ALTER TABLE DROP PRIMARY KEY instead.' };
278
+ }
279
+ const dropQuery = `DROP INDEX \`${index_name}\` ON \`${database}\`.\`${table_name}\``;
280
+ await this.db.query(dropQuery);
281
+ return {
282
+ status: 'success',
283
+ message: `Index '${index_name}' dropped successfully from table '${table_name}'`,
284
+ queryLog: this.db.getFormattedQueryLogs(1)
285
+ };
286
+ }
287
+ catch (error) {
288
+ return {
289
+ status: 'error',
290
+ error: error.message,
291
+ queryLog: this.db.getFormattedQueryLogs(1)
292
+ };
293
+ }
294
+ }
295
+ /**
296
+ * Analyze index usage and statistics
297
+ */
298
+ async analyzeIndex(params) {
299
+ try {
300
+ const dbValidation = this.validateDatabaseAccess(params?.database);
301
+ if (!dbValidation.valid) {
302
+ return { status: 'error', error: dbValidation.error };
303
+ }
304
+ const { table_name } = params;
305
+ const database = dbValidation.database;
306
+ // Validate table name
307
+ if (!this.security.validateIdentifier(table_name).valid) {
308
+ return { status: 'error', error: 'Invalid table name' };
309
+ }
310
+ // Run ANALYZE TABLE to update index statistics
311
+ const analyzeQuery = `ANALYZE TABLE \`${database}\`.\`${table_name}\``;
312
+ const analyzeResult = await this.db.query(analyzeQuery);
313
+ // Get updated index statistics
314
+ const statsQuery = `
315
+ SELECT
316
+ INDEX_NAME as index_name,
317
+ COLUMN_NAME as column_name,
318
+ SEQ_IN_INDEX as seq_in_index,
319
+ CARDINALITY as cardinality,
320
+ INDEX_TYPE as index_type,
321
+ NULLABLE as nullable
322
+ FROM INFORMATION_SCHEMA.STATISTICS
323
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
324
+ ORDER BY INDEX_NAME, SEQ_IN_INDEX
325
+ `;
326
+ const stats = await this.db.query(statsQuery, [database, table_name]);
327
+ return {
328
+ status: 'success',
329
+ data: {
330
+ analyze_result: analyzeResult[0],
331
+ index_statistics: stats
332
+ },
333
+ queryLog: this.db.getFormattedQueryLogs(2)
334
+ };
335
+ }
336
+ catch (error) {
337
+ return {
338
+ status: 'error',
339
+ error: error.message,
340
+ queryLog: this.db.getFormattedQueryLogs(2)
341
+ };
342
+ }
343
+ }
344
+ }
345
+ exports.IndexTools = IndexTools;
@@ -0,0 +1,111 @@
1
+ import { SecurityLayer } from '../security/securityLayer';
2
+ export declare class MaintenanceTools {
3
+ private db;
4
+ private security;
5
+ constructor(security: SecurityLayer);
6
+ /**
7
+ * Validate database access - ensures only the connected database can be accessed
8
+ */
9
+ private validateDatabaseAccess;
10
+ /**
11
+ * Analyze table to update index statistics
12
+ */
13
+ analyzeTable(params: {
14
+ table_name: string;
15
+ database?: string;
16
+ }): Promise<{
17
+ status: string;
18
+ data?: any;
19
+ error?: string;
20
+ queryLog?: string;
21
+ }>;
22
+ /**
23
+ * Optimize table to reclaim unused space and defragment
24
+ */
25
+ optimizeTable(params: {
26
+ table_name: string;
27
+ database?: string;
28
+ }): Promise<{
29
+ status: string;
30
+ data?: any;
31
+ error?: string;
32
+ queryLog?: string;
33
+ }>;
34
+ /**
35
+ * Check table for errors
36
+ */
37
+ checkTable(params: {
38
+ table_name: string;
39
+ check_type?: 'QUICK' | 'FAST' | 'MEDIUM' | 'EXTENDED' | 'CHANGED';
40
+ database?: string;
41
+ }): Promise<{
42
+ status: string;
43
+ data?: any;
44
+ error?: string;
45
+ queryLog?: string;
46
+ }>;
47
+ /**
48
+ * Repair table (MyISAM, ARCHIVE, CSV only)
49
+ */
50
+ repairTable(params: {
51
+ table_name: string;
52
+ quick?: boolean;
53
+ extended?: boolean;
54
+ use_frm?: boolean;
55
+ database?: string;
56
+ }): Promise<{
57
+ status: string;
58
+ data?: any;
59
+ error?: string;
60
+ queryLog?: string;
61
+ }>;
62
+ /**
63
+ * Truncate table (remove all rows quickly)
64
+ */
65
+ truncateTable(params: {
66
+ table_name: string;
67
+ database?: string;
68
+ }): Promise<{
69
+ status: string;
70
+ message?: string;
71
+ error?: string;
72
+ queryLog?: string;
73
+ }>;
74
+ /**
75
+ * Get table status and statistics
76
+ */
77
+ getTableStatus(params: {
78
+ table_name?: string;
79
+ database?: string;
80
+ }): Promise<{
81
+ status: string;
82
+ data?: any;
83
+ error?: string;
84
+ queryLog?: string;
85
+ }>;
86
+ /**
87
+ * Flush table (close and reopen)
88
+ */
89
+ flushTable(params: {
90
+ table_name?: string;
91
+ with_read_lock?: boolean;
92
+ database?: string;
93
+ }): Promise<{
94
+ status: string;
95
+ message?: string;
96
+ error?: string;
97
+ queryLog?: string;
98
+ }>;
99
+ /**
100
+ * Get table size information
101
+ */
102
+ getTableSize(params: {
103
+ table_name?: string;
104
+ database?: string;
105
+ }): Promise<{
106
+ status: string;
107
+ data?: any;
108
+ error?: string;
109
+ queryLog?: string;
110
+ }>;
111
+ }