@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,426 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { config, logger } from './config.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manages TCP connection to Unity Editor
|
|
7
|
+
*/
|
|
8
|
+
export class UnityConnection extends EventEmitter {
|
|
9
|
+
constructor() {
|
|
10
|
+
super();
|
|
11
|
+
this.socket = null;
|
|
12
|
+
this.connected = false;
|
|
13
|
+
this.reconnectAttempts = 0;
|
|
14
|
+
this.reconnectTimer = null;
|
|
15
|
+
this.commandId = 0;
|
|
16
|
+
this.pendingCommands = new Map();
|
|
17
|
+
this.isDisconnecting = false;
|
|
18
|
+
this.messageBuffer = Buffer.alloc(0);
|
|
19
|
+
// Simple concurrency limiter and send queue to avoid flooding Unity
|
|
20
|
+
this.sendQueue = [];
|
|
21
|
+
this.inFlight = 0;
|
|
22
|
+
this.maxInFlight = 1; // process one command at a time by default
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Connects to Unity Editor
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
async connect() {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
if (this.connected) {
|
|
32
|
+
resolve();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Skip connection in CI/test environments
|
|
37
|
+
if (process.env.NODE_ENV === 'test' || process.env.CI === 'true') {
|
|
38
|
+
logger.info('Skipping Unity connection in test/CI environment');
|
|
39
|
+
reject(new Error('Unity connection disabled in test environment'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
logger.info(`Connecting to Unity at ${config.unity.host}:${config.unity.port}...`);
|
|
44
|
+
|
|
45
|
+
this.socket = new net.Socket();
|
|
46
|
+
let connectionTimeout = null;
|
|
47
|
+
let resolved = false;
|
|
48
|
+
|
|
49
|
+
// Helper to clean up the connection timeout
|
|
50
|
+
const clearConnectionTimeout = () => {
|
|
51
|
+
if (connectionTimeout) {
|
|
52
|
+
clearTimeout(connectionTimeout);
|
|
53
|
+
connectionTimeout = null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Set up event handlers
|
|
58
|
+
this.socket.on('connect', () => {
|
|
59
|
+
logger.info('Connected to Unity Editor');
|
|
60
|
+
this.connected = true;
|
|
61
|
+
this.reconnectAttempts = 0;
|
|
62
|
+
resolved = true;
|
|
63
|
+
clearConnectionTimeout();
|
|
64
|
+
this.emit('connected');
|
|
65
|
+
resolve();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
this.socket.on('data', (data) => {
|
|
69
|
+
this.handleData(data);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
this.socket.on('error', (error) => {
|
|
73
|
+
logger.error('Socket error:', error.message);
|
|
74
|
+
this.emit('error', error);
|
|
75
|
+
|
|
76
|
+
if (!this.connected && !resolved) {
|
|
77
|
+
resolved = true;
|
|
78
|
+
clearConnectionTimeout();
|
|
79
|
+
// Mark as disconnecting to prevent reconnection
|
|
80
|
+
this.isDisconnecting = true;
|
|
81
|
+
// Destroy the socket to clean up properly
|
|
82
|
+
this.socket.destroy();
|
|
83
|
+
this.isDisconnecting = false;
|
|
84
|
+
reject(error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.socket.on('close', () => {
|
|
89
|
+
// Clear the connection timeout when socket closes
|
|
90
|
+
clearConnectionTimeout();
|
|
91
|
+
|
|
92
|
+
// Check if we're already handling disconnection
|
|
93
|
+
if (this.isDisconnecting || !this.socket) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
logger.info('Disconnected from Unity Editor');
|
|
98
|
+
this.connected = false;
|
|
99
|
+
const wasSocket = this.socket;
|
|
100
|
+
this.socket = null;
|
|
101
|
+
|
|
102
|
+
// Clear message buffer
|
|
103
|
+
this.messageBuffer = Buffer.alloc(0);
|
|
104
|
+
|
|
105
|
+
// Clear pending commands
|
|
106
|
+
for (const [id, pending] of this.pendingCommands) {
|
|
107
|
+
pending.reject(new Error('Connection closed'));
|
|
108
|
+
}
|
|
109
|
+
this.pendingCommands.clear();
|
|
110
|
+
|
|
111
|
+
// Emit disconnected event
|
|
112
|
+
this.emit('disconnected');
|
|
113
|
+
|
|
114
|
+
// Attempt reconnection only if not intentionally disconnecting
|
|
115
|
+
if (!this.isDisconnecting && process.env.DISABLE_AUTO_RECONNECT !== 'true') {
|
|
116
|
+
this.scheduleReconnect();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Attempt connection
|
|
121
|
+
this.socket.connect(config.unity.port, config.unity.host);
|
|
122
|
+
|
|
123
|
+
// Set timeout for initial connection
|
|
124
|
+
connectionTimeout = setTimeout(() => {
|
|
125
|
+
if (!this.connected && !resolved && this.socket) {
|
|
126
|
+
resolved = true;
|
|
127
|
+
// Remove event listeners before destroying to prevent callbacks after timeout
|
|
128
|
+
this.socket.removeAllListeners();
|
|
129
|
+
this.socket.destroy();
|
|
130
|
+
reject(new Error('Connection timeout'));
|
|
131
|
+
}
|
|
132
|
+
}, config.unity.commandTimeout);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Disconnects from Unity Editor
|
|
138
|
+
*/
|
|
139
|
+
disconnect() {
|
|
140
|
+
this.isDisconnecting = true;
|
|
141
|
+
|
|
142
|
+
if (this.reconnectTimer) {
|
|
143
|
+
clearTimeout(this.reconnectTimer);
|
|
144
|
+
this.reconnectTimer = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.socket) {
|
|
148
|
+
try {
|
|
149
|
+
// Remove all listeners before destroying to prevent async callbacks
|
|
150
|
+
this.socket.removeAllListeners();
|
|
151
|
+
this.socket.destroy();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Ignore errors during cleanup
|
|
154
|
+
}
|
|
155
|
+
this.socket = null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.connected = false;
|
|
159
|
+
this.isDisconnecting = false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Schedules a reconnection attempt
|
|
164
|
+
*/
|
|
165
|
+
scheduleReconnect() {
|
|
166
|
+
if (this.reconnectTimer) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const delay = Math.min(
|
|
171
|
+
config.unity.reconnectDelay * Math.pow(config.unity.reconnectBackoffMultiplier, this.reconnectAttempts),
|
|
172
|
+
config.unity.maxReconnectDelay
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
logger.info(`Scheduling reconnection in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
|
|
176
|
+
|
|
177
|
+
this.reconnectTimer = setTimeout(() => {
|
|
178
|
+
this.reconnectTimer = null;
|
|
179
|
+
this.reconnectAttempts++;
|
|
180
|
+
this.connect().catch((error) => {
|
|
181
|
+
logger.error('Reconnection failed:', error.message);
|
|
182
|
+
});
|
|
183
|
+
}, delay);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handles incoming data from Unity
|
|
188
|
+
* @param {Buffer} data
|
|
189
|
+
*/
|
|
190
|
+
handleData(data) {
|
|
191
|
+
// Check if this is an unframed Unity debug log
|
|
192
|
+
if (data.length > 0 && !this.messageBuffer.length) {
|
|
193
|
+
const dataStr = data.toString('utf8');
|
|
194
|
+
if (dataStr.startsWith('[Unity Editor MCP]') || dataStr.startsWith('[Unity]')) {
|
|
195
|
+
logger.debug(`[Unity] Received unframed debug log: ${dataStr.trim()}`);
|
|
196
|
+
// Don't process unframed logs as messages
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Append new data to buffer
|
|
202
|
+
this.messageBuffer = Buffer.concat([this.messageBuffer, data]);
|
|
203
|
+
|
|
204
|
+
// Process complete messages
|
|
205
|
+
while (this.messageBuffer.length >= 4) {
|
|
206
|
+
// Read message length (first 4 bytes, big-endian)
|
|
207
|
+
const messageLength = this.messageBuffer.readInt32BE(0);
|
|
208
|
+
|
|
209
|
+
// Validate message length
|
|
210
|
+
if (messageLength < 0 || messageLength > 1024 * 1024) { // Max 1MB messages
|
|
211
|
+
logger.error(`[Unity] Invalid message length: ${messageLength}`);
|
|
212
|
+
|
|
213
|
+
// Try to recover by looking for valid framed message
|
|
214
|
+
// Look for a reasonable length value (positive, less than 10KB for typical responses)
|
|
215
|
+
let recoveryIndex = -1;
|
|
216
|
+
for (let i = 4; i < Math.min(this.messageBuffer.length - 4, 100); i++) {
|
|
217
|
+
const testLength = this.messageBuffer.readInt32BE(i);
|
|
218
|
+
if (testLength > 0 && testLength < 10240) {
|
|
219
|
+
// Check if this could be a valid JSON message
|
|
220
|
+
if (i + 4 + testLength <= this.messageBuffer.length) {
|
|
221
|
+
const testData = this.messageBuffer.slice(i + 4, i + 4 + testLength).toString('utf8');
|
|
222
|
+
if (testData.trim().startsWith('{')) {
|
|
223
|
+
recoveryIndex = i;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (recoveryIndex > 0) {
|
|
231
|
+
logger.warn(`[Unity] Discarding ${recoveryIndex} bytes of invalid data`);
|
|
232
|
+
this.messageBuffer = this.messageBuffer.slice(recoveryIndex);
|
|
233
|
+
continue;
|
|
234
|
+
} else {
|
|
235
|
+
// Can't recover, clear buffer
|
|
236
|
+
logger.error('[Unity] Unable to recover from invalid frame, clearing buffer');
|
|
237
|
+
this.messageBuffer = Buffer.alloc(0);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if we have the complete message
|
|
243
|
+
if (this.messageBuffer.length >= 4 + messageLength) {
|
|
244
|
+
// Extract message
|
|
245
|
+
const messageData = this.messageBuffer.slice(4, 4 + messageLength);
|
|
246
|
+
this.messageBuffer = this.messageBuffer.slice(4 + messageLength);
|
|
247
|
+
|
|
248
|
+
// Process the message
|
|
249
|
+
try {
|
|
250
|
+
const message = messageData.toString('utf8');
|
|
251
|
+
|
|
252
|
+
// Skip non-JSON messages (like debug logs)
|
|
253
|
+
if (!message.trim().startsWith('{')) {
|
|
254
|
+
logger.warn(`[Unity] Skipping non-JSON message: ${message.substring(0, 50)}...`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
logger.debug(`[Unity] Received framed message (length=${message.length})`);
|
|
259
|
+
|
|
260
|
+
const response = JSON.parse(message);
|
|
261
|
+
logger.debug(`[Unity] Parsed response id=${response.id || 'n/a'} status=${response.status || (response.success === false ? 'error' : 'success')}`);
|
|
262
|
+
|
|
263
|
+
// Check if this is a response to a pending command
|
|
264
|
+
if (response.id && this.pendingCommands.has(response.id)) {
|
|
265
|
+
logger.info(`[Unity] Found pending command for ID ${response.id}`);
|
|
266
|
+
const pending = this.pendingCommands.get(response.id);
|
|
267
|
+
this.pendingCommands.delete(response.id);
|
|
268
|
+
|
|
269
|
+
// Handle both old and new response formats
|
|
270
|
+
if (response.status === 'success' || response.success === true) {
|
|
271
|
+
logger.info(`[Unity] Command ${response.id} succeeded`);
|
|
272
|
+
|
|
273
|
+
let result = response.result || response.data || {};
|
|
274
|
+
|
|
275
|
+
// If result is a string, try to parse it as JSON
|
|
276
|
+
if (typeof result === 'string') {
|
|
277
|
+
try {
|
|
278
|
+
result = JSON.parse(result);
|
|
279
|
+
logger.info(`[Unity] Parsed string result as JSON:`, result);
|
|
280
|
+
} catch (parseError) {
|
|
281
|
+
logger.warn(`[Unity] Failed to parse result as JSON: ${parseError.message}`);
|
|
282
|
+
// Keep the original string value
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Include version and editorState information if available
|
|
287
|
+
if (response.version) {
|
|
288
|
+
result._version = response.version;
|
|
289
|
+
}
|
|
290
|
+
if (response.editorState) {
|
|
291
|
+
result._editorState = response.editorState;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
logger.info(`[Unity] Command ${response.id} resolved successfully`);
|
|
295
|
+
pending.resolve(result);
|
|
296
|
+
} else if (response.status === 'error' || response.success === false) {
|
|
297
|
+
logger.error(`[Unity] Command ${response.id} failed:`, response.error);
|
|
298
|
+
pending.reject(new Error(response.error || 'Command failed'));
|
|
299
|
+
} else {
|
|
300
|
+
// Unknown format
|
|
301
|
+
logger.warn(`[Unity] Command ${response.id} has unknown response format`);
|
|
302
|
+
pending.resolve(response);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
// Handle unsolicited messages
|
|
306
|
+
logger.debug(`[Unity] Received unsolicited message id=${response.id || 'n/a'}`);
|
|
307
|
+
this.emit('message', response);
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
logger.error('[Unity] Failed to parse response:', error.message);
|
|
311
|
+
logger.debug(`[Unity] Raw message: ${messageData.toString().substring(0, 200)}...`);
|
|
312
|
+
|
|
313
|
+
// Check if this looks like a Unity log message
|
|
314
|
+
const messageStr = messageData.toString();
|
|
315
|
+
if (messageStr.includes('[Unity Editor MCP]')) {
|
|
316
|
+
logger.debug('[Unity] Received Unity log message instead of JSON response');
|
|
317
|
+
// Don't treat this as a critical error
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
// Not enough data yet, wait for more
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Sends a command to Unity
|
|
329
|
+
* @param {string} type - Command type
|
|
330
|
+
* @param {object} params - Command parameters
|
|
331
|
+
* @returns {Promise<any>} - Response from Unity
|
|
332
|
+
*/
|
|
333
|
+
async sendCommand(type, params = {}) {
|
|
334
|
+
logger.info(`[Unity] enqueue sendCommand: ${type}`, { connected: this.connected });
|
|
335
|
+
|
|
336
|
+
if (!this.connected) {
|
|
337
|
+
logger.error('[Unity] Cannot send command - not connected');
|
|
338
|
+
throw new Error('Not connected to Unity');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Create an external promise that will resolve when Unity responds
|
|
342
|
+
return new Promise((outerResolve, outerReject) => {
|
|
343
|
+
const task = { type, params, outerResolve, outerReject };
|
|
344
|
+
this.sendQueue.push(task);
|
|
345
|
+
this._pumpQueue();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_pumpQueue() {
|
|
350
|
+
if (!this.connected) return;
|
|
351
|
+
if (this.inFlight >= this.maxInFlight) return;
|
|
352
|
+
const task = this.sendQueue.shift();
|
|
353
|
+
if (!task) return;
|
|
354
|
+
|
|
355
|
+
const id = String(++this.commandId);
|
|
356
|
+
const command = { id, type: task.type, params: task.params };
|
|
357
|
+
const json = JSON.stringify(command);
|
|
358
|
+
const messageBuffer = Buffer.from(json, 'utf8');
|
|
359
|
+
const lengthBuffer = Buffer.allocUnsafe(4);
|
|
360
|
+
lengthBuffer.writeInt32BE(messageBuffer.length, 0);
|
|
361
|
+
const framedMessage = Buffer.concat([lengthBuffer, messageBuffer]);
|
|
362
|
+
|
|
363
|
+
this.inFlight++;
|
|
364
|
+
logger.info(`[Unity] Dispatching command ${id}: ${task.type}`);
|
|
365
|
+
|
|
366
|
+
// Set up timeout only when actually dispatched
|
|
367
|
+
const timeout = setTimeout(() => {
|
|
368
|
+
logger.error(`[Unity] Command ${id} timed out after ${config.unity.commandTimeout}ms`);
|
|
369
|
+
this.pendingCommands.delete(id);
|
|
370
|
+
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
371
|
+
task.outerReject(new Error('Command timeout'));
|
|
372
|
+
this._pumpQueue();
|
|
373
|
+
}, config.unity.commandTimeout);
|
|
374
|
+
|
|
375
|
+
// Store pending with wrappers to manage queue progression
|
|
376
|
+
this.pendingCommands.set(id, {
|
|
377
|
+
resolve: (data) => {
|
|
378
|
+
logger.info(`[Unity] Command ${id} resolved`);
|
|
379
|
+
clearTimeout(timeout);
|
|
380
|
+
try { task.outerResolve(data); } finally {
|
|
381
|
+
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
382
|
+
this._pumpQueue();
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
reject: (error) => {
|
|
386
|
+
logger.error(`[Unity] Command ${id} rejected: ${error.message}`);
|
|
387
|
+
clearTimeout(timeout);
|
|
388
|
+
try { task.outerReject(error); } finally {
|
|
389
|
+
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
390
|
+
this._pumpQueue();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Send framed message
|
|
396
|
+
this.socket.write(framedMessage, (error) => {
|
|
397
|
+
if (error) {
|
|
398
|
+
logger.error(`[Unity] Failed to write command ${id}: ${error.message}`);
|
|
399
|
+
clearTimeout(timeout);
|
|
400
|
+
this.pendingCommands.delete(id);
|
|
401
|
+
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
402
|
+
task.outerReject(error);
|
|
403
|
+
this._pumpQueue();
|
|
404
|
+
} else {
|
|
405
|
+
logger.debug(`[Unity] Command ${id} written; awaiting response`);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Sends a ping command to Unity
|
|
412
|
+
* @returns {Promise<any>}
|
|
413
|
+
*/
|
|
414
|
+
async ping() {
|
|
415
|
+
// Use normal command sending for ping with proper framing
|
|
416
|
+
return this.sendCommand('ping', {});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Checks if connected to Unity
|
|
421
|
+
* @returns {boolean}
|
|
422
|
+
*/
|
|
423
|
+
isConnected() {
|
|
424
|
+
return this.connected;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { analyzeSceneContentsToolDefinition, analyzeSceneContentsHandler } from '../../tools/analysis/analyzeSceneContents.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handler for analyze_scene_contents tool
|
|
6
|
+
*/
|
|
7
|
+
export class AnalyzeSceneContentsToolHandler extends BaseToolHandler {
|
|
8
|
+
constructor(unityConnection) {
|
|
9
|
+
super(
|
|
10
|
+
analyzeSceneContentsToolDefinition.name,
|
|
11
|
+
analyzeSceneContentsToolDefinition.description,
|
|
12
|
+
analyzeSceneContentsToolDefinition.inputSchema
|
|
13
|
+
);
|
|
14
|
+
this.unityConnection = unityConnection;
|
|
15
|
+
this.handler = analyzeSceneContentsHandler;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async execute(args) {
|
|
19
|
+
// Check connection
|
|
20
|
+
if (!this.unityConnection.isConnected()) {
|
|
21
|
+
throw new Error('Unity connection not available');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Use the handler function
|
|
25
|
+
const result = await this.handler(this.unityConnection, args);
|
|
26
|
+
|
|
27
|
+
// If the handler returns an error response, throw it
|
|
28
|
+
if (result.isError) {
|
|
29
|
+
throw new Error(result.content[0].text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Return the content
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { findByComponentToolDefinition, findByComponentHandler } from '../../tools/analysis/findByComponent.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handler for the find_by_component tool
|
|
6
|
+
*/
|
|
7
|
+
export class FindByComponentToolHandler extends BaseToolHandler {
|
|
8
|
+
constructor(unityConnection) {
|
|
9
|
+
super(
|
|
10
|
+
findByComponentToolDefinition.name,
|
|
11
|
+
findByComponentToolDefinition.description,
|
|
12
|
+
findByComponentToolDefinition.inputSchema
|
|
13
|
+
);
|
|
14
|
+
this.unityConnection = unityConnection;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async execute(args) {
|
|
18
|
+
return findByComponentHandler(this.unityConnection, args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import {
|
|
3
|
+
getAnimatorStateToolDefinition,
|
|
4
|
+
getAnimatorRuntimeInfoToolDefinition,
|
|
5
|
+
getAnimatorStateHandler,
|
|
6
|
+
getAnimatorRuntimeInfoHandler
|
|
7
|
+
} from '../../tools/analysis/getAnimatorState.js';
|
|
8
|
+
|
|
9
|
+
export class GetAnimatorStateToolHandler extends BaseToolHandler {
|
|
10
|
+
constructor(unityConnection) {
|
|
11
|
+
super(
|
|
12
|
+
getAnimatorStateToolDefinition.name,
|
|
13
|
+
getAnimatorStateToolDefinition.description,
|
|
14
|
+
getAnimatorStateToolDefinition.inputSchema
|
|
15
|
+
);
|
|
16
|
+
this.unityConnection = unityConnection;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async execute(args) {
|
|
20
|
+
return getAnimatorStateHandler(this.unityConnection, args);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class GetAnimatorRuntimeInfoToolHandler extends BaseToolHandler {
|
|
25
|
+
constructor(unityConnection) {
|
|
26
|
+
super(
|
|
27
|
+
getAnimatorRuntimeInfoToolDefinition.name,
|
|
28
|
+
getAnimatorRuntimeInfoToolDefinition.description,
|
|
29
|
+
getAnimatorRuntimeInfoToolDefinition.inputSchema
|
|
30
|
+
);
|
|
31
|
+
this.unityConnection = unityConnection;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async execute(args) {
|
|
35
|
+
return getAnimatorRuntimeInfoHandler(this.unityConnection, args);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { getComponentValuesToolDefinition, getComponentValuesHandler } from '../../tools/analysis/getComponentValues.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handler for the get_component_values tool
|
|
6
|
+
*/
|
|
7
|
+
export class GetComponentValuesToolHandler extends BaseToolHandler {
|
|
8
|
+
constructor(unityConnection) {
|
|
9
|
+
super(
|
|
10
|
+
getComponentValuesToolDefinition.name,
|
|
11
|
+
getComponentValuesToolDefinition.description,
|
|
12
|
+
getComponentValuesToolDefinition.inputSchema
|
|
13
|
+
);
|
|
14
|
+
this.unityConnection = unityConnection;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async execute(args) {
|
|
18
|
+
return getComponentValuesHandler(this.unityConnection, args);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { getGameObjectDetailsToolDefinition, getGameObjectDetailsHandler } from '../../tools/analysis/getGameObjectDetails.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handler for get_gameobject_details tool
|
|
6
|
+
*/
|
|
7
|
+
export class GetGameObjectDetailsToolHandler extends BaseToolHandler {
|
|
8
|
+
constructor(unityConnection) {
|
|
9
|
+
super(
|
|
10
|
+
getGameObjectDetailsToolDefinition.name,
|
|
11
|
+
getGameObjectDetailsToolDefinition.description,
|
|
12
|
+
getGameObjectDetailsToolDefinition.inputSchema
|
|
13
|
+
);
|
|
14
|
+
this.unityConnection = unityConnection;
|
|
15
|
+
this.handler = getGameObjectDetailsHandler;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async execute(args) {
|
|
19
|
+
// Check connection
|
|
20
|
+
if (!this.unityConnection.isConnected()) {
|
|
21
|
+
throw new Error('Unity connection not available');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Use the handler function
|
|
25
|
+
const result = await this.handler(this.unityConnection, args);
|
|
26
|
+
|
|
27
|
+
// If the handler returns an error response, throw it
|
|
28
|
+
if (result.isError) {
|
|
29
|
+
throw new Error(result.content[0].text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Return the content
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import {
|
|
3
|
+
getInputActionsStateToolDefinition,
|
|
4
|
+
analyzeInputActionsAssetToolDefinition,
|
|
5
|
+
getInputActionsStateHandler,
|
|
6
|
+
analyzeInputActionsAssetHandler
|
|
7
|
+
} from '../../tools/analysis/getInputActionsState.js';
|
|
8
|
+
|
|
9
|
+
export class GetInputActionsStateToolHandler extends BaseToolHandler {
|
|
10
|
+
constructor(unityConnection) {
|
|
11
|
+
super(
|
|
12
|
+
getInputActionsStateToolDefinition.name,
|
|
13
|
+
getInputActionsStateToolDefinition.description,
|
|
14
|
+
getInputActionsStateToolDefinition.inputSchema
|
|
15
|
+
);
|
|
16
|
+
this.unityConnection = unityConnection;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async execute(args) {
|
|
20
|
+
return getInputActionsStateHandler(this.unityConnection, args);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class AnalyzeInputActionsAssetToolHandler extends BaseToolHandler {
|
|
25
|
+
constructor(unityConnection) {
|
|
26
|
+
super(
|
|
27
|
+
analyzeInputActionsAssetToolDefinition.name,
|
|
28
|
+
analyzeInputActionsAssetToolDefinition.description,
|
|
29
|
+
analyzeInputActionsAssetToolDefinition.inputSchema
|
|
30
|
+
);
|
|
31
|
+
this.unityConnection = unityConnection;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async execute(args) {
|
|
35
|
+
return analyzeInputActionsAssetHandler(this.unityConnection, args);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
+
import { getObjectReferencesToolDefinition, getObjectReferencesHandler } from '../../tools/analysis/getObjectReferences.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handler for the get_object_references tool
|
|
6
|
+
*/
|
|
7
|
+
export class GetObjectReferencesToolHandler extends BaseToolHandler {
|
|
8
|
+
constructor(unityConnection) {
|
|
9
|
+
super(
|
|
10
|
+
getObjectReferencesToolDefinition.name,
|
|
11
|
+
getObjectReferencesToolDefinition.description,
|
|
12
|
+
getObjectReferencesToolDefinition.inputSchema
|
|
13
|
+
);
|
|
14
|
+
this.unityConnection = unityConnection;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async execute(args) {
|
|
18
|
+
return getObjectReferencesHandler(this.unityConnection, args);
|
|
19
|
+
}
|
|
20
|
+
}
|