@akiojin/unity-mcp-server 5.2.1 → 5.3.1

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.
Files changed (28) hide show
  1. package/README.md +1 -0
  2. package/package.json +28 -40
  3. package/src/core/codeIndex.js +54 -7
  4. package/src/core/config.js +15 -1
  5. package/src/core/httpServer.js +30 -6
  6. package/src/core/indexBuildWorkerPool.js +57 -3
  7. package/src/core/indexWatcher.js +10 -4
  8. package/src/core/projectInfo.js +34 -12
  9. package/src/core/server.js +58 -27
  10. package/src/core/toolManifest.json +145 -629
  11. package/src/handlers/addressables/AddressablesAnalyzeToolHandler.js +14 -6
  12. package/src/handlers/addressables/AddressablesBuildToolHandler.js +6 -3
  13. package/src/handlers/addressables/AddressablesManageToolHandler.js +6 -3
  14. package/src/handlers/input/InputSystemControlToolHandler.js +1 -1
  15. package/src/handlers/input/InputTouchToolHandler.js +7 -3
  16. package/src/handlers/package/PackageManagerToolHandler.js +6 -3
  17. package/src/handlers/script/CodeIndexStatusToolHandler.js +37 -1
  18. package/src/handlers/script/CodeIndexUpdateToolHandler.js +1 -1
  19. package/src/handlers/script/ScriptEditSnippetToolHandler.js +1 -2
  20. package/src/handlers/script/ScriptEditStructuredToolHandler.js +6 -1
  21. package/src/handlers/script/ScriptRefactorRenameToolHandler.js +3 -1
  22. package/src/handlers/script/ScriptRefsFindToolHandler.js +22 -4
  23. package/src/handlers/script/ScriptRemoveSymbolToolHandler.js +6 -1
  24. package/src/handlers/script/ScriptSymbolsGetToolHandler.js +1 -1
  25. package/src/lsp/CSharpLspUtils.js +11 -3
  26. package/src/lsp/LspProcessManager.js +3 -2
  27. package/src/lsp/LspRpcClient.js +115 -23
  28. package/src/lsp/LspRpcClientSingleton.js +79 -2
package/README.md CHANGED
@@ -63,6 +63,7 @@ Use the `search_tools` meta-tool to find the right tool quickly.
63
63
  ```
64
64
 
65
65
  More details:
66
+
66
67
  - Tools & Code Index workflow: [`docs/tools.md`](https://github.com/akiojin/unity-mcp-server/blob/main/docs/tools.md)
67
68
  - Configuration reference: [`docs/configuration.md`](https://github.com/akiojin/unity-mcp-server/blob/main/docs/configuration.md)
68
69
 
package/package.json CHANGED
@@ -1,38 +1,12 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "5.2.1",
3
+ "version": "5.3.1",
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",
7
7
  "bin": {
8
8
  "unity-mcp-server": "./bin/unity-mcp-server.js"
9
9
  },
10
- "scripts": {
11
- "start": "node src/core/server.js",
12
- "dev": "node --watch src/core/server.js",
13
- "build:index": "node src/tools/buildCodeIndex.js",
14
- "test": "node --test tests/unit/**/*.test.js tests/integration/*.test.js",
15
- "test:unit": "NODE_ENV=test node --test tests/unit/**/*.test.js",
16
- "test:integration": "NODE_ENV=test node scripts/run-non-unity-tests.mjs",
17
- "test:e2e": "NODE_ENV=test node --test tests/e2e/*.test.js",
18
- "test:coverage": "c8 --reporter=lcov --reporter=text --reporter=html node --test tests/unit/**/*.test.js tests/integration/*.test.js",
19
- "test:coverage:full": "c8 --reporter=lcov --reporter=text --reporter=html node --test tests/**/*.test.js",
20
- "test:watch": "node --watch --test tests/unit/**/*.test.js",
21
- "test:watch:all": "node --watch --test tests/**/*.test.js",
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/core/startupPerformance.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
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
- "test:ci:all": "c8 --reporter=lcov node --test tests/unit/**/*.test.js",
26
- "simulate:code-index": "node scripts/simulate-code-index-status.mjs",
27
- "test:verbose": "VERBOSE_TEST=true node --test tests/**/*.test.js",
28
- "prepare": "cd .. && husky || true",
29
- "prepublishOnly": "npm run test:ci",
30
- "postinstall": "node scripts/ensure-better-sqlite3.mjs",
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
- "test:unity": "node tests/run-unity-integration.mjs",
33
- "test:nounity": "npm run test:integration",
34
- "test:ci:integration": "CI=true NODE_ENV=test node --test tests/integration/code-index-background.test.js"
35
- },
36
10
  "keywords": [
37
11
  "mcp",
38
12
  "unity",
@@ -48,7 +22,7 @@
48
22
  "author": "Akio Jinsenji <akio-jinsenji@cloud-creative-studios.com>",
49
23
  "license": "MIT",
50
24
  "dependencies": {
51
- "@modelcontextprotocol/sdk": "^1.24.3",
25
+ "@modelcontextprotocol/sdk": "^1.25.2",
52
26
  "find-up": "^6.3.0",
53
27
  "@akiojin/fast-sql": "^0.1.0",
54
28
  "lru-cache": "^11.0.2"
@@ -78,17 +52,31 @@
78
52
  "access": "public"
79
53
  },
80
54
  "devDependencies": {
81
- "@commitlint/cli": "^18.6.1",
82
- "@commitlint/config-conventional": "^18.6.3",
83
55
  "c8": "^10.1.3",
84
- "eslint": "^8.57.1",
85
- "eslint-config-standard": "^17.1.0",
86
- "eslint-plugin-import": "^2.31.0",
87
- "eslint-plugin-n": "^16.6.2",
88
- "eslint-plugin-promise": "^6.6.0",
89
- "husky": "^9.1.7",
90
- "markdownlint-cli": "^0.43.0",
91
- "nodemon": "^3.1.7",
92
- "prettier": "^3.4.2"
56
+ "nodemon": "^3.1.7"
57
+ },
58
+ "scripts": {
59
+ "start": "node src/core/server.js",
60
+ "dev": "node --watch src/core/server.js",
61
+ "build:index": "node src/tools/buildCodeIndex.js",
62
+ "test": "node --test tests/unit/**/*.test.js tests/integration/*.test.js",
63
+ "test:unit": "NODE_ENV=test node --test tests/unit/**/*.test.js",
64
+ "test:integration": "NODE_ENV=test node scripts/run-non-unity-tests.mjs",
65
+ "test:e2e": "NODE_ENV=test node --test tests/e2e/*.test.js",
66
+ "test:coverage": "c8 --reporter=lcov --reporter=text --reporter=html node --test tests/unit/**/*.test.js tests/integration/*.test.js",
67
+ "test:coverage:full": "c8 --reporter=lcov --reporter=text --reporter=html node --test tests/**/*.test.js",
68
+ "test:watch": "node --watch --test tests/unit/**/*.test.js",
69
+ "test:watch:all": "node --watch --test tests/**/*.test.js",
70
+ "test:performance": "node --test tests/performance/*.test.js",
71
+ "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 tests/unit/handlers/input/InputTouchToolHandler.test.js tests/unit/handlers/addressables/AddressablesAnalyzeToolHandler.test.js tests/unit/handlers/addressables/AddressablesBuildToolHandler.test.js tests/unit/handlers/addressables/AddressablesManageToolHandler.test.js",
72
+ "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",
73
+ "test:ci:all": "c8 --reporter=lcov node --test tests/unit/**/*.test.js",
74
+ "simulate:code-index": "node scripts/simulate-code-index-status.mjs",
75
+ "test:verbose": "VERBOSE_TEST=true node --test tests/**/*.test.js",
76
+ "postinstall": "node scripts/ensure-better-sqlite3.mjs",
77
+ "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",
78
+ "test:unity": "node tests/run-unity-integration.mjs",
79
+ "test:nounity": "npm run test:integration",
80
+ "test:ci:integration": "CI=true NODE_ENV=test node --test tests/integration/code-index-background.test.js"
93
81
  }
94
- }
82
+ }
@@ -32,6 +32,7 @@ const driverStatus = {
32
32
  const sharedConnections = {
33
33
  db: null,
34
34
  dbPath: null,
35
+ dbStat: null,
35
36
  schemaInitialized: false,
36
37
  SQL: null // sql.js factory
37
38
  };
@@ -73,8 +74,25 @@ export class CodeIndex {
73
74
  }
74
75
  }
75
76
 
77
+ invalidateSharedConnection(reason) {
78
+ if (reason) logger.info(`[index] ${reason}`);
79
+ try {
80
+ if (sharedConnections.db) {
81
+ sharedConnections.db.close();
82
+ }
83
+ } catch {
84
+ // Ignore close errors
85
+ }
86
+ sharedConnections.db = null;
87
+ sharedConnections.dbPath = null;
88
+ sharedConnections.dbStat = null;
89
+ sharedConnections.schemaInitialized = false;
90
+ queryCache.clear();
91
+ statsCache.clear();
92
+ this.db = null;
93
+ }
94
+
76
95
  async open() {
77
- if (this.db) return this.db;
78
96
  const ok = await this._ensureDriver();
79
97
  if (!ok) return null;
80
98
  const info = await this.projectInfo.get();
@@ -83,15 +101,32 @@ export class CodeIndex {
83
101
  const dbPath = path.join(dir, 'code-index.db');
84
102
  this.dbPath = dbPath;
85
103
 
104
+ const fileStat = (() => {
105
+ try {
106
+ if (!fs.existsSync(dbPath)) return null;
107
+ const stat = fs.statSync(dbPath);
108
+ return { mtimeMs: stat.mtimeMs, size: stat.size };
109
+ } catch {
110
+ return null;
111
+ }
112
+ })();
113
+
114
+ if (this.db && (sharedConnections.db !== this.db || sharedConnections.dbPath !== dbPath)) {
115
+ this.db = null;
116
+ }
117
+
86
118
  // Use shared connection for all CodeIndex instances
87
119
  if (sharedConnections.db && sharedConnections.dbPath === dbPath) {
88
120
  // Verify the DB file still exists before returning cached connection
89
- if (!fs.existsSync(dbPath)) {
121
+ if (!fileStat) {
90
122
  // File was deleted or never created, invalidate cache
91
- logger.info('[index] DB file missing, invalidating cached connection');
92
- sharedConnections.db = null;
93
- sharedConnections.dbPath = null;
94
- sharedConnections.schemaInitialized = false;
123
+ this.invalidateSharedConnection('DB file missing, invalidating cached connection');
124
+ } else if (
125
+ sharedConnections.dbStat &&
126
+ (fileStat.mtimeMs !== sharedConnections.dbStat.mtimeMs ||
127
+ fileStat.size !== sharedConnections.dbStat.size)
128
+ ) {
129
+ this.invalidateSharedConnection('DB file changed on disk, reloading connection');
95
130
  } else {
96
131
  this.db = sharedConnections.db;
97
132
  return this.db;
@@ -108,6 +143,7 @@ export class CodeIndex {
108
143
  }
109
144
  sharedConnections.db = this.db;
110
145
  sharedConnections.dbPath = dbPath;
146
+ sharedConnections.dbStat = fileStat;
111
147
  } catch (e) {
112
148
  this.disabled = true;
113
149
  const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
@@ -131,6 +167,14 @@ export class CodeIndex {
131
167
  const data = this.db.exportDb();
132
168
  const buffer = Buffer.from(data);
133
169
  fs.writeFileSync(this.dbPath, buffer);
170
+ if (sharedConnections.dbPath === this.dbPath) {
171
+ try {
172
+ const stat = fs.statSync(this.dbPath);
173
+ sharedConnections.dbStat = { mtimeMs: stat.mtimeMs, size: stat.size };
174
+ } catch {
175
+ sharedConnections.dbStat = null;
176
+ }
177
+ }
134
178
  } catch (e) {
135
179
  const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
136
180
  logger.warn(`[index] Failed to save database: ${errMsg}`);
@@ -417,14 +461,16 @@ export class CodeIndex {
417
461
  * Close the database connection
418
462
  */
419
463
  close() {
464
+ const wasShared = sharedConnections.db === this.db;
420
465
  if (this.db) {
421
466
  this._saveToFile();
422
467
  this.db.close();
423
468
  this.db = null;
424
469
  }
425
- if (sharedConnections.db === this.db) {
470
+ if (wasShared) {
426
471
  sharedConnections.db = null;
427
472
  sharedConnections.dbPath = null;
473
+ sharedConnections.dbStat = null;
428
474
  }
429
475
  }
430
476
  }
@@ -449,4 +495,5 @@ export function __resetCodeIndexDriverStatusForTest() {
449
495
  sharedConnections.dbPath = null;
450
496
  sharedConnections.schemaInitialized = false;
451
497
  sharedConnections.SQL = null;
498
+ sharedConnections.dbStat = null;
452
499
  }
@@ -153,7 +153,8 @@ const baseConfig = {
153
153
 
154
154
  // LSP client defaults
155
155
  lsp: {
156
- requestTimeoutMs: 120000
156
+ requestTimeoutMs: 120000,
157
+ slowRequestWarnMs: 2000
157
158
  },
158
159
 
159
160
  // Indexing (code index) settings
@@ -184,6 +185,7 @@ function loadEnvConfig() {
184
185
 
185
186
  const telemetryEnabled = parseBoolEnv(process.env.UNITY_MCP_TELEMETRY_ENABLED);
186
187
  const lspRequestTimeoutMs = parseIntEnv(process.env.UNITY_MCP_LSP_REQUEST_TIMEOUT_MS);
188
+ const lspSlowRequestWarnMs = parseIntEnv(process.env.UNITY_MCP_LSP_SLOW_REQUEST_WARN_MS);
187
189
 
188
190
  const out = {};
189
191
 
@@ -221,6 +223,9 @@ function loadEnvConfig() {
221
223
  if (lspRequestTimeoutMs !== undefined) {
222
224
  out.lsp = { requestTimeoutMs: lspRequestTimeoutMs };
223
225
  }
226
+ if (lspSlowRequestWarnMs !== undefined) {
227
+ out.lsp = { ...(out.lsp || {}), slowRequestWarnMs: lspSlowRequestWarnMs };
228
+ }
224
229
 
225
230
  return out;
226
231
  }
@@ -301,6 +306,15 @@ function validateAndNormalizeConfig(cfg) {
301
306
  cfg.lsp.requestTimeoutMs = 60000;
302
307
  }
303
308
  }
309
+ if (cfg.lsp?.slowRequestWarnMs !== undefined) {
310
+ const t = Number(cfg.lsp.slowRequestWarnMs);
311
+ if (!Number.isFinite(t) || t < 0) {
312
+ logger.warning(
313
+ `[unity-mcp-server] WARN: Invalid UNITY_MCP_LSP_SLOW_REQUEST_WARN_MS (${cfg.lsp.slowRequestWarnMs}); using default 2000`
314
+ );
315
+ cfg.lsp.slowRequestWarnMs = 2000;
316
+ }
317
+ }
304
318
  }
305
319
 
306
320
  export const config = merge(baseConfig, loadEnvConfig());
@@ -59,7 +59,9 @@ export function createHttpServer({
59
59
  payload = JSON.parse(raw || '{}');
60
60
  } catch (e) {
61
61
  res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
62
- res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Invalid JSON' } }));
62
+ res.end(
63
+ JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Invalid JSON' } })
64
+ );
63
65
  return;
64
66
  }
65
67
 
@@ -77,7 +79,13 @@ export function createHttpServer({
77
79
  const handler = handlers.get(name);
78
80
  if (!handler) {
79
81
  res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
80
- res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32004, message: `Tool not found: ${name}` } }));
82
+ res.end(
83
+ JSON.stringify({
84
+ jsonrpc: '2.0',
85
+ id,
86
+ error: { code: -32004, message: `Tool not found: ${name}` }
87
+ })
88
+ );
81
89
  return;
82
90
  }
83
91
  try {
@@ -87,13 +95,21 @@ export function createHttpServer({
87
95
  } catch (e) {
88
96
  logger.error(`[http] tool error ${name}: ${e.message}`);
89
97
  res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
90
- res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: e.message } }));
98
+ res.end(
99
+ JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: e.message } })
100
+ );
91
101
  }
92
102
  return;
93
103
  }
94
104
 
95
105
  res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8' });
96
- res.end(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }));
106
+ res.end(
107
+ JSON.stringify({
108
+ jsonrpc: '2.0',
109
+ id,
110
+ error: { code: -32601, message: 'Method not found' }
111
+ })
112
+ );
97
113
  return;
98
114
  }
99
115
 
@@ -125,7 +141,9 @@ export function createHttpServer({
125
141
  server.listen(port, host, () => {
126
142
  server.off('error', onError);
127
143
  const address = server.address();
128
- logger.info(`HTTP listening on http://${host}:${address.port}, telemetry: ${telemetryEnabled ? 'on' : 'off'}`);
144
+ logger.info(
145
+ `HTTP listening on http://${host}:${address.port}, telemetry: ${telemetryEnabled ? 'on' : 'off'}`
146
+ );
129
147
  resolve(address.port);
130
148
  });
131
149
  });
@@ -142,6 +160,12 @@ export function createHttpServer({
142
160
  start,
143
161
  close,
144
162
  getPort: () => server.address()?.port,
145
- health: () => buildHealthResponse({ startedAt, mode: 'http', port: server.address()?.port, telemetryEnabled })
163
+ health: () =>
164
+ buildHealthResponse({
165
+ startedAt,
166
+ mode: 'http',
167
+ port: server.address()?.port,
168
+ telemetryEnabled
169
+ })
146
170
  };
147
171
  }
@@ -1,11 +1,58 @@
1
1
  import { Worker } from 'worker_threads';
2
2
  import path from 'path';
3
+ import os from 'os';
4
+ import fs from 'fs';
5
+ import crypto from 'crypto';
3
6
  import { fileURLToPath } from 'url';
4
- import { logger } from './config.js';
7
+ import { logger, WORKSPACE_ROOT } from './config.js';
5
8
  import { JobManager } from './jobManager.js';
6
9
 
7
10
  const __filename = fileURLToPath(import.meta.url);
8
11
  const __dirname = path.dirname(__filename);
12
+ const DEFAULT_DB_RELATIVE_PATH = path.join('.unity', 'cache', 'code-index', 'code-index.db');
13
+ const DEFAULT_TMP_DB_DIR = path.join(os.tmpdir(), 'unity-mcp-code-index');
14
+
15
+ /**
16
+ * Resolve database path for Worker Thread builds.
17
+ * Falls back to workspace-root based cache when dbPath is not provided.
18
+ * @param {Object} [options] - Build options
19
+ * @param {string} [options.dbPath] - Explicit database path
20
+ * @param {string} [options.projectRoot] - Unity project root path
21
+ * @returns {string} Resolved database path
22
+ */
23
+ export function resolveDbPath(options = {}) {
24
+ if (typeof options.dbPath === 'string' && options.dbPath.trim().length > 0) {
25
+ return options.dbPath;
26
+ }
27
+
28
+ const workspaceRoot =
29
+ typeof WORKSPACE_ROOT === 'string' && WORKSPACE_ROOT.trim().length > 0 ? WORKSPACE_ROOT : '';
30
+ if (workspaceRoot) {
31
+ return path.resolve(workspaceRoot, DEFAULT_DB_RELATIVE_PATH);
32
+ }
33
+
34
+ const projectRoot =
35
+ typeof options.projectRoot === 'string' && options.projectRoot.trim().length > 0
36
+ ? options.projectRoot
37
+ : '';
38
+ const projectExists = projectRoot && fs.existsSync(projectRoot);
39
+
40
+ if (projectExists) {
41
+ return path.join(projectRoot, DEFAULT_DB_RELATIVE_PATH);
42
+ }
43
+
44
+ if (projectRoot) {
45
+ // Fallback for mock/non-existent roots used in integration tests.
46
+ const hash = crypto
47
+ .createHash('sha1')
48
+ .update(projectRoot || 'unknown-project-root', 'utf8')
49
+ .digest('hex')
50
+ .slice(0, 12);
51
+ return path.join(DEFAULT_TMP_DB_DIR, hash, 'code-index.db');
52
+ }
53
+
54
+ return path.resolve(process.cwd(), DEFAULT_DB_RELATIVE_PATH);
55
+ }
9
56
 
10
57
  /**
11
58
  * Worker Thread pool for non-blocking code index builds.
@@ -52,12 +99,19 @@ export class IndexBuildWorkerPool {
52
99
 
53
100
  return new Promise((resolve, reject) => {
54
101
  const workerPath = path.join(__dirname, 'workers', 'indexBuildWorker.js');
102
+ const buildOptions = options ?? {};
103
+ const resolvedDbPath = resolveDbPath(buildOptions);
104
+ const concurrency =
105
+ Number.isFinite(buildOptions.concurrency) && buildOptions.concurrency > 0
106
+ ? buildOptions.concurrency
107
+ : 1;
55
108
 
56
109
  try {
57
110
  this.worker = new Worker(workerPath, {
58
111
  workerData: {
59
- ...options,
60
- concurrency: options.concurrency || 1
112
+ ...buildOptions,
113
+ dbPath: resolvedDbPath,
114
+ concurrency
61
115
  }
62
116
  });
63
117
 
@@ -6,6 +6,7 @@ export class IndexWatcher {
6
6
  constructor(unityConnection) {
7
7
  this.unityConnection = unityConnection;
8
8
  this.timer = null;
9
+ this.startTimeout = null;
9
10
  this.running = false;
10
11
  this.jobManager = JobManager.getInstance();
11
12
  this.currentWatcherJobId = null;
@@ -16,7 +17,7 @@ export class IndexWatcher {
16
17
 
17
18
  start() {
18
19
  if (!config.indexing?.watch) return;
19
- if (this.timer) return;
20
+ if (this.timer || this.startTimeout) return;
20
21
  const interval = Math.max(2000, Number(config.indexing.intervalMs || 15000));
21
22
  // Initial delay: wait longer to allow MCP server to fully initialize
22
23
  // and first tool calls to complete before starting background indexing
@@ -24,7 +25,8 @@ export class IndexWatcher {
24
25
  logger.info(`[index] watcher enabled (interval=${interval}ms, initialDelay=${initialDelay}ms)`);
25
26
 
26
27
  // Delay initial tick significantly to avoid blocking MCP server initialization
27
- const delayedStart = setTimeout(() => {
28
+ this.startTimeout = setTimeout(() => {
29
+ this.startTimeout = null;
28
30
  this.tick();
29
31
  // Start periodic timer only after first tick
30
32
  this.timer = setInterval(() => this.tick(), interval);
@@ -32,12 +34,16 @@ export class IndexWatcher {
32
34
  this.timer.unref();
33
35
  }
34
36
  }, initialDelay);
35
- if (typeof delayedStart.unref === 'function') {
36
- delayedStart.unref();
37
+ if (typeof this.startTimeout?.unref === 'function') {
38
+ this.startTimeout.unref();
37
39
  }
38
40
  }
39
41
 
40
42
  stop() {
43
+ if (this.startTimeout) {
44
+ clearTimeout(this.startTimeout);
45
+ this.startTimeout = null;
46
+ }
41
47
  if (this.timer) {
42
48
  clearInterval(this.timer);
43
49
  this.timer = null;
@@ -104,18 +104,40 @@ export class ProjectInfoProvider {
104
104
  try {
105
105
  const info = await this.unityConnection.sendCommand('get_editor_info', {});
106
106
  if (info && info.projectRoot && info.assetsPath) {
107
- this.cached = {
108
- projectRoot: info.projectRoot,
109
- assetsPath: info.assetsPath,
110
- packagesPath: normalize(info.packagesPath || path.join(info.projectRoot, 'Packages')),
111
- packageCachePath: normalize(
112
- info.packageCachePath || path.join(info.projectRoot, 'Library/PackageCache')
113
- ),
114
- codeIndexRoot: normalize(
115
- info.codeIndexRoot || resolveDefaultCodeIndexRoot(info.projectRoot)
116
- )
117
- };
118
- return this.cached;
107
+ const projectRoot = normalize(info.projectRoot);
108
+ const assetsPath = normalize(info.assetsPath || path.join(info.projectRoot, 'Assets'));
109
+ const packagesPath = normalize(
110
+ info.packagesPath || path.join(info.projectRoot, 'Packages')
111
+ );
112
+ let localReady = false;
113
+ try {
114
+ localReady =
115
+ fs.existsSync(assetsPath) &&
116
+ fs.existsSync(packagesPath) &&
117
+ fs.statSync(assetsPath).isDirectory() &&
118
+ fs.statSync(packagesPath).isDirectory();
119
+ } catch {
120
+ localReady = false;
121
+ }
122
+
123
+ if (localReady) {
124
+ this.cached = {
125
+ projectRoot,
126
+ assetsPath,
127
+ packagesPath,
128
+ packageCachePath: normalize(
129
+ info.packageCachePath || path.join(info.projectRoot, 'Library/PackageCache')
130
+ ),
131
+ codeIndexRoot: normalize(
132
+ info.codeIndexRoot || resolveDefaultCodeIndexRoot(info.projectRoot)
133
+ )
134
+ };
135
+ return this.cached;
136
+ }
137
+
138
+ logger.warning(
139
+ 'get_editor_info returned paths not found locally; falling back to local inference'
140
+ );
119
141
  }
120
142
  } catch (e) {
121
143
  logger.warning(`get_editor_info failed: ${e.message}`);