@akiojin/unity-mcp-server 2.40.3 → 2.40.4
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
|
+
"version": "2.40.4",
|
|
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"
|
package/src/core/codeIndex.js
CHANGED
|
@@ -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)
|
|
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
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/core/indexWatcher.js
CHANGED
|
@@ -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);
|
package/src/core/server.js
CHANGED
|
@@ -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();
|