@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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/bin/unity-mcp-server +2 -0
  4. package/package.json +73 -0
  5. package/src/core/codeIndex.js +163 -0
  6. package/src/core/codeIndexDb.js +96 -0
  7. package/src/core/config.js +165 -0
  8. package/src/core/indexWatcher.js +52 -0
  9. package/src/core/projectInfo.js +111 -0
  10. package/src/core/server.js +294 -0
  11. package/src/core/unityConnection.js +426 -0
  12. package/src/handlers/analysis/AnalyzeSceneContentsToolHandler.js +35 -0
  13. package/src/handlers/analysis/FindByComponentToolHandler.js +20 -0
  14. package/src/handlers/analysis/GetAnimatorStateToolHandler.js +37 -0
  15. package/src/handlers/analysis/GetComponentValuesToolHandler.js +20 -0
  16. package/src/handlers/analysis/GetGameObjectDetailsToolHandler.js +35 -0
  17. package/src/handlers/analysis/GetInputActionsStateToolHandler.js +37 -0
  18. package/src/handlers/analysis/GetObjectReferencesToolHandler.js +20 -0
  19. package/src/handlers/asset/AssetDatabaseToolHandler.js +221 -0
  20. package/src/handlers/asset/AssetDependencyToolHandler.js +201 -0
  21. package/src/handlers/asset/AssetImportSettingsToolHandler.js +170 -0
  22. package/src/handlers/asset/CreateMaterialToolHandler.js +96 -0
  23. package/src/handlers/asset/CreatePrefabToolHandler.js +78 -0
  24. package/src/handlers/asset/ExitPrefabModeToolHandler.js +83 -0
  25. package/src/handlers/asset/InstantiatePrefabToolHandler.js +133 -0
  26. package/src/handlers/asset/ModifyMaterialToolHandler.js +76 -0
  27. package/src/handlers/asset/ModifyPrefabToolHandler.js +72 -0
  28. package/src/handlers/asset/OpenPrefabToolHandler.js +121 -0
  29. package/src/handlers/asset/SavePrefabToolHandler.js +106 -0
  30. package/src/handlers/base/BaseToolHandler.js +133 -0
  31. package/src/handlers/compilation/GetCompilationStateToolHandler.js +90 -0
  32. package/src/handlers/component/AddComponentToolHandler.js +126 -0
  33. package/src/handlers/component/GetComponentTypesToolHandler.js +100 -0
  34. package/src/handlers/component/ListComponentsToolHandler.js +85 -0
  35. package/src/handlers/component/ModifyComponentToolHandler.js +143 -0
  36. package/src/handlers/component/RemoveComponentToolHandler.js +108 -0
  37. package/src/handlers/console/ClearConsoleToolHandler.js +160 -0
  38. package/src/handlers/console/ReadConsoleToolHandler.js +276 -0
  39. package/src/handlers/editor/LayerManagementToolHandler.js +160 -0
  40. package/src/handlers/editor/SelectionToolHandler.js +141 -0
  41. package/src/handlers/editor/TagManagementToolHandler.js +129 -0
  42. package/src/handlers/editor/ToolManagementToolHandler.js +135 -0
  43. package/src/handlers/editor/WindowManagementToolHandler.js +125 -0
  44. package/src/handlers/gameobject/CreateGameObjectToolHandler.js +131 -0
  45. package/src/handlers/gameobject/DeleteGameObjectToolHandler.js +101 -0
  46. package/src/handlers/gameobject/FindGameObjectToolHandler.js +119 -0
  47. package/src/handlers/gameobject/GetHierarchyToolHandler.js +132 -0
  48. package/src/handlers/gameobject/ModifyGameObjectToolHandler.js +128 -0
  49. package/src/handlers/index.js +389 -0
  50. package/src/handlers/input/AddInputActionToolHandler.js +20 -0
  51. package/src/handlers/input/AddInputBindingToolHandler.js +20 -0
  52. package/src/handlers/input/CreateActionMapToolHandler.js +20 -0
  53. package/src/handlers/input/CreateCompositeBindingToolHandler.js +20 -0
  54. package/src/handlers/input/GamepadSimulationHandler.js +116 -0
  55. package/src/handlers/input/InputSystemHandler.js +80 -0
  56. package/src/handlers/input/KeyboardSimulationHandler.js +79 -0
  57. package/src/handlers/input/ManageControlSchemesToolHandler.js +20 -0
  58. package/src/handlers/input/MouseSimulationHandler.js +107 -0
  59. package/src/handlers/input/RemoveActionMapToolHandler.js +20 -0
  60. package/src/handlers/input/RemoveAllBindingsToolHandler.js +20 -0
  61. package/src/handlers/input/RemoveInputActionToolHandler.js +20 -0
  62. package/src/handlers/input/RemoveInputBindingToolHandler.js +20 -0
  63. package/src/handlers/input/TouchSimulationHandler.js +142 -0
  64. package/src/handlers/menu/ExecuteMenuItemToolHandler.js +304 -0
  65. package/src/handlers/package/PackageManagerToolHandler.js +248 -0
  66. package/src/handlers/package/RegistryConfigToolHandler.js +198 -0
  67. package/src/handlers/playmode/GetEditorStateToolHandler.js +81 -0
  68. package/src/handlers/playmode/PauseToolHandler.js +44 -0
  69. package/src/handlers/playmode/PlayToolHandler.js +91 -0
  70. package/src/handlers/playmode/StopToolHandler.js +77 -0
  71. package/src/handlers/playmode/WaitForEditorStateToolHandler.js +45 -0
  72. package/src/handlers/scene/CreateSceneToolHandler.js +91 -0
  73. package/src/handlers/scene/GetSceneInfoToolHandler.js +20 -0
  74. package/src/handlers/scene/ListScenesToolHandler.js +58 -0
  75. package/src/handlers/scene/LoadSceneToolHandler.js +92 -0
  76. package/src/handlers/scene/SaveSceneToolHandler.js +76 -0
  77. package/src/handlers/screenshot/AnalyzeScreenshotToolHandler.js +238 -0
  78. package/src/handlers/screenshot/CaptureScreenshotToolHandler.js +692 -0
  79. package/src/handlers/script/BuildCodeIndexToolHandler.js +163 -0
  80. package/src/handlers/script/ScriptCreateClassFileToolHandler.js +60 -0
  81. package/src/handlers/script/ScriptEditStructuredToolHandler.js +173 -0
  82. package/src/handlers/script/ScriptIndexStatusToolHandler.js +61 -0
  83. package/src/handlers/script/ScriptPackagesListToolHandler.js +103 -0
  84. package/src/handlers/script/ScriptReadToolHandler.js +106 -0
  85. package/src/handlers/script/ScriptRefactorRenameToolHandler.js +83 -0
  86. package/src/handlers/script/ScriptRefsFindToolHandler.js +144 -0
  87. package/src/handlers/script/ScriptRemoveSymbolToolHandler.js +79 -0
  88. package/src/handlers/script/ScriptSearchToolHandler.js +320 -0
  89. package/src/handlers/script/ScriptSymbolFindToolHandler.js +117 -0
  90. package/src/handlers/script/ScriptSymbolsGetToolHandler.js +96 -0
  91. package/src/handlers/settings/GetProjectSettingsToolHandler.js +161 -0
  92. package/src/handlers/settings/UpdateProjectSettingsToolHandler.js +272 -0
  93. package/src/handlers/system/GetCommandStatsToolHandler.js +25 -0
  94. package/src/handlers/system/PingToolHandler.js +53 -0
  95. package/src/handlers/system/RefreshAssetsToolHandler.js +45 -0
  96. package/src/handlers/ui/ClickUIElementToolHandler.js +110 -0
  97. package/src/handlers/ui/FindUIElementsToolHandler.js +63 -0
  98. package/src/handlers/ui/GetUIElementStateToolHandler.js +50 -0
  99. package/src/handlers/ui/SetUIElementValueToolHandler.js +49 -0
  100. package/src/handlers/ui/SimulateUIInputToolHandler.js +156 -0
  101. package/src/handlers/video/CaptureVideoForToolHandler.js +96 -0
  102. package/src/handlers/video/CaptureVideoStartToolHandler.js +38 -0
  103. package/src/handlers/video/CaptureVideoStatusToolHandler.js +30 -0
  104. package/src/handlers/video/CaptureVideoStopToolHandler.js +32 -0
  105. package/src/lsp/CSharpLspUtils.js +134 -0
  106. package/src/lsp/LspProcessManager.js +60 -0
  107. package/src/lsp/LspRpcClient.js +133 -0
  108. package/src/tools/analysis/analyzeSceneContents.js +100 -0
  109. package/src/tools/analysis/findByComponent.js +87 -0
  110. package/src/tools/analysis/getAnimatorState.js +326 -0
  111. package/src/tools/analysis/getComponentValues.js +182 -0
  112. package/src/tools/analysis/getGameObjectDetails.js +159 -0
  113. package/src/tools/analysis/getInputActionsState.js +329 -0
  114. package/src/tools/analysis/getObjectReferences.js +86 -0
  115. package/src/tools/input/inputActionsEditor.js +556 -0
  116. package/src/tools/scene/createScene.js +112 -0
  117. package/src/tools/scene/getSceneInfo.js +95 -0
  118. package/src/tools/scene/listScenes.js +82 -0
  119. package/src/tools/scene/loadScene.js +122 -0
  120. package/src/tools/scene/saveScene.js +91 -0
  121. package/src/tools/system/ping.js +72 -0
  122. package/src/tools/video/recordFor.js +31 -0
  123. package/src/tools/video/recordPlayMode.js +61 -0
  124. package/src/utils/csharpParse.js +88 -0
  125. 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
+ });