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/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brian Murillo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
# Universal React Native SQLite MCP
|
|
1
|
+
# 🚀 Universal React Native SQLite MCP
|
|
2
2
|
|
|
3
|
-
A Model Context Protocol (MCP) server that
|
|
3
|
+
**TL;DR:** A Model Context Protocol (MCP) server that gives your favorite LLM (Claude, Cursor, Antigravity, etc.) X-ray vision into your local React Native SQLite databases.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
No more flying blind. No more manually exporting `.db` files from emulators to figure out why your app is broken. Just ask your AI: *"Hey, what does the `users` table look like on my Android emulator?"* and watch the magic happen. ✨
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
* NodeJS
|
|
9
|
-
* iOS: `xcrun simctl` (available with Xcode)
|
|
10
|
-
* Android: `adb` (available with Android Studio / Android SDK)
|
|
7
|
+
---
|
|
11
8
|
|
|
12
|
-
##
|
|
9
|
+
## 🤔 Why did I build this?
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
Honestly? I was tired of jumping through hoops to inspect local databases while building React Native apps. Extracting SQLite files from an iOS Simulator or bypassing root permissions on an Android Emulator just to run a `SELECT *` was ruining my flow state.
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
I wanted my AI assistant to just *know* what my database looked like and query it in real-time. So I built this bridge. It's mobile development less painful.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## 📦 Quick Start (The Magic Way)
|
|
17
|
+
|
|
18
|
+
You don't even need to clone this repo. The easiest way to get rolling is via `npx`.
|
|
19
|
+
|
|
20
|
+
Toss this bad boy into your `mcp.json` (or your Claude/Cursor/agent settings):
|
|
17
21
|
|
|
18
22
|
```json
|
|
19
23
|
{
|
|
@@ -30,18 +34,20 @@ Add this to your `claude_desktop_config.json` (or Cursor/other MCP client settin
|
|
|
30
34
|
}
|
|
31
35
|
```
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
Boom. You're connected. 🤝
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
## 🛠️ Manual Installation (For the brave)
|
|
40
|
+
|
|
41
|
+
Prefer to tinker with the source code yourself? I respect it.
|
|
36
42
|
|
|
37
43
|
```bash
|
|
38
|
-
git clone
|
|
44
|
+
git clone https://github.com/your-username/react-native-sqlite-mcp.git
|
|
39
45
|
cd react-native-sqlite-mcp
|
|
40
46
|
npm install
|
|
41
47
|
npm run build
|
|
42
48
|
```
|
|
43
49
|
|
|
44
|
-
Then
|
|
50
|
+
Then point your MCP client to your local build:
|
|
45
51
|
|
|
46
52
|
```json
|
|
47
53
|
{
|
|
@@ -57,27 +63,42 @@ Then configure your MCP client to point to the local file:
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
```
|
|
60
|
-
*Note: Replace `/absolute/path/to/react-native-sqlite-mcp` with the absolute path to where you cloned this repository.*
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- `
|
|
67
|
+
## 🎛️ Environment Variables (The Knobs)
|
|
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.
|
|
65
72
|
|
|
66
|
-
## Features
|
|
73
|
+
## 🦸♂️ Features (What this bad boy can do)
|
|
67
74
|
|
|
68
|
-
This MCP
|
|
75
|
+
This MCP arms your AI with four super-powered tools:
|
|
69
76
|
|
|
70
|
-
- **`list_databases`**:
|
|
71
|
-
- **`sync_database`**:
|
|
72
|
-
- **`inspect_schema`**: Returns the `CREATE TABLE` and column
|
|
73
|
-
- **`read_table_contents`**:
|
|
74
|
-
- **`query_db`**:
|
|
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.
|
|
75
82
|
|
|
76
|
-
## How it Works
|
|
83
|
+
## ⚙️ How it Actually Works (Under the hood)
|
|
77
84
|
|
|
78
|
-
1. **Auto-Detect Platform**:
|
|
85
|
+
1. **Auto-Detect Platform**: It scans **both** iOS and Android environments simultaneously. It hunts down booted iOS Simulators using `simctl` and active Android Emulators using `adb`.
|
|
79
86
|
2. **Auto-Locate Database**:
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
3. **Platform Switching**: The
|
|
83
|
-
4. **Execution**:
|
|
87
|
+
- **iOS:** We dive straight into the simulator's app sandbox. No root needed.
|
|
88
|
+
- **Android:** We do a sneaky `adb exec-out run-as com.pkg.name cat ...` to copy the database file, along with its `-wal` and `-shm` sidekicks, completely bypassing the strict root permission boundaries on debug profiles.
|
|
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.
|
|
91
|
+
|
|
92
|
+
## 🤝 Contributing (Yes, please!)
|
|
93
|
+
|
|
94
|
+
Got an idea to make this objectively cooler? Found a bug where it accidentally queried your smart fridge?
|
|
95
|
+
|
|
96
|
+
I am **all in** on community contributions. Whether you're fixing a typo, optimizing the ADB scripts, or adding support for Windows Phone (please don't), I want your PRs.
|
|
97
|
+
|
|
98
|
+
Check out the [CONTRIBUTING.md](./CONTRIBUTING.md) guide to see how we party.
|
|
99
|
+
|
|
100
|
+
## 📜 License
|
|
101
|
+
|
|
102
|
+
This project is licensed under the **MIT License**.
|
|
103
|
+
|
|
104
|
+
See the [LICENSE](./LICENSE) file for the legal jargon.
|
package/dist/db.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import sqlite3 from "sqlite3";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight sqlite3 wrapper with Promise-based API.
|
|
5
|
+
*/
|
|
2
6
|
export class Database {
|
|
3
7
|
db;
|
|
4
8
|
constructor(filename) {
|
|
@@ -35,40 +39,83 @@ export class Database {
|
|
|
35
39
|
});
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
const CACHE_TTL_MS = 60_000; // Close idle connections after 60s
|
|
43
|
+
const connectionCache = new Map();
|
|
44
|
+
function getCachedDb(dbPath) {
|
|
45
|
+
const existing = connectionCache.get(dbPath);
|
|
46
|
+
if (existing) {
|
|
47
|
+
// Refresh the idle timer
|
|
48
|
+
clearTimeout(existing.timer);
|
|
49
|
+
existing.lastUsed = Date.now();
|
|
50
|
+
existing.timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
|
|
51
|
+
return existing.db;
|
|
52
|
+
}
|
|
53
|
+
// Open a new connection
|
|
42
54
|
const db = new Database(dbPath);
|
|
55
|
+
const timer = setTimeout(() => evictConnection(dbPath), CACHE_TTL_MS);
|
|
56
|
+
connectionCache.set(dbPath, { db, lastUsed: Date.now(), timer });
|
|
57
|
+
logger.debug(`Opened DB connection: ${dbPath}`);
|
|
58
|
+
return db;
|
|
59
|
+
}
|
|
60
|
+
async function evictConnection(dbPath) {
|
|
61
|
+
const entry = connectionCache.get(dbPath);
|
|
62
|
+
if (!entry)
|
|
63
|
+
return;
|
|
64
|
+
connectionCache.delete(dbPath);
|
|
43
65
|
try {
|
|
44
|
-
|
|
66
|
+
await entry.db.close();
|
|
67
|
+
logger.debug(`Closed idle DB connection: ${dbPath}`);
|
|
45
68
|
}
|
|
46
|
-
|
|
47
|
-
|
|
69
|
+
catch (e) {
|
|
70
|
+
logger.warn(`Error closing DB: ${dbPath}`, { error: String(e) });
|
|
48
71
|
}
|
|
49
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Close all cached connections. Called during graceful shutdown.
|
|
75
|
+
*/
|
|
76
|
+
export async function closeAllConnections() {
|
|
77
|
+
const paths = [...connectionCache.keys()];
|
|
78
|
+
for (const p of paths) {
|
|
79
|
+
await evictConnection(p);
|
|
80
|
+
}
|
|
81
|
+
logger.info(`Closed ${paths.length} cached DB connection(s)`);
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Public query API
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
/** Query timeout — prevents stuck queries from blocking everything */
|
|
87
|
+
const QUERY_TIMEOUT_MS = 30_000;
|
|
88
|
+
function withTimeout(promise, ms, label) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const timer = setTimeout(() => {
|
|
91
|
+
reject(new Error(`Query timed out after ${ms}ms: ${label}`));
|
|
92
|
+
}, ms);
|
|
93
|
+
promise
|
|
94
|
+
.then((result) => { clearTimeout(timer); resolve(result); })
|
|
95
|
+
.catch((err) => { clearTimeout(timer); reject(err); });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Executes a simple query on the database.
|
|
100
|
+
*/
|
|
101
|
+
export async function queryDb(dbPath, sql, params = []) {
|
|
102
|
+
const db = getCachedDb(dbPath);
|
|
103
|
+
return withTimeout(db.all(sql, params), QUERY_TIMEOUT_MS, sql.slice(0, 80));
|
|
104
|
+
}
|
|
50
105
|
/**
|
|
51
106
|
* Returns a detailed schema of all tables in the database.
|
|
52
107
|
*/
|
|
53
108
|
export async function inspectSchema(dbPath) {
|
|
54
|
-
const db =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
schemaInfo[table.name] = {
|
|
65
|
-
columns,
|
|
66
|
-
createSql: createSql?.sql
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return schemaInfo;
|
|
70
|
-
}
|
|
71
|
-
finally {
|
|
72
|
-
await db.close();
|
|
109
|
+
const db = getCachedDb(dbPath);
|
|
110
|
+
const tables = await withTimeout(db.all("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"), QUERY_TIMEOUT_MS, "inspect_schema:tables");
|
|
111
|
+
const schemaInfo = {};
|
|
112
|
+
for (const table of tables) {
|
|
113
|
+
const columns = await db.all(`PRAGMA table_info("${table.name}");`);
|
|
114
|
+
const createSql = await db.get(`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`, [table.name]);
|
|
115
|
+
schemaInfo[table.name] = {
|
|
116
|
+
columns,
|
|
117
|
+
createSql: createSql?.sql
|
|
118
|
+
};
|
|
73
119
|
}
|
|
120
|
+
return schemaInfo;
|
|
74
121
|
}
|
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
|
});
|