@airabbit/sqlite-mcp-server 0.1.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/README.md +91 -0
- package/build/index.js +453 -0
- package/manifest.json +32 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
## Generic SQLite MCP Server
|
|
2
|
+
|
|
3
|
+
This folder contains a **self-contained, generic Model Context Protocol (MCP) server** for SQLite.
|
|
4
|
+
It is completely independent from the rest of the repo and can be reused with any SQLite database.
|
|
5
|
+
|
|
6
|
+
### Capabilities
|
|
7
|
+
|
|
8
|
+
The server exposes these tools:
|
|
9
|
+
|
|
10
|
+
- **`sqlite_list_tables`**: list tables in the database (optionally filtered by a LIKE pattern).
|
|
11
|
+
- **`sqlite_get_table_info`**: describe a table (columns, types, PK info, row count, optional sample rows).
|
|
12
|
+
- **`sqlite_run_query`**: run a **read-only `SELECT`** query with positional parameters.
|
|
13
|
+
- **`sqlite_get_db_info`**: basic info about the DB file (path, size, timestamps).
|
|
14
|
+
|
|
15
|
+
All results are returned as JSON text in the MCP response.
|
|
16
|
+
|
|
17
|
+
### Database configuration
|
|
18
|
+
|
|
19
|
+
The server locates the SQLite file using:
|
|
20
|
+
|
|
21
|
+
1. `SQLITE_DB_PATH` environment variable (preferred).
|
|
22
|
+
2. `MCP_DB_PATH` environment variable (fallback).
|
|
23
|
+
3. `./database.sqlite` in the current working directory (development default).
|
|
24
|
+
|
|
25
|
+
The database is opened in **read-only** mode.
|
|
26
|
+
|
|
27
|
+
### Install & build
|
|
28
|
+
|
|
29
|
+
From the `sqlite-mcp-server` directory:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Run
|
|
37
|
+
|
|
38
|
+
Set the database path and start the MCP server:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
SQLITE_DB_PATH=/absolute/path/to/your.sqlite npm start
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The server runs over **stdio**, as required by MCP clients (e.g. Claude Desktop, Cursor MCP, etc.).
|
|
45
|
+
|
|
46
|
+
### Example Database
|
|
47
|
+
|
|
48
|
+
An example SQLite database (`example-small.sqlite`) is included with sample data:
|
|
49
|
+
- `users` table: 3 users (Alice, Bob, Charlie)
|
|
50
|
+
- `posts` table: 3 posts linked to users
|
|
51
|
+
|
|
52
|
+
### Claude Desktop Configuration
|
|
53
|
+
|
|
54
|
+
Add this to your Claude Desktop config file (usually `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"@airabbit/sqlite-mcp-server": {
|
|
60
|
+
"command": "node",
|
|
61
|
+
"args": [
|
|
62
|
+
"/absolute/path/to/sqlite-mcp-server/build/index.js"
|
|
63
|
+
],
|
|
64
|
+
"env": {
|
|
65
|
+
"SQLITE_DB_PATH": "/absolute/path/to/your/database.sqlite"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Example with included database:**
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"@airabbit/sqlite-mcp-server": {
|
|
77
|
+
"command": "node",
|
|
78
|
+
"args": [
|
|
79
|
+
"/path/to/sqlite-mcp-server/build/index.js"
|
|
80
|
+
],
|
|
81
|
+
"env": {
|
|
82
|
+
"SQLITE_DB_PATH": "/path/to/sqlite-mcp-server/example-small.sqlite"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Replace the paths with your actual installation path and database file path.
|
|
90
|
+
|
|
91
|
+
|
package/build/index.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import Database from 'better-sqlite3';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
function resolveDbPath() {
|
|
10
|
+
let envPath = process.env.SQLITE_DB_PATH || process.env.MCP_DB_PATH;
|
|
11
|
+
if (envPath) {
|
|
12
|
+
// If host failed to substitute template (still contains ${user_config.db_path}),
|
|
13
|
+
// try to read the actual configured value from manifest.json
|
|
14
|
+
if (envPath.includes('${user_config.db_path}')) {
|
|
15
|
+
try {
|
|
16
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const manifestPath = path.join(moduleDir, '..', 'manifest.json');
|
|
18
|
+
if (fs.existsSync(manifestPath)) {
|
|
19
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
20
|
+
const userConfig = manifest.user_config?.db_path;
|
|
21
|
+
if (userConfig?.default && typeof userConfig.default === 'string') {
|
|
22
|
+
let resolvedPath = userConfig.default;
|
|
23
|
+
// Resolve ${__dirname} if present
|
|
24
|
+
if (resolvedPath.includes('${__dirname}')) {
|
|
25
|
+
resolvedPath = resolvedPath.replace(/\$\{__dirname\}/g, path.join(moduleDir, '..'));
|
|
26
|
+
}
|
|
27
|
+
envPath = resolvedPath;
|
|
28
|
+
console.error(`SQLite MCP: resolved template to configured default: ${envPath}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.error(`SQLite MCP: failed to read manifest for template resolution: ${e}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Use the env path (either original or resolved from manifest)
|
|
37
|
+
if (envPath) {
|
|
38
|
+
return path.resolve(envPath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Default: cwd/database.sqlite for local dev if no env var is set
|
|
42
|
+
return path.join(process.cwd(), 'database.sqlite');
|
|
43
|
+
}
|
|
44
|
+
export class SqliteMcpServer {
|
|
45
|
+
constructor() {
|
|
46
|
+
this.db = null;
|
|
47
|
+
this.dbErrorReason = null;
|
|
48
|
+
this.dbPath = resolveDbPath();
|
|
49
|
+
this.server = new Server({
|
|
50
|
+
name: 'sqlite-mcp-server',
|
|
51
|
+
version: '0.1.0',
|
|
52
|
+
}, {
|
|
53
|
+
capabilities: {
|
|
54
|
+
tools: {},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
this.server.onerror = (error) => {
|
|
58
|
+
console.error('[SQLite MCP Error]', error);
|
|
59
|
+
};
|
|
60
|
+
process.on('SIGINT', async () => {
|
|
61
|
+
await this.server.close();
|
|
62
|
+
if (this.db) {
|
|
63
|
+
this.db.close();
|
|
64
|
+
}
|
|
65
|
+
process.exit(0);
|
|
66
|
+
});
|
|
67
|
+
this.initializeDatabase();
|
|
68
|
+
this.setupToolHandlers();
|
|
69
|
+
}
|
|
70
|
+
initializeDatabase() {
|
|
71
|
+
try {
|
|
72
|
+
console.error(`SQLite MCP: attempting to open DB at ${this.dbPath}`);
|
|
73
|
+
if (!fs.existsSync(this.dbPath)) {
|
|
74
|
+
const msg = `SQLite database file not found at: ${this.dbPath}`;
|
|
75
|
+
console.error(msg);
|
|
76
|
+
this.dbErrorReason = msg;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.db = new Database(this.dbPath, { readonly: true });
|
|
80
|
+
console.error(`SQLite MCP: connected to ${this.dbPath}`);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const msg = `Failed to initialize SQLite database: ${error.message}`;
|
|
84
|
+
console.error(msg, error);
|
|
85
|
+
this.dbErrorReason = msg + (error.stack ? `\nStack: ${error.stack}` : '');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
ensureDb() {
|
|
89
|
+
if (!this.db) {
|
|
90
|
+
const reason = this.dbErrorReason || 'Database not initialized';
|
|
91
|
+
throw new McpError(ErrorCode.InternalError, reason);
|
|
92
|
+
}
|
|
93
|
+
return this.db;
|
|
94
|
+
}
|
|
95
|
+
setupToolHandlers() {
|
|
96
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
97
|
+
tools: [
|
|
98
|
+
{
|
|
99
|
+
name: 'sqlite_list_tables',
|
|
100
|
+
description: 'List tables in the SQLite database (optionally filter by pattern).',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
like: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: "Optional LIKE pattern to filter table names (e.g., 'user%'). Uses SQLite LIKE semantics.",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'sqlite_get_table_info',
|
|
113
|
+
description: 'Describe a table: columns, types, primary key info, row count, and optional sample rows.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
table: {
|
|
118
|
+
type: 'string',
|
|
119
|
+
description: 'Table name to inspect.',
|
|
120
|
+
},
|
|
121
|
+
sampleRows: {
|
|
122
|
+
type: 'number',
|
|
123
|
+
description: 'Number of sample rows to fetch (default 5, max 100).',
|
|
124
|
+
minimum: 0,
|
|
125
|
+
maximum: 100,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: ['table'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'sqlite_run_query',
|
|
133
|
+
description: 'Run a read-only SQL query (SELECT only) against the SQLite database and return the rows.',
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
sql: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'SQL query to execute. Must be a single SELECT statement.',
|
|
140
|
+
},
|
|
141
|
+
params: {
|
|
142
|
+
type: 'array',
|
|
143
|
+
description: 'Positional parameters for the query (e.g., ["foo", 123]). Use ? placeholders in SQL.',
|
|
144
|
+
items: {},
|
|
145
|
+
},
|
|
146
|
+
maxRows: {
|
|
147
|
+
type: 'number',
|
|
148
|
+
description: 'Maximum number of rows to return (default 100, max 1000).',
|
|
149
|
+
minimum: 1,
|
|
150
|
+
maximum: 1000,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
required: ['sql'],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'sqlite_get_db_info',
|
|
158
|
+
description: 'Return basic information about the SQLite database (path, size, tables count).',
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'sqlite_set_db_path',
|
|
166
|
+
description: 'Change the SQLite database file at runtime. Closes current connection and opens the new database.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
path: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'Absolute path to the SQLite database file to open.',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
required: ['path'],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'sqlite_list_databases',
|
|
180
|
+
description: 'List SQLite database files (.sqlite, .db, .sqlite3) in a given folder. Checks folder access first.',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
folder: {
|
|
185
|
+
type: 'string',
|
|
186
|
+
description: 'Absolute path to the folder to search for SQLite files.',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
required: ['folder'],
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
}));
|
|
194
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
195
|
+
try {
|
|
196
|
+
const args = request.params.arguments;
|
|
197
|
+
switch (request.params.name) {
|
|
198
|
+
case 'sqlite_list_tables':
|
|
199
|
+
return this.handleListTables(args);
|
|
200
|
+
case 'sqlite_get_table_info':
|
|
201
|
+
return this.handleGetTableInfo(args);
|
|
202
|
+
case 'sqlite_run_query':
|
|
203
|
+
return this.handleRunQuery(args);
|
|
204
|
+
case 'sqlite_get_db_info':
|
|
205
|
+
return this.handleGetDbInfo();
|
|
206
|
+
case 'sqlite_set_db_path':
|
|
207
|
+
return this.handleSetDbPath(args);
|
|
208
|
+
case 'sqlite_list_databases':
|
|
209
|
+
return this.handleListDatabases(args);
|
|
210
|
+
default:
|
|
211
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const message = error instanceof McpError ? error.message : `SQLite MCP tool error: ${error.message}`;
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: 'text',
|
|
220
|
+
text: message,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
isError: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
handleListTables(args) {
|
|
229
|
+
const db = this.ensureDb();
|
|
230
|
+
const like = args?.like;
|
|
231
|
+
let sql = `
|
|
232
|
+
SELECT name
|
|
233
|
+
FROM sqlite_master
|
|
234
|
+
WHERE type = 'table'
|
|
235
|
+
`;
|
|
236
|
+
const params = [];
|
|
237
|
+
if (like) {
|
|
238
|
+
sql += ' AND name LIKE ?';
|
|
239
|
+
params.push(like);
|
|
240
|
+
}
|
|
241
|
+
sql += ' ORDER BY name';
|
|
242
|
+
const stmt = db.prepare(sql);
|
|
243
|
+
const rows = stmt.all(...params);
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: 'text',
|
|
248
|
+
text: JSON.stringify(rows, null, 2),
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
handleGetTableInfo(args) {
|
|
254
|
+
if (!args || !args.table) {
|
|
255
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing required "table" parameter');
|
|
256
|
+
}
|
|
257
|
+
const db = this.ensureDb();
|
|
258
|
+
const table = args.table;
|
|
259
|
+
const sampleRows = Math.max(0, Math.min(typeof args.sampleRows === 'number' ? args.sampleRows : 5, 100));
|
|
260
|
+
// Schema info
|
|
261
|
+
const pragmaStmt = db.prepare(`PRAGMA table_info(${JSON.stringify(table).slice(1, -1)})`);
|
|
262
|
+
const columns = pragmaStmt.all();
|
|
263
|
+
// Row count
|
|
264
|
+
const countStmt = db.prepare(`SELECT COUNT(*) as count FROM "${table}"`);
|
|
265
|
+
const countRow = countStmt.get();
|
|
266
|
+
let samples = [];
|
|
267
|
+
if (sampleRows > 0) {
|
|
268
|
+
const sampleStmt = db.prepare(`SELECT * FROM "${table}" LIMIT ${sampleRows}`);
|
|
269
|
+
samples = sampleStmt.all();
|
|
270
|
+
}
|
|
271
|
+
const info = {
|
|
272
|
+
table,
|
|
273
|
+
columns,
|
|
274
|
+
rowCount: countRow?.count ?? 0,
|
|
275
|
+
sampleRows: samples,
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: 'text',
|
|
281
|
+
text: JSON.stringify(info, null, 2),
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
handleRunQuery(args) {
|
|
287
|
+
if (!args || !args.sql) {
|
|
288
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing required "sql" parameter');
|
|
289
|
+
}
|
|
290
|
+
const db = this.ensureDb();
|
|
291
|
+
// Normalize SQL: trim and drop trailing semicolons; still reject multiple statements
|
|
292
|
+
const trimmed = args.sql.trim();
|
|
293
|
+
const sql = trimmed.replace(/;+\s*$/g, ''); // allow a trailing semicolon
|
|
294
|
+
// Enforce read-only: only allow single SELECT statement
|
|
295
|
+
const lower = sql.toLowerCase();
|
|
296
|
+
if (!lower.startsWith('select')) {
|
|
297
|
+
throw new McpError(ErrorCode.InvalidParams, 'Only read-only SELECT queries are allowed in sqlite_run_query');
|
|
298
|
+
}
|
|
299
|
+
// Reject any remaining semicolons (would indicate additional statements)
|
|
300
|
+
if (sql.includes(';')) {
|
|
301
|
+
throw new McpError(ErrorCode.InvalidParams, 'Multiple statements are not allowed; provide a single SELECT query (trailing semicolon is optional)');
|
|
302
|
+
}
|
|
303
|
+
const maxRows = Math.max(1, Math.min(typeof args.maxRows === 'number' ? args.maxRows : 100, 1000));
|
|
304
|
+
const params = Array.isArray(args.params) ? args.params : [];
|
|
305
|
+
const stmt = db.prepare(sql);
|
|
306
|
+
let rows = stmt.all(...params);
|
|
307
|
+
if (rows.length > maxRows) {
|
|
308
|
+
rows = rows.slice(0, maxRows);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
content: [
|
|
312
|
+
{
|
|
313
|
+
type: 'text',
|
|
314
|
+
text: JSON.stringify({
|
|
315
|
+
rowCount: rows.length,
|
|
316
|
+
rows,
|
|
317
|
+
}, null, 2),
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
handleGetDbInfo() {
|
|
323
|
+
const dbExists = fs.existsSync(this.dbPath);
|
|
324
|
+
const stats = dbExists ? fs.statSync(this.dbPath) : null;
|
|
325
|
+
const info = {
|
|
326
|
+
path: this.dbPath,
|
|
327
|
+
exists: dbExists,
|
|
328
|
+
sizeBytes: stats?.size ?? null,
|
|
329
|
+
modifiedAt: stats?.mtime?.toISOString() ?? null,
|
|
330
|
+
};
|
|
331
|
+
return {
|
|
332
|
+
content: [
|
|
333
|
+
{
|
|
334
|
+
type: 'text',
|
|
335
|
+
text: JSON.stringify(info, null, 2),
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
handleSetDbPath(args) {
|
|
341
|
+
if (!args || !args.path) {
|
|
342
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing required "path" parameter');
|
|
343
|
+
}
|
|
344
|
+
const newPath = path.resolve(args.path);
|
|
345
|
+
// Validate file exists
|
|
346
|
+
if (!fs.existsSync(newPath)) {
|
|
347
|
+
throw new McpError(ErrorCode.InvalidParams, `SQLite database file not found at: ${newPath}`);
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
// Close current DB if open
|
|
351
|
+
if (this.db) {
|
|
352
|
+
this.db.close();
|
|
353
|
+
this.db = null;
|
|
354
|
+
}
|
|
355
|
+
// Clear error state
|
|
356
|
+
this.dbErrorReason = null;
|
|
357
|
+
// Open new database
|
|
358
|
+
this.dbPath = newPath;
|
|
359
|
+
this.db = new Database(newPath, { readonly: true });
|
|
360
|
+
console.error(`SQLite MCP: switched to database at ${newPath}`);
|
|
361
|
+
return {
|
|
362
|
+
content: [
|
|
363
|
+
{
|
|
364
|
+
type: 'text',
|
|
365
|
+
text: JSON.stringify({
|
|
366
|
+
success: true,
|
|
367
|
+
path: newPath,
|
|
368
|
+
message: `Successfully switched to database at ${newPath}`,
|
|
369
|
+
}, null, 2),
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
const msg = `Failed to open SQLite database at ${newPath}: ${error.message}`;
|
|
376
|
+
console.error(msg, error);
|
|
377
|
+
this.dbErrorReason = msg + (error.stack ? `\nStack: ${error.stack}` : '');
|
|
378
|
+
throw new McpError(ErrorCode.InternalError, msg);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
handleListDatabases(args) {
|
|
382
|
+
if (!args || !args.folder) {
|
|
383
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing required "folder" parameter');
|
|
384
|
+
}
|
|
385
|
+
const folderPath = path.resolve(args.folder);
|
|
386
|
+
// Check if folder exists
|
|
387
|
+
if (!fs.existsSync(folderPath)) {
|
|
388
|
+
throw new McpError(ErrorCode.InvalidParams, `Folder does not exist: ${folderPath}`);
|
|
389
|
+
}
|
|
390
|
+
// Check if it's actually a directory
|
|
391
|
+
const stats = fs.statSync(folderPath);
|
|
392
|
+
if (!stats.isDirectory()) {
|
|
393
|
+
throw new McpError(ErrorCode.InvalidParams, `Path is not a directory: ${folderPath}`);
|
|
394
|
+
}
|
|
395
|
+
// Check read access
|
|
396
|
+
try {
|
|
397
|
+
fs.accessSync(folderPath, fs.constants.R_OK);
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
throw new McpError(ErrorCode.InternalError, `Access denied: Cannot read folder ${folderPath}. ${error.message}`);
|
|
401
|
+
}
|
|
402
|
+
// List SQLite files
|
|
403
|
+
try {
|
|
404
|
+
const files = fs.readdirSync(folderPath);
|
|
405
|
+
const sqliteFiles = files
|
|
406
|
+
.filter((file) => {
|
|
407
|
+
const ext = path.extname(file).toLowerCase();
|
|
408
|
+
return ext === '.sqlite' || ext === '.db' || ext === '.sqlite3';
|
|
409
|
+
})
|
|
410
|
+
.map((file) => {
|
|
411
|
+
const fullPath = path.join(folderPath, file);
|
|
412
|
+
try {
|
|
413
|
+
const fileStats = fs.statSync(fullPath);
|
|
414
|
+
return {
|
|
415
|
+
name: file,
|
|
416
|
+
path: fullPath,
|
|
417
|
+
sizeBytes: fileStats.size,
|
|
418
|
+
modifiedAt: fileStats.mtime.toISOString(),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
// Skip files we can't stat
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
.filter((f) => f !== null);
|
|
427
|
+
return {
|
|
428
|
+
content: [
|
|
429
|
+
{
|
|
430
|
+
type: 'text',
|
|
431
|
+
text: JSON.stringify({
|
|
432
|
+
folder: folderPath,
|
|
433
|
+
count: sqliteFiles.length,
|
|
434
|
+
databases: sqliteFiles,
|
|
435
|
+
}, null, 2),
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
throw new McpError(ErrorCode.InternalError, `Failed to read folder ${folderPath}: ${error.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async run() {
|
|
445
|
+
const transport = new StdioServerTransport();
|
|
446
|
+
await this.server.connect(transport);
|
|
447
|
+
console.error('SQLite MCP server running on stdio');
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
451
|
+
const server = new SqliteMcpServer();
|
|
452
|
+
server.run().catch(console.error);
|
|
453
|
+
}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dxt_version": "1",
|
|
3
|
+
"name": "sqlite-mcp-server",
|
|
4
|
+
"display_name": "SQLite MCP Server",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"description": "Read-only SQLite MCP server exposing tools to inspect schema, list tables, and run SELECT queries.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Airabbit"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"server": {
|
|
12
|
+
"type": "node",
|
|
13
|
+
"entry_point": "build/index.js",
|
|
14
|
+
"mcp_config": {
|
|
15
|
+
"command": "node",
|
|
16
|
+
"args": [
|
|
17
|
+
"${__dirname}/build/index.js"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"user_config": {
|
|
22
|
+
"db_path": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"title": "SQLite database path",
|
|
25
|
+
"description": "Absolute path to the SQLite database file to open in read-only mode.",
|
|
26
|
+
"default": "${__dirname}/example.sqlite"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@airabbit/sqlite-mcp-server",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Generic SQLite Model Context Protocol (MCP) server",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"build/",
|
|
9
|
+
"manifest.json"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc && chmod +x build/index.js",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"start": "node build/index.js",
|
|
15
|
+
"smoke:test": "npm run build && node test/smoke.test.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
19
|
+
"better-sqlite3": "^8.5.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/better-sqlite3": "^7.6.4",
|
|
23
|
+
"@types/node": "^20.4.5",
|
|
24
|
+
"typescript": "^5.1.6"
|
|
25
|
+
}
|
|
26
|
+
}
|