react-native-sqlite-mcp 1.0.0
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 +83 -0
- package/dist/db.js +74 -0
- package/dist/index.js +247 -0
- package/dist/locator.js +229 -0
- package/package.json +33 -0
- package/src/db.ts +85 -0
- package/src/index.ts +285 -0
- package/src/locator.ts +255 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Universal React Native SQLite MCP
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that connects LLMs (Claude, Cursor, etc.) to your local React Native SQLite databases running on an iOS Simulator or Android Emulator.
|
|
4
|
+
|
|
5
|
+
This essentially acts as a "Database Inspector" for AI agents, allowing them to automatically view your DB schema and execute queries against the live app database without you having to manually export or describe tables.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
* NodeJS
|
|
9
|
+
* iOS: `xcrun simctl` (available with Xcode)
|
|
10
|
+
* Android: `adb` (available with Android Studio / Android SDK)
|
|
11
|
+
|
|
12
|
+
## Quick Start (Recommended)
|
|
13
|
+
|
|
14
|
+
The easiest way to use this MCP server is via `npx`. This doesn't require cloning the repository.
|
|
15
|
+
|
|
16
|
+
Add this to your `claude_desktop_config.json` (or Cursor/other MCP client settings):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"rn-sqlite-bridge": {
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["-y", "react-native-sqlite-mcp"],
|
|
24
|
+
"env": {
|
|
25
|
+
"DB_NAME": "my_database.db",
|
|
26
|
+
"ANDROID_BUNDLE_ID": "com.mycompany.myapp"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Manual Installation (From Source)
|
|
34
|
+
|
|
35
|
+
If you want to run it locally from source:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git clone <your-repo> react-native-sqlite-mcp
|
|
39
|
+
cd react-native-sqlite-mcp
|
|
40
|
+
npm install
|
|
41
|
+
npm run build
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then configure your MCP client to point to the local file:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"rn-sqlite-bridge": {
|
|
50
|
+
"command": "node",
|
|
51
|
+
"args": ["/absolute/path/to/react-native-sqlite-mcp/dist/index.js"],
|
|
52
|
+
"env": {
|
|
53
|
+
"DB_NAME": "my_database.db",
|
|
54
|
+
"ANDROID_BUNDLE_ID": "com.mycompany.myapp"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
*Note: Replace `/absolute/path/to/react-native-sqlite-mcp` with the absolute path to where you cloned this repository.*
|
|
61
|
+
|
|
62
|
+
### Environment Variables
|
|
63
|
+
- `DB_NAME`: The filename of your database (e.g., `my_app.db`) or a glob pattern (`*.db`).
|
|
64
|
+
- `ANDROID_BUNDLE_ID`: Only required for Android. The application ID/package name of your app (e.g., `com.mycompany.app`). Optional: If omitted, the MCP will scan all third-party apps on the emulator for SQLite databases.
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
This MCP provides four core tools:
|
|
69
|
+
|
|
70
|
+
- **`list_databases`**: Discovers and returns a list of all SQLite databases currently available. You can optionally pass `platform` ('ios' or 'android') to explicitly target one environment.
|
|
71
|
+
- **`sync_database`**: Pulls a local copy of a database from the active device so the AI can inspect it, and sets it as the active database. `dbName`, `bundleId`, and `platform` are all optional; if omitted, it will automatically select the first discovered default database across all running emulators.
|
|
72
|
+
- **`inspect_schema`**: Returns the `CREATE TABLE` and column information for the currently active synced database. Gives the AI the map of your database. Optionally accepts `tableName`, `dbName`, and `platform` to skip explicit syncing.
|
|
73
|
+
- **`read_table_contents`**: Returns all rows from a specified table. Equivalent to `SELECT * FROM table_name`, limited to 100 rows by default.
|
|
74
|
+
- **`query_db`**: Accepts a raw SQL query and returns the results for the currently active database. Optionally accepts `dbName` and `platform` to skip explicit syncing.
|
|
75
|
+
|
|
76
|
+
## How it Works
|
|
77
|
+
|
|
78
|
+
1. **Auto-Detect Platform**: By default, the tool will scan **both** iOS and Android environments. It locates booted iOS Simulators using `simctl` and active Android Emulators using `adb`.
|
|
79
|
+
2. **Auto-Locate Database**:
|
|
80
|
+
- For iOS, the simulator's app sandbox files are directly accessed without needing root.
|
|
81
|
+
- For Android, it uses `adb exec-out run-as com.pkg.name cat ...` to copy the database file, along with `-wal` and `-shm` temp files, bypassing strict root permission boundaries on debug profiles.
|
|
82
|
+
3. **Platform Switching**: The MCP server maintains a single active database connection. If you want to switch between iOS and Android, the AI simply calls `sync_database` targeting the desired platform.
|
|
83
|
+
4. **Execution**: Wraps arbitrary requests allowing the LLM to learn the schema and query live data.
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import sqlite3 from "sqlite3";
|
|
2
|
+
export class Database {
|
|
3
|
+
db;
|
|
4
|
+
constructor(filename) {
|
|
5
|
+
this.db = new sqlite3.Database(filename);
|
|
6
|
+
}
|
|
7
|
+
all(sql, params = []) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
this.db.all(sql, params, (err, rows) => {
|
|
10
|
+
if (err)
|
|
11
|
+
reject(err);
|
|
12
|
+
else
|
|
13
|
+
resolve(rows);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
get(sql, params = []) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
this.db.get(sql, params, (err, row) => {
|
|
20
|
+
if (err)
|
|
21
|
+
reject(err);
|
|
22
|
+
else
|
|
23
|
+
resolve(row);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
close() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
this.db.close((err) => {
|
|
30
|
+
if (err)
|
|
31
|
+
reject(err);
|
|
32
|
+
else
|
|
33
|
+
resolve();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Executes a simple query on the database.
|
|
40
|
+
*/
|
|
41
|
+
export async function queryDb(dbPath, sql, params = []) {
|
|
42
|
+
const db = new Database(dbPath);
|
|
43
|
+
try {
|
|
44
|
+
return await db.all(sql, params);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await db.close();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns a detailed schema of all tables in the database.
|
|
52
|
+
*/
|
|
53
|
+
export async function inspectSchema(dbPath) {
|
|
54
|
+
const db = new Database(dbPath);
|
|
55
|
+
try {
|
|
56
|
+
// Get all table names
|
|
57
|
+
const tables = await db.all("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;");
|
|
58
|
+
const schemaInfo = {};
|
|
59
|
+
for (const table of tables) {
|
|
60
|
+
// For each table, get column definitions
|
|
61
|
+
const columns = await db.all(`PRAGMA table_info("${table.name}");`);
|
|
62
|
+
// Get table creation SQL
|
|
63
|
+
const createSql = await db.get(`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`, [table.name]);
|
|
64
|
+
schemaInfo[table.name] = {
|
|
65
|
+
columns,
|
|
66
|
+
createSql: createSql?.sql
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return schemaInfo;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await db.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { listDatabases, syncDatabase } from "./locator.js";
|
|
6
|
+
import { inspectSchema, queryDb } from "./db.js";
|
|
7
|
+
let activeDatabases = [];
|
|
8
|
+
const server = new Server({
|
|
9
|
+
name: "react-native-sqlite-bridge",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
}, {
|
|
12
|
+
capabilities: {
|
|
13
|
+
tools: {},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
17
|
+
return {
|
|
18
|
+
tools: [
|
|
19
|
+
{
|
|
20
|
+
name: "sync_database",
|
|
21
|
+
description: "Re-runs the adb pull or file-find logic to ensure the AI is looking at the latest data from the emulator/simulator.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
dbName: {
|
|
26
|
+
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."
|
|
28
|
+
},
|
|
29
|
+
bundleId: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS."
|
|
32
|
+
},
|
|
33
|
+
platform: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Optional. Explicitly target 'ios' or 'android'."
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "list_databases",
|
|
42
|
+
description: "Lists all available SQLite databases found on the iOS Simulator or Android Emulator.",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
bundleId: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS."
|
|
49
|
+
},
|
|
50
|
+
platform: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Optional. Explicitly target 'ios' or 'android'."
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "inspect_schema",
|
|
59
|
+
description: "Returns a list of all tables and their column definitions. This gives the AI the 'map' of the database.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
dbName: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
|
|
66
|
+
},
|
|
67
|
+
platform: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
required: []
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "read_table_contents",
|
|
77
|
+
description: "Returns rows from a specific table. Equivalent to SELECT * FROM table_name.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
tableName: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "The name of the table to read."
|
|
84
|
+
},
|
|
85
|
+
limit: {
|
|
86
|
+
type: "number",
|
|
87
|
+
description: "Optional limit to the number of rows returned. Defaults to 100."
|
|
88
|
+
},
|
|
89
|
+
dbName: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
|
|
92
|
+
},
|
|
93
|
+
platform: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
required: ["tableName"]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "query_db",
|
|
103
|
+
description: "Accepts a raw SQL SELECT string and returns the JSON result set.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
sql: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "The raw SQL SELECT string to execute."
|
|
110
|
+
},
|
|
111
|
+
params: {
|
|
112
|
+
type: "array",
|
|
113
|
+
description: "Optional arguments to bind to the SQL query. Use this to safely substitute ? placeholders in your SQL string (e.g. ['value', 42]).",
|
|
114
|
+
items: {
|
|
115
|
+
type: ["string", "number", "boolean", "null"],
|
|
116
|
+
description: "A single bound parameter value."
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
dbName: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
|
|
122
|
+
},
|
|
123
|
+
platform: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
required: ["sql"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
// Helper to sanitize platform input if any
|
|
135
|
+
function cleanPlatform(raw) {
|
|
136
|
+
if (!raw)
|
|
137
|
+
return undefined;
|
|
138
|
+
const cleaned = raw.replace(/['"]/g, '').trim().toLowerCase();
|
|
139
|
+
if (cleaned === 'ios' || cleaned === 'android')
|
|
140
|
+
return cleaned;
|
|
141
|
+
return undefined; // If they pass garbage, just let locator try both
|
|
142
|
+
}
|
|
143
|
+
// Helper to ensure database is synced based on provided args
|
|
144
|
+
async function ensureDbState(args) {
|
|
145
|
+
const reqDbName = args?.dbName;
|
|
146
|
+
const reqPlatform = cleanPlatform(args?.platform);
|
|
147
|
+
// If nothing is synced, sync defaults
|
|
148
|
+
if (activeDatabases.length === 0) {
|
|
149
|
+
const envDb = reqDbName || process.env.DB_NAME;
|
|
150
|
+
const envBundle = process.env.ANDROID_BUNDLE_ID;
|
|
151
|
+
activeDatabases = await syncDatabase(envDb, envBundle, reqPlatform);
|
|
152
|
+
}
|
|
153
|
+
// Filter based on explicit requirements
|
|
154
|
+
let candidates = activeDatabases;
|
|
155
|
+
if (reqPlatform)
|
|
156
|
+
candidates = candidates.filter(db => db.platform === reqPlatform);
|
|
157
|
+
if (reqDbName)
|
|
158
|
+
candidates = candidates.filter(db => db.dbName === reqDbName);
|
|
159
|
+
if (candidates.length === 1) {
|
|
160
|
+
return candidates[0];
|
|
161
|
+
}
|
|
162
|
+
if (candidates.length === 0) {
|
|
163
|
+
throw new Error(`No synced databases match the criteria (platform: ${reqPlatform || 'any'}, dbName: ${reqDbName || 'any'}). Try calling sync_database first.`);
|
|
164
|
+
}
|
|
165
|
+
const matches = candidates.map(c => `[${c.platform}] ${c.dbName}`).join(", ");
|
|
166
|
+
throw new Error(`Multiple databases match the criteria. Please specify 'platform' or 'dbName'. Matches: ${matches}`);
|
|
167
|
+
}
|
|
168
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
169
|
+
const { name, arguments: args } = request.params;
|
|
170
|
+
try {
|
|
171
|
+
if (name === "list_databases") {
|
|
172
|
+
const bundleId = args?.bundleId;
|
|
173
|
+
const platform = cleanPlatform(args?.platform);
|
|
174
|
+
const results = await listDatabases(bundleId, platform);
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (name === "sync_database") {
|
|
180
|
+
const dbName = args?.dbName;
|
|
181
|
+
const bundleId = args?.bundleId;
|
|
182
|
+
const platform = cleanPlatform(args?.platform);
|
|
183
|
+
const results = await syncDatabase(dbName, bundleId, platform);
|
|
184
|
+
activeDatabases = results; // Replace the active list
|
|
185
|
+
let msg = "Successfully synced databases:\n";
|
|
186
|
+
for (const res of results) {
|
|
187
|
+
msg += `- Platform: ${res.platform} | DB: ${res.dbName}\n Path: ${res.localPath}\n`;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: msg }]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (name === "inspect_schema") {
|
|
194
|
+
const activeDb = await ensureDbState(args);
|
|
195
|
+
const schema = await inspectSchema(activeDb.localPath);
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` + JSON.stringify(schema, null, 2) }]
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (name === "read_table_contents") {
|
|
201
|
+
const activeDb = await ensureDbState(args);
|
|
202
|
+
const tableName = args?.tableName;
|
|
203
|
+
const limit = args?.limit || 100;
|
|
204
|
+
if (!tableName) {
|
|
205
|
+
throw new Error("Missing required argument: tableName");
|
|
206
|
+
}
|
|
207
|
+
const sql = `SELECT * FROM "${tableName}" LIMIT ?`;
|
|
208
|
+
const results = await queryDb(activeDb.localPath, sql, [limit]);
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName} | Table: ${tableName} | Limit: ${limit}]\n` + JSON.stringify(results, null, 2) }]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (name === "query_db") {
|
|
214
|
+
const activeDb = await ensureDbState(args);
|
|
215
|
+
const sql = args?.sql;
|
|
216
|
+
const params = args?.params || [];
|
|
217
|
+
if (!sql) {
|
|
218
|
+
throw new Error("Missing required argument: sql");
|
|
219
|
+
}
|
|
220
|
+
const results = await queryDb(activeDb.localPath, sql, params);
|
|
221
|
+
return {
|
|
222
|
+
content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` + JSON.stringify(results, null, 2) }]
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
return {
|
|
229
|
+
content: [
|
|
230
|
+
{
|
|
231
|
+
type: "text",
|
|
232
|
+
text: `Error: ${error.message}`
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
isError: true
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
async function run() {
|
|
240
|
+
const transport = new StdioServerTransport();
|
|
241
|
+
await server.connect(transport);
|
|
242
|
+
console.error("Universal React Native SQLite MCP Server running on stdio");
|
|
243
|
+
}
|
|
244
|
+
run().catch((error) => {
|
|
245
|
+
console.error("Server error:", error);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
package/dist/locator.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
export async function listDatabases(bundleId, targetPlatform) {
|
|
6
|
+
const results = [];
|
|
7
|
+
if (!targetPlatform || targetPlatform === 'ios') {
|
|
8
|
+
try {
|
|
9
|
+
const udidStr = execSync("xcrun simctl list devices booted | awk -F '[()]' '/Booted/{print $2; exit}'", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 }).toString().trim();
|
|
10
|
+
if (udidStr) {
|
|
11
|
+
const appDataDir = `${process.env.HOME}/Library/Developer/CoreSimulator/Devices/${udidStr}/data/Containers/Data/Application`;
|
|
12
|
+
if (fs.existsSync(appDataDir)) {
|
|
13
|
+
try {
|
|
14
|
+
const findCmd = `find "${appDataDir}" -type f \\( -name "*.db" -o -name "*.sqlite" -o -name "*.sqlite3" \\) -maxdepth 7 -print`;
|
|
15
|
+
const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 10000 }).toString().trim();
|
|
16
|
+
if (found) {
|
|
17
|
+
results.push({
|
|
18
|
+
platform: 'ios',
|
|
19
|
+
appDir: appDataDir,
|
|
20
|
+
databases: found.split('\n').map(p => path.basename(p.trim())).filter(Boolean)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
console.error("iOS find failed", e);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (targetPlatform === 'ios') {
|
|
30
|
+
throw new Error("No booted iOS Simulator found (simctl returned empty).");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
if (targetPlatform === 'ios' && !e.message?.includes("find failed")) {
|
|
35
|
+
throw new Error("No booted iOS Simulator found or xcrun failed.");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!targetPlatform || targetPlatform === 'android') {
|
|
40
|
+
try {
|
|
41
|
+
execSync("adb get-state", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 });
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (targetPlatform === 'android') {
|
|
45
|
+
throw new Error("No booted Android Emulator found or adb is unresponsive.");
|
|
46
|
+
}
|
|
47
|
+
if (results.length === 0) {
|
|
48
|
+
throw new Error("No booted iOS Simulator or Android Emulator device found.");
|
|
49
|
+
}
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
// if we have a specific bundleId-use it otherwise hunt
|
|
53
|
+
let packagesToScan = [];
|
|
54
|
+
if (bundleId) {
|
|
55
|
+
packagesToScan = [bundleId];
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
try {
|
|
59
|
+
const packagesStr = execSync("adb shell pm list packages -3", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
|
|
60
|
+
packagesToScan = packagesStr.split('\n')
|
|
61
|
+
.map(line => line.replace('package:', '').trim())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (results.length === 0) {
|
|
66
|
+
throw new Error("Could not list packages on Android Emulator to discover databases. Is it fully booted?");
|
|
67
|
+
}
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const allAndroidDatabases = [];
|
|
72
|
+
let lastSuccessfulAppDir;
|
|
73
|
+
for (const pkg of packagesToScan) {
|
|
74
|
+
const baseDirs = [`/data/user/0/${pkg}`, `/data/data/${pkg}`];
|
|
75
|
+
for (const baseDir of baseDirs) {
|
|
76
|
+
try {
|
|
77
|
+
execSync(`adb shell run-as ${pkg} ls -d ${baseDir}`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 2000 });
|
|
78
|
+
let foundFiles = [];
|
|
79
|
+
try {
|
|
80
|
+
const findCmd = `adb shell "run-as ${pkg} find ${baseDir} -type f \\( -name \\"*.db\\" -o -name \\"*.sqlite\\" -o -name \\"*.sqlite3\\" \\)"`;
|
|
81
|
+
const findOut = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
|
|
82
|
+
if (findOut) {
|
|
83
|
+
foundFiles.push(...findOut.split('\n').map(l => l.trim().replace(/\r/g, '')).filter(Boolean));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (e) { }
|
|
87
|
+
try {
|
|
88
|
+
const lsOut = execSync(`adb shell run-as ${pkg} ls -1p ${baseDir}/databases`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 2000 }).toString().trim();
|
|
89
|
+
const lsFiles = lsOut.split('\n')
|
|
90
|
+
.map(l => l.trim().replace(/\r/g, ''))
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.filter(f => !f.endsWith('/'))
|
|
93
|
+
.filter(f => !f.endsWith('-journal') && !f.endsWith('-wal') && !f.endsWith('-shm'))
|
|
94
|
+
.filter(f => !f.includes('.'))
|
|
95
|
+
.map(f => `${baseDir}/databases/${f}`);
|
|
96
|
+
foundFiles.push(...lsFiles);
|
|
97
|
+
}
|
|
98
|
+
catch (e) { }
|
|
99
|
+
// Deduplicate
|
|
100
|
+
foundFiles = [...new Set(foundFiles)];
|
|
101
|
+
if (foundFiles.length > 0) {
|
|
102
|
+
const displayFiles = foundFiles.map(f => f.replace(`${baseDir}/`, ''));
|
|
103
|
+
allAndroidDatabases.push(...displayFiles);
|
|
104
|
+
lastSuccessfulAppDir = `${baseDir}::${pkg}`;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
console.error(`Failed to list databases for app: ${pkg}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (allAndroidDatabases.length > 0) {
|
|
114
|
+
results.push({
|
|
115
|
+
platform: 'android',
|
|
116
|
+
appDir: lastSuccessfulAppDir,
|
|
117
|
+
databases: allAndroidDatabases
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else if (targetPlatform === 'android') {
|
|
121
|
+
throw new Error(`Android Emulator is booted, but no SQLite databases were found in any debuggable third-party packages.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Finds the DB file based on platform and a provided/detected filename.
|
|
128
|
+
* If dbNameGlob is empty/undefined, it will auto-select the first discovered database.
|
|
129
|
+
* Prioritizes iOS (simctl), falls back to Android (adb).
|
|
130
|
+
* Returns the local file path (either the iOS original or a pulled Android copy).
|
|
131
|
+
*/
|
|
132
|
+
export async function syncDatabase(dbNameGlob, bundleId, targetPlatform) {
|
|
133
|
+
// First, discover available databases
|
|
134
|
+
const locations = await listDatabases(bundleId, targetPlatform);
|
|
135
|
+
if (locations.length === 0) {
|
|
136
|
+
if (targetPlatform) {
|
|
137
|
+
throw new Error(`No SQLite databases found for platform '${targetPlatform}'.`);
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`No SQLite databases found on any platform.`);
|
|
140
|
+
}
|
|
141
|
+
const synced = [];
|
|
142
|
+
for (const loc of locations) {
|
|
143
|
+
if (targetPlatform && loc.platform !== targetPlatform)
|
|
144
|
+
continue;
|
|
145
|
+
let targetDbNames = [];
|
|
146
|
+
if (!dbNameGlob) {
|
|
147
|
+
// Auto-select: find the first .db or .sqlite file in this location
|
|
148
|
+
const preferred = loc.databases.find(d => d.endsWith('.db') || d.endsWith('.sqlite'));
|
|
149
|
+
if (preferred) {
|
|
150
|
+
targetDbNames.push(preferred);
|
|
151
|
+
}
|
|
152
|
+
else if (loc.databases.length > 0) {
|
|
153
|
+
targetDbNames.push(loc.databases[0]); // fallback to first file
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const globRegex = new RegExp('^' + dbNameGlob.replace(/\*/g, '.*') + '$');
|
|
158
|
+
targetDbNames = loc.databases.filter(name => globRegex.test(name));
|
|
159
|
+
}
|
|
160
|
+
for (const targetDbName of targetDbNames) {
|
|
161
|
+
const { platform, appDir } = loc;
|
|
162
|
+
// --- iOS Logic ---
|
|
163
|
+
if (platform === 'ios') {
|
|
164
|
+
if (!appDir)
|
|
165
|
+
continue;
|
|
166
|
+
// Find the exact path of the targetDbName
|
|
167
|
+
const findCmd = `find "${appDir}" -type f -name "${targetDbName}" -maxdepth 7 -print | head -n 1`;
|
|
168
|
+
try {
|
|
169
|
+
const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
|
|
170
|
+
if (found && fs.existsSync(found)) {
|
|
171
|
+
console.error(`Located iOS DB at: ${found}`);
|
|
172
|
+
synced.push({ localPath: found, dbName: targetDbName, platform: 'ios' });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
console.error(`Failed to locate full path for iOS DB: ${targetDbName}`);
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// --- Android Logic ---
|
|
181
|
+
if (!appDir || !appDir.includes("::")) {
|
|
182
|
+
console.error(`Invalid Android appDir format: ${appDir}`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const [targetDbDir, targetPkg] = appDir.split("::");
|
|
186
|
+
try {
|
|
187
|
+
execSync(`adb shell am force-stop ${targetPkg}`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 });
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
console.error(`Failed to force-stop app: ${targetPkg}`);
|
|
191
|
+
}
|
|
192
|
+
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "rn-sqlite-mcp-"));
|
|
193
|
+
const safeLocalName = targetDbName.replace(/\//g, '_');
|
|
194
|
+
const localDb = path.join(tmpdir, safeLocalName);
|
|
195
|
+
const localWal = `${localDb}-wal`;
|
|
196
|
+
const localShm = `${localDb}-shm`;
|
|
197
|
+
const remoteMain = `${targetDbDir}/${targetDbName}`;
|
|
198
|
+
const remoteWal = `${remoteMain}-wal`;
|
|
199
|
+
const remoteShm = `${remoteMain}-shm`;
|
|
200
|
+
const pullOne = (remote, local) => {
|
|
201
|
+
try {
|
|
202
|
+
const remoteBase = path.basename(remote);
|
|
203
|
+
const tmpRemote = `/data/local/tmp/${targetPkg}_${remoteBase}_${Date.now()}`;
|
|
204
|
+
execSync(`adb shell "run-as '${targetPkg}' cat '${remote}' > '${tmpRemote}'"`, { stdio: 'ignore', timeout: 5000 });
|
|
205
|
+
execSync(`adb pull '${tmpRemote}' '${local}'`, { stdio: 'ignore', timeout: 5000 });
|
|
206
|
+
execSync(`adb shell rm '${tmpRemote}'`, { stdio: 'ignore', timeout: 3000 });
|
|
207
|
+
return fs.existsSync(local) && fs.statSync(local).size > 0;
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
if (fs.existsSync(local))
|
|
211
|
+
fs.unlinkSync(local);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
if (!pullOne(remoteMain, localDb)) {
|
|
216
|
+
console.error(`Failed to pull main DB file from Android: ${remoteMain}`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
pullOne(remoteWal, localWal);
|
|
220
|
+
pullOne(remoteShm, localShm);
|
|
221
|
+
console.error(`Pulled Android DB to local temp: ${localDb}`);
|
|
222
|
+
synced.push({ localPath: localDb, dbName: targetDbName, platform: 'android' });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (synced.length === 0) {
|
|
226
|
+
throw new Error(`Failed to sync any databases matching '${dbNameGlob || 'auto-select'}'.`);
|
|
227
|
+
}
|
|
228
|
+
return synced;
|
|
229
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-sqlite-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Universal React Native SQLite MCP Server",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"react-native-sqlite-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"react-native",
|
|
18
|
+
"sqlite",
|
|
19
|
+
"claude",
|
|
20
|
+
"cursor"
|
|
21
|
+
],
|
|
22
|
+
"author": "Brian Murillo",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.6.0",
|
|
26
|
+
"sqlite3": "^5.1.7"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.13.4",
|
|
30
|
+
"@types/sqlite3": "^3.1.11",
|
|
31
|
+
"typescript": "^5.7.3"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import sqlite3 from "sqlite3";
|
|
2
|
+
|
|
3
|
+
export class Database {
|
|
4
|
+
private db: sqlite3.Database;
|
|
5
|
+
|
|
6
|
+
constructor(filename: string) {
|
|
7
|
+
this.db = new sqlite3.Database(filename);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public all<T>(sql: string, params: any[] = []): Promise<T[]> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
this.db.all(sql, params, (err, rows) => {
|
|
13
|
+
if (err) reject(err);
|
|
14
|
+
else resolve(rows as Array<T>);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public get<T>(sql: string, params: any[] = []): Promise<T | undefined> {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
this.db.get(sql, params, (err, row) => {
|
|
22
|
+
if (err) reject(err);
|
|
23
|
+
else resolve(row as T | undefined);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public close(): Promise<void> {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
this.db.close((err) => {
|
|
31
|
+
if (err) reject(err);
|
|
32
|
+
else resolve();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Executes a simple query on the database.
|
|
40
|
+
*/
|
|
41
|
+
export async function queryDb(dbPath: string, sql: string, params: any[] = []): Promise<any[]> {
|
|
42
|
+
const db = new Database(dbPath);
|
|
43
|
+
try {
|
|
44
|
+
return await db.all(sql, params);
|
|
45
|
+
} finally {
|
|
46
|
+
await db.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns a detailed schema of all tables in the database.
|
|
52
|
+
*/
|
|
53
|
+
export async function inspectSchema(dbPath: string): Promise<any> {
|
|
54
|
+
const db = new Database(dbPath);
|
|
55
|
+
try {
|
|
56
|
+
// Get all table names
|
|
57
|
+
const tables = await db.all<{ name: string }>(
|
|
58
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const schemaInfo: Record<string, any> = {};
|
|
62
|
+
|
|
63
|
+
for (const table of tables) {
|
|
64
|
+
// For each table, get column definitions
|
|
65
|
+
const columns = await db.all(
|
|
66
|
+
`PRAGMA table_info("${table.name}");`
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Get table creation SQL
|
|
70
|
+
const createSql = await db.get<{ sql: string }>(
|
|
71
|
+
`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
|
|
72
|
+
[table.name]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
schemaInfo[table.name] = {
|
|
76
|
+
columns,
|
|
77
|
+
createSql: createSql?.sql
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return schemaInfo;
|
|
82
|
+
} finally {
|
|
83
|
+
await db.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
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 } from "./db.js";
|
|
11
|
+
|
|
12
|
+
// Keep track of the currently active databases
|
|
13
|
+
interface SyncedDB {
|
|
14
|
+
localPath: string;
|
|
15
|
+
dbName: string;
|
|
16
|
+
platform: 'ios' | 'android';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let activeDatabases: SyncedDB[] = [];
|
|
20
|
+
|
|
21
|
+
const server = new Server(
|
|
22
|
+
{
|
|
23
|
+
name: "react-native-sqlite-bridge",
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
capabilities: {
|
|
28
|
+
tools: {},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
34
|
+
return {
|
|
35
|
+
tools: [
|
|
36
|
+
{
|
|
37
|
+
name: "sync_database",
|
|
38
|
+
description: "Re-runs the adb pull or file-find logic to ensure the AI is looking at the latest data from the emulator/simulator.",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
dbName: {
|
|
43
|
+
type: "string",
|
|
44
|
+
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."
|
|
45
|
+
},
|
|
46
|
+
bundleId: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS."
|
|
49
|
+
},
|
|
50
|
+
platform: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Optional. Explicitly target 'ios' or 'android'."
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "list_databases",
|
|
59
|
+
description: "Lists all available SQLite databases found on the iOS Simulator or Android Emulator.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
bundleId: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "(Android only) The application bundle ID (e.g., 'com.example.app'). Not required for iOS."
|
|
66
|
+
},
|
|
67
|
+
platform: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Optional. Explicitly target 'ios' or 'android'."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "inspect_schema",
|
|
76
|
+
description: "Returns a list of all tables and their column definitions. This gives the AI the 'map' of the database.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
dbName: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
|
|
83
|
+
},
|
|
84
|
+
platform: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
required: []
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "read_table_contents",
|
|
94
|
+
description: "Returns rows from a specific table. Equivalent to SELECT * FROM table_name.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {
|
|
98
|
+
tableName: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "The name of the table to read."
|
|
101
|
+
},
|
|
102
|
+
limit: {
|
|
103
|
+
type: "number",
|
|
104
|
+
description: "Optional limit to the number of rows returned. Defaults to 100."
|
|
105
|
+
},
|
|
106
|
+
dbName: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
|
|
109
|
+
},
|
|
110
|
+
platform: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
required: ["tableName"]
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "query_db",
|
|
120
|
+
description: "Accepts a raw SQL SELECT string and returns the JSON result set.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
sql: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "The raw SQL SELECT string to execute."
|
|
127
|
+
},
|
|
128
|
+
params: {
|
|
129
|
+
type: "array",
|
|
130
|
+
description: "Optional arguments to bind to the SQL query. Use this to safely substitute ? placeholders in your SQL string (e.g. ['value', 42]).",
|
|
131
|
+
items: {
|
|
132
|
+
type: ["string", "number", "boolean", "null"],
|
|
133
|
+
description: "A single bound parameter value."
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
dbName: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Optional. Target a specific database name. If omitted, uses the active DB or auto-selects."
|
|
139
|
+
},
|
|
140
|
+
platform: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Optional. Explicitly target 'ios' or 'android'. If omitted, uses the active DB or auto-selects."
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
required: ["sql"]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Helper to sanitize platform input if any
|
|
153
|
+
function cleanPlatform(raw?: string): 'ios' | 'android' | undefined {
|
|
154
|
+
if (!raw) return undefined;
|
|
155
|
+
const cleaned = raw.replace(/['"]/g, '').trim().toLowerCase();
|
|
156
|
+
if (cleaned === 'ios' || cleaned === 'android') return cleaned;
|
|
157
|
+
return undefined; // If they pass garbage, just let locator try both
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Helper to ensure database is synced based on provided args
|
|
161
|
+
async function ensureDbState(args: any): Promise<SyncedDB> {
|
|
162
|
+
const reqDbName = args?.dbName as string | undefined;
|
|
163
|
+
const reqPlatform = cleanPlatform(args?.platform as string | undefined);
|
|
164
|
+
|
|
165
|
+
// If nothing is synced, sync defaults
|
|
166
|
+
if (activeDatabases.length === 0) {
|
|
167
|
+
const envDb = reqDbName || process.env.DB_NAME;
|
|
168
|
+
const envBundle = process.env.ANDROID_BUNDLE_ID;
|
|
169
|
+
activeDatabases = await syncDatabase(envDb, envBundle, reqPlatform);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Filter based on explicit requirements
|
|
173
|
+
let candidates = activeDatabases;
|
|
174
|
+
if (reqPlatform) candidates = candidates.filter(db => db.platform === reqPlatform);
|
|
175
|
+
if (reqDbName) candidates = candidates.filter(db => db.dbName === reqDbName);
|
|
176
|
+
|
|
177
|
+
if (candidates.length === 1) {
|
|
178
|
+
return candidates[0];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (candidates.length === 0) {
|
|
182
|
+
throw new Error(`No synced databases match the criteria (platform: ${reqPlatform || 'any'}, dbName: ${reqDbName || 'any'}). Try calling sync_database first.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const matches = candidates.map(c => `[${c.platform}] ${c.dbName}`).join(", ");
|
|
186
|
+
throw new Error(`Multiple databases match the criteria. Please specify 'platform' or 'dbName'. Matches: ${matches}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
190
|
+
const { name, arguments: args } = request.params;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
if (name === "list_databases") {
|
|
194
|
+
const bundleId = args?.bundleId as string | undefined;
|
|
195
|
+
const platform = cleanPlatform(args?.platform as string | undefined);
|
|
196
|
+
const results = await listDatabases(bundleId, platform);
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (name === "sync_database") {
|
|
203
|
+
const dbName = args?.dbName as string | undefined;
|
|
204
|
+
const bundleId = args?.bundleId as string | undefined;
|
|
205
|
+
const platform = cleanPlatform(args?.platform as string | undefined);
|
|
206
|
+
|
|
207
|
+
const results = await syncDatabase(dbName, bundleId, platform);
|
|
208
|
+
|
|
209
|
+
activeDatabases = results; // Replace the active list
|
|
210
|
+
|
|
211
|
+
let msg = "Successfully synced databases:\n";
|
|
212
|
+
for (const res of results) {
|
|
213
|
+
msg += `- Platform: ${res.platform} | DB: ${res.dbName}\n Path: ${res.localPath}\n`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
content: [{ type: "text", text: msg }]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (name === "inspect_schema") {
|
|
222
|
+
const activeDb = await ensureDbState(args);
|
|
223
|
+
const schema = await inspectSchema(activeDb.localPath);
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` + JSON.stringify(schema, null, 2) }]
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (name === "read_table_contents") {
|
|
230
|
+
const activeDb = await ensureDbState(args);
|
|
231
|
+
|
|
232
|
+
const tableName = args?.tableName as string;
|
|
233
|
+
const limit = args?.limit as number || 100;
|
|
234
|
+
|
|
235
|
+
if (!tableName) {
|
|
236
|
+
throw new Error("Missing required argument: tableName");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const sql = `SELECT * FROM "${tableName}" LIMIT ?`;
|
|
240
|
+
const results = await queryDb(activeDb.localPath, sql, [limit]);
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName} | Table: ${tableName} | Limit: ${limit}]\n` + JSON.stringify(results, null, 2) }]
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (name === "query_db") {
|
|
247
|
+
const activeDb = await ensureDbState(args);
|
|
248
|
+
|
|
249
|
+
const sql = args?.sql as string;
|
|
250
|
+
const params = (args?.params as any[]) || [];
|
|
251
|
+
|
|
252
|
+
if (!sql) {
|
|
253
|
+
throw new Error("Missing required argument: sql");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const results = await queryDb(activeDb.localPath, sql, params);
|
|
257
|
+
return {
|
|
258
|
+
content: [{ type: "text", text: `[Active Platform: ${activeDb.platform} | DB: ${activeDb.dbName}]\n` + JSON.stringify(results, null, 2) }]
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
263
|
+
} catch (error: any) {
|
|
264
|
+
return {
|
|
265
|
+
content: [
|
|
266
|
+
{
|
|
267
|
+
type: "text",
|
|
268
|
+
text: `Error: ${error.message}`
|
|
269
|
+
}
|
|
270
|
+
],
|
|
271
|
+
isError: true
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
async function run() {
|
|
277
|
+
const transport = new StdioServerTransport();
|
|
278
|
+
await server.connect(transport);
|
|
279
|
+
console.error("Universal React Native SQLite MCP Server running on stdio");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
run().catch((error) => {
|
|
283
|
+
console.error("Server error:", error);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
});
|
package/src/locator.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
export interface DatabaseLocation {
|
|
7
|
+
platform: 'ios' | 'android';
|
|
8
|
+
databases: string[];
|
|
9
|
+
appDir?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function listDatabases(bundleId?: string, targetPlatform?: 'ios' | 'android'): Promise<DatabaseLocation[]> {
|
|
13
|
+
const results: DatabaseLocation[] = [];
|
|
14
|
+
|
|
15
|
+
if (!targetPlatform || targetPlatform === 'ios') {
|
|
16
|
+
try {
|
|
17
|
+
const udidStr = execSync("xcrun simctl list devices booted | awk -F '[()]' '/Booted/{print $2; exit}'", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 }).toString().trim();
|
|
18
|
+
if (udidStr) {
|
|
19
|
+
const appDataDir = `${process.env.HOME}/Library/Developer/CoreSimulator/Devices/${udidStr}/data/Containers/Data/Application`;
|
|
20
|
+
if (fs.existsSync(appDataDir)) {
|
|
21
|
+
try {
|
|
22
|
+
const findCmd = `find "${appDataDir}" -type f \\( -name "*.db" -o -name "*.sqlite" -o -name "*.sqlite3" \\) -maxdepth 7 -print`;
|
|
23
|
+
const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 10000 }).toString().trim();
|
|
24
|
+
if (found) {
|
|
25
|
+
results.push({
|
|
26
|
+
platform: 'ios',
|
|
27
|
+
appDir: appDataDir,
|
|
28
|
+
databases: found.split('\n').map(p => path.basename(p.trim())).filter(Boolean)
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error("iOS find failed", e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} else if (targetPlatform === 'ios') {
|
|
36
|
+
throw new Error("No booted iOS Simulator found (simctl returned empty).");
|
|
37
|
+
}
|
|
38
|
+
} catch (e: any) {
|
|
39
|
+
if (targetPlatform === 'ios' && !e.message?.includes("find failed")) {
|
|
40
|
+
throw new Error("No booted iOS Simulator found or xcrun failed.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!targetPlatform || targetPlatform === 'android') {
|
|
46
|
+
try {
|
|
47
|
+
execSync("adb get-state", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 });
|
|
48
|
+
} catch (e) {
|
|
49
|
+
if (targetPlatform === 'android') {
|
|
50
|
+
throw new Error("No booted Android Emulator found or adb is unresponsive.");
|
|
51
|
+
}
|
|
52
|
+
if (results.length === 0) {
|
|
53
|
+
throw new Error("No booted iOS Simulator or Android Emulator device found.");
|
|
54
|
+
}
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// if we have a specific bundleId-use it otherwise hunt
|
|
59
|
+
let packagesToScan: string[] = [];
|
|
60
|
+
if (bundleId) {
|
|
61
|
+
packagesToScan = [bundleId];
|
|
62
|
+
} else {
|
|
63
|
+
try {
|
|
64
|
+
const packagesStr = execSync("adb shell pm list packages -3", { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
|
|
65
|
+
packagesToScan = packagesStr.split('\n')
|
|
66
|
+
.map(line => line.replace('package:', '').trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (results.length === 0) {
|
|
70
|
+
throw new Error("Could not list packages on Android Emulator to discover databases. Is it fully booted?");
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const allAndroidDatabases: string[] = [];
|
|
77
|
+
let lastSuccessfulAppDir: string | undefined;
|
|
78
|
+
|
|
79
|
+
for (const pkg of packagesToScan) {
|
|
80
|
+
const baseDirs = [`/data/user/0/${pkg}`, `/data/data/${pkg}`];
|
|
81
|
+
|
|
82
|
+
for (const baseDir of baseDirs) {
|
|
83
|
+
try {
|
|
84
|
+
execSync(`adb shell run-as ${pkg} ls -d ${baseDir}`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 2000 });
|
|
85
|
+
|
|
86
|
+
let foundFiles: string[] = [];
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const findCmd = `adb shell "run-as ${pkg} find ${baseDir} -type f \\( -name \\"*.db\\" -o -name \\"*.sqlite\\" -o -name \\"*.sqlite3\\" \\)"`;
|
|
90
|
+
const findOut = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
|
|
91
|
+
if (findOut) {
|
|
92
|
+
foundFiles.push(...findOut.split('\n').map(l => l.trim().replace(/\r/g, '')).filter(Boolean));
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const lsOut = execSync(`adb shell run-as ${pkg} ls -1p ${baseDir}/databases`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 2000 }).toString().trim();
|
|
98
|
+
const lsFiles = lsOut.split('\n')
|
|
99
|
+
.map(l => l.trim().replace(/\r/g, ''))
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.filter(f => !f.endsWith('/'))
|
|
102
|
+
.filter(f => !f.endsWith('-journal') && !f.endsWith('-wal') && !f.endsWith('-shm'))
|
|
103
|
+
.filter(f => !f.includes('.'))
|
|
104
|
+
.map(f => `${baseDir}/databases/${f}`);
|
|
105
|
+
foundFiles.push(...lsFiles);
|
|
106
|
+
} catch (e) {}
|
|
107
|
+
|
|
108
|
+
// Deduplicate
|
|
109
|
+
foundFiles = [...new Set(foundFiles)];
|
|
110
|
+
|
|
111
|
+
if (foundFiles.length > 0) {
|
|
112
|
+
const displayFiles = foundFiles.map(f => f.replace(`${baseDir}/`, ''));
|
|
113
|
+
|
|
114
|
+
allAndroidDatabases.push(...displayFiles);
|
|
115
|
+
lastSuccessfulAppDir = `${baseDir}::${pkg}`;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error(`Failed to list databases for app: ${pkg}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (allAndroidDatabases.length > 0) {
|
|
125
|
+
results.push({
|
|
126
|
+
platform: 'android',
|
|
127
|
+
appDir: lastSuccessfulAppDir,
|
|
128
|
+
databases: allAndroidDatabases
|
|
129
|
+
});
|
|
130
|
+
} else if (targetPlatform === 'android') {
|
|
131
|
+
throw new Error(`Android Emulator is booted, but no SQLite databases were found in any debuggable third-party packages.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Finds the DB file based on platform and a provided/detected filename.
|
|
140
|
+
* If dbNameGlob is empty/undefined, it will auto-select the first discovered database.
|
|
141
|
+
* Prioritizes iOS (simctl), falls back to Android (adb).
|
|
142
|
+
* Returns the local file path (either the iOS original or a pulled Android copy).
|
|
143
|
+
*/
|
|
144
|
+
export async function syncDatabase(dbNameGlob?: string, bundleId?: string, targetPlatform?: 'ios' | 'android'): Promise<{ localPath: string, dbName: string, platform: 'ios' | 'android' }[]> {
|
|
145
|
+
// First, discover available databases
|
|
146
|
+
const locations = await listDatabases(bundleId, targetPlatform);
|
|
147
|
+
|
|
148
|
+
if (locations.length === 0) {
|
|
149
|
+
if (targetPlatform) {
|
|
150
|
+
throw new Error(`No SQLite databases found for platform '${targetPlatform}'.`);
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`No SQLite databases found on any platform.`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const synced: { localPath: string, dbName: string, platform: 'ios' | 'android' }[] = [];
|
|
156
|
+
|
|
157
|
+
for (const loc of locations) {
|
|
158
|
+
if (targetPlatform && loc.platform !== targetPlatform) continue;
|
|
159
|
+
|
|
160
|
+
let targetDbNames: string[] = [];
|
|
161
|
+
|
|
162
|
+
if (!dbNameGlob) {
|
|
163
|
+
// Auto-select: find the first .db or .sqlite file in this location
|
|
164
|
+
const preferred = loc.databases.find(d => d.endsWith('.db') || d.endsWith('.sqlite'));
|
|
165
|
+
if (preferred) {
|
|
166
|
+
targetDbNames.push(preferred);
|
|
167
|
+
} else if (loc.databases.length > 0) {
|
|
168
|
+
targetDbNames.push(loc.databases[0]); // fallback to first file
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
const globRegex = new RegExp('^' + dbNameGlob.replace(/\*/g, '.*') + '$');
|
|
172
|
+
targetDbNames = loc.databases.filter(name => globRegex.test(name));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const targetDbName of targetDbNames) {
|
|
176
|
+
const { platform, appDir } = loc;
|
|
177
|
+
|
|
178
|
+
// --- iOS Logic ---
|
|
179
|
+
if (platform === 'ios') {
|
|
180
|
+
if (!appDir) continue;
|
|
181
|
+
|
|
182
|
+
// Find the exact path of the targetDbName
|
|
183
|
+
const findCmd = `find "${appDir}" -type f -name "${targetDbName}" -maxdepth 7 -print | head -n 1`;
|
|
184
|
+
try {
|
|
185
|
+
const found = execSync(findCmd, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 5000 }).toString().trim();
|
|
186
|
+
if (found && fs.existsSync(found)) {
|
|
187
|
+
console.error(`Located iOS DB at: ${found}`);
|
|
188
|
+
synced.push({ localPath: found, dbName: targetDbName, platform: 'ios' });
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error(`Failed to locate full path for iOS DB: ${targetDbName}`);
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// --- Android Logic ---
|
|
197
|
+
if (!appDir || !appDir.includes("::")) {
|
|
198
|
+
console.error(`Invalid Android appDir format: ${appDir}`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const [targetDbDir, targetPkg] = appDir.split("::");
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
execSync(`adb shell am force-stop ${targetPkg}`, { stdio: ['pipe', 'pipe', 'ignore'], timeout: 3000 });
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error(`Failed to force-stop app: ${targetPkg}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "rn-sqlite-mcp-"));
|
|
211
|
+
const safeLocalName = targetDbName.replace(/\//g, '_');
|
|
212
|
+
const localDb = path.join(tmpdir, safeLocalName);
|
|
213
|
+
const localWal = `${localDb}-wal`;
|
|
214
|
+
const localShm = `${localDb}-shm`;
|
|
215
|
+
|
|
216
|
+
const remoteMain = `${targetDbDir}/${targetDbName}`;
|
|
217
|
+
const remoteWal = `${remoteMain}-wal`;
|
|
218
|
+
const remoteShm = `${remoteMain}-shm`;
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
const pullOne = (remote: string, local: string) => {
|
|
222
|
+
try {
|
|
223
|
+
const remoteBase = path.basename(remote);
|
|
224
|
+
const tmpRemote = `/data/local/tmp/${targetPkg}_${remoteBase}_${Date.now()}`;
|
|
225
|
+
|
|
226
|
+
execSync(`adb shell "run-as '${targetPkg}' cat '${remote}' > '${tmpRemote}'"`, { stdio: 'ignore', timeout: 5000 });
|
|
227
|
+
execSync(`adb pull '${tmpRemote}' '${local}'`, { stdio: 'ignore', timeout: 5000 });
|
|
228
|
+
execSync(`adb shell rm '${tmpRemote}'`, { stdio: 'ignore', timeout: 3000 });
|
|
229
|
+
|
|
230
|
+
return fs.existsSync(local) && fs.statSync(local).size > 0;
|
|
231
|
+
} catch (e) {
|
|
232
|
+
if (fs.existsSync(local)) fs.unlinkSync(local);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if (!pullOne(remoteMain, localDb)) {
|
|
238
|
+
console.error(`Failed to pull main DB file from Android: ${remoteMain}`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
pullOne(remoteWal, localWal);
|
|
243
|
+
pullOne(remoteShm, localShm);
|
|
244
|
+
|
|
245
|
+
console.error(`Pulled Android DB to local temp: ${localDb}`);
|
|
246
|
+
synced.push({ localPath: localDb, dbName: targetDbName, platform: 'android' });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (synced.length === 0) {
|
|
251
|
+
throw new Error(`Failed to sync any databases matching '${dbNameGlob || 'auto-select'}'.`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return synced;
|
|
255
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|