@akiojin/unity-mcp-server 2.14.14
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/LICENSE +21 -0
- package/README.md +206 -0
- package/bin/unity-mcp-server +2 -0
- package/package.json +73 -0
- package/src/core/codeIndex.js +163 -0
- package/src/core/codeIndexDb.js +96 -0
- package/src/core/config.js +165 -0
- package/src/core/indexWatcher.js +52 -0
- package/src/core/projectInfo.js +111 -0
- package/src/core/server.js +294 -0
- package/src/core/unityConnection.js +426 -0
- package/src/handlers/analysis/AnalyzeSceneContentsToolHandler.js +35 -0
- package/src/handlers/analysis/FindByComponentToolHandler.js +20 -0
- package/src/handlers/analysis/GetAnimatorStateToolHandler.js +37 -0
- package/src/handlers/analysis/GetComponentValuesToolHandler.js +20 -0
- package/src/handlers/analysis/GetGameObjectDetailsToolHandler.js +35 -0
- package/src/handlers/analysis/GetInputActionsStateToolHandler.js +37 -0
- package/src/handlers/analysis/GetObjectReferencesToolHandler.js +20 -0
- package/src/handlers/asset/AssetDatabaseToolHandler.js +221 -0
- package/src/handlers/asset/AssetDependencyToolHandler.js +201 -0
- package/src/handlers/asset/AssetImportSettingsToolHandler.js +170 -0
- package/src/handlers/asset/CreateMaterialToolHandler.js +96 -0
- package/src/handlers/asset/CreatePrefabToolHandler.js +78 -0
- package/src/handlers/asset/ExitPrefabModeToolHandler.js +83 -0
- package/src/handlers/asset/InstantiatePrefabToolHandler.js +133 -0
- package/src/handlers/asset/ModifyMaterialToolHandler.js +76 -0
- package/src/handlers/asset/ModifyPrefabToolHandler.js +72 -0
- package/src/handlers/asset/OpenPrefabToolHandler.js +121 -0
- package/src/handlers/asset/SavePrefabToolHandler.js +106 -0
- package/src/handlers/base/BaseToolHandler.js +133 -0
- package/src/handlers/compilation/GetCompilationStateToolHandler.js +90 -0
- package/src/handlers/component/AddComponentToolHandler.js +126 -0
- package/src/handlers/component/GetComponentTypesToolHandler.js +100 -0
- package/src/handlers/component/ListComponentsToolHandler.js +85 -0
- package/src/handlers/component/ModifyComponentToolHandler.js +143 -0
- package/src/handlers/component/RemoveComponentToolHandler.js +108 -0
- package/src/handlers/console/ClearConsoleToolHandler.js +160 -0
- package/src/handlers/console/ReadConsoleToolHandler.js +276 -0
- package/src/handlers/editor/LayerManagementToolHandler.js +160 -0
- package/src/handlers/editor/SelectionToolHandler.js +141 -0
- package/src/handlers/editor/TagManagementToolHandler.js +129 -0
- package/src/handlers/editor/ToolManagementToolHandler.js +135 -0
- package/src/handlers/editor/WindowManagementToolHandler.js +125 -0
- package/src/handlers/gameobject/CreateGameObjectToolHandler.js +131 -0
- package/src/handlers/gameobject/DeleteGameObjectToolHandler.js +101 -0
- package/src/handlers/gameobject/FindGameObjectToolHandler.js +119 -0
- package/src/handlers/gameobject/GetHierarchyToolHandler.js +132 -0
- package/src/handlers/gameobject/ModifyGameObjectToolHandler.js +128 -0
- package/src/handlers/index.js +389 -0
- package/src/handlers/input/AddInputActionToolHandler.js +20 -0
- package/src/handlers/input/AddInputBindingToolHandler.js +20 -0
- package/src/handlers/input/CreateActionMapToolHandler.js +20 -0
- package/src/handlers/input/CreateCompositeBindingToolHandler.js +20 -0
- package/src/handlers/input/GamepadSimulationHandler.js +116 -0
- package/src/handlers/input/InputSystemHandler.js +80 -0
- package/src/handlers/input/KeyboardSimulationHandler.js +79 -0
- package/src/handlers/input/ManageControlSchemesToolHandler.js +20 -0
- package/src/handlers/input/MouseSimulationHandler.js +107 -0
- package/src/handlers/input/RemoveActionMapToolHandler.js +20 -0
- package/src/handlers/input/RemoveAllBindingsToolHandler.js +20 -0
- package/src/handlers/input/RemoveInputActionToolHandler.js +20 -0
- package/src/handlers/input/RemoveInputBindingToolHandler.js +20 -0
- package/src/handlers/input/TouchSimulationHandler.js +142 -0
- package/src/handlers/menu/ExecuteMenuItemToolHandler.js +304 -0
- package/src/handlers/package/PackageManagerToolHandler.js +248 -0
- package/src/handlers/package/RegistryConfigToolHandler.js +198 -0
- package/src/handlers/playmode/GetEditorStateToolHandler.js +81 -0
- package/src/handlers/playmode/PauseToolHandler.js +44 -0
- package/src/handlers/playmode/PlayToolHandler.js +91 -0
- package/src/handlers/playmode/StopToolHandler.js +77 -0
- package/src/handlers/playmode/WaitForEditorStateToolHandler.js +45 -0
- package/src/handlers/scene/CreateSceneToolHandler.js +91 -0
- package/src/handlers/scene/GetSceneInfoToolHandler.js +20 -0
- package/src/handlers/scene/ListScenesToolHandler.js +58 -0
- package/src/handlers/scene/LoadSceneToolHandler.js +92 -0
- package/src/handlers/scene/SaveSceneToolHandler.js +76 -0
- package/src/handlers/screenshot/AnalyzeScreenshotToolHandler.js +238 -0
- package/src/handlers/screenshot/CaptureScreenshotToolHandler.js +692 -0
- package/src/handlers/script/BuildCodeIndexToolHandler.js +163 -0
- package/src/handlers/script/ScriptCreateClassFileToolHandler.js +60 -0
- package/src/handlers/script/ScriptEditStructuredToolHandler.js +173 -0
- package/src/handlers/script/ScriptIndexStatusToolHandler.js +61 -0
- package/src/handlers/script/ScriptPackagesListToolHandler.js +103 -0
- package/src/handlers/script/ScriptReadToolHandler.js +106 -0
- package/src/handlers/script/ScriptRefactorRenameToolHandler.js +83 -0
- package/src/handlers/script/ScriptRefsFindToolHandler.js +144 -0
- package/src/handlers/script/ScriptRemoveSymbolToolHandler.js +79 -0
- package/src/handlers/script/ScriptSearchToolHandler.js +320 -0
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +117 -0
- package/src/handlers/script/ScriptSymbolsGetToolHandler.js +96 -0
- package/src/handlers/settings/GetProjectSettingsToolHandler.js +161 -0
- package/src/handlers/settings/UpdateProjectSettingsToolHandler.js +272 -0
- package/src/handlers/system/GetCommandStatsToolHandler.js +25 -0
- package/src/handlers/system/PingToolHandler.js +53 -0
- package/src/handlers/system/RefreshAssetsToolHandler.js +45 -0
- package/src/handlers/ui/ClickUIElementToolHandler.js +110 -0
- package/src/handlers/ui/FindUIElementsToolHandler.js +63 -0
- package/src/handlers/ui/GetUIElementStateToolHandler.js +50 -0
- package/src/handlers/ui/SetUIElementValueToolHandler.js +49 -0
- package/src/handlers/ui/SimulateUIInputToolHandler.js +156 -0
- package/src/handlers/video/CaptureVideoForToolHandler.js +96 -0
- package/src/handlers/video/CaptureVideoStartToolHandler.js +38 -0
- package/src/handlers/video/CaptureVideoStatusToolHandler.js +30 -0
- package/src/handlers/video/CaptureVideoStopToolHandler.js +32 -0
- package/src/lsp/CSharpLspUtils.js +134 -0
- package/src/lsp/LspProcessManager.js +60 -0
- package/src/lsp/LspRpcClient.js +133 -0
- package/src/tools/analysis/analyzeSceneContents.js +100 -0
- package/src/tools/analysis/findByComponent.js +87 -0
- package/src/tools/analysis/getAnimatorState.js +326 -0
- package/src/tools/analysis/getComponentValues.js +182 -0
- package/src/tools/analysis/getGameObjectDetails.js +159 -0
- package/src/tools/analysis/getInputActionsState.js +329 -0
- package/src/tools/analysis/getObjectReferences.js +86 -0
- package/src/tools/input/inputActionsEditor.js +556 -0
- package/src/tools/scene/createScene.js +112 -0
- package/src/tools/scene/getSceneInfo.js +95 -0
- package/src/tools/scene/listScenes.js +82 -0
- package/src/tools/scene/loadScene.js +122 -0
- package/src/tools/scene/saveScene.js +91 -0
- package/src/tools/system/ping.js +72 -0
- package/src/tools/video/recordFor.js +31 -0
- package/src/tools/video/recordPlayMode.js +61 -0
- package/src/utils/csharpParse.js +88 -0
- package/src/utils/validators.js +90 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shallow merge utility (simple objects only)
|
|
6
|
+
*/
|
|
7
|
+
function merge(a, b) {
|
|
8
|
+
const out = { ...a };
|
|
9
|
+
for (const [k, v] of Object.entries(b || {})) {
|
|
10
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && a[k] && typeof a[k] === 'object') {
|
|
11
|
+
out[k] = { ...a[k], ...v };
|
|
12
|
+
} else {
|
|
13
|
+
out[k] = v;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Base configuration for Unity Editor MCP Server
|
|
21
|
+
*/
|
|
22
|
+
const baseConfig = {
|
|
23
|
+
// Unity connection settings
|
|
24
|
+
unity: {
|
|
25
|
+
host: process.env.UNITY_HOST || 'localhost',
|
|
26
|
+
port: parseInt(process.env.UNITY_PORT || '', 10) || 6400,
|
|
27
|
+
reconnectDelay: 1000,
|
|
28
|
+
maxReconnectDelay: 30000,
|
|
29
|
+
reconnectBackoffMultiplier: 2,
|
|
30
|
+
commandTimeout: 30000,
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Server settings
|
|
34
|
+
server: {
|
|
35
|
+
name: 'unity-mcp-server',
|
|
36
|
+
version: '0.1.0',
|
|
37
|
+
description: 'MCP server for Unity Editor integration',
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// Logging settings
|
|
41
|
+
logging: {
|
|
42
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
43
|
+
prefix: '[Unity Editor MCP]',
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Write queue removed: all edits go through structured Roslyn tools.
|
|
47
|
+
|
|
48
|
+
// Search-related defaults and engine selection
|
|
49
|
+
search: {
|
|
50
|
+
// detail alias: 'compact' maps to returnMode 'snippets'
|
|
51
|
+
defaultDetail: (process.env.SEARCH_DEFAULT_DETAIL || 'compact').toLowerCase(), // compact|metadata|snippets|full
|
|
52
|
+
engine: (process.env.SEARCH_ENGINE || 'naive').toLowerCase(), // naive|treesitter (future)
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// LSP client defaults
|
|
56
|
+
lsp: {
|
|
57
|
+
requestTimeoutMs: Number(process.env.LSP_REQUEST_TIMEOUT_MS || 60000),
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// Indexing (code index) settings
|
|
61
|
+
indexing: {
|
|
62
|
+
// Enable periodic incremental index updates (polling watcher)
|
|
63
|
+
watch: (process.env.INDEX_WATCH || 'false').toLowerCase() === 'true',
|
|
64
|
+
// Polling interval (ms)
|
|
65
|
+
intervalMs: Number(process.env.INDEX_WATCH_INTERVAL_MS || 15000),
|
|
66
|
+
// Build options
|
|
67
|
+
concurrency: Number(process.env.INDEX_CONCURRENCY || 8),
|
|
68
|
+
retry: Number(process.env.INDEX_RETRY || 2),
|
|
69
|
+
reportEvery: Number(process.env.INDEX_REPORT_EVERY || 500),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* External config resolution (no legacy compatibility):
|
|
75
|
+
* Priority:
|
|
76
|
+
* 1) UNITY_MCP_CONFIG (explicit file path)
|
|
77
|
+
* 2) ./.unity/config.json (project-local)
|
|
78
|
+
* 3) ~/.unity/config.json (user-global)
|
|
79
|
+
* If none found, return {} and rely on env/defaults.
|
|
80
|
+
*/
|
|
81
|
+
function loadExternalConfig() {
|
|
82
|
+
const explicitPath = process.env.UNITY_MCP_CONFIG;
|
|
83
|
+
|
|
84
|
+
// Find nearest <workspace>/.unity/config.json by walking up from process.cwd()
|
|
85
|
+
const findProjectConfigUp = () => {
|
|
86
|
+
let dir = process.cwd();
|
|
87
|
+
let prev = '';
|
|
88
|
+
for (let i = 0; i < 3 && dir && dir !== prev; i++) {
|
|
89
|
+
const p = path.resolve(dir, '.unity', 'config.json');
|
|
90
|
+
if (fs.existsSync(p)) return p;
|
|
91
|
+
prev = dir;
|
|
92
|
+
dir = path.dirname(dir);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const projectPath = findProjectConfigUp();
|
|
98
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
99
|
+
const userPath = homeDir ? path.resolve(homeDir, '.unity', 'config.json') : null;
|
|
100
|
+
|
|
101
|
+
const candidates = [explicitPath, projectPath, userPath].filter(Boolean);
|
|
102
|
+
for (const p of candidates) {
|
|
103
|
+
try {
|
|
104
|
+
if (p && fs.existsSync(p)) {
|
|
105
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
106
|
+
const json = JSON.parse(raw);
|
|
107
|
+
const out = json && typeof json === 'object' ? json : {};
|
|
108
|
+
out.__configPath = p;
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return { __configLoadError: `${p}: ${e.message}` };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const external = loadExternalConfig();
|
|
119
|
+
export const config = merge(baseConfig, external);
|
|
120
|
+
|
|
121
|
+
// Workspace root detection: directory that contains .unity/config.json used
|
|
122
|
+
const initialCwd = process.cwd();
|
|
123
|
+
let workspaceRoot = initialCwd;
|
|
124
|
+
try {
|
|
125
|
+
if (config.__configPath) {
|
|
126
|
+
const cfgDir = path.dirname(config.__configPath); // <workspace>/.unity
|
|
127
|
+
workspaceRoot = path.dirname(cfgDir); // <workspace>
|
|
128
|
+
}
|
|
129
|
+
} catch {}
|
|
130
|
+
export const WORKSPACE_ROOT = workspaceRoot;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Logger utility
|
|
134
|
+
* IMPORTANT: In MCP servers, all stdout output must be JSON-RPC protocol messages.
|
|
135
|
+
* Logging must go to stderr to avoid breaking the protocol.
|
|
136
|
+
*/
|
|
137
|
+
export const logger = {
|
|
138
|
+
info: (message, ...args) => {
|
|
139
|
+
if (['info', 'debug'].includes(config.logging.level)) {
|
|
140
|
+
console.error(`${config.logging.prefix} ${message}`, ...args);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
warn: (message, ...args) => {
|
|
145
|
+
if (['info', 'debug', 'warn'].includes(config.logging.level)) {
|
|
146
|
+
console.error(`${config.logging.prefix} WARN: ${message}`, ...args);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
error: (message, ...args) => {
|
|
151
|
+
console.error(`${config.logging.prefix} ERROR: ${message}`, ...args);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
debug: (message, ...args) => {
|
|
155
|
+
if (config.logging.level === 'debug') {
|
|
156
|
+
console.error(`${config.logging.prefix} DEBUG: ${message}`, ...args);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Late log if external config failed to load
|
|
162
|
+
if (config.__configLoadError) {
|
|
163
|
+
console.error(`${baseConfig.logging.prefix} WARN: Failed to load external config: ${config.__configLoadError}`);
|
|
164
|
+
delete config.__configLoadError;
|
|
165
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { logger, config } from './config.js';
|
|
2
|
+
|
|
3
|
+
export class IndexWatcher {
|
|
4
|
+
constructor(unityConnection) {
|
|
5
|
+
this.unityConnection = unityConnection;
|
|
6
|
+
this.timer = null;
|
|
7
|
+
this.running = false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
start() {
|
|
11
|
+
if (!config.indexing?.watch) return;
|
|
12
|
+
if (this.timer) return;
|
|
13
|
+
const interval = Math.max(2000, Number(config.indexing.intervalMs || 15000));
|
|
14
|
+
logger.info(`[index] watcher enabled (interval=${interval}ms)`);
|
|
15
|
+
this.timer = setInterval(() => this.tick(), interval);
|
|
16
|
+
// Initial kick
|
|
17
|
+
this.tick();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
stop() {
|
|
21
|
+
if (this.timer) {
|
|
22
|
+
clearInterval(this.timer);
|
|
23
|
+
this.timer = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async tick() {
|
|
28
|
+
if (this.running) return;
|
|
29
|
+
this.running = true;
|
|
30
|
+
try {
|
|
31
|
+
const { BuildCodeIndexToolHandler } = await import('../handlers/script/BuildCodeIndexToolHandler.js');
|
|
32
|
+
const handler = new BuildCodeIndexToolHandler(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
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
logger.warn(`[index] update exception: ${e.message}`);
|
|
47
|
+
} finally {
|
|
48
|
+
this.running = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { logger, config } from './config.js';
|
|
4
|
+
|
|
5
|
+
// Lazy project info resolver. Prefers Unity via get_editor_info, otherwise infers by walking up for Assets/Packages.
|
|
6
|
+
export class ProjectInfoProvider {
|
|
7
|
+
constructor(unityConnection) {
|
|
8
|
+
this.unityConnection = unityConnection;
|
|
9
|
+
this.cached = null;
|
|
10
|
+
this.lastTried = 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async get() {
|
|
14
|
+
if (this.cached) return this.cached;
|
|
15
|
+
// Config-driven project root (no env fallback)
|
|
16
|
+
const cfgRoot = config?.project?.root;
|
|
17
|
+
if (cfgRoot) {
|
|
18
|
+
const projectRoot = cfgRoot.replace(/\\/g, '/');
|
|
19
|
+
const codeIndexRoot = (config?.project?.codeIndexRoot
|
|
20
|
+
|| path.join(projectRoot, 'Library/UnityMCP/CodeIndex')).replace(/\\/g, '/');
|
|
21
|
+
this.cached = {
|
|
22
|
+
projectRoot,
|
|
23
|
+
assetsPath: path.join(projectRoot, 'Assets').replace(/\\/g, '/'),
|
|
24
|
+
packagesPath: path.join(projectRoot, 'Packages').replace(/\\/g, '/'),
|
|
25
|
+
codeIndexRoot,
|
|
26
|
+
};
|
|
27
|
+
return this.cached;
|
|
28
|
+
}
|
|
29
|
+
// Try Unity if connected (rate-limit attempts)
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
if (this.unityConnection && this.unityConnection.isConnected() && (now - this.lastTried > 1000)) {
|
|
32
|
+
this.lastTried = now;
|
|
33
|
+
try {
|
|
34
|
+
const info = await this.unityConnection.sendCommand('get_editor_info', {});
|
|
35
|
+
if (info && info.projectRoot && info.assetsPath) {
|
|
36
|
+
this.cached = {
|
|
37
|
+
projectRoot: info.projectRoot,
|
|
38
|
+
assetsPath: info.assetsPath,
|
|
39
|
+
packagesPath: info.packagesPath || path.join(info.projectRoot, 'Packages').replace(/\\/g, '/'),
|
|
40
|
+
codeIndexRoot: info.codeIndexRoot || path.join(info.projectRoot, 'Library/UnityMCP/CodeIndex').replace(/\\/g, '/'),
|
|
41
|
+
};
|
|
42
|
+
return this.cached;
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
logger.warn(`get_editor_info failed: ${e.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Fallback: infer by walking up from CWD to find Assets/
|
|
49
|
+
const inferred = this.inferFromCwd();
|
|
50
|
+
if (inferred) {
|
|
51
|
+
this.cached = inferred;
|
|
52
|
+
return this.cached;
|
|
53
|
+
}
|
|
54
|
+
throw new Error('Unable to resolve Unity project root (no connection and no Assets/ found)');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
inferFromCwd() {
|
|
58
|
+
// First, check for Docker environment default path
|
|
59
|
+
const dockerDefaultPath = '/unity-mcp-server/UnityMCPServer';
|
|
60
|
+
const dockerAssets = path.join(dockerDefaultPath, 'Assets');
|
|
61
|
+
if (fs.existsSync(dockerAssets)) {
|
|
62
|
+
const projectRoot = dockerDefaultPath.replace(/\\/g, '/');
|
|
63
|
+
logger.info(`Found Unity project at Docker default path: ${projectRoot}`);
|
|
64
|
+
return {
|
|
65
|
+
projectRoot,
|
|
66
|
+
assetsPath: dockerAssets.replace(/\\/g, '/'),
|
|
67
|
+
packagesPath: path.join(dockerDefaultPath, 'Packages').replace(/\\/g, '/'),
|
|
68
|
+
codeIndexRoot: path.join(dockerDefaultPath, 'Library/UnityMCP/CodeIndex').replace(/\\/g, '/'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let dir = process.cwd();
|
|
73
|
+
// Walk up max 5 levels (look for Assets directly under a directory)
|
|
74
|
+
for (let i = 0; i < 5; i++) {
|
|
75
|
+
const assets = path.join(dir, 'Assets');
|
|
76
|
+
if (fs.existsSync(assets)) {
|
|
77
|
+
const projectRoot = dir.replace(/\\/g, '/');
|
|
78
|
+
return {
|
|
79
|
+
projectRoot,
|
|
80
|
+
assetsPath: assets.replace(/\\/g, '/'),
|
|
81
|
+
packagesPath: path.join(dir, 'Packages').replace(/\\/g, '/'),
|
|
82
|
+
codeIndexRoot: path.join(dir, 'Library/UnityMCP/CodeIndex').replace(/\\/g, '/'),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
dir = path.dirname(dir);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fallback: search shallow subdirectories for Unity projects (depth 2)
|
|
89
|
+
const rootsToCheck = [process.cwd(), path.dirname(process.cwd())];
|
|
90
|
+
for (const root of rootsToCheck) {
|
|
91
|
+
try {
|
|
92
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
93
|
+
for (const e of entries) {
|
|
94
|
+
if (!e.isDirectory()) continue;
|
|
95
|
+
const candidate = path.join(root, e.name);
|
|
96
|
+
const assets = path.join(candidate, 'Assets');
|
|
97
|
+
if (fs.existsSync(assets)) {
|
|
98
|
+
const projectRoot = candidate.replace(/\\/g, '/');
|
|
99
|
+
return {
|
|
100
|
+
projectRoot,
|
|
101
|
+
assetsPath: assets.replace(/\\/g, '/'),
|
|
102
|
+
packagesPath: path.join(candidate, 'Packages').replace(/\\/g, '/'),
|
|
103
|
+
codeIndexRoot: path.join(candidate, 'Library/UnityMCP/CodeIndex').replace(/\\/g, '/'),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import {
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListResourcesRequestSchema,
|
|
8
|
+
ListPromptsRequestSchema
|
|
9
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
// Note: filename is lowercase on disk; use exact casing for POSIX filesystems
|
|
11
|
+
import { UnityConnection } from './unityConnection.js';
|
|
12
|
+
import { createHandlers } from '../handlers/index.js';
|
|
13
|
+
import { config, logger } from './config.js';
|
|
14
|
+
import { IndexWatcher } from './indexWatcher.js';
|
|
15
|
+
|
|
16
|
+
// Create Unity connection
|
|
17
|
+
const unityConnection = new UnityConnection();
|
|
18
|
+
|
|
19
|
+
// Create tool handlers
|
|
20
|
+
const handlers = createHandlers(unityConnection);
|
|
21
|
+
|
|
22
|
+
// Create MCP server
|
|
23
|
+
const server = new Server(
|
|
24
|
+
{
|
|
25
|
+
name: config.server.name,
|
|
26
|
+
version: config.server.version,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
capabilities: {
|
|
30
|
+
// Explicitly advertise tool support; some MCP clients expect a non-empty object
|
|
31
|
+
// Setting listChanged enables future push updates if we emit notifications
|
|
32
|
+
tools: { listChanged: true },
|
|
33
|
+
resources: {},
|
|
34
|
+
prompts: {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Register MCP protocol handlers
|
|
40
|
+
// Note: Do not log here as it breaks MCP protocol initialization
|
|
41
|
+
|
|
42
|
+
// Handle tool listing
|
|
43
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
44
|
+
const tools = Array.from(handlers.values()).map((handler, index) => {
|
|
45
|
+
try {
|
|
46
|
+
const definition = handler.getDefinition();
|
|
47
|
+
// Validate inputSchema
|
|
48
|
+
if (definition.inputSchema && definition.inputSchema.type !== 'object') {
|
|
49
|
+
logger.error(`[MCP] Tool ${handler.name} (index ${index}) has invalid inputSchema type: ${definition.inputSchema.type}`);
|
|
50
|
+
}
|
|
51
|
+
return definition;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.error(`[MCP] Failed to get definition for handler ${handler.name}:`, error);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}).filter(tool => tool !== null);
|
|
57
|
+
|
|
58
|
+
logger.info(`[MCP] Returning ${tools.length} tool definitions`);
|
|
59
|
+
return { tools };
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Handle resources listing
|
|
63
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
64
|
+
logger.debug('[MCP] Received resources/list request');
|
|
65
|
+
// Unity MCP server doesn't provide resources
|
|
66
|
+
return { resources: [] };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Handle prompts listing
|
|
70
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
71
|
+
logger.debug('[MCP] Received prompts/list request');
|
|
72
|
+
// Unity MCP server doesn't provide prompts
|
|
73
|
+
return { prompts: [] };
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Handle tool execution
|
|
77
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
78
|
+
const { name, arguments: args } = request.params;
|
|
79
|
+
const requestTime = Date.now();
|
|
80
|
+
|
|
81
|
+
logger.info(`[MCP] Received tool call request: ${name} at ${new Date(requestTime).toISOString()}`, { args });
|
|
82
|
+
|
|
83
|
+
const handler = handlers.get(name);
|
|
84
|
+
if (!handler) {
|
|
85
|
+
logger.error(`[MCP] Tool not found: ${name}`);
|
|
86
|
+
throw new Error(`Tool not found: ${name}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
logger.info(`[MCP] Starting handler execution for: ${name} at ${new Date().toISOString()}`);
|
|
91
|
+
const startTime = Date.now();
|
|
92
|
+
|
|
93
|
+
// Handler returns response in our format
|
|
94
|
+
const result = await handler.handle(args);
|
|
95
|
+
|
|
96
|
+
const duration = Date.now() - startTime;
|
|
97
|
+
const totalDuration = Date.now() - requestTime;
|
|
98
|
+
logger.info(`[MCP] Handler completed at ${new Date().toISOString()}: ${name}`, {
|
|
99
|
+
handlerDuration: `${duration}ms`,
|
|
100
|
+
totalDuration: `${totalDuration}ms`,
|
|
101
|
+
status: result.status
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Convert to MCP format
|
|
105
|
+
if (result.status === 'error') {
|
|
106
|
+
logger.error(`[MCP] Handler returned error: ${name}`, { error: result.error, code: result.code });
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: `Error: ${result.error}\nCode: ${result.code || 'UNKNOWN_ERROR'}${result.details ? '\nDetails: ' + JSON.stringify(result.details, null, 2) : ''}`
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Success response
|
|
118
|
+
logger.info(`[MCP] Returning success response for: ${name} at ${new Date().toISOString()}`);
|
|
119
|
+
|
|
120
|
+
// Handle undefined or null results from handlers
|
|
121
|
+
let responseText;
|
|
122
|
+
if (result.result === undefined || result.result === null) {
|
|
123
|
+
responseText = JSON.stringify({
|
|
124
|
+
status: 'success',
|
|
125
|
+
message: 'Operation completed successfully but no details were returned',
|
|
126
|
+
tool: name
|
|
127
|
+
}, null, 2);
|
|
128
|
+
} else {
|
|
129
|
+
responseText = JSON.stringify(result.result, null, 2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: responseText
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
};
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const errorTime = Date.now();
|
|
142
|
+
logger.error(`[MCP] Handler threw exception at ${new Date(errorTime).toISOString()}: ${name}`, {
|
|
143
|
+
error: error.message,
|
|
144
|
+
stack: error.stack,
|
|
145
|
+
duration: `${errorTime - requestTime}ms`
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: 'text',
|
|
151
|
+
text: `Error: ${error.message}`
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Handle connection events
|
|
159
|
+
unityConnection.on('connected', () => {
|
|
160
|
+
logger.info('Unity connection established');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
unityConnection.on('disconnected', () => {
|
|
164
|
+
logger.info('Unity connection lost');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
unityConnection.on('error', (error) => {
|
|
168
|
+
logger.error('Unity connection error:', error.message);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Initialize server
|
|
172
|
+
async function main() {
|
|
173
|
+
try {
|
|
174
|
+
// Create transport - no logging before connection
|
|
175
|
+
const transport = new StdioServerTransport();
|
|
176
|
+
|
|
177
|
+
// Connect to transport
|
|
178
|
+
await server.connect(transport);
|
|
179
|
+
|
|
180
|
+
// Now safe to log after connection established
|
|
181
|
+
logger.info('MCP server started successfully');
|
|
182
|
+
|
|
183
|
+
// Attempt to connect to Unity
|
|
184
|
+
try {
|
|
185
|
+
await unityConnection.connect();
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.error('Initial Unity connection failed:', error.message);
|
|
188
|
+
logger.info('Unity connection will retry automatically');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Best-effort: prepare and start persistent C# LSP process (non-blocking)
|
|
192
|
+
;(async () => {
|
|
193
|
+
try {
|
|
194
|
+
const { LspProcessManager } = await import('../lsp/LspProcessManager.js');
|
|
195
|
+
const mgr = new LspProcessManager();
|
|
196
|
+
await mgr.ensureStarted();
|
|
197
|
+
// Attach graceful shutdown
|
|
198
|
+
const shutdown = async () => { try { await mgr.stop(3000); } catch {} };
|
|
199
|
+
process.on('SIGINT', shutdown);
|
|
200
|
+
process.on('SIGTERM', shutdown);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
logger.warn(`[startup] csharp-lsp start failed: ${e.message}`);
|
|
203
|
+
}
|
|
204
|
+
})();
|
|
205
|
+
|
|
206
|
+
// Start periodic index watcher (incremental)
|
|
207
|
+
const watcher = new IndexWatcher(unityConnection);
|
|
208
|
+
watcher.start();
|
|
209
|
+
const stopWatch = () => { try { watcher.stop(); } catch {} };
|
|
210
|
+
process.on('SIGINT', stopWatch);
|
|
211
|
+
process.on('SIGTERM', stopWatch);
|
|
212
|
+
|
|
213
|
+
// Handle shutdown
|
|
214
|
+
process.on('SIGINT', async () => {
|
|
215
|
+
logger.info('Shutting down...');
|
|
216
|
+
unityConnection.disconnect();
|
|
217
|
+
await server.close();
|
|
218
|
+
process.exit(0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
process.on('SIGTERM', async () => {
|
|
222
|
+
logger.info('Shutting down...');
|
|
223
|
+
unityConnection.disconnect();
|
|
224
|
+
await server.close();
|
|
225
|
+
process.exit(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('Failed to start server:', error);
|
|
230
|
+
console.error('Stack trace:', error.stack);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Export for testing
|
|
236
|
+
export async function createServer(customConfig = config) {
|
|
237
|
+
const testUnityConnection = new UnityConnection();
|
|
238
|
+
const testHandlers = createHandlers(testUnityConnection);
|
|
239
|
+
|
|
240
|
+
const testServer = new Server(
|
|
241
|
+
{
|
|
242
|
+
name: customConfig.server.name,
|
|
243
|
+
version: customConfig.server.version,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
capabilities: {
|
|
247
|
+
tools: { listChanged: true },
|
|
248
|
+
resources: {},
|
|
249
|
+
prompts: {}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Register handlers for test server
|
|
255
|
+
testServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
256
|
+
const tools = Array.from(testHandlers.values()).map(handler => handler.getDefinition());
|
|
257
|
+
return { tools };
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
testServer.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
261
|
+
return { resources: [] };
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
testServer.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
265
|
+
return { prompts: [] };
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
testServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
269
|
+
const { name, arguments: args } = request.params;
|
|
270
|
+
|
|
271
|
+
const handler = testHandlers.get(name);
|
|
272
|
+
if (!handler) {
|
|
273
|
+
return {
|
|
274
|
+
status: 'error',
|
|
275
|
+
error: `Tool not found: ${name}`,
|
|
276
|
+
code: 'TOOL_NOT_FOUND'
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return await handler.handle(args);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
server: testServer,
|
|
285
|
+
unityConnection: testUnityConnection
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Start the server
|
|
290
|
+
main().catch((error) => {
|
|
291
|
+
console.error('Fatal error:', error);
|
|
292
|
+
console.error('Stack trace:', error.stack);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
});
|