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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Database Type Detection Utilities
3
+ * Utilities for detecting and handling encrypted vs unencrypted databases
4
+ */
5
+
6
+ /**
7
+ * Detect if a database is encrypted or unencrypted
8
+ * This is a helper function that attempts to determine database type
9
+ * @param {string} dbPath - Path to the database file
10
+ * @returns {Promise<{isEncrypted: boolean, needsPassword: boolean}>}
11
+ */
12
+ export async function detectDatabaseType(dbPath) {
13
+ // For now, we'll rely on the connection logic in database.js
14
+ // which handles both encrypted and unencrypted databases gracefully
15
+ // This function can be expanded in the future for more sophisticated detection
16
+ return {
17
+ isEncrypted: false, // Will be determined during connection
18
+ needsPassword: false // Will be determined during connection
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Sanitize table/column names to prevent SQL injection
24
+ * @param {string} name - Table or column name
25
+ * @returns {string} Sanitized name
26
+ * @throws {Error} If name contains invalid characters
27
+ */
28
+ export function sanitizeSqlIdentifier(name) {
29
+ if (!name || typeof name !== 'string') {
30
+ throw new Error('Invalid identifier: must be a non-empty string');
31
+ }
32
+
33
+ // SQLite identifiers can contain letters, digits, underscores, and dollar signs
34
+ // They cannot start with a digit (except for auto-generated names)
35
+ const validPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
36
+
37
+ if (!validPattern.test(name)) {
38
+ throw new Error(`Invalid identifier: "${name}" contains invalid characters. Only letters, numbers, underscores, and dollar signs are allowed.`);
39
+ }
40
+
41
+ return name;
42
+ }
43
+
44
+ /**
45
+ * Escape a SQL identifier for use in queries
46
+ * @param {string} identifier - Table or column name
47
+ * @returns {string} Escaped identifier wrapped in quotes
48
+ */
49
+ export function escapeIdentifier(identifier) {
50
+ // Sanitize first
51
+ sanitizeSqlIdentifier(identifier);
52
+
53
+ // Wrap in double quotes and escape any double quotes in the identifier
54
+ return `"${identifier.replace(/"/g, '""')}"`;
55
+ }
@@ -1,64 +1,470 @@
1
- /**
2
- * Formatting Utilities
3
- * Functions for formatting query results and other data
4
- */
5
-
6
- import { QUERY_CONFIG } from '../config/constants.js';
7
-
8
- /**
9
- * Format query results as a readable string
10
- * @param {Object} result - Query result object with columns, rows, and rowCount
11
- * @param {string[]} result.columns - Array of column names
12
- * @param {Object[]} result.rows - Array of row objects
13
- * @param {number} result.rowCount - Number of rows returned
14
- * @returns {string} Formatted result string
15
- */
16
- export function formatQueryResults(result) {
17
- const { columns, rows, rowCount } = result;
18
-
19
- if (rowCount === 0) {
20
- return `Query executed successfully. No rows returned.\nColumns: ${columns.join(', ')}`;
21
- }
22
-
23
- // Build table-like output
24
- let output = `Query executed successfully. ${rowCount} row(s) returned.\n\n`;
25
-
26
- // Add column headers
27
- output += `Columns: ${columns.join(' | ')}\n`;
28
- output += '-'.repeat(columns.join(' | ').length) + '\n';
29
-
30
- // Add rows (limit to first maxDisplayRows for display)
31
- const displayRows = rows.slice(0, QUERY_CONFIG.maxDisplayRows);
32
- for (const row of displayRows) {
33
- const values = columns.map(col => formatCellValue(row[col]));
34
- output += values.join(' | ') + '\n';
35
- }
36
-
37
- if (rows.length > QUERY_CONFIG.maxDisplayRows) {
38
- output += `\n... (showing first ${QUERY_CONFIG.maxDisplayRows} of ${rowCount} rows)`;
39
- }
40
-
41
- // Add JSON representation for programmatic access
42
- output += '\n\nJSON representation:\n';
43
- output += JSON.stringify(result, null, 2);
44
-
45
- return output;
46
- }
47
-
48
- /**
49
- * Format a single cell value for display
50
- * @param {any} value - Cell value to format
51
- * @returns {string} Formatted cell value
52
- */
53
- function formatCellValue(value) {
54
- // Handle null/undefined
55
- if (value === null || value === undefined) {
56
- return 'NULL';
57
- }
58
-
59
- // Convert to string and truncate long values
60
- const str = String(value);
61
- return str.length > QUERY_CONFIG.maxValueLength
62
- ? str.substring(0, QUERY_CONFIG.maxValueLength - 3) + '...'
63
- : str;
64
- }
1
+ /**
2
+ * Formatting Utilities
3
+ * Functions for formatting query results and other data
4
+ */
5
+
6
+ import { QUERY_CONFIG } from '../config/constants.js';
7
+
8
+ /**
9
+ * Format query results as a readable string
10
+ * @param {Object} result - Query result object with columns, rows, and rowCount
11
+ * @param {string[]} result.columns - Array of column names
12
+ * @param {Object[]} result.rows - Array of row objects
13
+ * @param {number} result.rowCount - Number of rows returned
14
+ * @returns {string} Formatted result string
15
+ */
16
+ export function formatQueryResults(result) {
17
+ const { columns, rows, rowCount } = result;
18
+
19
+ if (rowCount === 0) {
20
+ return `Query executed successfully. No rows returned.\nColumns: ${columns.join(', ')}`;
21
+ }
22
+
23
+ // Build table-like output
24
+ let output = `Query executed successfully. ${rowCount} row(s) returned.\n\n`;
25
+
26
+ // Add column headers
27
+ output += `Columns: ${columns.join(' | ')}\n`;
28
+ output += '-'.repeat(columns.join(' | ').length) + '\n';
29
+
30
+ // Add rows (limit to first maxDisplayRows for display)
31
+ const displayRows = rows.slice(0, QUERY_CONFIG.maxDisplayRows);
32
+ for (const row of displayRows) {
33
+ const values = columns.map(col => formatCellValue(row[col]));
34
+ output += values.join(' | ') + '\n';
35
+ }
36
+
37
+ if (rows.length > QUERY_CONFIG.maxDisplayRows) {
38
+ output += `\n... (showing first ${QUERY_CONFIG.maxDisplayRows} of ${rowCount} rows)`;
39
+ }
40
+
41
+ // Add JSON representation for programmatic access
42
+ output += '\n\nJSON representation:\n';
43
+ output += JSON.stringify(result, null, 2);
44
+
45
+ return output;
46
+ }
47
+
48
+ /**
49
+ * Format a single cell value for display
50
+ * @param {any} value - Cell value to format
51
+ * @returns {string} Formatted cell value
52
+ */
53
+ function formatCellValue(value) {
54
+ // Handle null/undefined
55
+ if (value === null || value === undefined) {
56
+ return 'NULL';
57
+ }
58
+
59
+ // Convert to string and truncate long values
60
+ const str = String(value);
61
+ return str.length > QUERY_CONFIG.maxValueLength
62
+ ? str.substring(0, QUERY_CONFIG.maxValueLength - 3) + '...'
63
+ : str;
64
+ }
65
+
66
+ /**
67
+ * Format table list results
68
+ * @param {Array} tables - Array of table objects
69
+ * @returns {string} Formatted table list
70
+ */
71
+ export function formatTableList(tables) {
72
+ if (!tables || tables.length === 0) {
73
+ return 'No tables found in database.';
74
+ }
75
+
76
+ let output = `Found ${tables.length} table(s):\n\n`;
77
+
78
+ for (const table of tables) {
79
+ output += `- ${table.name} (${table.type})`;
80
+ if (table.row_count !== undefined) {
81
+ output += ` - ${table.row_count} row(s)`;
82
+ }
83
+ output += '\n';
84
+ }
85
+
86
+ output += '\n\nJSON representation:\n';
87
+ output += JSON.stringify(tables, null, 2);
88
+
89
+ return output;
90
+ }
91
+
92
+ /**
93
+ * Format table schema results
94
+ * @param {Object|Array} schema - Table schema or array of schemas
95
+ * @returns {string} Formatted schema
96
+ */
97
+ export function formatTableSchema(schema) {
98
+ if (Array.isArray(schema)) {
99
+ // Batch result
100
+ let output = `Schema for ${schema.length} table(s):\n\n`;
101
+
102
+ for (const s of schema) {
103
+ if (s.error) {
104
+ output += `Table: ${s.tableName} - ERROR: ${s.error}\n\n`;
105
+ } else {
106
+ output += formatSingleTableSchema(s) + '\n\n';
107
+ }
108
+ }
109
+
110
+ output += 'JSON representation:\n';
111
+ output += JSON.stringify(schema, null, 2);
112
+
113
+ return output;
114
+ } else {
115
+ // Single table
116
+ let output = formatSingleTableSchema(schema);
117
+ output += '\n\nJSON representation:\n';
118
+ output += JSON.stringify(schema, null, 2);
119
+ return output;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Format a single table schema
125
+ * @param {Object} schema - Table schema object
126
+ * @returns {string} Formatted schema
127
+ */
128
+ function formatSingleTableSchema(schema) {
129
+ let output = `Table: ${schema.tableName}\n`;
130
+ output += '='.repeat(40) + '\n\n';
131
+
132
+ // Columns
133
+ output += 'Columns:\n';
134
+ for (const col of schema.columns) {
135
+ output += ` - ${col.name} (${col.type || 'UNKNOWN'})`;
136
+ if (col.pk) output += ' PRIMARY KEY';
137
+ if (col.notnull) output += ' NOT NULL';
138
+ if (col.dflt_value !== null) output += ` DEFAULT ${col.dflt_value}`;
139
+ output += '\n';
140
+ }
141
+
142
+ // Foreign keys
143
+ if (schema.foreign_keys && schema.foreign_keys.length > 0) {
144
+ output += '\nForeign Keys:\n';
145
+ for (const fk of schema.foreign_keys) {
146
+ output += ` - ${fk.from} -> ${fk.table}.${fk.to}\n`;
147
+ }
148
+ }
149
+
150
+ // Indexes
151
+ if (schema.indexes && schema.indexes.length > 0) {
152
+ output += '\nIndexes:\n';
153
+ for (const idx of schema.indexes) {
154
+ output += ` - ${idx.name}`;
155
+ if (idx.unique) output += ' (UNIQUE)';
156
+ if (idx.columns && idx.columns.length > 0) {
157
+ const colNames = idx.columns.map(c => c.name).join(', ');
158
+ output += ` on (${colNames})`;
159
+ }
160
+ output += '\n';
161
+ }
162
+ }
163
+
164
+ return output;
165
+ }
166
+
167
+ /**
168
+ * Format foreign keys results
169
+ * @param {Array} foreignKeys - Array of foreign key relationships
170
+ * @returns {string} Formatted foreign keys
171
+ */
172
+ export function formatForeignKeys(foreignKeys) {
173
+ if (!foreignKeys || foreignKeys.length === 0) {
174
+ return 'No foreign keys found.';
175
+ }
176
+
177
+ let output = `Found ${foreignKeys.length} foreign key relationship(s):\n\n`;
178
+
179
+ for (const fk of foreignKeys) {
180
+ output += `- ${fk.table}.${fk.from} -> ${fk.table}.${fk.to}`;
181
+ if (fk.on_update) output += ` ON UPDATE ${fk.on_update}`;
182
+ if (fk.on_delete) output += ` ON DELETE ${fk.on_delete}`;
183
+ output += '\n';
184
+ }
185
+
186
+ output += '\n\nJSON representation:\n';
187
+ output += JSON.stringify(foreignKeys, null, 2);
188
+
189
+ return output;
190
+ }
191
+
192
+ /**
193
+ * Format indexes results
194
+ * @param {Array} indexes - Array of index information
195
+ * @returns {string} Formatted indexes
196
+ */
197
+ export function formatIndexes(indexes) {
198
+ if (!indexes || indexes.length === 0) {
199
+ return 'No indexes found.';
200
+ }
201
+
202
+ let output = `Found ${indexes.length} index(es):\n\n`;
203
+
204
+ for (const idx of indexes) {
205
+ output += `- ${idx.name} on table ${idx.table}`;
206
+ if (idx.unique) output += ' (UNIQUE)';
207
+ if (idx.columns && idx.columns.length > 0) {
208
+ const colNames = idx.columns.map(c => c.name).join(', ');
209
+ output += ` - columns: (${colNames})`;
210
+ }
211
+ output += '\n';
212
+ }
213
+
214
+ output += '\n\nJSON representation:\n';
215
+ output += JSON.stringify(indexes, null, 2);
216
+
217
+ return output;
218
+ }
219
+
220
+ /**
221
+ * Format database info results
222
+ * @param {Object} info - Database metadata
223
+ * @returns {string} Formatted database info
224
+ */
225
+ export function formatDatabaseInfo(info) {
226
+ let output = 'Database Information:\n';
227
+ output += '='.repeat(40) + '\n\n';
228
+
229
+ if (info.path) output += `Path: ${info.path}\n`;
230
+ if (info.sqlite_version) output += `SQLite Version: ${info.sqlite_version}\n`;
231
+ if (info.size_bytes !== undefined) {
232
+ const sizeMB = (info.size_bytes / (1024 * 1024)).toFixed(2);
233
+ output += `Size: ${info.size_bytes} bytes (${sizeMB} MB)\n`;
234
+ }
235
+ if (info.page_size) output += `Page Size: ${info.page_size} bytes\n`;
236
+ if (info.page_count) output += `Page Count: ${info.page_count}\n`;
237
+ if (info.encoding) output += `Encoding: ${info.encoding}\n`;
238
+ if (info.user_version !== undefined) output += `User Version: ${info.user_version}\n`;
239
+ if (info.application_id !== undefined) output += `Application ID: ${info.application_id}\n`;
240
+ if (info.schema_version !== undefined) output += `Schema Version: ${info.schema_version}\n`;
241
+ if (info.freelist_count !== undefined) output += `Free Pages: ${info.freelist_count}\n`;
242
+
243
+ output += '\n\nJSON representation:\n';
244
+ output += JSON.stringify(info, null, 2);
245
+
246
+ return output;
247
+ }
248
+
249
+ /**
250
+ * Format table info results
251
+ * @param {Object} info - Table information
252
+ * @returns {string} Formatted table info
253
+ */
254
+ export function formatTableInfo(info) {
255
+ let output = `Table Information: ${info.name}\n`;
256
+ output += '='.repeat(40) + '\n\n';
257
+
258
+ output += `Type: ${info.type}\n`;
259
+ output += `Row Count: ${info.row_count}\n`;
260
+ output += `Column Count: ${info.column_count}\n`;
261
+
262
+ if (info.sql) {
263
+ output += `\nCreate Statement:\n${info.sql}\n`;
264
+ }
265
+
266
+ output += '\n\nJSON representation:\n';
267
+ output += JSON.stringify(info, null, 2);
268
+
269
+ return output;
270
+ }
271
+
272
+ /**
273
+ * Format query plan results
274
+ * @param {Array} plan - Query execution plan
275
+ * @returns {string} Formatted query plan
276
+ */
277
+ export function formatQueryPlan(plan) {
278
+ if (!plan || plan.length === 0) {
279
+ return 'No query plan available.';
280
+ }
281
+
282
+ let output = 'Query Execution Plan:\n';
283
+ output += '='.repeat(40) + '\n\n';
284
+
285
+ for (const step of plan) {
286
+ const indent = ' '.repeat(step.id || 0);
287
+ output += `${indent}${step.detail || step.notused || 'N/A'}\n`;
288
+ }
289
+
290
+ output += '\n\nJSON representation:\n';
291
+ output += JSON.stringify(plan, null, 2);
292
+
293
+ return output;
294
+ }
295
+
296
+ /**
297
+ * Format table statistics results
298
+ * @param {Object} stats - Table statistics
299
+ * @returns {string} Formatted statistics
300
+ */
301
+ export function formatTableStatistics(stats) {
302
+ let output = `Table Statistics: ${stats.table_name}\n`;
303
+ output += '='.repeat(40) + '\n\n';
304
+
305
+ output += `Total Rows: ${stats.total_rows}\n`;
306
+ output += `Column Count: ${stats.column_count}\n\n`;
307
+
308
+ if (stats.columns && stats.columns.length > 0) {
309
+ output += 'Column Statistics:\n';
310
+ for (const col of stats.columns) {
311
+ output += `\n ${col.name} (${col.type}):\n`;
312
+ output += ` Distinct Values: ${col.distinct_count}\n`;
313
+ output += ` Null Count: ${col.null_count}\n`;
314
+ if (col.min_value !== undefined) output += ` Min: ${col.min_value}\n`;
315
+ if (col.max_value !== undefined) output += ` Max: ${col.max_value}\n`;
316
+ if (col.avg_value !== undefined) output += ` Avg: ${col.avg_value}\n`;
317
+ }
318
+ }
319
+
320
+ output += '\n\nJSON representation:\n';
321
+ output += JSON.stringify(stats, null, 2);
322
+
323
+ return output;
324
+ }
325
+
326
+ /**
327
+ * Format sample data results
328
+ * @param {Object} sample - Sample data
329
+ * @returns {string} Formatted sample data
330
+ */
331
+ export function formatSampleData(sample) {
332
+ let output = `Sample Data from ${sample.table_name}\n`;
333
+ output += '='.repeat(40) + '\n\n';
334
+
335
+ output += `Showing ${sample.row_count} row(s) (limit: ${sample.limit}, offset: ${sample.offset})\n\n`;
336
+
337
+ if (sample.rows && sample.rows.length > 0) {
338
+ // Add column headers
339
+ output += `Columns: ${sample.columns.join(' | ')}\n`;
340
+ output += '-'.repeat(sample.columns.join(' | ').length) + '\n';
341
+
342
+ // Add rows
343
+ for (const row of sample.rows) {
344
+ const values = sample.columns.map(col => formatCellValue(row[col]));
345
+ output += values.join(' | ') + '\n';
346
+ }
347
+ } else {
348
+ output += 'No rows in sample.\n';
349
+ }
350
+
351
+ output += '\n\nJSON representation:\n';
352
+ output += JSON.stringify(sample, null, 2);
353
+
354
+ return output;
355
+ }
356
+
357
+ /**
358
+ * Format column statistics results
359
+ * @param {Array} stats - Array of column statistics
360
+ * @returns {string} Formatted column statistics
361
+ */
362
+ export function formatColumnStatistics(stats) {
363
+ if (!stats || stats.length === 0) {
364
+ return 'No column statistics available.';
365
+ }
366
+
367
+ let output = `Column Statistics (${stats.length} column(s)):\n`;
368
+ output += '='.repeat(40) + '\n\n';
369
+
370
+ for (const col of stats) {
371
+ output += `Column: ${col.table_name}.${col.column_name} (${col.column_type})\n`;
372
+ output += ` Distinct Values: ${col.distinct_count}\n`;
373
+ output += ` Null Count: ${col.null_count}\n`;
374
+ output += ` Non-Null Count: ${col.non_null_count}\n`;
375
+ if (col.min_value !== undefined) output += ` Min: ${col.min_value}\n`;
376
+ if (col.max_value !== undefined) output += ` Max: ${col.max_value}\n`;
377
+ if (col.avg_value !== undefined) output += ` Avg: ${col.avg_value}\n`;
378
+ if (col.sample_values && col.sample_values.length > 0) {
379
+ output += ` Sample Values: ${col.sample_values.map(v => formatCellValue(v)).join(', ')}\n`;
380
+ }
381
+ output += '\n';
382
+ }
383
+
384
+ output += 'JSON representation:\n';
385
+ output += JSON.stringify(stats, null, 2);
386
+
387
+ return output;
388
+ }
389
+
390
+ /**
391
+ * Format search results
392
+ * @param {Array} results - Search results
393
+ * @param {string} type - Type of search ('tables' or 'columns')
394
+ * @returns {string} Formatted search results
395
+ */
396
+ export function formatSearchResults(results, type) {
397
+ if (!results || results.length === 0) {
398
+ return `No ${type} found matching the pattern.`;
399
+ }
400
+
401
+ let output = `Found ${results.length} matching ${type}:\n\n`;
402
+
403
+ if (type === 'tables') {
404
+ for (const table of results) {
405
+ output += `- ${table.name} (${table.type})\n`;
406
+ }
407
+ } else if (type === 'columns') {
408
+ for (const col of results) {
409
+ output += `- ${col.table_name}.${col.column_name} (${col.column_type})`;
410
+ if (col.is_primary_key) output += ' [PK]';
411
+ if (col.is_nullable) output += ' [NULL]';
412
+ output += '\n';
413
+ }
414
+ }
415
+
416
+ output += '\n\nJSON representation:\n';
417
+ output += JSON.stringify(results, null, 2);
418
+
419
+ return output;
420
+ }
421
+
422
+ /**
423
+ * Format related tables results
424
+ * @param {Object} related - Related tables information
425
+ * @returns {string} Formatted related tables
426
+ */
427
+ export function formatRelatedTables(related) {
428
+ let output = `Related Tables for: ${related.table_name}\n`;
429
+ output += '='.repeat(40) + '\n\n';
430
+
431
+ if (related.references_tables && related.references_tables.length > 0) {
432
+ output += `References (${related.references_tables.length}):\n`;
433
+ for (const table of related.references_tables) {
434
+ output += ` - ${table}\n`;
435
+ }
436
+ output += '\n';
437
+ } else {
438
+ output += 'No outgoing references.\n\n';
439
+ }
440
+
441
+ if (related.referenced_by_tables && related.referenced_by_tables.length > 0) {
442
+ output += `Referenced By (${related.referenced_by_tables.length}):\n`;
443
+ for (const table of related.referenced_by_tables) {
444
+ output += ` - ${table}\n`;
445
+ }
446
+ output += '\n';
447
+ } else {
448
+ output += 'No incoming references.\n\n';
449
+ }
450
+
451
+ if (related.outgoing_foreign_keys && related.outgoing_foreign_keys.length > 0) {
452
+ output += 'Outgoing Foreign Keys:\n';
453
+ for (const fk of related.outgoing_foreign_keys) {
454
+ output += ` - ${fk.from} -> ${fk.table}.${fk.to}\n`;
455
+ }
456
+ output += '\n';
457
+ }
458
+
459
+ if (related.incoming_foreign_keys && related.incoming_foreign_keys.length > 0) {
460
+ output += 'Incoming Foreign Keys:\n';
461
+ for (const fk of related.incoming_foreign_keys) {
462
+ output += ` - ${fk.table}.${fk.from} -> ${fk.to}\n`;
463
+ }
464
+ }
465
+
466
+ output += '\n\nJSON representation:\n';
467
+ output += JSON.stringify(related, null, 2);
468
+
469
+ return output;
470
+ }