@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
package/src/core/codeIndex.js
CHANGED
|
@@ -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
|
-
|
|
76
|
-
|
|
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('
|
|
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
|
|
298
|
-
//
|
|
299
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
|
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
|
|