react-native-sqlite-mcp 1.0.1 → 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/db.js +35 -67
- package/dist/locator.js +3 -10
- package/dist/logger.js +0 -4
- package/dist/shell.js +0 -9
- package/package.json +3 -3
- package/src/db.ts +44 -52
package/dist/db.js
CHANGED
|
@@ -1,89 +1,52 @@
|
|
|
1
|
-
import
|
|
1
|
+
import initSqlJs from "sql.js";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import { logger } from "./logger.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
constructor(filename) {
|
|
9
|
-
this.db = new sqlite3.Database(filename);
|
|
10
|
-
}
|
|
11
|
-
all(sql, params = []) {
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
this.db.all(sql, params, (err, rows) => {
|
|
14
|
-
if (err)
|
|
15
|
-
reject(err);
|
|
16
|
-
else
|
|
17
|
-
resolve(rows);
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
get(sql, params = []) {
|
|
22
|
-
return new Promise((resolve, reject) => {
|
|
23
|
-
this.db.get(sql, params, (err, row) => {
|
|
24
|
-
if (err)
|
|
25
|
-
reject(err);
|
|
26
|
-
else
|
|
27
|
-
resolve(row);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
close() {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
this.db.close((err) => {
|
|
34
|
-
if (err)
|
|
35
|
-
reject(err);
|
|
36
|
-
else
|
|
37
|
-
resolve();
|
|
38
|
-
});
|
|
39
|
-
});
|
|
4
|
+
let SQL = null;
|
|
5
|
+
async function getSqlJs() {
|
|
6
|
+
if (!SQL) {
|
|
7
|
+
SQL = await initSqlJs();
|
|
8
|
+
logger.debug("sql.js WASM engine initialized");
|
|
40
9
|
}
|
|
10
|
+
return SQL;
|
|
41
11
|
}
|
|
42
|
-
const CACHE_TTL_MS = 60_000;
|
|
12
|
+
const CACHE_TTL_MS = 60_000;
|
|
43
13
|
const connectionCache = new Map();
|
|
44
|
-
function getCachedDb(dbPath) {
|
|
14
|
+
async function getCachedDb(dbPath) {
|
|
45
15
|
const existing = connectionCache.get(dbPath);
|
|
46
16
|
if (existing) {
|
|
47
|
-
// Refresh the idle timer
|
|
48
17
|
clearTimeout(existing.timer);
|
|
49
18
|
existing.lastUsed = Date.now();
|
|
50
19
|
existing.timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
|
|
51
20
|
return existing.db;
|
|
52
21
|
}
|
|
53
|
-
|
|
54
|
-
const
|
|
22
|
+
const sqlJs = await getSqlJs();
|
|
23
|
+
const buffer = fs.readFileSync(dbPath);
|
|
24
|
+
const db = new sqlJs.Database(buffer);
|
|
55
25
|
const timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
|
|
56
|
-
connectionCache.set(dbPath, { db, lastUsed: Date.now(), timer });
|
|
26
|
+
connectionCache.set(dbPath, { db, dbPath, lastUsed: Date.now(), timer });
|
|
57
27
|
logger.debug(`Opened DB connection: ${dbPath}`);
|
|
58
28
|
return db;
|
|
59
29
|
}
|
|
60
|
-
|
|
30
|
+
function evictConnection(dbPath) {
|
|
61
31
|
const entry = connectionCache.get(dbPath);
|
|
62
32
|
if (!entry)
|
|
63
33
|
return;
|
|
64
34
|
connectionCache.delete(dbPath);
|
|
65
35
|
try {
|
|
66
|
-
|
|
36
|
+
entry.db.close();
|
|
67
37
|
logger.debug(`Closed idle DB connection: ${dbPath}`);
|
|
68
38
|
}
|
|
69
39
|
catch (e) {
|
|
70
40
|
logger.warn(`Error closing DB: ${dbPath}`, { error: String(e) });
|
|
71
41
|
}
|
|
72
42
|
}
|
|
73
|
-
/**
|
|
74
|
-
* Close all cached connections. Called during graceful shutdown.
|
|
75
|
-
*/
|
|
76
43
|
export async function closeAllConnections() {
|
|
77
44
|
const paths = [...connectionCache.keys()];
|
|
78
45
|
for (const p of paths) {
|
|
79
|
-
|
|
46
|
+
evictConnection(p);
|
|
80
47
|
}
|
|
81
48
|
logger.info(`Closed ${paths.length} cached DB connection(s)`);
|
|
82
49
|
}
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Public query API
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
/** Query timeout — prevents stuck queries from blocking everything */
|
|
87
50
|
const QUERY_TIMEOUT_MS = 30_000;
|
|
88
51
|
function withTimeout(promise, ms, label) {
|
|
89
52
|
return new Promise((resolve, reject) => {
|
|
@@ -95,26 +58,31 @@ function withTimeout(promise, ms, label) {
|
|
|
95
58
|
.catch((err) => { clearTimeout(timer); reject(err); });
|
|
96
59
|
});
|
|
97
60
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
61
|
+
function runQuery(db, sql, params = []) {
|
|
62
|
+
const stmt = db.prepare(sql);
|
|
63
|
+
if (params.length > 0)
|
|
64
|
+
stmt.bind(params);
|
|
65
|
+
const results = [];
|
|
66
|
+
while (stmt.step()) {
|
|
67
|
+
results.push(stmt.getAsObject());
|
|
68
|
+
}
|
|
69
|
+
stmt.free();
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
101
72
|
export async function queryDb(dbPath, sql, params = []) {
|
|
102
|
-
const db = getCachedDb(dbPath);
|
|
103
|
-
return withTimeout(
|
|
73
|
+
const db = await getCachedDb(dbPath);
|
|
74
|
+
return withTimeout(Promise.resolve(runQuery(db, sql, params)), QUERY_TIMEOUT_MS, sql.slice(0, 80));
|
|
104
75
|
}
|
|
105
|
-
/**
|
|
106
|
-
* Returns a detailed schema of all tables in the database.
|
|
107
|
-
*/
|
|
108
76
|
export async function inspectSchema(dbPath) {
|
|
109
|
-
const db = getCachedDb(dbPath);
|
|
110
|
-
const tables =
|
|
77
|
+
const db = await getCachedDb(dbPath);
|
|
78
|
+
const tables = runQuery(db, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
|
|
111
79
|
const schemaInfo = {};
|
|
112
80
|
for (const table of tables) {
|
|
113
|
-
const columns =
|
|
114
|
-
const
|
|
81
|
+
const columns = runQuery(db, `PRAGMA table_info("${table.name}");`);
|
|
82
|
+
const createSqlRows = runQuery(db, `SELECT sql FROM sqlite_master WHERE type='table' AND name=?`, [table.name]);
|
|
115
83
|
schemaInfo[table.name] = {
|
|
116
84
|
columns,
|
|
117
|
-
createSql:
|
|
85
|
+
createSql: createSqlRows[0]?.sql
|
|
118
86
|
};
|
|
119
87
|
}
|
|
120
88
|
return schemaInfo;
|
package/dist/locator.js
CHANGED
|
@@ -49,7 +49,7 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
49
49
|
}
|
|
50
50
|
return results;
|
|
51
51
|
}
|
|
52
|
-
//
|
|
52
|
+
// if we have a specific bundleId-use it otherwise hunt
|
|
53
53
|
let packagesToScan = [];
|
|
54
54
|
if (bundleId) {
|
|
55
55
|
packagesToScan = [bundleId];
|
|
@@ -76,12 +76,12 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
76
76
|
try {
|
|
77
77
|
await shell(`adb shell run-as ${pkg} ls -d ${baseDir}`, { timeout: 3_000, label: `adb-ls-${pkg}` });
|
|
78
78
|
let foundFiles = [];
|
|
79
|
-
//
|
|
79
|
+
// find .db / .sqlite / .sqlite3 files recursively
|
|
80
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
81
|
if (findOut) {
|
|
82
82
|
foundFiles.push(...findOut.split('\n').map(l => l.trim().replace(/\r/g, '')).filter(Boolean));
|
|
83
83
|
}
|
|
84
|
-
//
|
|
84
|
+
// also check for extensionless files in /databases
|
|
85
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
86
|
if (lsOut) {
|
|
87
87
|
const lsFiles = lsOut.split('\n')
|
|
@@ -120,12 +120,6 @@ export async function listDatabases(bundleId, targetPlatform) {
|
|
|
120
120
|
}
|
|
121
121
|
return results;
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Finds the DB file based on platform and a provided/detected filename.
|
|
125
|
-
* If dbNameGlob is empty/undefined, it will auto-select the first discovered database.
|
|
126
|
-
* Prioritizes iOS (simctl), falls back to Android (adb).
|
|
127
|
-
* Returns the local file path (either the iOS original or a pulled Android copy).
|
|
128
|
-
*/
|
|
129
123
|
export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
|
|
130
124
|
const locations = await listDatabases(bundleId, targetPlatform);
|
|
131
125
|
if (locations.length === 0) {
|
|
@@ -176,7 +170,6 @@ export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
|
|
|
176
170
|
continue;
|
|
177
171
|
}
|
|
178
172
|
const [targetDbDir, targetPkg] = appDir.split("::");
|
|
179
|
-
// Force-stop is best-effort (ensures WAL is flushed)
|
|
180
173
|
await shell(`adb shell am force-stop ${targetPkg}`, { timeout: 5_000, ignoreErrors: true, label: `adb-force-stop-${targetPkg}` });
|
|
181
174
|
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "rn-sqlite-mcp-"));
|
|
182
175
|
const safeLocalName = targetDbName.replace(/\//g, '_');
|
package/dist/logger.js
CHANGED
package/dist/shell.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
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
1
|
import { exec as execCb } from "child_process";
|
|
7
2
|
import { promisify } from "util";
|
|
8
3
|
import { logger } from "./logger.js";
|
|
@@ -10,10 +5,6 @@ const execAsync = promisify(execCb);
|
|
|
10
5
|
function sleep(ms) {
|
|
11
6
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
7
|
}
|
|
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
8
|
export async function shell(command, options = {}) {
|
|
18
9
|
const { timeout = 10_000, retries = 0, retryDelay = 1_000, ignoreErrors = false, label, } = options;
|
|
19
10
|
const tag = label || command.slice(0, 60);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-sqlite-mcp",
|
|
3
|
-
"version": "1.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
|
-
"
|
|
26
|
+
"sql.js": "^1.14.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.13.4",
|
|
30
|
-
"@types/
|
|
30
|
+
"@types/sql.js": "^1.4.9",
|
|
31
31
|
"typescript": "^5.7.3"
|
|
32
32
|
}
|
|
33
33
|
}
|
package/src/db.ts
CHANGED
|
@@ -1,51 +1,28 @@
|
|
|
1
|
-
import
|
|
1
|
+
import initSqlJs, { type Database as SqlJsDatabase } from "sql.js";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import { logger } from "./logger.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
private db: sqlite3.Database;
|
|
5
|
+
let SQL: Awaited<ReturnType<typeof initSqlJs>> | null = null;
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
public all<T>(sql: string, params: any[] = []): Promise<T[]> {
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
this.db.all(sql, params, (err, rows) => {
|
|
14
|
-
if (err) reject(err);
|
|
15
|
-
else resolve(rows as Array<T>);
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
public get<T>(sql: string, params: any[] = []): Promise<T | undefined> {
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
this.db.get(sql, params, (err, row) => {
|
|
23
|
-
if (err) reject(err);
|
|
24
|
-
else resolve(row as T | undefined);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
public close(): Promise<void> {
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
this.db.close((err) => {
|
|
32
|
-
if (err) reject(err);
|
|
33
|
-
else resolve();
|
|
34
|
-
});
|
|
35
|
-
});
|
|
7
|
+
async function getSqlJs() {
|
|
8
|
+
if (!SQL) {
|
|
9
|
+
SQL = await initSqlJs();
|
|
10
|
+
logger.debug("sql.js WASM engine initialized");
|
|
36
11
|
}
|
|
12
|
+
return SQL;
|
|
37
13
|
}
|
|
38
14
|
|
|
39
15
|
interface CachedConnection {
|
|
40
|
-
db:
|
|
16
|
+
db: SqlJsDatabase;
|
|
17
|
+
dbPath: string;
|
|
41
18
|
lastUsed: number;
|
|
42
19
|
timer: ReturnType<typeof setTimeout>;
|
|
43
20
|
}
|
|
44
21
|
|
|
45
|
-
const CACHE_TTL_MS = 60_000;
|
|
22
|
+
const CACHE_TTL_MS = 60_000;
|
|
46
23
|
const connectionCache = new Map<string, CachedConnection>();
|
|
47
24
|
|
|
48
|
-
function getCachedDb(dbPath: string):
|
|
25
|
+
async function getCachedDb(dbPath: string): Promise<SqlJsDatabase> {
|
|
49
26
|
const existing = connectionCache.get(dbPath);
|
|
50
27
|
|
|
51
28
|
if (existing) {
|
|
@@ -55,21 +32,23 @@ function getCachedDb(dbPath: string): Database {
|
|
|
55
32
|
return existing.db;
|
|
56
33
|
}
|
|
57
34
|
|
|
58
|
-
const
|
|
59
|
-
const
|
|
35
|
+
const sqlJs = await getSqlJs();
|
|
36
|
+
const buffer = fs.readFileSync(dbPath);
|
|
37
|
+
const db = new sqlJs.Database(buffer);
|
|
60
38
|
|
|
61
|
-
|
|
39
|
+
const timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
|
|
40
|
+
connectionCache.set(dbPath, { db, dbPath, lastUsed: Date.now(), timer });
|
|
62
41
|
logger.debug(`Opened DB connection: ${dbPath}`);
|
|
63
42
|
return db;
|
|
64
43
|
}
|
|
65
44
|
|
|
66
|
-
|
|
45
|
+
function evictConnection(dbPath: string): void {
|
|
67
46
|
const entry = connectionCache.get(dbPath);
|
|
68
47
|
if (!entry) return;
|
|
69
48
|
|
|
70
49
|
connectionCache.delete(dbPath);
|
|
71
50
|
try {
|
|
72
|
-
|
|
51
|
+
entry.db.close();
|
|
73
52
|
logger.debug(`Closed idle DB connection: ${dbPath}`);
|
|
74
53
|
} catch (e) {
|
|
75
54
|
logger.warn(`Error closing DB: ${dbPath}`, { error: String(e) });
|
|
@@ -79,7 +58,7 @@ async function evictConnection(dbPath: string): Promise<void> {
|
|
|
79
58
|
export async function closeAllConnections(): Promise<void> {
|
|
80
59
|
const paths = [...connectionCache.keys()];
|
|
81
60
|
for (const p of paths) {
|
|
82
|
-
|
|
61
|
+
evictConnection(p);
|
|
83
62
|
}
|
|
84
63
|
logger.info(`Closed ${paths.length} cached DB connection(s)`);
|
|
85
64
|
}
|
|
@@ -98,32 +77,45 @@ function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise
|
|
|
98
77
|
});
|
|
99
78
|
}
|
|
100
79
|
|
|
80
|
+
function runQuery(db: SqlJsDatabase, sql: string, params: any[] = []): any[] {
|
|
81
|
+
const stmt = db.prepare(sql);
|
|
82
|
+
if (params.length > 0) stmt.bind(params);
|
|
83
|
+
|
|
84
|
+
const results: any[] = [];
|
|
85
|
+
while (stmt.step()) {
|
|
86
|
+
results.push(stmt.getAsObject());
|
|
87
|
+
}
|
|
88
|
+
stmt.free();
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
|
|
101
92
|
export async function queryDb(dbPath: string, sql: string, params: any[] = []): Promise<any[]> {
|
|
102
|
-
const db = getCachedDb(dbPath);
|
|
103
|
-
return withTimeout(
|
|
93
|
+
const db = await getCachedDb(dbPath);
|
|
94
|
+
return withTimeout(
|
|
95
|
+
Promise.resolve(runQuery(db, sql, params)),
|
|
96
|
+
QUERY_TIMEOUT_MS,
|
|
97
|
+
sql.slice(0, 80)
|
|
98
|
+
);
|
|
104
99
|
}
|
|
105
100
|
|
|
106
101
|
export async function inspectSchema(dbPath: string): Promise<any> {
|
|
107
|
-
const db = getCachedDb(dbPath);
|
|
102
|
+
const db = await getCachedDb(dbPath);
|
|
108
103
|
|
|
109
|
-
const tables =
|
|
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
|
-
);
|
|
104
|
+
const tables = runQuery(db, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
|
|
114
105
|
|
|
115
106
|
const schemaInfo: Record<string, any> = {};
|
|
116
107
|
|
|
117
108
|
for (const table of tables) {
|
|
118
|
-
const columns =
|
|
119
|
-
const
|
|
109
|
+
const columns = runQuery(db, `PRAGMA table_info("${table.name}");`);
|
|
110
|
+
const createSqlRows = runQuery(
|
|
111
|
+
db,
|
|
120
112
|
`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
|
|
121
113
|
[table.name]
|
|
122
114
|
);
|
|
123
115
|
|
|
124
116
|
schemaInfo[table.name] = {
|
|
125
117
|
columns,
|
|
126
|
-
createSql:
|
|
118
|
+
createSql: createSqlRows[0]?.sql
|
|
127
119
|
};
|
|
128
120
|
}
|
|
129
121
|
|