@berthojoris/mcp-mysql-server 1.10.4 → 1.12.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 +48 -12
- package/DOCUMENTATIONS.md +55 -27
- package/README.md +79 -8
- package/bin/mcp-mysql.js +107 -39
- package/dist/config/featureConfig.d.ts +41 -3
- package/dist/config/featureConfig.js +188 -13
- package/dist/index.d.ts +70 -1
- package/dist/index.js +44 -2
- package/dist/mcp-server.js +101 -11
- package/dist/tools/analysisTools.d.ts +35 -0
- package/dist/tools/analysisTools.js +327 -0
- package/dist/tools/databaseTools.d.ts +21 -0
- package/dist/tools/databaseTools.js +138 -0
- package/dist/tools/queryTools.d.ts +5 -0
- package/dist/tools/queryTools.js +27 -0
- package/manifest.json +22 -1
- package/package.json +89 -89
|
@@ -0,0 +1,327 @@
|
|
|
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.AnalysisTools = void 0;
|
|
7
|
+
const connection_1 = __importDefault(require("../db/connection"));
|
|
8
|
+
const config_1 = require("../config/config");
|
|
9
|
+
class AnalysisTools {
|
|
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
|
+
* Get statistics for a specific column
|
|
46
|
+
*/
|
|
47
|
+
async getColumnStatistics(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, column_name } = params;
|
|
54
|
+
const database = dbValidation.database;
|
|
55
|
+
// Validate names
|
|
56
|
+
if (!this.security.validateIdentifier(table_name).valid) {
|
|
57
|
+
return { status: "error", error: "Invalid table name" };
|
|
58
|
+
}
|
|
59
|
+
if (!this.security.validateIdentifier(column_name).valid) {
|
|
60
|
+
return { status: "error", error: "Invalid column name" };
|
|
61
|
+
}
|
|
62
|
+
// Check if column exists and get its type
|
|
63
|
+
const colCheckQuery = `
|
|
64
|
+
SELECT DATA_TYPE
|
|
65
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
66
|
+
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?
|
|
67
|
+
`;
|
|
68
|
+
const colCheck = await this.db.query(colCheckQuery, [
|
|
69
|
+
database,
|
|
70
|
+
table_name,
|
|
71
|
+
column_name,
|
|
72
|
+
]);
|
|
73
|
+
if (colCheck.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
status: "error",
|
|
76
|
+
error: `Column '${column_name}' not found in table '${table_name}'`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const dataType = colCheck[0].DATA_TYPE;
|
|
80
|
+
const isNumeric = [
|
|
81
|
+
"int",
|
|
82
|
+
"tinyint",
|
|
83
|
+
"smallint",
|
|
84
|
+
"mediumint",
|
|
85
|
+
"bigint",
|
|
86
|
+
"float",
|
|
87
|
+
"double",
|
|
88
|
+
"decimal",
|
|
89
|
+
].includes(dataType);
|
|
90
|
+
const isDate = [
|
|
91
|
+
"date",
|
|
92
|
+
"datetime",
|
|
93
|
+
"timestamp",
|
|
94
|
+
"time",
|
|
95
|
+
"year",
|
|
96
|
+
].includes(dataType);
|
|
97
|
+
// Build statistics query
|
|
98
|
+
let query = `
|
|
99
|
+
SELECT
|
|
100
|
+
COUNT(*) as total_rows,
|
|
101
|
+
COUNT(\`${column_name}\`) as non_null_count,
|
|
102
|
+
COUNT(DISTINCT \`${column_name}\`) as distinct_count,
|
|
103
|
+
SUM(CASE WHEN \`${column_name}\` IS NULL THEN 1 ELSE 0 END) as null_count
|
|
104
|
+
`;
|
|
105
|
+
if (isNumeric || isDate) {
|
|
106
|
+
query += `,
|
|
107
|
+
MIN(\`${column_name}\`) as min_value,
|
|
108
|
+
MAX(\`${column_name}\`) as max_value
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
if (isNumeric) {
|
|
112
|
+
query += `,
|
|
113
|
+
AVG(\`${column_name}\`) as avg_value
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
query += ` FROM \`${database}\`.\`${table_name}\``;
|
|
117
|
+
const statsResult = await this.db.query(query);
|
|
118
|
+
const stats = statsResult[0];
|
|
119
|
+
// Get top frequent values
|
|
120
|
+
const topValuesQuery = `
|
|
121
|
+
SELECT \`${column_name}\` as value, COUNT(*) as count
|
|
122
|
+
FROM \`${database}\`.\`${table_name}\`
|
|
123
|
+
GROUP BY \`${column_name}\`
|
|
124
|
+
ORDER BY count DESC
|
|
125
|
+
LIMIT 5
|
|
126
|
+
`;
|
|
127
|
+
const topValues = await this.db.query(topValuesQuery);
|
|
128
|
+
return {
|
|
129
|
+
status: "success",
|
|
130
|
+
data: {
|
|
131
|
+
column_name,
|
|
132
|
+
data_type: dataType,
|
|
133
|
+
statistics: {
|
|
134
|
+
total_rows: stats.total_rows,
|
|
135
|
+
non_null_count: stats.non_null_count,
|
|
136
|
+
null_count: stats.null_count,
|
|
137
|
+
distinct_count: stats.distinct_count,
|
|
138
|
+
unique_ratio: stats.total_rows > 0
|
|
139
|
+
? (stats.distinct_count / stats.total_rows).toFixed(4)
|
|
140
|
+
: 0,
|
|
141
|
+
...(isNumeric || isDate
|
|
142
|
+
? { min_value: stats.min_value, max_value: stats.max_value }
|
|
143
|
+
: {}),
|
|
144
|
+
...(isNumeric ? { avg_value: stats.avg_value } : {}),
|
|
145
|
+
},
|
|
146
|
+
top_values: topValues,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
status: "error",
|
|
153
|
+
error: error.message,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build a compact, schema-aware context pack for RAG (tables, PK/FK, columns, row estimates)
|
|
159
|
+
*/
|
|
160
|
+
async getSchemaRagContext(params = {}) {
|
|
161
|
+
try {
|
|
162
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
163
|
+
if (!dbValidation.valid) {
|
|
164
|
+
return { status: "error", error: dbValidation.error };
|
|
165
|
+
}
|
|
166
|
+
const database = dbValidation.database;
|
|
167
|
+
const maxTables = Math.min(Math.max(params.max_tables ?? 50, 1), 200);
|
|
168
|
+
const maxColumns = Math.min(Math.max(params.max_columns ?? 12, 1), 200);
|
|
169
|
+
const includeRelationships = params.include_relationships ?? true;
|
|
170
|
+
// Count total tables for truncation note
|
|
171
|
+
const totalTablesResult = await this.db.query(`SELECT COUNT(*) as total FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?`, [database]);
|
|
172
|
+
const totalTables = totalTablesResult[0]?.total ?? 0;
|
|
173
|
+
// Fetch tables limited for context pack
|
|
174
|
+
const tables = await this.db.query(`
|
|
175
|
+
SELECT TABLE_NAME, TABLE_ROWS
|
|
176
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
177
|
+
WHERE TABLE_SCHEMA = ?
|
|
178
|
+
ORDER BY TABLE_NAME
|
|
179
|
+
LIMIT ?
|
|
180
|
+
`, [database, maxTables]);
|
|
181
|
+
if (!tables.length) {
|
|
182
|
+
return {
|
|
183
|
+
status: "success",
|
|
184
|
+
data: {
|
|
185
|
+
database,
|
|
186
|
+
total_tables: 0,
|
|
187
|
+
tables: [],
|
|
188
|
+
relationships: [],
|
|
189
|
+
context_text: `Schema-Aware RAG Context Pack (${database}): no tables found.`,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const tableNames = tables.map((t) => t.TABLE_NAME);
|
|
194
|
+
const placeholders = tableNames.map(() => "?").join(",");
|
|
195
|
+
const columnParams = [database, ...tableNames];
|
|
196
|
+
const columns = await this.db.query(`
|
|
197
|
+
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY, IS_NULLABLE
|
|
198
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
199
|
+
WHERE TABLE_SCHEMA = ?
|
|
200
|
+
AND TABLE_NAME IN (${placeholders})
|
|
201
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
|
202
|
+
`, columnParams);
|
|
203
|
+
let foreignKeys = [];
|
|
204
|
+
if (includeRelationships) {
|
|
205
|
+
const fkParams = [database, ...tableNames, ...tableNames];
|
|
206
|
+
foreignKeys = await this.db.query(`
|
|
207
|
+
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
|
208
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
209
|
+
WHERE TABLE_SCHEMA = ?
|
|
210
|
+
AND TABLE_NAME IN (${placeholders})
|
|
211
|
+
AND REFERENCED_TABLE_NAME IN (${placeholders})
|
|
212
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
213
|
+
`, fkParams);
|
|
214
|
+
}
|
|
215
|
+
const fkLookup = new Map();
|
|
216
|
+
foreignKeys.forEach((fk) => {
|
|
217
|
+
fkLookup.set(`${fk.TABLE_NAME}.${fk.COLUMN_NAME}`, {
|
|
218
|
+
table: fk.REFERENCED_TABLE_NAME,
|
|
219
|
+
column: fk.REFERENCED_COLUMN_NAME,
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
const tableEntries = tables.map((table) => {
|
|
223
|
+
const tableColumns = columns.filter((c) => c.TABLE_NAME === table.TABLE_NAME);
|
|
224
|
+
const truncatedColumns = tableColumns.length > maxColumns ? tableColumns.length - maxColumns : 0;
|
|
225
|
+
const columnsForContext = tableColumns.slice(0, maxColumns).map((col) => {
|
|
226
|
+
const key = col.COLUMN_KEY === "PRI"
|
|
227
|
+
? "PK"
|
|
228
|
+
: col.COLUMN_KEY === "UNI"
|
|
229
|
+
? "UNI"
|
|
230
|
+
: undefined;
|
|
231
|
+
const fkRef = fkLookup.get(`${col.TABLE_NAME}.${col.COLUMN_NAME}`);
|
|
232
|
+
return {
|
|
233
|
+
name: col.COLUMN_NAME,
|
|
234
|
+
data_type: col.DATA_TYPE,
|
|
235
|
+
nullable: col.IS_NULLABLE === "YES",
|
|
236
|
+
key: fkRef ? "FK" : key,
|
|
237
|
+
references: fkRef
|
|
238
|
+
? `${fkRef.table}.${fkRef.column}`
|
|
239
|
+
: undefined,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
const primaryKeys = tableColumns
|
|
243
|
+
.filter((col) => col.COLUMN_KEY === "PRI")
|
|
244
|
+
.map((col) => col.COLUMN_NAME);
|
|
245
|
+
const foreignKeyList = foreignKeys
|
|
246
|
+
.filter((fk) => fk.TABLE_NAME === table.TABLE_NAME)
|
|
247
|
+
.map((fk) => ({
|
|
248
|
+
column: fk.COLUMN_NAME,
|
|
249
|
+
references: `${fk.REFERENCED_TABLE_NAME}.${fk.REFERENCED_COLUMN_NAME}`,
|
|
250
|
+
}));
|
|
251
|
+
return {
|
|
252
|
+
table_name: table.TABLE_NAME,
|
|
253
|
+
row_estimate: typeof table.TABLE_ROWS === "number"
|
|
254
|
+
? table.TABLE_ROWS
|
|
255
|
+
: parseInt(table.TABLE_ROWS || "0", 10) || 0,
|
|
256
|
+
primary_keys: primaryKeys,
|
|
257
|
+
columns: columnsForContext,
|
|
258
|
+
foreign_keys: foreignKeyList,
|
|
259
|
+
truncated_columns: truncatedColumns > 0 ? truncatedColumns : 0,
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
const relationships = foreignKeys.map((fk) => ({
|
|
263
|
+
from_table: fk.TABLE_NAME,
|
|
264
|
+
from_column: fk.COLUMN_NAME,
|
|
265
|
+
to_table: fk.REFERENCED_TABLE_NAME,
|
|
266
|
+
to_column: fk.REFERENCED_COLUMN_NAME,
|
|
267
|
+
}));
|
|
268
|
+
const lines = [];
|
|
269
|
+
lines.push(`Schema-Aware RAG Context Pack (${database})`);
|
|
270
|
+
lines.push(`Tables shown: ${tableEntries.length}/${totalTables || tableEntries.length} (rows are approximate)`);
|
|
271
|
+
lines.push(`Per-table column limit: ${maxColumns}${tableEntries.some((t) => t.truncated_columns > 0)
|
|
272
|
+
? " (additional columns truncated)"
|
|
273
|
+
: ""}`);
|
|
274
|
+
lines.push("");
|
|
275
|
+
tableEntries.forEach((t) => {
|
|
276
|
+
const approxRows = typeof t.row_estimate === "number" && t.row_estimate >= 0
|
|
277
|
+
? `~${t.row_estimate}`
|
|
278
|
+
: "~0";
|
|
279
|
+
const columnSnippets = t.columns.map((c) => {
|
|
280
|
+
const tags = [];
|
|
281
|
+
if (c.key)
|
|
282
|
+
tags.push(c.key);
|
|
283
|
+
if (c.references)
|
|
284
|
+
tags.push(`-> ${c.references}`);
|
|
285
|
+
const nullability = c.nullable ? "null" : "not null";
|
|
286
|
+
return `${c.name} ${c.data_type} (${nullability})${tags.length ? ` [${tags.join(", ")}]` : ""}`;
|
|
287
|
+
});
|
|
288
|
+
lines.push(`- ${t.table_name} (${approxRows} rows) PK: ${t.primary_keys.length ? t.primary_keys.join(", ") : "none"}`);
|
|
289
|
+
lines.push(` Columns: ${columnSnippets.join("; ")}`);
|
|
290
|
+
if (t.truncated_columns) {
|
|
291
|
+
lines.push(` ...and ${t.truncated_columns} more columns not shown`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
if (includeRelationships && relationships.length) {
|
|
295
|
+
lines.push("");
|
|
296
|
+
lines.push("Relationships:");
|
|
297
|
+
relationships.forEach((rel) => {
|
|
298
|
+
lines.push(`- ${rel.from_table}.${rel.from_column} -> ${rel.to_table}.${rel.to_column}`);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (totalTables > tableEntries.length) {
|
|
302
|
+
lines.push(`\nNote: ${totalTables - tableEntries.length} table(s) omitted (max_tables=${maxTables}).`);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
status: "success",
|
|
306
|
+
data: {
|
|
307
|
+
database,
|
|
308
|
+
total_tables: totalTables,
|
|
309
|
+
tables: tableEntries,
|
|
310
|
+
relationships: includeRelationships ? relationships : [],
|
|
311
|
+
context_text: lines.join("\n"),
|
|
312
|
+
limits: {
|
|
313
|
+
max_tables: maxTables,
|
|
314
|
+
max_columns: maxColumns,
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
return {
|
|
321
|
+
status: "error",
|
|
322
|
+
error: error.message,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
exports.AnalysisTools = AnalysisTools;
|
|
@@ -31,4 +31,25 @@ export declare class DatabaseTools {
|
|
|
31
31
|
data?: ColumnInfo[];
|
|
32
32
|
error?: string;
|
|
33
33
|
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Get a high-level summary of the database (tables, columns, row counts)
|
|
36
|
+
* Optimized for AI context window
|
|
37
|
+
*/
|
|
38
|
+
getDatabaseSummary(params: {
|
|
39
|
+
database?: string;
|
|
40
|
+
}): Promise<{
|
|
41
|
+
status: string;
|
|
42
|
+
data?: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Get a Mermaid.js ER diagram for the database schema
|
|
47
|
+
*/
|
|
48
|
+
getSchemaERD(params: {
|
|
49
|
+
database?: string;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
status: string;
|
|
52
|
+
data?: string;
|
|
53
|
+
error?: string;
|
|
54
|
+
}>;
|
|
34
55
|
}
|
|
@@ -139,5 +139,143 @@ class DatabaseTools {
|
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Get a high-level summary of the database (tables, columns, row counts)
|
|
144
|
+
* Optimized for AI context window
|
|
145
|
+
*/
|
|
146
|
+
async getDatabaseSummary(params) {
|
|
147
|
+
try {
|
|
148
|
+
// Security validation
|
|
149
|
+
if (params.database && params.database !== config_1.dbConfig.database) {
|
|
150
|
+
return {
|
|
151
|
+
status: "error",
|
|
152
|
+
error: `Access denied. You can only access the connected database '${config_1.dbConfig.database}'.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const database = params.database || config_1.dbConfig.database;
|
|
156
|
+
if (!database) {
|
|
157
|
+
return {
|
|
158
|
+
status: "error",
|
|
159
|
+
error: "No database specified and none connected.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Get tables and row counts
|
|
163
|
+
const tablesQuery = `
|
|
164
|
+
SELECT TABLE_NAME, TABLE_ROWS
|
|
165
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
166
|
+
WHERE TABLE_SCHEMA = ?
|
|
167
|
+
`;
|
|
168
|
+
const tables = await this.db.query(tablesQuery, [database]);
|
|
169
|
+
// Get columns for all tables
|
|
170
|
+
const columnsQuery = `
|
|
171
|
+
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY
|
|
172
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
173
|
+
WHERE TABLE_SCHEMA = ?
|
|
174
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
|
175
|
+
`;
|
|
176
|
+
const columns = await this.db.query(columnsQuery, [database]);
|
|
177
|
+
// Build text summary
|
|
178
|
+
let summary = `# Database Summary: ${database}\n\n`;
|
|
179
|
+
for (const table of tables) {
|
|
180
|
+
summary += `## Table: ${table.TABLE_NAME} (~${table.TABLE_ROWS || 0} rows)\n`;
|
|
181
|
+
const tableColumns = columns.filter(c => c.TABLE_NAME === table.TABLE_NAME);
|
|
182
|
+
const columnDefs = tableColumns.map(c => {
|
|
183
|
+
let def = `${c.COLUMN_NAME} (${c.DATA_TYPE})`;
|
|
184
|
+
if (c.COLUMN_KEY === 'PRI')
|
|
185
|
+
def += " [PK]";
|
|
186
|
+
if (c.COLUMN_KEY === 'MUL')
|
|
187
|
+
def += " [FK/Index]";
|
|
188
|
+
if (c.COLUMN_KEY === 'UNI')
|
|
189
|
+
def += " [Unique]";
|
|
190
|
+
return def;
|
|
191
|
+
});
|
|
192
|
+
summary += `Columns: ${columnDefs.join(", ")}\n\n`;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
status: "success",
|
|
196
|
+
data: summary
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return {
|
|
201
|
+
status: "error",
|
|
202
|
+
error: error.message
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get a Mermaid.js ER diagram for the database schema
|
|
208
|
+
*/
|
|
209
|
+
async getSchemaERD(params) {
|
|
210
|
+
try {
|
|
211
|
+
// Security validation
|
|
212
|
+
if (params.database && params.database !== config_1.dbConfig.database) {
|
|
213
|
+
return {
|
|
214
|
+
status: "error",
|
|
215
|
+
error: `Access denied. You can only access the connected database '${config_1.dbConfig.database}'.`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const database = params.database || config_1.dbConfig.database;
|
|
219
|
+
if (!database) {
|
|
220
|
+
return {
|
|
221
|
+
status: "error",
|
|
222
|
+
error: "No database specified and none connected.",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// Get tables and columns
|
|
226
|
+
const columnsQuery = `
|
|
227
|
+
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY
|
|
228
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
229
|
+
WHERE TABLE_SCHEMA = ?
|
|
230
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
|
231
|
+
`;
|
|
232
|
+
const columns = await this.db.query(columnsQuery, [database]);
|
|
233
|
+
// Get foreign keys
|
|
234
|
+
const fkQuery = `
|
|
235
|
+
SELECT
|
|
236
|
+
TABLE_NAME, COLUMN_NAME,
|
|
237
|
+
REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME
|
|
238
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
239
|
+
WHERE TABLE_SCHEMA = ?
|
|
240
|
+
AND REFERENCED_TABLE_SCHEMA = ?
|
|
241
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
242
|
+
`;
|
|
243
|
+
const fks = await this.db.query(fkQuery, [database, database]);
|
|
244
|
+
// Build Mermaid ER diagram
|
|
245
|
+
let mermaid = "erDiagram\n";
|
|
246
|
+
// Add entities (tables)
|
|
247
|
+
const tables = [...new Set(columns.map(c => c.TABLE_NAME))];
|
|
248
|
+
for (const table of tables) {
|
|
249
|
+
mermaid += ` ${table} {\n`;
|
|
250
|
+
const tableColumns = columns.filter(c => c.TABLE_NAME === table);
|
|
251
|
+
for (const col of tableColumns) {
|
|
252
|
+
let type = col.DATA_TYPE;
|
|
253
|
+
let key = "";
|
|
254
|
+
if (col.COLUMN_KEY === 'PRI')
|
|
255
|
+
key = "PK";
|
|
256
|
+
else if (col.COLUMN_KEY === 'MUL')
|
|
257
|
+
key = "FK";
|
|
258
|
+
mermaid += ` ${type} ${col.COLUMN_NAME} ${key}\n`;
|
|
259
|
+
}
|
|
260
|
+
mermaid += ` }\n`;
|
|
261
|
+
}
|
|
262
|
+
// Add relationships
|
|
263
|
+
for (const fk of fks) {
|
|
264
|
+
// Relationship logic: referenced_table ||--o{ table
|
|
265
|
+
// This is a simplification, usually FK implies 1 to Many
|
|
266
|
+
mermaid += ` ${fk.REFERENCED_TABLE_NAME} ||--o{ ${fk.TABLE_NAME} : "${fk.COLUMN_NAME}"\n`;
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
status: "success",
|
|
270
|
+
data: mermaid
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
status: "error",
|
|
276
|
+
error: error.message
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
142
280
|
}
|
|
143
281
|
exports.DatabaseTools = DatabaseTools;
|
|
@@ -13,11 +13,16 @@ export declare class QueryTools {
|
|
|
13
13
|
params?: any[];
|
|
14
14
|
hints?: QueryHints;
|
|
15
15
|
useCache?: boolean;
|
|
16
|
+
dry_run?: boolean;
|
|
16
17
|
}): Promise<{
|
|
17
18
|
status: string;
|
|
18
19
|
data?: any[];
|
|
19
20
|
error?: string;
|
|
20
21
|
optimizedQuery?: string;
|
|
22
|
+
dry_run?: boolean;
|
|
23
|
+
execution_plan?: any;
|
|
24
|
+
estimated_cost?: string;
|
|
25
|
+
message?: string;
|
|
21
26
|
}>;
|
|
22
27
|
/**
|
|
23
28
|
* Analyze a query and get optimization suggestions
|
package/dist/tools/queryTools.js
CHANGED
|
@@ -61,6 +61,33 @@ class QueryTools {
|
|
|
61
61
|
optimizedQuery = finalQuery;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
// Handle dry_run: return query plan and cost estimate without executing
|
|
65
|
+
if (queryParams.dry_run) {
|
|
66
|
+
const explainQuery = `EXPLAIN FORMAT=JSON ${finalQuery}`;
|
|
67
|
+
const explainResult = await this.db.query(explainQuery, paramValidation.sanitizedParams);
|
|
68
|
+
// Try to get cost from JSON format
|
|
69
|
+
let estimatedCost = "Unknown";
|
|
70
|
+
let executionPlan = explainResult;
|
|
71
|
+
if (explainResult[0] && explainResult[0].EXPLAIN) {
|
|
72
|
+
try {
|
|
73
|
+
const explainJson = JSON.parse(explainResult[0].EXPLAIN);
|
|
74
|
+
estimatedCost = explainJson.query_block?.cost_info?.query_cost || "Unknown";
|
|
75
|
+
executionPlan = explainJson;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
// Ignore parsing error
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
status: "success",
|
|
83
|
+
data: [],
|
|
84
|
+
optimizedQuery,
|
|
85
|
+
dry_run: true,
|
|
86
|
+
execution_plan: executionPlan,
|
|
87
|
+
estimated_cost: estimatedCost,
|
|
88
|
+
message: "Dry run completed. Query was not executed."
|
|
89
|
+
};
|
|
90
|
+
}
|
|
64
91
|
// Execute the query with sanitized parameters
|
|
65
92
|
const results = await this.db.query(finalQuery, paramValidation.sanitizedParams, useCache);
|
|
66
93
|
return {
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mysql-mcp",
|
|
3
3
|
"description": "A Model Context Protocol for MySQL database interaction",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.12.0",
|
|
5
5
|
"tools": [
|
|
6
6
|
{
|
|
7
7
|
"name": "list_databases",
|
|
@@ -244,6 +244,27 @@
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
},
|
|
247
|
+
{
|
|
248
|
+
"name": "get_schema_rag_context",
|
|
249
|
+
"description": "Returns a condensed schema-aware context pack with tables, PK/FK details, and row estimates for RAG prompts.",
|
|
250
|
+
"input_schema": {
|
|
251
|
+
"type": "object",
|
|
252
|
+
"properties": {
|
|
253
|
+
"database": { "type": "string", "description": "Optional specific database name" },
|
|
254
|
+
"max_tables": { "type": "number", "description": "Max tables to include (default 50, max 200)" },
|
|
255
|
+
"max_columns": { "type": "number", "description": "Max columns per table (default 12, max 200)" },
|
|
256
|
+
"include_relationships": { "type": "boolean", "description": "Whether to include FK relationships (default true)" }
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
"output_schema": {
|
|
260
|
+
"type": "object",
|
|
261
|
+
"properties": {
|
|
262
|
+
"database": { "type": "string" },
|
|
263
|
+
"total_tables": { "type": "number" },
|
|
264
|
+
"context_text": { "type": "string" }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
247
268
|
{
|
|
248
269
|
"name": "get_table_size",
|
|
249
270
|
"description": "Gets size information for one or all tables including data and index sizes.",
|