unreal-engine-mcp-server 0.2.1

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 (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. package/tsconfig.json +33 -0
@@ -0,0 +1,1093 @@
1
+ // Consolidated tool handlers - maps 10 tools to all 36 operations
2
+ import { handleToolCall } from './tool-handlers.js';
3
+ import { cleanObject } from '../utils/safe-json.js';
4
+
5
+ export async function handleConsolidatedToolCall(
6
+ name: string,
7
+ args: any,
8
+ tools: any
9
+ ) {
10
+ const startTime = Date.now();
11
+ console.log(`[ConsolidatedToolHandler] Starting execution of ${name} at ${new Date().toISOString()}`);
12
+
13
+ try {
14
+ // Validate args is not null/undefined
15
+ if (args === null || args === undefined) {
16
+ throw new Error('Invalid arguments: null or undefined');
17
+ }
18
+
19
+ let mappedName: string;
20
+ let mappedArgs: any = { ...args };
21
+
22
+ switch (name) {
23
+ // 1. ASSET MANAGER
24
+ case 'manage_asset':
25
+ // Validate args is not null/undefined
26
+ if (args === null || args === undefined) {
27
+ throw new Error('Invalid arguments: null or undefined');
28
+ }
29
+
30
+ // Validate action exists
31
+ if (!args.action) {
32
+ throw new Error('Missing required parameter: action');
33
+ }
34
+
35
+ switch (args.action) {
36
+ case 'list':
37
+ // Directory is optional
38
+ if (args.directory !== undefined && args.directory !== null) {
39
+ if (typeof args.directory !== 'string') {
40
+ throw new Error('Invalid directory: must be a string');
41
+ }
42
+ }
43
+
44
+ mappedName = 'list_assets';
45
+ mappedArgs = {
46
+ directory: args.directory
47
+ // recursive removed - always false internally
48
+ };
49
+ break;
50
+
51
+ case 'import':
52
+ // Validate required parameters
53
+ if (args.sourcePath === undefined || args.sourcePath === null) {
54
+ throw new Error('Missing required parameter: sourcePath');
55
+ }
56
+ if (typeof args.sourcePath !== 'string') {
57
+ throw new Error('Invalid sourcePath: must be a string');
58
+ }
59
+ if (args.sourcePath.trim() === '') {
60
+ throw new Error('Invalid sourcePath: cannot be empty');
61
+ }
62
+
63
+ if (args.destinationPath === undefined || args.destinationPath === null) {
64
+ throw new Error('Missing required parameter: destinationPath');
65
+ }
66
+ if (typeof args.destinationPath !== 'string') {
67
+ throw new Error('Invalid destinationPath: must be a string');
68
+ }
69
+ if (args.destinationPath.trim() === '') {
70
+ throw new Error('Invalid destinationPath: cannot be empty');
71
+ }
72
+
73
+ mappedName = 'import_asset';
74
+ mappedArgs = {
75
+ sourcePath: args.sourcePath,
76
+ destinationPath: args.destinationPath
77
+ };
78
+ break;
79
+
80
+ case 'create_material':
81
+ // Validate required parameters
82
+ if (args.name === undefined || args.name === null) {
83
+ throw new Error('Missing required parameter: name');
84
+ }
85
+ if (typeof args.name !== 'string') {
86
+ throw new Error('Invalid name: must be a string');
87
+ }
88
+ if (args.name.trim() === '') {
89
+ throw new Error('Invalid name: cannot be empty');
90
+ }
91
+
92
+ // Path is optional
93
+ if (args.path !== undefined && args.path !== null) {
94
+ if (typeof args.path !== 'string') {
95
+ throw new Error('Invalid path: must be a string');
96
+ }
97
+ }
98
+
99
+ mappedName = 'create_material';
100
+ mappedArgs = {
101
+ name: args.name,
102
+ path: args.path
103
+ };
104
+ break;
105
+
106
+ default:
107
+ throw new Error(`Unknown asset action: ${args.action}`);
108
+ }
109
+ break;
110
+
111
+ // 2. ACTOR CONTROL
112
+ case 'control_actor':
113
+ // Validate action exists
114
+ if (!args.action) {
115
+ throw new Error('Missing required parameter: action');
116
+ }
117
+
118
+ switch (args.action) {
119
+ case 'spawn':
120
+ // Validate spawn parameters
121
+ if (!args.classPath) {
122
+ throw new Error('Missing required parameter: classPath');
123
+ }
124
+ if (typeof args.classPath !== 'string' || args.classPath.trim() === '') {
125
+ throw new Error('Invalid classPath: must be a non-empty string');
126
+ }
127
+
128
+ mappedName = 'spawn_actor';
129
+ mappedArgs = {
130
+ classPath: args.classPath,
131
+ location: args.location,
132
+ rotation: args.rotation
133
+ };
134
+ break;
135
+
136
+ case 'delete':
137
+ // Validate delete parameters
138
+ if (!args.actorName) {
139
+ throw new Error('Missing required parameter: actorName');
140
+ }
141
+ if (typeof args.actorName !== 'string' || args.actorName.trim() === '') {
142
+ throw new Error('Invalid actorName: must be a non-empty string');
143
+ }
144
+
145
+ mappedName = 'delete_actor';
146
+ mappedArgs = {
147
+ actorName: args.actorName
148
+ };
149
+ break;
150
+
151
+ case 'apply_force':
152
+ // Validate apply_force parameters
153
+ if (!args.actorName) {
154
+ throw new Error('Missing required parameter: actorName');
155
+ }
156
+ if (typeof args.actorName !== 'string' || args.actorName.trim() === '') {
157
+ throw new Error('Invalid actorName: must be a non-empty string');
158
+ }
159
+ if (!args.force) {
160
+ throw new Error('Missing required parameter: force');
161
+ }
162
+ if (typeof args.force !== 'object' || args.force === null) {
163
+ throw new Error('Invalid force: must be an object with x, y, z properties');
164
+ }
165
+ if (typeof args.force.x !== 'number' ||
166
+ typeof args.force.y !== 'number' ||
167
+ typeof args.force.z !== 'number') {
168
+ throw new Error('Invalid force: x, y, z must all be numbers');
169
+ }
170
+
171
+ mappedName = 'apply_force';
172
+ mappedArgs = {
173
+ actorName: args.actorName,
174
+ force: args.force
175
+ };
176
+ break;
177
+
178
+ default:
179
+ throw new Error(`Unknown actor action: ${args.action}`);
180
+ }
181
+ break;
182
+
183
+ // 3. EDITOR CONTROL
184
+ case 'control_editor':
185
+ // Validate action exists
186
+ if (!args.action) {
187
+ throw new Error('Missing required parameter: action');
188
+ }
189
+
190
+ switch (args.action) {
191
+ case 'play':
192
+ mappedName = 'play_in_editor';
193
+ mappedArgs = {};
194
+ break;
195
+ case 'stop':
196
+ mappedName = 'stop_play_in_editor';
197
+ mappedArgs = {};
198
+ break;
199
+ case 'pause':
200
+ mappedName = 'pause_play_in_editor';
201
+ mappedArgs = {};
202
+ break;
203
+ case 'set_game_speed':
204
+ // Validate game speed parameter
205
+ if (args.speed === undefined || args.speed === null) {
206
+ throw new Error('Missing required parameter: speed');
207
+ }
208
+ if (typeof args.speed !== 'number') {
209
+ throw new Error('Invalid speed: must be a number');
210
+ }
211
+ if (isNaN(args.speed)) {
212
+ throw new Error('Invalid speed: cannot be NaN');
213
+ }
214
+ if (!isFinite(args.speed)) {
215
+ throw new Error('Invalid speed: must be finite');
216
+ }
217
+ if (args.speed <= 0) {
218
+ throw new Error('Invalid speed: must be positive');
219
+ }
220
+ mappedName = 'set_game_speed';
221
+ mappedArgs = {
222
+ speed: args.speed
223
+ };
224
+ break;
225
+ case 'eject':
226
+ mappedName = 'eject_from_pawn';
227
+ mappedArgs = {};
228
+ break;
229
+ case 'possess':
230
+ mappedName = 'possess_pawn';
231
+ mappedArgs = {};
232
+ break;
233
+ case 'set_camera':
234
+ // Allow either location or rotation or both
235
+ // Don't require both to be present
236
+ mappedName = 'set_camera';
237
+ mappedArgs = {
238
+ location: args.location,
239
+ rotation: args.rotation
240
+ };
241
+ break;
242
+ case 'set_view_mode':
243
+ // Validate view mode parameter
244
+ if (!args.viewMode) {
245
+ throw new Error('Missing required parameter: viewMode');
246
+ }
247
+ if (typeof args.viewMode !== 'string' || args.viewMode.trim() === '') {
248
+ throw new Error('Invalid viewMode: must be a non-empty string');
249
+ }
250
+
251
+ // Normalize view mode to match what debug.ts expects
252
+ const validModes = ['lit', 'unlit', 'wireframe', 'detail_lighting', 'lighting_only',
253
+ 'light_complexity', 'shader_complexity', 'lightmap_density',
254
+ 'stationary_light_overlap', 'reflections', 'visualize_buffer',
255
+ 'collision_pawn', 'collision_visibility', 'lod_coloration', 'quad_overdraw'];
256
+ const normalizedMode = args.viewMode.toLowerCase().replace(/_/g, '');
257
+
258
+ // Map to proper case for debug.ts
259
+ let mappedMode = '';
260
+ switch(normalizedMode) {
261
+ case 'lit': mappedMode = 'Lit'; break;
262
+ case 'unlit': mappedMode = 'Unlit'; break;
263
+ case 'wireframe': mappedMode = 'Wireframe'; break;
264
+ case 'detaillighting': mappedMode = 'DetailLighting'; break;
265
+ case 'lightingonly': mappedMode = 'LightingOnly'; break;
266
+ case 'lightcomplexity': mappedMode = 'LightComplexity'; break;
267
+ case 'shadercomplexity': mappedMode = 'ShaderComplexity'; break;
268
+ case 'lightmapdensity': mappedMode = 'LightmapDensity'; break;
269
+ case 'stationarylightoverlap': mappedMode = 'StationaryLightOverlap'; break;
270
+ case 'reflections': mappedMode = 'ReflectionOverride'; break;
271
+ case 'visualizebuffer': mappedMode = 'VisualizeBuffer'; break;
272
+ case 'collisionpawn': mappedMode = 'CollisionPawn'; break;
273
+ case 'collisionvisibility': mappedMode = 'CollisionVisibility'; break;
274
+ case 'lodcoloration': mappedMode = 'LODColoration'; break;
275
+ case 'quadoverdraw': mappedMode = 'QuadOverdraw'; break;
276
+ default:
277
+ throw new Error(`Invalid viewMode: '${args.viewMode}'. Valid modes are: ${validModes.join(', ')}`);
278
+ }
279
+
280
+ mappedName = 'set_view_mode';
281
+ mappedArgs = {
282
+ mode: mappedMode
283
+ };
284
+ break;
285
+ default:
286
+ throw new Error(`Unknown editor action: ${args.action}`);
287
+ }
288
+ break;
289
+
290
+ // 4. LEVEL MANAGER
291
+ case 'manage_level':
292
+ switch (args.action) {
293
+ case 'load':
294
+ mappedName = 'load_level';
295
+ mappedArgs = {
296
+ levelPath: args.levelPath
297
+ };
298
+ if (args.streaming !== undefined) {
299
+ mappedArgs.streaming = args.streaming;
300
+ }
301
+ break;
302
+ case 'save':
303
+ mappedName = 'save_level';
304
+ mappedArgs = {
305
+ levelName: args.levelName
306
+ };
307
+ if (args.savePath !== undefined) {
308
+ mappedArgs.savePath = args.savePath;
309
+ }
310
+ break;
311
+ case 'stream':
312
+ mappedName = 'stream_level';
313
+ mappedArgs = {
314
+ levelName: args.levelName,
315
+ shouldBeLoaded: args.shouldBeLoaded,
316
+ shouldBeVisible: args.shouldBeVisible
317
+ };
318
+ break;
319
+ case 'create_light':
320
+ // Validate light type
321
+ if (!args.lightType) {
322
+ throw new Error('Missing required parameter: lightType');
323
+ }
324
+ const validLightTypes = ['directional', 'point', 'spot', 'rect', 'sky'];
325
+ const normalizedLightType = String(args.lightType).toLowerCase();
326
+ if (!validLightTypes.includes(normalizedLightType)) {
327
+ throw new Error(`Invalid lightType: '${args.lightType}'. Valid types are: ${validLightTypes.join(', ')}`);
328
+ }
329
+
330
+ // Validate name
331
+ if (!args.name) {
332
+ throw new Error('Missing required parameter: name');
333
+ }
334
+ if (typeof args.name !== 'string' || args.name.trim() === '') {
335
+ throw new Error('Invalid name: must be a non-empty string');
336
+ }
337
+
338
+ // Validate intensity if provided
339
+ if (args.intensity !== undefined) {
340
+ if (typeof args.intensity !== 'number' || !isFinite(args.intensity)) {
341
+ throw new Error(`Invalid intensity: must be a finite number, got ${typeof args.intensity}`);
342
+ }
343
+ if (args.intensity < 0) {
344
+ throw new Error('Invalid intensity: must be non-negative');
345
+ }
346
+ }
347
+
348
+ // Validate location if provided
349
+ if (args.location !== undefined && args.location !== null) {
350
+ if (!Array.isArray(args.location) && typeof args.location !== 'object') {
351
+ throw new Error('Invalid location: must be an array [x,y,z] or object {x,y,z}');
352
+ }
353
+ }
354
+
355
+ mappedName = 'create_light';
356
+ mappedArgs = {
357
+ lightType: args.lightType,
358
+ name: args.name,
359
+ location: args.location,
360
+ intensity: args.intensity
361
+ };
362
+ break;
363
+ case 'build_lighting':
364
+ mappedName = 'build_lighting';
365
+ mappedArgs = {
366
+ quality: args.quality
367
+ };
368
+ break;
369
+ default:
370
+ throw new Error(`Unknown level action: ${args.action}`);
371
+ }
372
+ break;
373
+
374
+ // 5. ANIMATION & PHYSICS
375
+ case 'animation_physics':
376
+ // Validate action exists
377
+ if (!args.action) {
378
+ throw new Error('Missing required parameter: action');
379
+ }
380
+
381
+ switch (args.action) {
382
+ case 'create_animation_bp':
383
+ // Validate required parameters
384
+ if (args.name === undefined || args.name === null) {
385
+ throw new Error('Missing required parameter: name');
386
+ }
387
+ if (typeof args.name !== 'string') {
388
+ throw new Error('Invalid name: must be a string');
389
+ }
390
+ if (args.name.trim() === '') {
391
+ throw new Error('Invalid name: cannot be empty');
392
+ }
393
+
394
+ if (args.skeletonPath === undefined || args.skeletonPath === null) {
395
+ throw new Error('Missing required parameter: skeletonPath');
396
+ }
397
+ if (typeof args.skeletonPath !== 'string') {
398
+ throw new Error('Invalid skeletonPath: must be a string');
399
+ }
400
+ if (args.skeletonPath.trim() === '') {
401
+ throw new Error('Invalid skeletonPath: cannot be empty');
402
+ }
403
+
404
+ // Optional savePath validation
405
+ if (args.savePath !== undefined && args.savePath !== null) {
406
+ if (typeof args.savePath !== 'string') {
407
+ throw new Error('Invalid savePath: must be a string');
408
+ }
409
+ }
410
+
411
+ mappedName = 'create_animation_blueprint';
412
+ mappedArgs = {
413
+ name: args.name,
414
+ skeletonPath: args.skeletonPath,
415
+ savePath: args.savePath
416
+ };
417
+ break;
418
+
419
+ case 'play_montage':
420
+ // Validate required parameters
421
+ if (args.actorName === undefined || args.actorName === null) {
422
+ throw new Error('Missing required parameter: actorName');
423
+ }
424
+ if (typeof args.actorName !== 'string') {
425
+ throw new Error('Invalid actorName: must be a string');
426
+ }
427
+ if (args.actorName.trim() === '') {
428
+ throw new Error('Invalid actorName: cannot be empty');
429
+ }
430
+
431
+ // Check for montagePath or animationPath
432
+ const montagePath = args.montagePath || args.animationPath;
433
+ if (montagePath === undefined || montagePath === null) {
434
+ throw new Error('Missing required parameter: montagePath or animationPath');
435
+ }
436
+ if (typeof montagePath !== 'string') {
437
+ throw new Error('Invalid montagePath: must be a string');
438
+ }
439
+ if (montagePath.trim() === '') {
440
+ throw new Error('Invalid montagePath: cannot be empty');
441
+ }
442
+
443
+ // Optional playRate validation
444
+ if (args.playRate !== undefined && args.playRate !== null) {
445
+ if (typeof args.playRate !== 'number') {
446
+ throw new Error('Invalid playRate: must be a number');
447
+ }
448
+ if (isNaN(args.playRate)) {
449
+ throw new Error('Invalid playRate: cannot be NaN');
450
+ }
451
+ if (!isFinite(args.playRate)) {
452
+ throw new Error('Invalid playRate: must be finite');
453
+ }
454
+ }
455
+
456
+ mappedName = 'play_animation_montage';
457
+ mappedArgs = {
458
+ actorName: args.actorName,
459
+ montagePath: montagePath,
460
+ playRate: args.playRate
461
+ };
462
+ break;
463
+
464
+ case 'setup_ragdoll':
465
+ // Validate required parameters
466
+ if (args.skeletonPath === undefined || args.skeletonPath === null) {
467
+ throw new Error('Missing required parameter: skeletonPath');
468
+ }
469
+ if (typeof args.skeletonPath !== 'string') {
470
+ throw new Error('Invalid skeletonPath: must be a string');
471
+ }
472
+ if (args.skeletonPath.trim() === '') {
473
+ throw new Error('Invalid skeletonPath: cannot be empty');
474
+ }
475
+
476
+ if (args.physicsAssetName === undefined || args.physicsAssetName === null) {
477
+ throw new Error('Missing required parameter: physicsAssetName');
478
+ }
479
+ if (typeof args.physicsAssetName !== 'string') {
480
+ throw new Error('Invalid physicsAssetName: must be a string');
481
+ }
482
+ if (args.physicsAssetName.trim() === '') {
483
+ throw new Error('Invalid physicsAssetName: cannot be empty');
484
+ }
485
+
486
+ // Optional blendWeight validation
487
+ if (args.blendWeight !== undefined && args.blendWeight !== null) {
488
+ if (typeof args.blendWeight !== 'number') {
489
+ throw new Error('Invalid blendWeight: must be a number');
490
+ }
491
+ if (isNaN(args.blendWeight)) {
492
+ throw new Error('Invalid blendWeight: cannot be NaN');
493
+ }
494
+ if (!isFinite(args.blendWeight)) {
495
+ throw new Error('Invalid blendWeight: must be finite');
496
+ }
497
+ }
498
+
499
+ // Optional savePath validation
500
+ if (args.savePath !== undefined && args.savePath !== null) {
501
+ if (typeof args.savePath !== 'string') {
502
+ throw new Error('Invalid savePath: must be a string');
503
+ }
504
+ }
505
+
506
+ mappedName = 'setup_ragdoll';
507
+ mappedArgs = {
508
+ skeletonPath: args.skeletonPath,
509
+ physicsAssetName: args.physicsAssetName,
510
+ blendWeight: args.blendWeight,
511
+ savePath: args.savePath
512
+ };
513
+ break;
514
+
515
+ default:
516
+ throw new Error(`Unknown animation/physics action: ${args.action}`);
517
+ }
518
+ break;
519
+
520
+ // 6. EFFECTS SYSTEM
521
+ case 'create_effect':
522
+ switch (args.action) {
523
+ case 'particle':
524
+ mappedName = 'create_particle_effect';
525
+ mappedArgs = {
526
+ effectType: args.effectType,
527
+ name: args.name,
528
+ location: args.location
529
+ };
530
+ break;
531
+ case 'niagara':
532
+ mappedName = 'spawn_niagara_system';
533
+ mappedArgs = {
534
+ systemPath: args.systemPath,
535
+ location: args.location,
536
+ scale: args.scale
537
+ };
538
+ break;
539
+ case 'debug_shape':
540
+ mappedName = 'draw_debug_shape';
541
+ // Convert location object to array for position
542
+ const pos = args.location || args.position;
543
+ mappedArgs = {
544
+ shape: args.shape,
545
+ position: pos ? [pos.x || 0, pos.y || 0, pos.z || 0] : [0, 0, 0],
546
+ size: args.size,
547
+ color: args.color,
548
+ duration: args.duration
549
+ };
550
+ break;
551
+ default:
552
+ throw new Error(`Unknown effect action: ${args.action}`);
553
+ }
554
+ break;
555
+
556
+ // 7. BLUEPRINT MANAGER
557
+ case 'manage_blueprint':
558
+ switch (args.action) {
559
+ case 'create':
560
+ mappedName = 'create_blueprint';
561
+ mappedArgs = {
562
+ name: args.name,
563
+ blueprintType: args.blueprintType,
564
+ savePath: args.savePath
565
+ };
566
+ break;
567
+ case 'add_component':
568
+ mappedName = 'add_blueprint_component';
569
+ mappedArgs = {
570
+ blueprintName: args.name,
571
+ componentType: args.componentType,
572
+ componentName: args.componentName
573
+ };
574
+ break;
575
+ default:
576
+ throw new Error(`Unknown blueprint action: ${args.action}`);
577
+ }
578
+ break;
579
+
580
+ // 8. ENVIRONMENT BUILDER
581
+ case 'build_environment':
582
+ switch (args.action) {
583
+ case 'create_landscape':
584
+ mappedName = 'create_landscape';
585
+ mappedArgs = {
586
+ name: args.name,
587
+ sizeX: args.sizeX,
588
+ sizeY: args.sizeY,
589
+ materialPath: args.materialPath
590
+ };
591
+ break;
592
+ case 'sculpt':
593
+ mappedName = 'sculpt_landscape';
594
+ mappedArgs = {
595
+ landscapeName: args.name,
596
+ tool: args.tool,
597
+ brushSize: args.brushSize,
598
+ strength: args.strength
599
+ };
600
+ break;
601
+ case 'add_foliage':
602
+ // Validate foliage creation parameters to avoid bad console commands / engine warnings
603
+ if (args.name === undefined || args.name === null || typeof args.name !== 'string' || args.name.trim() === '' || String(args.name).toLowerCase() === 'undefined' || String(args.name).toLowerCase() === 'any') {
604
+ throw new Error(`Invalid foliage name: '${args.name}'`);
605
+ }
606
+ if (args.meshPath === undefined || args.meshPath === null || typeof args.meshPath !== 'string' || args.meshPath.trim() === '' || String(args.meshPath).toLowerCase() === 'undefined') {
607
+ throw new Error(`Invalid meshPath: '${args.meshPath}'`);
608
+ }
609
+ if (args.density !== undefined) {
610
+ if (typeof args.density !== 'number' || !isFinite(args.density) || args.density < 0) {
611
+ throw new Error(`Invalid density: '${args.density}' (must be non-negative finite number)`);
612
+ }
613
+ }
614
+ mappedName = 'add_foliage_type';
615
+ mappedArgs = {
616
+ name: args.name,
617
+ meshPath: args.meshPath,
618
+ density: args.density
619
+ };
620
+ break;
621
+ case 'paint_foliage':
622
+ // Validate paint parameters
623
+ if (args.foliageType === undefined || args.foliageType === null || typeof args.foliageType !== 'string' || args.foliageType.trim() === '' || String(args.foliageType).toLowerCase() === 'undefined' || String(args.foliageType).toLowerCase() === 'any') {
624
+ throw new Error(`Invalid foliageType: '${args.foliageType}'`);
625
+ }
626
+ // Convert position object to array if needed
627
+ let positionArray;
628
+ if (args.position) {
629
+ if (Array.isArray(args.position)) {
630
+ positionArray = args.position;
631
+ } else if (typeof args.position === 'object') {
632
+ positionArray = [args.position.x || 0, args.position.y || 0, args.position.z || 0];
633
+ } else {
634
+ positionArray = [0, 0, 0];
635
+ }
636
+ } else {
637
+ positionArray = [0, 0, 0];
638
+ }
639
+ // Validate numbers in position
640
+ if (!Array.isArray(positionArray) || positionArray.length !== 3 || positionArray.some(v => typeof v !== 'number' || !isFinite(v))) {
641
+ throw new Error(`Invalid position: '${JSON.stringify(args.position)}'`);
642
+ }
643
+ if (args.brushSize !== undefined) {
644
+ if (typeof args.brushSize !== 'number' || !isFinite(args.brushSize) || args.brushSize < 0) {
645
+ throw new Error(`Invalid brushSize: '${args.brushSize}' (must be non-negative finite number)`);
646
+ }
647
+ }
648
+ mappedName = 'paint_foliage';
649
+ mappedArgs = {
650
+ foliageType: args.foliageType,
651
+ position: positionArray,
652
+ brushSize: args.brushSize
653
+ };
654
+ break;
655
+ default:
656
+ throw new Error(`Unknown environment action: ${args.action}`);
657
+ }
658
+ break;
659
+
660
+ // 9. SYSTEM CONTROL
661
+ case 'system_control':
662
+ // Validate args is not null/undefined
663
+ if (args === null || args === undefined) {
664
+ throw new Error('Invalid arguments: null or undefined');
665
+ }
666
+ if (typeof args !== 'object') {
667
+ throw new Error('Invalid arguments: must be an object');
668
+ }
669
+ // Validate action exists
670
+ if (!args.action) {
671
+ throw new Error('Missing required parameter: action');
672
+ }
673
+
674
+ switch (args.action) {
675
+ case 'profile':
676
+ // Validate profile type
677
+ const validProfileTypes = ['CPU', 'GPU', 'Memory', 'RenderThread', 'GameThread', 'All'];
678
+ if (!args.profileType || !validProfileTypes.includes(args.profileType)) {
679
+ throw new Error(`Invalid profileType: '${args.profileType}'. Valid types: ${validProfileTypes.join(', ')}`);
680
+ }
681
+ mappedName = 'start_profiling';
682
+ mappedArgs = {
683
+ type: args.profileType,
684
+ duration: args.duration
685
+ };
686
+ break;
687
+
688
+ case 'show_fps':
689
+ // Validate enabled is boolean
690
+ if (args.enabled !== undefined && typeof args.enabled !== 'boolean') {
691
+ throw new Error(`Invalid enabled: must be boolean, got ${typeof args.enabled}`);
692
+ }
693
+ mappedName = 'show_fps';
694
+ mappedArgs = {
695
+ enabled: args.enabled,
696
+ verbose: args.verbose
697
+ };
698
+ break;
699
+
700
+ case 'set_quality':
701
+ // Validate category - normalize aliases and singular forms used by sg.*Quality
702
+ const validCategories = ['ViewDistance', 'AntiAliasing', 'PostProcessing', 'PostProcess',
703
+ 'Shadows', 'Shadow', 'GlobalIllumination', 'Reflections', 'Reflection', 'Textures', 'Texture',
704
+ 'Effects', 'Foliage', 'Shading'];
705
+ if (!args.category || !validCategories.includes(args.category)) {
706
+ throw new Error(`Invalid category: '${args.category}'. Valid categories: ${validCategories.join(', ')}`);
707
+ }
708
+ // Validate level
709
+ if (args.level === undefined || args.level === null) {
710
+ throw new Error('Missing required parameter: level');
711
+ }
712
+ if (typeof args.level !== 'number' || !Number.isInteger(args.level) || args.level < 0 || args.level > 4) {
713
+ throw new Error(`Invalid level: must be integer 0-4, got ${args.level}`);
714
+ }
715
+ // Normalize category to sg.<Base>Quality base (singular where needed)
716
+ const map: Record<string, string> = {
717
+ ViewDistance: 'ViewDistance',
718
+ AntiAliasing: 'AntiAliasing',
719
+ PostProcessing: 'PostProcess',
720
+ PostProcess: 'PostProcess',
721
+ Shadows: 'Shadow',
722
+ Shadow: 'Shadow',
723
+ GlobalIllumination: 'GlobalIllumination',
724
+ Reflections: 'Reflection',
725
+ Reflection: 'Reflection',
726
+ Textures: 'Texture',
727
+ Texture: 'Texture',
728
+ Effects: 'Effects',
729
+ Foliage: 'Foliage',
730
+ Shading: 'Shading',
731
+ };
732
+ const categoryName = map[String(args.category)] || args.category;
733
+ mappedName = 'set_scalability';
734
+ mappedArgs = {
735
+ category: categoryName,
736
+ level: args.level
737
+ };
738
+ break;
739
+
740
+ case 'play_sound':
741
+ // Validate sound path
742
+ if (!args.soundPath || typeof args.soundPath !== 'string') {
743
+ throw new Error('Invalid soundPath: must be a non-empty string');
744
+ }
745
+ // Validate volume if provided
746
+ if (args.volume !== undefined) {
747
+ if (typeof args.volume !== 'number' || args.volume < 0 || args.volume > 1) {
748
+ throw new Error(`Invalid volume: must be 0-1, got ${args.volume}`);
749
+ }
750
+ }
751
+ mappedName = 'play_sound';
752
+ mappedArgs = {
753
+ soundPath: args.soundPath,
754
+ location: args.location,
755
+ volume: args.volume,
756
+ is3D: args.is3D
757
+ };
758
+ break;
759
+
760
+ case 'create_widget':
761
+ // Validate widget name
762
+ if (!args.widgetName || typeof args.widgetName !== 'string' || args.widgetName.trim() === '') {
763
+ throw new Error('Invalid widgetName: must be a non-empty string');
764
+ }
765
+ mappedName = 'create_widget';
766
+ mappedArgs = {
767
+ name: args.widgetName,
768
+ type: args.widgetType,
769
+ savePath: args.savePath
770
+ };
771
+ break;
772
+
773
+ case 'show_widget':
774
+ // Validate widget name
775
+ if (!args.widgetName || typeof args.widgetName !== 'string') {
776
+ throw new Error('Invalid widgetName: must be a non-empty string');
777
+ }
778
+ // Validate visible is boolean (default to true if not provided)
779
+ const isVisible = args.visible !== undefined ? args.visible : true;
780
+ if (typeof isVisible !== 'boolean') {
781
+ throw new Error(`Invalid visible: must be boolean, got ${typeof isVisible}`);
782
+ }
783
+ mappedName = 'show_widget';
784
+ mappedArgs = {
785
+ widgetName: args.widgetName,
786
+ visible: isVisible
787
+ };
788
+ break;
789
+
790
+ case 'screenshot':
791
+ mappedName = 'take_screenshot';
792
+ mappedArgs = { resolution: args.resolution };
793
+ break;
794
+
795
+ case 'engine_start':
796
+ mappedName = 'launch_editor';
797
+ mappedArgs = { editorExe: args.editorExe, projectPath: args.projectPath };
798
+ break;
799
+
800
+ case 'engine_quit':
801
+ mappedName = 'quit_editor';
802
+ mappedArgs = {};
803
+ break;
804
+
805
+ default:
806
+ throw new Error(`Unknown system action: ${args.action}`);
807
+ }
808
+ break;
809
+
810
+ // 10. CONSOLE COMMAND - handle validation here
811
+ case 'console_command':
812
+ // Handle empty/invalid commands gracefully
813
+ if (args.command === undefined || args.command === null || args.command === '' || typeof args.command !== 'string') {
814
+ return {
815
+ content: [{
816
+ type: 'text',
817
+ text: 'Empty command ignored'
818
+ }],
819
+ isError: false,
820
+ success: true,
821
+ message: 'Empty command'
822
+ };
823
+ }
824
+
825
+ const cmdTrimmed = args.command.trim();
826
+ if (cmdTrimmed.length === 0) {
827
+ return {
828
+ content: [{
829
+ type: 'text',
830
+ text: 'Empty command ignored'
831
+ }],
832
+ isError: false,
833
+ success: true,
834
+ message: 'Empty command'
835
+ };
836
+ }
837
+
838
+ mappedName = 'console_command';
839
+ mappedArgs = args;
840
+ break;
841
+
842
+ // 11. REMOTE CONTROL PRESETS - Direct implementation
843
+ case 'manage_rc':
844
+ if (!args.action) throw new Error('Missing required parameter: action');
845
+
846
+ // Handle RC operations directly through RcTools
847
+ let rcResult: any;
848
+
849
+ switch (args.action) {
850
+ // Support both 'create_preset' and 'create' for compatibility
851
+ case 'create_preset':
852
+ case 'create':
853
+ // Support both 'name' and 'presetName' parameter names
854
+ const presetName = args.name || args.presetName;
855
+ if (!presetName) throw new Error('Missing required parameter: name or presetName');
856
+ rcResult = await tools.rcTools.createPreset({
857
+ name: presetName,
858
+ path: args.path
859
+ });
860
+ // Return consistent output with presetId for tests
861
+ if (rcResult.success) {
862
+ rcResult.message = `Remote Control preset created: ${presetName}`;
863
+ // Ensure presetId is set (for test compatibility)
864
+ if (rcResult.presetPath && !rcResult.presetId) {
865
+ rcResult.presetId = rcResult.presetPath;
866
+ }
867
+ }
868
+ break;
869
+
870
+ case 'list':
871
+ // List all presets - implement via RcTools
872
+ rcResult = await tools.rcTools.listPresets();
873
+ break;
874
+
875
+ case 'delete':
876
+ if (!args.presetId) throw new Error('Missing required parameter: presetId');
877
+ rcResult = await tools.rcTools.deletePreset(args.presetId);
878
+ if (rcResult.success) {
879
+ rcResult.message = 'Preset deleted successfully';
880
+ }
881
+ break;
882
+
883
+ case 'expose_actor':
884
+ if (!args.presetPath) throw new Error('Missing required parameter: presetPath');
885
+ if (!args.actorName) throw new Error('Missing required parameter: actorName');
886
+
887
+ rcResult = await tools.rcTools.exposeActor({
888
+ presetPath: args.presetPath,
889
+ actorName: args.actorName
890
+ });
891
+ if (rcResult.success) {
892
+ rcResult.message = `Actor '${args.actorName}' exposed to preset`;
893
+ }
894
+ break;
895
+
896
+ case 'expose_property':
897
+ case 'expose': // Support simplified name from tests
898
+ // Support both presetPath and presetId
899
+ const presetPathExp = args.presetPath || args.presetId;
900
+ if (!presetPathExp) throw new Error('Missing required parameter: presetPath or presetId');
901
+ if (!args.objectPath) throw new Error('Missing required parameter: objectPath');
902
+ if (!args.propertyName) throw new Error('Missing required parameter: propertyName');
903
+
904
+ rcResult = await tools.rcTools.exposeProperty({
905
+ presetPath: presetPathExp,
906
+ objectPath: args.objectPath,
907
+ propertyName: args.propertyName
908
+ });
909
+ if (rcResult.success) {
910
+ rcResult.message = `Property '${args.propertyName}' exposed to preset`;
911
+ }
912
+ break;
913
+
914
+ case 'list_fields':
915
+ case 'get_exposed': // Support test naming
916
+ const presetPathList = args.presetPath || args.presetId;
917
+ if (!presetPathList) throw new Error('Missing required parameter: presetPath or presetId');
918
+
919
+ rcResult = await tools.rcTools.listFields({
920
+ presetPath: presetPathList
921
+ });
922
+ // Map 'fields' to 'exposedProperties' for test compatibility
923
+ if (rcResult.success && rcResult.fields) {
924
+ rcResult.exposedProperties = rcResult.fields;
925
+ }
926
+ break;
927
+
928
+ case 'set_property':
929
+ case 'set_value': // Support test naming
930
+ // Support both patterns
931
+ const objPathSet = args.objectPath || args.presetId;
932
+ const propNameSet = args.propertyName || args.propertyLabel;
933
+
934
+ if (!objPathSet) throw new Error('Missing required parameter: objectPath or presetId');
935
+ if (!propNameSet) throw new Error('Missing required parameter: propertyName or propertyLabel');
936
+ if (args.value === undefined) throw new Error('Missing required parameter: value');
937
+
938
+ rcResult = await tools.rcTools.setProperty({
939
+ objectPath: objPathSet,
940
+ propertyName: propNameSet,
941
+ value: args.value
942
+ });
943
+ if (rcResult.success) {
944
+ rcResult.message = `Property '${propNameSet}' value updated`;
945
+ }
946
+ break;
947
+
948
+ case 'get_property':
949
+ case 'get_value': // Support test naming
950
+ const objPathGet = args.objectPath || args.presetId;
951
+ const propNameGet = args.propertyName || args.propertyLabel;
952
+
953
+ if (!objPathGet) throw new Error('Missing required parameter: objectPath or presetId');
954
+ if (!propNameGet) throw new Error('Missing required parameter: propertyName or propertyLabel');
955
+
956
+ rcResult = await tools.rcTools.getProperty({
957
+ objectPath: objPathGet,
958
+ propertyName: propNameGet
959
+ });
960
+ break;
961
+
962
+ case 'call_function':
963
+ if (!args.presetId) throw new Error('Missing required parameter: presetId');
964
+ if (!args.functionLabel) throw new Error('Missing required parameter: functionLabel');
965
+
966
+ // For now, return not implemented
967
+ rcResult = {
968
+ success: false,
969
+ error: 'Function calls not yet implemented'
970
+ };
971
+ break;
972
+
973
+ default:
974
+ throw new Error(`Unknown RC action: ${args.action}. Valid actions are: create_preset, expose_actor, expose_property, list_fields, set_property, get_property, or their simplified versions: create, list, delete, expose, get_exposed, set_value, get_value, call_function`);
975
+ }
976
+
977
+ // Return result directly - MCP formatting will be handled by response validator
978
+ // Clean to prevent circular references
979
+ return cleanObject(rcResult);
980
+
981
+ // 12. SEQUENCER / CINEMATICS
982
+ case 'manage_sequence':
983
+ if (!args.action) throw new Error('Missing required parameter: action');
984
+
985
+ // Direct handling for sequence operations
986
+ const seqResult = await (async () => {
987
+ const sequenceTools = tools.sequenceTools;
988
+ if (!sequenceTools) throw new Error('Sequence tools not available');
989
+
990
+ switch (args.action) {
991
+ case 'create':
992
+ return await sequenceTools.create({ name: args.name, path: args.path });
993
+ case 'open':
994
+ return await sequenceTools.open({ path: args.path });
995
+ case 'add_camera':
996
+ return await sequenceTools.addCamera({ spawnable: args.spawnable !== false });
997
+ case 'add_actor':
998
+ return await sequenceTools.addActor({ actorName: args.actorName });
999
+ case 'add_actors':
1000
+ if (!args.actorNames) throw new Error('Missing required parameter: actorNames');
1001
+ return await sequenceTools.addActors({ actorNames: args.actorNames });
1002
+ case 'remove_actors':
1003
+ if (!args.actorNames) throw new Error('Missing required parameter: actorNames');
1004
+ return await sequenceTools.removeActors({ actorNames: args.actorNames });
1005
+ case 'get_bindings':
1006
+ return await sequenceTools.getBindings({ path: args.path });
1007
+ case 'add_spawnable_from_class':
1008
+ if (!args.className) throw new Error('Missing required parameter: className');
1009
+ return await sequenceTools.addSpawnableFromClass({ className: args.className, path: args.path });
1010
+ case 'play':
1011
+ return await sequenceTools.play({ loopMode: args.loopMode });
1012
+ case 'pause':
1013
+ return await sequenceTools.pause();
1014
+ case 'stop':
1015
+ return await sequenceTools.stop();
1016
+ case 'set_properties':
1017
+ return await sequenceTools.setSequenceProperties({
1018
+ path: args.path,
1019
+ frameRate: args.frameRate,
1020
+ lengthInFrames: args.lengthInFrames,
1021
+ playbackStart: args.playbackStart,
1022
+ playbackEnd: args.playbackEnd
1023
+ });
1024
+ case 'get_properties':
1025
+ return await sequenceTools.getSequenceProperties({ path: args.path });
1026
+ case 'set_playback_speed':
1027
+ if (args.speed === undefined) throw new Error('Missing required parameter: speed');
1028
+ return await sequenceTools.setPlaybackSpeed({ speed: args.speed });
1029
+ default:
1030
+ throw new Error(`Unknown sequence action: ${args.action}`);
1031
+ }
1032
+ })();
1033
+
1034
+ // Return result directly - MCP formatting will be handled by response validator
1035
+ // Clean to prevent circular references
1036
+ return cleanObject(seqResult);
1037
+ // 13. INTROSPECTION
1038
+ case 'inspect':
1039
+ if (!args.action) throw new Error('Missing required parameter: action');
1040
+ switch (args.action) {
1041
+ case 'inspect_object':
1042
+ mappedName = 'inspect_object';
1043
+ mappedArgs = { objectPath: args.objectPath };
1044
+ break;
1045
+ case 'set_property':
1046
+ mappedName = 'inspect_set_property';
1047
+ mappedArgs = { objectPath: args.objectPath, propertyName: args.propertyName, value: args.value };
1048
+ break;
1049
+ default:
1050
+ throw new Error(`Unknown inspect action: ${args.action}`);
1051
+ }
1052
+ break;
1053
+
1054
+ default:
1055
+ throw new Error(`Unknown consolidated tool: ${name}`);
1056
+ }
1057
+
1058
+ // Call the original handler with mapped name and args with timeout
1059
+ const TOOL_TIMEOUT = 15000; // 15 seconds timeout for tool execution
1060
+
1061
+ const toolPromise = handleToolCall(mappedName, mappedArgs, tools);
1062
+ const timeoutPromise = new Promise<never>((_, reject) => {
1063
+ setTimeout(() => {
1064
+ reject(new Error(`Tool execution timeout after ${TOOL_TIMEOUT}ms`));
1065
+ }, TOOL_TIMEOUT);
1066
+ });
1067
+
1068
+ const result = await Promise.race([toolPromise, timeoutPromise]);
1069
+ const duration = Date.now() - startTime;
1070
+ console.log(`[ConsolidatedToolHandler] Completed execution of ${name} in ${duration}ms`);
1071
+
1072
+ // Clean the result to prevent circular reference errors
1073
+ return cleanObject(result);
1074
+
1075
+ } catch (err: any) {
1076
+ const duration = Date.now() - startTime;
1077
+ console.log(`[ConsolidatedToolHandler] Failed execution of ${name} after ${duration}ms: ${err?.message || String(err)}`);
1078
+
1079
+ // Return consistent error structure matching regular tool handlers
1080
+ const errorMessage = err?.message || String(err);
1081
+ const isTimeout = errorMessage.includes('timeout');
1082
+
1083
+ return {
1084
+ content: [{
1085
+ type: 'text',
1086
+ text: isTimeout
1087
+ ? `Tool ${name} timed out. Please check Unreal Engine connection.`
1088
+ : `Failed to execute ${name}: ${errorMessage}`
1089
+ }],
1090
+ isError: true
1091
+ };
1092
+ }
1093
+ }