@akiojin/unity-mcp-server 2.45.4 → 2.46.0
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 +5 -4
- package/src/constants/offlineTools.js +19 -0
- package/src/core/codeIndex.js +63 -13
- package/src/core/server.js +255 -174
- package/src/core/stdioRpcServer.js +258 -0
- package/src/core/toolManifest.json +4436 -0
- package/src/core/workers/indexBuildWorker.js +301 -46
- package/src/handlers/script/CodeIndexBuildToolHandler.js +1 -1
- package/src/handlers/script/CodeIndexStatusToolHandler.js +1 -1
- package/src/handlers/script/CodeIndexUpdateToolHandler.js +1 -1
- package/src/handlers/script/ScriptPackagesListToolHandler.js +1 -1
- package/src/handlers/script/ScriptReadToolHandler.js +9 -2
- package/src/handlers/script/ScriptRefsFindToolHandler.js +131 -36
- package/src/handlers/script/ScriptSearchToolHandler.js +1 -1
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +32 -33
- package/src/handlers/script/ScriptSymbolsGetToolHandler.js +18 -10
- package/src/handlers/system/SystemPingToolHandler.js +30 -17
- package/src/lsp/CSharpLspUtils.js +11 -50
|
@@ -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,217 @@ 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() {
|
|
157
|
+
const rid = detectRid();
|
|
158
|
+
const exe = getExecutableName();
|
|
159
|
+
|
|
160
|
+
// Single location: ~/.unity/tools/csharp-lsp/<rid>/server
|
|
161
|
+
const candidates = [path.join(os.homedir(), '.unity', 'tools', 'csharp-lsp', rid, exe)];
|
|
162
|
+
|
|
163
|
+
for (const p of candidates) {
|
|
164
|
+
try {
|
|
165
|
+
const resolved = path.resolve(p);
|
|
166
|
+
if (fs.existsSync(resolved)) {
|
|
167
|
+
return resolved;
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Ignore
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Worker-local LSP client class
|
|
179
|
+
* Manages its own csharp-lsp process independently from main thread
|
|
180
|
+
*/
|
|
181
|
+
class WorkerLspClient {
|
|
182
|
+
constructor(projectRoot) {
|
|
183
|
+
this.projectRoot = projectRoot;
|
|
184
|
+
this.proc = null;
|
|
185
|
+
this.seq = 1;
|
|
186
|
+
this.pending = new Map();
|
|
187
|
+
this.buf = Buffer.alloc(0);
|
|
188
|
+
this.initialized = false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async start() {
|
|
192
|
+
const binPath = findLspBinary();
|
|
193
|
+
if (!binPath) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`csharp-lsp binary not found. Expected at ~/.unity/tools/csharp-lsp/${detectRid()}/${getExecutableName()}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
log('info', `[worker-lsp] Starting LSP: ${binPath}`);
|
|
200
|
+
|
|
201
|
+
this.proc = spawn(binPath, [], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
202
|
+
|
|
203
|
+
this.proc.on('error', e => {
|
|
204
|
+
log('error', `[worker-lsp] Process error: ${e.message}`);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
this.proc.on('close', (code, sig) => {
|
|
208
|
+
log('info', `[worker-lsp] Process exited: code=${code}, signal=${sig || 'none'}`);
|
|
209
|
+
// Reject all pending requests
|
|
210
|
+
for (const [id, p] of this.pending.entries()) {
|
|
211
|
+
p.reject(new Error('LSP process exited'));
|
|
212
|
+
this.pending.delete(id);
|
|
213
|
+
}
|
|
214
|
+
this.proc = null;
|
|
215
|
+
this.initialized = false;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.proc.stderr.on('data', d => {
|
|
219
|
+
const s = String(d || '').trim();
|
|
220
|
+
if (s) log('debug', `[worker-lsp] stderr: ${s}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
this.proc.stdout.on('data', chunk => this._onData(chunk));
|
|
224
|
+
|
|
225
|
+
// Initialize LSP
|
|
226
|
+
await this._initialize();
|
|
227
|
+
this.initialized = true;
|
|
228
|
+
log('info', `[worker-lsp] Initialized successfully`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_onData(chunk) {
|
|
232
|
+
this.buf = Buffer.concat([this.buf, Buffer.from(chunk)]);
|
|
233
|
+
while (true) {
|
|
234
|
+
const headerEnd = this.buf.indexOf('\r\n\r\n');
|
|
235
|
+
if (headerEnd < 0) break;
|
|
236
|
+
const header = this.buf.slice(0, headerEnd).toString('utf8');
|
|
237
|
+
const m = header.match(/Content-Length:\s*(\d+)/i);
|
|
238
|
+
const len = m ? parseInt(m[1], 10) : 0;
|
|
239
|
+
const total = headerEnd + 4 + len;
|
|
240
|
+
if (this.buf.length < total) break;
|
|
241
|
+
const jsonBuf = this.buf.slice(headerEnd + 4, total);
|
|
242
|
+
this.buf = this.buf.slice(total);
|
|
243
|
+
try {
|
|
244
|
+
const msg = JSON.parse(jsonBuf.toString('utf8'));
|
|
245
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
246
|
+
this.pending.get(msg.id).resolve(msg);
|
|
247
|
+
this.pending.delete(msg.id);
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
// Ignore parse errors
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_writeMessage(obj) {
|
|
256
|
+
if (!this.proc || this.proc.killed) {
|
|
257
|
+
throw new Error('LSP process not available');
|
|
258
|
+
}
|
|
259
|
+
const json = JSON.stringify(obj);
|
|
260
|
+
const payload = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`;
|
|
261
|
+
this.proc.stdin.write(payload, 'utf8');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async _initialize() {
|
|
265
|
+
const id = this.seq++;
|
|
266
|
+
const req = {
|
|
267
|
+
jsonrpc: '2.0',
|
|
268
|
+
id,
|
|
269
|
+
method: 'initialize',
|
|
270
|
+
params: {
|
|
271
|
+
processId: process.pid,
|
|
272
|
+
rootUri: 'file://' + String(this.projectRoot).replace(/\\/g, '/'),
|
|
273
|
+
capabilities: {},
|
|
274
|
+
workspaceFolders: null
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const timeoutMs = 60000;
|
|
279
|
+
const p = new Promise((resolve, reject) => {
|
|
280
|
+
this.pending.set(id, { resolve, reject });
|
|
281
|
+
setTimeout(() => {
|
|
282
|
+
if (this.pending.has(id)) {
|
|
283
|
+
this.pending.delete(id);
|
|
284
|
+
reject(new Error(`initialize timed out after ${timeoutMs}ms`));
|
|
285
|
+
}
|
|
286
|
+
}, timeoutMs);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
this._writeMessage(req);
|
|
290
|
+
await p;
|
|
291
|
+
|
|
292
|
+
// Send initialized notification
|
|
293
|
+
this._writeMessage({ jsonrpc: '2.0', method: 'initialized', params: {} });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async request(method, params) {
|
|
297
|
+
if (!this.proc || this.proc.killed) {
|
|
298
|
+
throw new Error('LSP process not available');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const id = this.seq++;
|
|
302
|
+
const timeoutMs = 30000;
|
|
303
|
+
|
|
304
|
+
const p = new Promise((resolve, reject) => {
|
|
305
|
+
this.pending.set(id, { resolve, reject });
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
if (this.pending.has(id)) {
|
|
308
|
+
this.pending.delete(id);
|
|
309
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
310
|
+
}
|
|
311
|
+
}, timeoutMs);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
this._writeMessage({ jsonrpc: '2.0', id, method, params });
|
|
315
|
+
const resp = await p;
|
|
316
|
+
return resp?.result ?? resp;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
stop() {
|
|
320
|
+
if (this.proc && !this.proc.killed) {
|
|
321
|
+
try {
|
|
322
|
+
// Send shutdown/exit
|
|
323
|
+
this._writeMessage({ jsonrpc: '2.0', id: this.seq++, method: 'shutdown', params: {} });
|
|
324
|
+
this._writeMessage({ jsonrpc: '2.0', method: 'exit' });
|
|
325
|
+
this.proc.stdin.end();
|
|
326
|
+
} catch {
|
|
327
|
+
// Ignore
|
|
328
|
+
}
|
|
329
|
+
setTimeout(() => {
|
|
330
|
+
if (this.proc && !this.proc.killed) {
|
|
331
|
+
this.proc.kill('SIGTERM');
|
|
332
|
+
}
|
|
333
|
+
}, 1000);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ============================================================================
|
|
339
|
+
// End Worker-local LSP Client
|
|
340
|
+
// ============================================================================
|
|
341
|
+
|
|
126
342
|
/**
|
|
127
343
|
* Convert LSP symbol kind to string
|
|
128
344
|
*/
|
|
@@ -291,15 +507,23 @@ async function runBuild() {
|
|
|
291
507
|
|
|
292
508
|
log('info', `[worker] Changes: ${changed.length} to update, ${removed.length} to remove`);
|
|
293
509
|
|
|
294
|
-
// Remove vanished files
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
510
|
+
// Remove vanished files - optimized batch delete (Phase 2.2)
|
|
511
|
+
if (removed.length > 0) {
|
|
512
|
+
// SQLite has a limit on compound expressions, batch in groups of 500
|
|
513
|
+
const BATCH_SIZE = 500;
|
|
514
|
+
for (let i = 0; i < removed.length; i += BATCH_SIZE) {
|
|
515
|
+
const batch = removed.slice(i, i + BATCH_SIZE);
|
|
516
|
+
const placeholders = batch.map(() => '?').join(',');
|
|
517
|
+
|
|
518
|
+
const stmt1 = db.prepare(`DELETE FROM symbols WHERE path IN (${placeholders})`);
|
|
519
|
+
stmt1.run(batch);
|
|
520
|
+
stmt1.free();
|
|
521
|
+
|
|
522
|
+
const stmt2 = db.prepare(`DELETE FROM files WHERE path IN (${placeholders})`);
|
|
523
|
+
stmt2.run(batch);
|
|
524
|
+
stmt2.free();
|
|
525
|
+
}
|
|
526
|
+
log('info', `[worker] Removed ${removed.length} files in batches`);
|
|
303
527
|
}
|
|
304
528
|
|
|
305
529
|
// Prepare for updates
|
|
@@ -309,14 +533,12 @@ async function runBuild() {
|
|
|
309
533
|
let updated = 0;
|
|
310
534
|
let lastReportedPercentage = 0;
|
|
311
535
|
|
|
312
|
-
// Initialize LSP
|
|
313
|
-
//
|
|
314
|
-
|
|
536
|
+
// Initialize Worker-local LSP client
|
|
537
|
+
// This is independent from main thread's LspRpcClientSingleton
|
|
538
|
+
const lsp = new WorkerLspClient(projectRoot);
|
|
315
539
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
lsp = await LspRpcClientSingleton.getInstance(projectRoot);
|
|
319
|
-
log('info', `[worker] LSP initialized`);
|
|
540
|
+
await lsp.start();
|
|
541
|
+
log('info', `[worker] Worker-local LSP initialized`);
|
|
320
542
|
} catch (e) {
|
|
321
543
|
throw new Error(`LSP initialization failed: ${e.message}`);
|
|
322
544
|
}
|
|
@@ -327,7 +549,7 @@ async function runBuild() {
|
|
|
327
549
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
328
550
|
try {
|
|
329
551
|
const res = await lsp.request('textDocument/documentSymbol', { textDocument: { uri } });
|
|
330
|
-
return res
|
|
552
|
+
return res;
|
|
331
553
|
} catch (err) {
|
|
332
554
|
lastErr = err;
|
|
333
555
|
await new Promise(r => setTimeout(r, 200 * (attempt + 1)));
|
|
@@ -336,7 +558,57 @@ async function runBuild() {
|
|
|
336
558
|
throw lastErr || new Error('documentSymbol failed');
|
|
337
559
|
};
|
|
338
560
|
|
|
339
|
-
// Process files
|
|
561
|
+
// Phase 2.1 & 2.3: Process files with batched transactions
|
|
562
|
+
// Collect LSP results, then write in batched transactions to reduce overhead
|
|
563
|
+
const TX_BATCH_SIZE = 100; // Files per transaction
|
|
564
|
+
let pendingWrites = []; // { rel, rows, sig }
|
|
565
|
+
|
|
566
|
+
const flushPendingWrites = () => {
|
|
567
|
+
if (pendingWrites.length === 0) return;
|
|
568
|
+
|
|
569
|
+
runSQL(db, 'BEGIN TRANSACTION');
|
|
570
|
+
try {
|
|
571
|
+
// Batch delete old symbols for all pending files
|
|
572
|
+
const pathsToDelete = pendingWrites.map(w => w.rel);
|
|
573
|
+
const delPlaceholders = pathsToDelete.map(() => '?').join(',');
|
|
574
|
+
const delStmt = db.prepare(`DELETE FROM symbols WHERE path IN (${delPlaceholders})`);
|
|
575
|
+
delStmt.run(pathsToDelete);
|
|
576
|
+
delStmt.free();
|
|
577
|
+
|
|
578
|
+
// Batch insert all symbols
|
|
579
|
+
const insertStmt = db.prepare(
|
|
580
|
+
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
581
|
+
);
|
|
582
|
+
for (const { rows } of pendingWrites) {
|
|
583
|
+
for (const r of rows) {
|
|
584
|
+
insertStmt.run([r.path, r.name, r.kind, r.container, r.ns, r.line, r.column]);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
insertStmt.free();
|
|
588
|
+
|
|
589
|
+
// Batch update file signatures
|
|
590
|
+
const fileStmt = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
591
|
+
const now = new Date().toISOString();
|
|
592
|
+
for (const { rel, sig } of pendingWrites) {
|
|
593
|
+
fileStmt.run([rel, sig, now]);
|
|
594
|
+
}
|
|
595
|
+
fileStmt.free();
|
|
596
|
+
|
|
597
|
+
// Update meta once per batch
|
|
598
|
+
const metaStmt = db.prepare("REPLACE INTO meta(key,value) VALUES ('lastIndexedAt',?)");
|
|
599
|
+
metaStmt.run([now]);
|
|
600
|
+
metaStmt.free();
|
|
601
|
+
|
|
602
|
+
runSQL(db, 'COMMIT');
|
|
603
|
+
updated += pendingWrites.length;
|
|
604
|
+
} catch (txErr) {
|
|
605
|
+
runSQL(db, 'ROLLBACK');
|
|
606
|
+
log('error', `[worker] Batch write failed: ${txErr.message}`);
|
|
607
|
+
}
|
|
608
|
+
pendingWrites = [];
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// Process files sequentially (LSP requests), batch DB writes
|
|
340
612
|
for (let i = 0; i < absList.length; i++) {
|
|
341
613
|
const abs = absList[i];
|
|
342
614
|
const rel = toRel(abs, projectRoot);
|
|
@@ -346,36 +618,13 @@ async function runBuild() {
|
|
|
346
618
|
const docSymbols = await requestWithRetry(uri);
|
|
347
619
|
const rows = toRows(uri, docSymbols, projectRoot);
|
|
348
620
|
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
try {
|
|
352
|
-
const delStmt = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
353
|
-
delStmt.run([rel]);
|
|
354
|
-
delStmt.free();
|
|
355
|
-
|
|
356
|
-
const insertStmt = db.prepare(
|
|
357
|
-
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
358
|
-
);
|
|
359
|
-
for (const r of rows) {
|
|
360
|
-
insertStmt.run([r.path, r.name, r.kind, r.container, r.ns, r.line, r.column]);
|
|
361
|
-
}
|
|
362
|
-
insertStmt.free();
|
|
363
|
-
|
|
364
|
-
const fileStmt = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
365
|
-
fileStmt.run([rel, wanted.get(rel), new Date().toISOString()]);
|
|
366
|
-
fileStmt.free();
|
|
367
|
-
|
|
368
|
-
const metaStmt = db.prepare("REPLACE INTO meta(key,value) VALUES ('lastIndexedAt',?)");
|
|
369
|
-
metaStmt.run([new Date().toISOString()]);
|
|
370
|
-
metaStmt.free();
|
|
621
|
+
// Queue for batch write
|
|
622
|
+
pendingWrites.push({ rel, rows, sig: wanted.get(rel) });
|
|
371
623
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
throw txErr;
|
|
624
|
+
// Flush when batch is full
|
|
625
|
+
if (pendingWrites.length >= TX_BATCH_SIZE) {
|
|
626
|
+
flushPendingWrites();
|
|
376
627
|
}
|
|
377
|
-
|
|
378
|
-
updated++;
|
|
379
628
|
} catch (err) {
|
|
380
629
|
// Log occasionally to avoid spam
|
|
381
630
|
if (processed % 50 === 0) {
|
|
@@ -408,6 +657,12 @@ async function runBuild() {
|
|
|
408
657
|
}
|
|
409
658
|
}
|
|
410
659
|
|
|
660
|
+
// Flush remaining pending writes
|
|
661
|
+
flushPendingWrites();
|
|
662
|
+
|
|
663
|
+
// Stop LSP process
|
|
664
|
+
lsp.stop();
|
|
665
|
+
|
|
411
666
|
// Save database to file
|
|
412
667
|
saveDatabase(db, dbPath);
|
|
413
668
|
|
|
@@ -8,7 +8,7 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
|
|
|
8
8
|
constructor(unityConnection) {
|
|
9
9
|
super(
|
|
10
10
|
'code_index_build',
|
|
11
|
-
'Build (or rebuild) the persistent SQLite symbol index by scanning document symbols via the C# LSP. Returns immediately with jobId for background execution. Check progress with code_index_status. Stores DB under .unity/cache/code-index/code-index.db.',
|
|
11
|
+
'[OFFLINE] No Unity connection required. Build (or rebuild) the persistent SQLite symbol index by scanning document symbols via the C# LSP. Returns immediately with jobId for background execution. Check progress with code_index_status. Stores DB under .unity/cache/code-index/code-index.db.',
|
|
12
12
|
{
|
|
13
13
|
type: 'object',
|
|
14
14
|
properties: {
|
|
@@ -6,7 +6,7 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
6
6
|
constructor(unityConnection) {
|
|
7
7
|
super(
|
|
8
8
|
'code_index_status',
|
|
9
|
-
'Report code index status and readiness for symbol/search operations. BEST PRACTICES: Check before heavy symbol operations. Shows total files indexed and coverage percentage. If coverage is low, some symbol operations may be incomplete. Index is automatically built on first use. No parameters needed - lightweight status check.',
|
|
9
|
+
'[OFFLINE] No Unity connection required. Report code index status and readiness for symbol/search operations. BEST PRACTICES: Check before heavy symbol operations. Shows total files indexed and coverage percentage. If coverage is low, some symbol operations may be incomplete. Index is automatically built on first use. No parameters needed - lightweight status check.',
|
|
10
10
|
{
|
|
11
11
|
type: 'object',
|
|
12
12
|
properties: {},
|
|
@@ -17,7 +17,7 @@ export class CodeIndexUpdateToolHandler extends BaseToolHandler {
|
|
|
17
17
|
constructor(unityConnection) {
|
|
18
18
|
super(
|
|
19
19
|
'code_index_update',
|
|
20
|
-
'Refresh code index entries for specific C# files. Use this after modifying files so script editing tools see the latest symbols.',
|
|
20
|
+
'[OFFLINE] No Unity connection required. Refresh code index entries for specific C# files. Use this after modifying files so script editing tools see the latest symbols.',
|
|
21
21
|
{
|
|
22
22
|
type: 'object',
|
|
23
23
|
properties: {
|
|
@@ -8,7 +8,7 @@ export class ScriptPackagesListToolHandler extends BaseToolHandler {
|
|
|
8
8
|
constructor(unityConnection) {
|
|
9
9
|
super(
|
|
10
10
|
'script_packages_list',
|
|
11
|
-
'List Unity packages in the project (optionally include built‑in). BEST PRACTICES: Use to discover available packages and their paths. Set includeBuiltIn=false to see only user packages. Returns package IDs, versions, and resolved paths. Embedded packages can be edited directly. Essential for understanding project dependencies.',
|
|
11
|
+
'[OFFLINE] No Unity connection required. List Unity packages in the project (optionally include built‑in). BEST PRACTICES: Use to discover available packages and their paths. Set includeBuiltIn=false to see only user packages. Returns package IDs, versions, and resolved paths. Embedded packages can be edited directly. Essential for understanding project dependencies.',
|
|
12
12
|
{
|
|
13
13
|
type: 'object',
|
|
14
14
|
properties: {
|
|
@@ -7,7 +7,7 @@ export class ScriptReadToolHandler extends BaseToolHandler {
|
|
|
7
7
|
constructor(unityConnection) {
|
|
8
8
|
super(
|
|
9
9
|
'script_read',
|
|
10
|
-
'Read a C# file with optional line range and payload limits. Files must be under Assets/ or Packages/ and have .cs extension. PRIORITY: Read minimally — locate the target with script_symbols_get and read only the signature area (~30–40 lines). For large files, always pass startLine/endLine and (optionally) maxBytes.',
|
|
10
|
+
'[OFFLINE] No Unity connection required. Read a C# file with optional line range and payload limits. Files must be under Assets/ or Packages/ and have .cs extension. PRIORITY: Read minimally — locate the target with script_symbols_get and read only the signature area (~30–40 lines). For large files, always pass startLine/endLine and (optionally) maxBytes.',
|
|
11
11
|
{
|
|
12
12
|
type: 'object',
|
|
13
13
|
properties: {
|
|
@@ -79,7 +79,14 @@ export class ScriptReadToolHandler extends BaseToolHandler {
|
|
|
79
79
|
|
|
80
80
|
const abs = info.projectRoot + '/' + norm;
|
|
81
81
|
const stat = await fs.stat(abs).catch(() => null);
|
|
82
|
-
if (!stat || !stat.isFile())
|
|
82
|
+
if (!stat || !stat.isFile()) {
|
|
83
|
+
return {
|
|
84
|
+
error: 'File not found',
|
|
85
|
+
path: norm,
|
|
86
|
+
resolvedPath: abs,
|
|
87
|
+
hint: `Verify the file exists at: ${abs}. Path must be relative to Unity project root (e.g., "Assets/Scripts/Foo.cs" or "Packages/com.example/Runtime/Bar.cs").`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
83
90
|
|
|
84
91
|
const data = await fs.readFile(abs, 'utf8');
|
|
85
92
|
const lines = data.split('\n');
|