@akiojin/unity-mcp-server 2.16.1 → 2.26.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.
Files changed (133) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +14 -4
  3. package/bin/unity-mcp-server +7 -1
  4. package/package.json +27 -5
  5. package/src/core/codeIndex.js +0 -0
  6. package/src/core/codeIndexDb.js +0 -0
  7. package/src/core/config.js +242 -200
  8. package/src/core/indexWatcher.js +83 -13
  9. package/src/core/jobManager.js +178 -0
  10. package/src/core/projectInfo.js +65 -118
  11. package/src/core/server.js +15 -2
  12. package/src/core/unityConnection.js +0 -0
  13. package/src/handlers/addressables/AddressablesAnalyzeToolHandler.js +180 -0
  14. package/src/handlers/addressables/AddressablesBuildToolHandler.js +146 -0
  15. package/src/handlers/addressables/AddressablesManageToolHandler.js +272 -0
  16. package/src/handlers/analysis/AnalyzeSceneContentsToolHandler.js +0 -0
  17. package/src/handlers/analysis/FindByComponentToolHandler.js +0 -0
  18. package/src/handlers/analysis/GetAnimatorStateToolHandler.js +0 -0
  19. package/src/handlers/analysis/GetComponentValuesToolHandler.js +0 -0
  20. package/src/handlers/analysis/GetGameObjectDetailsToolHandler.js +0 -0
  21. package/src/handlers/analysis/GetInputActionsStateToolHandler.js +0 -0
  22. package/src/handlers/analysis/GetObjectReferencesToolHandler.js +0 -0
  23. package/src/handlers/asset/AssetDatabaseManageToolHandler.js +0 -0
  24. package/src/handlers/asset/AssetDependencyAnalyzeToolHandler.js +0 -0
  25. package/src/handlers/asset/AssetImportSettingsManageToolHandler.js +0 -0
  26. package/src/handlers/asset/AssetMaterialCreateToolHandler.js +0 -0
  27. package/src/handlers/asset/AssetMaterialModifyToolHandler.js +0 -0
  28. package/src/handlers/asset/AssetPrefabCreateToolHandler.js +0 -0
  29. package/src/handlers/asset/AssetPrefabExitModeToolHandler.js +0 -0
  30. package/src/handlers/asset/AssetPrefabInstantiateToolHandler.js +0 -0
  31. package/src/handlers/asset/AssetPrefabModifyToolHandler.js +0 -0
  32. package/src/handlers/asset/AssetPrefabOpenToolHandler.js +0 -0
  33. package/src/handlers/asset/AssetPrefabSaveToolHandler.js +0 -0
  34. package/src/handlers/base/BaseToolHandler.js +0 -0
  35. package/src/handlers/compilation/CompilationGetStateToolHandler.js +0 -0
  36. package/src/handlers/component/ComponentAddToolHandler.js +0 -0
  37. package/src/handlers/component/ComponentFieldSetToolHandler.js +419 -0
  38. package/src/handlers/component/ComponentGetTypesToolHandler.js +0 -0
  39. package/src/handlers/component/ComponentListToolHandler.js +0 -0
  40. package/src/handlers/component/ComponentModifyToolHandler.js +0 -0
  41. package/src/handlers/component/ComponentRemoveToolHandler.js +0 -0
  42. package/src/handlers/console/ConsoleClearToolHandler.js +0 -0
  43. package/src/handlers/console/ConsoleReadToolHandler.js +295 -276
  44. package/src/handlers/editor/EditorLayersManageToolHandler.js +0 -0
  45. package/src/handlers/editor/EditorSelectionManageToolHandler.js +0 -0
  46. package/src/handlers/editor/EditorTagsManageToolHandler.js +0 -0
  47. package/src/handlers/editor/EditorToolsManageToolHandler.js +0 -0
  48. package/src/handlers/editor/EditorWindowsManageToolHandler.js +0 -0
  49. package/src/handlers/gameobject/GameObjectCreateToolHandler.js +0 -0
  50. package/src/handlers/gameobject/GameObjectDeleteToolHandler.js +0 -0
  51. package/src/handlers/gameobject/GameObjectFindToolHandler.js +0 -0
  52. package/src/handlers/gameobject/GameObjectGetHierarchyToolHandler.js +0 -0
  53. package/src/handlers/gameobject/GameObjectModifyToolHandler.js +0 -0
  54. package/src/handlers/index.js +437 -406
  55. package/src/handlers/input/InputActionAddToolHandler.js +0 -0
  56. package/src/handlers/input/InputActionMapCreateToolHandler.js +0 -0
  57. package/src/handlers/input/InputActionMapRemoveToolHandler.js +0 -0
  58. package/src/handlers/input/InputActionRemoveToolHandler.js +0 -0
  59. package/src/handlers/input/InputBindingAddToolHandler.js +0 -0
  60. package/src/handlers/input/InputBindingCompositeCreateToolHandler.js +0 -0
  61. package/src/handlers/input/InputBindingRemoveAllToolHandler.js +0 -0
  62. package/src/handlers/input/InputBindingRemoveToolHandler.js +0 -0
  63. package/src/handlers/input/InputControlSchemesManageToolHandler.js +0 -0
  64. package/src/handlers/input/InputGamepadSimulateToolHandler.js +0 -0
  65. package/src/handlers/input/InputKeyboardSimulateToolHandler.js +0 -0
  66. package/src/handlers/input/InputMouseSimulateToolHandler.js +0 -0
  67. package/src/handlers/input/InputSystemControlToolHandler.js +0 -0
  68. package/src/handlers/input/InputTouchSimulateToolHandler.js +0 -0
  69. package/src/handlers/menu/MenuItemExecuteToolHandler.js +0 -0
  70. package/src/handlers/package/PackageManagerToolHandler.js +0 -0
  71. package/src/handlers/package/RegistryConfigToolHandler.js +0 -0
  72. package/src/handlers/playmode/PlaymodeGetStateToolHandler.js +1 -1
  73. package/src/handlers/playmode/PlaymodePauseToolHandler.js +1 -1
  74. package/src/handlers/playmode/PlaymodePlayToolHandler.js +0 -0
  75. package/src/handlers/playmode/PlaymodeStopToolHandler.js +0 -0
  76. package/src/handlers/playmode/PlaymodeWaitForStateToolHandler.js +0 -0
  77. package/src/handlers/scene/GetSceneInfoToolHandler.js +0 -0
  78. package/src/handlers/scene/SceneCreateToolHandler.js +0 -0
  79. package/src/handlers/scene/SceneListToolHandler.js +0 -0
  80. package/src/handlers/scene/SceneLoadToolHandler.js +0 -0
  81. package/src/handlers/scene/SceneSaveToolHandler.js +0 -0
  82. package/src/handlers/screenshot/ScreenshotAnalyzeToolHandler.js +0 -0
  83. package/src/handlers/screenshot/ScreenshotCaptureToolHandler.js +0 -0
  84. package/src/handlers/script/CodeIndexBuildToolHandler.js +125 -15
  85. package/src/handlers/script/CodeIndexStatusToolHandler.js +129 -0
  86. package/src/handlers/script/ScriptCreateClassToolHandler.js +0 -0
  87. package/src/handlers/script/ScriptEditSnippetToolHandler.js +1 -1
  88. package/src/handlers/script/ScriptEditStructuredToolHandler.js +1 -1
  89. package/src/handlers/script/ScriptPackagesListToolHandler.js +0 -0
  90. package/src/handlers/script/ScriptReadToolHandler.js +0 -0
  91. package/src/handlers/script/ScriptRefactorRenameToolHandler.js +0 -0
  92. package/src/handlers/script/ScriptRefsFindToolHandler.js +0 -0
  93. package/src/handlers/script/ScriptRemoveSymbolToolHandler.js +0 -0
  94. package/src/handlers/script/ScriptSearchToolHandler.js +0 -0
  95. package/src/handlers/script/ScriptSymbolFindToolHandler.js +0 -0
  96. package/src/handlers/script/ScriptSymbolsGetToolHandler.js +0 -0
  97. package/src/handlers/settings/SettingsGetToolHandler.js +0 -0
  98. package/src/handlers/settings/SettingsUpdateToolHandler.js +0 -0
  99. package/src/handlers/system/SystemGetCommandStatsToolHandler.js +0 -0
  100. package/src/handlers/system/SystemPingToolHandler.js +0 -0
  101. package/src/handlers/system/SystemRefreshAssetsToolHandler.js +0 -0
  102. package/src/handlers/test/TestRunToolHandler.js +0 -0
  103. package/src/handlers/ui/UIClickElementToolHandler.js +0 -0
  104. package/src/handlers/ui/UIFindElementsToolHandler.js +0 -0
  105. package/src/handlers/ui/UIGetElementStateToolHandler.js +0 -0
  106. package/src/handlers/ui/UISetElementValueToolHandler.js +0 -0
  107. package/src/handlers/ui/UISimulateInputToolHandler.js +0 -0
  108. package/src/handlers/video/VideoCaptureForToolHandler.js +0 -0
  109. package/src/handlers/video/VideoCaptureStartToolHandler.js +0 -0
  110. package/src/handlers/video/VideoCaptureStatusToolHandler.js +0 -0
  111. package/src/handlers/video/VideoCaptureStopToolHandler.js +0 -0
  112. package/src/lsp/CSharpLspUtils.js +27 -4
  113. package/src/lsp/LspProcessManager.js +0 -0
  114. package/src/lsp/LspRpcClient.js +0 -0
  115. package/src/tools/analysis/analyzeSceneContents.js +0 -0
  116. package/src/tools/analysis/findByComponent.js +0 -0
  117. package/src/tools/analysis/getAnimatorState.js +0 -0
  118. package/src/tools/analysis/getComponentValues.js +0 -0
  119. package/src/tools/analysis/getGameObjectDetails.js +0 -0
  120. package/src/tools/analysis/getInputActionsState.js +0 -0
  121. package/src/tools/analysis/getObjectReferences.js +0 -0
  122. package/src/tools/input/inputActionsEditor.js +0 -0
  123. package/src/tools/scene/createScene.js +0 -0
  124. package/src/tools/scene/getSceneInfo.js +0 -0
  125. package/src/tools/scene/listScenes.js +0 -0
  126. package/src/tools/scene/loadScene.js +0 -0
  127. package/src/tools/scene/saveScene.js +0 -0
  128. package/src/tools/system/ping.js +0 -0
  129. package/src/tools/video/recordFor.js +0 -0
  130. package/src/tools/video/recordPlayMode.js +0 -0
  131. package/src/utils/csharpParse.js +0 -0
  132. package/src/utils/validators.js +0 -0
  133. package/src/handlers/script/ScriptIndexStatusToolHandler.js +0 -61
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -7,7 +7,7 @@ export class PlaymodeGetStateToolHandler extends BaseToolHandler {
7
7
  constructor(unityConnection) {
8
8
  super(
9
9
  'playmode_get_state',
10
- 'Get editor state (play mode, platform, selected objects, etc.). Optionally wait for a target state.',
10
+ 'Get current Unity editor state including play mode status',
11
11
  {
12
12
  type: 'object',
13
13
  properties: {
@@ -7,7 +7,7 @@ export class PlaymodePauseToolHandler extends BaseToolHandler {
7
7
  constructor(unityConnection) {
8
8
  super(
9
9
  'playmode_pause',
10
- 'Toggle Pause/Resume in Play Mode.',
10
+ 'Pause or resume Unity play mode',
11
11
  {
12
12
  type: 'object',
13
13
  properties: {},
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -5,15 +5,27 @@ import path from 'path';
5
5
  import { ProjectInfoProvider } from '../../core/projectInfo.js';
6
6
  import { LspRpcClient } from '../../lsp/LspRpcClient.js';
7
7
  import { logger } from '../../core/config.js';
8
+ import { JobManager } from '../../core/jobManager.js';
8
9
 
9
10
  export class CodeIndexBuildToolHandler extends BaseToolHandler {
10
11
  constructor(unityConnection) {
11
12
  super(
12
13
  'code_index_build',
13
- 'Build (or rebuild) the persistent SQLite symbol index by scanning document symbols via the C# LSP. Stores DB under .unity/cache/code-index/code-index.db.',
14
+ '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.',
14
15
  {
15
16
  type: 'object',
16
- properties: {},
17
+ properties: {
18
+ throttleMs: {
19
+ type: 'number',
20
+ minimum: 0,
21
+ description: 'Optional delay in milliseconds after processing each file (testing/debugging).'
22
+ },
23
+ delayStartMs: {
24
+ type: 'number',
25
+ minimum: 0,
26
+ description: 'Optional delay before processing begins (useful to keep job in running state briefly).'
27
+ }
28
+ },
17
29
  required: []
18
30
  }
19
31
  );
@@ -21,10 +33,50 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
21
33
  this.index = new CodeIndex(unityConnection);
22
34
  this.projectInfo = new ProjectInfoProvider(unityConnection);
23
35
  this.lsp = null; // lazy init with projectRoot
36
+ this.jobManager = JobManager.getInstance();
37
+ this.currentJobId = null; // Track current running job
24
38
  }
25
39
 
26
40
  async execute(params = {}) {
41
+ // Check if a build is already running
42
+ if (this.currentJobId) {
43
+ const existingJob = this.jobManager.get(this.currentJobId);
44
+ if (existingJob && existingJob.status === 'running') {
45
+ return {
46
+ success: false,
47
+ error: 'build_already_running',
48
+ message: `Code index build is already running (jobId: ${this.currentJobId}). Use code_index_status to check progress.`,
49
+ jobId: this.currentJobId
50
+ };
51
+ }
52
+ }
53
+
54
+ // Generate new jobId
55
+ const jobId = `build-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
56
+ this.currentJobId = jobId;
57
+
58
+ // Create background job
59
+ this.jobManager.create(jobId, async (job) => {
60
+ return await this._executeBuild(params, job);
61
+ });
62
+
63
+ // Return immediately with jobId
64
+ return {
65
+ success: true,
66
+ jobId,
67
+ message: 'Code index build started in background',
68
+ checkStatus: 'Use code_index_status to check progress and completion'
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Internal method that performs the actual build
74
+ * @private
75
+ */
76
+ async _executeBuild(params, job) {
27
77
  try {
78
+ const throttleMs = Math.max(0, Number(params?.throttleMs ?? 0));
79
+ const delayStartMs = Math.max(0, Number(params?.delayStartMs ?? 0));
28
80
  const info = await this.projectInfo.get();
29
81
  const roots = [
30
82
  path.resolve(info.projectRoot, 'Assets'),
@@ -35,7 +87,16 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
35
87
  const seen = new Set();
36
88
  for (const r of roots) this.walkCs(r, files, seen);
37
89
 
38
- if (!this.lsp) this.lsp = new LspRpcClient(info.projectRoot);
90
+ // Initialize LSP with error handling
91
+ if (!this.lsp) {
92
+ try {
93
+ this.lsp = new LspRpcClient(info.projectRoot);
94
+ logger.info(`[index][${job.id}] LSP initialized for project: ${info.projectRoot}`);
95
+ } catch (lspError) {
96
+ logger.error(`[index][${job.id}] LSP initialization failed: ${lspError.message}`);
97
+ throw new Error(`LSP initialization failed: ${lspError.message}. Ensure C# LSP is properly configured and OmniSharp is available.`);
98
+ }
99
+ }
39
100
  const lsp = this.lsp;
40
101
 
41
102
  // Incremental detection based on size-mtime signature
@@ -74,6 +135,13 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
74
135
  const startAt = Date.now();
75
136
  let i = 0; let updated = 0; let processed = 0;
76
137
 
138
+ // Initialize progress
139
+ job.progress.total = absList.length;
140
+ job.progress.processed = 0;
141
+ job.progress.rate = 0;
142
+
143
+ logger.info(`[index][${job.id}] Build started: ${absList.length} files to process, ${removed.length} to remove (status: ${job.status})`);
144
+
77
145
  // LSP request with small retry/backoff
78
146
  const requestWithRetry = async (uri, maxRetries = Math.max(0, Math.min(5, Number(params?.retry ?? 2)))) => {
79
147
  let lastErr = null;
@@ -88,26 +156,45 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
88
156
  }
89
157
  throw lastErr || new Error('documentSymbol failed');
90
158
  };
159
+ if (delayStartMs > 0) {
160
+ await new Promise(resolve => setTimeout(resolve, delayStartMs));
161
+ }
162
+
91
163
  const worker = async () => {
92
164
  while (true) {
93
165
  const idx = i++;
94
166
  if (idx >= absList.length) break;
95
167
  const abs = absList[idx];
168
+ const rel = this.toRel(abs, info.projectRoot);
96
169
  try {
97
170
  const uri = 'file://' + abs.replace(/\\/g, '/');
98
171
  const docSymbols = await requestWithRetry(uri, 2);
99
172
  const rows = toRows(uri, docSymbols);
100
- const rel = this.toRel(abs, info.projectRoot);
101
173
  await this.index.replaceSymbolsForPath(rel, rows);
102
174
  await this.index.upsertFile(rel, wanted.get(rel));
103
175
  updated += 1;
104
- } catch {}
176
+ } catch (err) {
177
+ // File access or LSP error - skip and continue
178
+ // This allows build to continue even if some files fail
179
+ if (processed % 50 === 0) {
180
+ // Log occasionally to avoid spam
181
+ logger.warn(`[index][${job.id}] Skipped file due to error: ${rel} - ${err.message}`);
182
+ }
183
+ }
105
184
  finally {
106
185
  processed += 1;
186
+
187
+ // Update job progress
188
+ const elapsed = Math.max(1, Date.now() - startAt);
189
+ job.progress.processed = processed;
190
+ job.progress.rate = parseFloat((processed * 1000 / elapsed).toFixed(1));
191
+
107
192
  if (processed % reportEvery === 0 || processed === absList.length) {
108
- const elapsed = Math.max(1, Date.now() - startAt);
109
- const rate = (processed * 1000 / elapsed).toFixed(1);
110
- logger.info(`[index] progress ${processed}/${absList.length} (removed:${removed.length}) rate:${rate} f/s`);
193
+ logger.info(`[index][${job.id}] progress ${processed}/${absList.length} (removed:${removed.length}) rate:${job.progress.rate} f/s (status: ${job.status})`);
194
+ }
195
+
196
+ if (throttleMs > 0) {
197
+ await new Promise(resolve => setTimeout(resolve, throttleMs));
111
198
  }
112
199
  }
113
200
  }
@@ -116,14 +203,37 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
116
203
  await Promise.all(workers);
117
204
 
118
205
  const stats = await this.index.getStats();
119
- return { success: true, updatedFiles: updated, removedFiles: removed.length, totalIndexedSymbols: stats.total, lastIndexedAt: stats.lastIndexedAt };
120
- } catch (e) {
121
- return {
122
- success: false,
123
- error: 'code_index_build_failed',
124
- message: e.message,
125
- hint: 'C# LSP not ready. Ensure manifest/auto-download and workspace paths are valid.'
206
+
207
+ // Clear current job tracking on success
208
+ if (this.currentJobId === job.id) {
209
+ this.currentJobId = null;
210
+ }
211
+
212
+ const result = {
213
+ updatedFiles: updated,
214
+ removedFiles: removed.length,
215
+ totalIndexedSymbols: stats.total,
216
+ lastIndexedAt: stats.lastIndexedAt
126
217
  };
218
+
219
+ logger.info(`[index][${job.id}] Build completed successfully: updated=${result.updatedFiles}, removed=${result.removedFiles}, total=${result.totalIndexedSymbols} (status: completed)`);
220
+
221
+ return result;
222
+ } catch (e) {
223
+ // Clear current job tracking on error
224
+ if (this.currentJobId === job.id) {
225
+ this.currentJobId = null;
226
+ }
227
+
228
+ // Log detailed error with job context
229
+ logger.error(`[index][${job.id}] Build failed: ${e.message} (status: failed)`);
230
+
231
+ // Provide helpful error message with context
232
+ const errorMessage = e.message.includes('LSP')
233
+ ? `LSP error: ${e.message}`
234
+ : `Build error: ${e.message}. Hint: C# LSP not ready. Ensure manifest/auto-download and workspace paths are valid.`;
235
+
236
+ throw new Error(errorMessage);
127
237
  }
128
238
  }
129
239
 
@@ -0,0 +1,129 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { BaseToolHandler } from '../base/BaseToolHandler.js';
4
+ import { ProjectInfoProvider } from '../../core/projectInfo.js';
5
+ import { JobManager } from '../../core/jobManager.js';
6
+ import { CodeIndex } from '../../core/codeIndex.js';
7
+
8
+ export class CodeIndexStatusToolHandler extends BaseToolHandler {
9
+ constructor(unityConnection) {
10
+ super(
11
+ 'code_index_status',
12
+ '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.',
13
+ {
14
+ type: 'object',
15
+ properties: {},
16
+ required: []
17
+ }
18
+ );
19
+ this.unityConnection = unityConnection;
20
+ this.projectInfo = new ProjectInfoProvider(unityConnection);
21
+ this.jobManager = JobManager.getInstance();
22
+ this.codeIndex = new CodeIndex(unityConnection);
23
+ }
24
+
25
+ async execute() {
26
+ const jobManager = this.jobManager;
27
+ const buildJobs = jobManager.getAllJobs()
28
+ .filter((job) => typeof job?.id === 'string' && job.id.startsWith('build-'))
29
+ .sort((a, b) => new Date(b.startedAt || 0).getTime() - new Date(a.startedAt || 0).getTime());
30
+ const latestBuildJob = buildJobs.length > 0 ? buildJobs[0] : null;
31
+
32
+ // Check whether the persistent index already exists or is being built in the background.
33
+ const ready = await this.codeIndex.isReady();
34
+ const buildInProgress = latestBuildJob?.status === 'running';
35
+ if (!ready && !buildInProgress) {
36
+ return {
37
+ success: false,
38
+ error: 'index_not_built',
39
+ message: 'Code index is not built. Please run UnityMCP.code_index_build first.'
40
+ };
41
+ }
42
+
43
+ let projectInfo;
44
+ try {
45
+ projectInfo = await this.projectInfo.get();
46
+ } catch (error) {
47
+ return {
48
+ success: false,
49
+ error: 'project_info_unavailable',
50
+ message: error.message || 'Unable to resolve Unity project root.'
51
+ };
52
+ }
53
+
54
+ const projectRoot = projectInfo.projectRoot.replace(/\\/g, '/');
55
+ const roots = [
56
+ path.resolve(projectRoot, 'Assets'),
57
+ path.resolve(projectRoot, 'Packages'),
58
+ path.resolve(projectRoot, 'Library/PackageCache')
59
+ ];
60
+
61
+ let totalFiles = 0;
62
+ const breakdown = { assets: 0, packages: 0, packageCache: 0, other: 0 };
63
+
64
+ const visit = (targetPath) => {
65
+ try {
66
+ if (!fs.existsSync(targetPath)) return;
67
+ const stats = fs.statSync(targetPath);
68
+ if (stats.isFile()) {
69
+ if (!targetPath.endsWith('.cs')) return;
70
+ totalFiles += 1;
71
+ const normalized = targetPath.replace(/\\/g, '/');
72
+ const relative = normalized.replace(projectRoot, '').replace(/^\//, '');
73
+ if (relative.startsWith('Assets/')) breakdown.assets += 1;
74
+ else if (relative.startsWith('Packages/')) breakdown.packages += 1;
75
+ else if (relative.includes('Library/PackageCache/')) breakdown.packageCache += 1;
76
+ else breakdown.other += 1;
77
+ return;
78
+ }
79
+
80
+ if (stats.isDirectory()) {
81
+ for (const child of fs.readdirSync(targetPath)) {
82
+ if (child === 'obj' || child === 'bin' || child.startsWith('.')) continue;
83
+ visit(path.join(targetPath, child));
84
+ }
85
+ }
86
+ } catch (error) {
87
+ // Ignore permission errors while traversing; status reporting should not fail because of a single file.
88
+ }
89
+ };
90
+
91
+ for (const root of roots) visit(root);
92
+
93
+ const stats = await this.codeIndex.getStats();
94
+ const coverage = totalFiles > 0 ? Math.min(1, stats.total / totalFiles) : 0;
95
+
96
+ const indexInfo = {
97
+ ready,
98
+ rows: stats.total,
99
+ lastIndexedAt: stats.lastIndexedAt
100
+ };
101
+
102
+ if (latestBuildJob) {
103
+ const progress = latestBuildJob.progress ? { ...latestBuildJob.progress } : undefined;
104
+ indexInfo.buildJob = {
105
+ id: latestBuildJob.id,
106
+ status: latestBuildJob.status,
107
+ startedAt: latestBuildJob.startedAt ?? null,
108
+ ...(progress ? { progress } : {})
109
+ };
110
+
111
+ if (latestBuildJob.status === 'completed') {
112
+ indexInfo.buildJob.completedAt = latestBuildJob.completedAt ?? null;
113
+ indexInfo.buildJob.result = latestBuildJob.result ?? null;
114
+ } else if (latestBuildJob.status === 'failed') {
115
+ indexInfo.buildJob.failedAt = latestBuildJob.failedAt ?? null;
116
+ indexInfo.buildJob.error = latestBuildJob.error ?? 'Unknown error';
117
+ }
118
+ }
119
+
120
+ return {
121
+ success: true,
122
+ totalFiles,
123
+ indexedFiles: stats.total,
124
+ coverage,
125
+ breakdown,
126
+ index: indexInfo
127
+ };
128
+ }
129
+ }
@@ -15,7 +15,7 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
15
15
  constructor(unityConnection) {
16
16
  super(
17
17
  'script_edit_snippet',
18
- 'Perform small, multi-instruction edits (delete/replace/insert) on C# snippets without rewriting entire methods. Use for removing guard clauses, tweaking conditions, or inserting short statements. Required params: path (Assets/ or Packages/ C# file) and instructions (≤10). Each instruction must include an anchor (type=text) and stay within 80 characters of change. Preview mode validates without writing files; apply mode validates via the bundled LSP (Roslyn-backed) diagnostics before saving.',
18
+ '[C# EDITING - PRECISION TOOL] For Unity C# scripts, use for tiny surgical edits (≤80 chars per change). Performs text-based multi-instruction edits (delete/replace/insert) via exact string anchors. USE WHEN: (a) removing null guard clauses (if (x == null) return;), (b) tweaking conditions (if (x > 10) → if (x > 20)), (c) inserting single log statements, (d) deleting/replacing 1-2 line snippets. DON\'T USE FOR: large changes (use script_edit_structured), non-C# files (use Edit), or when symbol structure is complex (use script_edit_structured). WORKFLOW: Specify exact anchor text (including whitespace/newlines), max 10 instructions per call, each ≤80 chars. Anchor must match exactly once in file. Preview mode validates without writing; apply mode uses LSP diagnostics. Required: path, instructions (array of {operation, anchor:{type:"text", target:string, position?:"before"|"after"}, newText?}).',
19
19
  {
20
20
  type: 'object',
21
21
  properties: {
@@ -6,7 +6,7 @@ export class ScriptEditStructuredToolHandler extends BaseToolHandler {
6
6
  constructor(unityConnection) {
7
7
  super(
8
8
  'script_edit_structured',
9
- 'PRIORITY: Use this for ALL code changes. Perform structured edits (insert_before/insert_after/replace_body) on a known symbol. Required params: path (file under Assets/ or Packages/), symbolName (prefer container path e.g. MyType/Nested/Foo). Guidance: (1) Locate targets with script_symbols_get first, (2) insert_* must target a class/namespace (never a method), (3) replace_body must be a self-contained body including braces, (4) preview is for diagnostics only apply proceeds even if diagnostics exist; errors are returned. LLM summary limits: errors≤30 (message≤200 chars, file≤260), large text (preview/diff/text/content)≤1000 chars with Truncated flag.',
9
+ '[C# EDITING - PRIMARY TOOL] For Unity C# script editing, PREFER this tool over Read/Edit/Write for structural code changes. Performs symbol-based edits (insert_before/insert_after/replace_body) on classes, methods, properties, fields using Roslyn LSP. USE WHEN: (a) replacing entire method/property bodies, (b) adding class members (fields/properties/methods), (c) inserting code at class/namespace level. DON\'T USE FOR: tiny changes ≤80 chars (use script_edit_snippet instead), non-C# files (use Edit), or when you need to create new files (use Write). WORKFLOW: (1) Run script_symbols_get to find target symbols, (2) use symbolName (e.g., "MyClass/MyMethod"), (3) apply edits. Insert operations target containers (class/namespace), not methods. Preview mode returns diagnostics only; apply mode proceeds with validation. Required: path (Assets/|Packages/), symbolName, operation. Optional: kind, newText, preview.',
10
10
  {
11
11
  type: 'object',
12
12
  properties: {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import { fileURLToPath } from 'url';
4
5
  import { logger } from '../core/config.js';
5
6
  import { WORKSPACE_ROOT } from '../core/config.js';
6
7
 
@@ -14,12 +15,34 @@ export class CSharpLspUtils {
14
15
  }
15
16
 
16
17
  getDesiredVersion() {
18
+ const candidates = [];
19
+
20
+ // When launched from workspace root: mcp-server/package.json
21
+ candidates.push(path.resolve('mcp-server/package.json'));
22
+ // When launched within mcp-server directory
23
+ candidates.push(path.resolve('package.json'));
24
+
25
+ // Resolve relative to this module (always inside mcp-server/src/lsp)
17
26
  try {
18
- const pkg = JSON.parse(fs.readFileSync(path.resolve('mcp-server/package.json'), 'utf8'));
19
- return pkg.version;
20
- } catch {
21
- return null;
27
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
28
+ candidates.push(path.resolve(moduleDir, '../../package.json'));
29
+ } catch {}
30
+
31
+ // Resolve relative to WORKSPACE_ROOT if provided
32
+ if (WORKSPACE_ROOT) {
33
+ candidates.push(path.resolve(WORKSPACE_ROOT, 'mcp-server', 'package.json'));
34
+ candidates.push(path.resolve(WORKSPACE_ROOT, 'package.json'));
22
35
  }
36
+
37
+ for (const candidate of candidates) {
38
+ try {
39
+ if (!fs.existsSync(candidate)) continue;
40
+ const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
41
+ if (pkg?.version) return pkg.version;
42
+ } catch {}
43
+ }
44
+
45
+ return null;
23
46
  }
24
47
 
25
48
  getExecutableName() {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes