@akiojin/unity-mcp-server 2.39.2 → 2.40.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akiojin/unity-mcp-server",
3
- "version": "2.39.2",
3
+ "version": "2.40.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",
@@ -97,7 +97,7 @@ const baseConfig = {
97
97
  // Indexing (code index) settings
98
98
  indexing: {
99
99
  // Enable periodic incremental index updates (polling watcher)
100
- watch: (process.env.INDEX_WATCH || 'false').toLowerCase() === 'true',
100
+ watch: true,
101
101
  // Polling interval (ms)
102
102
  intervalMs: Number(process.env.INDEX_WATCH_INTERVAL_MS || 15000),
103
103
  // Build options
@@ -34,10 +34,44 @@ export class IndexWatcher {
34
34
  if (this.running) return;
35
35
  this.running = true;
36
36
  try {
37
+ // Check if code index DB file exists (before opening DB)
38
+ const { ProjectInfoProvider } = await import('./projectInfo.js');
39
+ const projectInfo = new ProjectInfoProvider(this.unityConnection);
40
+ const info = await projectInfo.get();
41
+ const fs = await import('fs');
42
+ const path = await import('path');
43
+ const dbPath = path.default.join(info.codeIndexRoot, 'code-index.db');
44
+ const dbExists = fs.default.existsSync(dbPath);
45
+
46
+ if (!dbExists) {
47
+ logger.warn('[index] watcher: code index DB file not found, triggering full rebuild');
48
+ // Force full rebuild when DB file is missing
49
+ const jobId = `watcher-rebuild-${Date.now()}`;
50
+ this.currentWatcherJobId = jobId;
51
+
52
+ const { CodeIndexBuildToolHandler } = await import(
53
+ '../handlers/script/CodeIndexBuildToolHandler.js'
54
+ );
55
+ const handler = new CodeIndexBuildToolHandler(this.unityConnection);
56
+
57
+ this.jobManager.create(jobId, async job => {
58
+ const params = {
59
+ concurrency: config.indexing.concurrency || 8,
60
+ retry: config.indexing.retry || 2,
61
+ reportPercentage: 10
62
+ };
63
+ return await handler._executeBuild(params, job);
64
+ });
65
+
66
+ logger.info(`[index] watcher: started DB rebuild job ${jobId}`);
67
+ this._monitorJob(jobId);
68
+ return;
69
+ }
70
+
37
71
  // Check if manual build is already running (jobs starting with 'build-')
38
72
  const allJobs = this.jobManager.getAllJobs();
39
- const manualBuildRunning = allJobs.some(job =>
40
- job.id.startsWith('build-') && job.status === 'running'
73
+ const manualBuildRunning = allJobs.some(
74
+ job => job.id.startsWith('build-') && job.status === 'running'
41
75
  );
42
76
 
43
77
  if (manualBuildRunning) {
@@ -60,15 +94,17 @@ export class IndexWatcher {
60
94
  const jobId = `watcher-${Date.now()}`;
61
95
  this.currentWatcherJobId = jobId;
62
96
 
63
- const { CodeIndexBuildToolHandler } = await import('../handlers/script/CodeIndexBuildToolHandler.js');
97
+ const { CodeIndexBuildToolHandler } = await import(
98
+ '../handlers/script/CodeIndexBuildToolHandler.js'
99
+ );
64
100
  const handler = new CodeIndexBuildToolHandler(this.unityConnection);
65
101
 
66
102
  // Create the build job through JobManager
67
- this.jobManager.create(jobId, async (job) => {
103
+ this.jobManager.create(jobId, async job => {
68
104
  const params = {
69
105
  concurrency: config.indexing.concurrency || 8,
70
106
  retry: config.indexing.retry || 2,
71
- reportEvery: config.indexing.reportEvery || 500,
107
+ reportEvery: config.indexing.reportEvery || 500
72
108
  };
73
109
  return await handler._executeBuild(params, job);
74
110
  });
@@ -78,7 +114,6 @@ export class IndexWatcher {
78
114
  // Monitor job completion in background
79
115
  // (Job result will be logged when it completes/fails)
80
116
  this._monitorJob(jobId);
81
-
82
117
  } catch (e) {
83
118
  logger.warn(`[index] watcher exception: ${e.message}`);
84
119
  } finally {
@@ -101,7 +136,9 @@ export class IndexWatcher {
101
136
  }
102
137
 
103
138
  if (job.status === 'completed') {
104
- logger.info(`[index] watcher: auto-build completed - updated=${job.result?.updatedFiles || 0} removed=${job.result?.removedFiles || 0} total=${job.result?.totalIndexedSymbols || 0}`);
139
+ logger.info(
140
+ `[index] watcher: auto-build completed - updated=${job.result?.updatedFiles || 0} removed=${job.result?.removedFiles || 0} total=${job.result?.totalIndexedSymbols || 0}`
141
+ );
105
142
  clearInterval(checkInterval);
106
143
  } else if (job.status === 'failed') {
107
144
  logger.warn(`[index] watcher: auto-build failed - ${job.error}`);
@@ -232,6 +232,36 @@ export async function startServer() {
232
232
  process.on('SIGINT', stopWatch);
233
233
  process.on('SIGTERM', stopWatch);
234
234
 
235
+ // Auto-initialize code index if DB doesn't exist
236
+ (async () => {
237
+ try {
238
+ const { CodeIndex } = await import('./codeIndex.js');
239
+ const index = new CodeIndex(unityConnection);
240
+ const ready = await index.isReady();
241
+
242
+ if (!ready) {
243
+ logger.info('[startup] Code index DB not ready. Starting auto-build...');
244
+ const { CodeIndexBuildToolHandler } = await import(
245
+ '../handlers/script/CodeIndexBuildToolHandler.js'
246
+ );
247
+ const builder = new CodeIndexBuildToolHandler(unityConnection);
248
+ const result = await builder.execute({});
249
+
250
+ if (result.success) {
251
+ logger.info(
252
+ `[startup] Code index auto-build started: jobId=${result.jobId}. Use code_index_status to check progress.`
253
+ );
254
+ } else {
255
+ logger.warn(`[startup] Code index auto-build failed: ${result.message}`);
256
+ }
257
+ } else {
258
+ logger.info('[startup] Code index DB already exists. Skipping auto-build.');
259
+ }
260
+ } catch (e) {
261
+ logger.warn(`[startup] Code index auto-init failed: ${e.message}`);
262
+ }
263
+ })();
264
+
235
265
  // Handle shutdown
236
266
  process.on('SIGINT', async () => {
237
267
  logger.info('Shutting down...');
@@ -18,12 +18,14 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
18
18
  throttleMs: {
19
19
  type: 'number',
20
20
  minimum: 0,
21
- description: 'Optional delay in milliseconds after processing each file (testing/debugging).'
21
+ description:
22
+ 'Optional delay in milliseconds after processing each file (testing/debugging).'
22
23
  },
23
24
  delayStartMs: {
24
25
  type: 'number',
25
26
  minimum: 0,
26
- description: 'Optional delay before processing begins (useful to keep job in running state briefly).'
27
+ description:
28
+ 'Optional delay before processing begins (useful to keep job in running state briefly).'
27
29
  }
28
30
  },
29
31
  required: []
@@ -56,7 +58,7 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
56
58
  this.currentJobId = jobId;
57
59
 
58
60
  // Create background job
59
- this.jobManager.create(jobId, async (job) => {
61
+ this.jobManager.create(jobId, async job => {
60
62
  return await this._executeBuild(params, job);
61
63
  });
62
64
 
@@ -81,7 +83,7 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
81
83
  const roots = [
82
84
  path.resolve(info.projectRoot, 'Assets'),
83
85
  path.resolve(info.projectRoot, 'Packages'),
84
- path.resolve(info.projectRoot, 'Library/PackageCache'),
86
+ path.resolve(info.projectRoot, 'Library/PackageCache')
85
87
  ];
86
88
  const files = [];
87
89
  const seen = new Set();
@@ -94,14 +96,21 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
94
96
  logger.info(`[index][${job.id}] LSP initialized for project: ${info.projectRoot}`);
95
97
  } catch (lspError) {
96
98
  logger.error(`[index][${job.id}] LSP initialization failed: ${lspError.message}`);
97
- throw new Error(`LSP initialization failed: ${lspError.message}. Ensure C# LSP is properly configured and OmniSharp is available.`);
99
+ throw new Error(
100
+ `LSP initialization failed: ${lspError.message}. Ensure C# LSP is properly configured and OmniSharp is available.`
101
+ );
98
102
  }
99
103
  }
100
104
  const lsp = this.lsp;
101
105
 
102
106
  // Incremental detection based on size-mtime signature
103
- const makeSig = (abs) => {
104
- try { const st = fs.statSync(abs); return `${st.size}-${Math.floor(st.mtimeMs)}`; } catch { return '0-0'; }
107
+ const makeSig = abs => {
108
+ try {
109
+ const st = fs.statSync(abs);
110
+ return `${st.size}-${Math.floor(st.mtimeMs)}`;
111
+ } catch {
112
+ return '0-0';
113
+ }
105
114
  };
106
115
  const wanted = new Map(files.map(abs => [this.toRel(abs, info.projectRoot), makeSig(abs)]));
107
116
  const current = await this.index.getFiles();
@@ -118,7 +127,15 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
118
127
  const kind = this.kindFromLsp(s.kind);
119
128
  const name = s.name || '';
120
129
  const start = s.range?.start || s.selectionRange?.start || {};
121
- rows.push({ path: rel, name, kind, container: container || null, ns: null, line: (start.line ?? 0) + 1, column: (start.character ?? 0) + 1 });
130
+ rows.push({
131
+ path: rel,
132
+ name,
133
+ kind,
134
+ container: container || null,
135
+ ns: null,
136
+ line: (start.line ?? 0) + 1,
137
+ column: (start.character ?? 0) + 1
138
+ });
122
139
  if (Array.isArray(s.children)) for (const c of s.children) visit(c, name || container);
123
140
  };
124
141
  if (Array.isArray(symbols)) for (const s of symbols) visit(s, null);
@@ -131,19 +148,27 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
131
148
  // Update changed files
132
149
  const absList = changed.map(rel => path.resolve(info.projectRoot, rel));
133
150
  const concurrency = Math.max(1, Math.min(64, Number(params?.concurrency ?? 8)));
134
- const reportEvery = Math.max(1, Number(params?.reportEvery ?? 100));
151
+ const reportPercentage = Math.max(1, Math.min(100, Number(params?.reportPercentage ?? 10)));
135
152
  const startAt = Date.now();
136
- let i = 0; let updated = 0; let processed = 0;
153
+ let i = 0;
154
+ let updated = 0;
155
+ let processed = 0;
156
+ let lastReportedPercentage = 0;
137
157
 
138
158
  // Initialize progress
139
159
  job.progress.total = absList.length;
140
160
  job.progress.processed = 0;
141
161
  job.progress.rate = 0;
142
162
 
143
- logger.info(`[index][${job.id}] Build started: ${absList.length} files to process, ${removed.length} to remove (status: ${job.status})`);
163
+ logger.info(
164
+ `[index][${job.id}] Build started: ${absList.length} files to process, ${removed.length} to remove (status: ${job.status})`
165
+ );
144
166
 
145
167
  // LSP request with small retry/backoff
146
- const requestWithRetry = async (uri, maxRetries = Math.max(0, Math.min(5, Number(params?.retry ?? 2)))) => {
168
+ const requestWithRetry = async (
169
+ uri,
170
+ maxRetries = Math.max(0, Math.min(5, Number(params?.retry ?? 2)))
171
+ ) => {
147
172
  let lastErr = null;
148
173
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
149
174
  try {
@@ -180,17 +205,26 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
180
205
  // Log occasionally to avoid spam
181
206
  logger.warn(`[index][${job.id}] Skipped file due to error: ${rel} - ${err.message}`);
182
207
  }
183
- }
184
- finally {
208
+ } finally {
185
209
  processed += 1;
186
210
 
187
211
  // Update job progress
188
212
  const elapsed = Math.max(1, Date.now() - startAt);
189
213
  job.progress.processed = processed;
190
- job.progress.rate = parseFloat((processed * 1000 / elapsed).toFixed(1));
214
+ job.progress.rate = parseFloat(((processed * 1000) / elapsed).toFixed(1));
191
215
 
192
- if (processed % reportEvery === 0 || processed === absList.length) {
193
- logger.info(`[index][${job.id}] progress ${processed}/${absList.length} (removed:${removed.length}) rate:${job.progress.rate} f/s (status: ${job.status})`);
216
+ // Calculate current percentage
217
+ const currentPercentage = Math.floor((processed / absList.length) * 100);
218
+
219
+ // Log when percentage increases by reportPercentage (default: 10%)
220
+ if (
221
+ currentPercentage >= lastReportedPercentage + reportPercentage ||
222
+ processed === absList.length
223
+ ) {
224
+ logger.info(
225
+ `[index][${job.id}] progress ${currentPercentage}% (${processed}/${absList.length}) removed:${removed.length} rate:${job.progress.rate} f/s`
226
+ );
227
+ lastReportedPercentage = currentPercentage;
194
228
  }
195
229
 
196
230
  if (throttleMs > 0) {
@@ -216,7 +250,9 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
216
250
  lastIndexedAt: stats.lastIndexedAt
217
251
  };
218
252
 
219
- logger.info(`[index][${job.id}] Build completed successfully: updated=${result.updatedFiles}, removed=${result.removedFiles}, total=${result.totalIndexedSymbols} (status: completed)`);
253
+ logger.info(
254
+ `[index][${job.id}] Build completed successfully: updated=${result.updatedFiles}, removed=${result.removedFiles}, total=${result.totalIndexedSymbols} (status: completed)`
255
+ );
220
256
 
221
257
  return result;
222
258
  } catch (e) {
@@ -242,7 +278,10 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
242
278
  if (!fs.existsSync(root)) return;
243
279
  const st = fs.statSync(root);
244
280
  if (st.isFile()) {
245
- if (root.endsWith('.cs') && !seen.has(root)) { files.push(root); seen.add(root); }
281
+ if (root.endsWith('.cs') && !seen.has(root)) {
282
+ files.push(root);
283
+ seen.add(root);
284
+ }
246
285
  return;
247
286
  }
248
287
  const entries = fs.readdirSync(root, { withFileTypes: true });
@@ -261,13 +300,22 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
261
300
 
262
301
  kindFromLsp(k) {
263
302
  switch (k) {
264
- case 5: return 'class';
265
- case 23: return 'struct';
266
- case 11: return 'interface';
267
- case 10: return 'enum';
268
- case 6: return 'method';
269
- case 7: return 'property';
270
- case 8: return 'field';
271
- case 3: return 'namespace'; }
303
+ case 5:
304
+ return 'class';
305
+ case 23:
306
+ return 'struct';
307
+ case 11:
308
+ return 'interface';
309
+ case 10:
310
+ return 'enum';
311
+ case 6:
312
+ return 'method';
313
+ case 7:
314
+ return 'property';
315
+ case 8:
316
+ return 'field';
317
+ case 3:
318
+ return 'namespace';
319
+ }
272
320
  }
273
321
  }