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/locator.ts CHANGED
@@ -1,7 +1,8 @@
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
 
6
7
  export interface DatabaseLocation {
7
8
  platform: 'ios' | 'android';
@@ -14,13 +15,19 @@ export async function listDatabases(bundleId?: string, targetPlatform?: 'ios' |
14
15
 
15
16
  if (!targetPlatform || targetPlatform === 'ios') {
16
17
  try {
17
- const udidStr = execSync("xcrun simctl list devices booted | awk -F '[()]' '/Booted/{print $2; exit}'", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 }).toString().trim();
18
+ const udidStr = await shell(
19
+ "xcrun simctl list devices booted | awk -F '[()]' '/Booted/{print $2; exit}'",
20
+ { timeout: 5_000, label: "xcrun-simctl-booted" }
21
+ );
22
+
18
23
  if (udidStr) {
19
24
  const appDataDir = `${process.env.HOME}/Library/Developer/CoreSimulator/Devices/${udidStr}/data/Containers/Data/Application`;
20
25
  if (fs.existsSync(appDataDir)) {
21
26
  try {
22
- const findCmd = `find "${appDataDir}" -type f \\( -name "*.db" -o -name "*.sqlite" -o -name "*.sqlite3" \\) -maxdepth 7 -print`;
23
- const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 10000 }).toString().trim();
27
+ const found = await shell(
28
+ `find "${appDataDir}" -type f \\( -name "*.db" -o -name "*.sqlite" -o -name "*.sqlite3" \\) -maxdepth 7 -print`,
29
+ { timeout: 15_000, label: "ios-find-dbs" }
30
+ );
24
31
  if (found) {
25
32
  results.push({
26
33
  platform: 'ios',
@@ -29,7 +36,7 @@ export async function listDatabases(bundleId?: string, targetPlatform?: 'ios' |
29
36
  });
30
37
  }
31
38
  } catch (e) {
32
- console.error("iOS find failed", e);
39
+ logger.warn("iOS find failed", { error: String(e) });
33
40
  }
34
41
  }
35
42
  } else if (targetPlatform === 'ios') {
@@ -44,7 +51,7 @@ export async function listDatabases(bundleId?: string, targetPlatform?: 'ios' |
44
51
 
45
52
  if (!targetPlatform || targetPlatform === 'android') {
46
53
  try {
47
- execSync("adb get-state", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 });
54
+ await shell("adb get-state", { timeout: 5_000, label: "adb-get-state" });
48
55
  } catch (e) {
49
56
  if (targetPlatform === 'android') {
50
57
  throw new Error("No booted Android Emulator found or adb is unresponsive.");
@@ -52,7 +59,7 @@ export async function listDatabases(bundleId?: string, targetPlatform?: 'ios' |
52
59
  if (results.length === 0) {
53
60
  throw new Error("No booted iOS Simulator or Android Emulator device found.");
54
61
  }
55
- return results;
62
+ return results;
56
63
  }
57
64
 
58
65
  // if we have a specific bundleId-use it otherwise hunt
@@ -61,7 +68,10 @@ export async function listDatabases(bundleId?: string, targetPlatform?: 'ios' |
61
68
  packagesToScan = [bundleId];
62
69
  } else {
63
70
  try {
64
- const packagesStr = execSync("adb shell pm list packages -3", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
71
+ const packagesStr = await shell(
72
+ "adb shell pm list packages -3",
73
+ { timeout: 8_000, label: "adb-list-packages", retries: 1, retryDelay: 1_000 }
74
+ );
65
75
  packagesToScan = packagesStr.split('\n')
66
76
  .map(line => line.replace('package:', '').trim())
67
77
  .filter(Boolean);
@@ -78,76 +88,76 @@ export async function listDatabases(bundleId?: string, targetPlatform?: 'ios' |
78
88
 
79
89
  for (const pkg of packagesToScan) {
80
90
  const baseDirs = [`/data/user/0/${pkg}`, `/data/data/${pkg}`];
81
-
82
91
  for (const baseDir of baseDirs) {
83
92
  try {
84
- execSync(`adb shell run-as ${pkg} ls -d ${baseDir}`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 2000 });
85
-
93
+ await shell(
94
+ `adb shell run-as ${pkg} ls -d ${baseDir}`,
95
+ { timeout: 3_000, label: `adb-ls-${pkg}` }
96
+ );
97
+
86
98
  let foundFiles: string[] = [];
87
-
88
- try {
89
- const findCmd = `adb shell "run-as ${pkg} find ${baseDir} -type f \\( -name \\"*.db\\" -o -name \\"*.sqlite\\" -o -name \\"*.sqlite3\\" \\)"`;
90
- const findOut = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
91
- if (findOut) {
92
- foundFiles.push(...findOut.split('\n').map(l => l.trim().replace(/\r/g, '')).filter(Boolean));
93
- }
94
- } catch (e) {}
95
-
96
- try {
97
- const lsOut = execSync(`adb shell run-as ${pkg} ls -1p ${baseDir}/databases`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 2000 }).toString().trim();
99
+
100
+ // find .db / .sqlite / .sqlite3 files recursively
101
+ const findOut = await shell(
102
+ `adb shell "run-as ${pkg} find ${baseDir} -type f \\( -name \\"*.db\\" -o -name \\"*.sqlite\\" -o -name \\"*.sqlite3\\" \\)"`,
103
+ { timeout: 8_000, ignoreErrors: true, label: `adb-find-${pkg}` }
104
+ );
105
+ if (findOut) {
106
+ foundFiles.push(...findOut.split('\n').map(l => l.trim().replace(/\r/g, '')).filter(Boolean));
107
+ }
108
+
109
+ // also check for extensionless files in /databases
110
+ const lsOut = await shell(
111
+ `adb shell run-as ${pkg} ls -1p ${baseDir}/databases`,
112
+ { timeout: 3_000, ignoreErrors: true, label: `adb-ls-dbs-${pkg}` }
113
+ );
114
+ if (lsOut) {
98
115
  const lsFiles = lsOut.split('\n')
99
116
  .map(l => l.trim().replace(/\r/g, ''))
100
117
  .filter(Boolean)
101
118
  .filter(f => !f.endsWith('/'))
102
119
  .filter(f => !f.endsWith('-journal') && !f.endsWith('-wal') && !f.endsWith('-shm'))
103
- .filter(f => !f.includes('.'))
120
+ .filter(f => !f.includes('.'))
104
121
  .map(f => `${baseDir}/databases/${f}`);
105
- foundFiles.push(...lsFiles);
106
- } catch (e) {}
122
+ foundFiles.push(...lsFiles);
123
+ }
107
124
 
108
125
  // Deduplicate
109
126
  foundFiles = [...new Set(foundFiles)];
110
127
 
111
128
  if (foundFiles.length > 0) {
112
- const displayFiles = foundFiles.map(f => f.replace(`${baseDir}/`, ''));
113
-
114
- allAndroidDatabases.push(...displayFiles);
115
- lastSuccessfulAppDir = `${baseDir}::${pkg}`;
116
- break;
129
+ const displayFiles = foundFiles.map(f => f.replace(`${baseDir}/`, ''));
130
+ allAndroidDatabases.push(...displayFiles);
131
+ lastSuccessfulAppDir = `${baseDir}::${pkg}`;
132
+ break;
117
133
  }
118
134
  } catch (e) {
119
- console.error(`Failed to list databases for app: ${pkg}`);
135
+ logger.debug(`Failed to list databases for app: ${pkg}`);
120
136
  }
121
137
  }
122
138
  }
123
139
 
124
140
  if (allAndroidDatabases.length > 0) {
125
- results.push({
141
+ results.push({
126
142
  platform: 'android',
127
143
  appDir: lastSuccessfulAppDir,
128
144
  databases: allAndroidDatabases
129
145
  });
130
146
  } else if (targetPlatform === 'android') {
131
- throw new Error(`Android Emulator is booted, but no SQLite databases were found in any debuggable third-party packages.`);
147
+ throw new Error(`Android Emulator is booted, but no SQLite databases were found in any debuggable third-party packages.`);
132
148
  }
133
149
  }
134
-
150
+
135
151
  return results;
136
152
  }
137
153
 
138
- /**
139
- * Finds the DB file based on platform and a provided/detected filename.
140
- * If dbNameGlob is empty/undefined, it will auto-select the first discovered database.
141
- * Prioritizes iOS (simctl), falls back to Android (adb).
142
- * Returns the local file path (either the iOS original or a pulled Android copy).
143
- */
154
+
144
155
  export async function syncDatabase(dbNameGlob?: string, bundleId?: string, targetPlatform?: 'ios' | 'android'): Promise<{ localPath: string, dbName: string, platform: 'ios' | 'android' }[]> {
145
- // First, discover available databases
146
156
  const locations = await listDatabases(bundleId, targetPlatform);
147
157
 
148
158
  if (locations.length === 0) {
149
159
  if (targetPlatform) {
150
- throw new Error(`No SQLite databases found for platform '${targetPlatform}'.`);
160
+ throw new Error(`No SQLite databases found for platform '${targetPlatform}'.`);
151
161
  }
152
162
  throw new Error(`No SQLite databases found on any platform.`);
153
163
  }
@@ -156,16 +166,14 @@ export async function syncDatabase(dbNameGlob?: string, bundleId?: string, targe
156
166
 
157
167
  for (const loc of locations) {
158
168
  if (targetPlatform && loc.platform !== targetPlatform) continue;
159
-
160
169
  let targetDbNames: string[] = [];
161
170
 
162
171
  if (!dbNameGlob) {
163
- // Auto-select: find the first .db or .sqlite file in this location
164
172
  const preferred = loc.databases.find(d => d.endsWith('.db') || d.endsWith('.sqlite'));
165
173
  if (preferred) {
166
174
  targetDbNames.push(preferred);
167
175
  } else if (loc.databases.length > 0) {
168
- targetDbNames.push(loc.databases[0]); // fallback to first file
176
+ targetDbNames.push(loc.databases[0]);
169
177
  }
170
178
  } else {
171
179
  const globRegex = new RegExp('^' + dbNameGlob.replace(/\*/g, '.*') + '$');
@@ -178,34 +186,34 @@ export async function syncDatabase(dbNameGlob?: string, bundleId?: string, targe
178
186
  // --- iOS Logic ---
179
187
  if (platform === 'ios') {
180
188
  if (!appDir) continue;
181
-
182
- // Find the exact path of the targetDbName
183
- const findCmd = `find "${appDir}" -type f -name "${targetDbName}" -maxdepth 7 -print | head -n 1`;
189
+
184
190
  try {
185
- const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
191
+ const found = await shell(
192
+ `find "${appDir}" -type f -name "${targetDbName}" -maxdepth 7 -print | head -n 1`,
193
+ { timeout: 8_000, label: `ios-find-${targetDbName}` }
194
+ );
186
195
  if (found && fs.existsSync(found)) {
187
- console.error(`Located iOS DB at: ${found}`);
196
+ logger.info(`Located iOS DB at: ${found}`);
188
197
  synced.push({ localPath: found, dbName: targetDbName, platform: 'ios' });
189
198
  }
190
199
  } catch (e) {
191
- console.error(`Failed to locate full path for iOS DB: ${targetDbName}`);
200
+ logger.warn(`Failed to locate full path for iOS DB: ${targetDbName}`);
192
201
  }
193
202
  continue;
194
203
  }
195
204
 
196
205
  // --- Android Logic ---
197
206
  if (!appDir || !appDir.includes("::")) {
198
- console.error(`Invalid Android appDir format: ${appDir}`);
207
+ logger.warn(`Invalid Android appDir format: ${appDir}`);
199
208
  continue;
200
209
  }
201
-
210
+
202
211
  const [targetDbDir, targetPkg] = appDir.split("::");
203
-
204
- try {
205
- execSync(`adb shell am force-stop ${targetPkg}`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 });
206
- } catch (e) {
207
- console.error(`Failed to force-stop app: ${targetPkg}`);
208
- }
212
+
213
+ await shell(
214
+ `adb shell am force-stop ${targetPkg}`,
215
+ { timeout: 5_000, ignoreErrors: true, label: `adb-force-stop-${targetPkg}` }
216
+ );
209
217
 
210
218
  const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "rn-sqlite-mcp-"));
211
219
  const safeLocalName = targetDbName.replace(/\//g, '_');
@@ -217,16 +225,24 @@ export async function syncDatabase(dbNameGlob?: string, bundleId?: string, targe
217
225
  const remoteWal = `${remoteMain}-wal`;
218
226
  const remoteShm = `${remoteMain}-shm`;
219
227
 
220
-
221
- const pullOne = (remote: string, local: string) => {
228
+ const pullOne = async (remote: string, local: string): Promise<boolean> => {
222
229
  try {
223
230
  const remoteBase = path.basename(remote);
224
231
  const tmpRemote = `/data/local/tmp/${targetPkg}_${remoteBase}_${Date.now()}`;
225
-
226
- execSync(`adb shell "run-as '${targetPkg}' cat '${remote}' > '${tmpRemote}'"`, { stdio: 'ignore', timeout: 5000 });
227
- execSync(`adb pull '${tmpRemote}' '${local}'`, { stdio: 'ignore', timeout: 5000 });
228
- execSync(`adb shell rm '${tmpRemote}'`, { stdio: 'ignore', timeout: 3000 });
229
-
232
+
233
+ await shell(
234
+ `adb shell "run-as '${targetPkg}' cat '${remote}' > '${tmpRemote}'"`,
235
+ { timeout: 10_000, retries: 1, retryDelay: 1_000, label: `adb-cat-${remoteBase}` }
236
+ );
237
+ await shell(
238
+ `adb pull '${tmpRemote}' '${local}'`,
239
+ { timeout: 10_000, label: `adb-pull-${remoteBase}` }
240
+ );
241
+ await shell(
242
+ `adb shell rm '${tmpRemote}'`,
243
+ { timeout: 5_000, ignoreErrors: true, label: `adb-rm-tmp-${remoteBase}` }
244
+ );
245
+
230
246
  return fs.existsSync(local) && fs.statSync(local).size > 0;
231
247
  } catch (e) {
232
248
  if (fs.existsSync(local)) fs.unlinkSync(local);
@@ -234,15 +250,16 @@ export async function syncDatabase(dbNameGlob?: string, bundleId?: string, targe
234
250
  }
235
251
  };
236
252
 
237
- if (!pullOne(remoteMain, localDb)) {
238
- console.error(`Failed to pull main DB file from Android: ${remoteMain}`);
253
+ if (!(await pullOne(remoteMain, localDb))) {
254
+ logger.warn(`Failed to pull main DB file from Android: ${remoteMain}`);
239
255
  continue;
240
256
  }
241
257
 
242
- pullOne(remoteWal, localWal);
243
- pullOne(remoteShm, localShm);
258
+ // WAL and SHM are best-effort
259
+ await pullOne(remoteWal, localWal);
260
+ await pullOne(remoteShm, localShm);
244
261
 
245
- console.error(`Pulled Android DB to local temp: ${localDb}`);
262
+ logger.info(`Pulled Android DB to local temp: ${localDb}`);
246
263
  synced.push({ localPath: localDb, dbName: targetDbName, platform: 'android' });
247
264
  }
248
265
  }
package/src/logger.ts ADDED
@@ -0,0 +1,37 @@
1
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+
3
+ const LOG_LEVELS: Record<LogLevel, number> = {
4
+ debug: 0,
5
+ info: 1,
6
+ warn: 2,
7
+ error: 3,
8
+ };
9
+
10
+ const minLevel: LogLevel = (process.env.MCP_LOG_LEVEL as LogLevel) || 'info';
11
+
12
+ function shouldLog(level: LogLevel): boolean {
13
+ return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
14
+ }
15
+
16
+ function formatTimestamp(): string {
17
+ return new Date().toISOString();
18
+ }
19
+
20
+ function log(level: LogLevel, message: string, meta?: Record<string, unknown>): void {
21
+ if (!shouldLog(level)) return;
22
+
23
+ const parts = [`[${formatTimestamp()}]`, `[${level.toUpperCase()}]`, message];
24
+ if (meta && Object.keys(meta).length > 0) {
25
+ parts.push(JSON.stringify(meta));
26
+ }
27
+
28
+ // CRITICAL: Always stderr — stdout is reserved for MCP JSON-RPC
29
+ process.stderr.write(parts.join(' ') + '\n');
30
+ }
31
+
32
+ export const logger = {
33
+ debug: (msg: string, meta?: Record<string, unknown>) => log('debug', msg, meta),
34
+ info: (msg: string, meta?: Record<string, unknown>) => log('info', msg, meta),
35
+ warn: (msg: string, meta?: Record<string, unknown>) => log('warn', msg, meta),
36
+ error: (msg: string, meta?: Record<string, unknown>) => log('error', msg, meta),
37
+ };
package/src/shell.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { exec as execCb } from "child_process";
2
+ import { promisify } from "util";
3
+ import { logger } from "./logger.js";
4
+
5
+ const execAsync = promisify(execCb);
6
+
7
+ export interface ShellOptions {
8
+ timeout?: number;
9
+ retries?: number;
10
+ retryDelay?: number;
11
+ ignoreErrors?: boolean;
12
+ label?: string;
13
+ }
14
+
15
+ export interface ShellResult {
16
+ stdout: string;
17
+ stderr: string;
18
+ }
19
+
20
+ function sleep(ms: number): Promise<void> {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+
24
+ export async function shell(
25
+ command: string,
26
+ options: ShellOptions = {}
27
+ ): Promise<string> {
28
+ const {
29
+ timeout = 10_000,
30
+ retries = 0,
31
+ retryDelay = 1_000,
32
+ ignoreErrors = false,
33
+ label,
34
+ } = options;
35
+
36
+ const tag = label || command.slice(0, 60);
37
+
38
+ for (let attempt = 0; attempt <= retries; attempt++) {
39
+ try {
40
+ if (attempt > 0) {
41
+ const delay = retryDelay * Math.pow(2, attempt - 1);
42
+ logger.debug(`Retry ${attempt}/${retries} for "${tag}" after ${delay}ms`);
43
+ await sleep(delay);
44
+ }
45
+
46
+ logger.debug(`Executing: "${tag}"`, { timeout, attempt });
47
+
48
+ const result = await execAsync(command, {
49
+ timeout,
50
+ maxBuffer: 10 * 1024 * 1024, // 10MB — large schema dumps
51
+ env: { ...process.env },
52
+ });
53
+
54
+ return result.stdout.trim();
55
+ } catch (error: any) {
56
+ const isLastAttempt = attempt >= retries;
57
+
58
+ if (error.killed) {
59
+ logger.warn(`Command timed out after ${timeout}ms: "${tag}"`);
60
+ } else {
61
+ logger.debug(`Command failed: "${tag}"`, {
62
+ code: error.code,
63
+ stderr: error.stderr?.slice(0, 200),
64
+ });
65
+ }
66
+
67
+ if (isLastAttempt) {
68
+ if (ignoreErrors) {
69
+ logger.debug(`Ignoring error for "${tag}"`);
70
+ return "";
71
+ }
72
+ throw new Error(
73
+ `Shell command failed: ${tag}\n${error.message || error.stderr || "Unknown error"}`
74
+ );
75
+ }
76
+ }
77
+ }
78
+
79
+ // Unreachable, but TypeScript needs it
80
+ return "";
81
+ }