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/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') {
@@ -49,14 +49,14 @@ export async function listDatabases(bundleId, targetPlatform) {
49
49
  }
50
50
  return results;
51
51
  }
52
- // if we have a specific bundleId-use it otherwise hunt
52
+ // Discover packages to scan
53
53
  let packagesToScan = [];
54
54
  if (bundleId) {
55
55
  packagesToScan = [bundleId];
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
  }
@@ -130,7 +127,6 @@ export async function listDatabases(bundleId, targetPlatform) {
130
127
  * Returns the local file path (either the iOS original or a pulled Android copy).
131
128
  */
132
129
  export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
133
- // First, discover available databases
134
130
  const locations = await listDatabases(bundleId, targetPlatform);
135
131
  if (locations.length === 0) {
136
132
  if (targetPlatform) {
@@ -144,13 +140,12 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
144
140
  continue;
145
141
  let targetDbNames = [];
146
142
  if (!dbNameGlob) {
147
- // Auto-select: find the first .db or .sqlite file in this location
148
143
  const preferred = loc.databases.find(d => d.endsWith('.db') || d.endsWith('.sqlite'));
149
144
  if (preferred) {
150
145
  targetDbNames.push(preferred);
151
146
  }
152
147
  else if (loc.databases.length > 0) {
153
- targetDbNames.push(loc.databases[0]); // fallback to first file
148
+ targetDbNames.push(loc.databases[0]);
154
149
  }
155
150
  }
156
151
  else {
@@ -163,32 +158,26 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
163
158
  if (platform === 'ios') {
164
159
  if (!appDir)
165
160
  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
161
  try {
169
- const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
162
+ const found = await shell(`find "${appDir}" -type f -name "${targetDbName}" -maxdepth 7 -print | head -n 1`, { timeout: 8_000, label: `ios-find-${targetDbName}` });
170
163
  if (found && fs.existsSync(found)) {
171
- console.error(`Located iOS DB at: ${found}`);
164
+ logger.info(`Located iOS DB at: ${found}`);
172
165
  synced.push({ localPath: found, dbName: targetDbName, platform: 'ios' });
173
166
  }
174
167
  }
175
168
  catch (e) {
176
- console.error(`Failed to locate full path for iOS DB: ${targetDbName}`);
169
+ logger.warn(`Failed to locate full path for iOS DB: ${targetDbName}`);
177
170
  }
178
171
  continue;
179
172
  }
180
173
  // --- Android Logic ---
181
174
  if (!appDir || !appDir.includes("::")) {
182
- console.error(`Invalid Android appDir format: ${appDir}`);
175
+ logger.warn(`Invalid Android appDir format: ${appDir}`);
183
176
  continue;
184
177
  }
185
178
  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
- }
179
+ // Force-stop is best-effort (ensures WAL is flushed)
180
+ await shell(`adb shell am force-stop ${targetPkg}`, { timeout: 5_000, ignoreErrors: true, label: `adb-force-stop-${targetPkg}` });
192
181
  const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "rn-sqlite-mcp-"));
193
182
  const safeLocalName = targetDbName.replace(/\//g, '_');
194
183
  const localDb = path.join(tmpdir, safeLocalName);
@@ -197,13 +186,13 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
197
186
  const remoteMain = `${targetDbDir}/${targetDbName}`;
198
187
  const remoteWal = `${remoteMain}-wal`;
199
188
  const remoteShm = `${remoteMain}-shm`;
200
- const pullOne = (remote, local) => {
189
+ const pullOne = async (remote, local) => {
201
190
  try {
202
191
  const remoteBase = path.basename(remote);
203
192
  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 });
193
+ await shell(`adb shell "run-as '${targetPkg}' cat '${remote}' > '${tmpRemote}'"`, { timeout: 10_000, retries: 1, retryDelay: 1_000, label: `adb-cat-${remoteBase}` });
194
+ await shell(`adb pull '${tmpRemote}' '${local}'`, { timeout: 10_000, label: `adb-pull-${remoteBase}` });
195
+ await shell(`adb shell rm '${tmpRemote}'`, { timeout: 5_000, ignoreErrors: true, label: `adb-rm-tmp-${remoteBase}` });
207
196
  return fs.existsSync(local) && fs.statSync(local).size > 0;
208
197
  }
209
198
  catch (e) {
@@ -212,13 +201,14 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
212
201
  return false;
213
202
  }
214
203
  };
215
- if (!pullOne(remoteMain, localDb)) {
216
- console.error(`Failed to pull main DB file from Android: ${remoteMain}`);
204
+ if (!(await pullOne(remoteMain, localDb))) {
205
+ logger.warn(`Failed to pull main DB file from Android: ${remoteMain}`);
217
206
  continue;
218
207
  }
219
- pullOne(remoteWal, localWal);
220
- pullOne(remoteShm, localShm);
221
- console.error(`Pulled Android DB to local temp: ${localDb}`);
208
+ // WAL and SHM are best-effort
209
+ await pullOne(remoteWal, localWal);
210
+ await pullOne(remoteShm, localShm);
211
+ logger.info(`Pulled Android DB to local temp: ${localDb}`);
222
212
  synced.push({ localPath: localDb, dbName: targetDbName, platform: 'android' });
223
213
  }
224
214
  }
package/dist/logger.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Structured logging utility for MCP servers.
3
+ * ALL output goes to stderr to avoid polluting the stdio JSON-RPC transport.
4
+ */
5
+ const LOG_LEVELS = {
6
+ debug: 0,
7
+ info: 1,
8
+ warn: 2,
9
+ error: 3,
10
+ };
11
+ const minLevel = process.env.MCP_LOG_LEVEL || 'info';
12
+ function shouldLog(level) {
13
+ return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
14
+ }
15
+ function formatTimestamp() {
16
+ return new Date().toISOString();
17
+ }
18
+ function log(level, message, meta) {
19
+ if (!shouldLog(level))
20
+ return;
21
+ const parts = [`[${formatTimestamp()}]`, `[${level.toUpperCase()}]`, message];
22
+ if (meta && Object.keys(meta).length > 0) {
23
+ parts.push(JSON.stringify(meta));
24
+ }
25
+ // CRITICAL: Always stderr — stdout is reserved for MCP JSON-RPC
26
+ process.stderr.write(parts.join(' ') + '\n');
27
+ }
28
+ export const logger = {
29
+ debug: (msg, meta) => log('debug', msg, meta),
30
+ info: (msg, meta) => log('info', msg, meta),
31
+ warn: (msg, meta) => log('warn', msg, meta),
32
+ error: (msg, meta) => log('error', msg, meta),
33
+ };
package/dist/shell.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Async shell execution wrapper.
3
+ * Replaces all execSync usage to keep the Node.js event loop alive
4
+ * so MCP stdio transport can continue processing heartbeats/messages.
5
+ */
6
+ import { exec as execCb } from "child_process";
7
+ import { promisify } from "util";
8
+ import { logger } from "./logger.js";
9
+ const execAsync = promisify(execCb);
10
+ function sleep(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+ /**
14
+ * Execute a shell command asynchronously with timeout and retry support.
15
+ * This is the core replacement for `execSync` — it does NOT block the event loop.
16
+ */
17
+ export async function shell(command, options = {}) {
18
+ const { timeout = 10_000, retries = 0, retryDelay = 1_000, ignoreErrors = false, label, } = options;
19
+ const tag = label || command.slice(0, 60);
20
+ for (let attempt = 0; attempt <= retries; attempt++) {
21
+ try {
22
+ if (attempt > 0) {
23
+ const delay = retryDelay * Math.pow(2, attempt - 1);
24
+ logger.debug(`Retry ${attempt}/${retries} for "${tag}" after ${delay}ms`);
25
+ await sleep(delay);
26
+ }
27
+ logger.debug(`Executing: "${tag}"`, { timeout, attempt });
28
+ const result = await execAsync(command, {
29
+ timeout,
30
+ maxBuffer: 10 * 1024 * 1024, // 10MB — large schema dumps
31
+ env: { ...process.env },
32
+ });
33
+ return result.stdout.trim();
34
+ }
35
+ catch (error) {
36
+ const isLastAttempt = attempt >= retries;
37
+ if (error.killed) {
38
+ logger.warn(`Command timed out after ${timeout}ms: "${tag}"`);
39
+ }
40
+ else {
41
+ logger.debug(`Command failed: "${tag}"`, {
42
+ code: error.code,
43
+ stderr: error.stderr?.slice(0, 200),
44
+ });
45
+ }
46
+ if (isLastAttempt) {
47
+ if (ignoreErrors) {
48
+ logger.debug(`Ignoring error for "${tag}"`);
49
+ return "";
50
+ }
51
+ throw new Error(`Shell command failed: ${tag}\n${error.message || error.stderr || "Unknown error"}`);
52
+ }
53
+ }
54
+ }
55
+ // Unreachable, but TypeScript needs it
56
+ return "";
57
+ }
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.1",
4
4
  "description": "Universal React Native SQLite MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
package/src/db.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import sqlite3 from "sqlite3";
2
+ import { logger } from "./logger.js";
2
3
 
3
4
  export class Database {
4
5
  private db: sqlite3.Database;
@@ -35,51 +36,96 @@ export class Database {
35
36
  }
36
37
  }
37
38
 
38
- /**
39
- * Executes a simple query on the database.
40
- */
41
- export async function queryDb(dbPath: string, sql: string, params: any[] = []): Promise<any[]> {
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
+
42
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);
43
71
  try {
44
- return await db.all(sql, params);
45
- } finally {
46
- await db.close();
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);
47
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));
48
104
  }
49
105
 
50
- /**
51
- * Returns a detailed schema of all tables in the database.
52
- */
53
106
  export async function inspectSchema(dbPath: string): Promise<any> {
54
- const db = new Database(dbPath);
55
- try {
56
- // Get all table names
57
- const tables = await db.all<{ name: string }>(
58
- "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
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]
59
122
  );
60
123
 
61
- const schemaInfo: Record<string, any> = {};
62
-
63
- for (const table of tables) {
64
- // For each table, get column definitions
65
- const columns = await db.all(
66
- `PRAGMA table_info("${table.name}");`
67
- );
68
-
69
- // Get table creation SQL
70
- const createSql = await db.get<{ sql: string }>(
71
- `SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
72
- [table.name]
73
- );
74
-
75
- schemaInfo[table.name] = {
76
- columns,
77
- createSql: createSql?.sql
78
- };
79
- }
80
-
81
- return schemaInfo;
82
- } finally {
83
- await db.close();
124
+ schemaInfo[table.name] = {
125
+ columns,
126
+ createSql: createSql?.sql
127
+ };
84
128
  }
129
+
130
+ return schemaInfo;
85
131
  }