@akiojin/unity-mcp-server 2.40.3 → 2.40.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "2.40.3",
3
+ "version": "2.40.5",
4
4
  "description": "MCP server and Unity Editor bridge — enables AI assistants to control Unity for AI-assisted workflows",
5
5
  "type": "module",
6
6
  "main": "src/core/server.js",
@@ -27,7 +27,7 @@
27
27
  "test:verbose": "VERBOSE_TEST=true node --test tests/**/*.test.js",
28
28
  "prepare": "cd .. && husky || true",
29
29
  "prepublishOnly": "npm run test:ci",
30
- "postinstall": "chmod +x bin/unity-mcp-server || true",
30
+ "postinstall": "node scripts/ensure-better-sqlite3.mjs && chmod +x bin/unity-mcp-server || true",
31
31
  "test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.test.js tests/unit/core/codeIndexDb.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js || exit 0",
32
32
  "test:unity": "node tests/run-unity-integration.mjs",
33
33
  "test:nounity": "npm run test:integration",
@@ -50,7 +50,8 @@
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/sdk": "^0.6.1",
52
52
  "better-sqlite3": "^9.4.3",
53
- "find-up": "^6.3.0"
53
+ "find-up": "^6.3.0",
54
+ "sql.js": "^1.13.0"
54
55
  },
55
56
  "engines": {
56
57
  "node": ">=18 <23"
@@ -1,6 +1,15 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { ProjectInfoProvider } from './projectInfo.js';
4
+ import { logger } from './config.js';
5
+ import { createSqliteFallback } from './sqliteFallback.js';
6
+
7
+ // Shared driver availability state across CodeIndex instances
8
+ const driverStatus = {
9
+ available: null,
10
+ error: null,
11
+ logged: false
12
+ };
4
13
 
5
14
  export class CodeIndex {
6
15
  constructor(unityConnection) {
@@ -9,21 +18,42 @@ export class CodeIndex {
9
18
  this.db = null;
10
19
  this.dbPath = null;
11
20
  this.disabled = false; // set true if better-sqlite3 is unavailable
21
+ this.disableReason = null;
12
22
  this._Database = null;
23
+ this._openFallback = null;
24
+ this._persistFallback = null;
13
25
  }
14
26
 
15
27
  async _ensureDriver() {
16
- if (this.disabled) return false;
28
+ if (driverStatus.available === false || this.disabled) {
29
+ this.disabled = true;
30
+ this.disableReason = this.disableReason || driverStatus.error;
31
+ return false;
32
+ }
17
33
  if (this._Database) return true;
18
34
  try {
19
35
  // Dynamic import to avoid hard failure when native binding is missing
20
36
  const mod = await import('better-sqlite3');
21
37
  this._Database = mod.default || mod;
38
+ driverStatus.available = true;
39
+ driverStatus.error = null;
22
40
  return true;
23
41
  } catch (e) {
24
- // Mark as disabled and operate in fallback (index unavailable)
25
- this.disabled = true;
26
- return false;
42
+ // Try wasm fallback (sql.js) before giving up
43
+ try {
44
+ this._openFallback = createSqliteFallback;
45
+ driverStatus.available = true;
46
+ driverStatus.error = null;
47
+ logger?.info?.('[index] falling back to sql.js (WASM) for code index');
48
+ return true;
49
+ } catch (fallbackError) {
50
+ this.disabled = true;
51
+ this.disableReason = `better-sqlite3 unavailable: ${e?.message || e}. Fallback failed: ${fallbackError?.message || fallbackError}`;
52
+ driverStatus.available = false;
53
+ driverStatus.error = this.disableReason;
54
+ this._logDisable(this.disableReason);
55
+ return false;
56
+ }
27
57
  }
28
58
  }
29
59
 
@@ -36,11 +66,35 @@ export class CodeIndex {
36
66
  fs.mkdirSync(dir, { recursive: true });
37
67
  const dbPath = path.join(dir, 'code-index.db');
38
68
  this.dbPath = dbPath;
39
- this.db = new this._Database(dbPath);
69
+ try {
70
+ if (this._Database) {
71
+ this.db = new this._Database(dbPath);
72
+ } else if (this._openFallback) {
73
+ this.db = await this._openFallback(dbPath);
74
+ this._persistFallback = this.db.persist;
75
+ } else {
76
+ throw new Error('No database driver available');
77
+ }
78
+ } catch (e) {
79
+ this.disabled = true;
80
+ this.disableReason = e?.message || 'Failed to open code index database';
81
+ driverStatus.available = false;
82
+ driverStatus.error = this.disableReason;
83
+ this._logDisable(this.disableReason);
84
+ return null;
85
+ }
40
86
  this._initSchema();
41
87
  return this.db;
42
88
  }
43
89
 
90
+ _logDisable(reason) {
91
+ if (driverStatus.logged) return;
92
+ driverStatus.logged = true;
93
+ try {
94
+ logger?.warn?.(`[index] code index disabled: ${reason}`);
95
+ } catch {}
96
+ }
97
+
44
98
  _initSchema() {
45
99
  if (!this.db) return;
46
100
  const db = this.db;
@@ -68,6 +122,7 @@ export class CodeIndex {
68
122
  CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
69
123
  CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path);
70
124
  `);
125
+ if (this._persistFallback) this._persistFallback();
71
126
  }
72
127
 
73
128
  async isReady() {
@@ -103,6 +158,7 @@ export class CodeIndex {
103
158
  );
104
159
  });
105
160
  tx(symbols || []);
161
+ await this._flushFallback();
106
162
  return { total: symbols?.length || 0 };
107
163
  }
108
164
 
@@ -124,6 +180,7 @@ export class CodeIndex {
124
180
  sig || '',
125
181
  new Date().toISOString()
126
182
  );
183
+ await this._flushFallback();
127
184
  }
128
185
 
129
186
  async removeFile(pathStr) {
@@ -134,6 +191,7 @@ export class CodeIndex {
134
191
  db.prepare('DELETE FROM files WHERE path = ?').run(p);
135
192
  });
136
193
  tx(pathStr);
194
+ await this._flushFallback();
137
195
  }
138
196
 
139
197
  async replaceSymbolsForPath(pathStr, rows) {
@@ -160,6 +218,7 @@ export class CodeIndex {
160
218
  );
161
219
  });
162
220
  tx(pathStr, rows || []);
221
+ await this._flushFallback();
163
222
  }
164
223
 
165
224
  async querySymbols({ name, kind, scope = 'all', exact = false }) {
@@ -209,4 +268,19 @@ export class CodeIndex {
209
268
  db.prepare("SELECT value AS v FROM meta WHERE key = 'lastIndexedAt'").get()?.v || null;
210
269
  return { total, lastIndexedAt: last };
211
270
  }
271
+
272
+ async _flushFallback() {
273
+ if (typeof this._persistFallback === 'function') {
274
+ try {
275
+ await this._persistFallback();
276
+ } catch {}
277
+ }
278
+ }
279
+ }
280
+
281
+ // Test-only helper to reset cached driver status between runs
282
+ export function __resetCodeIndexDriverStatusForTest() {
283
+ driverStatus.available = null;
284
+ driverStatus.error = null;
285
+ driverStatus.logged = false;
212
286
  }
@@ -34,6 +34,17 @@ export class IndexWatcher {
34
34
  if (this.running) return;
35
35
  this.running = true;
36
36
  try {
37
+ // Skip watcher entirely when the native SQLite binding is unavailable
38
+ const { CodeIndex } = await import('./codeIndex.js');
39
+ const probe = new CodeIndex(this.unityConnection);
40
+ const driverOk = await probe._ensureDriver();
41
+ if (!driverOk || probe.disabled) {
42
+ const reason = probe.disableReason || 'SQLite native binding not available';
43
+ logger.warn(`[index] watcher: code index disabled (${reason}); stopping watcher`);
44
+ this.stop();
45
+ return;
46
+ }
47
+
37
48
  // Check if code index DB file exists (before opening DB)
38
49
  const { ProjectInfoProvider } = await import('./projectInfo.js');
39
50
  const projectInfo = new ProjectInfoProvider(this.unityConnection);
@@ -219,6 +219,12 @@ export async function startServer() {
219
219
  const ready = await index.isReady();
220
220
 
221
221
  if (!ready) {
222
+ if (index.disabled) {
223
+ logger.warn(
224
+ `[startup] Code index disabled: ${index.disableReason || 'SQLite native binding missing'}. Skipping auto-build.`
225
+ );
226
+ return;
227
+ }
222
228
  logger.info('[startup] Code index DB not ready. Starting auto-build...');
223
229
  const { CodeIndexBuildToolHandler } = await import(
224
230
  '../handlers/script/CodeIndexBuildToolHandler.js'
@@ -0,0 +1,75 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import initSqlJs from 'sql.js';
4
+
5
+ // Create a lightweight better-sqlite3 compatible surface using sql.js (WASM)
6
+ export async function createSqliteFallback(dbPath) {
7
+ const wasmPath = path.resolve('node_modules/sql.js/dist/sql-wasm.wasm');
8
+ const SQL = await initSqlJs({ locateFile: () => wasmPath });
9
+
10
+ const loadDb = () => {
11
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
12
+ if (fs.existsSync(dbPath)) {
13
+ const data = fs.readFileSync(dbPath);
14
+ return new SQL.Database(new Uint8Array(data));
15
+ }
16
+ return new SQL.Database();
17
+ };
18
+
19
+ const db = loadDb();
20
+
21
+ const persist = () => {
22
+ const data = db.export();
23
+ fs.writeFileSync(dbPath, Buffer.from(data));
24
+ };
25
+
26
+ // Wrap sql.js Statement to look like better-sqlite3's
27
+ const wrapStatement = stmt => ({
28
+ run(...params) {
29
+ stmt.bind(params);
30
+ // sql.js run via stepping through the statement
31
+ while (stmt.step()) {
32
+ /* consume rows for statements that return data */
33
+ }
34
+ stmt.reset();
35
+ persist();
36
+ return this;
37
+ },
38
+ get(...params) {
39
+ stmt.bind(params);
40
+ const has = stmt.step();
41
+ const row = has ? stmt.getAsObject() : undefined;
42
+ stmt.reset();
43
+ return row;
44
+ },
45
+ all(...params) {
46
+ stmt.bind(params);
47
+ const rows = [];
48
+ while (stmt.step()) rows.push(stmt.getAsObject());
49
+ stmt.reset();
50
+ return rows;
51
+ }
52
+ });
53
+
54
+ const prepare = sql => wrapStatement(db.prepare(sql));
55
+
56
+ // Mimic better-sqlite3 transaction(fn)
57
+ const transaction =
58
+ fn =>
59
+ (...args) => {
60
+ const result = fn(...args);
61
+ persist();
62
+ return result;
63
+ };
64
+
65
+ // Minimal surface used by CodeIndex
66
+ return {
67
+ exec: sql => {
68
+ db.exec(sql);
69
+ persist();
70
+ },
71
+ prepare,
72
+ transaction,
73
+ persist
74
+ };
75
+ }
@@ -77,6 +77,13 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
77
77
  */
78
78
  async _executeBuild(params, job) {
79
79
  try {
80
+ // Fail fast when native SQLite binding is unavailable
81
+ const db = await this.index.open();
82
+ if (!db) {
83
+ const reason = this.index.disableReason || 'Code index unavailable (SQLite driver missing)';
84
+ throw new Error(reason);
85
+ }
86
+
80
87
  const throttleMs = Math.max(0, Number(params?.throttleMs ?? 0));
81
88
  const delayStartMs = Math.max(0, Number(params?.delayStartMs ?? 0));
82
89
  const info = await this.projectInfo.get();