sqlcipher-mcp-server 1.0.2 → 1.0.3
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/index.js +4 -231
- package/package.json +29 -27
- package/server-http.js +14 -0
- package/src/config/constants.js +40 -0
- package/src/config/environment.js +63 -0
- package/src/handlers/http-handlers.js +89 -0
- package/src/handlers/mcp-handlers.js +69 -0
- package/src/server/http-server.js +54 -0
- package/src/server/mcp-server.js +76 -0
- package/src/services/database-service.js +55 -0
- package/src/utils/errors.js +69 -0
- package/src/utils/formatters.js +64 -0
- package/src/utils/validators.js +58 -0
package/index.js
CHANGED
|
@@ -1,241 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
-
import {
|
|
6
|
-
CallToolRequestSchema,
|
|
7
|
-
ListToolsRequestSchema,
|
|
8
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
-
import { connectDatabase, executeQuery, closeConnection } from './lib/database.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* SQLCipher MCP Server
|
|
13
|
-
* Provides read-only access to SQLCipher-encrypted SQLite databases
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// Get database configuration from environment variables
|
|
17
|
-
const DB_PASSWORD = process.env.SQLCIPHER_PASSWORD;
|
|
18
|
-
const DB_PATH = process.env.SQLCIPHER_DATABASE_PATH;
|
|
19
|
-
|
|
20
|
-
// Create MCP server instance
|
|
21
|
-
const server = new Server(
|
|
22
|
-
{
|
|
23
|
-
name: 'sqlcipher-mcp-server',
|
|
24
|
-
version: '1.0.0',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
capabilities: {
|
|
28
|
-
tools: {},
|
|
29
|
-
},
|
|
30
|
-
}
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* List available tools
|
|
35
|
-
*/
|
|
36
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
37
|
-
return {
|
|
38
|
-
tools: [
|
|
39
|
-
{
|
|
40
|
-
name: 'execute_query',
|
|
41
|
-
description: 'Execute a SELECT query on a SQLCipher-encrypted SQLite database. Only read-only queries are allowed. Database path can be provided as parameter or via SQLCIPHER_DATABASE_PATH environment variable.',
|
|
42
|
-
inputSchema: {
|
|
43
|
-
type: 'object',
|
|
44
|
-
properties: {
|
|
45
|
-
database_path: {
|
|
46
|
-
type: 'string',
|
|
47
|
-
description: 'Path to the SQLCipher database file (optional if SQLCIPHER_DATABASE_PATH is set)',
|
|
48
|
-
},
|
|
49
|
-
query: {
|
|
50
|
-
type: 'string',
|
|
51
|
-
description: 'SQL SELECT query to execute (read-only)',
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
required: ['query'],
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
};
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Handle tool execution requests
|
|
63
|
-
*/
|
|
64
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
65
|
-
const { name, arguments: args } = request.params;
|
|
66
|
-
|
|
67
|
-
if (name === 'execute_query') {
|
|
68
|
-
try {
|
|
69
|
-
// Validate required parameters
|
|
70
|
-
if (!args || typeof args !== 'object') {
|
|
71
|
-
throw new Error('Invalid arguments: arguments must be an object');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const { database_path, query } = args;
|
|
75
|
-
|
|
76
|
-
// Validate query
|
|
77
|
-
if (!query || typeof query !== 'string') {
|
|
78
|
-
throw new Error('query is required and must be a string');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Determine database path: use parameter if provided, otherwise use environment variable
|
|
82
|
-
const dbPath = database_path || DB_PATH;
|
|
83
|
-
|
|
84
|
-
// Validate database_path
|
|
85
|
-
if (!dbPath || typeof dbPath !== 'string') {
|
|
86
|
-
throw new Error(
|
|
87
|
-
'database_path is required. Provide it as a parameter or set SQLCIPHER_DATABASE_PATH environment variable.'
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Connect to database (password is optional - will work with unencrypted databases)
|
|
92
|
-
let db = null;
|
|
93
|
-
try {
|
|
94
|
-
db = await connectDatabase(dbPath, DB_PASSWORD);
|
|
95
|
-
} catch (error) {
|
|
96
|
-
return {
|
|
97
|
-
content: [
|
|
98
|
-
{
|
|
99
|
-
type: 'text',
|
|
100
|
-
text: `Failed to connect to database: ${error.message}`,
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
isError: true,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Execute query
|
|
108
|
-
try {
|
|
109
|
-
const result = await executeQuery(db, query);
|
|
110
|
-
|
|
111
|
-
// Format results for response
|
|
112
|
-
const responseText = formatQueryResults(result);
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
content: [
|
|
116
|
-
{
|
|
117
|
-
type: 'text',
|
|
118
|
-
text: responseText,
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
};
|
|
122
|
-
} catch (error) {
|
|
123
|
-
return {
|
|
124
|
-
content: [
|
|
125
|
-
{
|
|
126
|
-
type: 'text',
|
|
127
|
-
text: `Query execution failed: ${error.message}`,
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
isError: true,
|
|
131
|
-
};
|
|
132
|
-
} finally {
|
|
133
|
-
// Always close the database connection
|
|
134
|
-
closeConnection(db);
|
|
135
|
-
}
|
|
136
|
-
} catch (error) {
|
|
137
|
-
return {
|
|
138
|
-
content: [
|
|
139
|
-
{
|
|
140
|
-
type: 'text',
|
|
141
|
-
text: `Error: ${error.message}`,
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
isError: true,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
return {
|
|
149
|
-
content: [
|
|
150
|
-
{
|
|
151
|
-
type: 'text',
|
|
152
|
-
text: `Unknown tool: ${name}`,
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
isError: true,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
3
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* @param {Object} result - Query result object with columns, rows, and rowCount
|
|
164
|
-
* @returns {string} Formatted result string
|
|
4
|
+
* SQLCipher MCP Server - Entry Point
|
|
5
|
+
* Provides read-only access to SQLCipher-encrypted SQLite databases via MCP
|
|
165
6
|
*/
|
|
166
|
-
function formatQueryResults(result) {
|
|
167
|
-
const { columns, rows, rowCount } = result;
|
|
168
|
-
|
|
169
|
-
if (rowCount === 0) {
|
|
170
|
-
return `Query executed successfully. No rows returned.\nColumns: ${columns.join(', ')}`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Build table-like output
|
|
174
|
-
let output = `Query executed successfully. ${rowCount} row(s) returned.\n\n`;
|
|
175
|
-
|
|
176
|
-
// Add column headers
|
|
177
|
-
output += `Columns: ${columns.join(' | ')}\n`;
|
|
178
|
-
output += '-'.repeat(columns.join(' | ').length) + '\n';
|
|
179
|
-
|
|
180
|
-
// Add rows (limit to first 1000 rows for display)
|
|
181
|
-
const displayRows = rows.slice(0, 1000);
|
|
182
|
-
for (const row of displayRows) {
|
|
183
|
-
const values = columns.map(col => {
|
|
184
|
-
const value = row[col];
|
|
185
|
-
// Handle null/undefined
|
|
186
|
-
if (value === null || value === undefined) {
|
|
187
|
-
return 'NULL';
|
|
188
|
-
}
|
|
189
|
-
// Convert to string and truncate long values
|
|
190
|
-
const str = String(value);
|
|
191
|
-
return str.length > 50 ? str.substring(0, 47) + '...' : str;
|
|
192
|
-
});
|
|
193
|
-
output += values.join(' | ') + '\n';
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (rows.length > 1000) {
|
|
197
|
-
output += `\n... (showing first 1000 of ${rowCount} rows)`;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Add JSON representation for programmatic access
|
|
201
|
-
output += '\n\nJSON representation:\n';
|
|
202
|
-
output += JSON.stringify(result, null, 2);
|
|
203
|
-
|
|
204
|
-
return output;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Start the MCP server
|
|
209
|
-
*/
|
|
210
|
-
async function main() {
|
|
211
|
-
// Check if configuration is set (warn but don't fail - might be set later)
|
|
212
|
-
const warnings = [];
|
|
213
|
-
|
|
214
|
-
if (!DB_PASSWORD) {
|
|
215
|
-
warnings.push('SQLCIPHER_PASSWORD environment variable is not set. Database connections may fail if password is required.');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!DB_PATH) {
|
|
219
|
-
warnings.push('SQLCIPHER_DATABASE_PATH environment variable is not set. Database path must be provided in each query.');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (warnings.length > 0) {
|
|
223
|
-
console.error('Warning: ' + warnings.join(' '));
|
|
224
|
-
} else {
|
|
225
|
-
console.error('Configuration loaded: Database path and password set via environment variables.');
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Create stdio transport
|
|
229
|
-
const transport = new StdioServerTransport();
|
|
230
|
-
|
|
231
|
-
// Connect server to transport
|
|
232
|
-
await server.connect(transport);
|
|
233
7
|
|
|
234
|
-
|
|
235
|
-
}
|
|
8
|
+
import { startMcpServer } from './src/server/mcp-server.js';
|
|
236
9
|
|
|
237
10
|
// Start the server
|
|
238
|
-
|
|
11
|
+
startMcpServer().catch((error) => {
|
|
239
12
|
console.error('Fatal error starting server:', error);
|
|
240
13
|
process.exit(1);
|
|
241
14
|
});
|
package/package.json
CHANGED
|
@@ -1,54 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sqlcipher-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "MCP Server for querying SQLCipher-encrypted SQLite databases",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"sqlcipher",
|
|
8
|
+
"sqlite",
|
|
9
|
+
"database",
|
|
10
|
+
"model-context-protocol",
|
|
11
|
+
"encrypted-database",
|
|
12
|
+
"cursor-ide"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/sathiraguruge/SQLLiteMCP",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/sathiraguruge/SQLLiteMCP/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/sathiraguruge/SQLLiteMCP.git"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Sathira Guruge",
|
|
5
24
|
"main": "index.js",
|
|
6
|
-
"type": "module",
|
|
7
25
|
"bin": {
|
|
8
26
|
"sqlcipher-mcp-server": "./index.js"
|
|
9
27
|
},
|
|
28
|
+
"type": "module",
|
|
10
29
|
"files": [
|
|
11
30
|
"index.js",
|
|
31
|
+
"server-http.js",
|
|
32
|
+
"src/**/*",
|
|
12
33
|
"lib/**/*",
|
|
13
34
|
"README.md",
|
|
14
35
|
"LICENSE"
|
|
15
36
|
],
|
|
16
37
|
"scripts": {
|
|
17
38
|
"start": "node index.js",
|
|
18
|
-
"start:http": "node server-http.js",
|
|
19
|
-
"test:http": "node server-http.js",
|
|
20
39
|
"dev": "nodemon index.js",
|
|
40
|
+
"start:http": "node server-http.js",
|
|
21
41
|
"dev:http": "nodemon server-http.js",
|
|
42
|
+
"test:http": "node server-http.js",
|
|
22
43
|
"prepublishOnly": "npm test || exit 0"
|
|
23
44
|
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"mcp",
|
|
26
|
-
"sqlcipher",
|
|
27
|
-
"sqlite",
|
|
28
|
-
"database",
|
|
29
|
-
"model-context-protocol",
|
|
30
|
-
"encrypted-database",
|
|
31
|
-
"cursor-ide"
|
|
32
|
-
],
|
|
33
|
-
"author": "Sathira Guruge",
|
|
34
|
-
"license": "MIT",
|
|
35
|
-
"repository": {
|
|
36
|
-
"type": "git",
|
|
37
|
-
"url": "https://github.com/sathiraguruge/SQLLiteMCP.git"
|
|
38
|
-
},
|
|
39
|
-
"bugs": {
|
|
40
|
-
"url": "https://github.com/sathiraguruge/SQLLiteMCP/issues"
|
|
41
|
-
},
|
|
42
|
-
"homepage": "https://github.com/sathiraguruge/SQLLiteMCP",
|
|
43
|
-
"engines": {
|
|
44
|
-
"node": ">=18.0.0"
|
|
45
|
-
},
|
|
46
45
|
"dependencies": {
|
|
47
46
|
"@journeyapps/sqlcipher": "^5.1.1",
|
|
48
|
-
"@modelcontextprotocol/sdk": "^
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
49
48
|
"express": "^4.22.1"
|
|
50
49
|
},
|
|
51
50
|
"devDependencies": {
|
|
52
51
|
"nodemon": "^3.1.11"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
53
55
|
}
|
|
54
56
|
}
|
package/server-http.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SQLCipher MCP HTTP Server - Entry Point
|
|
5
|
+
* HTTP wrapper for testing the SQLCipher MCP Server with tools like Postman
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { startHttpServer } from './src/server/http-server.js';
|
|
9
|
+
|
|
10
|
+
// Start the server
|
|
11
|
+
startHttpServer().catch((error) => {
|
|
12
|
+
console.error('Fatal error starting HTTP server:', error);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application Constants
|
|
3
|
+
* Central location for all application constants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const SERVER_CONFIG = {
|
|
7
|
+
name: 'sqlcipher-mcp-server',
|
|
8
|
+
version: '1.0.2',
|
|
9
|
+
description: 'MCP Server for querying SQLCipher-encrypted SQLite databases',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const QUERY_CONFIG = {
|
|
13
|
+
maxDisplayRows: 1000,
|
|
14
|
+
maxValueLength: 50,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const HTTP_CONFIG = {
|
|
18
|
+
defaultPort: 3000,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const TOOL_DEFINITIONS = {
|
|
22
|
+
execute_query: {
|
|
23
|
+
name: 'execute_query',
|
|
24
|
+
description: 'Execute a SELECT query on a SQLCipher-encrypted SQLite database. Only read-only queries are allowed. Database path can be provided as parameter or via SQLCIPHER_DATABASE_PATH environment variable.',
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
database_path: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Path to the SQLCipher database file (optional if SQLCIPHER_DATABASE_PATH is set)',
|
|
31
|
+
},
|
|
32
|
+
query: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'SQL SELECT query to execute (read-only)',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['query'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Configuration
|
|
3
|
+
* Manages environment variables and application configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get database password from environment variable
|
|
8
|
+
* @returns {string|undefined} Database password or undefined if not set
|
|
9
|
+
*/
|
|
10
|
+
export function getDatabasePassword() {
|
|
11
|
+
return process.env.SQLCIPHER_PASSWORD;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get default database path from environment variable
|
|
16
|
+
* @returns {string|undefined} Database path or undefined if not set
|
|
17
|
+
*/
|
|
18
|
+
export function getDatabasePath() {
|
|
19
|
+
return process.env.SQLCIPHER_DATABASE_PATH;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get HTTP server port from environment variable
|
|
24
|
+
* @param {number} defaultPort - Default port to use if not set
|
|
25
|
+
* @returns {number} Port number
|
|
26
|
+
*/
|
|
27
|
+
export function getPort(defaultPort = 3000) {
|
|
28
|
+
return process.env.PORT || defaultPort;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if password is configured
|
|
33
|
+
* @returns {boolean} True if password is set
|
|
34
|
+
*/
|
|
35
|
+
export function isPasswordConfigured() {
|
|
36
|
+
return !!getDatabasePassword();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if database path is configured
|
|
41
|
+
* @returns {boolean} True if database path is set
|
|
42
|
+
*/
|
|
43
|
+
export function isDatabasePathConfigured() {
|
|
44
|
+
return !!getDatabasePath();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get configuration warnings for missing environment variables
|
|
49
|
+
* @returns {string[]} Array of warning messages
|
|
50
|
+
*/
|
|
51
|
+
export function getConfigurationWarnings() {
|
|
52
|
+
const warnings = [];
|
|
53
|
+
|
|
54
|
+
if (!isPasswordConfigured()) {
|
|
55
|
+
warnings.push('SQLCIPHER_PASSWORD environment variable is not set. Database connections may fail if password is required.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!isDatabasePathConfigured()) {
|
|
59
|
+
warnings.push('SQLCIPHER_DATABASE_PATH environment variable is not set. Database path must be provided in each query.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return warnings;
|
|
63
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Route Handlers
|
|
3
|
+
* Handlers for HTTP API endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SERVER_CONFIG } from '../config/constants.js';
|
|
7
|
+
import { getDatabasePassword, isPasswordConfigured } from '../config/environment.js';
|
|
8
|
+
import { validateDatabasePath, validateQuery } from '../utils/validators.js';
|
|
9
|
+
import { executeQueryOnDatabase } from '../services/database-service.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle health check endpoint
|
|
13
|
+
* @param {Object} req - Express request object
|
|
14
|
+
* @param {Object} res - Express response object
|
|
15
|
+
*/
|
|
16
|
+
export function handleHealthCheck(req, res) {
|
|
17
|
+
res.json({
|
|
18
|
+
status: 'ok',
|
|
19
|
+
message: 'SQLCipher MCP HTTP Server is running',
|
|
20
|
+
passwordConfigured: isPasswordConfigured(),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handle server info endpoint
|
|
26
|
+
* @param {Object} req - Express request object
|
|
27
|
+
* @param {Object} res - Express response object
|
|
28
|
+
*/
|
|
29
|
+
export function handleInfo(req, res) {
|
|
30
|
+
res.json({
|
|
31
|
+
name: SERVER_CONFIG.name,
|
|
32
|
+
version: SERVER_CONFIG.version,
|
|
33
|
+
description: 'HTTP wrapper for SQLCipher MCP Server',
|
|
34
|
+
endpoints: {
|
|
35
|
+
health: 'GET /health',
|
|
36
|
+
query: 'POST /api/query',
|
|
37
|
+
info: 'GET /api/info',
|
|
38
|
+
},
|
|
39
|
+
passwordConfigured: isPasswordConfigured(),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handle query execution endpoint
|
|
45
|
+
* @param {Object} req - Express request object
|
|
46
|
+
* @param {Object} res - Express response object
|
|
47
|
+
*/
|
|
48
|
+
export async function handleQuery(req, res) {
|
|
49
|
+
try {
|
|
50
|
+
const { database_path, query } = req.body;
|
|
51
|
+
|
|
52
|
+
// Validate database_path
|
|
53
|
+
try {
|
|
54
|
+
validateDatabasePath(database_path);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return res.status(400).json({ error: error.message });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate query
|
|
60
|
+
try {
|
|
61
|
+
validateQuery(query);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return res.status(400).json({ error: error.message });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get database password
|
|
67
|
+
const password = getDatabasePassword();
|
|
68
|
+
|
|
69
|
+
// Execute query
|
|
70
|
+
try {
|
|
71
|
+
const result = await executeQueryOnDatabase(database_path, password, query);
|
|
72
|
+
|
|
73
|
+
// Return successful response
|
|
74
|
+
res.json({
|
|
75
|
+
success: true,
|
|
76
|
+
data: result,
|
|
77
|
+
message: `Query executed successfully. ${result.rowCount} row(s) returned.`,
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
res.status(400).json({
|
|
81
|
+
error: `Query execution failed: ${error.message}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
res.status(500).json({
|
|
86
|
+
error: `Internal server error: ${error.message}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Handlers
|
|
3
|
+
* Handlers for MCP tool requests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TOOL_DEFINITIONS } from '../config/constants.js';
|
|
7
|
+
import { getDatabasePassword } from '../config/environment.js';
|
|
8
|
+
import { validateArguments, validateQuery, resolveDatabasePath } from '../utils/validators.js';
|
|
9
|
+
import { formatQueryResults } from '../utils/formatters.js';
|
|
10
|
+
import { createMcpErrorResponse, createMcpSuccessResponse } from '../utils/errors.js';
|
|
11
|
+
import { executeQueryOnDatabase } from '../services/database-service.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handle list tools request
|
|
15
|
+
* @returns {Object} List of available tools
|
|
16
|
+
*/
|
|
17
|
+
export function handleListTools() {
|
|
18
|
+
return {
|
|
19
|
+
tools: [TOOL_DEFINITIONS.execute_query],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Handle execute_query tool request
|
|
25
|
+
* @param {Object} args - Tool arguments
|
|
26
|
+
* @param {string} [args.database_path] - Database path (optional if env var set)
|
|
27
|
+
* @param {string} args.query - SQL query to execute
|
|
28
|
+
* @returns {Promise<Object>} MCP response object
|
|
29
|
+
*/
|
|
30
|
+
export async function handleExecuteQuery(args) {
|
|
31
|
+
try {
|
|
32
|
+
// Validate arguments
|
|
33
|
+
validateArguments(args);
|
|
34
|
+
|
|
35
|
+
const { database_path, query } = args;
|
|
36
|
+
|
|
37
|
+
// Validate query
|
|
38
|
+
validateQuery(query);
|
|
39
|
+
|
|
40
|
+
// Resolve database path
|
|
41
|
+
const dbPath = resolveDatabasePath(database_path);
|
|
42
|
+
|
|
43
|
+
// Get database password
|
|
44
|
+
const password = getDatabasePassword();
|
|
45
|
+
|
|
46
|
+
// Execute query
|
|
47
|
+
try {
|
|
48
|
+
const result = await executeQueryOnDatabase(dbPath, password, query);
|
|
49
|
+
|
|
50
|
+
// Format results for response
|
|
51
|
+
const responseText = formatQueryResults(result);
|
|
52
|
+
|
|
53
|
+
return createMcpSuccessResponse(responseText);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return createMcpErrorResponse(`Query execution failed: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return createMcpErrorResponse(`Error: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handle unknown tool request
|
|
64
|
+
* @param {string} toolName - Name of the unknown tool
|
|
65
|
+
* @returns {Object} Error response
|
|
66
|
+
*/
|
|
67
|
+
export function handleUnknownTool(toolName) {
|
|
68
|
+
return createMcpErrorResponse(`Unknown tool: ${toolName}`);
|
|
69
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Server
|
|
3
|
+
* Express HTTP server setup and initialization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { HTTP_CONFIG } from '../config/constants.js';
|
|
8
|
+
import { getPort, isPasswordConfigured } from '../config/environment.js';
|
|
9
|
+
import { handleHealthCheck, handleInfo, handleQuery } from '../handlers/http-handlers.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create and configure Express app
|
|
13
|
+
* @returns {express.Application} Configured Express app
|
|
14
|
+
*/
|
|
15
|
+
export function createHttpApp() {
|
|
16
|
+
const app = express();
|
|
17
|
+
|
|
18
|
+
// Middleware to parse JSON request bodies
|
|
19
|
+
app.use(express.json());
|
|
20
|
+
|
|
21
|
+
// Register routes
|
|
22
|
+
app.get('/health', handleHealthCheck);
|
|
23
|
+
app.get('/api/info', handleInfo);
|
|
24
|
+
app.post('/api/query', handleQuery);
|
|
25
|
+
|
|
26
|
+
return app;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Start the HTTP server
|
|
31
|
+
* @param {number} [port] - Port to listen on (defaults to env or 3000)
|
|
32
|
+
* @returns {Promise<void>}
|
|
33
|
+
*/
|
|
34
|
+
export async function startHttpServer(port) {
|
|
35
|
+
const app = createHttpApp();
|
|
36
|
+
const serverPort = port || getPort(HTTP_CONFIG.defaultPort);
|
|
37
|
+
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
app.listen(serverPort, () => {
|
|
40
|
+
console.log(`SQLCipher MCP HTTP Server running on http://localhost:${serverPort}`);
|
|
41
|
+
console.log(`Health check: http://localhost:${serverPort}/health`);
|
|
42
|
+
console.log(`API info: http://localhost:${serverPort}/api/info`);
|
|
43
|
+
|
|
44
|
+
if (!isPasswordConfigured()) {
|
|
45
|
+
console.warn('\n⚠️ Warning: SQLCIPHER_PASSWORD environment variable is not set.');
|
|
46
|
+
console.warn(' Database queries will fail until this is configured.\n');
|
|
47
|
+
} else {
|
|
48
|
+
console.log('✅ Database password is configured.\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
resolve();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server
|
|
3
|
+
* Model Context Protocol server setup and initialization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import {
|
|
9
|
+
CallToolRequestSchema,
|
|
10
|
+
ListToolsRequestSchema,
|
|
11
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
+
|
|
13
|
+
import { SERVER_CONFIG } from '../config/constants.js';
|
|
14
|
+
import { getConfigurationWarnings } from '../config/environment.js';
|
|
15
|
+
import { handleListTools, handleExecuteQuery, handleUnknownTool } from '../handlers/mcp-handlers.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create and configure MCP server
|
|
19
|
+
* @returns {Server} Configured MCP server instance
|
|
20
|
+
*/
|
|
21
|
+
export function createMcpServer() {
|
|
22
|
+
const server = new Server(
|
|
23
|
+
{
|
|
24
|
+
name: SERVER_CONFIG.name,
|
|
25
|
+
version: SERVER_CONFIG.version,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
capabilities: {
|
|
29
|
+
tools: {},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Register list tools handler
|
|
35
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
36
|
+
return handleListTools();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Register tool execution handler
|
|
40
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
41
|
+
const { name, arguments: args } = request.params;
|
|
42
|
+
|
|
43
|
+
if (name === 'execute_query') {
|
|
44
|
+
return await handleExecuteQuery(args);
|
|
45
|
+
} else {
|
|
46
|
+
return handleUnknownTool(name);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return server;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start the MCP server
|
|
55
|
+
* @returns {Promise<void>}
|
|
56
|
+
*/
|
|
57
|
+
export async function startMcpServer() {
|
|
58
|
+
// Check configuration and log warnings
|
|
59
|
+
const warnings = getConfigurationWarnings();
|
|
60
|
+
if (warnings.length > 0) {
|
|
61
|
+
console.error('Warning: ' + warnings.join(' '));
|
|
62
|
+
} else {
|
|
63
|
+
console.error('Configuration loaded: Database path and password set via environment variables.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create server
|
|
67
|
+
const server = createMcpServer();
|
|
68
|
+
|
|
69
|
+
// Create stdio transport
|
|
70
|
+
const transport = new StdioServerTransport();
|
|
71
|
+
|
|
72
|
+
// Connect server to transport
|
|
73
|
+
await server.connect(transport);
|
|
74
|
+
|
|
75
|
+
console.error('SQLCipher MCP Server running on stdio');
|
|
76
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Service
|
|
3
|
+
* Service layer that wraps database operations with error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { connectDatabase, executeQuery, closeConnection } from '../../lib/database.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Execute a query on a database
|
|
10
|
+
* Handles connection, query execution, and cleanup
|
|
11
|
+
* @param {string} dbPath - Path to the database file
|
|
12
|
+
* @param {string|undefined} password - Database password (optional)
|
|
13
|
+
* @param {string} query - SQL query to execute
|
|
14
|
+
* @returns {Promise<Object>} Query results
|
|
15
|
+
* @throws {Error} If connection or query execution fails
|
|
16
|
+
*/
|
|
17
|
+
export async function executeQueryOnDatabase(dbPath, password, query) {
|
|
18
|
+
let db = null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Connect to database
|
|
22
|
+
db = await connectDatabase(dbPath, password);
|
|
23
|
+
|
|
24
|
+
// Execute query
|
|
25
|
+
const result = await executeQuery(db, query);
|
|
26
|
+
|
|
27
|
+
return result;
|
|
28
|
+
} finally {
|
|
29
|
+
// Always close the database connection
|
|
30
|
+
if (db) {
|
|
31
|
+
closeConnection(db);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Test database connection
|
|
38
|
+
* @param {string} dbPath - Path to the database file
|
|
39
|
+
* @param {string|undefined} password - Database password (optional)
|
|
40
|
+
* @returns {Promise<boolean>} True if connection successful
|
|
41
|
+
*/
|
|
42
|
+
export async function testDatabaseConnection(dbPath, password) {
|
|
43
|
+
let db = null;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
db = await connectDatabase(dbPath, password);
|
|
47
|
+
return true;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`Failed to connect to database: ${error.message}`);
|
|
50
|
+
} finally {
|
|
51
|
+
if (db) {
|
|
52
|
+
closeConnection(db);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handling Utilities
|
|
3
|
+
* Standardized error response creation for MCP and HTTP
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create MCP error response
|
|
8
|
+
* @param {string} message - Error message
|
|
9
|
+
* @returns {Object} MCP error response object
|
|
10
|
+
*/
|
|
11
|
+
export function createMcpErrorResponse(message) {
|
|
12
|
+
return {
|
|
13
|
+
content: [
|
|
14
|
+
{
|
|
15
|
+
type: 'text',
|
|
16
|
+
text: message,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
isError: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create MCP success response
|
|
25
|
+
* @param {string} text - Response text
|
|
26
|
+
* @returns {Object} MCP success response object
|
|
27
|
+
*/
|
|
28
|
+
export function createMcpSuccessResponse(text) {
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: text,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create HTTP error response
|
|
41
|
+
* @param {number} statusCode - HTTP status code
|
|
42
|
+
* @param {string} message - Error message
|
|
43
|
+
* @returns {Object} HTTP error response object
|
|
44
|
+
*/
|
|
45
|
+
export function createHttpErrorResponse(statusCode, message) {
|
|
46
|
+
return {
|
|
47
|
+
statusCode,
|
|
48
|
+
body: {
|
|
49
|
+
error: message,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create HTTP success response
|
|
56
|
+
* @param {Object} data - Response data
|
|
57
|
+
* @param {string} message - Success message
|
|
58
|
+
* @returns {Object} HTTP success response object
|
|
59
|
+
*/
|
|
60
|
+
export function createHttpSuccessResponse(data, message) {
|
|
61
|
+
return {
|
|
62
|
+
statusCode: 200,
|
|
63
|
+
body: {
|
|
64
|
+
success: true,
|
|
65
|
+
data: data,
|
|
66
|
+
message: message,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,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
|
+
}
|