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/CONTRIBUTING.md +35 -0
- package/LICENSE +21 -0
- package/README.md +52 -31
- package/dist/db.js +73 -26
- package/dist/index.js +126 -60
- package/dist/locator.js +34 -44
- package/dist/logger.js +33 -0
- package/dist/shell.js +57 -0
- package/package.json +1 -1
- package/src/db.ts +84 -38
- 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/src/index.ts
CHANGED
|
@@ -7,17 +7,43 @@ import {
|
|
|
7
7
|
ListToolsRequestSchema,
|
|
8
8
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
9
|
import { listDatabases, syncDatabase } from "./locator.js";
|
|
10
|
-
import { inspectSchema, queryDb } from "./db.js";
|
|
10
|
+
import { inspectSchema, queryDb, closeAllConnections } from "./db.js";
|
|
11
|
+
import { logger } from "./logger.js";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Process-level guards — prevent silent crashes that cause EOF errors
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
process.on("uncaughtException", (error) => {
|
|
18
|
+
logger.error("Uncaught exception (process kept alive)", {
|
|
19
|
+
message: error.message,
|
|
20
|
+
stack: error.stack?.slice(0, 500),
|
|
21
|
+
});
|
|
22
|
+
// Do NOT call process.exit() — keep the MCP server alive
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
process.on("unhandledRejection", (reason) => {
|
|
26
|
+
logger.error("Unhandled promise rejection (process kept alive)", {
|
|
27
|
+
reason: String(reason),
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// State
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
11
34
|
|
|
12
|
-
// Keep track of the currently active databases
|
|
13
35
|
interface SyncedDB {
|
|
14
36
|
localPath: string;
|
|
15
37
|
dbName: string;
|
|
16
|
-
platform:
|
|
38
|
+
platform: "ios" | "android";
|
|
17
39
|
}
|
|
18
40
|
|
|
19
41
|
let activeDatabases: SyncedDB[] = [];
|
|
20
42
|
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Server setup
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
21
47
|
const server = new Server(
|
|
22
48
|
{
|
|
23
49
|
name: "react-native-sqlite-bridge",
|
|
@@ -30,138 +56,166 @@ const server = new Server(
|
|
|
30
56
|
}
|
|
31
57
|
);
|
|
32
58
|
|
|
59
|
+
// MCP-level transport error handler
|
|
60
|
+
server.onerror = (error) => {
|
|
61
|
+
logger.error("MCP transport error", { message: String(error) });
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Tool definitions
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
33
68
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
34
69
|
return {
|
|
35
70
|
tools: [
|
|
36
71
|
{
|
|
37
72
|
name: "sync_database",
|
|
38
|
-
description:
|
|
73
|
+
description:
|
|
74
|
+
"Re-runs the adb pull or file-find logic to ensure the AI is looking at the latest data from the emulator/simulator.",
|
|
39
75
|
inputSchema: {
|
|
40
76
|
type: "object",
|
|
41
77
|
properties: {
|
|
42
78
|
dbName: {
|
|
43
79
|
type: "string",
|
|
44
|
-
description:
|
|
80
|
+
description:
|
|
81
|
+
"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.",
|
|
45
82
|
},
|
|
46
83
|
bundleId: {
|
|
47
84
|
type: "string",
|
|
48
|
-
description:
|
|
85
|
+
description:
|
|
86
|
+
"(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
|
|
49
87
|
},
|
|
50
88
|
platform: {
|
|
51
89
|
type: "string",
|
|
52
|
-
description:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
90
|
+
description:
|
|
91
|
+
"Optional. Explicitly target 'ios' or 'android'.",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
56
95
|
},
|
|
57
96
|
{
|
|
58
97
|
name: "list_databases",
|
|
59
|
-
description:
|
|
98
|
+
description:
|
|
99
|
+
"Lists all available SQLite databases found on the iOS Simulator or Android Emulator.",
|
|
60
100
|
inputSchema: {
|
|
61
101
|
type: "object",
|
|
62
102
|
properties: {
|
|
63
103
|
bundleId: {
|
|
64
104
|
type: "string",
|
|
65
|
-
description:
|
|
105
|
+
description:
|
|
106
|
+
"(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
|
|
66
107
|
},
|
|
67
108
|
platform: {
|
|
68
109
|
type: "string",
|
|
69
|
-
description:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
110
|
+
description:
|
|
111
|
+
"Optional. Explicitly target 'ios' or 'android'.",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
73
115
|
},
|
|
74
116
|
{
|
|
75
117
|
name: "inspect_schema",
|
|
76
|
-
description:
|
|
118
|
+
description:
|
|
119
|
+
"Returns a list of all tables and their column definitions. This gives the AI the 'map' of the database.",
|
|
77
120
|
inputSchema: {
|
|
78
121
|
type: "object",
|
|
79
122
|
properties: {
|
|
80
123
|
dbName: {
|
|
81
124
|
type: "string",
|
|
82
|
-
description:
|
|
125
|
+
description:
|
|
126
|
+
"Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
|
|
83
127
|
},
|
|
84
128
|
platform: {
|
|
85
129
|
type: "string",
|
|
86
|
-
description:
|
|
87
|
-
|
|
130
|
+
description:
|
|
131
|
+
"Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
|
|
132
|
+
},
|
|
88
133
|
},
|
|
89
|
-
required: []
|
|
90
|
-
}
|
|
134
|
+
required: [],
|
|
135
|
+
},
|
|
91
136
|
},
|
|
92
137
|
{
|
|
93
138
|
name: "read_table_contents",
|
|
94
|
-
description:
|
|
139
|
+
description:
|
|
140
|
+
"Returns rows from a specific table. Equivalent to SELECT * FROM table_name.",
|
|
95
141
|
inputSchema: {
|
|
96
142
|
type: "object",
|
|
97
143
|
properties: {
|
|
98
144
|
tableName: {
|
|
99
145
|
type: "string",
|
|
100
|
-
description: "The name of the table to read."
|
|
146
|
+
description: "The name of the table to read.",
|
|
101
147
|
},
|
|
102
148
|
limit: {
|
|
103
149
|
type: "number",
|
|
104
|
-
description:
|
|
150
|
+
description:
|
|
151
|
+
"Optional limit to the number of rows returned. Defaults to 100.",
|
|
105
152
|
},
|
|
106
153
|
dbName: {
|
|
107
154
|
type: "string",
|
|
108
|
-
description:
|
|
155
|
+
description:
|
|
156
|
+
"Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
|
|
109
157
|
},
|
|
110
158
|
platform: {
|
|
111
159
|
type: "string",
|
|
112
|
-
description:
|
|
113
|
-
|
|
160
|
+
description:
|
|
161
|
+
"Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
|
|
162
|
+
},
|
|
114
163
|
},
|
|
115
|
-
required: ["tableName"]
|
|
116
|
-
}
|
|
164
|
+
required: ["tableName"],
|
|
165
|
+
},
|
|
117
166
|
},
|
|
118
167
|
{
|
|
119
168
|
name: "query_db",
|
|
120
|
-
description:
|
|
169
|
+
description:
|
|
170
|
+
"Accepts a raw SQL SELECT string and returns the JSON result set.",
|
|
121
171
|
inputSchema: {
|
|
122
172
|
type: "object",
|
|
123
173
|
properties: {
|
|
124
174
|
sql: {
|
|
125
175
|
type: "string",
|
|
126
|
-
description: "The raw SQL SELECT string to execute."
|
|
176
|
+
description: "The raw SQL SELECT string to execute.",
|
|
127
177
|
},
|
|
128
178
|
params: {
|
|
129
179
|
type: "array",
|
|
130
|
-
description:
|
|
180
|
+
description:
|
|
181
|
+
"Optional arguments to bind to the SQL query. Use this to safely substitute ? placeholders in your SQL string (e.g. ['value', 42]).",
|
|
131
182
|
items: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
183
|
+
description: "A single bound parameter value.",
|
|
184
|
+
},
|
|
135
185
|
},
|
|
136
186
|
dbName: {
|
|
137
187
|
type: "string",
|
|
138
|
-
description:
|
|
188
|
+
description:
|
|
189
|
+
"Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
|
|
139
190
|
},
|
|
140
191
|
platform: {
|
|
141
192
|
type: "string",
|
|
142
|
-
description:
|
|
143
|
-
|
|
193
|
+
description:
|
|
194
|
+
"Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
|
|
195
|
+
},
|
|
144
196
|
},
|
|
145
|
-
required: ["sql"]
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
]
|
|
197
|
+
required: ["sql"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
],
|
|
149
201
|
};
|
|
150
202
|
});
|
|
151
203
|
|
|
152
|
-
//
|
|
153
|
-
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Helpers
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
function cleanPlatform(raw?: string): "ios" | "android" | undefined {
|
|
154
209
|
if (!raw) return undefined;
|
|
155
|
-
const cleaned = raw.replace(/['"]/g,
|
|
156
|
-
if (cleaned ===
|
|
157
|
-
return undefined;
|
|
210
|
+
const cleaned = raw.replace(/['"]/g, "").trim().toLowerCase();
|
|
211
|
+
if (cleaned === "ios" || cleaned === "android") return cleaned;
|
|
212
|
+
return undefined;
|
|
158
213
|
}
|
|
159
214
|
|
|
160
|
-
// Helper to ensure database is synced based on provided args
|
|
161
215
|
async function ensureDbState(args: any): Promise<SyncedDB> {
|
|
162
216
|
const reqDbName = args?.dbName as string | undefined;
|
|
163
217
|
const reqPlatform = cleanPlatform(args?.platform as string | undefined);
|
|
164
|
-
|
|
218
|
+
|
|
165
219
|
// If nothing is synced, sync defaults
|
|
166
220
|
if (activeDatabases.length === 0) {
|
|
167
221
|
const envDb = reqDbName || process.env.DB_NAME;
|
|
@@ -169,23 +223,32 @@ async function ensureDbState(args: any): Promise<SyncedDB> {
|
|
|
169
223
|
activeDatabases = await syncDatabase(envDb, envBundle, reqPlatform);
|
|
170
224
|
}
|
|
171
225
|
|
|
172
|
-
// Filter based on explicit requirements
|
|
173
226
|
let candidates = activeDatabases;
|
|
174
|
-
if (reqPlatform)
|
|
175
|
-
|
|
227
|
+
if (reqPlatform)
|
|
228
|
+
candidates = candidates.filter((db) => db.platform === reqPlatform);
|
|
229
|
+
if (reqDbName)
|
|
230
|
+
candidates = candidates.filter((db) => db.dbName === reqDbName);
|
|
176
231
|
|
|
177
|
-
if (candidates.length === 1)
|
|
178
|
-
return candidates[0];
|
|
179
|
-
}
|
|
232
|
+
if (candidates.length === 1) return candidates[0];
|
|
180
233
|
|
|
181
234
|
if (candidates.length === 0) {
|
|
182
|
-
throw new Error(
|
|
235
|
+
throw new Error(
|
|
236
|
+
`No synced databases match the criteria (platform: ${reqPlatform || "any"}, dbName: ${reqDbName || "any"}). Try calling sync_database first.`
|
|
237
|
+
);
|
|
183
238
|
}
|
|
184
239
|
|
|
185
|
-
const matches = candidates
|
|
186
|
-
|
|
240
|
+
const matches = candidates
|
|
241
|
+
.map((c) => `[${c.platform}] ${c.dbName}`)
|
|
242
|
+
.join(", ");
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Multiple databases match the criteria. Please specify 'platform' or 'dbName'. Matches: ${matches}`
|
|
245
|
+
);
|
|
187
246
|
}
|
|
188
247
|
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Tool handlers
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
189
252
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
190
253
|
const { name, arguments: args } = request.params;
|
|
191
254
|
|
|
@@ -195,7 +258,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
195
258
|
const platform = cleanPlatform(args?.platform as string | undefined);
|
|
196
259
|
const results = await listDatabases(bundleId, platform);
|
|
197
260
|
return {
|
|
198
|
-
content: [
|
|
261
|
+
content: [
|
|
262
|
+
{ type: "text", text: JSON.stringify(results, null, 2) },
|
|
263
|
+
],
|
|
199
264
|
};
|
|
200
265
|
}
|
|
201
266
|
|
|
@@ -205,33 +270,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
205
270
|
const platform = cleanPlatform(args?.platform as string | undefined);
|
|
206
271
|
|
|
207
272
|
const results = await syncDatabase(dbName, bundleId, platform);
|
|
208
|
-
|
|
209
|
-
activeDatabases = results; // Replace the active list
|
|
273
|
+
activeDatabases = results;
|
|
210
274
|
|
|
211
275
|
let msg = "Successfully synced databases:\n";
|
|
212
276
|
for (const res of results) {
|
|
213
277
|
msg += `- Platform: ${res.platform} | DB: ${res.dbName}\n Path: ${res.localPath}\n`;
|
|
214
278
|
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
content: [{ type: "text", text: msg }]
|
|
218
|
-
};
|
|
279
|
+
return { content: [{ type: "text", text: msg }] };
|
|
219
280
|
}
|
|
220
281
|
|
|
221
282
|
if (name === "inspect_schema") {
|
|
222
283
|
const activeDb = await ensureDbState(args);
|
|
223
284
|
const schema = await inspectSchema(activeDb.localPath);
|
|
224
285
|
return {
|
|
225
|
-
content: [
|
|
286
|
+
content: [
|
|
287
|
+
{
|
|
288
|
+
type: "text",
|
|
289
|
+
text:
|
|
290
|
+
`[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` +
|
|
291
|
+
JSON.stringify(schema, null, 2),
|
|
292
|
+
},
|
|
293
|
+
],
|
|
226
294
|
};
|
|
227
295
|
}
|
|
228
296
|
|
|
229
297
|
if (name === "read_table_contents") {
|
|
230
298
|
const activeDb = await ensureDbState(args);
|
|
231
|
-
|
|
232
299
|
const tableName = args?.tableName as string;
|
|
233
|
-
const limit = args?.limit as number || 100;
|
|
234
|
-
|
|
300
|
+
const limit = (args?.limit as number) || 100;
|
|
301
|
+
|
|
235
302
|
if (!tableName) {
|
|
236
303
|
throw new Error("Missing required argument: tableName");
|
|
237
304
|
}
|
|
@@ -239,47 +306,82 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
239
306
|
const sql = `SELECT * FROM "${tableName}" LIMIT ?`;
|
|
240
307
|
const results = await queryDb(activeDb.localPath, sql, [limit]);
|
|
241
308
|
return {
|
|
242
|
-
content: [
|
|
309
|
+
content: [
|
|
310
|
+
{
|
|
311
|
+
type: "text",
|
|
312
|
+
text:
|
|
313
|
+
`[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName} | Table: ${tableName} | Limit: ${limit}]\n` +
|
|
314
|
+
JSON.stringify(results, null, 2),
|
|
315
|
+
},
|
|
316
|
+
],
|
|
243
317
|
};
|
|
244
318
|
}
|
|
245
319
|
|
|
246
320
|
if (name === "query_db") {
|
|
247
321
|
const activeDb = await ensureDbState(args);
|
|
248
|
-
|
|
249
322
|
const sql = args?.sql as string;
|
|
250
323
|
const params = (args?.params as any[]) || [];
|
|
251
|
-
|
|
324
|
+
|
|
252
325
|
if (!sql) {
|
|
253
326
|
throw new Error("Missing required argument: sql");
|
|
254
327
|
}
|
|
255
328
|
|
|
256
329
|
const results = await queryDb(activeDb.localPath, sql, params);
|
|
257
330
|
return {
|
|
258
|
-
content: [
|
|
331
|
+
content: [
|
|
332
|
+
{
|
|
333
|
+
type: "text",
|
|
334
|
+
text:
|
|
335
|
+
`[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` +
|
|
336
|
+
JSON.stringify(results, null, 2),
|
|
337
|
+
},
|
|
338
|
+
],
|
|
259
339
|
};
|
|
260
340
|
}
|
|
261
341
|
|
|
262
342
|
throw new Error(`Unknown tool: ${name}`);
|
|
263
343
|
} catch (error: any) {
|
|
344
|
+
logger.error(`Tool "${name}" failed`, { message: error.message });
|
|
264
345
|
return {
|
|
265
346
|
content: [
|
|
266
347
|
{
|
|
267
348
|
type: "text",
|
|
268
|
-
text: `Error: ${error.message}
|
|
269
|
-
}
|
|
349
|
+
text: `Error: ${error.message}`,
|
|
350
|
+
},
|
|
270
351
|
],
|
|
271
|
-
isError: true
|
|
352
|
+
isError: true,
|
|
272
353
|
};
|
|
273
354
|
}
|
|
274
355
|
});
|
|
275
356
|
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Graceful shutdown
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
async function shutdown(signal: string) {
|
|
362
|
+
logger.info(`Received ${signal}, shutting down gracefully...`);
|
|
363
|
+
try {
|
|
364
|
+
await closeAllConnections();
|
|
365
|
+
} catch (e) {
|
|
366
|
+
logger.error("Error during shutdown", { error: String(e) });
|
|
367
|
+
}
|
|
368
|
+
process.exit(0);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
372
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Start
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
276
378
|
async function run() {
|
|
277
379
|
const transport = new StdioServerTransport();
|
|
278
380
|
await server.connect(transport);
|
|
279
|
-
|
|
381
|
+
logger.info("Universal React Native SQLite MCP Server running on stdio");
|
|
280
382
|
}
|
|
281
383
|
|
|
282
384
|
run().catch((error) => {
|
|
283
|
-
|
|
385
|
+
logger.error("Server startup error", { message: error.message, stack: error.stack });
|
|
284
386
|
process.exit(1);
|
|
285
387
|
});
|