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.
- package/README.md +197 -0
- package/dist/database/connection.d.ts +13 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/connection.js +155 -0
- package/dist/database/connection.js.map +1 -0
- package/dist/database/manager.d.ts +28 -0
- package/dist/database/manager.d.ts.map +1 -0
- package/dist/database/manager.js +621 -0
- package/dist/database/manager.js.map +1 -0
- package/dist/database/postgres-connection.d.ts +10 -0
- package/dist/database/postgres-connection.d.ts.map +1 -0
- package/dist/database/postgres-connection.js +113 -0
- package/dist/database/postgres-connection.js.map +1 -0
- package/dist/database/types.d.ts +84 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +6 -0
- package/dist/database/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +186 -0
- package/dist/server.js.map +1 -0
- package/dist/test-defaults.d.ts +2 -0
- package/dist/test-defaults.d.ts.map +1 -0
- package/dist/test-defaults.js +57 -0
- package/dist/test-defaults.js.map +1 -0
- package/dist/tools/analyze-query.d.ts +27 -0
- package/dist/tools/analyze-query.d.ts.map +1 -0
- package/dist/tools/analyze-query.js +71 -0
- package/dist/tools/analyze-query.js.map +1 -0
- package/dist/tools/execute-query.d.ts +33 -0
- package/dist/tools/execute-query.d.ts.map +1 -0
- package/dist/tools/execute-query.js +57 -0
- package/dist/tools/execute-query.js.map +1 -0
- package/dist/tools/get-foreign-keys.d.ts +38 -0
- package/dist/tools/get-foreign-keys.d.ts.map +1 -0
- package/dist/tools/get-foreign-keys.js +391 -0
- package/dist/tools/get-foreign-keys.js.map +1 -0
- package/dist/tools/get-indexes.d.ts +38 -0
- package/dist/tools/get-indexes.d.ts.map +1 -0
- package/dist/tools/get-indexes.js +472 -0
- package/dist/tools/get-indexes.js.map +1 -0
- package/dist/tools/information-schema-query.d.ts +33 -0
- package/dist/tools/information-schema-query.d.ts.map +1 -0
- package/dist/tools/information-schema-query.js +76 -0
- package/dist/tools/information-schema-query.js.map +1 -0
- package/dist/tools/inspect-table.d.ts +38 -0
- package/dist/tools/inspect-table.d.ts.map +1 -0
- package/dist/tools/inspect-table.js +351 -0
- package/dist/tools/inspect-table.js.map +1 -0
- package/dist/tools/list-databases.d.ts +14 -0
- package/dist/tools/list-databases.d.ts.map +1 -0
- package/dist/tools/list-databases.js +83 -0
- package/dist/tools/list-databases.js.map +1 -0
- package/dist/tools/list-tables.d.ts +19 -0
- package/dist/tools/list-tables.d.ts.map +1 -0
- package/dist/tools/list-tables.js +130 -0
- package/dist/tools/list-tables.js.map +1 -0
- package/dist/utils/errors.d.ts +32 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +98 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +28 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +132 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/validators/input-validator.d.ts +76 -0
- package/dist/validators/input-validator.d.ts.map +1 -0
- package/dist/validators/input-validator.js +295 -0
- package/dist/validators/input-validator.js.map +1 -0
- package/dist/validators/query-validator.d.ts +19 -0
- package/dist/validators/query-validator.d.ts.map +1 -0
- package/dist/validators/query-validator.js +229 -0
- package/dist/validators/query-validator.js.map +1 -0
- package/enhanced_sql_prompt.md +324 -0
- package/examples/claude-config.json +23 -0
- package/examples/roo-config.json +16 -0
- package/package.json +42 -0
- package/src/database/connection.ts +165 -0
- package/src/database/manager.ts +682 -0
- package/src/database/postgres-connection.ts +123 -0
- package/src/database/types.ts +93 -0
- package/src/index.ts +136 -0
- package/src/server.ts +254 -0
- package/src/test-defaults.ts +63 -0
- package/src/tools/analyze-query.test.ts +100 -0
- package/src/tools/analyze-query.ts +112 -0
- package/src/tools/execute-query.ts +91 -0
- package/src/tools/get-foreign-keys.test.ts +51 -0
- package/src/tools/get-foreign-keys.ts +488 -0
- package/src/tools/get-indexes.test.ts +51 -0
- package/src/tools/get-indexes.ts +570 -0
- package/src/tools/information-schema-query.ts +125 -0
- package/src/tools/inspect-table.test.ts +59 -0
- package/src/tools/inspect-table.ts +440 -0
- package/src/tools/list-databases.ts +119 -0
- package/src/tools/list-tables.ts +181 -0
- package/src/utils/errors.ts +103 -0
- package/src/utils/logger.ts +158 -0
- package/src/validators/input-validator.ts +318 -0
- package/src/validators/query-validator.ts +267 -0
- 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
|
+
}
|