@akiojin/unity-mcp-server 2.45.4 → 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.4",
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",
@@ -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
@@ -123,6 +128,224 @@ function makeSig(abs) {
123
128
  }
124
129
  }
125
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
+
126
349
  /**
127
350
  * Convert LSP symbol kind to string
128
351
  */
@@ -309,14 +532,12 @@ async function runBuild() {
309
532
  let updated = 0;
310
533
  let lastReportedPercentage = 0;
311
534
 
312
- // Initialize LSP connection
313
- // Note: LspRpcClientSingleton is in src/lsp/, not src/core/lsp/
314
- let lsp = null;
535
+ // Initialize Worker-local LSP client
536
+ // This is independent from main thread's LspRpcClientSingleton
537
+ const lsp = new WorkerLspClient(projectRoot);
315
538
  try {
316
- const lspModulePath = path.join(__dirname, '..', '..', 'lsp', 'LspRpcClientSingleton.js');
317
- const { LspRpcClientSingleton } = await import(`file://${lspModulePath}`);
318
- lsp = await LspRpcClientSingleton.getInstance(projectRoot);
319
- log('info', `[worker] LSP initialized`);
539
+ await lsp.start();
540
+ log('info', `[worker] Worker-local LSP initialized`);
320
541
  } catch (e) {
321
542
  throw new Error(`LSP initialization failed: ${e.message}`);
322
543
  }
@@ -327,7 +548,7 @@ async function runBuild() {
327
548
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
328
549
  try {
329
550
  const res = await lsp.request('textDocument/documentSymbol', { textDocument: { uri } });
330
- return res?.result ?? res;
551
+ return res;
331
552
  } catch (err) {
332
553
  lastErr = err;
333
554
  await new Promise(r => setTimeout(r, 200 * (attempt + 1)));
@@ -408,6 +629,9 @@ async function runBuild() {
408
629
  }
409
630
  }
410
631
 
632
+ // Stop LSP process
633
+ lsp.stop();
634
+
411
635
  // Save database to file
412
636
  saveDatabase(db, dbPath);
413
637