@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.
- package/CHANGELOG.md +19 -0
- package/DOCUMENTATIONS.md +495 -17
- package/README.md +102 -5
- package/dist/index.d.ts +401 -0
- package/dist/index.js +311 -0
- package/dist/mcp-server.js +1067 -0
- package/dist/tools/constraintTools.d.ts +108 -0
- package/dist/tools/constraintTools.js +405 -0
- package/dist/tools/functionTools.d.ts +93 -0
- package/dist/tools/functionTools.js +351 -0
- package/dist/tools/indexTools.d.ts +81 -0
- package/dist/tools/indexTools.js +345 -0
- package/dist/tools/maintenanceTools.d.ts +111 -0
- package/dist/tools/maintenanceTools.js +371 -0
- package/dist/tools/processTools.d.ts +106 -0
- package/dist/tools/processTools.js +305 -0
- package/dist/tools/triggerTools.d.ts +76 -0
- package/dist/tools/triggerTools.js +294 -0
- package/dist/tools/viewTools.d.ts +91 -0
- package/dist/tools/viewTools.js +330 -0
- package/manifest.json +290 -248
- package/package.json +1 -1
|
@@ -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
|
+
}
|