@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "2.45.4",
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.';
@@ -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
- // fast-sql applies optimal PRAGMAs automatically
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
- // Apply path-based scope filter in JS
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
- return { total, lastIndexedAt: last };
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 {