mcp-database-inspector 2.0.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.
Files changed (105) hide show
  1. package/README.md +197 -0
  2. package/dist/database/connection.d.ts +13 -0
  3. package/dist/database/connection.d.ts.map +1 -0
  4. package/dist/database/connection.js +155 -0
  5. package/dist/database/connection.js.map +1 -0
  6. package/dist/database/manager.d.ts +28 -0
  7. package/dist/database/manager.d.ts.map +1 -0
  8. package/dist/database/manager.js +621 -0
  9. package/dist/database/manager.js.map +1 -0
  10. package/dist/database/postgres-connection.d.ts +10 -0
  11. package/dist/database/postgres-connection.d.ts.map +1 -0
  12. package/dist/database/postgres-connection.js +113 -0
  13. package/dist/database/postgres-connection.js.map +1 -0
  14. package/dist/database/types.d.ts +84 -0
  15. package/dist/database/types.d.ts.map +1 -0
  16. package/dist/database/types.js +6 -0
  17. package/dist/database/types.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +120 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/server.d.ts +14 -0
  23. package/dist/server.d.ts.map +1 -0
  24. package/dist/server.js +186 -0
  25. package/dist/server.js.map +1 -0
  26. package/dist/test-defaults.d.ts +2 -0
  27. package/dist/test-defaults.d.ts.map +1 -0
  28. package/dist/test-defaults.js +57 -0
  29. package/dist/test-defaults.js.map +1 -0
  30. package/dist/tools/analyze-query.d.ts +27 -0
  31. package/dist/tools/analyze-query.d.ts.map +1 -0
  32. package/dist/tools/analyze-query.js +71 -0
  33. package/dist/tools/analyze-query.js.map +1 -0
  34. package/dist/tools/execute-query.d.ts +33 -0
  35. package/dist/tools/execute-query.d.ts.map +1 -0
  36. package/dist/tools/execute-query.js +57 -0
  37. package/dist/tools/execute-query.js.map +1 -0
  38. package/dist/tools/get-foreign-keys.d.ts +38 -0
  39. package/dist/tools/get-foreign-keys.d.ts.map +1 -0
  40. package/dist/tools/get-foreign-keys.js +391 -0
  41. package/dist/tools/get-foreign-keys.js.map +1 -0
  42. package/dist/tools/get-indexes.d.ts +38 -0
  43. package/dist/tools/get-indexes.d.ts.map +1 -0
  44. package/dist/tools/get-indexes.js +472 -0
  45. package/dist/tools/get-indexes.js.map +1 -0
  46. package/dist/tools/information-schema-query.d.ts +33 -0
  47. package/dist/tools/information-schema-query.d.ts.map +1 -0
  48. package/dist/tools/information-schema-query.js +76 -0
  49. package/dist/tools/information-schema-query.js.map +1 -0
  50. package/dist/tools/inspect-table.d.ts +38 -0
  51. package/dist/tools/inspect-table.d.ts.map +1 -0
  52. package/dist/tools/inspect-table.js +351 -0
  53. package/dist/tools/inspect-table.js.map +1 -0
  54. package/dist/tools/list-databases.d.ts +14 -0
  55. package/dist/tools/list-databases.d.ts.map +1 -0
  56. package/dist/tools/list-databases.js +83 -0
  57. package/dist/tools/list-databases.js.map +1 -0
  58. package/dist/tools/list-tables.d.ts +19 -0
  59. package/dist/tools/list-tables.d.ts.map +1 -0
  60. package/dist/tools/list-tables.js +130 -0
  61. package/dist/tools/list-tables.js.map +1 -0
  62. package/dist/utils/errors.d.ts +32 -0
  63. package/dist/utils/errors.d.ts.map +1 -0
  64. package/dist/utils/errors.js +98 -0
  65. package/dist/utils/errors.js.map +1 -0
  66. package/dist/utils/logger.d.ts +28 -0
  67. package/dist/utils/logger.d.ts.map +1 -0
  68. package/dist/utils/logger.js +132 -0
  69. package/dist/utils/logger.js.map +1 -0
  70. package/dist/validators/input-validator.d.ts +76 -0
  71. package/dist/validators/input-validator.d.ts.map +1 -0
  72. package/dist/validators/input-validator.js +295 -0
  73. package/dist/validators/input-validator.js.map +1 -0
  74. package/dist/validators/query-validator.d.ts +19 -0
  75. package/dist/validators/query-validator.d.ts.map +1 -0
  76. package/dist/validators/query-validator.js +229 -0
  77. package/dist/validators/query-validator.js.map +1 -0
  78. package/enhanced_sql_prompt.md +324 -0
  79. package/examples/claude-config.json +23 -0
  80. package/examples/roo-config.json +16 -0
  81. package/package.json +42 -0
  82. package/src/database/connection.ts +165 -0
  83. package/src/database/manager.ts +682 -0
  84. package/src/database/postgres-connection.ts +123 -0
  85. package/src/database/types.ts +93 -0
  86. package/src/index.ts +136 -0
  87. package/src/server.ts +254 -0
  88. package/src/test-defaults.ts +63 -0
  89. package/src/tools/analyze-query.test.ts +100 -0
  90. package/src/tools/analyze-query.ts +112 -0
  91. package/src/tools/execute-query.ts +91 -0
  92. package/src/tools/get-foreign-keys.test.ts +51 -0
  93. package/src/tools/get-foreign-keys.ts +488 -0
  94. package/src/tools/get-indexes.test.ts +51 -0
  95. package/src/tools/get-indexes.ts +570 -0
  96. package/src/tools/information-schema-query.ts +125 -0
  97. package/src/tools/inspect-table.test.ts +59 -0
  98. package/src/tools/inspect-table.ts +440 -0
  99. package/src/tools/list-databases.ts +119 -0
  100. package/src/tools/list-tables.ts +181 -0
  101. package/src/utils/errors.ts +103 -0
  102. package/src/utils/logger.ts +158 -0
  103. package/src/validators/input-validator.ts +318 -0
  104. package/src/validators/query-validator.ts +267 -0
  105. package/tsconfig.json +30 -0
@@ -0,0 +1,59 @@
1
+ // @ts-nocheck
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { handleInspectTable } from './inspect-table';
4
+ import { DatabaseManager } from '../database/manager';
5
+
6
+ const mockDbManager = {
7
+ getTableSchema: vi.fn(),
8
+ getIndexes: vi.fn(),
9
+ getForeignKeys: vi.fn()
10
+ } as unknown as DatabaseManager;
11
+
12
+ describe('inspect_table', () => {
13
+ it('should return schema for a single table', async () => {
14
+ mockDbManager.getTableSchema.mockResolvedValueOnce([{ columnName: 'id', dataType: 'int', isNullable: 'NO', isPrimaryKey: true, isAutoIncrement: true }]);
15
+ mockDbManager.getIndexes.mockResolvedValueOnce([{ indexName: 'PRIMARY', columnName: 'id', nonUnique: false, nullable: false, isPrimary: true }]);
16
+ mockDbManager.getForeignKeys.mockResolvedValueOnce([]);
17
+ const args = { database: 'testdb', table: 'users' };
18
+ const result = await handleInspectTable(args, mockDbManager);
19
+ const parsed = JSON.parse(result.content[0].text);
20
+ expect(parsed.table).toBe('users');
21
+ });
22
+
23
+ it('should return schemas for multiple tables', async () => {
24
+ mockDbManager.getTableSchema
25
+ .mockResolvedValueOnce([{ columnName: 'id', dataType: 'int', isNullable: 'NO', isPrimaryKey: true, isAutoIncrement: true }])
26
+ .mockResolvedValueOnce([{ columnName: 'id', dataType: 'int', isNullable: 'NO', isPrimaryKey: true, isAutoIncrement: true }]);
27
+ mockDbManager.getIndexes
28
+ .mockResolvedValueOnce([{ indexName: 'PRIMARY', columnName: 'id', nonUnique: false, nullable: false, isPrimary: true }])
29
+ .mockResolvedValueOnce([{ indexName: 'PRIMARY', columnName: 'id', nonUnique: false, nullable: false, isPrimary: true }]);
30
+ mockDbManager.getForeignKeys
31
+ .mockResolvedValueOnce([])
32
+ .mockResolvedValueOnce([]);
33
+ const args = { database: 'testdb', tables: ['users', 'orders'] };
34
+ const result = await handleInspectTable(args, mockDbManager);
35
+ const parsed = JSON.parse(result.content[0].text);
36
+ expect(Object.keys(parsed)).toHaveLength(2);
37
+ expect(parsed.users.table).toBe('users');
38
+ expect(parsed.orders.table).toBe('orders');
39
+ });
40
+
41
+ it('should throw error if neither table nor tables is provided', async () => {
42
+ await expect(() =>
43
+ handleInspectTable({ database: 'testdb' }, mockDbManager)
44
+ ).rejects.toThrow(/Either 'table' or non-empty 'tables'/);
45
+ });
46
+
47
+ it('should throw error for invalid table name', async () => {
48
+ await expect(() =>
49
+ handleInspectTable({ database: 'testdb', table: 'invalid;DROP' }, mockDbManager)
50
+ ).rejects.toThrow(/Invalid table name/);
51
+ });
52
+
53
+ it('should throw error if table not found', async () => {
54
+ mockDbManager.getTableSchema.mockResolvedValueOnce([]);
55
+ await expect(() =>
56
+ handleInspectTable({ database: 'testdb', table: 'notfound' }, mockDbManager)
57
+ ).rejects.toThrow(/not found/);
58
+ });
59
+ });
@@ -0,0 +1,440 @@
1
+ import { z } from 'zod';
2
+ import { DatabaseManager } from '../database/manager.js';
3
+ import { InputValidator } from '../validators/input-validator.js';
4
+ import { Logger } from '../utils/logger.js';
5
+ import { ToolError } from '../utils/errors.js';
6
+
7
+ // Tool schema
8
+ const InspectTableArgsSchema = z.object({
9
+ database: z.string().min(1, 'Database name is required'),
10
+ table: z.string().optional(),
11
+ tables: z.array(z.string().min(1)).optional()
12
+ }).superRefine((data, ctx) => {
13
+ const hasTable = typeof data.table === 'string' && data.table.length > 0;
14
+ const hasTables = Array.isArray(data.tables) && data.tables.length > 0;
15
+ if (hasTable && hasTables) {
16
+ ctx.addIssue({
17
+ code: z.ZodIssueCode.custom,
18
+ message: "Provide either 'table' or 'tables', not both"
19
+ });
20
+ } else if (!hasTable && !hasTables) {
21
+ ctx.addIssue({
22
+ code: z.ZodIssueCode.custom,
23
+ message: "Either 'table' or non-empty 'tables' must be provided"
24
+ });
25
+ }
26
+ });
27
+
28
+ /**
29
+ * Tool: inspect_table
30
+ * Get complete table schema including columns, types, constraints, and metadata.
31
+ *
32
+ * Supports both single-table and multi-table inspection:
33
+ * - Provide either `table` (string) for a single table, or `tables` (string[]) for multiple tables.
34
+ * - If `tables` is provided, returns a mapping of table names to their schema analysis.
35
+ * - Do not provide both `table` and `tables` at the same time.
36
+ */
37
+ export interface InspectTableTool {
38
+ name: 'inspect_table';
39
+ description: 'Get complete table schema including columns, types, constraints, and metadata. Supports multi-table inspection via the tables: string[] parameter.';
40
+ inputSchema: {
41
+ type: 'object';
42
+ properties: {
43
+ database: {
44
+ type: 'string';
45
+ description: 'Name of the database containing the table(s)';
46
+ };
47
+ table: {
48
+ type: 'string';
49
+ description: 'Name of the table to inspect (single-table mode)';
50
+ };
51
+ tables: {
52
+ type: 'array';
53
+ items: { type: 'string' };
54
+ description: 'Array of table names to inspect (multi-table mode)';
55
+ };
56
+ };
57
+ required: ['database'];
58
+ };
59
+ }
60
+
61
+ export const inspectTableToolDefinition: InspectTableTool = {
62
+ name: 'inspect_table',
63
+ description: 'Get complete table schema including columns, types, constraints, and metadata. Supports multi-table inspection via the tables: string[] parameter.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ database: {
68
+ type: 'string',
69
+ description: 'Name of the database containing the table(s)'
70
+ },
71
+ table: {
72
+ type: 'string',
73
+ description: 'Name of the table to inspect (single-table mode)'
74
+ },
75
+ tables: {
76
+ type: 'array',
77
+ items: { type: 'string' },
78
+ description: 'Array of table names to inspect (multi-table mode)'
79
+ }
80
+ },
81
+ required: ['database']
82
+ }
83
+ };
84
+
85
+ export async function handleInspectTable(
86
+ args: unknown,
87
+ dbManager: DatabaseManager
88
+ ): Promise<any> {
89
+ try {
90
+ Logger.info('Executing inspect_table tool');
91
+
92
+ // Validate arguments
93
+ const validationResult = InspectTableArgsSchema.safeParse(args);
94
+ if (!validationResult.success) {
95
+ Logger.warn('Invalid arguments for inspect_table', validationResult.error);
96
+ throw new ToolError(
97
+ `Invalid arguments: ${validationResult.error.issues.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
98
+ 'inspect_table'
99
+ );
100
+ }
101
+
102
+ const { database, table, tables } = validationResult.data;
103
+
104
+ // Sanitize database input
105
+ const sanitizedDatabase = InputValidator.sanitizeString(database);
106
+
107
+ // Validate database name format
108
+ const dbNameValidation = InputValidator.validateDatabaseName(sanitizedDatabase);
109
+ if (!dbNameValidation.isValid) {
110
+ throw new ToolError(
111
+ `Invalid database name: ${dbNameValidation.error}`,
112
+ 'inspect_table'
113
+ );
114
+ }
115
+
116
+ // Helper to process a single table
117
+ const processTable = async (tableName: string) => {
118
+ try {
119
+ const sanitizedTable = InputValidator.sanitizeString(tableName);
120
+
121
+ // Validate table name format
122
+ const tableNameValidation = InputValidator.validateTableName(sanitizedTable);
123
+ if (!tableNameValidation.isValid) {
124
+ throw new ToolError(
125
+ `Invalid table name: ${tableNameValidation.error}`,
126
+ 'inspect_table'
127
+ );
128
+ }
129
+
130
+ Logger.info(`Inspecting table: ${sanitizedDatabase}.${sanitizedTable}`);
131
+ Logger.time(`inspect_table_execution_${sanitizedTable}`);
132
+
133
+ // Get table schema
134
+ const columns = await dbManager.getTableSchema(sanitizedDatabase, sanitizedTable);
135
+
136
+ // Get additional metadata in parallel
137
+ const [foreignKeys, indexes] = await Promise.all([
138
+ dbManager.getForeignKeys(sanitizedDatabase, sanitizedTable),
139
+ dbManager.getIndexes(sanitizedDatabase, sanitizedTable)
140
+ ]);
141
+
142
+ Logger.timeEnd(`inspect_table_execution_${sanitizedTable}`);
143
+ Logger.info(`Retrieved schema for table: ${sanitizedDatabase}.${sanitizedTable} with ${columns.length} columns`);
144
+
145
+ if (columns.length === 0) {
146
+ throw new ToolError(
147
+ `Table '${sanitizedTable}' not found in database '${sanitizedDatabase}' or has no accessible columns`,
148
+ 'inspect_table'
149
+ );
150
+ }
151
+
152
+ // Analyze schema patterns
153
+ const analysis = analyzeTableSchema(columns, indexes, foreignKeys);
154
+
155
+ // Group columns by characteristics
156
+ const columnsByType = groupColumnsByType(columns);
157
+
158
+ // Create comprehensive response
159
+ const response = {
160
+ database: sanitizedDatabase,
161
+ table: sanitizedTable,
162
+ columns: columns.map(col => ({
163
+ name: col.columnName,
164
+ dataType: col.dataType,
165
+ fullType: formatFullDataType(col),
166
+ nullable: col.isNullable === 'YES',
167
+ defaultValue: col.columnDefault,
168
+ isPrimaryKey: col.isPrimaryKey,
169
+ isAutoIncrement: col.isAutoIncrement,
170
+ comment: col.columnComment,
171
+ constraints: getColumnConstraints(col),
172
+ properties: {
173
+ hasLength: col.characterMaximumLength !== null && col.characterMaximumLength !== undefined,
174
+ hasPrecision: col.numericPrecision !== null && col.numericPrecision !== undefined,
175
+ hasScale: col.numericScale !== null && col.numericScale !== undefined
176
+ }
177
+ })),
178
+ constraints: {
179
+ primaryKey: columns.filter(col => col.isPrimaryKey).map(col => col.columnName),
180
+ foreignKeys: foreignKeys.map(fk => ({
181
+ constraintName: fk.constraintName,
182
+ columnName: fk.columnName,
183
+ referencedTable: fk.referencedTableName,
184
+ referencedColumn: fk.referencedColumnName,
185
+ updateRule: fk.updateRule,
186
+ deleteRule: fk.deleteRule
187
+ })),
188
+ unique: indexes.filter(idx => !idx.nonUnique && !idx.isPrimary)
189
+ .map(idx => idx.indexName)
190
+ .filter((value, index, self) => self.indexOf(value) === index) // Remove duplicates
191
+ },
192
+ indexes: groupIndexesByName(indexes),
193
+ statistics: {
194
+ totalColumns: columns.length,
195
+ nullableColumns: columns.filter(col => col.isNullable === 'YES').length,
196
+ primaryKeyColumns: columns.filter(col => col.isPrimaryKey).length,
197
+ autoIncrementColumns: columns.filter(col => col.isAutoIncrement).length,
198
+ indexedColumns: [...new Set(indexes.map(idx => idx.columnName))].length,
199
+ foreignKeyColumns: [...new Set(foreignKeys.map(fk => fk.columnName))].length,
200
+ totalIndexes: [...new Set(indexes.map(idx => idx.indexName))].length,
201
+ totalForeignKeys: foreignKeys.length
202
+ },
203
+ columnsByType,
204
+ analysis,
205
+ summary: {
206
+ description: `Table '${sanitizedTable}' has ${columns.length} columns with ${
207
+ columns.filter(col => col.isPrimaryKey).length
208
+ } primary key column(s) and ${indexes.length > 0 ? [...new Set(indexes.map(idx => idx.indexName))].length : 0} index(es)`,
209
+ recommendations: analysis.recommendations
210
+ }
211
+ };
212
+
213
+ Logger.debug('inspect_table completed successfully', {
214
+ database: sanitizedDatabase,
215
+ table: sanitizedTable,
216
+ columnCount: columns.length,
217
+ indexCount: indexes.length,
218
+ foreignKeyCount: foreignKeys.length
219
+ });
220
+
221
+ return { ok: true, result: response };
222
+ } catch (err) {
223
+ Logger.error(`Error inspecting table '${tableName}'`, err);
224
+ return {
225
+ ok: false,
226
+ error: err instanceof ToolError
227
+ ? err.message
228
+ : (err instanceof Error ? err.message : 'Unknown error')
229
+ };
230
+ }
231
+ };
232
+
233
+ // Multi-table support
234
+ if (Array.isArray(tables) && tables.length > 0) {
235
+ const results: Record<string, any> = {};
236
+ for (const t of tables) {
237
+ const res = await processTable(t);
238
+ if (res.ok) {
239
+ results[t] = res.result;
240
+ } else {
241
+ results[t] = { error: res.error };
242
+ }
243
+ }
244
+ return {
245
+ content: [{
246
+ type: 'text',
247
+ text: JSON.stringify(results, null, 2)
248
+ }]
249
+ };
250
+ }
251
+
252
+ // Single-table fallback (backward compatible)
253
+ if (typeof table === 'string' && table.length > 0) {
254
+ const res = await processTable(table);
255
+ if (res.ok) {
256
+ return {
257
+ content: [{
258
+ type: 'text',
259
+ text: JSON.stringify(res.result, null, 2)
260
+ }]
261
+ };
262
+ } else {
263
+ throw new ToolError(
264
+ `Failed to inspect table '${table}': ${res.error}`,
265
+ 'inspect_table'
266
+ );
267
+ }
268
+ }
269
+
270
+ // Should not reach here due to schema refinement
271
+ throw new ToolError(
272
+ "Either 'table' or non-empty 'tables' must be provided",
273
+ 'inspect_table'
274
+ );
275
+ } catch (error) {
276
+ // Add table context to error logs
277
+ let tableContext: string | undefined;
278
+ let argTables: string[] | undefined;
279
+ let argTable: string | undefined;
280
+ if (args && typeof args === 'object') {
281
+ // @ts-ignore
282
+ argTables = Array.isArray(args.tables) ? args.tables : undefined;
283
+ // @ts-ignore
284
+ argTable = typeof args.table === 'string' ? args.table : undefined;
285
+ }
286
+ if (Array.isArray(argTables) && argTables.length > 0) {
287
+ tableContext = `tables: [${argTables.join(', ')}]`;
288
+ } else if (typeof argTable === 'string' && argTable.length > 0) {
289
+ tableContext = `table: ${argTable}`;
290
+ } else {
291
+ tableContext = 'no table(s) specified';
292
+ }
293
+ Logger.error(`Error in inspect_table tool (${tableContext})`, error);
294
+
295
+ if (error instanceof ToolError) {
296
+ throw error;
297
+ }
298
+
299
+ throw new ToolError(
300
+ `Failed to inspect table(s) (${tableContext}): ${error instanceof Error ? error.message : 'Unknown error'}`,
301
+ 'inspect_table',
302
+ error instanceof Error ? error : undefined
303
+ );
304
+ }
305
+ }
306
+
307
+ // Helper functions
308
+ function formatFullDataType(column: any): string {
309
+ let type = column.dataType.toLowerCase();
310
+
311
+ if (column.characterMaximumLength !== null && column.characterMaximumLength !== undefined) {
312
+ type += `(${column.characterMaximumLength})`;
313
+ } else if (column.numericPrecision !== null && column.numericPrecision !== undefined) {
314
+ if (column.numericScale !== null && column.numericScale !== undefined && column.numericScale > 0) {
315
+ type += `(${column.numericPrecision},${column.numericScale})`;
316
+ } else {
317
+ type += `(${column.numericPrecision})`;
318
+ }
319
+ }
320
+
321
+ return type;
322
+ }
323
+
324
+ function getColumnConstraints(column: any): string[] {
325
+ const constraints = [];
326
+
327
+ if (column.isPrimaryKey) constraints.push('PRIMARY KEY');
328
+ if (column.isAutoIncrement) constraints.push('AUTO_INCREMENT');
329
+ if (column.isNullable === 'NO') constraints.push('NOT NULL');
330
+
331
+ return constraints;
332
+ }
333
+
334
+ function groupColumnsByType(columns: any[]): Record<string, string[]> {
335
+ const groups: Record<string, string[]> = {
336
+ numeric: [],
337
+ string: [],
338
+ datetime: [],
339
+ binary: [],
340
+ json: [],
341
+ other: []
342
+ };
343
+
344
+ columns.forEach(col => {
345
+ const type = col.dataType.toLowerCase();
346
+ if (['int', 'bigint', 'smallint', 'tinyint', 'mediumint', 'decimal', 'numeric', 'float', 'double', 'bit', 'real', 'serial'].some(t => type.includes(t))) {
347
+ groups.numeric.push(col.columnName);
348
+ } else if (['varchar', 'char', 'text', 'longtext', 'mediumtext', 'tinytext', 'enum', 'set', 'uuid', 'inet', 'cidr', 'macaddr'].some(t => type.includes(t))) {
349
+ groups.string.push(col.columnName);
350
+ } else if (['datetime', 'date', 'time', 'timestamp', 'year', 'interval'].some(t => type.includes(t))) {
351
+ groups.datetime.push(col.columnName);
352
+ } else if (['binary', 'varbinary', 'blob', 'longblob', 'mediumblob', 'tinyblob', 'bytea'].some(t => type.includes(t))) {
353
+ groups.binary.push(col.columnName);
354
+ } else if (type === 'json' || type === 'jsonb') {
355
+ groups.json.push(col.columnName);
356
+ } else {
357
+ groups.other.push(col.columnName);
358
+ }
359
+ });
360
+
361
+ // Remove empty groups
362
+ Object.keys(groups).forEach(key => {
363
+ if (groups[key].length === 0) {
364
+ delete groups[key];
365
+ }
366
+ });
367
+
368
+ return groups;
369
+ }
370
+
371
+ function groupIndexesByName(indexes: any[]): any[] {
372
+ const indexGroups: Record<string, any> = {};
373
+
374
+ indexes.forEach(idx => {
375
+ if (!indexGroups[idx.indexName]) {
376
+ indexGroups[idx.indexName] = {
377
+ name: idx.indexName,
378
+ type: idx.indexType,
379
+ unique: !idx.nonUnique,
380
+ columns: []
381
+ };
382
+ }
383
+ indexGroups[idx.indexName].columns.push({
384
+ name: idx.columnName,
385
+ cardinality: idx.cardinality,
386
+ subPart: idx.subPart
387
+ });
388
+ });
389
+
390
+ return Object.values(indexGroups);
391
+ }
392
+
393
+ function analyzeTableSchema(columns: any[], indexes: any[], foreignKeys: any[]): any {
394
+ const recommendations = [];
395
+ const patterns = [];
396
+
397
+ // Check for common patterns
398
+ const hasId = columns.some(col => col.columnName.toLowerCase() === 'id');
399
+ const hasCreatedAt = columns.some(col => col.columnName.toLowerCase().includes('created'));
400
+ const hasUpdatedAt = columns.some(col => col.columnName.toLowerCase().includes('updated'));
401
+
402
+ if (hasId && hasCreatedAt && hasUpdatedAt) {
403
+ patterns.push('Standard entity pattern (id, created_at, updated_at)');
404
+ }
405
+
406
+ // Check for missing primary key
407
+ const hasPrimaryKey = columns.some(col => col.isPrimaryKey);
408
+ if (!hasPrimaryKey) {
409
+ recommendations.push('Consider adding a primary key for better performance and replication');
410
+ }
411
+
412
+ // Check for missing indexes on foreign key columns
413
+ const fkColumns = new Set(foreignKeys.map(fk => fk.columnName));
414
+ const indexedColumns = new Set(indexes.map(idx => idx.columnName));
415
+
416
+ fkColumns.forEach(fkCol => {
417
+ if (!indexedColumns.has(fkCol)) {
418
+ recommendations.push(`Consider adding an index on foreign key column '${fkCol}' for better join performance`);
419
+ }
420
+ });
421
+
422
+ // Check for very wide tables
423
+ if (columns.length > 50) {
424
+ recommendations.push('Table has many columns. Consider normalization or vertical partitioning');
425
+ }
426
+
427
+ // Check for nullable primary key (should not happen but good to check)
428
+ const nullablePK = columns.find(col => col.isPrimaryKey && col.isNullable === 'YES');
429
+ if (nullablePK) {
430
+ recommendations.push(`Primary key column '${nullablePK.columnName}' should not be nullable`);
431
+ }
432
+
433
+ return {
434
+ patterns,
435
+ recommendations,
436
+ hasStandardAuditFields: hasCreatedAt && hasUpdatedAt,
437
+ hasAutoIncrementPK: columns.some(col => col.isPrimaryKey && col.isAutoIncrement),
438
+ isWellIndexed: fkColumns.size === 0 || Array.from(fkColumns).every(col => indexedColumns.has(col))
439
+ };
440
+ }
@@ -0,0 +1,119 @@
1
+ import { z } from 'zod';
2
+ import { DatabaseManager } from '../database/manager.js';
3
+ import { Logger } from '../utils/logger.js';
4
+ import { ToolError, createErrorResponse } from '../utils/errors.js';
5
+
6
+ // Tool schema - no input required
7
+ const ListDatabasesArgsSchema = z.object({});
8
+
9
+ export interface ListDatabasesTool {
10
+ name: 'list_databases';
11
+ description: 'List all connected database names with connection status';
12
+ inputSchema: {
13
+ type: 'object';
14
+ properties: {};
15
+ required: never[];
16
+ };
17
+ }
18
+
19
+ export const listDatabasesToolDefinition: ListDatabasesTool = {
20
+ name: 'list_databases',
21
+ description: 'List all connected database names with connection status',
22
+ inputSchema: {
23
+ type: 'object',
24
+ properties: {},
25
+ required: []
26
+ }
27
+ };
28
+
29
+ export async function handleListDatabases(
30
+ args: unknown,
31
+ dbManager: DatabaseManager
32
+ ): Promise<any> {
33
+ try {
34
+ Logger.info('Executing list_databases tool');
35
+
36
+ // Validate arguments (should be empty object)
37
+ const validationResult = ListDatabasesArgsSchema.safeParse(args);
38
+ if (!validationResult.success) {
39
+ Logger.warn('Invalid arguments for list_databases', validationResult.error);
40
+ throw new ToolError(
41
+ `Invalid arguments: ${validationResult.error.issues.map(e => e.message).join(', ')}`,
42
+ 'list_databases'
43
+ );
44
+ }
45
+
46
+ Logger.time('list_databases_execution');
47
+
48
+ // Get database list from manager
49
+ const databases = dbManager.listDatabases();
50
+
51
+ Logger.timeEnd('list_databases_execution');
52
+ Logger.info(`Found ${databases.length} configured databases`);
53
+
54
+ // Format response with detailed information
55
+ const response = {
56
+ databases: databases.map(db => ({
57
+ name: db.name,
58
+ type: db.type,
59
+ host: db.host,
60
+ database: db.database,
61
+ connected: db.connected,
62
+ lastUsed: db.lastUsed.toISOString(),
63
+ connectionStatus: db.connected ? 'active' : 'disconnected'
64
+ })),
65
+ totalCount: databases.length,
66
+ connectedCount: databases.filter(db => db.connected).length,
67
+ summary: {
68
+ hasConnectedDatabases: databases.length > 0,
69
+ allConnected: databases.every(db => db.connected),
70
+ message: databases.length === 0
71
+ ? 'No databases configured. Add database connections first.'
72
+ : `Found ${databases.length} database(s), ${databases.filter(db => db.connected).length} connected.`
73
+ }
74
+ };
75
+
76
+ Logger.debug('list_databases completed successfully', {
77
+ totalDatabases: response.totalCount,
78
+ connectedCount: response.connectedCount
79
+ });
80
+
81
+ return {
82
+ content: [{
83
+ type: 'text',
84
+ text: JSON.stringify(response, null, 2)
85
+ }]
86
+ };
87
+
88
+ } catch (error) {
89
+ Logger.error('Error in list_databases tool', error);
90
+
91
+ if (error instanceof ToolError) {
92
+ throw error;
93
+ }
94
+
95
+ throw new ToolError(
96
+ `Failed to list databases: ${error instanceof Error ? error.message : 'Unknown error'}`,
97
+ 'list_databases',
98
+ error instanceof Error ? error : undefined
99
+ );
100
+ }
101
+ }
102
+
103
+ export function getListDatabasesSummary(dbManager: DatabaseManager): string {
104
+ try {
105
+ const databases = dbManager.listDatabases();
106
+ const connectedCount = databases.filter(db => db.connected).length;
107
+
108
+ if (databases.length === 0) {
109
+ return 'No databases configured';
110
+ }
111
+
112
+ return `${databases.length} database(s) configured, ${connectedCount} connected: ${
113
+ databases.map(db => `${db.name} (${db.connected ? 'connected' : 'disconnected'})`).join(', ')
114
+ }`;
115
+ } catch (error) {
116
+ Logger.error('Error getting database summary', error);
117
+ return 'Error retrieving database information';
118
+ }
119
+ }