unreal-engine-mcp-server 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.env.production +6 -1
  2. package/Dockerfile +11 -28
  3. package/README.md +1 -2
  4. package/dist/index.js +120 -54
  5. package/dist/resources/actors.js +71 -13
  6. package/dist/resources/assets.d.ts +3 -2
  7. package/dist/resources/assets.js +96 -72
  8. package/dist/resources/levels.js +2 -2
  9. package/dist/tools/assets.js +6 -2
  10. package/dist/tools/build_environment_advanced.js +46 -42
  11. package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
  12. package/dist/tools/consolidated-tool-definitions.js +173 -8
  13. package/dist/tools/consolidated-tool-handlers.js +331 -718
  14. package/dist/tools/debug.js +4 -6
  15. package/dist/tools/rc.js +2 -2
  16. package/dist/tools/sequence.js +21 -2
  17. package/dist/unreal-bridge.d.ts +4 -1
  18. package/dist/unreal-bridge.js +211 -53
  19. package/dist/utils/http.js +4 -2
  20. package/dist/utils/response-validator.d.ts +6 -1
  21. package/dist/utils/response-validator.js +43 -15
  22. package/package.json +5 -5
  23. package/server.json +2 -2
  24. package/src/index.ts +120 -56
  25. package/src/resources/actors.ts +51 -13
  26. package/src/resources/assets.ts +97 -73
  27. package/src/resources/levels.ts +2 -2
  28. package/src/tools/assets.ts +6 -2
  29. package/src/tools/build_environment_advanced.ts +46 -42
  30. package/src/tools/consolidated-tool-definitions.ts +173 -8
  31. package/src/tools/consolidated-tool-handlers.ts +318 -747
  32. package/src/tools/debug.ts +4 -6
  33. package/src/tools/rc.ts +2 -2
  34. package/src/tools/sequence.ts +21 -2
  35. package/src/unreal-bridge.ts +163 -60
  36. package/src/utils/http.ts +7 -4
  37. package/src/utils/response-validator.ts +48 -19
  38. package/dist/tools/tool-definitions.d.ts +0 -4919
  39. package/dist/tools/tool-definitions.js +0 -1007
  40. package/dist/tools/tool-handlers.d.ts +0 -47
  41. package/dist/tools/tool-handlers.js +0 -863
  42. package/src/tools/tool-definitions.ts +0 -1023
  43. package/src/tools/tool-handlers.ts +0 -973
@@ -1,6 +1,8 @@
1
1
  // Consolidated tool handlers - maps 13 tools to all 36 operations
2
- import { handleToolCall } from './tool-handlers.js';
3
2
  import { cleanObject } from '../utils/safe-json.js';
3
+ import { Logger } from '../utils/logger.js';
4
+
5
+ const log = new Logger('ConsolidatedToolHandler');
4
6
 
5
7
  export async function handleConsolidatedToolCall(
6
8
  name: string,
@@ -8,7 +10,8 @@ export async function handleConsolidatedToolCall(
8
10
  tools: any
9
11
  ) {
10
12
  const startTime = Date.now();
11
- console.log(`[ConsolidatedToolHandler] Starting execution of ${name} at ${new Date().toISOString()}`);
13
+ // Use scoped logger (stderr) to avoid polluting stdout JSON
14
+ log.debug(`Starting execution of ${name} at ${new Date().toISOString()}`);
12
15
 
13
16
  try {
14
17
  // Validate args is not null/undefined
@@ -16,8 +19,6 @@ export async function handleConsolidatedToolCall(
16
19
  throw new Error('Invalid arguments: null or undefined');
17
20
  }
18
21
 
19
- let mappedName: string;
20
- let mappedArgs: any = { ...args };
21
22
 
22
23
  switch (name) {
23
24
  // 1. ASSET MANAGER
@@ -32,81 +33,34 @@ export async function handleConsolidatedToolCall(
32
33
  throw new Error('Missing required parameter: action');
33
34
  }
34
35
 
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');
36
+ switch (args.action) {
37
+ case 'list': {
38
+ if (args.directory !== undefined && args.directory !== null && typeof args.directory !== 'string') {
39
+ throw new Error('Invalid directory: must be a string');
58
40
  }
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');
41
+ const res = await tools.assetResources.list(args.directory || '/Game', false);
42
+ return cleanObject({ success: true, ...res });
43
+ }
44
+ case 'import': {
45
+ if (typeof args.sourcePath !== 'string' || args.sourcePath.trim() === '') {
46
+ throw new Error('Invalid sourcePath');
87
47
  }
88
- if (args.name.trim() === '') {
89
- throw new Error('Invalid name: cannot be empty');
48
+ if (typeof args.destinationPath !== 'string' || args.destinationPath.trim() === '') {
49
+ throw new Error('Invalid destinationPath');
90
50
  }
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
- }
51
+ const res = await tools.assetTools.importAsset(args.sourcePath, args.destinationPath);
52
+ return cleanObject(res);
53
+ }
54
+ case 'create_material': {
55
+ if (typeof args.name !== 'string' || args.name.trim() === '') {
56
+ throw new Error('Invalid name: must be a non-empty string');
97
57
  }
98
-
99
- mappedName = 'create_material';
100
- mappedArgs = {
101
- name: args.name,
102
- path: args.path
103
- };
104
- break;
105
-
58
+ const res = await tools.materialTools.createMaterial(args.name, args.path || '/Game/Materials');
59
+ return cleanObject(res);
60
+ }
106
61
  default:
107
62
  throw new Error(`Unknown asset action: ${args.action}`);
108
63
  }
109
- break;
110
64
 
111
65
  // 2. ACTOR CONTROL
112
66
  case 'control_actor':
@@ -115,70 +69,42 @@ export async function handleConsolidatedToolCall(
115
69
  throw new Error('Missing required parameter: action');
116
70
  }
117
71
 
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() === '') {
72
+ switch (args.action) {
73
+ case 'spawn': {
74
+ if (!args.classPath || typeof args.classPath !== 'string' || args.classPath.trim() === '') {
125
75
  throw new Error('Invalid classPath: must be a non-empty string');
126
76
  }
127
-
128
- mappedName = 'spawn_actor';
129
- mappedArgs = {
77
+ const res = await tools.actorTools.spawn({
130
78
  classPath: args.classPath,
131
79
  location: args.location,
132
80
  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');
81
+ });
82
+ return cleanObject(res);
83
+ }
84
+ case 'delete': {
85
+ if (!args.actorName || typeof args.actorName !== 'string' || args.actorName.trim() === '') {
86
+ throw new Error('Invalid actorName');
161
87
  }
162
- if (typeof args.force !== 'object' || args.force === null) {
163
- throw new Error('Invalid force: must be an object with x, y, z properties');
88
+ const res = await tools.bridge.executeEditorFunction('DELETE_ACTOR', { actor_name: args.actorName });
89
+ return cleanObject(res);
90
+ }
91
+ case 'apply_force': {
92
+ if (!args.actorName || typeof args.actorName !== 'string' || args.actorName.trim() === '') {
93
+ throw new Error('Invalid actorName');
164
94
  }
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');
95
+ if (!args.force || typeof args.force.x !== 'number' || typeof args.force.y !== 'number' || typeof args.force.z !== 'number') {
96
+ throw new Error('Invalid force: must have numeric x,y,z');
169
97
  }
170
-
171
- mappedName = 'apply_force';
172
- mappedArgs = {
98
+ const res = await tools.physicsTools.applyForce({
173
99
  actorName: args.actorName,
174
- force: args.force
175
- };
176
- break;
177
-
100
+ forceType: 'Force',
101
+ vector: [args.force.x, args.force.y, args.force.z]
102
+ });
103
+ return cleanObject(res);
104
+ }
178
105
  default:
179
106
  throw new Error(`Unknown actor action: ${args.action}`);
180
107
  }
181
- break;
182
108
 
183
109
  // 3. EDITOR CONTROL
184
110
  case 'control_editor':
@@ -187,657 +113,317 @@ export async function handleConsolidatedToolCall(
187
113
  throw new Error('Missing required parameter: action');
188
114
  }
189
115
 
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(', ')}`);
116
+ switch (args.action) {
117
+ case 'play': {
118
+ const res = await tools.editorTools.playInEditor();
119
+ return cleanObject(res);
120
+ }
121
+ case 'stop': {
122
+ const res = await tools.editorTools.stopPlayInEditor();
123
+ return cleanObject(res);
124
+ }
125
+ case 'pause': {
126
+ const res = await tools.editorTools.pausePlayInEditor();
127
+ return cleanObject(res);
128
+ }
129
+ case 'set_game_speed': {
130
+ if (typeof args.speed !== 'number' || !isFinite(args.speed) || args.speed <= 0) {
131
+ throw new Error('Invalid speed: must be a positive number');
278
132
  }
279
-
280
- mappedName = 'set_view_mode';
281
- mappedArgs = {
282
- mode: mappedMode
283
- };
284
- break;
133
+ // Use console command via bridge
134
+ const res = await tools.bridge.executeConsoleCommand(`slomo ${args.speed}`);
135
+ return cleanObject(res);
136
+ }
137
+ case 'eject': {
138
+ const res = await tools.bridge.executeConsoleCommand('eject');
139
+ return cleanObject(res);
140
+ }
141
+ case 'possess': {
142
+ const res = await tools.bridge.executeConsoleCommand('viewself');
143
+ return cleanObject(res);
144
+ }
145
+ case 'set_camera': {
146
+ const res = await tools.editorTools.setViewportCamera(args.location, args.rotation);
147
+ return cleanObject(res);
148
+ }
149
+ case 'set_view_mode': {
150
+ if (!args.viewMode || typeof args.viewMode !== 'string') throw new Error('Missing required parameter: viewMode');
151
+ const res = await tools.bridge.setSafeViewMode(args.viewMode);
152
+ return cleanObject(res);
153
+ }
285
154
  default:
286
155
  throw new Error(`Unknown editor action: ${args.action}`);
287
156
  }
288
- break;
289
157
 
290
158
  // 4. LEVEL MANAGER
291
- case 'manage_level':
159
+ case 'manage_level':
160
+ if (!args.action) throw new Error('Missing required parameter: action');
292
161
  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;
162
+ case 'load': {
163
+ if (!args.levelPath || typeof args.levelPath !== 'string') throw new Error('Missing required parameter: levelPath');
164
+ const res = await tools.levelTools.loadLevel({ levelPath: args.levelPath, streaming: !!args.streaming });
165
+ return cleanObject(res);
166
+ }
167
+ case 'save': {
168
+ const res = await tools.levelTools.saveLevel({ levelName: args.levelName, savePath: args.savePath });
169
+ return cleanObject(res);
170
+ }
171
+ case 'stream': {
172
+ if (!args.levelName || typeof args.levelName !== 'string') throw new Error('Missing required parameter: levelName');
173
+ const res = await tools.levelTools.streamLevel({ levelName: args.levelName, shouldBeLoaded: !!args.shouldBeLoaded, shouldBeVisible: !!args.shouldBeVisible });
174
+ return cleanObject(res);
175
+ }
176
+ case 'create_light': {
177
+ if (!args.lightType) throw new Error('Missing required parameter: lightType');
178
+ if (!args.name || typeof args.name !== 'string' || args.name.trim() === '') throw new Error('Invalid name');
179
+ const t = String(args.lightType).toLowerCase();
180
+ if (t === 'directional') return cleanObject(await tools.lightingTools.createDirectionalLight({ name: args.name, intensity: args.intensity }));
181
+ if (t === 'point') return cleanObject(await tools.lightingTools.createPointLight({ name: args.name, location: args.location ? [args.location.x, args.location.y, args.location.z] : [0,0,0], intensity: args.intensity }));
182
+ if (t === 'spot') return cleanObject(await tools.lightingTools.createSpotLight({ name: args.name, location: args.location ? [args.location.x, args.location.y, args.location.z] : [0,0,0], rotation: [0,0,0], intensity: args.intensity }));
183
+ if (t === 'rect') return cleanObject(await tools.lightingTools.createRectLight({ name: args.name, location: args.location ? [args.location.x, args.location.y, args.location.z] : [0,0,0], rotation: [0,0,0], intensity: args.intensity }));
184
+ throw new Error(`Unknown light type: ${args.lightType}`);
185
+ }
186
+ case 'build_lighting': {
187
+ const res = await tools.lightingTools.buildLighting({ quality: args.quality || 'High', buildReflectionCaptures: true });
188
+ return cleanObject(res);
189
+ }
369
190
  default:
370
191
  throw new Error(`Unknown level action: ${args.action}`);
371
192
  }
372
- break;
373
193
 
374
194
  // 5. ANIMATION & PHYSICS
375
- case 'animation_physics':
195
+ case 'animation_physics':
376
196
  // Validate action exists
377
197
  if (!args.action) {
378
198
  throw new Error('Missing required parameter: action');
379
199
  }
380
200
 
381
201
  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
202
+ case 'create_animation_bp': {
203
+ if (typeof args.name !== 'string' || args.name.trim() === '') throw new Error('Invalid name');
204
+ if (typeof args.skeletonPath !== 'string' || args.skeletonPath.trim() === '') throw new Error('Invalid skeletonPath');
205
+ const res = await tools.animationTools.createAnimationBlueprint({ name: args.name, skeletonPath: args.skeletonPath, savePath: args.savePath });
206
+ return cleanObject(res);
207
+ }
208
+ case 'play_montage': {
209
+ if (typeof args.actorName !== 'string' || args.actorName.trim() === '') throw new Error('Invalid actorName');
432
210
  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
-
211
+ if (typeof montagePath !== 'string' || montagePath.trim() === '') throw new Error('Invalid montagePath');
212
+ const res = await tools.animationTools.playAnimation({ actorName: args.actorName, animationType: 'Montage', animationPath: montagePath, playRate: args.playRate });
213
+ return cleanObject(res);
214
+ }
215
+ case 'setup_ragdoll': {
216
+ if (typeof args.skeletonPath !== 'string' || args.skeletonPath.trim() === '') throw new Error('Invalid skeletonPath');
217
+ if (typeof args.physicsAssetName !== 'string' || args.physicsAssetName.trim() === '') throw new Error('Invalid physicsAssetName');
218
+ const res = await tools.physicsTools.setupRagdoll({ skeletonPath: args.skeletonPath, physicsAssetName: args.physicsAssetName, blendWeight: args.blendWeight, savePath: args.savePath });
219
+ return cleanObject(res);
220
+ }
515
221
  default:
516
222
  throw new Error(`Unknown animation/physics action: ${args.action}`);
517
223
  }
518
- break;
519
224
 
520
- // 6. EFFECTS SYSTEM
225
+ // 6. EFFECTS SYSTEM
521
226
  case 'create_effect':
522
227
  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;
228
+ case 'particle': {
229
+ const res = await tools.niagaraTools.createEffect({ effectType: args.effectType, name: args.name, location: args.location, scale: args.scale, customParameters: args.customParameters });
230
+ return cleanObject(res);
231
+ }
232
+ case 'niagara': {
233
+ if (typeof args.systemPath !== 'string' || args.systemPath.trim() === '') throw new Error('Invalid systemPath');
234
+ // Create or ensure system exists (spawning in editor is not universally supported via RC)
235
+ const name = args.name || args.systemPath.split('/').pop();
236
+ const res = await tools.niagaraTools.createSystem({ name, savePath: args.savePath || '/Game/Effects/Niagara' });
237
+ return cleanObject(res);
238
+ }
239
+ case 'debug_shape': {
240
+ const shape = String(args.shape || 'Sphere').toLowerCase();
241
+ const loc = args.location || { x: 0, y: 0, z: 0 };
242
+ const size = args.size || 100;
243
+ const color = args.color || [255, 0, 0, 255];
244
+ const duration = args.duration || 5;
245
+ if (shape === 'line') {
246
+ const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z };
247
+ return cleanObject(await tools.debugTools.drawDebugLine({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration }));
248
+ } else if (shape === 'box') {
249
+ const extent = [size, size, size];
250
+ return cleanObject(await tools.debugTools.drawDebugBox({ center: [loc.x, loc.y, loc.z], extent, color, duration }));
251
+ } else if (shape === 'sphere') {
252
+ return cleanObject(await tools.debugTools.drawDebugSphere({ center: [loc.x, loc.y, loc.z], radius: size, color, duration }));
253
+ } else if (shape === 'capsule') {
254
+ return cleanObject(await tools.debugTools.drawDebugCapsule({ center: [loc.x, loc.y, loc.z], halfHeight: size, radius: Math.max(10, size/3), color, duration }));
255
+ } else if (shape === 'cone') {
256
+ return cleanObject(await tools.debugTools.drawDebugCone({ origin: [loc.x, loc.y, loc.z], direction: [0,0,1], length: size, angleWidth: 0.5, angleHeight: 0.5, color, duration }));
257
+ } else if (shape === 'arrow') {
258
+ const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z };
259
+ return cleanObject(await tools.debugTools.drawDebugArrow({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration }));
260
+ } else if (shape === 'point') {
261
+ return cleanObject(await tools.debugTools.drawDebugPoint({ location: [loc.x, loc.y, loc.z], size, color, duration }));
262
+ } else if (shape === 'text' || shape === 'string') {
263
+ const text = args.text || 'Debug';
264
+ return cleanObject(await tools.debugTools.drawDebugString({ location: [loc.x, loc.y, loc.z], text, color, duration }));
265
+ }
266
+ // Default fallback
267
+ return cleanObject(await tools.debugTools.drawDebugSphere({ center: [loc.x, loc.y, loc.z], radius: size, color, duration }));
268
+ }
551
269
  default:
552
270
  throw new Error(`Unknown effect action: ${args.action}`);
553
271
  }
554
- break;
555
272
 
556
- // 7. BLUEPRINT MANAGER
273
+ // 7. BLUEPRINT MANAGER
557
274
  case 'manage_blueprint':
558
275
  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;
276
+ case 'create': {
277
+ const res = await tools.blueprintTools.createBlueprint({ name: args.name, blueprintType: args.blueprintType || 'Actor', savePath: args.savePath });
278
+ return cleanObject(res);
279
+ }
280
+ case 'add_component': {
281
+ const res = await tools.blueprintTools.addComponent({ blueprintName: args.name, componentType: args.componentType, componentName: args.componentName });
282
+ return cleanObject(res);
283
+ }
575
284
  default:
576
285
  throw new Error(`Unknown blueprint action: ${args.action}`);
577
286
  }
578
- break;
579
287
 
580
- // 8. ENVIRONMENT BUILDER
288
+ // 8. ENVIRONMENT BUILDER
581
289
  case 'build_environment':
582
290
  switch (args.action) {
583
- case 'create_landscape':
584
- mappedName = 'create_landscape';
585
- mappedArgs = {
586
- name: args.name,
291
+ case 'create_landscape': {
292
+ const res = await tools.landscapeTools.createLandscape({ name: args.name, sizeX: args.sizeX, sizeY: args.sizeY, materialPath: args.materialPath });
293
+ return cleanObject(res);
294
+ }
295
+ case 'sculpt': {
296
+ const res = await tools.landscapeTools.sculptLandscape({ landscapeName: args.name, tool: args.tool, brushSize: args.brushSize, strength: args.strength });
297
+ return cleanObject(res);
298
+ }
299
+ case 'add_foliage': {
300
+ const res = await tools.foliageTools.addFoliageType({ name: args.name, meshPath: args.meshPath, density: args.density });
301
+ return cleanObject(res);
302
+ }
303
+ case 'paint_foliage': {
304
+ const pos = args.position ? [args.position.x || 0, args.position.y || 0, args.position.z || 0] : [0,0,0];
305
+ const res = await tools.foliageTools.paintFoliage({ foliageType: args.foliageType, position: pos, brushSize: args.brushSize, paintDensity: args.paintDensity, eraseMode: args.eraseMode });
306
+ return cleanObject(res);
307
+ }
308
+ case 'create_procedural_terrain': {
309
+ const loc = args.location ? [args.location.x||0, args.location.y||0, args.location.z||0] : [0,0,0];
310
+ const res = await tools.buildEnvAdvanced.createProceduralTerrain({
311
+ name: args.name || 'ProceduralTerrain',
312
+ location: loc as [number,number,number],
587
313
  sizeX: args.sizeX,
588
314
  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,
315
+ subdivisions: args.subdivisions,
316
+ heightFunction: args.heightFunction,
317
+ material: args.materialPath
318
+ });
319
+ return cleanObject(res);
320
+ }
321
+ case 'create_procedural_foliage': {
322
+ if (!args.bounds || !args.bounds.location || !args.bounds.size) throw new Error('bounds.location and bounds.size are required');
323
+ const bounds = {
324
+ location: [args.bounds.location.x||0, args.bounds.location.y||0, args.bounds.location.z||0] as [number,number,number],
325
+ size: [args.bounds.size.x||1000, args.bounds.size.y||1000, args.bounds.size.z||100] as [number,number,number]
326
+ };
327
+ const res = await tools.buildEnvAdvanced.createProceduralFoliage({
328
+ name: args.name || 'ProceduralFoliage',
329
+ bounds,
330
+ foliageTypes: args.foliageTypes || [],
331
+ seed: args.seed
332
+ });
333
+ return cleanObject(res);
334
+ }
335
+ case 'add_foliage_instances': {
336
+ if (!args.foliageType) throw new Error('foliageType is required');
337
+ if (!Array.isArray(args.transforms)) throw new Error('transforms array is required');
338
+ const transforms = (args.transforms as any[]).map(t => ({
339
+ location: [t.location?.x||0, t.location?.y||0, t.location?.z||0] as [number,number,number],
340
+ rotation: t.rotation ? [t.rotation.pitch||0, t.rotation.yaw||0, t.rotation.roll||0] as [number,number,number] : undefined,
341
+ scale: t.scale ? [t.scale.x||1, t.scale.y||1, t.scale.z||1] as [number,number,number] : undefined
342
+ }));
343
+ const res = await tools.buildEnvAdvanced.addFoliageInstances({ foliageType: args.foliageType, transforms });
344
+ return cleanObject(res);
345
+ }
346
+ case 'create_landscape_grass_type': {
347
+ const res = await tools.buildEnvAdvanced.createLandscapeGrassType({
348
+ name: args.name || 'GrassType',
617
349
  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;
350
+ density: args.density,
351
+ minScale: args.minScale,
352
+ maxScale: args.maxScale
353
+ });
354
+ return cleanObject(res);
355
+ }
655
356
  default:
656
357
  throw new Error(`Unknown environment action: ${args.action}`);
657
358
  }
658
- break;
659
359
 
660
360
  // 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
-
361
+ case 'system_control':
362
+ if (!args.action) throw new Error('Missing required parameter: action');
674
363
  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
-
364
+ case 'profile': {
365
+ const res = await tools.performanceTools.startProfiling({ type: args.profileType, duration: args.duration });
366
+ return cleanObject(res);
367
+ }
368
+ case 'show_fps': {
369
+ const res = await tools.performanceTools.showFPS({ enabled: !!args.enabled, verbose: !!args.verbose });
370
+ return cleanObject(res);
371
+ }
372
+ case 'set_quality': {
373
+ const res = await tools.performanceTools.setScalability({ category: args.category, level: args.level });
374
+ return cleanObject(res);
375
+ }
376
+ case 'play_sound': {
377
+ if (args.location && typeof args.location === 'object') {
378
+ const loc = [args.location.x || 0, args.location.y || 0, args.location.z || 0];
379
+ const res = await tools.audioTools.playSoundAtLocation({ soundPath: args.soundPath, location: loc as [number, number, number], volume: args.volume, pitch: args.pitch, startTime: args.startTime });
380
+ return cleanObject(res);
381
+ }
382
+ const res = await tools.audioTools.playSound2D({ soundPath: args.soundPath, volume: args.volume, pitch: args.pitch, startTime: args.startTime });
383
+ return cleanObject(res);
384
+ }
385
+ case 'create_widget': {
386
+ const res = await tools.uiTools.createWidget({ name: args.widgetName, type: args.widgetType, savePath: args.savePath });
387
+ return cleanObject(res);
388
+ }
389
+ case 'show_widget': {
390
+ const res = await tools.uiTools.setWidgetVisibility({ widgetName: args.widgetName, visible: args.visible !== false });
391
+ return cleanObject(res);
392
+ }
393
+ case 'screenshot': {
394
+ const res = await tools.visualTools.takeScreenshot({ resolution: args.resolution });
395
+ return cleanObject(res);
396
+ }
397
+ case 'engine_start': {
398
+ const res = await tools.engineTools.launchEditor({ editorExe: args.editorExe, projectPath: args.projectPath });
399
+ return cleanObject(res);
400
+ }
401
+ case 'engine_quit': {
402
+ const res = await tools.engineTools.quitEditor();
403
+ return cleanObject(res);
404
+ }
805
405
  default:
806
406
  throw new Error(`Unknown system action: ${args.action}`);
807
407
  }
808
- break;
809
408
 
810
409
  // 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
- };
410
+ case 'console_command':
411
+ if (!args.command || typeof args.command !== 'string' || args.command.trim() === '') {
412
+ return { success: true, message: 'Empty command' } as any;
823
413
  }
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
- };
414
+ // Basic safety filter
415
+ const cmd = String(args.command).trim();
416
+ const blocked = [/\bquit\b/i, /\bexit\b/i, /debugcrash/i];
417
+ if (blocked.some(r => r.test(cmd))) {
418
+ return { success: false, error: 'Command blocked for safety' } as any;
419
+ }
420
+ try {
421
+ const res = await tools.bridge.executeConsoleCommand(cmd);
422
+ return cleanObject({ success: true, command: cmd, result: res });
423
+ } catch (e: any) {
424
+ return cleanObject({ success: false, command: cmd, error: e?.message || String(e) });
836
425
  }
837
426
 
838
- mappedName = 'console_command';
839
- mappedArgs = args;
840
- break;
841
427
 
842
428
  // 11. REMOTE CONTROL PRESETS - Direct implementation
843
429
  case 'manage_rc':
@@ -1035,42 +621,27 @@ export async function handleConsolidatedToolCall(
1035
621
  // Clean to prevent circular references
1036
622
  return cleanObject(seqResult);
1037
623
  // 13. INTROSPECTION
1038
- case 'inspect':
624
+ case 'inspect':
1039
625
  if (!args.action) throw new Error('Missing required parameter: action');
1040
626
  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;
627
+ case 'inspect_object': {
628
+ const res = await tools.introspectionTools.inspectObject({ objectPath: args.objectPath, detailed: args.detailed });
629
+ return cleanObject(res);
630
+ }
631
+ case 'set_property': {
632
+ const res = await tools.introspectionTools.setProperty({ objectPath: args.objectPath, propertyName: args.propertyName, value: args.value });
633
+ return cleanObject(res);
634
+ }
1049
635
  default:
1050
636
  throw new Error(`Unknown inspect action: ${args.action}`);
1051
637
  }
1052
- break;
638
+
1053
639
 
1054
640
  default:
1055
641
  throw new Error(`Unknown consolidated tool: ${name}`);
1056
642
  }
1057
643
 
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);
644
+ // All cases return (or throw) above; this is a type guard for exhaustiveness.
1074
645
 
1075
646
  } catch (err: any) {
1076
647
  const duration = Date.now() - startTime;