@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
|
@@ -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
|
|
313
|
-
//
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
|
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
|
|