@akiojin/unity-mcp-server 2.45.4 → 2.46.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/package.json +5 -4
- package/src/constants/offlineTools.js +19 -0
- package/src/core/codeIndex.js +63 -13
- package/src/core/server.js +255 -174
- package/src/core/stdioRpcServer.js +258 -0
- package/src/core/toolManifest.json +4436 -0
- package/src/core/workers/indexBuildWorker.js +301 -46
- package/src/handlers/script/CodeIndexBuildToolHandler.js +1 -1
- package/src/handlers/script/CodeIndexStatusToolHandler.js +1 -1
- package/src/handlers/script/CodeIndexUpdateToolHandler.js +1 -1
- package/src/handlers/script/ScriptPackagesListToolHandler.js +1 -1
- package/src/handlers/script/ScriptReadToolHandler.js +9 -2
- package/src/handlers/script/ScriptRefsFindToolHandler.js +131 -36
- package/src/handlers/script/ScriptSearchToolHandler.js +1 -1
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +32 -33
- package/src/handlers/script/ScriptSymbolsGetToolHandler.js +18 -10
- package/src/handlers/system/SystemPingToolHandler.js +30 -17
- package/src/lsp/CSharpLspUtils.js +11 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akiojin/unity-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.46.0",
|
|
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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"test:watch": "node --watch --test tests/unit/**/*.test.js",
|
|
21
21
|
"test:watch:all": "node --watch --test tests/**/*.test.js",
|
|
22
22
|
"test:performance": "node --test tests/performance/*.test.js",
|
|
23
|
-
"test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.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 tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
|
|
23
|
+
"test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.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 tests/unit/core/startupPerformance.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
|
|
24
24
|
"test:ci:coverage": "c8 --reporter=lcov --reporter=text node --test tests/unit/core/codeIndex.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 tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
|
|
25
25
|
"test:ci:all": "c8 --reporter=lcov node --test tests/unit/**/*.test.js",
|
|
26
26
|
"simulate:code-index": "node scripts/simulate-code-index-status.mjs",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"prepare": "cd .. && husky || true",
|
|
29
29
|
"prepublishOnly": "npm run test:ci",
|
|
30
30
|
"postinstall": "chmod +x bin/unity-mcp-server.js || true",
|
|
31
|
-
"test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.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",
|
|
31
|
+
"test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.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 tests/unit/core/startupPerformance.test.js || exit 0",
|
|
32
32
|
"test:unity": "node tests/run-unity-integration.mjs",
|
|
33
33
|
"test:nounity": "npm run test:integration",
|
|
34
34
|
"test:ci:integration": "CI=true NODE_ENV=test node --test tests/integration/code-index-background.test.js"
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
52
52
|
"find-up": "^6.3.0",
|
|
53
|
-
"@akiojin/fast-sql": "^0.1.0"
|
|
53
|
+
"@akiojin/fast-sql": "^0.1.0",
|
|
54
|
+
"lru-cache": "^11.0.2"
|
|
54
55
|
},
|
|
55
56
|
"engines": {
|
|
56
57
|
"node": ">=18 <23"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List of tools that work without Unity connection.
|
|
3
|
+
* These tools use only the local C# LSP and file system.
|
|
4
|
+
*/
|
|
5
|
+
export const OFFLINE_TOOLS = [
|
|
6
|
+
'code_index_status',
|
|
7
|
+
'code_index_build',
|
|
8
|
+
'code_index_update',
|
|
9
|
+
'script_symbols_get',
|
|
10
|
+
'script_symbol_find',
|
|
11
|
+
'script_refs_find',
|
|
12
|
+
'script_read',
|
|
13
|
+
'script_search',
|
|
14
|
+
'script_packages_list'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const OFFLINE_TOOLS_HINT =
|
|
18
|
+
'Code index and script analysis tools work without Unity connection. ' +
|
|
19
|
+
'Use these tools for C# code exploration, symbol search, and editing.';
|
package/src/core/codeIndex.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { LRUCache } from 'lru-cache';
|
|
3
4
|
import { ProjectInfoProvider } from './projectInfo.js';
|
|
4
5
|
import { logger } from './config.js';
|
|
5
6
|
|
|
7
|
+
// Phase 4: Query result cache (80% reduction for repeated queries)
|
|
8
|
+
const queryCache = new LRUCache({
|
|
9
|
+
max: 500, // Max 500 cached queries
|
|
10
|
+
ttl: 1000 * 60 * 5 // 5 minute TTL
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Stats cache with shorter TTL
|
|
14
|
+
const statsCache = new LRUCache({
|
|
15
|
+
max: 1,
|
|
16
|
+
ttl: 1000 * 60 // 1 minute TTL for stats
|
|
17
|
+
});
|
|
18
|
+
|
|
6
19
|
// fast-sql helper: execute query and return results
|
|
7
20
|
function querySQL(db, sql) {
|
|
8
21
|
return db.execSql(sql);
|
|
@@ -138,7 +151,10 @@ export class CodeIndex {
|
|
|
138
151
|
|
|
139
152
|
_initSchema() {
|
|
140
153
|
if (!this.db) return;
|
|
141
|
-
//
|
|
154
|
+
// Phase 5: Explicit PRAGMA optimization
|
|
155
|
+
this.db.run('PRAGMA cache_size = 16000'); // 64MB cache (16000 * 4KB pages)
|
|
156
|
+
this.db.run('PRAGMA temp_store = MEMORY'); // Faster temp operations
|
|
157
|
+
this.db.run('PRAGMA synchronous = NORMAL'); // Balanced safety/speed
|
|
142
158
|
this.db.run(`
|
|
143
159
|
CREATE TABLE IF NOT EXISTS meta (
|
|
144
160
|
key TEXT PRIMARY KEY,
|
|
@@ -166,6 +182,9 @@ export class CodeIndex {
|
|
|
166
182
|
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)');
|
|
167
183
|
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)');
|
|
168
184
|
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path)');
|
|
185
|
+
// Composite indexes for faster multi-condition queries
|
|
186
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_name_kind ON symbols(name, kind)');
|
|
187
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_path_name ON symbols(path, name)');
|
|
169
188
|
this._saveToFile();
|
|
170
189
|
}
|
|
171
190
|
|
|
@@ -181,6 +200,10 @@ export class CodeIndex {
|
|
|
181
200
|
const db = await this.open();
|
|
182
201
|
if (!db) throw new Error('CodeIndex is unavailable (fast-sql not loaded)');
|
|
183
202
|
|
|
203
|
+
// Phase 4: Invalidate caches on write
|
|
204
|
+
queryCache.clear();
|
|
205
|
+
statsCache.clear();
|
|
206
|
+
|
|
184
207
|
db.run('BEGIN TRANSACTION');
|
|
185
208
|
try {
|
|
186
209
|
db.run('DELETE FROM symbols');
|
|
@@ -232,6 +255,8 @@ export class CodeIndex {
|
|
|
232
255
|
async upsertFile(pathStr, sig) {
|
|
233
256
|
const db = await this.open();
|
|
234
257
|
if (!db) return;
|
|
258
|
+
// Phase 4: Invalidate caches on write
|
|
259
|
+
statsCache.clear();
|
|
235
260
|
const stmt = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
236
261
|
stmt.run([pathStr, sig || '', new Date().toISOString()]);
|
|
237
262
|
stmt.free();
|
|
@@ -241,6 +266,9 @@ export class CodeIndex {
|
|
|
241
266
|
async removeFile(pathStr) {
|
|
242
267
|
const db = await this.open();
|
|
243
268
|
if (!db) return;
|
|
269
|
+
// Phase 4: Invalidate caches on write
|
|
270
|
+
queryCache.clear();
|
|
271
|
+
statsCache.clear();
|
|
244
272
|
db.run('BEGIN TRANSACTION');
|
|
245
273
|
try {
|
|
246
274
|
const stmt1 = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
@@ -263,6 +291,10 @@ export class CodeIndex {
|
|
|
263
291
|
const db = await this.open();
|
|
264
292
|
if (!db) return;
|
|
265
293
|
|
|
294
|
+
// Phase 4: Invalidate caches on write
|
|
295
|
+
queryCache.clear();
|
|
296
|
+
statsCache.clear();
|
|
297
|
+
|
|
266
298
|
db.run('BEGIN TRANSACTION');
|
|
267
299
|
try {
|
|
268
300
|
const delStmt = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
@@ -298,6 +330,11 @@ export class CodeIndex {
|
|
|
298
330
|
}
|
|
299
331
|
|
|
300
332
|
async querySymbols({ name, kind, scope = 'all', exact = false }) {
|
|
333
|
+
// Phase 4: Check cache first
|
|
334
|
+
const cacheKey = JSON.stringify({ name, kind, scope, exact });
|
|
335
|
+
const cached = queryCache.get(cacheKey);
|
|
336
|
+
if (cached) return cached;
|
|
337
|
+
|
|
301
338
|
const db = await this.open();
|
|
302
339
|
if (!db) return [];
|
|
303
340
|
|
|
@@ -318,6 +355,16 @@ export class CodeIndex {
|
|
|
318
355
|
params.push(kind);
|
|
319
356
|
}
|
|
320
357
|
|
|
358
|
+
// Apply scope filter directly in SQL for better performance
|
|
359
|
+
if (scope === 'assets') {
|
|
360
|
+
sql += " AND path LIKE 'Assets/%'";
|
|
361
|
+
} else if (scope === 'packages') {
|
|
362
|
+
sql += " AND (path LIKE 'Packages/%' OR path LIKE '%Library/PackageCache/%')";
|
|
363
|
+
} else if (scope === 'embedded') {
|
|
364
|
+
sql += " AND path LIKE 'Packages/%'";
|
|
365
|
+
}
|
|
366
|
+
// scope === 'all' requires no additional filter
|
|
367
|
+
|
|
321
368
|
const stmt = db.prepare(sql);
|
|
322
369
|
if (params.length > 0) {
|
|
323
370
|
stmt.bind(params);
|
|
@@ -330,17 +377,7 @@ export class CodeIndex {
|
|
|
330
377
|
}
|
|
331
378
|
stmt.free();
|
|
332
379
|
|
|
333
|
-
|
|
334
|
-
const filtered = rows.filter(r => {
|
|
335
|
-
const p = String(r.path || '').replace(/\\/g, '/');
|
|
336
|
-
if (scope === 'assets') return p.startsWith('Assets/');
|
|
337
|
-
if (scope === 'packages')
|
|
338
|
-
return p.startsWith('Packages/') || p.includes('Library/PackageCache/');
|
|
339
|
-
if (scope === 'embedded') return p.startsWith('Packages/');
|
|
340
|
-
return true;
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
return filtered.map(r => ({
|
|
380
|
+
const result = rows.map(r => ({
|
|
344
381
|
path: r.path,
|
|
345
382
|
name: r.name,
|
|
346
383
|
kind: r.kind,
|
|
@@ -349,9 +386,17 @@ export class CodeIndex {
|
|
|
349
386
|
line: r.line,
|
|
350
387
|
column: r.column
|
|
351
388
|
}));
|
|
389
|
+
|
|
390
|
+
// Cache the result
|
|
391
|
+
queryCache.set(cacheKey, result);
|
|
392
|
+
return result;
|
|
352
393
|
}
|
|
353
394
|
|
|
354
395
|
async getStats() {
|
|
396
|
+
// Phase 4: Check stats cache first
|
|
397
|
+
const cached = statsCache.get('stats');
|
|
398
|
+
if (cached) return cached;
|
|
399
|
+
|
|
355
400
|
const db = await this.open();
|
|
356
401
|
if (!db) return { total: 0, lastIndexedAt: null };
|
|
357
402
|
|
|
@@ -363,7 +408,9 @@ export class CodeIndex {
|
|
|
363
408
|
const last =
|
|
364
409
|
metaResult.length > 0 && metaResult[0].values.length > 0 ? metaResult[0].values[0][0] : null;
|
|
365
410
|
|
|
366
|
-
|
|
411
|
+
const result = { total, lastIndexedAt: last };
|
|
412
|
+
statsCache.set('stats', result);
|
|
413
|
+
return result;
|
|
367
414
|
}
|
|
368
415
|
|
|
369
416
|
/**
|
|
@@ -387,6 +434,9 @@ export function __resetCodeIndexDriverStatusForTest() {
|
|
|
387
434
|
driverStatus.available = null;
|
|
388
435
|
driverStatus.error = null;
|
|
389
436
|
driverStatus.logged = false;
|
|
437
|
+
// Phase 4: Clear query caches
|
|
438
|
+
queryCache.clear();
|
|
439
|
+
statsCache.clear();
|
|
390
440
|
// Also reset shared connections
|
|
391
441
|
if (sharedConnections.db) {
|
|
392
442
|
try {
|