react-native-sqlite-mcp 1.0.0 → 1.0.1

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/index.ts CHANGED
@@ -7,17 +7,43 @@ import {
7
7
  ListToolsRequestSchema,
8
8
  } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { listDatabases, syncDatabase } from "./locator.js";
10
- import { inspectSchema, queryDb } from "./db.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
+ // ---------------------------------------------------------------------------
11
34
 
12
- // Keep track of the currently active databases
13
35
  interface SyncedDB {
14
36
  localPath: string;
15
37
  dbName: string;
16
- platform: 'ios' | 'android';
38
+ platform: "ios" | "android";
17
39
  }
18
40
 
19
41
  let activeDatabases: SyncedDB[] = [];
20
42
 
43
+ // ---------------------------------------------------------------------------
44
+ // Server setup
45
+ // ---------------------------------------------------------------------------
46
+
21
47
  const server = new Server(
22
48
  {
23
49
  name: "react-native-sqlite-bridge",
@@ -30,138 +56,166 @@ const server = new Server(
30
56
  }
31
57
  );
32
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
+
33
68
  server.setRequestHandler(ListToolsRequestSchema, async () => {
34
69
  return {
35
70
  tools: [
36
71
  {
37
72
  name: "sync_database",
38
- description: "Re-runs the adb pull or file-find logic to ensure the AI is looking at the latest data from the emulator/simulator.",
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.",
39
75
  inputSchema: {
40
76
  type: "object",
41
77
  properties: {
42
78
  dbName: {
43
79
  type: "string",
44
- description: "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."
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.",
45
82
  },
46
83
  bundleId: {
47
84
  type: "string",
48
- description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS."
85
+ description:
86
+ "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
49
87
  },
50
88
  platform: {
51
89
  type: "string",
52
- description: "Optional. Explicitly target 'ios' or 'android'."
53
- }
54
- }
55
- }
90
+ description:
91
+ "Optional. Explicitly target 'ios' or 'android'.",
92
+ },
93
+ },
94
+ },
56
95
  },
57
96
  {
58
97
  name: "list_databases",
59
- description: "Lists all available SQLite databases found on the iOS Simulator or Android Emulator.",
98
+ description:
99
+ "Lists all available SQLite databases found on the iOS Simulator or Android Emulator.",
60
100
  inputSchema: {
61
101
  type: "object",
62
102
  properties: {
63
103
  bundleId: {
64
104
  type: "string",
65
- description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS."
105
+ description:
106
+ "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
66
107
  },
67
108
  platform: {
68
109
  type: "string",
69
- description: "Optional. Explicitly target 'ios' or 'android'."
70
- }
71
- }
72
- }
110
+ description:
111
+ "Optional. Explicitly target 'ios' or 'android'.",
112
+ },
113
+ },
114
+ },
73
115
  },
74
116
  {
75
117
  name: "inspect_schema",
76
- description: "Returns a list of all tables and their column definitions. This gives the AI the 'map' of the database.",
118
+ description:
119
+ "Returns a list of all tables and their column definitions. This gives the AI the 'map' of the database.",
77
120
  inputSchema: {
78
121
  type: "object",
79
122
  properties: {
80
123
  dbName: {
81
124
  type: "string",
82
- description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
125
+ description:
126
+ "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
83
127
  },
84
128
  platform: {
85
129
  type: "string",
86
- description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
87
- }
130
+ description:
131
+ "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
132
+ },
88
133
  },
89
- required: []
90
- }
134
+ required: [],
135
+ },
91
136
  },
92
137
  {
93
138
  name: "read_table_contents",
94
- description: "Returns rows from a specific table. Equivalent to SELECT * FROM table_name.",
139
+ description:
140
+ "Returns rows from a specific table. Equivalent to SELECT * FROM table_name.",
95
141
  inputSchema: {
96
142
  type: "object",
97
143
  properties: {
98
144
  tableName: {
99
145
  type: "string",
100
- description: "The name of the table to read."
146
+ description: "The name of the table to read.",
101
147
  },
102
148
  limit: {
103
149
  type: "number",
104
- description: "Optional limit to the number of rows returned. Defaults to 100."
150
+ description:
151
+ "Optional limit to the number of rows returned. Defaults to 100.",
105
152
  },
106
153
  dbName: {
107
154
  type: "string",
108
- description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
155
+ description:
156
+ "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
109
157
  },
110
158
  platform: {
111
159
  type: "string",
112
- description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
113
- }
160
+ description:
161
+ "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
162
+ },
114
163
  },
115
- required: ["tableName"]
116
- }
164
+ required: ["tableName"],
165
+ },
117
166
  },
118
167
  {
119
168
  name: "query_db",
120
- description: "Accepts a raw SQL SELECT string and returns the JSON result set.",
169
+ description:
170
+ "Accepts a raw SQL SELECT string and returns the JSON result set.",
121
171
  inputSchema: {
122
172
  type: "object",
123
173
  properties: {
124
174
  sql: {
125
175
  type: "string",
126
- description: "The raw SQL SELECT string to execute."
176
+ description: "The raw SQL SELECT string to execute.",
127
177
  },
128
178
  params: {
129
179
  type: "array",
130
- description: "Optional arguments to bind to the SQL query. Use this to safely substitute ? placeholders in your SQL string (e.g. ['value', 42]).",
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]).",
131
182
  items: {
132
- type: ["string", "number", "boolean", "null"],
133
- description: "A single bound parameter value."
134
- }
183
+ description: "A single bound parameter value.",
184
+ },
135
185
  },
136
186
  dbName: {
137
187
  type: "string",
138
- description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
188
+ description:
189
+ "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
139
190
  },
140
191
  platform: {
141
192
  type: "string",
142
- description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
143
- }
193
+ description:
194
+ "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
195
+ },
144
196
  },
145
- required: ["sql"]
146
- }
147
- }
148
- ]
197
+ required: ["sql"],
198
+ },
199
+ },
200
+ ],
149
201
  };
150
202
  });
151
203
 
152
- // Helper to sanitize platform input if any
153
- function cleanPlatform(raw?: string): 'ios' | 'android' | undefined {
204
+ // ---------------------------------------------------------------------------
205
+ // Helpers
206
+ // ---------------------------------------------------------------------------
207
+
208
+ function cleanPlatform(raw?: string): "ios" | "android" | undefined {
154
209
  if (!raw) return undefined;
155
- const cleaned = raw.replace(/['"]/g, '').trim().toLowerCase();
156
- if (cleaned === 'ios' || cleaned === 'android') return cleaned;
157
- return undefined; // If they pass garbage, just let locator try both
210
+ const cleaned = raw.replace(/['"]/g, "").trim().toLowerCase();
211
+ if (cleaned === "ios" || cleaned === "android") return cleaned;
212
+ return undefined;
158
213
  }
159
214
 
160
- // Helper to ensure database is synced based on provided args
161
215
  async function ensureDbState(args: any): Promise<SyncedDB> {
162
216
  const reqDbName = args?.dbName as string | undefined;
163
217
  const reqPlatform = cleanPlatform(args?.platform as string | undefined);
164
-
218
+
165
219
  // If nothing is synced, sync defaults
166
220
  if (activeDatabases.length === 0) {
167
221
  const envDb = reqDbName || process.env.DB_NAME;
@@ -169,23 +223,32 @@ async function ensureDbState(args: any): Promise<SyncedDB> {
169
223
  activeDatabases = await syncDatabase(envDb, envBundle, reqPlatform);
170
224
  }
171
225
 
172
- // Filter based on explicit requirements
173
226
  let candidates = activeDatabases;
174
- if (reqPlatform) candidates = candidates.filter(db => db.platform === reqPlatform);
175
- if (reqDbName) candidates = candidates.filter(db => db.dbName === reqDbName);
227
+ if (reqPlatform)
228
+ candidates = candidates.filter((db) => db.platform === reqPlatform);
229
+ if (reqDbName)
230
+ candidates = candidates.filter((db) => db.dbName === reqDbName);
176
231
 
177
- if (candidates.length === 1) {
178
- return candidates[0];
179
- }
232
+ if (candidates.length === 1) return candidates[0];
180
233
 
181
234
  if (candidates.length === 0) {
182
- throw new Error(`No synced databases match the criteria (platform: ${reqPlatform || 'any'}, dbName: ${reqDbName || 'any'}). Try calling sync_database first.`);
235
+ throw new Error(
236
+ `No synced databases match the criteria (platform: ${reqPlatform || "any"}, dbName: ${reqDbName || "any"}). Try calling sync_database first.`
237
+ );
183
238
  }
184
239
 
185
- const matches = candidates.map(c => `[${c.platform}] ${c.dbName}`).join(", ");
186
- throw new Error(`Multiple databases match the criteria. Please specify 'platform' or 'dbName'. Matches: ${matches}`);
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
+ );
187
246
  }
188
247
 
248
+ // ---------------------------------------------------------------------------
249
+ // Tool handlers
250
+ // ---------------------------------------------------------------------------
251
+
189
252
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
190
253
  const { name, arguments: args } = request.params;
191
254
 
@@ -195,7 +258,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
195
258
  const platform = cleanPlatform(args?.platform as string | undefined);
196
259
  const results = await listDatabases(bundleId, platform);
197
260
  return {
198
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
261
+ content: [
262
+ { type: "text", text: JSON.stringify(results, null, 2) },
263
+ ],
199
264
  };
200
265
  }
201
266
 
@@ -205,33 +270,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
205
270
  const platform = cleanPlatform(args?.platform as string | undefined);
206
271
 
207
272
  const results = await syncDatabase(dbName, bundleId, platform);
208
-
209
- activeDatabases = results; // Replace the active list
273
+ activeDatabases = results;
210
274
 
211
275
  let msg = "Successfully synced databases:\n";
212
276
  for (const res of results) {
213
277
  msg += `- Platform: ${res.platform} | DB: ${res.dbName}\n Path: ${res.localPath}\n`;
214
278
  }
215
-
216
- return {
217
- content: [{ type: "text", text: msg }]
218
- };
279
+ return { content: [{ type: "text", text: msg }] };
219
280
  }
220
281
 
221
282
  if (name === "inspect_schema") {
222
283
  const activeDb = await ensureDbState(args);
223
284
  const schema = await inspectSchema(activeDb.localPath);
224
285
  return {
225
- content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` + JSON.stringify(schema, null, 2) }]
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
+ ],
226
294
  };
227
295
  }
228
296
 
229
297
  if (name === "read_table_contents") {
230
298
  const activeDb = await ensureDbState(args);
231
-
232
299
  const tableName = args?.tableName as string;
233
- const limit = args?.limit as number || 100;
234
-
300
+ const limit = (args?.limit as number) || 100;
301
+
235
302
  if (!tableName) {
236
303
  throw new Error("Missing required argument: tableName");
237
304
  }
@@ -239,47 +306,82 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
239
306
  const sql = `SELECT * FROM "${tableName}" LIMIT ?`;
240
307
  const results = await queryDb(activeDb.localPath, sql, [limit]);
241
308
  return {
242
- content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName} | Table: ${tableName} | Limit: ${limit}]\n` + JSON.stringify(results, null, 2) }]
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
+ ],
243
317
  };
244
318
  }
245
319
 
246
320
  if (name === "query_db") {
247
321
  const activeDb = await ensureDbState(args);
248
-
249
322
  const sql = args?.sql as string;
250
323
  const params = (args?.params as any[]) || [];
251
-
324
+
252
325
  if (!sql) {
253
326
  throw new Error("Missing required argument: sql");
254
327
  }
255
328
 
256
329
  const results = await queryDb(activeDb.localPath, sql, params);
257
330
  return {
258
- content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` + JSON.stringify(results, null, 2) }]
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
+ ],
259
339
  };
260
340
  }
261
341
 
262
342
  throw new Error(`Unknown tool: ${name}`);
263
343
  } catch (error: any) {
344
+ logger.error(`Tool "${name}" failed`, { message: error.message });
264
345
  return {
265
346
  content: [
266
347
  {
267
348
  type: "text",
268
- text: `Error: ${error.message}`
269
- }
349
+ text: `Error: ${error.message}`,
350
+ },
270
351
  ],
271
- isError: true
352
+ isError: true,
272
353
  };
273
354
  }
274
355
  });
275
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
+
276
378
  async function run() {
277
379
  const transport = new StdioServerTransport();
278
380
  await server.connect(transport);
279
- console.error("Universal React Native SQLite MCP Server running on stdio");
381
+ logger.info("Universal React Native SQLite MCP Server running on stdio");
280
382
  }
281
383
 
282
384
  run().catch((error) => {
283
- console.error("Server error:", error);
385
+ logger.error("Server startup error", { message: error.message, stack: error.stack });
284
386
  process.exit(1);
285
387
  });