@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 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
+ }