unreal-engine-mcp-server 0.5.2 → 0.5.4

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 (98) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/README.md +9 -6
  3. package/dist/automation/bridge.d.ts +1 -0
  4. package/dist/automation/bridge.js +62 -4
  5. package/dist/automation/types.d.ts +1 -0
  6. package/dist/config/class-aliases.d.ts +5 -0
  7. package/dist/config/class-aliases.js +30 -0
  8. package/dist/constants.d.ts +5 -0
  9. package/dist/constants.js +5 -0
  10. package/dist/graphql/server.d.ts +0 -1
  11. package/dist/graphql/server.js +15 -16
  12. package/dist/index.js +1 -1
  13. package/dist/services/metrics-server.d.ts +2 -1
  14. package/dist/services/metrics-server.js +29 -4
  15. package/dist/tools/consolidated-tool-definitions.js +3 -3
  16. package/dist/tools/debug.d.ts +5 -0
  17. package/dist/tools/debug.js +7 -0
  18. package/dist/tools/handlers/actor-handlers.js +4 -27
  19. package/dist/tools/handlers/asset-handlers.js +13 -1
  20. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -1
  21. package/dist/tools/handlers/common-handlers.d.ts +11 -11
  22. package/dist/tools/handlers/common-handlers.js +6 -4
  23. package/dist/tools/handlers/editor-handlers.d.ts +2 -1
  24. package/dist/tools/handlers/editor-handlers.js +6 -6
  25. package/dist/tools/handlers/effect-handlers.js +3 -0
  26. package/dist/tools/handlers/graph-handlers.d.ts +2 -1
  27. package/dist/tools/handlers/graph-handlers.js +1 -1
  28. package/dist/tools/handlers/input-handlers.d.ts +5 -1
  29. package/dist/tools/handlers/level-handlers.d.ts +2 -1
  30. package/dist/tools/handlers/level-handlers.js +3 -3
  31. package/dist/tools/handlers/lighting-handlers.d.ts +2 -1
  32. package/dist/tools/handlers/lighting-handlers.js +3 -0
  33. package/dist/tools/handlers/pipeline-handlers.d.ts +2 -1
  34. package/dist/tools/handlers/pipeline-handlers.js +64 -10
  35. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  36. package/dist/tools/handlers/system-handlers.d.ts +1 -1
  37. package/dist/tools/input.d.ts +5 -1
  38. package/dist/tools/input.js +37 -1
  39. package/dist/tools/lighting.d.ts +1 -0
  40. package/dist/tools/lighting.js +7 -0
  41. package/dist/tools/physics.d.ts +1 -1
  42. package/dist/tools/sequence.d.ts +1 -0
  43. package/dist/tools/sequence.js +7 -0
  44. package/dist/types/handler-types.d.ts +343 -0
  45. package/dist/types/handler-types.js +2 -0
  46. package/dist/unreal-bridge.d.ts +1 -1
  47. package/dist/unreal-bridge.js +8 -6
  48. package/dist/utils/command-validator.d.ts +1 -0
  49. package/dist/utils/command-validator.js +11 -1
  50. package/dist/utils/error-handler.js +3 -1
  51. package/dist/utils/response-validator.js +2 -2
  52. package/dist/utils/safe-json.d.ts +1 -1
  53. package/dist/utils/safe-json.js +3 -6
  54. package/dist/utils/unreal-command-queue.js +1 -1
  55. package/dist/utils/validation.js +6 -2
  56. package/docs/handler-mapping.md +6 -1
  57. package/package.json +2 -2
  58. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +25 -1
  59. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +40 -58
  60. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +27 -46
  61. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +16 -1
  62. package/server.json +2 -2
  63. package/src/automation/bridge.ts +80 -10
  64. package/src/automation/types.ts +1 -0
  65. package/src/config/class-aliases.ts +65 -0
  66. package/src/constants.ts +10 -0
  67. package/src/graphql/server.ts +23 -23
  68. package/src/index.ts +1 -1
  69. package/src/services/metrics-server.ts +40 -6
  70. package/src/tools/consolidated-tool-definitions.ts +3 -3
  71. package/src/tools/debug.ts +8 -0
  72. package/src/tools/handlers/actor-handlers.ts +5 -31
  73. package/src/tools/handlers/asset-handlers.ts +19 -1
  74. package/src/tools/handlers/blueprint-handlers.ts +1 -1
  75. package/src/tools/handlers/common-handlers.ts +32 -11
  76. package/src/tools/handlers/editor-handlers.ts +8 -7
  77. package/src/tools/handlers/effect-handlers.ts +4 -0
  78. package/src/tools/handlers/graph-handlers.ts +7 -6
  79. package/src/tools/handlers/level-handlers.ts +5 -4
  80. package/src/tools/handlers/lighting-handlers.ts +5 -1
  81. package/src/tools/handlers/pipeline-handlers.ts +83 -16
  82. package/src/tools/input.ts +60 -1
  83. package/src/tools/lighting.ts +11 -0
  84. package/src/tools/physics.ts +1 -1
  85. package/src/tools/sequence.ts +11 -0
  86. package/src/types/handler-types.ts +442 -0
  87. package/src/unreal-bridge.ts +8 -6
  88. package/src/utils/command-validator.ts +23 -1
  89. package/src/utils/error-handler.ts +4 -1
  90. package/src/utils/response-validator.ts +7 -9
  91. package/src/utils/safe-json.ts +20 -15
  92. package/src/utils/unreal-command-queue.ts +3 -1
  93. package/src/utils/validation.test.ts +3 -3
  94. package/src/utils/validation.ts +36 -26
  95. package/tests/test-console-command.mjs +1 -1
  96. package/tests/test-runner.mjs +63 -3
  97. package/tests/run-unreal-tool-tests.mjs +0 -948
  98. package/tests/test-asset-errors.mjs +0 -35
@@ -3,7 +3,6 @@ import { HealthMonitor } from './health-monitor.js';
3
3
  import { AutomationBridge } from '../automation/index.js';
4
4
  import { Logger } from '../utils/logger.js';
5
5
  import { wasmIntegration } from '../wasm/index.js';
6
- import { DEFAULT_AUTOMATION_HOST } from '../constants.js';
7
6
 
8
7
  interface MetricsServerOptions {
9
8
  healthMonitor: HealthMonitor;
@@ -85,7 +84,7 @@ function formatPrometheusMetrics(options: MetricsServerOptions): string {
85
84
  return lines.join('\n') + '\n';
86
85
  }
87
86
 
88
- export function startMetricsServer(options: MetricsServerOptions): void {
87
+ export function startMetricsServer(options: MetricsServerOptions): http.Server | null {
89
88
  const { logger } = options;
90
89
 
91
90
  const portEnv = process.env.MCP_METRICS_PORT || process.env.PROMETHEUS_PORT;
@@ -93,7 +92,31 @@ export function startMetricsServer(options: MetricsServerOptions): void {
93
92
 
94
93
  if (!port || !Number.isFinite(port) || port <= 0) {
95
94
  logger.debug('Metrics server disabled (set MCP_METRICS_PORT to enable Prometheus /metrics endpoint).');
96
- return;
95
+ return null;
96
+ }
97
+
98
+ const host = process.env.MCP_METRICS_HOST || '127.0.0.1';
99
+
100
+ // Simple rate limiting: max 60 requests per minute per IP
101
+ const RATE_LIMIT_WINDOW_MS = 60000;
102
+ const RATE_LIMIT_MAX_REQUESTS = 60;
103
+ const requestCounts = new Map<string, { count: number; resetAt: number }>();
104
+
105
+ function checkRateLimit(ip: string): boolean {
106
+ const now = Date.now();
107
+ const record = requestCounts.get(ip);
108
+
109
+ if (!record || now >= record.resetAt) {
110
+ requestCounts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
111
+ return true;
112
+ }
113
+
114
+ if (record.count >= RATE_LIMIT_MAX_REQUESTS) {
115
+ return false;
116
+ }
117
+
118
+ record.count++;
119
+ return true;
97
120
  }
98
121
 
99
122
  try {
@@ -117,26 +140,37 @@ export function startMetricsServer(options: MetricsServerOptions): void {
117
140
  return;
118
141
  }
119
142
 
143
+ const clientIp = req.socket.remoteAddress || 'unknown';
144
+ if (!checkRateLimit(clientIp)) {
145
+ res.statusCode = 429;
146
+ res.setHeader('Retry-After', '60');
147
+ res.end('Too Many Requests');
148
+ return;
149
+ }
150
+
120
151
  try {
121
152
  const body = formatPrometheusMetrics(options);
122
153
  res.statusCode = 200;
123
154
  res.setHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
124
155
  res.end(body);
125
156
  } catch (err) {
126
- logger.warn('Failed to render /metrics payload', err as any);
157
+ logger.warn('Failed to render /metrics payload', err as Error);
127
158
  res.statusCode = 500;
128
159
  res.end('Internal Server Error');
129
160
  }
130
161
  });
131
162
 
132
- server.listen(port, () => {
133
- logger.info(`Prometheus metrics server listening on http://${DEFAULT_AUTOMATION_HOST}:${port}/metrics`);
163
+ server.listen(port, host, () => {
164
+ logger.info(`Prometheus metrics server listening on http://${host}:${port}/metrics`);
134
165
  });
135
166
 
136
167
  server.on('error', (err) => {
137
168
  logger.warn('Metrics server error', err as any);
138
169
  });
170
+
171
+ return server;
139
172
  } catch (err) {
140
173
  logger.warn('Failed to start metrics server', err as any);
174
+ return null;
141
175
  }
142
176
  }
@@ -487,7 +487,7 @@ Supported actions:
487
487
  'create_volumetric_fog', 'create_particle_trail', 'create_environment_effect', 'create_impact_effect', 'create_niagara_ribbon',
488
488
  'activate', 'activate_effect', 'deactivate', 'reset', 'advance_simulation',
489
489
  'add_niagara_module', 'connect_niagara_pins', 'remove_niagara_node', 'set_niagara_parameter',
490
- 'clear_debug_shapes', 'cleanup'
490
+ 'clear_debug_shapes', 'cleanup', 'list_debug_shapes'
491
491
  ],
492
492
  description: 'Action'
493
493
  },
@@ -737,7 +737,7 @@ Supported actions:
737
737
  'get_properties', 'set_properties', 'duplicate', 'rename', 'delete', 'list', 'get_metadata', 'set_metadata',
738
738
  'add_spawnable_from_class', 'add_track', 'add_section', 'set_display_rate', 'set_tick_resolution',
739
739
  'set_work_range', 'set_view_range', 'set_track_muted', 'set_track_solo', 'set_track_locked',
740
- 'list_tracks', 'remove_track'
740
+ 'list_tracks', 'remove_track', 'list_track_types'
741
741
  ],
742
742
  description: 'Action'
743
743
  },
@@ -1100,7 +1100,7 @@ Supported actions:
1100
1100
  'spawn_light', 'create_light', 'spawn_sky_light', 'create_sky_light', 'ensure_single_sky_light',
1101
1101
  'create_lightmass_volume', 'create_lighting_enabled_level', 'create_dynamic_light',
1102
1102
  'setup_global_illumination', 'configure_shadows', 'set_exposure', 'set_ambient_occlusion', 'setup_volumetric_fog',
1103
- 'build_lighting'
1103
+ 'build_lighting', 'list_light_types'
1104
1104
  ],
1105
1105
  description: 'Action'
1106
1106
  },
@@ -611,4 +611,12 @@ export class DebugVisualizationTools {
611
611
  return { success: false, error: `Failed to clear debug shapes: ${err}` };
612
612
  }
613
613
  }
614
+
615
+ async listDebugShapes() {
616
+ if (this.automationBridge) {
617
+ const response = await this.automationBridge.sendAutomationRequest('list_debug_shapes', {});
618
+ return response;
619
+ }
620
+ return { success: false, error: 'AUTOMATION_BRIDGE_REQUIRED', message: 'Listing debug shapes requires Automation Bridge' };
621
+ }
614
622
  }
@@ -2,33 +2,14 @@ import { ITools } from '../../types/tool-interfaces.js';
2
2
  import { executeAutomationRequest } from './common-handlers.js';
3
3
  import { normalizeArgs } from './argument-helper.js';
4
4
  import { ResponseFactory } from '../../utils/response-factory.js';
5
+ import { ACTOR_CLASS_ALIASES, getRequiredComponent } from '../../config/class-aliases.js';
5
6
 
6
7
  type ActorActionHandler = (args: any, tools: ITools) => Promise<any>;
7
8
 
8
9
  const handlers: Record<string, ActorActionHandler> = {
9
10
  spawn: async (args, tools) => {
10
- // Class name aliases for user-friendly names
11
- const classAliases: Record<string, string> = {
12
- 'SplineActor': '/Script/Engine.Actor', // Use Actor with SplineComponent
13
- 'Spline': '/Script/Engine.Actor', // Use Actor with SplineComponent
14
- 'PointLight': '/Script/Engine.PointLight',
15
- 'SpotLight': '/Script/Engine.SpotLight',
16
- 'DirectionalLight': '/Script/Engine.DirectionalLight',
17
- 'Camera': '/Script/Engine.CameraActor',
18
- 'CameraActor': '/Script/Engine.CameraActor',
19
- 'StaticMeshActor': '/Script/Engine.StaticMeshActor',
20
- 'SkeletalMeshActor': '/Script/Engine.SkeletalMeshActor',
21
- 'PlayerStart': '/Script/Engine.PlayerStart',
22
- 'TriggerBox': '/Script/Engine.TriggerBox',
23
- 'TriggerSphere': '/Script/Engine.TriggerSphere',
24
- 'BlockingVolume': '/Script/Engine.BlockingVolume',
25
- 'Pawn': '/Script/Engine.Pawn',
26
- 'Character': '/Script/Engine.Character',
27
- 'Actor': '/Script/Engine.Actor'
28
- };
29
-
30
11
  const params = normalizeArgs(args, [
31
- { key: 'classPath', aliases: ['class', 'type', 'actorClass'], required: true, map: classAliases },
12
+ { key: 'classPath', aliases: ['class', 'type', 'actorClass'], required: true, map: ACTOR_CLASS_ALIASES },
32
13
  { key: 'actorName', aliases: ['name'] },
33
14
  { key: 'timeoutMs', default: undefined }
34
15
  ]);
@@ -39,20 +20,13 @@ const handlers: Record<string, ActorActionHandler> = {
39
20
  // failure so tests can exercise timeout handling deterministically
40
21
  // without relying on editor performance.
41
22
  if (typeof timeoutMs === 'number' && timeoutMs > 0 && timeoutMs < 200) {
42
- return {
43
- success: false,
44
- error: `Timeout too small for spawn operation: ${timeoutMs}ms`,
45
- message: 'Timeout too small for spawn operation'
46
- };
23
+ throw new Error(`Timeout too small for spawn operation: ${timeoutMs}ms`);
47
24
  }
48
25
 
49
26
  // For SplineActor alias, add SplineComponent automatically
50
- // Check original args for raw input or assume normalized classPath check is sufficient if map didn't obscure it?
51
- // Map transforms 'SplineActor' -> '/Script/Engine.Actor', so we check original args.
27
+ // Check original args for raw input since map transforms the alias
52
28
  const originalClass = args.classPath || args.class || args.type || args.actorClass;
53
- const componentToAdd = (originalClass === 'SplineActor' || originalClass === 'Spline')
54
- ? 'SplineComponent'
55
- : undefined;
29
+ const componentToAdd = getRequiredComponent(originalClass);
56
30
 
57
31
  const result = await tools.actorTools.spawn({
58
32
  classPath: params.classPath,
@@ -188,9 +188,27 @@ export async function handleAssetTools(action: string, args: any, tools: ITools)
188
188
  throw new Error('No paths provided for delete action');
189
189
  }
190
190
 
191
- const res = await tools.assetTools.deleteAssets({ paths });
191
+ // Normalize paths: strip object sub-path suffix (e.g., /Game/Folder/Asset.Asset -> /Game/Folder/Asset)
192
+ // This handles the common pattern where full object paths are provided instead of package paths
193
+ const normalizedPaths = paths.map(p => {
194
+ let normalized = p.replace(/\\/g, '/').trim();
195
+ // If the path contains a dot after the last slash, it's likely an object path (e.g., /Game/Folder/Asset.Asset)
196
+ const lastSlash = normalized.lastIndexOf('/');
197
+ if (lastSlash >= 0) {
198
+ const afterSlash = normalized.substring(lastSlash + 1);
199
+ const dotIndex = afterSlash.indexOf('.');
200
+ if (dotIndex > 0) {
201
+ // Strip the .ObjectName suffix
202
+ normalized = normalized.substring(0, lastSlash + 1 + dotIndex);
203
+ }
204
+ }
205
+ return normalized;
206
+ });
207
+
208
+ const res = await tools.assetTools.deleteAssets({ paths: normalizedPaths });
192
209
  return ResponseFactory.success(res, 'Assets deleted successfully');
193
210
  }
211
+
194
212
  case 'generate_lods': {
195
213
  const params = normalizeArgs(args, [
196
214
  { key: 'assetPath', required: true },
@@ -368,7 +368,7 @@ export async function handleBlueprintTools(action: string, args: any, tools: ITo
368
368
  }
369
369
 
370
370
  export async function handleBlueprintGet(args: any, tools: ITools) {
371
- const res = await executeAutomationRequest(tools, 'blueprint_get', args, 'Automation bridge not available for blueprint operations');
371
+ const res = await executeAutomationRequest(tools, 'blueprint_get', args, 'Automation bridge not available for blueprint operations') as { success?: boolean; message?: string } | null;
372
372
  if (res && res.success) {
373
373
  return cleanObject({
374
374
  ...res,
@@ -1,12 +1,19 @@
1
1
  import { ITools } from '../../types/tool-interfaces.js';
2
+ import type { HandlerArgs, Vector3, Rotator } from '../../types/handler-types.js';
2
3
 
3
- export function ensureArgsPresent(args: any) {
4
+ /**
5
+ * Validates that args is not null/undefined.
6
+ */
7
+ export function ensureArgsPresent(args: unknown): asserts args is Record<string, unknown> {
4
8
  if (args === null || args === undefined) {
5
9
  throw new Error('Invalid arguments: null or undefined');
6
10
  }
7
11
  }
8
12
 
9
- export function requireAction(args: any): string {
13
+ /**
14
+ * Extracts and validates the 'action' field from args.
15
+ */
16
+ export function requireAction(args: HandlerArgs): string {
10
17
  ensureArgsPresent(args);
11
18
  const action = args.action;
12
19
  if (typeof action !== 'string' || action.trim() === '') {
@@ -15,20 +22,26 @@ export function requireAction(args: any): string {
15
22
  return action;
16
23
  }
17
24
 
18
- export function requireNonEmptyString(value: any, field: string, message?: string): string {
25
+ /**
26
+ * Validates that a value is a non-empty string.
27
+ */
28
+ export function requireNonEmptyString(value: unknown, field: string, message?: string): string {
19
29
  if (typeof value !== 'string' || value.trim() === '') {
20
30
  throw new Error(message ?? `Invalid ${field}: must be a non-empty string`);
21
31
  }
22
32
  return value;
23
33
  }
24
34
 
35
+ /**
36
+ * Execute a request via the automation bridge.
37
+ */
25
38
  export async function executeAutomationRequest(
26
39
  tools: ITools,
27
40
  toolName: string,
28
- args: any,
41
+ args: HandlerArgs,
29
42
  errorMessage: string = 'Automation bridge not available',
30
43
  options: { timeoutMs?: number } = {}
31
- ) {
44
+ ): Promise<unknown> {
32
45
  const automationBridge = tools.automationBridge;
33
46
  // If the bridge is missing or not a function, we can't proceed with automation requests
34
47
  if (!automationBridge || typeof automationBridge.sendAutomationRequest !== 'function') {
@@ -42,11 +55,14 @@ export async function executeAutomationRequest(
42
55
  return await automationBridge.sendAutomationRequest(toolName, args, options);
43
56
  }
44
57
 
58
+ /** Input type for location normalization */
59
+ type LocationInput = Vector3 | [number, number, number] | number[] | null | undefined;
60
+
45
61
  /**
46
62
  * Normalize location to [x, y, z] array format
47
63
  * Accepts both {x,y,z} object and [x,y,z] array formats
48
64
  */
49
- export function normalizeLocation(location: any): [number, number, number] | undefined {
65
+ export function normalizeLocation(location: LocationInput): [number, number, number] | undefined {
50
66
  if (!location) return undefined;
51
67
 
52
68
  // Already array format
@@ -56,17 +72,21 @@ export function normalizeLocation(location: any): [number, number, number] | und
56
72
 
57
73
  // Object format {x, y, z}
58
74
  if (typeof location === 'object' && ('x' in location || 'y' in location || 'z' in location)) {
59
- return [Number(location.x) || 0, Number(location.y) || 0, Number(location.z) || 0];
75
+ const loc = location as Vector3;
76
+ return [Number(loc.x) || 0, Number(loc.y) || 0, Number(loc.z) || 0];
60
77
  }
61
78
 
62
79
  return undefined;
63
80
  }
64
81
 
82
+ /** Input type for rotation normalization */
83
+ type RotationInput = Rotator | [number, number, number] | number[] | null | undefined;
84
+
65
85
  /**
66
86
  * Normalize rotation to {pitch, yaw, roll} object format
67
87
  * Accepts both {pitch,yaw,roll} object and [pitch,yaw,roll] array formats
68
88
  */
69
- export function normalizeRotation(rotation: any): { pitch: number; yaw: number; roll: number } | undefined {
89
+ export function normalizeRotation(rotation: RotationInput): Rotator | undefined {
70
90
  if (!rotation) return undefined;
71
91
 
72
92
  // Array format [pitch, yaw, roll]
@@ -76,10 +96,11 @@ export function normalizeRotation(rotation: any): { pitch: number; yaw: number;
76
96
 
77
97
  // Already object format
78
98
  if (typeof rotation === 'object') {
99
+ const rot = rotation as Rotator;
79
100
  return {
80
- pitch: Number(rotation.pitch) || 0,
81
- yaw: Number(rotation.yaw) || 0,
82
- roll: Number(rotation.roll) || 0
101
+ pitch: Number(rot.pitch) || 0,
102
+ yaw: Number(rot.yaw) || 0,
103
+ roll: Number(rot.roll) || 0
83
104
  };
84
105
  }
85
106
 
@@ -1,8 +1,9 @@
1
1
  import { cleanObject } from '../../utils/safe-json.js';
2
2
  import { ITools } from '../../types/tool-interfaces.js';
3
+ import type { EditorArgs } from '../../types/handler-types.js';
3
4
  import { executeAutomationRequest, requireNonEmptyString } from './common-handlers.js';
4
5
 
5
- export async function handleEditorTools(action: string, args: any, tools: ITools) {
6
+ export async function handleEditorTools(action: string, args: EditorArgs, tools: ITools) {
6
7
  switch (action) {
7
8
  case 'play': {
8
9
  const res = await tools.editorTools.playInEditor(args.timeoutMs);
@@ -16,14 +17,14 @@ export async function handleEditorTools(action: string, args: any, tools: ITools
16
17
  case 'eject': {
17
18
  const inPie = await tools.editorTools.isInPIE();
18
19
  if (!inPie) {
19
- return { success: false, error: 'NOT_IN_PIE', message: 'Cannot eject while not in PIE' };
20
+ throw new Error('Cannot eject while not in PIE');
20
21
  }
21
22
  return await executeAutomationRequest(tools, 'control_editor', { action: 'eject' });
22
23
  }
23
24
  case 'possess': {
24
25
  const inPie = await tools.editorTools.isInPIE();
25
26
  if (!inPie) {
26
- return { success: false, error: 'NOT_IN_PIE', message: 'Cannot possess actor while not in PIE' };
27
+ throw new Error('Cannot possess actor while not in PIE');
27
28
  }
28
29
  return await executeAutomationRequest(tools, 'control_editor', args);
29
30
  }
@@ -40,7 +41,7 @@ export async function handleEditorTools(action: string, args: any, tools: ITools
40
41
  return cleanObject(res);
41
42
  }
42
43
  case 'console_command': {
43
- const res = await tools.editorTools.executeConsoleCommand(args.command);
44
+ const res = await tools.editorTools.executeConsoleCommand(args.command ?? '');
44
45
  return cleanObject(res);
45
46
  }
46
47
  case 'set_camera': {
@@ -63,17 +64,17 @@ export async function handleEditorTools(action: string, args: any, tools: ITools
63
64
  return { success: true, message: 'Stepped frame', action: 'step_frame' };
64
65
  }
65
66
  case 'create_bookmark': {
66
- const idx = parseInt(args.bookmarkName) || 0;
67
+ const idx = parseInt(args.bookmarkName ?? '0') || 0;
67
68
  await tools.editorTools.executeConsoleCommand(`r.SetBookmark ${idx}`);
68
69
  return { success: true, message: `Created bookmark ${idx}`, action: 'create_bookmark' };
69
70
  }
70
71
  case 'jump_to_bookmark': {
71
- const idx = parseInt(args.bookmarkName) || 0;
72
+ const idx = parseInt(args.bookmarkName ?? '0') || 0;
72
73
  await tools.editorTools.executeConsoleCommand(`r.JumpToBookmark ${idx}`);
73
74
  return { success: true, message: `Jumped to bookmark ${idx}`, action: 'jump_to_bookmark' };
74
75
  }
75
76
  case 'set_preferences': {
76
- const res = await tools.editorTools.setEditorPreferences(args.category, args.preferences);
77
+ const res = await tools.editorTools.setEditorPreferences(args.category ?? '', args.preferences ?? {});
77
78
  return cleanObject(res);
78
79
  }
79
80
  case 'open_asset': {
@@ -100,6 +100,10 @@ export async function handleEffectTools(action: string, args: any, tools: ITools
100
100
  if (action === 'clear_debug_shapes') {
101
101
  return executeAutomationRequest(tools, action, args);
102
102
  }
103
+ // Discovery action: list available debug shape types
104
+ if (action === 'list_debug_shapes') {
105
+ return executeAutomationRequest(tools, 'list_debug_shapes', args);
106
+ }
103
107
  if (action === 'cleanup') {
104
108
  args.action = 'cleanup';
105
109
  args.subAction = 'cleanup';
@@ -1,8 +1,9 @@
1
1
  import { cleanObject } from '../../utils/safe-json.js';
2
2
  import { ITools } from '../../types/tool-interfaces.js';
3
+ import type { GraphArgs } from '../../types/handler-types.js';
3
4
  import { executeAutomationRequest } from './common-handlers.js';
4
5
 
5
- export async function handleGraphTools(toolName: string, action: string, args: any, tools: ITools) {
6
+ export async function handleGraphTools(toolName: string, action: string, args: GraphArgs, tools: ITools) {
6
7
  // Common validation
7
8
  if (!args.assetPath && !args.blueprintPath && !args.systemPath) {
8
9
  // Some actions might not need a path if they operate on "currently open" asset,
@@ -20,11 +21,11 @@ export async function handleGraphTools(toolName: string, action: string, args: a
20
21
  case 'manage_behavior_tree':
21
22
  return handleBehaviorTree(action, args, tools);
22
23
  default:
23
- return { success: false, error: 'UNKNOWN_TOOL', message: `Unknown graph tool: ${toolName}` };
24
+ throw new Error(`Unknown graph tool: ${toolName}`);
24
25
  }
25
26
  }
26
27
 
27
- async function handleBlueprintGraph(action: string, args: any, tools: ITools) {
28
+ async function handleBlueprintGraph(action: string, args: GraphArgs, tools: ITools) {
28
29
  const processedArgs = { ...args, subAction: action };
29
30
 
30
31
  // Default graphName
@@ -95,7 +96,7 @@ async function handleBlueprintGraph(action: string, args: any, tools: ITools) {
95
96
  return cleanObject({ ...res, ...(res.result || {}) });
96
97
  }
97
98
 
98
- async function handleNiagaraGraph(action: string, args: any, tools: ITools) {
99
+ async function handleNiagaraGraph(action: string, args: GraphArgs, tools: ITools) {
99
100
  const payload = { ...args, subAction: action };
100
101
  // Map systemPath to assetPath if missing
101
102
  if (payload.systemPath && !payload.assetPath) {
@@ -105,12 +106,12 @@ async function handleNiagaraGraph(action: string, args: any, tools: ITools) {
105
106
  return cleanObject({ ...res, ...(res.result || {}) });
106
107
  }
107
108
 
108
- async function handleMaterialGraph(action: string, args: any, tools: ITools) {
109
+ async function handleMaterialGraph(action: string, args: GraphArgs, tools: ITools) {
109
110
  const res: any = await executeAutomationRequest(tools, 'manage_material_graph', { ...args, subAction: action }, 'Automation bridge not available');
110
111
  return cleanObject({ ...res, ...(res.result || {}) });
111
112
  }
112
113
 
113
- async function handleBehaviorTree(action: string, args: any, tools: ITools) {
114
+ async function handleBehaviorTree(action: string, args: GraphArgs, tools: ITools) {
114
115
  const res: any = await executeAutomationRequest(tools, 'manage_behavior_tree', { ...args, subAction: action }, 'Automation bridge not available');
115
116
  return cleanObject({ ...res, ...(res.result || {}) });
116
117
  }
@@ -1,8 +1,9 @@
1
1
  import { cleanObject } from '../../utils/safe-json.js';
2
2
  import { ITools } from '../../types/tool-interfaces.js';
3
+ import type { LevelArgs } from '../../types/handler-types.js';
3
4
  import { executeAutomationRequest, requireNonEmptyString } from './common-handlers.js';
4
5
 
5
- export async function handleLevelTools(action: string, args: any, tools: ITools) {
6
+ export async function handleLevelTools(action: string, args: LevelArgs, tools: ITools) {
6
7
  switch (action) {
7
8
  case 'load':
8
9
  case 'load_level': {
@@ -118,14 +119,14 @@ export async function handleLevelTools(action: string, args: any, tools: ITools)
118
119
  case 'export_level': {
119
120
  const res = await tools.levelTools.exportLevel({
120
121
  levelPath: args.levelPath,
121
- exportPath: args.exportPath || args.destinationPath,
122
+ exportPath: args.exportPath ?? args.destinationPath ?? '',
122
123
  timeoutMs: typeof args.timeoutMs === 'number' ? args.timeoutMs : undefined
123
124
  });
124
125
  return cleanObject(res);
125
126
  }
126
127
  case 'import_level': {
127
128
  const res = await tools.levelTools.importLevel({
128
- packagePath: args.packagePath || args.sourcePath, // Allow sourcePath as fallback for backward compat
129
+ packagePath: args.packagePath ?? args.sourcePath ?? '',
129
130
  destinationPath: args.destinationPath,
130
131
  timeoutMs: typeof args.timeoutMs === 'number' ? args.timeoutMs : undefined
131
132
  });
@@ -140,7 +141,7 @@ export async function handleLevelTools(action: string, args: any, tools: ITools)
140
141
  return cleanObject(res);
141
142
  }
142
143
  case 'delete': {
143
- const levelPaths = Array.isArray(args.levelPaths) ? args.levelPaths : [args.levelPath];
144
+ const levelPaths = Array.isArray(args.levelPaths) ? args.levelPaths.filter((p): p is string => typeof p === 'string') : (args.levelPath ? [args.levelPath] : []);
144
145
  const res = await tools.levelTools.deleteLevels({ levelPaths });
145
146
  return cleanObject(res);
146
147
  }
@@ -1,8 +1,9 @@
1
1
  import { cleanObject } from '../../utils/safe-json.js';
2
2
  import { ITools } from '../../types/tool-interfaces.js';
3
+ import type { LightingArgs } from '../../types/handler-types.js';
3
4
  import { normalizeLocation } from './common-handlers.js';
4
5
 
5
- export async function handleLightingTools(action: string, args: any, tools: ITools) {
6
+ export async function handleLightingTools(action: string, args: LightingArgs, tools: ITools) {
6
7
  // Normalize location parameter to accept both {x,y,z} and [x,y,z] formats
7
8
  const normalizedLocation = normalizeLocation(args.location);
8
9
 
@@ -141,6 +142,9 @@ export async function handleLightingTools(action: string, args: any, tools: IToo
141
142
  useTemplate: args.useTemplate
142
143
  }));
143
144
  }
145
+ case 'list_light_types': {
146
+ return cleanObject(await tools.lightingTools.listLightTypes());
147
+ }
144
148
  default:
145
149
  throw new Error(`Unknown lighting action: ${action}`);
146
150
  }