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
package/src/utils/validators.js
CHANGED
|
@@ -1,58 +1,147 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation Utilities
|
|
3
|
-
* Input validation and sanitization functions
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getDatabasePath } from '../config/environment.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Validate that arguments is a valid object
|
|
10
|
-
* @param {any} args - Arguments to validate
|
|
11
|
-
* @throws {Error} If args is not a valid object
|
|
12
|
-
*/
|
|
13
|
-
export function validateArguments(args) {
|
|
14
|
-
if (!args || typeof args !== 'object') {
|
|
15
|
-
throw new Error('Invalid arguments: arguments must be an object');
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Validate query parameter
|
|
21
|
-
* @param {any} query - Query to validate
|
|
22
|
-
* @throws {Error} If query is invalid
|
|
23
|
-
*/
|
|
24
|
-
export function validateQuery(query) {
|
|
25
|
-
if (!query || typeof query !== 'string') {
|
|
26
|
-
throw new Error('query is required and must be a string');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Validate and resolve database path
|
|
32
|
-
* Uses provided path or falls back to environment variable
|
|
33
|
-
* @param {string|undefined} providedPath - Database path from arguments
|
|
34
|
-
* @returns {string} Resolved database path
|
|
35
|
-
* @throws {Error} If no valid path is available
|
|
36
|
-
*/
|
|
37
|
-
export function resolveDatabasePath(providedPath) {
|
|
38
|
-
const dbPath = providedPath || getDatabasePath();
|
|
39
|
-
|
|
40
|
-
if (!dbPath || typeof dbPath !== 'string') {
|
|
41
|
-
throw new Error(
|
|
42
|
-
'database_path is required. Provide it as a parameter or set SQLCIPHER_DATABASE_PATH environment variable.'
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return dbPath;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Validate database path parameter for HTTP requests
|
|
51
|
-
* @param {any} database_path - Database path to validate
|
|
52
|
-
* @throws {Error} If database_path is invalid
|
|
53
|
-
*/
|
|
54
|
-
export function validateDatabasePath(database_path) {
|
|
55
|
-
if (!database_path || typeof database_path !== 'string') {
|
|
56
|
-
throw new Error('database_path is required and must be a string');
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Validation Utilities
|
|
3
|
+
* Input validation and sanitization functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getDatabasePath } from '../config/environment.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate that arguments is a valid object
|
|
10
|
+
* @param {any} args - Arguments to validate
|
|
11
|
+
* @throws {Error} If args is not a valid object
|
|
12
|
+
*/
|
|
13
|
+
export function validateArguments(args) {
|
|
14
|
+
if (!args || typeof args !== 'object') {
|
|
15
|
+
throw new Error('Invalid arguments: arguments must be an object');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate query parameter
|
|
21
|
+
* @param {any} query - Query to validate
|
|
22
|
+
* @throws {Error} If query is invalid
|
|
23
|
+
*/
|
|
24
|
+
export function validateQuery(query) {
|
|
25
|
+
if (!query || typeof query !== 'string') {
|
|
26
|
+
throw new Error('query is required and must be a string');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate and resolve database path
|
|
32
|
+
* Uses provided path or falls back to environment variable
|
|
33
|
+
* @param {string|undefined} providedPath - Database path from arguments
|
|
34
|
+
* @returns {string} Resolved database path
|
|
35
|
+
* @throws {Error} If no valid path is available
|
|
36
|
+
*/
|
|
37
|
+
export function resolveDatabasePath(providedPath) {
|
|
38
|
+
const dbPath = providedPath || getDatabasePath();
|
|
39
|
+
|
|
40
|
+
if (!dbPath || typeof dbPath !== 'string') {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'database_path is required. Provide it as a parameter or set SQLCIPHER_DATABASE_PATH environment variable.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return dbPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate database path parameter for HTTP requests
|
|
51
|
+
* @param {any} database_path - Database path to validate
|
|
52
|
+
* @throws {Error} If database_path is invalid
|
|
53
|
+
*/
|
|
54
|
+
export function validateDatabasePath(database_path) {
|
|
55
|
+
if (!database_path || typeof database_path !== 'string') {
|
|
56
|
+
throw new Error('database_path is required and must be a string');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate table name parameter
|
|
62
|
+
* @param {any} tableName - Table name to validate
|
|
63
|
+
* @throws {Error} If tableName is invalid
|
|
64
|
+
*/
|
|
65
|
+
export function validateTableName(tableName) {
|
|
66
|
+
if (!tableName) {
|
|
67
|
+
throw new Error('table_name is required');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof tableName !== 'string' && !Array.isArray(tableName)) {
|
|
71
|
+
throw new Error('table_name must be a string or array of strings');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(tableName)) {
|
|
75
|
+
if (tableName.length === 0) {
|
|
76
|
+
throw new Error('table_name array cannot be empty');
|
|
77
|
+
}
|
|
78
|
+
tableName.forEach((name, index) => {
|
|
79
|
+
if (typeof name !== 'string') {
|
|
80
|
+
throw new Error(`table_name[${index}] must be a string`);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate column name parameter
|
|
88
|
+
* @param {any} columnName - Column name to validate
|
|
89
|
+
* @throws {Error} If columnName is invalid
|
|
90
|
+
*/
|
|
91
|
+
export function validateColumnName(columnName) {
|
|
92
|
+
if (!columnName) {
|
|
93
|
+
throw new Error('column_name is required');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof columnName !== 'string' && !Array.isArray(columnName)) {
|
|
97
|
+
throw new Error('column_name must be a string or array of strings');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Array.isArray(columnName)) {
|
|
101
|
+
if (columnName.length === 0) {
|
|
102
|
+
throw new Error('column_name array cannot be empty');
|
|
103
|
+
}
|
|
104
|
+
columnName.forEach((name, index) => {
|
|
105
|
+
if (typeof name !== 'string') {
|
|
106
|
+
throw new Error(`column_name[${index}] must be a string`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate pattern parameter for search operations
|
|
114
|
+
* @param {any} pattern - Pattern to validate
|
|
115
|
+
* @throws {Error} If pattern is invalid
|
|
116
|
+
*/
|
|
117
|
+
export function validatePattern(pattern) {
|
|
118
|
+
if (!pattern || typeof pattern !== 'string') {
|
|
119
|
+
throw new Error('pattern is required and must be a string');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validate numeric parameter
|
|
125
|
+
* @param {any} value - Value to validate
|
|
126
|
+
* @param {string} paramName - Parameter name for error messages
|
|
127
|
+
* @param {number} min - Minimum allowed value
|
|
128
|
+
* @param {number} max - Maximum allowed value
|
|
129
|
+
* @returns {number} Validated number
|
|
130
|
+
* @throws {Error} If value is invalid
|
|
131
|
+
*/
|
|
132
|
+
export function validateNumericParameter(value, paramName, min = 0, max = Number.MAX_SAFE_INTEGER) {
|
|
133
|
+
if (value === undefined || value === null) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const num = Number(value);
|
|
138
|
+
if (isNaN(num)) {
|
|
139
|
+
throw new Error(`${paramName} must be a number`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (num < min || num > max) {
|
|
143
|
+
throw new Error(`${paramName} must be between ${min} and ${max}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return num;
|
|
147
|
+
}
|
package/lib/database.js
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import sqlcipher from '@journeyapps/sqlcipher';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
|
|
4
|
-
// Extract Database from the sqlcipher module object
|
|
5
|
-
const Database = sqlcipher.Database;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Connects to a SQLite database (encrypted or unencrypted)
|
|
9
|
-
* Supports both SQLCipher-encrypted and plain SQLite databases
|
|
10
|
-
*
|
|
11
|
-
* @param {string} dbPath - Path to the database file
|
|
12
|
-
* @param {string} [password] - Optional database password (for encrypted databases)
|
|
13
|
-
* @returns {Promise<Database>} Database connection instance
|
|
14
|
-
* @throws {Error} If database file doesn't exist or connection fails
|
|
15
|
-
*/
|
|
16
|
-
export function connectDatabase(dbPath, password) {
|
|
17
|
-
return new Promise((resolve, reject) => {
|
|
18
|
-
// Validate database path exists
|
|
19
|
-
if (!fs.existsSync(dbPath)) {
|
|
20
|
-
return reject(new Error(`Database file not found: ${dbPath}`));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let db = null;
|
|
24
|
-
// Open database connection with callback
|
|
25
|
-
db = new Database(dbPath, (err) => {
|
|
26
|
-
if (err) {
|
|
27
|
-
return reject(new Error(`Failed to open database: ${err.message}`));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// If no password provided, treat as unencrypted SQLite database
|
|
31
|
-
if (!password || password.trim() === '') {
|
|
32
|
-
// Verify the database is accessible by running a simple query
|
|
33
|
-
db.get('SELECT 1', (getErr, row) => {
|
|
34
|
-
if (getErr) {
|
|
35
|
-
db.close((closeErr) => {
|
|
36
|
-
// Ignore close errors
|
|
37
|
-
});
|
|
38
|
-
return reject(new Error(`Failed to verify database: ${getErr.message}`));
|
|
39
|
-
}
|
|
40
|
-
resolve(db);
|
|
41
|
-
});
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Password provided - treat as encrypted SQLCipher database
|
|
46
|
-
// Explicitly set SQLCipher 3 compatibility mode
|
|
47
|
-
// This ensures SQLCipher 3 defaults are used:
|
|
48
|
-
// - Page size: 1024 bytes
|
|
49
|
-
// - PBKDF2 iterations: 64,000
|
|
50
|
-
// - KDF algorithm: PBKDF2-HMAC-SHA1
|
|
51
|
-
// - HMAC algorithm: HMAC-SHA1
|
|
52
|
-
db.exec('PRAGMA cipher_compatibility = 3', (compatErr) => {
|
|
53
|
-
if (compatErr) {
|
|
54
|
-
db.close((closeErr) => {
|
|
55
|
-
// Ignore close errors
|
|
56
|
-
});
|
|
57
|
-
return reject(new Error(`Failed to set SQLCipher 3 compatibility: ${compatErr.message}`));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Set SQLCipher 3 default encryption settings
|
|
61
|
-
// PRAGMA key sets the encryption key using SQLCipher 3 defaults
|
|
62
|
-
// PRAGMA key does NOT support parameterized queries, so we must embed the password directly
|
|
63
|
-
// Escape single quotes in password for SQL (double them) and escape backslashes
|
|
64
|
-
const escapedPassword = password.replace(/\\/g, '\\\\').replace(/'/g, "''");
|
|
65
|
-
|
|
66
|
-
// Use db.exec() with callback for PRAGMA key
|
|
67
|
-
db.exec(`PRAGMA key = '${escapedPassword}'`, (execErr) => {
|
|
68
|
-
if (execErr) {
|
|
69
|
-
db.close((closeErr) => {
|
|
70
|
-
// Ignore close errors
|
|
71
|
-
});
|
|
72
|
-
return reject(new Error(`Failed to set encryption key: ${execErr.message}`));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Verify the database is accessible by running a simple query
|
|
76
|
-
// This will throw an error if the password is incorrect
|
|
77
|
-
db.get('SELECT 1', (getErr, row) => {
|
|
78
|
-
if (getErr) {
|
|
79
|
-
db.close((closeErr) => {
|
|
80
|
-
// Ignore close errors
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (getErr.message.includes('file is not a database') ||
|
|
84
|
-
getErr.message.includes('malformed database') ||
|
|
85
|
-
getErr.code === 'SQLITE_NOTADB') {
|
|
86
|
-
return reject(new Error('Invalid password or database is corrupted'));
|
|
87
|
-
}
|
|
88
|
-
return reject(new Error(`Failed to verify database: ${getErr.message}`));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
resolve(db);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Validates that a SQL query is a SELECT query (read-only)
|
|
101
|
-
*
|
|
102
|
-
* @param {string} query - SQL query string
|
|
103
|
-
* @returns {boolean} True if query is a SELECT query
|
|
104
|
-
* @throws {Error} If query is not a SELECT query
|
|
105
|
-
*/
|
|
106
|
-
function validateSelectQuery(query) {
|
|
107
|
-
if (!query || typeof query !== 'string') {
|
|
108
|
-
throw new Error('Query must be a non-empty string');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Trim and normalize whitespace
|
|
112
|
-
const normalizedQuery = query.trim().replace(/\s+/g, ' ');
|
|
113
|
-
|
|
114
|
-
// Check if query starts with SELECT (case-insensitive)
|
|
115
|
-
if (!normalizedQuery.match(/^SELECT\s+/i)) {
|
|
116
|
-
throw new Error('Only SELECT queries are allowed (read-only mode)');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Additional check: ensure no DDL or DML keywords are present
|
|
120
|
-
const forbiddenKeywords = [
|
|
121
|
-
'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER',
|
|
122
|
-
'TRUNCATE', 'REPLACE', 'PRAGMA'
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
const upperQuery = normalizedQuery.toUpperCase();
|
|
126
|
-
for (const keyword of forbiddenKeywords) {
|
|
127
|
-
// Check for keyword followed by space or semicolon (to avoid false positives)
|
|
128
|
-
const regex = new RegExp(`\\b${keyword}\\s+`, 'i');
|
|
129
|
-
if (regex.test(normalizedQuery) && keyword !== 'SELECT') {
|
|
130
|
-
throw new Error(`Query contains forbidden keyword: ${keyword}. Only SELECT queries are allowed.`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Executes a SELECT query on the database and returns results
|
|
139
|
-
*
|
|
140
|
-
* @param {Database} db - Database connection instance
|
|
141
|
-
* @param {string} query - SQL SELECT query to execute
|
|
142
|
-
* @returns {Promise<Object>} Query results with columns, rows, and rowCount
|
|
143
|
-
* @throws {Error} If query is invalid or execution fails
|
|
144
|
-
*/
|
|
145
|
-
export function executeQuery(db, query) {
|
|
146
|
-
return new Promise((resolve, reject) => {
|
|
147
|
-
// Validate query is a SELECT query
|
|
148
|
-
try {
|
|
149
|
-
validateSelectQuery(query);
|
|
150
|
-
} catch (validationError) {
|
|
151
|
-
return reject(validationError);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Prepare and execute the query with callback
|
|
155
|
-
const statement = db.prepare(query, (prepareErr) => {
|
|
156
|
-
if (prepareErr) {
|
|
157
|
-
return reject(new Error(`Query preparation failed: ${prepareErr.message}`));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Execute query with callback - statement.all() requires a callback
|
|
161
|
-
statement.all((allErr, rows) => {
|
|
162
|
-
if (allErr) {
|
|
163
|
-
statement.finalize();
|
|
164
|
-
if (allErr.message.includes('no such table')) {
|
|
165
|
-
return reject(new Error(`Table not found: ${allErr.message}`));
|
|
166
|
-
} else if (allErr.message.includes('no such column')) {
|
|
167
|
-
return reject(new Error(`Column not found: ${allErr.message}`));
|
|
168
|
-
} else if (allErr.message.includes('syntax error')) {
|
|
169
|
-
return reject(new Error(`SQL syntax error: ${allErr.message}`));
|
|
170
|
-
}
|
|
171
|
-
return reject(new Error(`Query execution failed: ${allErr.message}`));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Get column names from the first row
|
|
175
|
-
let columns = [];
|
|
176
|
-
if (rows && rows.length > 0) {
|
|
177
|
-
columns = Object.keys(rows[0]);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Finalize the statement
|
|
181
|
-
statement.finalize();
|
|
182
|
-
|
|
183
|
-
resolve({
|
|
184
|
-
columns: columns,
|
|
185
|
-
rows: rows || [],
|
|
186
|
-
rowCount: rows ? rows.length : 0
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Closes a database connection
|
|
195
|
-
* Ensures all statements are finalized before closing
|
|
196
|
-
*
|
|
197
|
-
* @param {Database} db - Database connection instance
|
|
198
|
-
*/
|
|
199
|
-
export function closeConnection(db) {
|
|
200
|
-
if (!db || typeof db.close !== 'function') {
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
// Close with callback to handle any errors gracefully
|
|
206
|
-
db.close((err) => {
|
|
207
|
-
if (err) {
|
|
208
|
-
// Log but don't throw - closing should be best effort
|
|
209
|
-
console.error('Error closing database connection:', err.message);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
} catch (error) {
|
|
213
|
-
// Log but don't throw - closing should be best effort
|
|
214
|
-
console.error('Error closing database connection:', error.message);
|
|
215
|
-
}
|
|
216
|
-
}
|