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/CONTRIBUTING.md +35 -0
- package/LICENSE +21 -0
- package/README.md +52 -31
- package/dist/db.js +78 -63
- package/dist/index.js +126 -60
- package/dist/locator.js +32 -49
- package/dist/logger.js +29 -0
- package/dist/shell.js +48 -0
- package/package.json +3 -3
- package/src/db.ts +104 -66
- package/src/index.ts +181 -79
- package/src/locator.ts +88 -71
- package/src/logger.ts +37 -0
- package/src/shell.ts +81 -0
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
|
-
|
|
116
|
-
|
|
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
|
-
//
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Helpers
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
135
162
|
function cleanPlatform(raw) {
|
|
136
163
|
if (!raw)
|
|
137
164
|
return undefined;
|
|
138
|
-
const cleaned = raw.replace(/['"]/g,
|
|
139
|
-
if (cleaned ===
|
|
165
|
+
const cleaned = raw.replace(/['"]/g, "").trim().toLowerCase();
|
|
166
|
+
if (cleaned === "ios" || cleaned === "android")
|
|
140
167
|
return cleaned;
|
|
141
|
-
return undefined;
|
|
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 ||
|
|
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
|
|
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: [
|
|
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;
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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
|
-
|
|
308
|
+
logger.info("Universal React Native SQLite MCP Server running on stdio");
|
|
243
309
|
}
|
|
244
310
|
run().catch((error) => {
|
|
245
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
77
|
+
await shell(`adb shell run-as ${pkg} ls -d ${baseDir}`, { timeout: 3_000, label: `adb-ls-${pkg}` });
|
|
78
78
|
let foundFiles = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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]);
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
logger.warn(`Invalid Android appDir format: ${appDir}`);
|
|
183
170
|
continue;
|
|
184
171
|
}
|
|
185
172
|
const [targetDbDir, targetPkg] = appDir.split("::");
|
|
186
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
197
|
+
if (!(await pullOne(remoteMain, localDb))) {
|
|
198
|
+
logger.warn(`Failed to pull main DB file from Android: ${remoteMain}`);
|
|
217
199
|
continue;
|
|
218
200
|
}
|
|
219
|
-
|
|
220
|
-
pullOne(
|
|
221
|
-
|
|
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.
|
|
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
|
}
|