@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
@@ -1,10 +1,13 @@
1
1
  import { logger, config } from './config.js';
2
+ import { JobManager } from './jobManager.js';
2
3
 
3
4
  export class IndexWatcher {
4
5
  constructor(unityConnection) {
5
6
  this.unityConnection = unityConnection;
6
7
  this.timer = null;
7
8
  this.running = false;
9
+ this.jobManager = JobManager.getInstance();
10
+ this.currentWatcherJobId = null;
8
11
  }
9
12
 
10
13
  start() {
@@ -13,6 +16,9 @@ export class IndexWatcher {
13
16
  const interval = Math.max(2000, Number(config.indexing.intervalMs || 15000));
14
17
  logger.info(`[index] watcher enabled (interval=${interval}ms)`);
15
18
  this.timer = setInterval(() => this.tick(), interval);
19
+ if (typeof this.timer.unref === 'function') {
20
+ this.timer.unref();
21
+ }
16
22
  // Initial kick
17
23
  this.tick();
18
24
  }
@@ -28,24 +34,88 @@ export class IndexWatcher {
28
34
  if (this.running) return;
29
35
  this.running = true;
30
36
  try {
37
+ // Check if manual build is already running (jobs starting with 'build-')
38
+ const allJobs = this.jobManager.getAllJobs();
39
+ const manualBuildRunning = allJobs.some(job =>
40
+ job.id.startsWith('build-') && job.status === 'running'
41
+ );
42
+
43
+ if (manualBuildRunning) {
44
+ logger.info('[index] watcher: skipping auto-build (manual build in progress)');
45
+ return;
46
+ }
47
+
48
+ // Check if our watcher job is still running
49
+ if (this.currentWatcherJobId) {
50
+ const watcherJob = this.jobManager.get(this.currentWatcherJobId);
51
+ if (watcherJob && watcherJob.status === 'running') {
52
+ logger.info('[index] watcher: previous auto-build still running, skipping');
53
+ return;
54
+ }
55
+ // Previous watcher job completed or cleaned up
56
+ this.currentWatcherJobId = null;
57
+ }
58
+
59
+ // Start new watcher build job
60
+ const jobId = `watcher-${Date.now()}`;
61
+ this.currentWatcherJobId = jobId;
62
+
31
63
  const { CodeIndexBuildToolHandler } = await import('../handlers/script/CodeIndexBuildToolHandler.js');
32
64
  const handler = new CodeIndexBuildToolHandler(this.unityConnection);
33
- const params = {
34
- concurrency: config.indexing.concurrency || 8,
35
- retry: config.indexing.retry || 2,
36
- reportEvery: config.indexing.reportEvery || 500,
37
- };
38
- const res = await handler.handle(params);
39
- const ok = res?.result?.success;
40
- if (ok) {
41
- logger.info(`[index] updated=${res.result.updatedFiles || 0} removed=${res.result.removedFiles || 0} total=${res.result.totalIndexedSymbols || 0}`);
42
- } else if (res?.result?.error) {
43
- logger.warn(`[index] update failed: ${res.result.error}`);
44
- }
65
+
66
+ // Create the build job through JobManager
67
+ this.jobManager.create(jobId, async (job) => {
68
+ const params = {
69
+ concurrency: config.indexing.concurrency || 8,
70
+ retry: config.indexing.retry || 2,
71
+ reportEvery: config.indexing.reportEvery || 500,
72
+ };
73
+ return await handler._executeBuild(params, job);
74
+ });
75
+
76
+ logger.info(`[index] watcher: started auto-build job ${jobId}`);
77
+
78
+ // Monitor job completion in background
79
+ // (Job result will be logged when it completes/fails)
80
+ this._monitorJob(jobId);
81
+
45
82
  } catch (e) {
46
- logger.warn(`[index] update exception: ${e.message}`);
83
+ logger.warn(`[index] watcher exception: ${e.message}`);
47
84
  } finally {
48
85
  this.running = false;
49
86
  }
50
87
  }
88
+
89
+ /**
90
+ * Monitor job completion for logging
91
+ * @private
92
+ */
93
+ async _monitorJob(jobId) {
94
+ // Check job status periodically
95
+ const checkInterval = setInterval(() => {
96
+ const job = this.jobManager.get(jobId);
97
+ if (!job) {
98
+ // Job cleaned up
99
+ clearInterval(checkInterval);
100
+ return;
101
+ }
102
+
103
+ if (job.status === 'completed') {
104
+ logger.info(`[index] watcher: auto-build completed - updated=${job.result?.updatedFiles || 0} removed=${job.result?.removedFiles || 0} total=${job.result?.totalIndexedSymbols || 0}`);
105
+ clearInterval(checkInterval);
106
+ } else if (job.status === 'failed') {
107
+ logger.warn(`[index] watcher: auto-build failed - ${job.error}`);
108
+ clearInterval(checkInterval);
109
+ }
110
+ }, 1000);
111
+ if (typeof checkInterval.unref === 'function') {
112
+ checkInterval.unref();
113
+ }
114
+
115
+ // Cleanup interval after 10 minutes (longer than job cleanup)
116
+ const cleanupTimeout = setTimeout(() => clearInterval(checkInterval), 600000);
117
+ if (typeof cleanupTimeout.unref === 'function') {
118
+ cleanupTimeout.unref();
119
+ }
120
+ }
51
121
  }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * JobManager - Manages background job execution and tracking
3
+ *
4
+ * SPEC-yt3ikddd: Background job execution for code index build
5
+ * - Memory-based job tracking (no persistence)
6
+ * - Single concurrent job execution
7
+ * - Auto-cleanup after 5 minutes (configurable)
8
+ */
9
+
10
+ /**
11
+ * BuildJob structure
12
+ * @typedef {Object} BuildJob
13
+ * @property {string} id - Unique job identifier
14
+ * @property {'running'|'completed'|'failed'} status - Current job status
15
+ * @property {Object} progress - Progress information
16
+ * @property {number} progress.processed - Number of processed items
17
+ * @property {number} progress.total - Total items to process
18
+ * @property {number} progress.rate - Processing rate (items/sec)
19
+ * @property {Object|null} result - Result object (only for completed jobs)
20
+ * @property {number} result.updatedFiles - Number of updated files
21
+ * @property {number} result.removedFiles - Number of removed files
22
+ * @property {number} result.totalIndexedSymbols - Total indexed symbols
23
+ * @property {string} result.lastIndexedAt - ISO8601 timestamp of last index
24
+ * @property {string|null} error - Error message (only for failed jobs)
25
+ * @property {string} startedAt - ISO8601 timestamp when job started
26
+ * @property {string|null} completedAt - ISO8601 timestamp when job completed
27
+ * @property {string|null} failedAt - ISO8601 timestamp when job failed
28
+ */
29
+
30
+ export class JobManager {
31
+ constructor() {
32
+ /**
33
+ * Map of jobId -> BuildJob
34
+ * @type {Map<string, BuildJob>}
35
+ */
36
+ this.jobs = new Map();
37
+
38
+ /**
39
+ * Map of jobId -> cleanup timeout handle
40
+ * @type {Map<string, NodeJS.Timeout>}
41
+ */
42
+ this.cleanupTimers = new Map();
43
+ }
44
+
45
+ /**
46
+ * Get the singleton instance
47
+ * @returns {JobManager}
48
+ */
49
+ static getInstance() {
50
+ if (!JobManager._instance) {
51
+ JobManager._instance = new JobManager();
52
+ }
53
+ return JobManager._instance;
54
+ }
55
+
56
+ /**
57
+ * Create a new job and execute it in background
58
+ *
59
+ * @param {string} jobId - Unique job identifier
60
+ * @param {Function} jobFn - Async function to execute: async (job) => result
61
+ * @returns {string} jobId - Returns immediately
62
+ */
63
+ create(jobId, jobFn) {
64
+ // Create initial job object
65
+ const job = {
66
+ id: jobId,
67
+ status: 'running',
68
+ progress: {
69
+ processed: 0,
70
+ total: 0,
71
+ rate: 0
72
+ },
73
+ result: null,
74
+ error: null,
75
+ startedAt: new Date().toISOString(),
76
+ completedAt: null,
77
+ failedAt: null
78
+ };
79
+
80
+ // Store job
81
+ this.jobs.set(jobId, job);
82
+
83
+ // Execute in background (don't await)
84
+ this._executeJob(jobId, jobFn, job);
85
+
86
+ // Return jobId immediately
87
+ return jobId;
88
+ }
89
+
90
+ /**
91
+ * Get job by ID
92
+ *
93
+ * @param {string} jobId - Job identifier
94
+ * @returns {BuildJob|undefined} Job object or undefined if not found
95
+ */
96
+ get(jobId) {
97
+ return this.jobs.get(jobId);
98
+ }
99
+
100
+ /**
101
+ * Schedule job cleanup after retention period
102
+ *
103
+ * @param {string} jobId - Job identifier
104
+ * @param {number} retentionMs - Retention period in milliseconds (default: 5 minutes)
105
+ */
106
+ cleanup(jobId, retentionMs = 300000) {
107
+ // Clear any existing cleanup timer
108
+ if (this.cleanupTimers.has(jobId)) {
109
+ clearTimeout(this.cleanupTimers.get(jobId));
110
+ }
111
+
112
+ // Schedule new cleanup
113
+ const timer = setTimeout(() => {
114
+ this.jobs.delete(jobId);
115
+ this.cleanupTimers.delete(jobId);
116
+ }, retentionMs);
117
+ if (typeof timer.unref === 'function') {
118
+ timer.unref();
119
+ }
120
+
121
+ this.cleanupTimers.set(jobId, timer);
122
+ }
123
+
124
+ /**
125
+ * Execute job function in background
126
+ *
127
+ * @private
128
+ * @param {string} jobId - Job identifier
129
+ * @param {Function} jobFn - Async job function
130
+ * @param {BuildJob} job - Job object (passed by reference for progress updates)
131
+ */
132
+ async _executeJob(jobId, jobFn, job) {
133
+ try {
134
+ // Execute job function
135
+ // jobFn can update job.progress directly
136
+ const result = await jobFn(job);
137
+
138
+ // Update job on success
139
+ job.status = 'completed';
140
+ job.result = result;
141
+ job.completedAt = new Date().toISOString();
142
+
143
+ // Schedule cleanup after 5 minutes
144
+ this.cleanup(jobId, 300000);
145
+ } catch (error) {
146
+ // Update job on failure
147
+ job.status = 'failed';
148
+ job.error = error.message || String(error);
149
+ job.failedAt = new Date().toISOString();
150
+
151
+ // Schedule cleanup after 5 minutes
152
+ this.cleanup(jobId, 300000);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Get all jobs (for debugging/testing)
158
+ *
159
+ * @returns {BuildJob[]} Array of all jobs
160
+ */
161
+ getAllJobs() {
162
+ return Array.from(this.jobs.values());
163
+ }
164
+
165
+ /**
166
+ * Clear all jobs (for testing)
167
+ */
168
+ clearAll() {
169
+ // Clear all cleanup timers
170
+ for (const timer of this.cleanupTimers.values()) {
171
+ clearTimeout(timer);
172
+ }
173
+ this.cleanupTimers.clear();
174
+
175
+ // Clear all jobs
176
+ this.jobs.clear();
177
+ }
178
+ }
@@ -1,118 +1,65 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { logger, config, WORKSPACE_ROOT } from './config.js';
4
-
5
- const normalize = (p) => p.replace(/\\/g, '/');
6
-
7
- const resolveDefaultCodeIndexRoot = (projectRoot) => {
8
- const base = WORKSPACE_ROOT || projectRoot || process.cwd();
9
- return normalize(path.join(base, '.unity', 'cache', 'code-index'));
10
- };
11
-
12
- // Lazy project info resolver. Prefers Unity via get_editor_info, otherwise infers by walking up for Assets/Packages.
13
- export class ProjectInfoProvider {
14
- constructor(unityConnection) {
15
- this.unityConnection = unityConnection;
16
- this.cached = null;
17
- this.lastTried = 0;
18
- }
19
-
20
- async get() {
21
- if (this.cached) return this.cached;
22
- // Config-driven project root (no env fallback)
23
- const cfgRoot = config?.project?.root;
24
- if (cfgRoot) {
25
- // Resolve relative paths against WORKSPACE_ROOT
26
- const projectRoot = normalize(path.isAbsolute(cfgRoot) ? cfgRoot : path.resolve(WORKSPACE_ROOT, cfgRoot));
27
- const codeIndexRoot = normalize(config?.project?.codeIndexRoot || resolveDefaultCodeIndexRoot(projectRoot));
28
- this.cached = {
29
- projectRoot,
30
- assetsPath: normalize(path.join(projectRoot, 'Assets')),
31
- packagesPath: normalize(path.join(projectRoot, 'Packages')),
32
- codeIndexRoot,
33
- };
34
- return this.cached;
35
- }
36
- // Try Unity if connected (rate-limit attempts)
37
- const now = Date.now();
38
- if (this.unityConnection && this.unityConnection.isConnected() && (now - this.lastTried > 1000)) {
39
- this.lastTried = now;
40
- try {
41
- const info = await this.unityConnection.sendCommand('get_editor_info', {});
42
- if (info && info.projectRoot && info.assetsPath) {
43
- this.cached = {
44
- projectRoot: info.projectRoot,
45
- assetsPath: info.assetsPath,
46
- packagesPath: normalize(info.packagesPath || path.join(info.projectRoot, 'Packages')),
47
- codeIndexRoot: normalize(info.codeIndexRoot || resolveDefaultCodeIndexRoot(info.projectRoot)),
48
- };
49
- return this.cached;
50
- }
51
- } catch (e) {
52
- logger.warn(`get_editor_info failed: ${e.message}`);
53
- }
54
- }
55
- // Fallback: infer by walking up from CWD to find Assets/
56
- const inferred = this.inferFromCwd();
57
- if (inferred) {
58
- this.cached = inferred;
59
- return this.cached;
60
- }
61
- throw new Error('Unable to resolve Unity project root (no connection and no Assets/ found)');
62
- }
63
-
64
- inferFromCwd() {
65
- // First, check for Docker environment default path
66
- const dockerDefaultPath = '/unity-mcp-server/UnityMCPServer';
67
- const dockerAssets = path.join(dockerDefaultPath, 'Assets');
68
- if (fs.existsSync(dockerAssets)) {
69
- const projectRoot = normalize(dockerDefaultPath);
70
- logger.info(`Found Unity project at Docker default path: ${projectRoot}`);
71
- return {
72
- projectRoot,
73
- assetsPath: normalize(dockerAssets),
74
- packagesPath: normalize(path.join(dockerDefaultPath, 'Packages')),
75
- codeIndexRoot: resolveDefaultCodeIndexRoot(projectRoot),
76
- };
77
- }
78
-
79
- let dir = process.cwd();
80
- // Walk up max 5 levels (look for Assets directly under a directory)
81
- for (let i = 0; i < 5; i++) {
82
- const assets = path.join(dir, 'Assets');
83
- if (fs.existsSync(assets)) {
84
- const projectRoot = normalize(dir);
85
- return {
86
- projectRoot,
87
- assetsPath: normalize(assets),
88
- packagesPath: normalize(path.join(dir, 'Packages')),
89
- codeIndexRoot: resolveDefaultCodeIndexRoot(projectRoot),
90
- };
91
- }
92
- dir = path.dirname(dir);
93
- }
94
-
95
- // Fallback: search shallow subdirectories for Unity projects (depth 2)
96
- const rootsToCheck = [process.cwd(), path.dirname(process.cwd())];
97
- for (const root of rootsToCheck) {
98
- try {
99
- const entries = fs.readdirSync(root, { withFileTypes: true });
100
- for (const e of entries) {
101
- if (!e.isDirectory()) continue;
102
- const candidate = path.join(root, e.name);
103
- const assets = path.join(candidate, 'Assets');
104
- if (fs.existsSync(assets)) {
105
- const projectRoot = normalize(candidate);
106
- return {
107
- projectRoot,
108
- assetsPath: normalize(assets),
109
- packagesPath: normalize(path.join(candidate, 'Packages')),
110
- codeIndexRoot: resolveDefaultCodeIndexRoot(projectRoot),
111
- };
112
- }
113
- }
114
- } catch {}
115
- }
116
- return null;
117
- }
118
- }
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { logger, config, WORKSPACE_ROOT } from './config.js';
4
+
5
+ const normalize = (p) => p.replace(/\\/g, '/');
6
+
7
+ const resolveDefaultCodeIndexRoot = (projectRoot) => {
8
+ const base = WORKSPACE_ROOT || projectRoot || process.cwd();
9
+ return normalize(path.join(base, '.unity', 'cache', 'code-index'));
10
+ };
11
+
12
+ // Lazy project info resolver. Prefers Unity via get_editor_info, otherwise infers by walking up for Assets/Packages.
13
+ export class ProjectInfoProvider {
14
+ constructor(unityConnection) {
15
+ this.unityConnection = unityConnection;
16
+ this.cached = null;
17
+ this.lastTried = 0;
18
+ }
19
+
20
+ async get() {
21
+ if (this.cached) return this.cached;
22
+ // Config-driven project root (no env fallback)
23
+ const cfgRootRaw = config?.project?.root;
24
+ if (typeof cfgRootRaw === 'string' && cfgRootRaw.trim().length > 0) {
25
+ const cfgRoot = cfgRootRaw.trim();
26
+ // Resolve relative paths against WORKSPACE_ROOT
27
+ const projectRoot = normalize(path.isAbsolute(cfgRoot) ? cfgRoot : path.resolve(WORKSPACE_ROOT, cfgRoot));
28
+ const codeIndexRoot = normalize(config?.project?.codeIndexRoot || resolveDefaultCodeIndexRoot(projectRoot));
29
+ this.cached = {
30
+ projectRoot,
31
+ assetsPath: normalize(path.join(projectRoot, 'Assets')),
32
+ packagesPath: normalize(path.join(projectRoot, 'Packages')),
33
+ codeIndexRoot,
34
+ };
35
+ return this.cached;
36
+ }
37
+ // Try Unity if connected (rate-limit attempts)
38
+ const now = Date.now();
39
+ if (this.unityConnection && this.unityConnection.isConnected() && (now - this.lastTried > 1000)) {
40
+ this.lastTried = now;
41
+ try {
42
+ const info = await this.unityConnection.sendCommand('get_editor_info', {});
43
+ if (info && info.projectRoot && info.assetsPath) {
44
+ this.cached = {
45
+ projectRoot: info.projectRoot,
46
+ assetsPath: info.assetsPath,
47
+ packagesPath: normalize(info.packagesPath || path.join(info.projectRoot, 'Packages')),
48
+ codeIndexRoot: normalize(info.codeIndexRoot || resolveDefaultCodeIndexRoot(info.projectRoot)),
49
+ };
50
+ return this.cached;
51
+ }
52
+ } catch (e) {
53
+ logger.warn(`get_editor_info failed: ${e.message}`);
54
+ }
55
+ }
56
+ if (typeof cfgRootRaw === 'string') {
57
+ throw new Error('project.root is configured but empty. Set a valid path in .unity/config.json or UNITY_MCP_CONFIG.');
58
+ }
59
+ throw new Error('Unable to resolve Unity project root. Configure project.root in .unity/config.json or provide UNITY_MCP_CONFIG.');
60
+ }
61
+
62
+ inferFromCwd() {
63
+ return null;
64
+ }
65
+ }
@@ -169,7 +169,7 @@ unityConnection.on('error', (error) => {
169
169
  });
170
170
 
171
171
  // Initialize server
172
- async function main() {
172
+ export async function startServer() {
173
173
  try {
174
174
  // Create transport - no logging before connection
175
175
  const transport = new StdioServerTransport();
@@ -232,6 +232,9 @@ async function main() {
232
232
  }
233
233
  }
234
234
 
235
+ // Maintain backwards compatibility for older callers that expect main()
236
+ const main = startServer;
237
+
235
238
  // Export for testing
236
239
  export async function createServer(customConfig = config) {
237
240
  const testUnityConnection = new UnityConnection();
@@ -287,7 +290,17 @@ export async function createServer(customConfig = config) {
287
290
  }
288
291
 
289
292
  // Start the server
290
- main().catch((error) => {
293
+ const isDirectExecution = (() => {
294
+ if (process.env.NODE_ENV === 'test') return false;
295
+ if (!process.argv[1]) return false;
296
+ try {
297
+ return import.meta.url === new URL('file://' + process.argv[1]).href;
298
+ } catch {
299
+ return false;
300
+ }
301
+ })();
302
+
303
+ if (isDirectExecution) startServer().catch((error) => {
291
304
  console.error('Fatal error:', error);
292
305
  console.error('Stack trace:', error.stack);
293
306
  process.exit(1);
File without changes