react-native-sqlite-mcp 1.0.0 → 1.0.2

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/dist/index.js CHANGED
@@ -3,8 +3,27 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { listDatabases, syncDatabase } from "./locator.js";
6
- import { inspectSchema, queryDb } from "./db.js";
6
+ import { inspectSchema, queryDb, closeAllConnections } from "./db.js";
7
+ import { logger } from "./logger.js";
8
+ // ---------------------------------------------------------------------------
9
+ // Process-level guards — prevent silent crashes that cause EOF errors
10
+ // ---------------------------------------------------------------------------
11
+ process.on("uncaughtException", (error) => {
12
+ logger.error("Uncaught exception (process kept alive)", {
13
+ message: error.message,
14
+ stack: error.stack?.slice(0, 500),
15
+ });
16
+ // Do NOT call process.exit() — keep the MCP server alive
17
+ });
18
+ process.on("unhandledRejection", (reason) => {
19
+ logger.error("Unhandled promise rejection (process kept alive)", {
20
+ reason: String(reason),
21
+ });
22
+ });
7
23
  let activeDatabases = [];
24
+ // ---------------------------------------------------------------------------
25
+ // Server setup
26
+ // ---------------------------------------------------------------------------
8
27
  const server = new Server({
9
28
  name: "react-native-sqlite-bridge",
10
29
  version: "1.0.0",
@@ -13,6 +32,13 @@ const server = new Server({
13
32
  tools: {},
14
33
  },
15
34
  });
35
+ // MCP-level transport error handler
36
+ server.onerror = (error) => {
37
+ logger.error("MCP transport error", { message: String(error) });
38
+ };
39
+ // ---------------------------------------------------------------------------
40
+ // Tool definitions
41
+ // ---------------------------------------------------------------------------
16
42
  server.setRequestHandler(ListToolsRequestSchema, async () => {
17
43
  return {
18
44
  tools: [
@@ -24,18 +50,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
24
50
  properties: {
25
51
  dbName: {
26
52
  type: "string",
27
- 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."
53
+ 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.",
28
54
  },
29
55
  bundleId: {
30
56
  type: "string",
31
- description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS."
57
+ description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
32
58
  },
33
59
  platform: {
34
60
  type: "string",
35
- description: "Optional. Explicitly target 'ios' or 'android'."
36
- }
37
- }
38
- }
61
+ description: "Optional. Explicitly target 'ios' or 'android'.",
62
+ },
63
+ },
64
+ },
39
65
  },
40
66
  {
41
67
  name: "list_databases",
@@ -45,14 +71,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
45
71
  properties: {
46
72
  bundleId: {
47
73
  type: "string",
48
- description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS."
74
+ description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
49
75
  },
50
76
  platform: {
51
77
  type: "string",
52
- description: "Optional. Explicitly target 'ios' or 'android'."
53
- }
54
- }
55
- }
78
+ description: "Optional. Explicitly target 'ios' or 'android'.",
79
+ },
80
+ },
81
+ },
56
82
  },
57
83
  {
58
84
  name: "inspect_schema",
@@ -62,15 +88,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
62
88
  properties: {
63
89
  dbName: {
64
90
  type: "string",
65
- description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
91
+ description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
66
92
  },
67
93
  platform: {
68
94
  type: "string",
69
- description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
70
- }
95
+ description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
96
+ },
71
97
  },
72
- required: []
73
- }
98
+ required: [],
99
+ },
74
100
  },
75
101
  {
76
102
  name: "read_table_contents",
@@ -80,23 +106,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
80
106
  properties: {
81
107
  tableName: {
82
108
  type: "string",
83
- description: "The name of the table to read."
109
+ description: "The name of the table to read.",
84
110
  },
85
111
  limit: {
86
112
  type: "number",
87
- description: "Optional limit to the number of rows returned. Defaults to 100."
113
+ description: "Optional limit to the number of rows returned. Defaults to 100.",
88
114
  },
89
115
  dbName: {
90
116
  type: "string",
91
- description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
117
+ description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
92
118
  },
93
119
  platform: {
94
120
  type: "string",
95
- description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
96
- }
121
+ description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
122
+ },
97
123
  },
98
- required: ["tableName"]
99
- }
124
+ required: ["tableName"],
125
+ },
100
126
  },
101
127
  {
102
128
  name: "query_db",
@@ -106,41 +132,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
106
132
  properties: {
107
133
  sql: {
108
134
  type: "string",
109
- description: "The raw SQL SELECT string to execute."
135
+ description: "The raw SQL SELECT string to execute.",
110
136
  },
111
137
  params: {
112
138
  type: "array",
113
139
  description: "Optional arguments to bind to the SQL query. Use this to safely substitute ? placeholders in your SQL string (e.g. ['value', 42]).",
114
140
  items: {
115
- type: ["string", "number", "boolean", "null"],
116
- description: "A single bound parameter value."
117
- }
141
+ description: "A single bound parameter value.",
142
+ },
118
143
  },
119
144
  dbName: {
120
145
  type: "string",
121
- description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
146
+ description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
122
147
  },
123
148
  platform: {
124
149
  type: "string",
125
- description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
126
- }
150
+ description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
151
+ },
127
152
  },
128
- required: ["sql"]
129
- }
130
- }
131
- ]
153
+ required: ["sql"],
154
+ },
155
+ },
156
+ ],
132
157
  };
133
158
  });
134
- // Helper to sanitize platform input if any
159
+ // ---------------------------------------------------------------------------
160
+ // Helpers
161
+ // ---------------------------------------------------------------------------
135
162
  function cleanPlatform(raw) {
136
163
  if (!raw)
137
164
  return undefined;
138
- const cleaned = raw.replace(/['"]/g, '').trim().toLowerCase();
139
- if (cleaned === 'ios' || cleaned === 'android')
165
+ const cleaned = raw.replace(/['"]/g, "").trim().toLowerCase();
166
+ if (cleaned === "ios" || cleaned === "android")
140
167
  return cleaned;
141
- return undefined; // If they pass garbage, just let locator try both
168
+ return undefined;
142
169
  }
143
- // Helper to ensure database is synced based on provided args
144
170
  async function ensureDbState(args) {
145
171
  const reqDbName = args?.dbName;
146
172
  const reqPlatform = cleanPlatform(args?.platform);
@@ -150,21 +176,24 @@ async function ensureDbState(args) {
150
176
  const envBundle = process.env.ANDROID_BUNDLE_ID;
151
177
  activeDatabases = await syncDatabase(envDb, envBundle, reqPlatform);
152
178
  }
153
- // Filter based on explicit requirements
154
179
  let candidates = activeDatabases;
155
180
  if (reqPlatform)
156
- candidates = candidates.filter(db => db.platform === reqPlatform);
181
+ candidates = candidates.filter((db) => db.platform === reqPlatform);
157
182
  if (reqDbName)
158
- candidates = candidates.filter(db => db.dbName === reqDbName);
159
- if (candidates.length === 1) {
183
+ candidates = candidates.filter((db) => db.dbName === reqDbName);
184
+ if (candidates.length === 1)
160
185
  return candidates[0];
161
- }
162
186
  if (candidates.length === 0) {
163
- throw new Error(`No synced databases match the criteria (platform: ${reqPlatform || 'any'}, dbName: ${reqDbName || 'any'}). Try calling sync_database first.`);
187
+ throw new Error(`No synced databases match the criteria (platform: ${reqPlatform || "any"}, dbName: ${reqDbName || "any"}). Try calling sync_database first.`);
164
188
  }
165
- const matches = candidates.map(c => `[${c.platform}] ${c.dbName}`).join(", ");
189
+ const matches = candidates
190
+ .map((c) => `[${c.platform}] ${c.dbName}`)
191
+ .join(", ");
166
192
  throw new Error(`Multiple databases match the criteria. Please specify 'platform' or 'dbName'. Matches: ${matches}`);
167
193
  }
194
+ // ---------------------------------------------------------------------------
195
+ // Tool handlers
196
+ // ---------------------------------------------------------------------------
168
197
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
169
198
  const { name, arguments: args } = request.params;
170
199
  try {
@@ -173,7 +202,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
173
202
  const platform = cleanPlatform(args?.platform);
174
203
  const results = await listDatabases(bundleId, platform);
175
204
  return {
176
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
205
+ content: [
206
+ { type: "text", text: JSON.stringify(results, null, 2) },
207
+ ],
177
208
  };
178
209
  }
179
210
  if (name === "sync_database") {
@@ -181,20 +212,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
181
212
  const bundleId = args?.bundleId;
182
213
  const platform = cleanPlatform(args?.platform);
183
214
  const results = await syncDatabase(dbName, bundleId, platform);
184
- activeDatabases = results; // Replace the active list
215
+ activeDatabases = results;
185
216
  let msg = "Successfully synced databases:\n";
186
217
  for (const res of results) {
187
218
  msg += `- Platform: ${res.platform} | DB: ${res.dbName}\n Path: ${res.localPath}\n`;
188
219
  }
189
- return {
190
- content: [{ type: "text", text: msg }]
191
- };
220
+ return { content: [{ type: "text", text: msg }] };
192
221
  }
193
222
  if (name === "inspect_schema") {
194
223
  const activeDb = await ensureDbState(args);
195
224
  const schema = await inspectSchema(activeDb.localPath);
196
225
  return {
197
- content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` + JSON.stringify(schema, null, 2) }]
226
+ content: [
227
+ {
228
+ type: "text",
229
+ text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` +
230
+ JSON.stringify(schema, null, 2),
231
+ },
232
+ ],
198
233
  };
199
234
  }
200
235
  if (name === "read_table_contents") {
@@ -207,7 +242,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
207
242
  const sql = `SELECT * FROM "${tableName}" LIMIT ?`;
208
243
  const results = await queryDb(activeDb.localPath, sql, [limit]);
209
244
  return {
210
- content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName} | Table: ${tableName} | Limit: ${limit}]\n` + JSON.stringify(results, null, 2) }]
245
+ content: [
246
+ {
247
+ type: "text",
248
+ text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName} | Table: ${tableName} | Limit: ${limit}]\n` +
249
+ JSON.stringify(results, null, 2),
250
+ },
251
+ ],
211
252
  };
212
253
  }
213
254
  if (name === "query_db") {
@@ -219,29 +260,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
219
260
  }
220
261
  const results = await queryDb(activeDb.localPath, sql, params);
221
262
  return {
222
- content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` + JSON.stringify(results, null, 2) }]
263
+ content: [
264
+ {
265
+ type: "text",
266
+ text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` +
267
+ JSON.stringify(results, null, 2),
268
+ },
269
+ ],
223
270
  };
224
271
  }
225
272
  throw new Error(`Unknown tool: ${name}`);
226
273
  }
227
274
  catch (error) {
275
+ logger.error(`Tool "${name}" failed`, { message: error.message });
228
276
  return {
229
277
  content: [
230
278
  {
231
279
  type: "text",
232
- text: `Error: ${error.message}`
233
- }
280
+ text: `Error: ${error.message}`,
281
+ },
234
282
  ],
235
- isError: true
283
+ isError: true,
236
284
  };
237
285
  }
238
286
  });
287
+ // ---------------------------------------------------------------------------
288
+ // Graceful shutdown
289
+ // ---------------------------------------------------------------------------
290
+ async function shutdown(signal) {
291
+ logger.info(`Received ${signal}, shutting down gracefully...`);
292
+ try {
293
+ await closeAllConnections();
294
+ }
295
+ catch (e) {
296
+ logger.error("Error during shutdown", { error: String(e) });
297
+ }
298
+ process.exit(0);
299
+ }
300
+ process.on("SIGINT", () => shutdown("SIGINT"));
301
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
302
+ // ---------------------------------------------------------------------------
303
+ // Start
304
+ // ---------------------------------------------------------------------------
239
305
  async function run() {
240
306
  const transport = new StdioServerTransport();
241
307
  await server.connect(transport);
242
- console.error("Universal React Native SQLite MCP Server running on stdio");
308
+ logger.info("Universal React Native SQLite MCP Server running on stdio");
243
309
  }
244
310
  run().catch((error) => {
245
- console.error("Server error:", error);
311
+ logger.error("Server startup error", { message: error.message, stack: error.stack });
246
312
  process.exit(1);
247
313
  });
package/dist/locator.js CHANGED
@@ -1,18 +1,18 @@
1
- import { execSync } from "child_process";
2
1
  import fs from "fs";
3
2
  import path from "path";
4
3
  import os from "os";
4
+ import { shell } from "./shell.js";
5
+ import { logger } from "./logger.js";
5
6
  export async function listDatabases(bundleId, targetPlatform) {
6
7
  const results = [];
7
8
  if (!targetPlatform || targetPlatform === 'ios') {
8
9
  try {
9
- const udidStr = execSync("xcrun simctl list devices booted | awk -F '[()]' '/Booted/{print $2; exit}'", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 }).toString().trim();
10
+ const udidStr = await shell("xcrun simctl list devices booted | awk -F '[()]' '/Booted/{print $2; exit}'", { timeout: 5_000, label: "xcrun-simctl-booted" });
10
11
  if (udidStr) {
11
12
  const appDataDir = `${process.env.HOME}/Library/Developer/CoreSimulator/Devices/${udidStr}/data/Containers/Data/Application`;
12
13
  if (fs.existsSync(appDataDir)) {
13
14
  try {
14
- const findCmd = `find "${appDataDir}" -type f \\( -name "*.db" -o -name "*.sqlite" -o -name "*.sqlite3" \\) -maxdepth 7 -print`;
15
- const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 10000 }).toString().trim();
15
+ const found = await shell(`find "${appDataDir}" -type f \\( -name "*.db" -o -name "*.sqlite" -o -name "*.sqlite3" \\) -maxdepth 7 -print`, { timeout: 15_000, label: "ios-find-dbs" });
16
16
  if (found) {
17
17
  results.push({
18
18
  platform: 'ios',
@@ -22,7 +22,7 @@ export async function listDatabases(bundleId, targetPlatform) {
22
22
  }
23
23
  }
24
24
  catch (e) {
25
- console.error("iOS find failed", e);
25
+ logger.warn("iOS find failed", { error: String(e) });
26
26
  }
27
27
  }
28
28
  }
@@ -38,7 +38,7 @@ export async function listDatabases(bundleId, targetPlatform) {
38
38
  }
39
39
  if (!targetPlatform || targetPlatform === 'android') {
40
40
  try {
41
- execSync("adb get-state", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 });
41
+ await shell("adb get-state", { timeout: 5_000, label: "adb-get-state" });
42
42
  }
43
43
  catch (e) {
44
44
  if (targetPlatform === 'android') {
@@ -56,7 +56,7 @@ export async function listDatabases(bundleId, targetPlatform) {
56
56
  }
57
57
  else {
58
58
  try {
59
- const packagesStr = execSync("adb shell pm list packages -3", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
59
+ const packagesStr = await shell("adb shell pm list packages -3", { timeout: 8_000, label: "adb-list-packages", retries: 1, retryDelay: 1_000 });
60
60
  packagesToScan = packagesStr.split('\n')
61
61
  .map(line => line.replace('package:', '').trim())
62
62
  .filter(Boolean);
@@ -74,18 +74,16 @@ export async function listDatabases(bundleId, targetPlatform) {
74
74
  const baseDirs = [`/data/user/0/${pkg}`, `/data/data/${pkg}`];
75
75
  for (const baseDir of baseDirs) {
76
76
  try {
77
- execSync(`adb shell run-as ${pkg} ls -d ${baseDir}`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 2000 });
77
+ await shell(`adb shell run-as ${pkg} ls -d ${baseDir}`, { timeout: 3_000, label: `adb-ls-${pkg}` });
78
78
  let foundFiles = [];
79
- try {
80
- const findCmd = `adb shell "run-as ${pkg} find ${baseDir} -type f \\( -name \\"*.db\\" -o -name \\"*.sqlite\\" -o -name \\"*.sqlite3\\" \\)"`;
81
- const findOut = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
82
- if (findOut) {
83
- foundFiles.push(...findOut.split('\n').map(l => l.trim().replace(/\r/g, '')).filter(Boolean));
84
- }
79
+ // find .db / .sqlite / .sqlite3 files recursively
80
+ const findOut = await shell(`adb shell "run-as ${pkg} find ${baseDir} -type f \\( -name \\"*.db\\" -o -name \\"*.sqlite\\" -o -name \\"*.sqlite3\\" \\)"`, { timeout: 8_000, ignoreErrors: true, label: `adb-find-${pkg}` });
81
+ if (findOut) {
82
+ foundFiles.push(...findOut.split('\n').map(l => l.trim().replace(/\r/g, '')).filter(Boolean));
85
83
  }
86
- catch (e) { }
87
- try {
88
- const lsOut = execSync(`adb shell run-as ${pkg} ls -1p ${baseDir}/databases`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 2000 }).toString().trim();
84
+ // also check for extensionless files in /databases
85
+ const lsOut = await shell(`adb shell run-as ${pkg} ls -1p ${baseDir}/databases`, { timeout: 3_000, ignoreErrors: true, label: `adb-ls-dbs-${pkg}` });
86
+ if (lsOut) {
89
87
  const lsFiles = lsOut.split('\n')
90
88
  .map(l => l.trim().replace(/\r/g, ''))
91
89
  .filter(Boolean)
@@ -95,7 +93,6 @@ export async function listDatabases(bundleId, targetPlatform) {
95
93
  .map(f => `${baseDir}/databases/${f}`);
96
94
  foundFiles.push(...lsFiles);
97
95
  }
98
- catch (e) { }
99
96
  // Deduplicate
100
97
  foundFiles = [...new Set(foundFiles)];
101
98
  if (foundFiles.length > 0) {
@@ -106,7 +103,7 @@ export async function listDatabases(bundleId, targetPlatform) {
106
103
  }
107
104
  }
108
105
  catch (e) {
109
- console.error(`Failed to list databases for app: ${pkg}`);
106
+ logger.debug(`Failed to list databases for app: ${pkg}`);
110
107
  }
111
108
  }
112
109
  }
@@ -123,14 +120,7 @@ export async function listDatabases(bundleId, targetPlatform) {
123
120
  }
124
121
  return results;
125
122
  }
126
- /**
127
- * Finds the DB file based on platform and a provided/detected filename.
128
- * If dbNameGlob is empty/undefined, it will auto-select the first discovered database.
129
- * Prioritizes iOS (simctl), falls back to Android (adb).
130
- * Returns the local file path (either the iOS original or a pulled Android copy).
131
- */
132
123
  export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
133
- // First, discover available databases
134
124
  const locations = await listDatabases(bundleId, targetPlatform);
135
125
  if (locations.length === 0) {
136
126
  if (targetPlatform) {
@@ -144,13 +134,12 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
144
134
  continue;
145
135
  let targetDbNames = [];
146
136
  if (!dbNameGlob) {
147
- // Auto-select: find the first .db or .sqlite file in this location
148
137
  const preferred = loc.databases.find(d => d.endsWith('.db') || d.endsWith('.sqlite'));
149
138
  if (preferred) {
150
139
  targetDbNames.push(preferred);
151
140
  }
152
141
  else if (loc.databases.length > 0) {
153
- targetDbNames.push(loc.databases[0]); // fallback to first file
142
+ targetDbNames.push(loc.databases[0]);
154
143
  }
155
144
  }
156
145
  else {
@@ -163,32 +152,25 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
163
152
  if (platform === 'ios') {
164
153
  if (!appDir)
165
154
  continue;
166
- // Find the exact path of the targetDbName
167
- const findCmd = `find "${appDir}" -type f -name "${targetDbName}" -maxdepth 7 -print | head -n 1`;
168
155
  try {
169
- const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
156
+ const found = await shell(`find "${appDir}" -type f -name "${targetDbName}" -maxdepth 7 -print | head -n 1`, { timeout: 8_000, label: `ios-find-${targetDbName}` });
170
157
  if (found && fs.existsSync(found)) {
171
- console.error(`Located iOS DB at: ${found}`);
158
+ logger.info(`Located iOS DB at: ${found}`);
172
159
  synced.push({ localPath: found, dbName: targetDbName, platform: 'ios' });
173
160
  }
174
161
  }
175
162
  catch (e) {
176
- console.error(`Failed to locate full path for iOS DB: ${targetDbName}`);
163
+ logger.warn(`Failed to locate full path for iOS DB: ${targetDbName}`);
177
164
  }
178
165
  continue;
179
166
  }
180
167
  // --- Android Logic ---
181
168
  if (!appDir || !appDir.includes("::")) {
182
- console.error(`Invalid Android appDir format: ${appDir}`);
169
+ logger.warn(`Invalid Android appDir format: ${appDir}`);
183
170
  continue;
184
171
  }
185
172
  const [targetDbDir, targetPkg] = appDir.split("::");
186
- try {
187
- execSync(`adb shell am force-stop ${targetPkg}`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 });
188
- }
189
- catch (e) {
190
- console.error(`Failed to force-stop app: ${targetPkg}`);
191
- }
173
+ await shell(`adb shell am force-stop ${targetPkg}`, { timeout: 5_000, ignoreErrors: true, label: `adb-force-stop-${targetPkg}` });
192
174
  const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "rn-sqlite-mcp-"));
193
175
  const safeLocalName = targetDbName.replace(/\//g, '_');
194
176
  const localDb = path.join(tmpdir, safeLocalName);
@@ -197,13 +179,13 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
197
179
  const remoteMain = `${targetDbDir}/${targetDbName}`;
198
180
  const remoteWal = `${remoteMain}-wal`;
199
181
  const remoteShm = `${remoteMain}-shm`;
200
- const pullOne = (remote, local) => {
182
+ const pullOne = async (remote, local) => {
201
183
  try {
202
184
  const remoteBase = path.basename(remote);
203
185
  const tmpRemote = `/data/local/tmp/${targetPkg}_${remoteBase}_${Date.now()}`;
204
- execSync(`adb shell "run-as '${targetPkg}' cat '${remote}' > '${tmpRemote}'"`, { stdio: 'ignore', timeout: 5000 });
205
- execSync(`adb pull '${tmpRemote}' '${local}'`, { stdio: 'ignore', timeout: 5000 });
206
- execSync(`adb shell rm '${tmpRemote}'`, { stdio: 'ignore', timeout: 3000 });
186
+ await shell(`adb shell "run-as '${targetPkg}' cat '${remote}' > '${tmpRemote}'"`, { timeout: 10_000, retries: 1, retryDelay: 1_000, label: `adb-cat-${remoteBase}` });
187
+ await shell(`adb pull '${tmpRemote}' '${local}'`, { timeout: 10_000, label: `adb-pull-${remoteBase}` });
188
+ await shell(`adb shell rm '${tmpRemote}'`, { timeout: 5_000, ignoreErrors: true, label: `adb-rm-tmp-${remoteBase}` });
207
189
  return fs.existsSync(local) && fs.statSync(local).size > 0;
208
190
  }
209
191
  catch (e) {
@@ -212,13 +194,14 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
212
194
  return false;
213
195
  }
214
196
  };
215
- if (!pullOne(remoteMain, localDb)) {
216
- console.error(`Failed to pull main DB file from Android: ${remoteMain}`);
197
+ if (!(await pullOne(remoteMain, localDb))) {
198
+ logger.warn(`Failed to pull main DB file from Android: ${remoteMain}`);
217
199
  continue;
218
200
  }
219
- pullOne(remoteWal, localWal);
220
- pullOne(remoteShm, localShm);
221
- console.error(`Pulled Android DB to local temp: ${localDb}`);
201
+ // WAL and SHM are best-effort
202
+ await pullOne(remoteWal, localWal);
203
+ await pullOne(remoteShm, localShm);
204
+ logger.info(`Pulled Android DB to local temp: ${localDb}`);
222
205
  synced.push({ localPath: localDb, dbName: targetDbName, platform: 'android' });
223
206
  }
224
207
  }
package/dist/logger.js ADDED
@@ -0,0 +1,29 @@
1
+ const LOG_LEVELS = {
2
+ debug: 0,
3
+ info: 1,
4
+ warn: 2,
5
+ error: 3,
6
+ };
7
+ const minLevel = process.env.MCP_LOG_LEVEL || 'info';
8
+ function shouldLog(level) {
9
+ return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
10
+ }
11
+ function formatTimestamp() {
12
+ return new Date().toISOString();
13
+ }
14
+ function log(level, message, meta) {
15
+ if (!shouldLog(level))
16
+ return;
17
+ const parts = [`[${formatTimestamp()}]`, `[${level.toUpperCase()}]`, message];
18
+ if (meta && Object.keys(meta).length > 0) {
19
+ parts.push(JSON.stringify(meta));
20
+ }
21
+ // CRITICAL: Always stderr — stdout is reserved for MCP JSON-RPC
22
+ process.stderr.write(parts.join(' ') + '\n');
23
+ }
24
+ export const logger = {
25
+ debug: (msg, meta) => log('debug', msg, meta),
26
+ info: (msg, meta) => log('info', msg, meta),
27
+ warn: (msg, meta) => log('warn', msg, meta),
28
+ error: (msg, meta) => log('error', msg, meta),
29
+ };
package/dist/shell.js ADDED
@@ -0,0 +1,48 @@
1
+ import { exec as execCb } from "child_process";
2
+ import { promisify } from "util";
3
+ import { logger } from "./logger.js";
4
+ const execAsync = promisify(execCb);
5
+ function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+ export async function shell(command, options = {}) {
9
+ const { timeout = 10_000, retries = 0, retryDelay = 1_000, ignoreErrors = false, label, } = options;
10
+ const tag = label || command.slice(0, 60);
11
+ for (let attempt = 0; attempt <= retries; attempt++) {
12
+ try {
13
+ if (attempt > 0) {
14
+ const delay = retryDelay * Math.pow(2, attempt - 1);
15
+ logger.debug(`Retry ${attempt}/${retries} for "${tag}" after ${delay}ms`);
16
+ await sleep(delay);
17
+ }
18
+ logger.debug(`Executing: "${tag}"`, { timeout, attempt });
19
+ const result = await execAsync(command, {
20
+ timeout,
21
+ maxBuffer: 10 * 1024 * 1024, // 10MB — large schema dumps
22
+ env: { ...process.env },
23
+ });
24
+ return result.stdout.trim();
25
+ }
26
+ catch (error) {
27
+ const isLastAttempt = attempt >= retries;
28
+ if (error.killed) {
29
+ logger.warn(`Command timed out after ${timeout}ms: "${tag}"`);
30
+ }
31
+ else {
32
+ logger.debug(`Command failed: "${tag}"`, {
33
+ code: error.code,
34
+ stderr: error.stderr?.slice(0, 200),
35
+ });
36
+ }
37
+ if (isLastAttempt) {
38
+ if (ignoreErrors) {
39
+ logger.debug(`Ignoring error for "${tag}"`);
40
+ return "";
41
+ }
42
+ throw new Error(`Shell command failed: ${tag}\n${error.message || error.stderr || "Unknown error"}`);
43
+ }
44
+ }
45
+ }
46
+ // Unreachable, but TypeScript needs it
47
+ return "";
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-sqlite-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Universal React Native SQLite MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -23,11 +23,11 @@
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/sdk": "^1.6.0",
26
- "sqlite3": "^5.1.7"
26
+ "sql.js": "^1.14.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.13.4",
30
- "@types/sqlite3": "^3.1.11",
30
+ "@types/sql.js": "^1.4.9",
31
31
  "typescript": "^5.7.3"
32
32
  }
33
33
  }