sqlcipher-mcp-server 1.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 +282 -0
- package/index.js +226 -0
- package/lib/database.js +216 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# SQLCipher MCP Server
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server that provides read-only access to SQLCipher-encrypted SQLite databases. This server allows you to query encrypted SQLite databases using SQLCipher 3 default encryption settings.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This MCP server enables you to:
|
|
8
|
+
- Connect to SQLCipher-encrypted SQLite databases
|
|
9
|
+
- Execute SELECT queries (read-only mode)
|
|
10
|
+
- Retrieve query results in a structured format
|
|
11
|
+
- Use SQLCipher 3 default encryption settings
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- Node.js (v18 or higher)
|
|
16
|
+
- npm or yarn package manager
|
|
17
|
+
- Access to a SQLCipher-encrypted SQLite database file
|
|
18
|
+
- Database password (will be provided via environment variable)
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
### Option 1: Install from npm (Recommended)
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g sqlcipher-mcp-server
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install locally in your project:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install sqlcipher-mcp-server
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Option 2: Install from source
|
|
35
|
+
|
|
36
|
+
1. Clone or download this repository
|
|
37
|
+
2. Install dependencies:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
### Environment Variable
|
|
46
|
+
|
|
47
|
+
Set the `SQLCIPHER_PASSWORD` environment variable with your database password:
|
|
48
|
+
|
|
49
|
+
**Windows (PowerShell):**
|
|
50
|
+
```powershell
|
|
51
|
+
$env:SQLCIPHER_PASSWORD = "your_database_password"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Windows (Command Prompt):**
|
|
55
|
+
```cmd
|
|
56
|
+
set SQLCIPHER_PASSWORD=your_database_password
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Linux/macOS:**
|
|
60
|
+
```bash
|
|
61
|
+
export SQLCIPHER_PASSWORD="your_database_password"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Permanent setup (recommended):**
|
|
65
|
+
- Add the environment variable to your system settings
|
|
66
|
+
- Or use a `.env` file with a tool like `dotenv` (requires additional setup)
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### Starting the Server
|
|
71
|
+
|
|
72
|
+
The MCP server communicates via stdio (standard input/output). Start it with:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm start
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or directly:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node index.js
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### MCP Client Configuration
|
|
85
|
+
|
|
86
|
+
#### For Cursor IDE
|
|
87
|
+
|
|
88
|
+
1. **Install the package globally:**
|
|
89
|
+
```bash
|
|
90
|
+
npm install -g sqlcipher-mcp-server
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
2. **Find the global installation path:**
|
|
94
|
+
```bash
|
|
95
|
+
npm root -g
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
3. **Configure Cursor IDE:**
|
|
99
|
+
|
|
100
|
+
Open Cursor's MCP settings file (location varies by OS):
|
|
101
|
+
- **Windows**: `%APPDATA%\Cursor\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json`
|
|
102
|
+
- **macOS**: `~/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
|
|
103
|
+
- **Linux**: `~/.config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`
|
|
104
|
+
|
|
105
|
+
Add this configuration:
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mcpServers": {
|
|
109
|
+
"sqlcipher": {
|
|
110
|
+
"command": "node",
|
|
111
|
+
"args": ["C:\\Users\\YourUsername\\AppData\\Roaming\\npm\\node_modules\\sqlcipher-mcp-server\\index.js"],
|
|
112
|
+
"env": {
|
|
113
|
+
"SQLCIPHER_PASSWORD": "your_database_password"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Note:** Replace the path with your actual global npm path from step 2.
|
|
121
|
+
|
|
122
|
+
4. **Restart Cursor IDE** for changes to take effect.
|
|
123
|
+
|
|
124
|
+
#### Alternative: Using npx (no global installation)
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"mcpServers": {
|
|
129
|
+
"sqlcipher": {
|
|
130
|
+
"command": "npx",
|
|
131
|
+
"args": ["-y", "sqlcipher-mcp-server"],
|
|
132
|
+
"env": {
|
|
133
|
+
"SQLCIPHER_PASSWORD": "your_database_password"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### For other MCP clients
|
|
141
|
+
|
|
142
|
+
Configure your MCP client to use this server:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"mcpServers": {
|
|
147
|
+
"sqlcipher": {
|
|
148
|
+
"command": "node",
|
|
149
|
+
"args": ["path/to/sqlcipher-mcp-server/index.js"],
|
|
150
|
+
"env": {
|
|
151
|
+
"SQLCIPHER_PASSWORD": "your_database_password"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For detailed integration instructions, see [PUBLISHING.md](./PUBLISHING.md).
|
|
159
|
+
|
|
160
|
+
### Available Tools
|
|
161
|
+
|
|
162
|
+
#### `execute_query`
|
|
163
|
+
|
|
164
|
+
Execute a SELECT query on a SQLCipher-encrypted database.
|
|
165
|
+
|
|
166
|
+
**Parameters:**
|
|
167
|
+
- `database_path` (string, required): Full path to the SQLCipher database file
|
|
168
|
+
- `query` (string, required): SQL SELECT query to execute
|
|
169
|
+
|
|
170
|
+
**Returns:**
|
|
171
|
+
- Formatted text output with query results
|
|
172
|
+
- JSON representation of results including:
|
|
173
|
+
- `columns`: Array of column names
|
|
174
|
+
- `rows`: Array of row objects
|
|
175
|
+
- `rowCount`: Number of rows returned
|
|
176
|
+
|
|
177
|
+
**Example Usage:**
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
// Example 1: Simple SELECT query
|
|
181
|
+
{
|
|
182
|
+
"tool": "execute_query",
|
|
183
|
+
"arguments": {
|
|
184
|
+
"database_path": "C:\\Users\\Username\\AppData\\Local\\AppName\\database.db",
|
|
185
|
+
"query": "SELECT * FROM users LIMIT 10"
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Example 2: SELECT with WHERE clause
|
|
190
|
+
{
|
|
191
|
+
"tool": "execute_query",
|
|
192
|
+
"arguments": {
|
|
193
|
+
"database_path": "C:\\Users\\Username\\AppData\\Local\\AppName\\database.db",
|
|
194
|
+
"query": "SELECT id, name, email FROM users WHERE active = 1"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Example 3: SELECT with JOIN
|
|
199
|
+
{
|
|
200
|
+
"tool": "execute_query",
|
|
201
|
+
"arguments": {
|
|
202
|
+
"database_path": "C:\\Users\\Username\\AppData\\Local\\AppName\\database.db",
|
|
203
|
+
"query": "SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Security Features
|
|
209
|
+
|
|
210
|
+
- **Read-Only Mode**: Only SELECT queries are allowed. INSERT, UPDATE, DELETE, and other modifying operations are blocked.
|
|
211
|
+
- **Password Protection**: Database password is read from environment variable and never exposed in logs or error messages.
|
|
212
|
+
- **Query Validation**: All queries are validated to ensure they are SELECT queries only.
|
|
213
|
+
- **Error Handling**: Sensitive information is not exposed in error messages.
|
|
214
|
+
|
|
215
|
+
## Error Handling
|
|
216
|
+
|
|
217
|
+
The server handles various error scenarios:
|
|
218
|
+
|
|
219
|
+
- **Database file not found**: Returns clear error message
|
|
220
|
+
- **Invalid password**: Returns generic error (doesn't expose password details)
|
|
221
|
+
- **SQL syntax errors**: Returns specific syntax error messages
|
|
222
|
+
- **Table/column not found**: Returns specific error messages
|
|
223
|
+
- **Non-SELECT queries**: Returns error explaining read-only restriction
|
|
224
|
+
|
|
225
|
+
## Database Location
|
|
226
|
+
|
|
227
|
+
The database file path should be provided each time you execute a query. Common locations for Windows applications:
|
|
228
|
+
|
|
229
|
+
- `C:\Users\<Username>\AppData\Local\<AppName>\database.db`
|
|
230
|
+
- `C:\Users\<Username>\AppData\Roaming\<AppName>\database.db`
|
|
231
|
+
- `C:\ProgramData\<AppName>\database.db`
|
|
232
|
+
|
|
233
|
+
## Limitations
|
|
234
|
+
|
|
235
|
+
- **Read-Only**: Only SELECT queries are supported
|
|
236
|
+
- **Single Connection**: Each query opens and closes a new database connection
|
|
237
|
+
- **Result Size**: Results are limited to 1000 rows for display (full results available in JSON)
|
|
238
|
+
- **SQLCipher 3 Only**: Uses SQLCipher 3 default encryption settings
|
|
239
|
+
|
|
240
|
+
## Troubleshooting
|
|
241
|
+
|
|
242
|
+
### "Database password not found" Error
|
|
243
|
+
- Ensure `SQLCIPHER_PASSWORD` environment variable is set
|
|
244
|
+
- Restart your MCP client after setting the environment variable
|
|
245
|
+
- Check that the environment variable is available to the Node.js process
|
|
246
|
+
|
|
247
|
+
### "Database file not found" Error
|
|
248
|
+
- Verify the database path is correct
|
|
249
|
+
- Use absolute paths instead of relative paths
|
|
250
|
+
- Check file permissions
|
|
251
|
+
|
|
252
|
+
### "Invalid password" Error
|
|
253
|
+
- Verify the password matches the one used to encrypt the database
|
|
254
|
+
- Ensure SQLCipher 3 defaults are used (as this server expects)
|
|
255
|
+
- Check for extra spaces or special characters in the password
|
|
256
|
+
|
|
257
|
+
### Connection Issues
|
|
258
|
+
- Ensure the database file is not locked by another application
|
|
259
|
+
- Verify the database file is a valid SQLCipher database
|
|
260
|
+
- Check that SQLCipher 3 encryption was used (not SQLCipher 4)
|
|
261
|
+
|
|
262
|
+
## Development
|
|
263
|
+
|
|
264
|
+
### Project Structure
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
MyMCP/
|
|
268
|
+
├── index.js # Main MCP server entry point
|
|
269
|
+
├── lib/
|
|
270
|
+
│ └── database.js # Database connection and query logic
|
|
271
|
+
├── package.json # Project dependencies
|
|
272
|
+
└── README.md # This file
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Dependencies
|
|
276
|
+
|
|
277
|
+
- `@modelcontextprotocol/sdk`: MCP SDK for Node.js
|
|
278
|
+
- `@journeyapps/sqlcipher`: SQLCipher bindings for Node.js
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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 password from environment variable
|
|
17
|
+
const DB_PASSWORD = process.env.SQLCIPHER_PASSWORD;
|
|
18
|
+
|
|
19
|
+
// Create MCP server instance
|
|
20
|
+
const server = new Server(
|
|
21
|
+
{
|
|
22
|
+
name: 'sqlcipher-mcp-server',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
capabilities: {
|
|
27
|
+
tools: {},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* List available tools
|
|
34
|
+
*/
|
|
35
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
36
|
+
return {
|
|
37
|
+
tools: [
|
|
38
|
+
{
|
|
39
|
+
name: 'execute_query',
|
|
40
|
+
description: 'Execute a SELECT query on a SQLCipher-encrypted SQLite database. Only read-only queries are allowed.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
database_path: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Path to the SQLCipher database file',
|
|
47
|
+
},
|
|
48
|
+
query: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'SQL SELECT query to execute (read-only)',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ['database_path', 'query'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Handle tool execution requests
|
|
62
|
+
*/
|
|
63
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
64
|
+
const { name, arguments: args } = request.params;
|
|
65
|
+
|
|
66
|
+
if (name === 'execute_query') {
|
|
67
|
+
try {
|
|
68
|
+
// Validate required parameters
|
|
69
|
+
if (!args || typeof args !== 'object') {
|
|
70
|
+
throw new Error('Invalid arguments: arguments must be an object');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { database_path, query } = args;
|
|
74
|
+
|
|
75
|
+
// Validate database_path
|
|
76
|
+
if (!database_path || typeof database_path !== 'string') {
|
|
77
|
+
throw new Error('database_path is required and must be a string');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate query
|
|
81
|
+
if (!query || typeof query !== 'string') {
|
|
82
|
+
throw new Error('query is required and must be a string');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Connect to database (password is optional - will work with unencrypted databases)
|
|
86
|
+
let db = null;
|
|
87
|
+
try {
|
|
88
|
+
db = await connectDatabase(database_path, DB_PASSWORD);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: `Failed to connect to database: ${error.message}`,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
isError: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Execute query
|
|
102
|
+
try {
|
|
103
|
+
const result = executeQuery(db, query);
|
|
104
|
+
|
|
105
|
+
// Format results for response
|
|
106
|
+
const responseText = formatQueryResults(result);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: responseText,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: `Query execution failed: ${error.message}`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
isError: true,
|
|
125
|
+
};
|
|
126
|
+
} finally {
|
|
127
|
+
// Always close the database connection
|
|
128
|
+
closeConnection(db);
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: 'text',
|
|
135
|
+
text: `Error: ${error.message}`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: 'text',
|
|
146
|
+
text: `Unknown tool: ${name}`,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
isError: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Format query results as a readable string
|
|
156
|
+
*
|
|
157
|
+
* @param {Object} result - Query result object with columns, rows, and rowCount
|
|
158
|
+
* @returns {string} Formatted result string
|
|
159
|
+
*/
|
|
160
|
+
function formatQueryResults(result) {
|
|
161
|
+
const { columns, rows, rowCount } = result;
|
|
162
|
+
|
|
163
|
+
if (rowCount === 0) {
|
|
164
|
+
return `Query executed successfully. No rows returned.\nColumns: ${columns.join(', ')}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Build table-like output
|
|
168
|
+
let output = `Query executed successfully. ${rowCount} row(s) returned.\n\n`;
|
|
169
|
+
|
|
170
|
+
// Add column headers
|
|
171
|
+
output += `Columns: ${columns.join(' | ')}\n`;
|
|
172
|
+
output += '-'.repeat(columns.join(' | ').length) + '\n';
|
|
173
|
+
|
|
174
|
+
// Add rows (limit to first 1000 rows for display)
|
|
175
|
+
const displayRows = rows.slice(0, 1000);
|
|
176
|
+
for (const row of displayRows) {
|
|
177
|
+
const values = columns.map(col => {
|
|
178
|
+
const value = row[col];
|
|
179
|
+
// Handle null/undefined
|
|
180
|
+
if (value === null || value === undefined) {
|
|
181
|
+
return 'NULL';
|
|
182
|
+
}
|
|
183
|
+
// Convert to string and truncate long values
|
|
184
|
+
const str = String(value);
|
|
185
|
+
return str.length > 50 ? str.substring(0, 47) + '...' : str;
|
|
186
|
+
});
|
|
187
|
+
output += values.join(' | ') + '\n';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (rows.length > 1000) {
|
|
191
|
+
output += `\n... (showing first 1000 of ${rowCount} rows)`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Add JSON representation for programmatic access
|
|
195
|
+
output += '\n\nJSON representation:\n';
|
|
196
|
+
output += JSON.stringify(result, null, 2);
|
|
197
|
+
|
|
198
|
+
return output;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Start the MCP server
|
|
203
|
+
*/
|
|
204
|
+
async function main() {
|
|
205
|
+
// Check if password is set (warn but don't fail - might be set later)
|
|
206
|
+
if (!DB_PASSWORD) {
|
|
207
|
+
console.error(
|
|
208
|
+
'Warning: SQLCIPHER_PASSWORD environment variable is not set. ' +
|
|
209
|
+
'Database connections will fail until this is configured.'
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Create stdio transport
|
|
214
|
+
const transport = new StdioServerTransport();
|
|
215
|
+
|
|
216
|
+
// Connect server to transport
|
|
217
|
+
await server.connect(transport);
|
|
218
|
+
|
|
219
|
+
console.error('SQLCipher MCP Server running on stdio');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Start the server
|
|
223
|
+
main().catch((error) => {
|
|
224
|
+
console.error('Fatal error starting server:', error);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
});
|
package/lib/database.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sqlcipher-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP Server for querying SQLCipher-encrypted SQLite databases",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sqlcipher-mcp-server": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"lib/**/*",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node index.js",
|
|
18
|
+
"start:http": "node server-http.js",
|
|
19
|
+
"test:http": "node server-http.js",
|
|
20
|
+
"dev": "nodemon index.js",
|
|
21
|
+
"dev:http": "nodemon server-http.js",
|
|
22
|
+
"prepublishOnly": "npm test || exit 0"
|
|
23
|
+
},
|
|
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
|
+
"dependencies": {
|
|
47
|
+
"@journeyapps/sqlcipher": "^5.1.1",
|
|
48
|
+
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
49
|
+
"express": "^4.22.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"nodemon": "^3.1.11"
|
|
53
|
+
}
|
|
54
|
+
}
|