@berthojoris/mcp-mysql-server 1.42.1 → 1.43.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 +17 -0
- package/DOCUMENTATIONS.md +44 -9
- package/README.md +4 -4
- package/dist/config/featureConfig.js +14 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +27 -0
- package/dist/mcp-server.js +133 -5
- package/dist/security/securityLayer.js +9 -0
- package/dist/tools/analysisTools.d.ts +57 -0
- package/dist/tools/analysisTools.js +544 -13
- package/dist/tools/queryTools.d.ts +2 -1
- package/dist/tools/queryTools.js +2 -1
- package/dist/tools/toolArgumentValidation.js +113 -1
- package/dist/tools/utilityTools.js +11 -2
- package/manifest.json +154 -2
- package/package.json +1 -1
|
@@ -41,6 +41,64 @@ class AnalysisTools {
|
|
|
41
41
|
database: connectedDatabase,
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
clampNumber(value, fallback, min, max) {
|
|
45
|
+
const numeric = Number(value ?? fallback);
|
|
46
|
+
if (!Number.isFinite(numeric))
|
|
47
|
+
return fallback;
|
|
48
|
+
return Math.min(Math.max(Math.floor(numeric), min), max);
|
|
49
|
+
}
|
|
50
|
+
escapeLikePattern(value) {
|
|
51
|
+
return value.replace(/[\\%_]/g, (match) => `\\${match}`);
|
|
52
|
+
}
|
|
53
|
+
quoteIdentifier(identifier) {
|
|
54
|
+
return `\`${identifier.replace(/`/g, "``")}\``;
|
|
55
|
+
}
|
|
56
|
+
truncateValue(value, maxLength = 180) {
|
|
57
|
+
if (value === null || value === undefined)
|
|
58
|
+
return "";
|
|
59
|
+
const stringValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
60
|
+
if (stringValue.length <= maxLength)
|
|
61
|
+
return stringValue;
|
|
62
|
+
return `${stringValue.slice(0, maxLength)}...`;
|
|
63
|
+
}
|
|
64
|
+
getMatchScore(value, keyword, baseScore) {
|
|
65
|
+
if (value === null || value === undefined)
|
|
66
|
+
return 0;
|
|
67
|
+
const normalizedValue = String(value).toLowerCase();
|
|
68
|
+
const normalizedKeyword = keyword.toLowerCase();
|
|
69
|
+
if (!normalizedValue || !normalizedKeyword)
|
|
70
|
+
return 0;
|
|
71
|
+
if (normalizedValue === normalizedKeyword)
|
|
72
|
+
return baseScore;
|
|
73
|
+
if (normalizedValue.startsWith(normalizedKeyword))
|
|
74
|
+
return Math.max(baseScore - 10, 1);
|
|
75
|
+
if (normalizedValue.includes(normalizedKeyword))
|
|
76
|
+
return Math.max(baseScore - 20, 1);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
addMatchedField(match, field) {
|
|
80
|
+
const key = `${field.type}:${field.field}:${field.value || ""}`;
|
|
81
|
+
const exists = match.matched_fields.some((item) => `${item.type}:${item.field}:${item.value || ""}` === key);
|
|
82
|
+
if (!exists) {
|
|
83
|
+
match.matched_fields.push(field);
|
|
84
|
+
}
|
|
85
|
+
if (!match.matched_on.includes(field.type)) {
|
|
86
|
+
match.matched_on.push(field.type);
|
|
87
|
+
}
|
|
88
|
+
match.score = Math.max(match.score, field.score);
|
|
89
|
+
}
|
|
90
|
+
async getTablesByName(database, tableNames) {
|
|
91
|
+
if (!tableNames.length)
|
|
92
|
+
return [];
|
|
93
|
+
const placeholders = tableNames.map(() => "?").join(",");
|
|
94
|
+
return await this.db.query(`
|
|
95
|
+
SELECT TABLE_NAME, TABLE_ROWS, TABLE_COMMENT
|
|
96
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
97
|
+
WHERE TABLE_SCHEMA = ?
|
|
98
|
+
AND TABLE_NAME IN (${placeholders})
|
|
99
|
+
ORDER BY TABLE_NAME
|
|
100
|
+
`, [database, ...tableNames]);
|
|
101
|
+
}
|
|
44
102
|
/**
|
|
45
103
|
* Get statistics for a specific column
|
|
46
104
|
*/
|
|
@@ -154,6 +212,440 @@ class AnalysisTools {
|
|
|
154
212
|
};
|
|
155
213
|
}
|
|
156
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Find candidate tables by keyword across schema metadata.
|
|
217
|
+
*/
|
|
218
|
+
async findTablesByKeyword(params) {
|
|
219
|
+
try {
|
|
220
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
221
|
+
if (!dbValidation.valid) {
|
|
222
|
+
return { status: "error", error: dbValidation.error };
|
|
223
|
+
}
|
|
224
|
+
const keyword = params.keyword?.trim();
|
|
225
|
+
if (!keyword) {
|
|
226
|
+
return { status: "error", error: "keyword is required" };
|
|
227
|
+
}
|
|
228
|
+
const searchIn = params.search_in || "all";
|
|
229
|
+
if (!["table_names", "column_names", "comments", "all"].includes(searchIn)) {
|
|
230
|
+
return {
|
|
231
|
+
status: "error",
|
|
232
|
+
error: "search_in must be one of table_names, column_names, comments, all",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const limit = this.clampNumber(params.limit, 20, 1, 100);
|
|
236
|
+
const database = dbValidation.database;
|
|
237
|
+
const likePattern = `%${this.escapeLikePattern(keyword)}%`;
|
|
238
|
+
const conditions = [];
|
|
239
|
+
const queryParams = [database];
|
|
240
|
+
if (searchIn === "table_names" || searchIn === "all") {
|
|
241
|
+
conditions.push("t.TABLE_NAME LIKE ? ESCAPE '\\\\'");
|
|
242
|
+
queryParams.push(likePattern);
|
|
243
|
+
}
|
|
244
|
+
if (searchIn === "column_names" || searchIn === "all") {
|
|
245
|
+
conditions.push("c.COLUMN_NAME LIKE ? ESCAPE '\\\\'");
|
|
246
|
+
queryParams.push(likePattern);
|
|
247
|
+
}
|
|
248
|
+
if (searchIn === "comments" || searchIn === "all") {
|
|
249
|
+
conditions.push("(t.TABLE_COMMENT LIKE ? ESCAPE '\\\\' OR c.COLUMN_COMMENT LIKE ? ESCAPE '\\\\')");
|
|
250
|
+
queryParams.push(likePattern, likePattern);
|
|
251
|
+
}
|
|
252
|
+
const rows = await this.db.query(`
|
|
253
|
+
SELECT
|
|
254
|
+
t.TABLE_NAME,
|
|
255
|
+
t.TABLE_ROWS,
|
|
256
|
+
t.TABLE_COMMENT,
|
|
257
|
+
c.COLUMN_NAME,
|
|
258
|
+
c.COLUMN_COMMENT,
|
|
259
|
+
c.ORDINAL_POSITION
|
|
260
|
+
FROM INFORMATION_SCHEMA.TABLES t
|
|
261
|
+
LEFT JOIN INFORMATION_SCHEMA.COLUMNS c
|
|
262
|
+
ON c.TABLE_SCHEMA = t.TABLE_SCHEMA
|
|
263
|
+
AND c.TABLE_NAME = t.TABLE_NAME
|
|
264
|
+
WHERE t.TABLE_SCHEMA = ?
|
|
265
|
+
AND (${conditions.join(" OR ")})
|
|
266
|
+
ORDER BY t.TABLE_NAME, c.ORDINAL_POSITION
|
|
267
|
+
`, queryParams);
|
|
268
|
+
const matchMap = new Map();
|
|
269
|
+
const allowsTable = searchIn === "table_names" || searchIn === "all";
|
|
270
|
+
const allowsColumn = searchIn === "column_names" || searchIn === "all";
|
|
271
|
+
const allowsComments = searchIn === "comments" || searchIn === "all";
|
|
272
|
+
for (const row of rows) {
|
|
273
|
+
if (!matchMap.has(row.TABLE_NAME)) {
|
|
274
|
+
matchMap.set(row.TABLE_NAME, {
|
|
275
|
+
table_name: row.TABLE_NAME,
|
|
276
|
+
row_estimate: typeof row.TABLE_ROWS === "number"
|
|
277
|
+
? row.TABLE_ROWS
|
|
278
|
+
: parseInt(row.TABLE_ROWS || "0", 10) || 0,
|
|
279
|
+
score: 0,
|
|
280
|
+
matched_on: [],
|
|
281
|
+
matched_fields: [],
|
|
282
|
+
column_names: [],
|
|
283
|
+
table_comment: row.TABLE_COMMENT || undefined,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const match = matchMap.get(row.TABLE_NAME);
|
|
287
|
+
if (row.COLUMN_NAME && !match.column_names.includes(row.COLUMN_NAME)) {
|
|
288
|
+
match.column_names.push(row.COLUMN_NAME);
|
|
289
|
+
}
|
|
290
|
+
if (allowsTable) {
|
|
291
|
+
const score = this.getMatchScore(row.TABLE_NAME, keyword, 100);
|
|
292
|
+
if (score > 0) {
|
|
293
|
+
this.addMatchedField(match, {
|
|
294
|
+
type: "table_name",
|
|
295
|
+
field: row.TABLE_NAME,
|
|
296
|
+
score,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (allowsColumn && row.COLUMN_NAME) {
|
|
301
|
+
const score = this.getMatchScore(row.COLUMN_NAME, keyword, 90);
|
|
302
|
+
if (score > 0) {
|
|
303
|
+
this.addMatchedField(match, {
|
|
304
|
+
type: "column_name",
|
|
305
|
+
field: row.COLUMN_NAME,
|
|
306
|
+
score,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (allowsComments) {
|
|
311
|
+
const tableCommentScore = this.getMatchScore(row.TABLE_COMMENT, keyword, 75);
|
|
312
|
+
if (tableCommentScore > 0) {
|
|
313
|
+
this.addMatchedField(match, {
|
|
314
|
+
type: "table_comment",
|
|
315
|
+
field: "TABLE_COMMENT",
|
|
316
|
+
value: this.truncateValue(row.TABLE_COMMENT),
|
|
317
|
+
score: tableCommentScore,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const columnCommentScore = this.getMatchScore(row.COLUMN_COMMENT, keyword, 65);
|
|
321
|
+
if (columnCommentScore > 0) {
|
|
322
|
+
this.addMatchedField(match, {
|
|
323
|
+
type: "column_comment",
|
|
324
|
+
field: row.COLUMN_NAME,
|
|
325
|
+
value: this.truncateValue(row.COLUMN_COMMENT),
|
|
326
|
+
score: columnCommentScore,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const matches = Array.from(matchMap.values())
|
|
332
|
+
.map((match) => ({
|
|
333
|
+
...match,
|
|
334
|
+
score: Math.min(100, match.score + Math.min(20, Math.max(match.matched_fields.length - 1, 0) * 5)),
|
|
335
|
+
}))
|
|
336
|
+
.sort((a, b) => b.score - a.score || a.table_name.localeCompare(b.table_name))
|
|
337
|
+
.slice(0, limit);
|
|
338
|
+
return {
|
|
339
|
+
status: "success",
|
|
340
|
+
data: {
|
|
341
|
+
database,
|
|
342
|
+
keyword,
|
|
343
|
+
search_in: searchIn,
|
|
344
|
+
matches,
|
|
345
|
+
total_matches: matches.length,
|
|
346
|
+
recommended_next_steps: matches.slice(0, 5).flatMap((match) => [
|
|
347
|
+
`read_table_schema({ table_name: '${match.table_name}' })`,
|
|
348
|
+
`read_records({ table_name: '${match.table_name}', pagination: { page: 1, limit: 5 } })`,
|
|
349
|
+
]),
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
return {
|
|
355
|
+
status: "error",
|
|
356
|
+
error: error.message,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Guarded read-only keyword search across text-like data columns.
|
|
362
|
+
*/
|
|
363
|
+
async searchDataAcrossTables(params) {
|
|
364
|
+
try {
|
|
365
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
366
|
+
if (!dbValidation.valid) {
|
|
367
|
+
return { status: "error", error: dbValidation.error };
|
|
368
|
+
}
|
|
369
|
+
const keyword = params.keyword?.trim();
|
|
370
|
+
if (!keyword) {
|
|
371
|
+
return { status: "error", error: "keyword is required" };
|
|
372
|
+
}
|
|
373
|
+
const database = dbValidation.database;
|
|
374
|
+
const maxTables = this.clampNumber(params.max_tables, 20, 1, 100);
|
|
375
|
+
const limitPerTable = this.clampNumber(params.limit_per_table, 5, 1, 20);
|
|
376
|
+
if (params.tables) {
|
|
377
|
+
for (const table of params.tables) {
|
|
378
|
+
if (!this.security.validateIdentifier(table).valid) {
|
|
379
|
+
return { status: "error", error: `Invalid table name: ${table}` };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (params.columns) {
|
|
384
|
+
for (const column of params.columns) {
|
|
385
|
+
if (!this.security.validateIdentifier(column).valid) {
|
|
386
|
+
return { status: "error", error: `Invalid column name: ${column}` };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const candidateTables = params.tables?.length
|
|
391
|
+
? (await this.getTablesByName(database, params.tables)).slice(0, maxTables)
|
|
392
|
+
: await this.db.query(`
|
|
393
|
+
SELECT TABLE_NAME, TABLE_ROWS, TABLE_COMMENT
|
|
394
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
395
|
+
WHERE TABLE_SCHEMA = ?
|
|
396
|
+
ORDER BY TABLE_NAME
|
|
397
|
+
LIMIT ?
|
|
398
|
+
`, [database, maxTables]);
|
|
399
|
+
if (!candidateTables.length) {
|
|
400
|
+
return {
|
|
401
|
+
status: "success",
|
|
402
|
+
data: {
|
|
403
|
+
database,
|
|
404
|
+
keyword,
|
|
405
|
+
matches: [],
|
|
406
|
+
total_hits: 0,
|
|
407
|
+
tables_scanned: 0,
|
|
408
|
+
message: "No tables found to scan.",
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const tableNames = candidateTables.map((table) => table.TABLE_NAME);
|
|
413
|
+
const tablePlaceholders = tableNames.map(() => "?").join(",");
|
|
414
|
+
const textTypes = [
|
|
415
|
+
"char",
|
|
416
|
+
"varchar",
|
|
417
|
+
"tinytext",
|
|
418
|
+
"text",
|
|
419
|
+
"mediumtext",
|
|
420
|
+
"longtext",
|
|
421
|
+
"json",
|
|
422
|
+
"enum",
|
|
423
|
+
"set",
|
|
424
|
+
];
|
|
425
|
+
const typePlaceholders = textTypes.map(() => "?").join(",");
|
|
426
|
+
const columnParams = [database, ...tableNames, ...textTypes];
|
|
427
|
+
let columnFilter = "";
|
|
428
|
+
if (params.columns?.length) {
|
|
429
|
+
columnFilter = ` AND COLUMN_NAME IN (${params.columns.map(() => "?").join(",")})`;
|
|
430
|
+
columnParams.push(...params.columns);
|
|
431
|
+
}
|
|
432
|
+
const columns = await this.db.query(`
|
|
433
|
+
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
|
|
434
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
435
|
+
WHERE TABLE_SCHEMA = ?
|
|
436
|
+
AND TABLE_NAME IN (${tablePlaceholders})
|
|
437
|
+
AND DATA_TYPE IN (${typePlaceholders})
|
|
438
|
+
${columnFilter}
|
|
439
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION
|
|
440
|
+
`, columnParams);
|
|
441
|
+
const columnsByTable = new Map();
|
|
442
|
+
for (const column of columns) {
|
|
443
|
+
if (!columnsByTable.has(column.TABLE_NAME)) {
|
|
444
|
+
columnsByTable.set(column.TABLE_NAME, []);
|
|
445
|
+
}
|
|
446
|
+
columnsByTable.get(column.TABLE_NAME).push(column);
|
|
447
|
+
}
|
|
448
|
+
const likePattern = `%${this.escapeLikePattern(keyword)}%`;
|
|
449
|
+
const keywordLower = keyword.toLowerCase();
|
|
450
|
+
const matches = [];
|
|
451
|
+
const skippedTables = [];
|
|
452
|
+
for (const table of candidateTables) {
|
|
453
|
+
const tableColumns = columnsByTable.get(table.TABLE_NAME) || [];
|
|
454
|
+
if (!tableColumns.length) {
|
|
455
|
+
skippedTables.push({
|
|
456
|
+
table_name: table.TABLE_NAME,
|
|
457
|
+
reason: "No searchable text-like columns found.",
|
|
458
|
+
});
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const selectList = tableColumns
|
|
462
|
+
.map((column) => `CAST(${this.quoteIdentifier(column.COLUMN_NAME)} AS CHAR) AS ${this.quoteIdentifier(column.COLUMN_NAME)}`)
|
|
463
|
+
.join(", ");
|
|
464
|
+
const whereClause = tableColumns
|
|
465
|
+
.map((column) => `CAST(${this.quoteIdentifier(column.COLUMN_NAME)} AS CHAR) LIKE ? ESCAPE '\\\\'`)
|
|
466
|
+
.join(" OR ");
|
|
467
|
+
const queryParams = [...tableColumns.map(() => likePattern), limitPerTable];
|
|
468
|
+
const dataRows = await this.db.query(`
|
|
469
|
+
SELECT ${selectList}
|
|
470
|
+
FROM ${this.quoteIdentifier(database)}.${this.quoteIdentifier(table.TABLE_NAME)}
|
|
471
|
+
WHERE ${whereClause}
|
|
472
|
+
LIMIT ?
|
|
473
|
+
`, queryParams, false);
|
|
474
|
+
if (!dataRows.length) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const rowHits = dataRows
|
|
478
|
+
.map((row) => {
|
|
479
|
+
const matchedColumns = tableColumns
|
|
480
|
+
.map((column) => {
|
|
481
|
+
const rawValue = row[column.COLUMN_NAME];
|
|
482
|
+
const sampleValue = this.truncateValue(rawValue);
|
|
483
|
+
return sampleValue.toLowerCase().includes(keywordLower)
|
|
484
|
+
? {
|
|
485
|
+
column_name: column.COLUMN_NAME,
|
|
486
|
+
data_type: column.DATA_TYPE,
|
|
487
|
+
sample_value: sampleValue,
|
|
488
|
+
}
|
|
489
|
+
: undefined;
|
|
490
|
+
})
|
|
491
|
+
.filter(Boolean);
|
|
492
|
+
return matchedColumns.length
|
|
493
|
+
? {
|
|
494
|
+
matched_columns: matchedColumns,
|
|
495
|
+
}
|
|
496
|
+
: undefined;
|
|
497
|
+
})
|
|
498
|
+
.filter(Boolean);
|
|
499
|
+
if (rowHits.length) {
|
|
500
|
+
matches.push({
|
|
501
|
+
table_name: table.TABLE_NAME,
|
|
502
|
+
row_estimate: typeof table.TABLE_ROWS === "number"
|
|
503
|
+
? table.TABLE_ROWS
|
|
504
|
+
: parseInt(table.TABLE_ROWS || "0", 10) || 0,
|
|
505
|
+
hit_count: rowHits.length,
|
|
506
|
+
hits: rowHits,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
status: "success",
|
|
512
|
+
data: {
|
|
513
|
+
database,
|
|
514
|
+
keyword,
|
|
515
|
+
matches,
|
|
516
|
+
total_hits: matches.reduce((sum, match) => sum + match.hit_count, 0),
|
|
517
|
+
tables_scanned: candidateTables.length - skippedTables.length,
|
|
518
|
+
skipped_tables: skippedTables,
|
|
519
|
+
limits: {
|
|
520
|
+
max_tables: maxTables,
|
|
521
|
+
limit_per_table: limitPerTable,
|
|
522
|
+
},
|
|
523
|
+
recommended_next_steps: matches.slice(0, 5).flatMap((match) => [
|
|
524
|
+
`read_table_schema({ table_name: '${match.table_name}' })`,
|
|
525
|
+
`read_records({ table_name: '${match.table_name}', pagination: { page: 1, limit: 5 } })`,
|
|
526
|
+
]),
|
|
527
|
+
},
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
return {
|
|
532
|
+
status: "error",
|
|
533
|
+
error: error.message,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Unified discovery entry point for "where is X?" questions.
|
|
539
|
+
*/
|
|
540
|
+
async searchSchema(params) {
|
|
541
|
+
try {
|
|
542
|
+
const dbValidation = this.validateDatabaseAccess(params?.database);
|
|
543
|
+
if (!dbValidation.valid) {
|
|
544
|
+
return { status: "error", error: dbValidation.error };
|
|
545
|
+
}
|
|
546
|
+
const query = params.query?.trim();
|
|
547
|
+
if (!query) {
|
|
548
|
+
return { status: "error", error: "query is required" };
|
|
549
|
+
}
|
|
550
|
+
const database = dbValidation.database;
|
|
551
|
+
const maxResults = this.clampNumber(params.max_results, 20, 1, 100);
|
|
552
|
+
const modes = params.modes?.length
|
|
553
|
+
? Array.from(new Set(params.modes))
|
|
554
|
+
: ["table_names", "column_names", "comments"];
|
|
555
|
+
const validModes = ["table_names", "column_names", "comments", "sample_data"];
|
|
556
|
+
for (const mode of modes) {
|
|
557
|
+
if (!validModes.includes(mode)) {
|
|
558
|
+
return {
|
|
559
|
+
status: "error",
|
|
560
|
+
error: `Invalid mode '${mode}'. Must be one of ${validModes.join(", ")}`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const schemaModes = modes.filter((mode) => mode !== "sample_data");
|
|
565
|
+
const combinedMatches = [];
|
|
566
|
+
let schemaMatches = [];
|
|
567
|
+
let dataMatches = [];
|
|
568
|
+
if (schemaModes.length) {
|
|
569
|
+
const schemaResult = await this.findTablesByKeyword({
|
|
570
|
+
keyword: query,
|
|
571
|
+
search_in: schemaModes.length === 1 ? schemaModes[0] : "all",
|
|
572
|
+
database,
|
|
573
|
+
limit: maxResults,
|
|
574
|
+
});
|
|
575
|
+
if (schemaResult.status === "error") {
|
|
576
|
+
return schemaResult;
|
|
577
|
+
}
|
|
578
|
+
const allowedTypes = new Set(schemaModes.flatMap((mode) => mode === "comments"
|
|
579
|
+
? ["table_comment", "column_comment"]
|
|
580
|
+
: [mode === "table_names" ? "table_name" : "column_name"]));
|
|
581
|
+
schemaMatches = (schemaResult.data?.matches || [])
|
|
582
|
+
.map((match) => ({
|
|
583
|
+
...match,
|
|
584
|
+
matched_fields: match.matched_fields.filter((field) => allowedTypes.has(field.type)),
|
|
585
|
+
}))
|
|
586
|
+
.filter((match) => match.matched_fields.length)
|
|
587
|
+
.map((match) => ({
|
|
588
|
+
source: "schema",
|
|
589
|
+
table_name: match.table_name,
|
|
590
|
+
match_type: match.matched_fields[0]?.type || "schema",
|
|
591
|
+
score: match.score,
|
|
592
|
+
columns: match.column_names,
|
|
593
|
+
row_estimate: match.row_estimate,
|
|
594
|
+
matched_fields: match.matched_fields,
|
|
595
|
+
table_comment: match.table_comment,
|
|
596
|
+
}));
|
|
597
|
+
combinedMatches.push(...schemaMatches);
|
|
598
|
+
}
|
|
599
|
+
if (modes.includes("sample_data")) {
|
|
600
|
+
const dataResult = await this.searchDataAcrossTables({
|
|
601
|
+
keyword: query,
|
|
602
|
+
database,
|
|
603
|
+
tables: params.tables,
|
|
604
|
+
columns: params.columns,
|
|
605
|
+
max_tables: params.max_tables,
|
|
606
|
+
limit_per_table: params.limit_per_table,
|
|
607
|
+
});
|
|
608
|
+
if (dataResult.status === "error") {
|
|
609
|
+
return dataResult;
|
|
610
|
+
}
|
|
611
|
+
dataMatches = (dataResult.data?.matches || []).map((match) => ({
|
|
612
|
+
source: "sample_data",
|
|
613
|
+
table_name: match.table_name,
|
|
614
|
+
match_type: "sample_data",
|
|
615
|
+
score: 50,
|
|
616
|
+
row_estimate: match.row_estimate,
|
|
617
|
+
hit_count: match.hit_count,
|
|
618
|
+
hits: match.hits,
|
|
619
|
+
}));
|
|
620
|
+
combinedMatches.push(...dataMatches);
|
|
621
|
+
}
|
|
622
|
+
const matches = combinedMatches
|
|
623
|
+
.sort((a, b) => b.score - a.score || a.table_name.localeCompare(b.table_name))
|
|
624
|
+
.slice(0, maxResults);
|
|
625
|
+
return {
|
|
626
|
+
status: "success",
|
|
627
|
+
data: {
|
|
628
|
+
database,
|
|
629
|
+
query,
|
|
630
|
+
modes,
|
|
631
|
+
matches,
|
|
632
|
+
schema_matches: schemaMatches,
|
|
633
|
+
data_matches: dataMatches,
|
|
634
|
+
total_matches: matches.length,
|
|
635
|
+
recommended_next_steps: Array.from(new Set(matches.slice(0, 5).flatMap((match) => [
|
|
636
|
+
`read_table_schema({ table_name: '${match.table_name}' })`,
|
|
637
|
+
`read_records({ table_name: '${match.table_name}', pagination: { page: 1, limit: 5 } })`,
|
|
638
|
+
]))),
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
return {
|
|
644
|
+
status: "error",
|
|
645
|
+
error: error.message,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
157
649
|
/**
|
|
158
650
|
* Build a compact, schema-aware context pack for RAG (tables, PK/FK, columns, row estimates)
|
|
159
651
|
*/
|
|
@@ -164,20 +656,42 @@ class AnalysisTools {
|
|
|
164
656
|
return { status: "error", error: dbValidation.error };
|
|
165
657
|
}
|
|
166
658
|
const database = dbValidation.database;
|
|
167
|
-
const maxTables =
|
|
168
|
-
const maxColumns =
|
|
659
|
+
const maxTables = this.clampNumber(params.max_tables, 50, 1, 200);
|
|
660
|
+
const maxColumns = this.clampNumber(params.max_columns, 12, 1, 200);
|
|
169
661
|
const includeRelationships = params.include_relationships ?? true;
|
|
662
|
+
const includeComments = params.include_comments ?? false;
|
|
663
|
+
const keywordFilter = params.keyword_filter?.trim();
|
|
170
664
|
// Count total tables for truncation note
|
|
171
665
|
const totalTablesResult = await this.db.query(`SELECT COUNT(*) as total FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ?`, [database]);
|
|
172
666
|
const totalTables = totalTablesResult[0]?.total ?? 0;
|
|
173
|
-
// Fetch tables limited for context pack
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
667
|
+
// Fetch tables limited for context pack. Keyword searches are relevance ordered.
|
|
668
|
+
let tables;
|
|
669
|
+
if (keywordFilter) {
|
|
670
|
+
const searchResult = await this.findTablesByKeyword({
|
|
671
|
+
keyword: keywordFilter,
|
|
672
|
+
search_in: "all",
|
|
673
|
+
database,
|
|
674
|
+
limit: maxTables,
|
|
675
|
+
});
|
|
676
|
+
if (searchResult.status === "error") {
|
|
677
|
+
return searchResult;
|
|
678
|
+
}
|
|
679
|
+
const rankedNames = (searchResult.data?.matches || []).map((match) => match.table_name);
|
|
680
|
+
const fetchedTables = await this.getTablesByName(database, rankedNames);
|
|
681
|
+
const tableByName = new Map(fetchedTables.map((table) => [table.TABLE_NAME, table]));
|
|
682
|
+
tables = rankedNames
|
|
683
|
+
.map((tableName) => tableByName.get(tableName))
|
|
684
|
+
.filter(Boolean);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
tables = await this.db.query(`
|
|
688
|
+
SELECT TABLE_NAME, TABLE_ROWS, TABLE_COMMENT
|
|
689
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
690
|
+
WHERE TABLE_SCHEMA = ?
|
|
691
|
+
ORDER BY TABLE_NAME
|
|
692
|
+
LIMIT ?
|
|
693
|
+
`, [database, maxTables]);
|
|
694
|
+
}
|
|
181
695
|
if (!tables.length) {
|
|
182
696
|
return {
|
|
183
697
|
status: "success",
|
|
@@ -186,7 +700,7 @@ class AnalysisTools {
|
|
|
186
700
|
total_tables: 0,
|
|
187
701
|
tables: [],
|
|
188
702
|
relationships: [],
|
|
189
|
-
context_text: `Schema-Aware RAG Context Pack (${database}): no tables found.`,
|
|
703
|
+
context_text: `Schema-Aware RAG Context Pack (${database}): no tables found${keywordFilter ? ` for keyword "${keywordFilter}"` : ""}.`,
|
|
190
704
|
},
|
|
191
705
|
};
|
|
192
706
|
}
|
|
@@ -194,7 +708,7 @@ class AnalysisTools {
|
|
|
194
708
|
const placeholders = tableNames.map(() => "?").join(",");
|
|
195
709
|
const columnParams = [database, ...tableNames];
|
|
196
710
|
const columns = await this.db.query(`
|
|
197
|
-
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY, IS_NULLABLE
|
|
711
|
+
SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_KEY, IS_NULLABLE, COLUMN_COMMENT
|
|
198
712
|
FROM INFORMATION_SCHEMA.COLUMNS
|
|
199
713
|
WHERE TABLE_SCHEMA = ?
|
|
200
714
|
AND TABLE_NAME IN (${placeholders})
|
|
@@ -237,6 +751,9 @@ class AnalysisTools {
|
|
|
237
751
|
references: fkRef
|
|
238
752
|
? `${fkRef.table}.${fkRef.column}`
|
|
239
753
|
: undefined,
|
|
754
|
+
comment: includeComments && col.COLUMN_COMMENT
|
|
755
|
+
? col.COLUMN_COMMENT
|
|
756
|
+
: undefined,
|
|
240
757
|
};
|
|
241
758
|
});
|
|
242
759
|
const primaryKeys = tableColumns
|
|
@@ -253,6 +770,9 @@ class AnalysisTools {
|
|
|
253
770
|
row_estimate: typeof table.TABLE_ROWS === "number"
|
|
254
771
|
? table.TABLE_ROWS
|
|
255
772
|
: parseInt(table.TABLE_ROWS || "0", 10) || 0,
|
|
773
|
+
table_comment: includeComments && table.TABLE_COMMENT
|
|
774
|
+
? table.TABLE_COMMENT
|
|
775
|
+
: undefined,
|
|
256
776
|
primary_keys: primaryKeys,
|
|
257
777
|
columns: columnsForContext,
|
|
258
778
|
foreign_keys: foreignKeyList,
|
|
@@ -267,6 +787,9 @@ class AnalysisTools {
|
|
|
267
787
|
}));
|
|
268
788
|
const lines = [];
|
|
269
789
|
lines.push(`Schema-Aware RAG Context Pack (${database})`);
|
|
790
|
+
if (keywordFilter) {
|
|
791
|
+
lines.push(`Keyword filter: "${keywordFilter}"`);
|
|
792
|
+
}
|
|
270
793
|
lines.push(`Tables shown: ${tableEntries.length}/${totalTables || tableEntries.length} (rows are approximate)`);
|
|
271
794
|
lines.push(`Per-table column limit: ${maxColumns}${tableEntries.some((t) => t.truncated_columns > 0)
|
|
272
795
|
? " (additional columns truncated)"
|
|
@@ -283,9 +806,15 @@ class AnalysisTools {
|
|
|
283
806
|
if (c.references)
|
|
284
807
|
tags.push(`-> ${c.references}`);
|
|
285
808
|
const nullability = c.nullable ? "null" : "not null";
|
|
286
|
-
|
|
809
|
+
const comment = c.comment
|
|
810
|
+
? ` — ${this.truncateValue(c.comment, 120)}`
|
|
811
|
+
: "";
|
|
812
|
+
return `${c.name} ${c.data_type} (${nullability})${tags.length ? ` [${tags.join(", ")}]` : ""}${comment}`;
|
|
287
813
|
});
|
|
288
814
|
lines.push(`- ${t.table_name} (${approxRows} rows) PK: ${t.primary_keys.length ? t.primary_keys.join(", ") : "none"}`);
|
|
815
|
+
if (t.table_comment) {
|
|
816
|
+
lines.push(` Comment: ${this.truncateValue(t.table_comment, 240)}`);
|
|
817
|
+
}
|
|
289
818
|
lines.push(` Columns: ${columnSnippets.join("; ")}`);
|
|
290
819
|
if (t.truncated_columns) {
|
|
291
820
|
lines.push(` ...and ${t.truncated_columns} more columns not shown`);
|
|
@@ -312,6 +841,8 @@ class AnalysisTools {
|
|
|
312
841
|
limits: {
|
|
313
842
|
max_tables: maxTables,
|
|
314
843
|
max_columns: maxColumns,
|
|
844
|
+
include_comments: includeComments,
|
|
845
|
+
keyword_filter: keywordFilter,
|
|
315
846
|
},
|
|
316
847
|
},
|
|
317
848
|
};
|
|
@@ -33,7 +33,8 @@ export declare class QueryTools {
|
|
|
33
33
|
*/
|
|
34
34
|
getSuggestedHints(goal: "SPEED" | "MEMORY" | "STABILITY"): QueryHints;
|
|
35
35
|
/**
|
|
36
|
-
* Execute write operations (INSERT, UPDATE
|
|
36
|
+
* Execute write operations (INSERT, UPDATE) with validation.
|
|
37
|
+
* DELETE requires the separate delete permission and is validated in the security layer.
|
|
37
38
|
* Note: DDL operations are blocked by the security layer for safety
|
|
38
39
|
*/
|
|
39
40
|
executeWriteQuery(queryParams: {
|
package/dist/tools/queryTools.js
CHANGED
|
@@ -118,7 +118,8 @@ class QueryTools {
|
|
|
118
118
|
return this.optimizer.getSuggestedHints(goal);
|
|
119
119
|
}
|
|
120
120
|
/**
|
|
121
|
-
* Execute write operations (INSERT, UPDATE
|
|
121
|
+
* Execute write operations (INSERT, UPDATE) with validation.
|
|
122
|
+
* DELETE requires the separate delete permission and is validated in the security layer.
|
|
122
123
|
* Note: DDL operations are blocked by the security layer for safety
|
|
123
124
|
*/
|
|
124
125
|
async executeWriteQuery(queryParams) {
|