react-native-sqlite-mcp 1.0.2 → 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/README.md +143 -46
- package/dist/index.js +14 -1
- package/package.json +4 -1
- package/CONTRIBUTING.md +0 -35
- package/src/db.ts +0 -123
- package/src/index.ts +0 -387
- package/src/locator.ts +0 -272
- package/src/logger.ts +0 -37
- package/src/shell.ts +0 -81
- package/tsconfig.json +0 -16
package/README.md
CHANGED
|
@@ -1,23 +1,70 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# react-native-sqlite-mcp
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Expose your React Native SQLite database to MCP-compatible tooling — with emulator support.
|
|
6
|
+
|
|
7
|
+
<br />
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
<br />
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+

|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
</div>
|
|
6
21
|
|
|
7
22
|
---
|
|
8
23
|
|
|
9
|
-
##
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
SQLite MCP servers exist, but they all assume your database is a file sitting on your desktop or server. They open a path, read the file, done.
|
|
10
27
|
|
|
11
|
-
|
|
28
|
+
That doesn't work for mobile. On iOS Simulator, SQLite databases are buried deep inside sandboxed app containers. On Android Emulator, they're behind `run-as` permission boundaries that require `adb` to access. No existing SQLite MCP server handles either of these.
|
|
12
29
|
|
|
13
|
-
I
|
|
30
|
+
I built this because I was tired of manually extracting `.db` files from emulators just to figure out why my app's local state was wrong.
|
|
14
31
|
|
|
32
|
+
`react-native-sqlite-mcp` is a SQLite MCP server purpose-built for mobile emulators. It auto-discovers databases inside iOS Simulator and Android Emulator sandboxes, pulls them transparently, and exposes schema + query access to any MCP-compatible tool.
|
|
15
33
|
|
|
16
|
-
|
|
34
|
+
---
|
|
17
35
|
|
|
18
|
-
|
|
36
|
+
## Why This Exists
|
|
19
37
|
|
|
20
|
-
|
|
38
|
+
- Existing SQLite MCP servers can't reach databases inside iOS Simulator or Android Emulator sandboxes
|
|
39
|
+
- Mobile MCP tooling focuses on UI automation, not the data layer
|
|
40
|
+
- SQLite-backed state is critical in local-first React Native apps
|
|
41
|
+
- There was no MCP bridge for inspecting emulator databases directly from your AI tooling
|
|
42
|
+
|
|
43
|
+
This project fills that gap.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## How It Works
|
|
48
|
+
|
|
49
|
+
```mermaid
|
|
50
|
+
graph LR
|
|
51
|
+
subgraph Emulator
|
|
52
|
+
RN[React Native App] --> SQLite[(SQLite DB)]
|
|
53
|
+
end
|
|
54
|
+
SQLite -- "simctl / adb pull" --> MCP[MCP Server]
|
|
55
|
+
MCP -- "JSON-RPC stdio" --> AI[Claude / Cursor / Antigravity]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
1. Auto-detects booted iOS Simulators (`xcrun simctl`) and Android Emulators (`adb`)
|
|
59
|
+
2. Discovers SQLite databases inside sandboxed app containers
|
|
60
|
+
3. Pulls database files transparently (iOS reads in-place, Android uses `run-as` + `adb pull`)
|
|
61
|
+
4. Exposes schema and query access over the MCP stdio transport
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
Add this to your MCP config (`mcp.json`, `.cursor/mcp.json`, or `.gemini/settings.json`):
|
|
21
68
|
|
|
22
69
|
```json
|
|
23
70
|
{
|
|
@@ -26,7 +73,7 @@ Toss this bad boy into your `mcp.json` (or your Claude/Cursor/agent settings):
|
|
|
26
73
|
"command": "npx",
|
|
27
74
|
"args": ["-y", "react-native-sqlite-mcp"],
|
|
28
75
|
"env": {
|
|
29
|
-
"DB_NAME": "
|
|
76
|
+
"DB_NAME": "my_app.db",
|
|
30
77
|
"ANDROID_BUNDLE_ID": "com.mycompany.myapp"
|
|
31
78
|
}
|
|
32
79
|
}
|
|
@@ -34,20 +81,87 @@ Toss this bad boy into your `mcp.json` (or your Claude/Cursor/agent settings):
|
|
|
34
81
|
}
|
|
35
82
|
```
|
|
36
83
|
|
|
37
|
-
|
|
84
|
+
Restart your editor. Your AI can now query your emulator databases directly.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Environment Variables
|
|
89
|
+
|
|
90
|
+
| Variable | Required | Description |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `DB_NAME` | No | Database filename to auto-sync on startup (e.g. `my_app.db`). Supports glob patterns (`*.db`). If omitted, auto-selects the first discovered database. |
|
|
93
|
+
| `ANDROID_BUNDLE_ID` | No | Android app package name (e.g. `com.mycompany.app`). If omitted, scans all third-party packages on the emulator. |
|
|
94
|
+
| `READ_ONLY` | No | Set to `true` to restrict `query_db` to SELECT, PRAGMA, and EXPLAIN statements only. Default: `false`. |
|
|
95
|
+
| `MCP_LOG_LEVEL` | No | Log verbosity: `debug`, `info`, `warn`, `error`. Default: `info`. Logs go to stderr only. |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Tools
|
|
38
100
|
|
|
39
|
-
|
|
101
|
+
### `list_databases`
|
|
40
102
|
|
|
41
|
-
|
|
103
|
+
Scans for all SQLite databases on booted emulators/simulators.
|
|
104
|
+
|
|
105
|
+
| Argument | Type | Description |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `platform` | string | Optional. `ios` or `android`. If omitted, scans both. |
|
|
108
|
+
| `bundleId` | string | Optional. Android-only app package name to narrow the search. |
|
|
109
|
+
|
|
110
|
+
### `sync_database`
|
|
111
|
+
|
|
112
|
+
Pulls a fresh copy of the database from the emulator so all subsequent queries use the latest data.
|
|
113
|
+
|
|
114
|
+
| Argument | Type | Description |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `dbName` | string | Optional. Database filename or glob pattern. Auto-selects if omitted. |
|
|
117
|
+
| `bundleId` | string | Optional. Android-only app package name. |
|
|
118
|
+
| `platform` | string | Optional. `ios` or `android`. |
|
|
119
|
+
|
|
120
|
+
### `inspect_schema`
|
|
121
|
+
|
|
122
|
+
Returns all tables, columns, and `CREATE TABLE` statements for the synced database.
|
|
123
|
+
|
|
124
|
+
| Argument | Type | Description |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `dbName` | string | Optional. Target a specific database. |
|
|
127
|
+
| `platform` | string | Optional. `ios` or `android`. |
|
|
128
|
+
|
|
129
|
+
### `read_table_contents`
|
|
130
|
+
|
|
131
|
+
Returns rows from a table (`SELECT * FROM table LIMIT n`).
|
|
132
|
+
|
|
133
|
+
| Argument | Type | Description |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `tableName` | string | **Required.** The table to read. |
|
|
136
|
+
| `limit` | number | Optional. Max rows to return. Default: `100`. |
|
|
137
|
+
| `dbName` | string | Optional. Target a specific database. |
|
|
138
|
+
| `platform` | string | Optional. `ios` or `android`. |
|
|
139
|
+
|
|
140
|
+
### `query_db`
|
|
141
|
+
|
|
142
|
+
Executes a raw SQL query and returns the result set.
|
|
143
|
+
|
|
144
|
+
| Argument | Type | Description |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `sql` | string | **Required.** The SQL statement to execute. |
|
|
147
|
+
| `params` | array | Optional. Bind parameters for `?` placeholders. |
|
|
148
|
+
| `dbName` | string | Optional. Target a specific database. |
|
|
149
|
+
| `platform` | string | Optional. `ios` or `android`. |
|
|
150
|
+
|
|
151
|
+
> When `READ_ONLY=true`, only `SELECT`, `PRAGMA`, and `EXPLAIN` statements are allowed.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Manual Installation
|
|
42
156
|
|
|
43
157
|
```bash
|
|
44
|
-
git clone https://github.com/
|
|
158
|
+
git clone https://github.com/MisterMur/react-native-sqlite-mcp.git
|
|
45
159
|
cd react-native-sqlite-mcp
|
|
46
160
|
npm install
|
|
47
161
|
npm run build
|
|
48
162
|
```
|
|
49
163
|
|
|
50
|
-
|
|
164
|
+
Point your MCP config at the local build:
|
|
51
165
|
|
|
52
166
|
```json
|
|
53
167
|
{
|
|
@@ -56,49 +170,32 @@ Then point your MCP client to your local build:
|
|
|
56
170
|
"command": "node",
|
|
57
171
|
"args": ["/absolute/path/to/react-native-sqlite-mcp/dist/index.js"],
|
|
58
172
|
"env": {
|
|
59
|
-
"DB_NAME": "
|
|
60
|
-
"ANDROID_BUNDLE_ID": "com.mycompany.myapp"
|
|
173
|
+
"DB_NAME": "my_app.db"
|
|
61
174
|
}
|
|
62
175
|
}
|
|
63
176
|
}
|
|
64
177
|
}
|
|
65
178
|
```
|
|
66
179
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- `DB_NAME`: The filename of your database (e.g., `my_app.db`). You can also use a glob pattern (`*.db`) if you're feeling adventurous.
|
|
70
|
-
- `ANDROID_BUNDLE_ID`: *(Android Only)* The application ID/package name of your app (e.g., `com.mycompany.app`).
|
|
71
|
-
- **Pro-Tip:** If you leave this out, the MCP will go rogue and scan *all* third-party apps on your emulator for SQLite databases. Use with caution/glee.
|
|
72
|
-
|
|
73
|
-
## 🦸♂️ Features (What this bad boy can do)
|
|
74
|
-
|
|
75
|
-
This MCP arms your AI with four super-powered tools:
|
|
76
|
-
|
|
77
|
-
- 🕵️♂️ **`list_databases`**: Scours the device and returns a list of all available SQLite databases. Toss in `platform` ('ios' or 'android') to narrow the search.
|
|
78
|
-
- 🔄 **`sync_database`**: Yanks a copy of a database from your active device into the MCP's working directory so the AI can inspect it to its heart's content. Leave the arguments blank, and it'll just grab the first default database it finds.
|
|
79
|
-
- 🗺️ **`inspect_schema`**: The holy grail. Returns the `CREATE TABLE` and column info for your synced database. It literally gives the AI the map to your data.
|
|
80
|
-
- 📖 **`read_table_contents`**: Dumps all rows from a specific table (capped at 100 rows so we don't blow up the context window).
|
|
81
|
-
- 🤖 **`query_db`**: Lets the AI fire raw SQL queries right at the database and get the results back.
|
|
180
|
+
---
|
|
82
181
|
|
|
83
|
-
##
|
|
182
|
+
## Limitations
|
|
84
183
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
3. **Platform Switching**: The server keeps one active database connection open. Want to switch from iOS to Android? The AI just calls `sync_database` for the other platform. Simple.
|
|
90
|
-
4. **Execution**: It wraps all this up nicely so the LLM can learn your schema and query live data without bothering you.
|
|
184
|
+
- Development environments only — not designed for production
|
|
185
|
+
- iOS requires a booted Simulator with `xcrun simctl`
|
|
186
|
+
- Android requires a booted Emulator with `adb` and a debuggable app (debug build)
|
|
187
|
+
- Queries operate on a pulled snapshot, not a live connection
|
|
91
188
|
|
|
92
|
-
|
|
189
|
+
---
|
|
93
190
|
|
|
94
|
-
|
|
191
|
+
## Contributing
|
|
95
192
|
|
|
96
|
-
|
|
193
|
+
Issues and pull requests are welcome.
|
|
97
194
|
|
|
98
|
-
|
|
195
|
+
If you're working in the local-first React Native space and hit edge cases, open an issue. This tool exists because that gap was real.
|
|
99
196
|
|
|
100
|
-
|
|
197
|
+
---
|
|
101
198
|
|
|
102
|
-
|
|
199
|
+
## License
|
|
103
200
|
|
|
104
|
-
|
|
201
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
|
|
|
5
5
|
import { listDatabases, syncDatabase } from "./locator.js";
|
|
6
6
|
import { inspectSchema, queryDb, closeAllConnections } from "./db.js";
|
|
7
7
|
import { logger } from "./logger.js";
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { version } = require("../package.json");
|
|
8
11
|
// ---------------------------------------------------------------------------
|
|
9
12
|
// Process-level guards — prevent silent crashes that cause EOF errors
|
|
10
13
|
// ---------------------------------------------------------------------------
|
|
@@ -20,13 +23,17 @@ process.on("unhandledRejection", (reason) => {
|
|
|
20
23
|
reason: String(reason),
|
|
21
24
|
});
|
|
22
25
|
});
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Config
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const READ_ONLY = process.env.READ_ONLY === 'true' || process.env.READ_ONLY === '1';
|
|
23
30
|
let activeDatabases = [];
|
|
24
31
|
// ---------------------------------------------------------------------------
|
|
25
32
|
// Server setup
|
|
26
33
|
// ---------------------------------------------------------------------------
|
|
27
34
|
const server = new Server({
|
|
28
35
|
name: "react-native-sqlite-bridge",
|
|
29
|
-
version
|
|
36
|
+
version,
|
|
30
37
|
}, {
|
|
31
38
|
capabilities: {
|
|
32
39
|
tools: {},
|
|
@@ -258,6 +265,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
258
265
|
if (!sql) {
|
|
259
266
|
throw new Error("Missing required argument: sql");
|
|
260
267
|
}
|
|
268
|
+
if (READ_ONLY) {
|
|
269
|
+
const normalized = sql.trim().toUpperCase();
|
|
270
|
+
if (!normalized.startsWith("SELECT") && !normalized.startsWith("PRAGMA") && !normalized.startsWith("EXPLAIN")) {
|
|
271
|
+
throw new Error("READ_ONLY mode is enabled. Only SELECT, PRAGMA, and EXPLAIN statements are allowed.");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
261
274
|
const results = await queryDb(activeDb.localPath, sql, params);
|
|
262
275
|
return {
|
|
263
276
|
content: [
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-sqlite-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Universal React Native SQLite MCP Server",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"react-native-sqlite-mcp": "dist/index.js"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
10
13
|
"scripts": {
|
|
11
14
|
"build": "tsc",
|
|
12
15
|
"start": "node dist/index.js",
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# Welcome to the Contributing Guide
|
|
2
|
-
|
|
3
|
-
First off, thank you for considering contributing to the **Universal React Native SQLite MCP**!
|
|
4
|
-
|
|
5
|
-
Whether you're fixing a typo, optimizing an ADB command, or pointing out a glaring architecture flaw, your help is what makes open source awesome. I built this tool to fill a massive gap in the current MCP ecosystem around mobile development. There are plenty of SQLite tools out there, but none that seamlessly hook into your local emulators and simulators. And all the MCP's around mobile development are primarily UI focused. So hopefully this fills a niche. Let's make mobile dev less painful together!
|
|
6
|
-
|
|
7
|
-
## The Vibe
|
|
8
|
-
|
|
9
|
-
- **Keep it chill, but keep it working**: Have fun with the code and the docs, but let's make sure the core functionality is rock solid.
|
|
10
|
-
- **Assume good intent**: We're all here to build cool stuff and learn. Be kind in your PRs and specific in your code reviews.
|
|
11
|
-
- **Developer Experience (DX) is king**: This tool exists to make life easier. If a feature makes the tool harder to use or setup, we probably need to rethink it.
|
|
12
|
-
|
|
13
|
-
## 🛠️ How to get started
|
|
14
|
-
|
|
15
|
-
1. **Fork the repo** and clone it locally.
|
|
16
|
-
2. **Install dependencies**: `npm install`
|
|
17
|
-
3. **Make your changes**: Branch off `main` and do your thing.
|
|
18
|
-
4. **Test it out locally**: Build it with `npm run build` and point your local MCP client to it to verify it actually works against an emulator/simulator.
|
|
19
|
-
5. **Open a PR**: Give it a descriptive title and tell me what you fixed or added.
|
|
20
|
-
|
|
21
|
-
## Found a Bug?
|
|
22
|
-
|
|
23
|
-
Please open an issue! Tell me:
|
|
24
|
-
- What you were trying to do.
|
|
25
|
-
- What actually happened.
|
|
26
|
-
- Are you on iOS, Android, or both?
|
|
27
|
-
- Any logs you have from your AI client.
|
|
28
|
-
|
|
29
|
-
## Got a Feature Idea?
|
|
30
|
-
|
|
31
|
-
Open an issue and let's chat about it before you spend hours coding it up. I'm totally open to new ideas (especially if they involve making the auto-discovery even smarter).
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
Let's build something epic together.
|
package/src/db.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import initSqlJs, { type Database as SqlJsDatabase } from "sql.js";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import { logger } from "./logger.js";
|
|
4
|
-
|
|
5
|
-
let SQL: Awaited<ReturnType<typeof initSqlJs>> | null = null;
|
|
6
|
-
|
|
7
|
-
async function getSqlJs() {
|
|
8
|
-
if (!SQL) {
|
|
9
|
-
SQL = await initSqlJs();
|
|
10
|
-
logger.debug("sql.js WASM engine initialized");
|
|
11
|
-
}
|
|
12
|
-
return SQL;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface CachedConnection {
|
|
16
|
-
db: SqlJsDatabase;
|
|
17
|
-
dbPath: string;
|
|
18
|
-
lastUsed: number;
|
|
19
|
-
timer: ReturnType<typeof setTimeout>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const CACHE_TTL_MS = 60_000;
|
|
23
|
-
const connectionCache = new Map<string, CachedConnection>();
|
|
24
|
-
|
|
25
|
-
async function getCachedDb(dbPath: string): Promise<SqlJsDatabase> {
|
|
26
|
-
const existing = connectionCache.get(dbPath);
|
|
27
|
-
|
|
28
|
-
if (existing) {
|
|
29
|
-
clearTimeout(existing.timer);
|
|
30
|
-
existing.lastUsed = Date.now();
|
|
31
|
-
existing.timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
|
|
32
|
-
return existing.db;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const sqlJs = await getSqlJs();
|
|
36
|
-
const buffer = fs.readFileSync(dbPath);
|
|
37
|
-
const db = new sqlJs.Database(buffer);
|
|
38
|
-
|
|
39
|
-
const timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
|
|
40
|
-
connectionCache.set(dbPath, { db, dbPath, lastUsed: Date.now(), timer });
|
|
41
|
-
logger.debug(`Opened DB connection: ${dbPath}`);
|
|
42
|
-
return db;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function evictConnection(dbPath: string): void {
|
|
46
|
-
const entry = connectionCache.get(dbPath);
|
|
47
|
-
if (!entry) return;
|
|
48
|
-
|
|
49
|
-
connectionCache.delete(dbPath);
|
|
50
|
-
try {
|
|
51
|
-
entry.db.close();
|
|
52
|
-
logger.debug(`Closed idle DB connection: ${dbPath}`);
|
|
53
|
-
} catch (e) {
|
|
54
|
-
logger.warn(`Error closing DB: ${dbPath}`, { error: String(e) });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function closeAllConnections(): Promise<void> {
|
|
59
|
-
const paths = [...connectionCache.keys()];
|
|
60
|
-
for (const p of paths) {
|
|
61
|
-
evictConnection(p);
|
|
62
|
-
}
|
|
63
|
-
logger.info(`Closed ${paths.length} cached DB connection(s)`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const QUERY_TIMEOUT_MS = 30_000;
|
|
67
|
-
|
|
68
|
-
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
69
|
-
return new Promise<T>((resolve, reject) => {
|
|
70
|
-
const timer = setTimeout(() => {
|
|
71
|
-
reject(new Error(`Query timed out after ${ms}ms: ${label}`));
|
|
72
|
-
}, ms);
|
|
73
|
-
|
|
74
|
-
promise
|
|
75
|
-
.then((result) => { clearTimeout(timer); resolve(result); })
|
|
76
|
-
.catch((err) => { clearTimeout(timer); reject(err); });
|
|
77
|
-
});
|
|
78
|
-
}
|
|
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
|
-
|
|
92
|
-
export async function queryDb(dbPath: string, sql: string, params: any[] = []): Promise<any[]> {
|
|
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
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function inspectSchema(dbPath: string): Promise<any> {
|
|
102
|
-
const db = await getCachedDb(dbPath);
|
|
103
|
-
|
|
104
|
-
const tables = runQuery(db, "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
|
|
105
|
-
|
|
106
|
-
const schemaInfo: Record<string, any> = {};
|
|
107
|
-
|
|
108
|
-
for (const table of tables) {
|
|
109
|
-
const columns = runQuery(db, `PRAGMA table_info("${table.name}");`);
|
|
110
|
-
const createSqlRows = runQuery(
|
|
111
|
-
db,
|
|
112
|
-
`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
|
|
113
|
-
[table.name]
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
schemaInfo[table.name] = {
|
|
117
|
-
columns,
|
|
118
|
-
createSql: createSqlRows[0]?.sql
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return schemaInfo;
|
|
123
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import {
|
|
6
|
-
CallToolRequestSchema,
|
|
7
|
-
ListToolsRequestSchema,
|
|
8
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { listDatabases, syncDatabase } from "./locator.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
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
interface SyncedDB {
|
|
36
|
-
localPath: string;
|
|
37
|
-
dbName: string;
|
|
38
|
-
platform: "ios" | "android";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let activeDatabases: SyncedDB[] = [];
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Server setup
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
const server = new Server(
|
|
48
|
-
{
|
|
49
|
-
name: "react-native-sqlite-bridge",
|
|
50
|
-
version: "1.0.0",
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
capabilities: {
|
|
54
|
-
tools: {},
|
|
55
|
-
},
|
|
56
|
-
}
|
|
57
|
-
);
|
|
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
|
-
|
|
68
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
69
|
-
return {
|
|
70
|
-
tools: [
|
|
71
|
-
{
|
|
72
|
-
name: "sync_database",
|
|
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.",
|
|
75
|
-
inputSchema: {
|
|
76
|
-
type: "object",
|
|
77
|
-
properties: {
|
|
78
|
-
dbName: {
|
|
79
|
-
type: "string",
|
|
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.",
|
|
82
|
-
},
|
|
83
|
-
bundleId: {
|
|
84
|
-
type: "string",
|
|
85
|
-
description:
|
|
86
|
-
"(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
|
|
87
|
-
},
|
|
88
|
-
platform: {
|
|
89
|
-
type: "string",
|
|
90
|
-
description:
|
|
91
|
-
"Optional. Explicitly target 'ios' or 'android'.",
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
name: "list_databases",
|
|
98
|
-
description:
|
|
99
|
-
"Lists all available SQLite databases found on the iOS Simulator or Android Emulator.",
|
|
100
|
-
inputSchema: {
|
|
101
|
-
type: "object",
|
|
102
|
-
properties: {
|
|
103
|
-
bundleId: {
|
|
104
|
-
type: "string",
|
|
105
|
-
description:
|
|
106
|
-
"(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS.",
|
|
107
|
-
},
|
|
108
|
-
platform: {
|
|
109
|
-
type: "string",
|
|
110
|
-
description:
|
|
111
|
-
"Optional. Explicitly target 'ios' or 'android'.",
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: "inspect_schema",
|
|
118
|
-
description:
|
|
119
|
-
"Returns a list of all tables and their column definitions. This gives the AI the 'map' of the database.",
|
|
120
|
-
inputSchema: {
|
|
121
|
-
type: "object",
|
|
122
|
-
properties: {
|
|
123
|
-
dbName: {
|
|
124
|
-
type: "string",
|
|
125
|
-
description:
|
|
126
|
-
"Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
|
|
127
|
-
},
|
|
128
|
-
platform: {
|
|
129
|
-
type: "string",
|
|
130
|
-
description:
|
|
131
|
-
"Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
required: [],
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
name: "read_table_contents",
|
|
139
|
-
description:
|
|
140
|
-
"Returns rows from a specific table. Equivalent to SELECT * FROM table_name.",
|
|
141
|
-
inputSchema: {
|
|
142
|
-
type: "object",
|
|
143
|
-
properties: {
|
|
144
|
-
tableName: {
|
|
145
|
-
type: "string",
|
|
146
|
-
description: "The name of the table to read.",
|
|
147
|
-
},
|
|
148
|
-
limit: {
|
|
149
|
-
type: "number",
|
|
150
|
-
description:
|
|
151
|
-
"Optional limit to the number of rows returned. Defaults to 100.",
|
|
152
|
-
},
|
|
153
|
-
dbName: {
|
|
154
|
-
type: "string",
|
|
155
|
-
description:
|
|
156
|
-
"Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
|
|
157
|
-
},
|
|
158
|
-
platform: {
|
|
159
|
-
type: "string",
|
|
160
|
-
description:
|
|
161
|
-
"Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
required: ["tableName"],
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
name: "query_db",
|
|
169
|
-
description:
|
|
170
|
-
"Accepts a raw SQL SELECT string and returns the JSON result set.",
|
|
171
|
-
inputSchema: {
|
|
172
|
-
type: "object",
|
|
173
|
-
properties: {
|
|
174
|
-
sql: {
|
|
175
|
-
type: "string",
|
|
176
|
-
description: "The raw SQL SELECT string to execute.",
|
|
177
|
-
},
|
|
178
|
-
params: {
|
|
179
|
-
type: "array",
|
|
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]).",
|
|
182
|
-
items: {
|
|
183
|
-
description: "A single bound parameter value.",
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
dbName: {
|
|
187
|
-
type: "string",
|
|
188
|
-
description:
|
|
189
|
-
"Optional. Target a specific database name. If omitted, uses the active DB or auto-selects.",
|
|
190
|
-
},
|
|
191
|
-
platform: {
|
|
192
|
-
type: "string",
|
|
193
|
-
description:
|
|
194
|
-
"Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects.",
|
|
195
|
-
},
|
|
196
|
-
},
|
|
197
|
-
required: ["sql"],
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
],
|
|
201
|
-
};
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
// Helpers
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
|
|
208
|
-
function cleanPlatform(raw?: string): "ios" | "android" | undefined {
|
|
209
|
-
if (!raw) return undefined;
|
|
210
|
-
const cleaned = raw.replace(/['"]/g, "").trim().toLowerCase();
|
|
211
|
-
if (cleaned === "ios" || cleaned === "android") return cleaned;
|
|
212
|
-
return undefined;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function ensureDbState(args: any): Promise<SyncedDB> {
|
|
216
|
-
const reqDbName = args?.dbName as string | undefined;
|
|
217
|
-
const reqPlatform = cleanPlatform(args?.platform as string | undefined);
|
|
218
|
-
|
|
219
|
-
// If nothing is synced, sync defaults
|
|
220
|
-
if (activeDatabases.length === 0) {
|
|
221
|
-
const envDb = reqDbName || process.env.DB_NAME;
|
|
222
|
-
const envBundle = process.env.ANDROID_BUNDLE_ID;
|
|
223
|
-
activeDatabases = await syncDatabase(envDb, envBundle, reqPlatform);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
let candidates = activeDatabases;
|
|
227
|
-
if (reqPlatform)
|
|
228
|
-
candidates = candidates.filter((db) => db.platform === reqPlatform);
|
|
229
|
-
if (reqDbName)
|
|
230
|
-
candidates = candidates.filter((db) => db.dbName === reqDbName);
|
|
231
|
-
|
|
232
|
-
if (candidates.length === 1) return candidates[0];
|
|
233
|
-
|
|
234
|
-
if (candidates.length === 0) {
|
|
235
|
-
throw new Error(
|
|
236
|
-
`No synced databases match the criteria (platform: ${reqPlatform || "any"}, dbName: ${reqDbName || "any"}). Try calling sync_database first.`
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
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
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// ---------------------------------------------------------------------------
|
|
249
|
-
// Tool handlers
|
|
250
|
-
// ---------------------------------------------------------------------------
|
|
251
|
-
|
|
252
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
253
|
-
const { name, arguments: args } = request.params;
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
if (name === "list_databases") {
|
|
257
|
-
const bundleId = args?.bundleId as string | undefined;
|
|
258
|
-
const platform = cleanPlatform(args?.platform as string | undefined);
|
|
259
|
-
const results = await listDatabases(bundleId, platform);
|
|
260
|
-
return {
|
|
261
|
-
content: [
|
|
262
|
-
{ type: "text", text: JSON.stringify(results, null, 2) },
|
|
263
|
-
],
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (name === "sync_database") {
|
|
268
|
-
const dbName = args?.dbName as string | undefined;
|
|
269
|
-
const bundleId = args?.bundleId as string | undefined;
|
|
270
|
-
const platform = cleanPlatform(args?.platform as string | undefined);
|
|
271
|
-
|
|
272
|
-
const results = await syncDatabase(dbName, bundleId, platform);
|
|
273
|
-
activeDatabases = results;
|
|
274
|
-
|
|
275
|
-
let msg = "Successfully synced databases:\n";
|
|
276
|
-
for (const res of results) {
|
|
277
|
-
msg += `- Platform: ${res.platform} | DB: ${res.dbName}\n Path: ${res.localPath}\n`;
|
|
278
|
-
}
|
|
279
|
-
return { content: [{ type: "text", text: msg }] };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (name === "inspect_schema") {
|
|
283
|
-
const activeDb = await ensureDbState(args);
|
|
284
|
-
const schema = await inspectSchema(activeDb.localPath);
|
|
285
|
-
return {
|
|
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
|
-
],
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (name === "read_table_contents") {
|
|
298
|
-
const activeDb = await ensureDbState(args);
|
|
299
|
-
const tableName = args?.tableName as string;
|
|
300
|
-
const limit = (args?.limit as number) || 100;
|
|
301
|
-
|
|
302
|
-
if (!tableName) {
|
|
303
|
-
throw new Error("Missing required argument: tableName");
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const sql = `SELECT * FROM "${tableName}" LIMIT ?`;
|
|
307
|
-
const results = await queryDb(activeDb.localPath, sql, [limit]);
|
|
308
|
-
return {
|
|
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
|
-
],
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (name === "query_db") {
|
|
321
|
-
const activeDb = await ensureDbState(args);
|
|
322
|
-
const sql = args?.sql as string;
|
|
323
|
-
const params = (args?.params as any[]) || [];
|
|
324
|
-
|
|
325
|
-
if (!sql) {
|
|
326
|
-
throw new Error("Missing required argument: sql");
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const results = await queryDb(activeDb.localPath, sql, params);
|
|
330
|
-
return {
|
|
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
|
-
],
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
343
|
-
} catch (error: any) {
|
|
344
|
-
logger.error(`Tool "${name}" failed`, { message: error.message });
|
|
345
|
-
return {
|
|
346
|
-
content: [
|
|
347
|
-
{
|
|
348
|
-
type: "text",
|
|
349
|
-
text: `Error: ${error.message}`,
|
|
350
|
-
},
|
|
351
|
-
],
|
|
352
|
-
isError: true,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
});
|
|
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
|
-
|
|
378
|
-
async function run() {
|
|
379
|
-
const transport = new StdioServerTransport();
|
|
380
|
-
await server.connect(transport);
|
|
381
|
-
logger.info("Universal React Native SQLite MCP Server running on stdio");
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
run().catch((error) => {
|
|
385
|
-
logger.error("Server startup error", { message: error.message, stack: error.stack });
|
|
386
|
-
process.exit(1);
|
|
387
|
-
});
|
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
|
-
}
|