@akiojin/unity-mcp-server 2.45.3 → 2.45.5

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.3",
3
+ "version": "2.45.5",
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",
@@ -72,8 +72,17 @@ export class CodeIndex {
72
72
 
73
73
  // Use shared connection for all CodeIndex instances
74
74
  if (sharedConnections.db && sharedConnections.dbPath === dbPath) {
75
- this.db = sharedConnections.db;
76
- return this.db;
75
+ // Verify the DB file still exists before returning cached connection
76
+ if (!fs.existsSync(dbPath)) {
77
+ // File was deleted or never created, invalidate cache
78
+ logger.info('[index] DB file missing, invalidating cached connection');
79
+ sharedConnections.db = null;
80
+ sharedConnections.dbPath = null;
81
+ sharedConnections.schemaInitialized = false;
82
+ } else {
83
+ this.db = sharedConnections.db;
84
+ return this.db;
85
+ }
77
86
  }
78
87
 
79
88
  try {
@@ -9,11 +9,16 @@
9
9
  * Communication with main thread:
10
10
  * - Receives: workerData with build options
11
11
  * - Sends: progress updates, completion, errors, logs
12
+ *
13
+ * IMPORTANT: This worker uses an inline LSP client implementation to avoid
14
+ * importing main thread modules that may not work correctly in Worker Threads.
12
15
  */
13
16
 
14
17
  import { parentPort, workerData } from 'worker_threads';
18
+ import { spawn } from 'child_process';
15
19
  import fs from 'fs';
16
20
  import path from 'path';
21
+ import os from 'os';
17
22
  import { fileURLToPath } from 'url';
18
23
 
19
24
  // fast-sql helper: run SQL statement
@@ -31,9 +36,24 @@ function saveDatabase(db, dbPath) {
31
36
  try {
32
37
  const data = db.exportDb();
33
38
  const buffer = Buffer.from(data);
39
+
40
+ // Ensure parent directory exists
41
+ const dir = path.dirname(dbPath);
42
+ if (!fs.existsSync(dir)) {
43
+ fs.mkdirSync(dir, { recursive: true });
44
+ }
45
+
34
46
  fs.writeFileSync(dbPath, buffer);
47
+
48
+ // Verify file was written
49
+ if (!fs.existsSync(dbPath)) {
50
+ throw new Error('Database file was not created');
51
+ }
52
+
53
+ log('info', `[worker] Database saved successfully: ${dbPath} (${buffer.length} bytes)`);
35
54
  } catch (e) {
36
- log('warn', `[worker] Failed to save database: ${e.message}`);
55
+ log('error', `[worker] Failed to save database: ${e.message}`);
56
+ throw e; // Re-throw to fail the build
37
57
  }
38
58
  }
39
59
 
@@ -108,6 +128,224 @@ function makeSig(abs) {
108
128
  }
109
129
  }
110
130
 
131
+ // ============================================================================
132
+ // Worker-local LSP Client Implementation
133
+ // ============================================================================
134
+ // This is an inline implementation specifically for Worker Threads.
135
+ // It does NOT share state with main thread and manages its own LSP process.
136
+
137
+ /**
138
+ * Detect runtime identifier for csharp-lsp binary
139
+ */
140
+ function detectRid() {
141
+ if (process.platform === 'win32') return process.arch === 'arm64' ? 'win-arm64' : 'win-x64';
142
+ if (process.platform === 'darwin') return process.arch === 'arm64' ? 'osx-arm64' : 'osx-x64';
143
+ return process.arch === 'arm64' ? 'linux-arm64' : 'linux-x64';
144
+ }
145
+
146
+ /**
147
+ * Get csharp-lsp executable name
148
+ */
149
+ function getExecutableName() {
150
+ return process.platform === 'win32' ? 'server.exe' : 'server';
151
+ }
152
+
153
+ /**
154
+ * Find csharp-lsp binary path
155
+ */
156
+ function findLspBinary(projectRoot) {
157
+ const rid = detectRid();
158
+ const exe = getExecutableName();
159
+
160
+ // Check multiple possible locations
161
+ const candidates = [
162
+ // Primary: ~/.unity/tools/csharp-lsp/<rid>/server
163
+ path.join(os.homedir(), '.unity', 'tools', 'csharp-lsp', rid, exe),
164
+ // Legacy: <workspace>/.unity/tools/csharp-lsp/<rid>/server
165
+ path.join(projectRoot, '.unity', 'tools', 'csharp-lsp', rid, exe),
166
+ // Also check parent directories in case projectRoot is nested
167
+ path.join(projectRoot, '..', '.unity', 'tools', 'csharp-lsp', rid, exe)
168
+ ];
169
+
170
+ for (const p of candidates) {
171
+ try {
172
+ const resolved = path.resolve(p);
173
+ if (fs.existsSync(resolved)) {
174
+ return resolved;
175
+ }
176
+ } catch {
177
+ // Ignore
178
+ }
179
+ }
180
+
181
+ return null;
182
+ }
183
+
184
+ /**
185
+ * Worker-local LSP client class
186
+ * Manages its own csharp-lsp process independently from main thread
187
+ */
188
+ class WorkerLspClient {
189
+ constructor(projectRoot) {
190
+ this.projectRoot = projectRoot;
191
+ this.proc = null;
192
+ this.seq = 1;
193
+ this.pending = new Map();
194
+ this.buf = Buffer.alloc(0);
195
+ this.initialized = false;
196
+ }
197
+
198
+ async start() {
199
+ const binPath = findLspBinary(this.projectRoot);
200
+ if (!binPath) {
201
+ throw new Error(
202
+ `csharp-lsp binary not found. Expected at ~/.unity/tools/csharp-lsp/${detectRid()}/${getExecutableName()}`
203
+ );
204
+ }
205
+
206
+ log('info', `[worker-lsp] Starting LSP: ${binPath}`);
207
+
208
+ this.proc = spawn(binPath, [], { stdio: ['pipe', 'pipe', 'pipe'] });
209
+
210
+ this.proc.on('error', e => {
211
+ log('error', `[worker-lsp] Process error: ${e.message}`);
212
+ });
213
+
214
+ this.proc.on('close', (code, sig) => {
215
+ log('info', `[worker-lsp] Process exited: code=${code}, signal=${sig || 'none'}`);
216
+ // Reject all pending requests
217
+ for (const [id, p] of this.pending.entries()) {
218
+ p.reject(new Error('LSP process exited'));
219
+ this.pending.delete(id);
220
+ }
221
+ this.proc = null;
222
+ this.initialized = false;
223
+ });
224
+
225
+ this.proc.stderr.on('data', d => {
226
+ const s = String(d || '').trim();
227
+ if (s) log('debug', `[worker-lsp] stderr: ${s}`);
228
+ });
229
+
230
+ this.proc.stdout.on('data', chunk => this._onData(chunk));
231
+
232
+ // Initialize LSP
233
+ await this._initialize();
234
+ this.initialized = true;
235
+ log('info', `[worker-lsp] Initialized successfully`);
236
+ }
237
+
238
+ _onData(chunk) {
239
+ this.buf = Buffer.concat([this.buf, Buffer.from(chunk)]);
240
+ while (true) {
241
+ const headerEnd = this.buf.indexOf('\r\n\r\n');
242
+ if (headerEnd < 0) break;
243
+ const header = this.buf.slice(0, headerEnd).toString('utf8');
244
+ const m = header.match(/Content-Length:\s*(\d+)/i);
245
+ const len = m ? parseInt(m[1], 10) : 0;
246
+ const total = headerEnd + 4 + len;
247
+ if (this.buf.length < total) break;
248
+ const jsonBuf = this.buf.slice(headerEnd + 4, total);
249
+ this.buf = this.buf.slice(total);
250
+ try {
251
+ const msg = JSON.parse(jsonBuf.toString('utf8'));
252
+ if (msg.id && this.pending.has(msg.id)) {
253
+ this.pending.get(msg.id).resolve(msg);
254
+ this.pending.delete(msg.id);
255
+ }
256
+ } catch {
257
+ // Ignore parse errors
258
+ }
259
+ }
260
+ }
261
+
262
+ _writeMessage(obj) {
263
+ if (!this.proc || this.proc.killed) {
264
+ throw new Error('LSP process not available');
265
+ }
266
+ const json = JSON.stringify(obj);
267
+ const payload = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`;
268
+ this.proc.stdin.write(payload, 'utf8');
269
+ }
270
+
271
+ async _initialize() {
272
+ const id = this.seq++;
273
+ const req = {
274
+ jsonrpc: '2.0',
275
+ id,
276
+ method: 'initialize',
277
+ params: {
278
+ processId: process.pid,
279
+ rootUri: 'file://' + String(this.projectRoot).replace(/\\/g, '/'),
280
+ capabilities: {},
281
+ workspaceFolders: null
282
+ }
283
+ };
284
+
285
+ const timeoutMs = 60000;
286
+ const p = new Promise((resolve, reject) => {
287
+ this.pending.set(id, { resolve, reject });
288
+ setTimeout(() => {
289
+ if (this.pending.has(id)) {
290
+ this.pending.delete(id);
291
+ reject(new Error(`initialize timed out after ${timeoutMs}ms`));
292
+ }
293
+ }, timeoutMs);
294
+ });
295
+
296
+ this._writeMessage(req);
297
+ await p;
298
+
299
+ // Send initialized notification
300
+ this._writeMessage({ jsonrpc: '2.0', method: 'initialized', params: {} });
301
+ }
302
+
303
+ async request(method, params) {
304
+ if (!this.proc || this.proc.killed) {
305
+ throw new Error('LSP process not available');
306
+ }
307
+
308
+ const id = this.seq++;
309
+ const timeoutMs = 30000;
310
+
311
+ const p = new Promise((resolve, reject) => {
312
+ this.pending.set(id, { resolve, reject });
313
+ setTimeout(() => {
314
+ if (this.pending.has(id)) {
315
+ this.pending.delete(id);
316
+ reject(new Error(`${method} timed out after ${timeoutMs}ms`));
317
+ }
318
+ }, timeoutMs);
319
+ });
320
+
321
+ this._writeMessage({ jsonrpc: '2.0', id, method, params });
322
+ const resp = await p;
323
+ return resp?.result ?? resp;
324
+ }
325
+
326
+ stop() {
327
+ if (this.proc && !this.proc.killed) {
328
+ try {
329
+ // Send shutdown/exit
330
+ this._writeMessage({ jsonrpc: '2.0', id: this.seq++, method: 'shutdown', params: {} });
331
+ this._writeMessage({ jsonrpc: '2.0', method: 'exit' });
332
+ this.proc.stdin.end();
333
+ } catch {
334
+ // Ignore
335
+ }
336
+ setTimeout(() => {
337
+ if (this.proc && !this.proc.killed) {
338
+ this.proc.kill('SIGTERM');
339
+ }
340
+ }, 1000);
341
+ }
342
+ }
343
+ }
344
+
345
+ // ============================================================================
346
+ // End Worker-local LSP Client
347
+ // ============================================================================
348
+
111
349
  /**
112
350
  * Convert LSP symbol kind to string
113
351
  */
@@ -294,14 +532,12 @@ async function runBuild() {
294
532
  let updated = 0;
295
533
  let lastReportedPercentage = 0;
296
534
 
297
- // Initialize LSP connection
298
- // Note: LspRpcClientSingleton is in src/lsp/, not src/core/lsp/
299
- let lsp = null;
535
+ // Initialize Worker-local LSP client
536
+ // This is independent from main thread's LspRpcClientSingleton
537
+ const lsp = new WorkerLspClient(projectRoot);
300
538
  try {
301
- const lspModulePath = path.join(__dirname, '..', '..', 'lsp', 'LspRpcClientSingleton.js');
302
- const { LspRpcClientSingleton } = await import(`file://${lspModulePath}`);
303
- lsp = await LspRpcClientSingleton.getInstance(projectRoot);
304
- log('info', `[worker] LSP initialized`);
539
+ await lsp.start();
540
+ log('info', `[worker] Worker-local LSP initialized`);
305
541
  } catch (e) {
306
542
  throw new Error(`LSP initialization failed: ${e.message}`);
307
543
  }
@@ -312,7 +548,7 @@ async function runBuild() {
312
548
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
313
549
  try {
314
550
  const res = await lsp.request('textDocument/documentSymbol', { textDocument: { uri } });
315
- return res?.result ?? res;
551
+ return res;
316
552
  } catch (err) {
317
553
  lastErr = err;
318
554
  await new Promise(r => setTimeout(r, 200 * (attempt + 1)));
@@ -393,6 +629,9 @@ async function runBuild() {
393
629
  }
394
630
  }
395
631
 
632
+ // Stop LSP process
633
+ lsp.stop();
634
+
396
635
  // Save database to file
397
636
  saveDatabase(db, dbPath);
398
637