sqlcipher-mcp-server 1.0.4 → 2.0.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/README.md +433 -210
- package/package.json +1 -1
- package/src/config/constants.js +18 -40
- package/src/definitions/prompts.js +124 -0
- package/src/definitions/tools.js +363 -0
- package/src/handlers/http-handlers.js +576 -4
- package/src/handlers/mcp-handlers.js +558 -69
- package/src/handlers/prompt-handlers.js +601 -0
- package/src/server/http-server.js +52 -2
- package/src/server/mcp-server.js +208 -95
- package/src/services/database-service.js +395 -55
- package/src/utils/database-operations.js +967 -0
- package/src/utils/detectors.js +55 -0
- package/src/utils/formatters.js +470 -64
- package/src/utils/validators.js +147 -58
- package/lib/database.js +0 -216
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Prompt Handlers
|
|
3
|
+
* Handlers for MCP prompt requests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { PROMPT_DEFINITIONS } from '../definitions/prompts.js';
|
|
7
|
+
import { getDatabasePassword } from '../config/environment.js';
|
|
8
|
+
import { resolveDatabasePath } from '../utils/validators.js';
|
|
9
|
+
import {
|
|
10
|
+
getTableListFromDatabase,
|
|
11
|
+
getTableSchemaFromDatabase,
|
|
12
|
+
getForeignKeysFromDatabase,
|
|
13
|
+
getTableInfoFromDatabase,
|
|
14
|
+
sampleTableDataFromDatabase,
|
|
15
|
+
explainQueryPlanFromDatabase,
|
|
16
|
+
getTableStatisticsFromDatabase
|
|
17
|
+
} from '../services/database-service.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handle list prompts request
|
|
21
|
+
* @returns {Object} List of available prompts
|
|
22
|
+
*/
|
|
23
|
+
export function handleListPrompts() {
|
|
24
|
+
return {
|
|
25
|
+
prompts: Object.values(PROMPT_DEFINITIONS),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Handle explore_database_schema prompt
|
|
31
|
+
* @param {Object} args - Prompt arguments
|
|
32
|
+
* @returns {Promise<Object>} Prompt response
|
|
33
|
+
*/
|
|
34
|
+
export async function handleExploreDatabaseSchemaPrompt(args) {
|
|
35
|
+
const { database_path } = args || {};
|
|
36
|
+
const dbPath = resolveDatabasePath(database_path);
|
|
37
|
+
const password = getDatabasePassword();
|
|
38
|
+
|
|
39
|
+
const tables = await getTableListFromDatabase(dbPath, password);
|
|
40
|
+
|
|
41
|
+
let messages = [
|
|
42
|
+
{
|
|
43
|
+
role: 'user',
|
|
44
|
+
content: {
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: `I want to explore the database schema at: ${dbPath}`
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
role: 'assistant',
|
|
51
|
+
content: {
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: `I'll help you explore the database schema. I found ${tables.length} table(s) in the database.\n\n` +
|
|
54
|
+
`Tables:\n${tables.map(t => `- ${t.name} (${t.type}) - ${t.row_count} rows`).join('\n')}\n\n` +
|
|
55
|
+
`Would you like me to:\n` +
|
|
56
|
+
`1. Show detailed schema for a specific table?\n` +
|
|
57
|
+
`2. Show foreign key relationships?\n` +
|
|
58
|
+
`3. Analyze data in a specific table?`
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
return { messages };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Handle describe_table_structure prompt
|
|
68
|
+
* @param {Object} args - Prompt arguments
|
|
69
|
+
* @returns {Promise<Object>} Prompt response
|
|
70
|
+
*/
|
|
71
|
+
export async function handleDescribeTableStructurePrompt(args) {
|
|
72
|
+
const { database_path, table_name } = args || {};
|
|
73
|
+
|
|
74
|
+
if (!table_name) {
|
|
75
|
+
return {
|
|
76
|
+
messages: [
|
|
77
|
+
{
|
|
78
|
+
role: 'user',
|
|
79
|
+
content: {
|
|
80
|
+
type: 'text',
|
|
81
|
+
text: 'I want to understand the structure of a table'
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
role: 'assistant',
|
|
86
|
+
content: {
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: 'Please provide the table name you want to explore.'
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const dbPath = resolveDatabasePath(database_path);
|
|
96
|
+
const password = getDatabasePassword();
|
|
97
|
+
|
|
98
|
+
const schema = await getTableSchemaFromDatabase(dbPath, password, table_name);
|
|
99
|
+
const info = await getTableInfoFromDatabase(dbPath, password, table_name);
|
|
100
|
+
const sample = await sampleTableDataFromDatabase(dbPath, password, table_name, 5, 0);
|
|
101
|
+
|
|
102
|
+
let description = `Table: ${table_name}\n\n`;
|
|
103
|
+
description += `Type: ${info.type}\n`;
|
|
104
|
+
description += `Rows: ${info.row_count}\n`;
|
|
105
|
+
description += `Columns: ${info.column_count}\n\n`;
|
|
106
|
+
|
|
107
|
+
description += `Column Details:\n`;
|
|
108
|
+
schema.columns.forEach(col => {
|
|
109
|
+
description += `- ${col.name} (${col.type || 'UNKNOWN'})`;
|
|
110
|
+
if (col.pk) description += ' [PRIMARY KEY]';
|
|
111
|
+
if (col.notnull) description += ' [NOT NULL]';
|
|
112
|
+
description += '\n';
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (schema.foreign_keys && schema.foreign_keys.length > 0) {
|
|
116
|
+
description += `\nForeign Keys:\n`;
|
|
117
|
+
schema.foreign_keys.forEach(fk => {
|
|
118
|
+
description += `- ${fk.from} -> ${fk.table}.${fk.to}\n`;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (schema.indexes && schema.indexes.length > 0) {
|
|
123
|
+
description += `\nIndexes:\n`;
|
|
124
|
+
schema.indexes.forEach(idx => {
|
|
125
|
+
description += `- ${idx.name}`;
|
|
126
|
+
if (idx.unique) description += ' (UNIQUE)';
|
|
127
|
+
description += '\n';
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
description += `\nSample Data (first 5 rows):\n`;
|
|
132
|
+
if (sample.rows.length > 0) {
|
|
133
|
+
description += `Columns: ${sample.columns.join(', ')}\n`;
|
|
134
|
+
sample.rows.forEach((row, i) => {
|
|
135
|
+
description += `Row ${i + 1}: ${sample.columns.map(c => row[c]).join(', ')}\n`;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
messages: [
|
|
141
|
+
{
|
|
142
|
+
role: 'user',
|
|
143
|
+
content: {
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: `Describe the structure of table "${table_name}"`
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
role: 'assistant',
|
|
150
|
+
content: {
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: description
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle find_data_relationships prompt
|
|
161
|
+
* @param {Object} args - Prompt arguments
|
|
162
|
+
* @returns {Promise<Object>} Prompt response
|
|
163
|
+
*/
|
|
164
|
+
export async function handleFindDataRelationshipsPrompt(args) {
|
|
165
|
+
const { database_path, table_name } = args || {};
|
|
166
|
+
const dbPath = resolveDatabasePath(database_path);
|
|
167
|
+
const password = getDatabasePassword();
|
|
168
|
+
|
|
169
|
+
const foreignKeys = await getForeignKeysFromDatabase(dbPath, password, table_name);
|
|
170
|
+
|
|
171
|
+
let description = table_name
|
|
172
|
+
? `Foreign key relationships for table "${table_name}":\n\n`
|
|
173
|
+
: `All foreign key relationships in the database:\n\n`;
|
|
174
|
+
|
|
175
|
+
if (foreignKeys.length === 0) {
|
|
176
|
+
description += 'No foreign key relationships found.';
|
|
177
|
+
} else {
|
|
178
|
+
// Group by table
|
|
179
|
+
const byTable = {};
|
|
180
|
+
foreignKeys.forEach(fk => {
|
|
181
|
+
if (!byTable[fk.table]) {
|
|
182
|
+
byTable[fk.table] = [];
|
|
183
|
+
}
|
|
184
|
+
byTable[fk.table].push(fk);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
Object.keys(byTable).forEach(tbl => {
|
|
188
|
+
description += `Table: ${tbl}\n`;
|
|
189
|
+
byTable[tbl].forEach(fk => {
|
|
190
|
+
description += ` - ${fk.from} -> ${fk.table}.${fk.to}\n`;
|
|
191
|
+
});
|
|
192
|
+
description += '\n';
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
messages: [
|
|
198
|
+
{
|
|
199
|
+
role: 'user',
|
|
200
|
+
content: {
|
|
201
|
+
type: 'text',
|
|
202
|
+
text: table_name
|
|
203
|
+
? `Show me the data relationships for table "${table_name}"`
|
|
204
|
+
: 'Show me all data relationships in the database'
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
role: 'assistant',
|
|
209
|
+
content: {
|
|
210
|
+
type: 'text',
|
|
211
|
+
text: description
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Handle generate_query_template prompt
|
|
220
|
+
* @param {Object} args - Prompt arguments
|
|
221
|
+
* @returns {Promise<Object>} Prompt response
|
|
222
|
+
*/
|
|
223
|
+
export async function handleGenerateQueryTemplatePrompt(args) {
|
|
224
|
+
const { database_path, table_name, intent } = args || {};
|
|
225
|
+
|
|
226
|
+
if (!table_name) {
|
|
227
|
+
return {
|
|
228
|
+
messages: [
|
|
229
|
+
{
|
|
230
|
+
role: 'user',
|
|
231
|
+
content: {
|
|
232
|
+
type: 'text',
|
|
233
|
+
text: 'I need help writing a SQL query'
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
role: 'assistant',
|
|
238
|
+
content: {
|
|
239
|
+
type: 'text',
|
|
240
|
+
text: 'Please provide the table name and what you want to do (count, sample, join, aggregate, or search).'
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const dbPath = resolveDatabasePath(database_path);
|
|
248
|
+
const password = getDatabasePassword();
|
|
249
|
+
|
|
250
|
+
const schema = await getTableSchemaFromDatabase(dbPath, password, table_name);
|
|
251
|
+
const columns = schema.columns.map(c => c.name).join(', ');
|
|
252
|
+
|
|
253
|
+
let templates = [];
|
|
254
|
+
const intentType = intent || 'sample';
|
|
255
|
+
|
|
256
|
+
switch (intentType) {
|
|
257
|
+
case 'count':
|
|
258
|
+
templates.push({
|
|
259
|
+
description: 'Count all rows',
|
|
260
|
+
query: `SELECT COUNT(*) as total FROM "${table_name}"`
|
|
261
|
+
});
|
|
262
|
+
templates.push({
|
|
263
|
+
description: 'Count by group',
|
|
264
|
+
query: `SELECT column_name, COUNT(*) as count FROM "${table_name}" GROUP BY column_name`
|
|
265
|
+
});
|
|
266
|
+
break;
|
|
267
|
+
case 'sample':
|
|
268
|
+
templates.push({
|
|
269
|
+
description: 'Get first 10 rows',
|
|
270
|
+
query: `SELECT ${columns} FROM "${table_name}" LIMIT 10`
|
|
271
|
+
});
|
|
272
|
+
templates.push({
|
|
273
|
+
description: 'Get rows with condition',
|
|
274
|
+
query: `SELECT ${columns} FROM "${table_name}" WHERE condition LIMIT 10`
|
|
275
|
+
});
|
|
276
|
+
break;
|
|
277
|
+
case 'aggregate':
|
|
278
|
+
const numericCols = schema.columns.filter(c =>
|
|
279
|
+
c.type && (c.type.toUpperCase().includes('INT') ||
|
|
280
|
+
c.type.toUpperCase().includes('REAL') ||
|
|
281
|
+
c.type.toUpperCase().includes('NUMERIC'))
|
|
282
|
+
);
|
|
283
|
+
if (numericCols.length > 0) {
|
|
284
|
+
const col = numericCols[0].name;
|
|
285
|
+
templates.push({
|
|
286
|
+
description: 'Calculate statistics',
|
|
287
|
+
query: `SELECT MIN("${col}"), MAX("${col}"), AVG("${col}"), SUM("${col}") FROM "${table_name}"`
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
case 'join':
|
|
292
|
+
if (schema.foreign_keys && schema.foreign_keys.length > 0) {
|
|
293
|
+
const fk = schema.foreign_keys[0];
|
|
294
|
+
templates.push({
|
|
295
|
+
description: 'Join with related table',
|
|
296
|
+
query: `SELECT t1.*, t2.* FROM "${table_name}" t1 JOIN "${fk.table}" t2 ON t1."${fk.from}" = t2."${fk.to}"`
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
case 'search':
|
|
301
|
+
const textCols = schema.columns.filter(c =>
|
|
302
|
+
!c.type || c.type.toUpperCase().includes('TEXT') ||
|
|
303
|
+
c.type.toUpperCase().includes('VARCHAR')
|
|
304
|
+
);
|
|
305
|
+
if (textCols.length > 0) {
|
|
306
|
+
const col = textCols[0].name;
|
|
307
|
+
templates.push({
|
|
308
|
+
description: 'Search by text',
|
|
309
|
+
query: `SELECT ${columns} FROM "${table_name}" WHERE "${col}" LIKE '%search_term%'`
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let response = `Query templates for table "${table_name}" (intent: ${intentType}):\n\n`;
|
|
316
|
+
templates.forEach((t, i) => {
|
|
317
|
+
response += `${i + 1}. ${t.description}:\n${t.query}\n\n`;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
messages: [
|
|
322
|
+
{
|
|
323
|
+
role: 'user',
|
|
324
|
+
content: {
|
|
325
|
+
type: 'text',
|
|
326
|
+
text: `Generate query templates for table "${table_name}" with intent "${intentType}"`
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
role: 'assistant',
|
|
331
|
+
content: {
|
|
332
|
+
type: 'text',
|
|
333
|
+
text: response
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Handle optimize_query prompt
|
|
342
|
+
* @param {Object} args - Prompt arguments
|
|
343
|
+
* @returns {Promise<Object>} Prompt response
|
|
344
|
+
*/
|
|
345
|
+
export async function handleOptimizeQueryPrompt(args) {
|
|
346
|
+
const { database_path, query } = args || {};
|
|
347
|
+
|
|
348
|
+
if (!query) {
|
|
349
|
+
return {
|
|
350
|
+
messages: [
|
|
351
|
+
{
|
|
352
|
+
role: 'user',
|
|
353
|
+
content: {
|
|
354
|
+
type: 'text',
|
|
355
|
+
text: 'I want to optimize a SQL query'
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
role: 'assistant',
|
|
360
|
+
content: {
|
|
361
|
+
type: 'text',
|
|
362
|
+
text: 'Please provide the SQL query you want to optimize.'
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const dbPath = resolveDatabasePath(database_path);
|
|
370
|
+
const password = getDatabasePassword();
|
|
371
|
+
|
|
372
|
+
const plan = await explainQueryPlanFromDatabase(dbPath, password, query);
|
|
373
|
+
|
|
374
|
+
let response = `Query Execution Plan:\n\n`;
|
|
375
|
+
plan.forEach(step => {
|
|
376
|
+
response += `${step.detail || step.notused || 'N/A'}\n`;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
response += `\n\nOptimization Suggestions:\n`;
|
|
380
|
+
|
|
381
|
+
// Analyze plan for common issues
|
|
382
|
+
const planText = JSON.stringify(plan).toLowerCase();
|
|
383
|
+
if (planText.includes('scan')) {
|
|
384
|
+
response += `- Consider adding indexes to avoid table scans\n`;
|
|
385
|
+
}
|
|
386
|
+
if (planText.includes('temp')) {
|
|
387
|
+
response += `- Query uses temporary tables, consider simplifying\n`;
|
|
388
|
+
}
|
|
389
|
+
if (!planText.includes('index')) {
|
|
390
|
+
response += `- No indexes detected in execution plan\n`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
response += `- Ensure WHERE clauses use indexed columns\n`;
|
|
394
|
+
response += `- Limit result sets when possible\n`;
|
|
395
|
+
response += `- Avoid SELECT * in production queries\n`;
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
messages: [
|
|
399
|
+
{
|
|
400
|
+
role: 'user',
|
|
401
|
+
content: {
|
|
402
|
+
type: 'text',
|
|
403
|
+
text: `Optimize this query: ${query}`
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
role: 'assistant',
|
|
408
|
+
content: {
|
|
409
|
+
type: 'text',
|
|
410
|
+
text: response
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
]
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Handle analyze_table_data prompt
|
|
419
|
+
* @param {Object} args - Prompt arguments
|
|
420
|
+
* @returns {Promise<Object>} Prompt response
|
|
421
|
+
*/
|
|
422
|
+
export async function handleAnalyzeTableDataPrompt(args) {
|
|
423
|
+
const { database_path, table_name } = args || {};
|
|
424
|
+
|
|
425
|
+
if (!table_name) {
|
|
426
|
+
return {
|
|
427
|
+
messages: [
|
|
428
|
+
{
|
|
429
|
+
role: 'user',
|
|
430
|
+
content: {
|
|
431
|
+
type: 'text',
|
|
432
|
+
text: 'I want to analyze table data'
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
role: 'assistant',
|
|
437
|
+
content: {
|
|
438
|
+
type: 'text',
|
|
439
|
+
text: 'Please provide the table name you want to analyze.'
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const dbPath = resolveDatabasePath(database_path);
|
|
447
|
+
const password = getDatabasePassword();
|
|
448
|
+
|
|
449
|
+
const stats = await getTableStatisticsFromDatabase(dbPath, password, table_name);
|
|
450
|
+
const sample = await sampleTableDataFromDatabase(dbPath, password, table_name, 5, 0);
|
|
451
|
+
|
|
452
|
+
let response = `Data Analysis for table "${table_name}":\n\n`;
|
|
453
|
+
response += `Total Rows: ${stats.total_rows}\n`;
|
|
454
|
+
response += `Total Columns: ${stats.column_count}\n\n`;
|
|
455
|
+
|
|
456
|
+
response += `Column Statistics:\n`;
|
|
457
|
+
stats.columns.forEach(col => {
|
|
458
|
+
response += `\n${col.name} (${col.type}):\n`;
|
|
459
|
+
response += ` - Distinct values: ${col.distinct_count}\n`;
|
|
460
|
+
response += ` - Null values: ${col.null_count}\n`;
|
|
461
|
+
if (col.min_value !== undefined) {
|
|
462
|
+
response += ` - Min: ${col.min_value}\n`;
|
|
463
|
+
response += ` - Max: ${col.max_value}\n`;
|
|
464
|
+
response += ` - Avg: ${col.avg_value}\n`;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
response += `\n\nSample Data (first 5 rows):\n`;
|
|
469
|
+
if (sample.rows.length > 0) {
|
|
470
|
+
sample.rows.forEach((row, i) => {
|
|
471
|
+
response += `Row ${i + 1}: ${sample.columns.map(c => `${c}=${row[c]}`).join(', ')}\n`;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Data quality checks
|
|
476
|
+
response += `\n\nData Quality Observations:\n`;
|
|
477
|
+
stats.columns.forEach(col => {
|
|
478
|
+
const nullPercentage = (col.null_count / stats.total_rows) * 100;
|
|
479
|
+
if (nullPercentage > 50) {
|
|
480
|
+
response += `- Column "${col.name}" has ${nullPercentage.toFixed(1)}% null values\n`;
|
|
481
|
+
}
|
|
482
|
+
if (col.distinct_count === stats.total_rows && stats.total_rows > 0) {
|
|
483
|
+
response += `- Column "${col.name}" appears to be unique (potential key)\n`;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
messages: [
|
|
489
|
+
{
|
|
490
|
+
role: 'user',
|
|
491
|
+
content: {
|
|
492
|
+
type: 'text',
|
|
493
|
+
text: `Analyze the data in table "${table_name}"`
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
role: 'assistant',
|
|
498
|
+
content: {
|
|
499
|
+
type: 'text',
|
|
500
|
+
text: response
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Handle compare_tables prompt
|
|
509
|
+
* @param {Object} args - Prompt arguments
|
|
510
|
+
* @returns {Promise<Object>} Prompt response
|
|
511
|
+
*/
|
|
512
|
+
export async function handleCompareTablesPrompt(args) {
|
|
513
|
+
const { database_path, table1_name, table2_name } = args || {};
|
|
514
|
+
|
|
515
|
+
if (!table1_name || !table2_name) {
|
|
516
|
+
return {
|
|
517
|
+
messages: [
|
|
518
|
+
{
|
|
519
|
+
role: 'user',
|
|
520
|
+
content: {
|
|
521
|
+
type: 'text',
|
|
522
|
+
text: 'I want to compare two tables'
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
role: 'assistant',
|
|
527
|
+
content: {
|
|
528
|
+
type: 'text',
|
|
529
|
+
text: 'Please provide both table names you want to compare.'
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
]
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const dbPath = resolveDatabasePath(database_path);
|
|
537
|
+
const password = getDatabasePassword();
|
|
538
|
+
|
|
539
|
+
const schema1 = await getTableSchemaFromDatabase(dbPath, password, table1_name);
|
|
540
|
+
const schema2 = await getTableSchemaFromDatabase(dbPath, password, table2_name);
|
|
541
|
+
const info1 = await getTableInfoFromDatabase(dbPath, password, table1_name);
|
|
542
|
+
const info2 = await getTableInfoFromDatabase(dbPath, password, table2_name);
|
|
543
|
+
|
|
544
|
+
let response = `Comparison of "${table1_name}" and "${table2_name}":\n\n`;
|
|
545
|
+
|
|
546
|
+
response += `Row Counts:\n`;
|
|
547
|
+
response += ` ${table1_name}: ${info1.row_count} rows\n`;
|
|
548
|
+
response += ` ${table2_name}: ${info2.row_count} rows\n\n`;
|
|
549
|
+
|
|
550
|
+
response += `Column Counts:\n`;
|
|
551
|
+
response += ` ${table1_name}: ${info1.column_count} columns\n`;
|
|
552
|
+
response += ` ${table2_name}: ${info2.column_count} columns\n\n`;
|
|
553
|
+
|
|
554
|
+
// Compare columns
|
|
555
|
+
const cols1 = schema1.columns.map(c => c.name);
|
|
556
|
+
const cols2 = schema2.columns.map(c => c.name);
|
|
557
|
+
|
|
558
|
+
const commonCols = cols1.filter(c => cols2.includes(c));
|
|
559
|
+
const uniqueToCols1 = cols1.filter(c => !cols2.includes(c));
|
|
560
|
+
const uniqueToCols2 = cols2.filter(c => !cols1.includes(c));
|
|
561
|
+
|
|
562
|
+
response += `Common Columns (${commonCols.length}):\n`;
|
|
563
|
+
commonCols.forEach(c => {
|
|
564
|
+
const col1 = schema1.columns.find(col => col.name === c);
|
|
565
|
+
const col2 = schema2.columns.find(col => col.name === c);
|
|
566
|
+
response += ` - ${c}: ${col1.type} vs ${col2.type}`;
|
|
567
|
+
if (col1.type !== col2.type) {
|
|
568
|
+
response += ` [TYPE MISMATCH]`;
|
|
569
|
+
}
|
|
570
|
+
response += `\n`;
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
if (uniqueToCols1.length > 0) {
|
|
574
|
+
response += `\nColumns only in "${table1_name}" (${uniqueToCols1.length}):\n`;
|
|
575
|
+
uniqueToCols1.forEach(c => response += ` - ${c}\n`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (uniqueToCols2.length > 0) {
|
|
579
|
+
response += `\nColumns only in "${table2_name}" (${uniqueToCols2.length}):\n`;
|
|
580
|
+
uniqueToCols2.forEach(c => response += ` - ${c}\n`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
messages: [
|
|
585
|
+
{
|
|
586
|
+
role: 'user',
|
|
587
|
+
content: {
|
|
588
|
+
type: 'text',
|
|
589
|
+
text: `Compare tables "${table1_name}" and "${table2_name}"`
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
role: 'assistant',
|
|
594
|
+
content: {
|
|
595
|
+
type: 'text',
|
|
596
|
+
text: response
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
]
|
|
600
|
+
};
|
|
601
|
+
}
|
|
@@ -6,7 +6,28 @@
|
|
|
6
6
|
import express from 'express';
|
|
7
7
|
import { HTTP_CONFIG } from '../config/constants.js';
|
|
8
8
|
import { getPort, isPasswordConfigured } from '../config/environment.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
handleHealthCheck,
|
|
11
|
+
handleInfo,
|
|
12
|
+
handleQuery,
|
|
13
|
+
handleListTables,
|
|
14
|
+
handleGetTableSchema,
|
|
15
|
+
handleListColumns,
|
|
16
|
+
handleGetForeignKeys,
|
|
17
|
+
handleGetIndexes,
|
|
18
|
+
handleFindRelatedTables,
|
|
19
|
+
handleGetDatabaseInfo,
|
|
20
|
+
handleGetTableInfo,
|
|
21
|
+
handleTestConnection,
|
|
22
|
+
handleExplainQuery,
|
|
23
|
+
handleValidateQuerySyntax,
|
|
24
|
+
handleSuggestQuery,
|
|
25
|
+
handleGetTableStatistics,
|
|
26
|
+
handleSampleTableData,
|
|
27
|
+
handleGetColumnStatistics,
|
|
28
|
+
handleSearchTables,
|
|
29
|
+
handleSearchColumns
|
|
30
|
+
} from '../handlers/http-handlers.js';
|
|
10
31
|
|
|
11
32
|
/**
|
|
12
33
|
* Create and configure Express app
|
|
@@ -18,11 +39,40 @@ export function createHttpApp() {
|
|
|
18
39
|
// Middleware to parse JSON request bodies
|
|
19
40
|
app.use(express.json());
|
|
20
41
|
|
|
21
|
-
//
|
|
42
|
+
// Server Status Routes
|
|
22
43
|
app.get('/health', handleHealthCheck);
|
|
23
44
|
app.get('/api/info', handleInfo);
|
|
45
|
+
|
|
46
|
+
// Query Execution
|
|
24
47
|
app.post('/api/query', handleQuery);
|
|
25
48
|
|
|
49
|
+
// Schema Exploration Routes
|
|
50
|
+
app.post('/api/tool/list_tables', handleListTables);
|
|
51
|
+
app.post('/api/tool/get_table_schema', handleGetTableSchema);
|
|
52
|
+
app.post('/api/tool/list_columns', handleListColumns);
|
|
53
|
+
app.post('/api/tool/get_foreign_keys', handleGetForeignKeys);
|
|
54
|
+
app.post('/api/tool/get_indexes', handleGetIndexes);
|
|
55
|
+
app.post('/api/tool/find_related_tables', handleFindRelatedTables);
|
|
56
|
+
|
|
57
|
+
// Database & Table Info Routes
|
|
58
|
+
app.post('/api/tool/get_database_info', handleGetDatabaseInfo);
|
|
59
|
+
app.post('/api/tool/get_table_info', handleGetTableInfo);
|
|
60
|
+
app.post('/api/tool/test_connection', handleTestConnection);
|
|
61
|
+
|
|
62
|
+
// Query Helper Routes
|
|
63
|
+
app.post('/api/tool/explain_query', handleExplainQuery);
|
|
64
|
+
app.post('/api/tool/validate_query_syntax', handleValidateQuerySyntax);
|
|
65
|
+
app.post('/api/tool/suggest_query', handleSuggestQuery);
|
|
66
|
+
|
|
67
|
+
// Data Analysis Routes
|
|
68
|
+
app.post('/api/tool/get_table_statistics', handleGetTableStatistics);
|
|
69
|
+
app.post('/api/tool/sample_table_data', handleSampleTableData);
|
|
70
|
+
app.post('/api/tool/get_column_statistics', handleGetColumnStatistics);
|
|
71
|
+
|
|
72
|
+
// Search Routes
|
|
73
|
+
app.post('/api/tool/search_tables', handleSearchTables);
|
|
74
|
+
app.post('/api/tool/search_columns', handleSearchColumns);
|
|
75
|
+
|
|
26
76
|
return app;
|
|
27
77
|
}
|
|
28
78
|
|