@berthojoris/mcp-mysql-server 1.42.2 → 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.
@@ -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 = Math.min(Math.max(params.max_tables ?? 50, 1), 200);
168
- const maxColumns = Math.min(Math.max(params.max_columns ?? 12, 1), 200);
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
- 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]);
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
- return `${c.name} ${c.data_type} (${nullability})${tags.length ? ` [${tags.join(", ")}]` : ""}`;
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
  };