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/locator.ts DELETED
@@ -1,272 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import os from "os";
4
- import { shell } from "./shell.js";
5
- import { logger } from "./logger.js";
6
-
7
- export interface DatabaseLocation {
8
- platform: 'ios' | 'android';
9
- databases: string[];
10
- appDir?: string;
11
- }
12
-
13
- export async function listDatabases(bundleId?: string, targetPlatform?: 'ios' | 'android'): Promise<DatabaseLocation[]> {
14
- const results: DatabaseLocation[] = [];
15
-
16
- if (!targetPlatform || targetPlatform === 'ios') {
17
- try {
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
-
23
- if (udidStr) {
24
- const appDataDir = `${process.env.HOME}/Library/Developer/CoreSimulator/Devices/${udidStr}/data/Containers/Data/Application`;
25
- if (fs.existsSync(appDataDir)) {
26
- try {
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
- );
31
- if (found) {
32
- results.push({
33
- platform: 'ios',
34
- appDir: appDataDir,
35
- databases: found.split('\n').map(p => path.basename(p.trim())).filter(Boolean)
36
- });
37
- }
38
- } catch (e) {
39
- logger.warn("iOS find failed", { error: String(e) });
40
- }
41
- }
42
- } else if (targetPlatform === 'ios') {
43
- throw new Error("No booted iOS Simulator found (simctl returned empty).");
44
- }
45
- } catch (e: any) {
46
- if (targetPlatform === 'ios' && !e.message?.includes("find failed")) {
47
- throw new Error("No booted iOS Simulator found or xcrun failed.");
48
- }
49
- }
50
- }
51
-
52
- if (!targetPlatform || targetPlatform === 'android') {
53
- try {
54
- await shell("adb get-state", { timeout: 5_000, label: "adb-get-state" });
55
- } catch (e) {
56
- if (targetPlatform === 'android') {
57
- throw new Error("No booted Android Emulator found or adb is unresponsive.");
58
- }
59
- if (results.length === 0) {
60
- throw new Error("No booted iOS Simulator or Android Emulator device found.");
61
- }
62
- return results;
63
- }
64
-
65
- // if we have a specific bundleId-use it otherwise hunt
66
- let packagesToScan: string[] = [];
67
- if (bundleId) {
68
- packagesToScan = [bundleId];
69
- } else {
70
- try {
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
- );
75
- packagesToScan = packagesStr.split('\n')
76
- .map(line => line.replace('package:', '').trim())
77
- .filter(Boolean);
78
- } catch (e) {
79
- if (results.length === 0) {
80
- throw new Error("Could not list packages on Android Emulator to discover databases. Is it fully booted?");
81
- }
82
- return results;
83
- }
84
- }
85
-
86
- const allAndroidDatabases: string[] = [];
87
- let lastSuccessfulAppDir: string | undefined;
88
-
89
- for (const pkg of packagesToScan) {
90
- const baseDirs = [`/data/user/0/${pkg}`, `/data/data/${pkg}`];
91
- for (const baseDir of baseDirs) {
92
- try {
93
- await shell(
94
- `adb shell run-as ${pkg} ls -d ${baseDir}`,
95
- { timeout: 3_000, label: `adb-ls-${pkg}` }
96
- );
97
-
98
- let foundFiles: string[] = [];
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) {
115
- const lsFiles = lsOut.split('\n')
116
- .map(l => l.trim().replace(/\r/g, ''))
117
- .filter(Boolean)
118
- .filter(f => !f.endsWith('/'))
119
- .filter(f => !f.endsWith('-journal') && !f.endsWith('-wal') && !f.endsWith('-shm'))
120
- .filter(f => !f.includes('.'))
121
- .map(f => `${baseDir}/databases/${f}`);
122
- foundFiles.push(...lsFiles);
123
- }
124
-
125
- // Deduplicate
126
- foundFiles = [...new Set(foundFiles)];
127
-
128
- if (foundFiles.length > 0) {
129
- const displayFiles = foundFiles.map(f => f.replace(`${baseDir}/`, ''));
130
- allAndroidDatabases.push(...displayFiles);
131
- lastSuccessfulAppDir = `${baseDir}::${pkg}`;
132
- break;
133
- }
134
- } catch (e) {
135
- logger.debug(`Failed to list databases for app: ${pkg}`);
136
- }
137
- }
138
- }
139
-
140
- if (allAndroidDatabases.length > 0) {
141
- results.push({
142
- platform: 'android',
143
- appDir: lastSuccessfulAppDir,
144
- databases: allAndroidDatabases
145
- });
146
- } else if (targetPlatform === 'android') {
147
- throw new Error(`Android Emulator is booted, but no SQLite databases were found in any debuggable third-party packages.`);
148
- }
149
- }
150
-
151
- return results;
152
- }
153
-
154
-
155
- export async function syncDatabase(dbNameGlob?: string, bundleId?: string, targetPlatform?: 'ios' | 'android'): Promise<{ localPath: string, dbName: string, platform: 'ios' | 'android' }[]> {
156
- const locations = await listDatabases(bundleId, targetPlatform);
157
-
158
- if (locations.length === 0) {
159
- if (targetPlatform) {
160
- throw new Error(`No SQLite databases found for platform '${targetPlatform}'.`);
161
- }
162
- throw new Error(`No SQLite databases found on any platform.`);
163
- }
164
-
165
- const synced: { localPath: string, dbName: string, platform: 'ios' | 'android' }[] = [];
166
-
167
- for (const loc of locations) {
168
- if (targetPlatform && loc.platform !== targetPlatform) continue;
169
- let targetDbNames: string[] = [];
170
-
171
- if (!dbNameGlob) {
172
- const preferred = loc.databases.find(d => d.endsWith('.db') || d.endsWith('.sqlite'));
173
- if (preferred) {
174
- targetDbNames.push(preferred);
175
- } else if (loc.databases.length > 0) {
176
- targetDbNames.push(loc.databases[0]);
177
- }
178
- } else {
179
- const globRegex = new RegExp('^' + dbNameGlob.replace(/\*/g, '.*') + '$');
180
- targetDbNames = loc.databases.filter(name => globRegex.test(name));
181
- }
182
-
183
- for (const targetDbName of targetDbNames) {
184
- const { platform, appDir } = loc;
185
-
186
- // --- iOS Logic ---
187
- if (platform === 'ios') {
188
- if (!appDir) continue;
189
-
190
- try {
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
- );
195
- if (found && fs.existsSync(found)) {
196
- logger.info(`Located iOS DB at: ${found}`);
197
- synced.push({ localPath: found, dbName: targetDbName, platform: 'ios' });
198
- }
199
- } catch (e) {
200
- logger.warn(`Failed to locate full path for iOS DB: ${targetDbName}`);
201
- }
202
- continue;
203
- }
204
-
205
- // --- Android Logic ---
206
- if (!appDir || !appDir.includes("::")) {
207
- logger.warn(`Invalid Android appDir format: ${appDir}`);
208
- continue;
209
- }
210
-
211
- const [targetDbDir, targetPkg] = appDir.split("::");
212
-
213
- await shell(
214
- `adb shell am force-stop ${targetPkg}`,
215
- { timeout: 5_000, ignoreErrors: true, label: `adb-force-stop-${targetPkg}` }
216
- );
217
-
218
- const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "rn-sqlite-mcp-"));
219
- const safeLocalName = targetDbName.replace(/\//g, '_');
220
- const localDb = path.join(tmpdir, safeLocalName);
221
- const localWal = `${localDb}-wal`;
222
- const localShm = `${localDb}-shm`;
223
-
224
- const remoteMain = `${targetDbDir}/${targetDbName}`;
225
- const remoteWal = `${remoteMain}-wal`;
226
- const remoteShm = `${remoteMain}-shm`;
227
-
228
- const pullOne = async (remote: string, local: string): Promise<boolean> => {
229
- try {
230
- const remoteBase = path.basename(remote);
231
- const tmpRemote = `/data/local/tmp/${targetPkg}_${remoteBase}_${Date.now()}`;
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
-
246
- return fs.existsSync(local) && fs.statSync(local).size > 0;
247
- } catch (e) {
248
- if (fs.existsSync(local)) fs.unlinkSync(local);
249
- return false;
250
- }
251
- };
252
-
253
- if (!(await pullOne(remoteMain, localDb))) {
254
- logger.warn(`Failed to pull main DB file from Android: ${remoteMain}`);
255
- continue;
256
- }
257
-
258
- // WAL and SHM are best-effort
259
- await pullOne(remoteWal, localWal);
260
- await pullOne(remoteShm, localShm);
261
-
262
- logger.info(`Pulled Android DB to local temp: ${localDb}`);
263
- synced.push({ localPath: localDb, dbName: targetDbName, platform: 'android' });
264
- }
265
- }
266
-
267
- if (synced.length === 0) {
268
- throw new Error(`Failed to sync any databases matching '${dbNameGlob || 'auto-select'}'.`);
269
- }
270
-
271
- return synced;
272
- }
package/src/logger.ts DELETED
@@ -1,37 +0,0 @@
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 DELETED
@@ -1,81 +0,0 @@
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
- }
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true
13
- },
14
- "include": ["src/**/*"],
15
- "exclude": ["node_modules", "dist"]
16
- }