react-native-sqlite-mcp 1.0.1 → 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/src/db.ts DELETED
@@ -1,131 +0,0 @@
1
- import sqlite3 from "sqlite3";
2
- import { logger } from "./logger.js";
3
-
4
- export class Database {
5
- private db: sqlite3.Database;
6
-
7
- constructor(filename: string) {
8
- this.db = new sqlite3.Database(filename);
9
- }
10
-
11
- public all<T>(sql: string, params: any[] = []): Promise<T[]> {
12
- return new Promise((resolve, reject) => {
13
- this.db.all(sql, params, (err, rows) => {
14
- if (err) reject(err);
15
- else resolve(rows as Array<T>);
16
- });
17
- });
18
- }
19
-
20
- public get<T>(sql: string, params: any[] = []): Promise<T | undefined> {
21
- return new Promise((resolve, reject) => {
22
- this.db.get(sql, params, (err, row) => {
23
- if (err) reject(err);
24
- else resolve(row as T | undefined);
25
- });
26
- });
27
- }
28
-
29
- public close(): Promise<void> {
30
- return new Promise((resolve, reject) => {
31
- this.db.close((err) => {
32
- if (err) reject(err);
33
- else resolve();
34
- });
35
- });
36
- }
37
- }
38
-
39
- interface CachedConnection {
40
- db: Database;
41
- lastUsed: number;
42
- timer: ReturnType<typeof setTimeout>;
43
- }
44
-
45
- const CACHE_TTL_MS = 60_000; // close idle connections after 60s
46
- const connectionCache = new Map<string, CachedConnection>();
47
-
48
- function getCachedDb(dbPath: string): Database {
49
- const existing = connectionCache.get(dbPath);
50
-
51
- if (existing) {
52
- clearTimeout(existing.timer);
53
- existing.lastUsed = Date.now();
54
- existing.timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
55
- return existing.db;
56
- }
57
-
58
- const db = new Database(dbPath);
59
- const timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
60
-
61
- connectionCache.set(dbPath, { db, lastUsed: Date.now(), timer });
62
- logger.debug(`Opened DB connection: ${dbPath}`);
63
- return db;
64
- }
65
-
66
- async function evictConnection(dbPath: string): Promise<void> {
67
- const entry = connectionCache.get(dbPath);
68
- if (!entry) return;
69
-
70
- connectionCache.delete(dbPath);
71
- try {
72
- await entry.db.close();
73
- logger.debug(`Closed idle DB connection: ${dbPath}`);
74
- } catch (e) {
75
- logger.warn(`Error closing DB: ${dbPath}`, { error: String(e) });
76
- }
77
- }
78
-
79
- export async function closeAllConnections(): Promise<void> {
80
- const paths = [...connectionCache.keys()];
81
- for (const p of paths) {
82
- await evictConnection(p);
83
- }
84
- logger.info(`Closed ${paths.length} cached DB connection(s)`);
85
- }
86
-
87
- const QUERY_TIMEOUT_MS = 30_000;
88
-
89
- function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
90
- return new Promise<T>((resolve, reject) => {
91
- const timer = setTimeout(() => {
92
- reject(new Error(`Query timed out after ${ms}ms: ${label}`));
93
- }, ms);
94
-
95
- promise
96
- .then((result) => { clearTimeout(timer); resolve(result); })
97
- .catch((err) => { clearTimeout(timer); reject(err); });
98
- });
99
- }
100
-
101
- export async function queryDb(dbPath: string, sql: string, params: any[] = []): Promise<any[]> {
102
- const db = getCachedDb(dbPath);
103
- return withTimeout(db.all(sql, params), QUERY_TIMEOUT_MS, sql.slice(0, 80));
104
- }
105
-
106
- export async function inspectSchema(dbPath: string): Promise<any> {
107
- const db = getCachedDb(dbPath);
108
-
109
- const tables = await withTimeout(
110
- db.all<{ name: string }>("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"),
111
- QUERY_TIMEOUT_MS,
112
- "inspect_schema:tables"
113
- );
114
-
115
- const schemaInfo: Record<string, any> = {};
116
-
117
- for (const table of tables) {
118
- const columns = await db.all(`PRAGMA table_info("${table.name}");`);
119
- const createSql = await db.get<{ sql: string }>(
120
- `SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
121
- [table.name]
122
- );
123
-
124
- schemaInfo[table.name] = {
125
- columns,
126
- createSql: createSql?.sql
127
- };
128
- }
129
-
130
- return schemaInfo;
131
- }
package/src/index.ts DELETED
@@ -1,387 +0,0 @@
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 { listDatabases, syncDatabase } from "./locator.js";
10
- import { inspectSchema, queryDb, closeAllConnections } from "./db.js";
11
- import { logger } from "./logger.js";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Process-level guards — prevent silent crashes that cause EOF errors
15
- // ---------------------------------------------------------------------------
16
-
17
- process.on("uncaughtException", (error) => {
18
- logger.error("Uncaught exception (process kept alive)", {
19
- message: error.message,
20
- stack: error.stack?.slice(0, 500),
21
- });
22
- // Do NOT call process.exit() — keep the MCP server alive
23
- });
24
-
25
- process.on("unhandledRejection", (reason) => {
26
- logger.error("Unhandled promise rejection (process kept alive)", {
27
- reason: String(reason),
28
- });
29
- });
30
-
31
- // ---------------------------------------------------------------------------
32
- // State
33
- // ---------------------------------------------------------------------------
34
-
35
- interface SyncedDB {
36
- localPath: string;
37
- dbName: string;
38
- platform: "ios" | "android";
39
- }
40
-
41
- let activeDatabases: SyncedDB[] = [];
42
-
43
- // ---------------------------------------------------------------------------
44
- // Server setup
45
- // ---------------------------------------------------------------------------
46
-
47
- const server = new Server(
48
- {
49
- name: "react-native-sqlite-bridge",
50
- version: "1.0.0",
51
- },
52
- {
53
- capabilities: {
54
- tools: {},
55
- },
56
- }
57
- );
58
-
59
- // MCP-level transport error handler
60
- server.onerror = (error) => {
61
- logger.error("MCP transport error", { message: String(error) });
62
- };
63
-
64
- // ---------------------------------------------------------------------------
65
- // Tool definitions
66
- // ---------------------------------------------------------------------------
67
-
68
- server.setRequestHandler(ListToolsRequestSchema, async () => {
69
- return {
70
- tools: [
71
- {
72
- name: "sync_database",
73
- description:
74
- "Re-runs the adb pull or file-find logic to ensure the AI is looking at the latest data from the emulator/simulator.",
75
- inputSchema: {
76
- type: "object",
77
- properties: {
78
- dbName: {
79
- type: "string",
80
- description:
81
- "The name of the database file or a glob pattern (e.g., 'my_app.db' or '*.db'). Optional. If omitted, it will select the first discovered database.",
82
- },
83
- bundleId: {
84
- type: "string",
85
- description:
86
- "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
87
- },
88
- platform: {
89
- type: "string",
90
- description:
91
- "Optional. Explicitly target 'ios' or 'android'.",
92
- },
93
- },
94
- },
95
- },
96
- {
97
- name: "list_databases",
98
- description:
99
- "Lists all available SQLite databases found on the iOS Simulator or Android Emulator.",
100
- inputSchema: {
101
- type: "object",
102
- properties: {
103
- bundleId: {
104
- type: "string",
105
- description:
106
- "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
107
- },
108
- platform: {
109
- type: "string",
110
- description:
111
- "Optional. Explicitly target 'ios' or 'android'.",
112
- },
113
- },
114
- },
115
- },
116
- {
117
- name: "inspect_schema",
118
- description:
119
- "Returns a list of all tables and their column definitions. This gives the AI the 'map' of the database.",
120
- inputSchema: {
121
- type: "object",
122
- properties: {
123
- dbName: {
124
- type: "string",
125
- description:
126
- "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
127
- },
128
- platform: {
129
- type: "string",
130
- description:
131
- "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
132
- },
133
- },
134
- required: [],
135
- },
136
- },
137
- {
138
- name: "read_table_contents",
139
- description:
140
- "Returns rows from a specific table. Equivalent to SELECT * FROM table_name.",
141
- inputSchema: {
142
- type: "object",
143
- properties: {
144
- tableName: {
145
- type: "string",
146
- description: "The name of the table to read.",
147
- },
148
- limit: {
149
- type: "number",
150
- description:
151
- "Optional limit to the number of rows returned. Defaults to 100.",
152
- },
153
- dbName: {
154
- type: "string",
155
- description:
156
- "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
157
- },
158
- platform: {
159
- type: "string",
160
- description:
161
- "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
162
- },
163
- },
164
- required: ["tableName"],
165
- },
166
- },
167
- {
168
- name: "query_db",
169
- description:
170
- "Accepts a raw SQL SELECT string and returns the JSON result set.",
171
- inputSchema: {
172
- type: "object",
173
- properties: {
174
- sql: {
175
- type: "string",
176
- description: "The raw SQL SELECT string to execute.",
177
- },
178
- params: {
179
- type: "array",
180
- description:
181
- "Optional arguments to bind to the SQL query. Use this to safely substitute ? placeholders in your SQL string (e.g. ['value', 42]).",
182
- items: {
183
- description: "A single bound parameter value.",
184
- },
185
- },
186
- dbName: {
187
- type: "string",
188
- description:
189
- "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
190
- },
191
- platform: {
192
- type: "string",
193
- description:
194
- "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
195
- },
196
- },
197
- required: ["sql"],
198
- },
199
- },
200
- ],
201
- };
202
- });
203
-
204
- // ---------------------------------------------------------------------------
205
- // Helpers
206
- // ---------------------------------------------------------------------------
207
-
208
- function cleanPlatform(raw?: string): "ios" | "android" | undefined {
209
- if (!raw) return undefined;
210
- const cleaned = raw.replace(/['"]/g, "").trim().toLowerCase();
211
- if (cleaned === "ios" || cleaned === "android") return cleaned;
212
- return undefined;
213
- }
214
-
215
- async function ensureDbState(args: any): Promise<SyncedDB> {
216
- const reqDbName = args?.dbName as string | undefined;
217
- const reqPlatform = cleanPlatform(args?.platform as string | undefined);
218
-
219
- // If nothing is synced, sync defaults
220
- if (activeDatabases.length === 0) {
221
- const envDb = reqDbName || process.env.DB_NAME;
222
- const envBundle = process.env.ANDROID_BUNDLE_ID;
223
- activeDatabases = await syncDatabase(envDb, envBundle, reqPlatform);
224
- }
225
-
226
- let candidates = activeDatabases;
227
- if (reqPlatform)
228
- candidates = candidates.filter((db) => db.platform === reqPlatform);
229
- if (reqDbName)
230
- candidates = candidates.filter((db) => db.dbName === reqDbName);
231
-
232
- if (candidates.length === 1) return candidates[0];
233
-
234
- if (candidates.length === 0) {
235
- throw new Error(
236
- `No synced databases match the criteria (platform: ${reqPlatform || "any"}, dbName: ${reqDbName || "any"}). Try calling sync_database first.`
237
- );
238
- }
239
-
240
- const matches = candidates
241
- .map((c) => `[${c.platform}] ${c.dbName}`)
242
- .join(", ");
243
- throw new Error(
244
- `Multiple databases match the criteria. Please specify 'platform' or 'dbName'. Matches: ${matches}`
245
- );
246
- }
247
-
248
- // ---------------------------------------------------------------------------
249
- // Tool handlers
250
- // ---------------------------------------------------------------------------
251
-
252
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
253
- const { name, arguments: args } = request.params;
254
-
255
- try {
256
- if (name === "list_databases") {
257
- const bundleId = args?.bundleId as string | undefined;
258
- const platform = cleanPlatform(args?.platform as string | undefined);
259
- const results = await listDatabases(bundleId, platform);
260
- return {
261
- content: [
262
- { type: "text", text: JSON.stringify(results, null, 2) },
263
- ],
264
- };
265
- }
266
-
267
- if (name === "sync_database") {
268
- const dbName = args?.dbName as string | undefined;
269
- const bundleId = args?.bundleId as string | undefined;
270
- const platform = cleanPlatform(args?.platform as string | undefined);
271
-
272
- const results = await syncDatabase(dbName, bundleId, platform);
273
- activeDatabases = results;
274
-
275
- let msg = "Successfully synced databases:\n";
276
- for (const res of results) {
277
- msg += `- Platform: ${res.platform} | DB: ${res.dbName}\n Path: ${res.localPath}\n`;
278
- }
279
- return { content: [{ type: "text", text: msg }] };
280
- }
281
-
282
- if (name === "inspect_schema") {
283
- const activeDb = await ensureDbState(args);
284
- const schema = await inspectSchema(activeDb.localPath);
285
- return {
286
- content: [
287
- {
288
- type: "text",
289
- text:
290
- `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` +
291
- JSON.stringify(schema, null, 2),
292
- },
293
- ],
294
- };
295
- }
296
-
297
- if (name === "read_table_contents") {
298
- const activeDb = await ensureDbState(args);
299
- const tableName = args?.tableName as string;
300
- const limit = (args?.limit as number) || 100;
301
-
302
- if (!tableName) {
303
- throw new Error("Missing required argument: tableName");
304
- }
305
-
306
- const sql = `SELECT * FROM "${tableName}" LIMIT ?`;
307
- const results = await queryDb(activeDb.localPath, sql, [limit]);
308
- return {
309
- content: [
310
- {
311
- type: "text",
312
- text:
313
- `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName} | Table: ${tableName} | Limit: ${limit}]\n` +
314
- JSON.stringify(results, null, 2),
315
- },
316
- ],
317
- };
318
- }
319
-
320
- if (name === "query_db") {
321
- const activeDb = await ensureDbState(args);
322
- const sql = args?.sql as string;
323
- const params = (args?.params as any[]) || [];
324
-
325
- if (!sql) {
326
- throw new Error("Missing required argument: sql");
327
- }
328
-
329
- const results = await queryDb(activeDb.localPath, sql, params);
330
- return {
331
- content: [
332
- {
333
- type: "text",
334
- text:
335
- `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` +
336
- JSON.stringify(results, null, 2),
337
- },
338
- ],
339
- };
340
- }
341
-
342
- throw new Error(`Unknown tool: ${name}`);
343
- } catch (error: any) {
344
- logger.error(`Tool "${name}" failed`, { message: error.message });
345
- return {
346
- content: [
347
- {
348
- type: "text",
349
- text: `Error: ${error.message}`,
350
- },
351
- ],
352
- isError: true,
353
- };
354
- }
355
- });
356
-
357
- // ---------------------------------------------------------------------------
358
- // Graceful shutdown
359
- // ---------------------------------------------------------------------------
360
-
361
- async function shutdown(signal: string) {
362
- logger.info(`Received ${signal}, shutting down gracefully...`);
363
- try {
364
- await closeAllConnections();
365
- } catch (e) {
366
- logger.error("Error during shutdown", { error: String(e) });
367
- }
368
- process.exit(0);
369
- }
370
-
371
- process.on("SIGINT", () => shutdown("SIGINT"));
372
- process.on("SIGTERM", () => shutdown("SIGTERM"));
373
-
374
- // ---------------------------------------------------------------------------
375
- // Start
376
- // ---------------------------------------------------------------------------
377
-
378
- async function run() {
379
- const transport = new StdioServerTransport();
380
- await server.connect(transport);
381
- logger.info("Universal React Native SQLite MCP Server running on stdio");
382
- }
383
-
384
- run().catch((error) => {
385
- logger.error("Server startup error", { message: error.message, stack: error.stack });
386
- process.exit(1);
387
- });