unreal-engine-mcp-server 0.5.1 → 0.5.2

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 (75) hide show
  1. package/.github/workflows/publish-mcp.yml +1 -4
  2. package/.github/workflows/release-drafter.yml +2 -1
  3. package/CHANGELOG.md +38 -0
  4. package/dist/automation/bridge.d.ts +1 -2
  5. package/dist/automation/bridge.js +24 -23
  6. package/dist/automation/connection-manager.d.ts +1 -0
  7. package/dist/automation/connection-manager.js +10 -0
  8. package/dist/automation/message-handler.js +5 -4
  9. package/dist/automation/request-tracker.d.ts +4 -0
  10. package/dist/automation/request-tracker.js +11 -3
  11. package/dist/tools/actors.d.ts +19 -1
  12. package/dist/tools/actors.js +15 -5
  13. package/dist/tools/assets.js +1 -1
  14. package/dist/tools/blueprint.d.ts +12 -0
  15. package/dist/tools/blueprint.js +43 -14
  16. package/dist/tools/consolidated-tool-definitions.js +2 -1
  17. package/dist/tools/editor.js +3 -2
  18. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  19. package/dist/tools/handlers/actor-handlers.js +14 -8
  20. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  21. package/dist/tools/handlers/sequence-handlers.js +24 -13
  22. package/dist/tools/introspection.d.ts +1 -1
  23. package/dist/tools/introspection.js +1 -1
  24. package/dist/tools/level.js +3 -3
  25. package/dist/tools/lighting.d.ts +54 -7
  26. package/dist/tools/lighting.js +4 -4
  27. package/dist/tools/materials.d.ts +1 -1
  28. package/dist/types/tool-types.d.ts +2 -0
  29. package/dist/unreal-bridge.js +4 -4
  30. package/dist/utils/command-validator.js +6 -5
  31. package/dist/utils/error-handler.d.ts +24 -2
  32. package/dist/utils/error-handler.js +58 -23
  33. package/dist/utils/normalize.d.ts +7 -4
  34. package/dist/utils/normalize.js +12 -10
  35. package/dist/utils/response-validator.js +88 -73
  36. package/dist/utils/unreal-command-queue.d.ts +2 -0
  37. package/dist/utils/unreal-command-queue.js +8 -1
  38. package/docs/handler-mapping.md +4 -2
  39. package/package.json +1 -1
  40. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  41. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  42. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  43. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  44. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  45. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  46. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  47. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  48. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  49. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  50. package/server.json +3 -3
  51. package/src/automation/bridge.ts +27 -25
  52. package/src/automation/connection-manager.ts +18 -0
  53. package/src/automation/message-handler.ts +33 -8
  54. package/src/automation/request-tracker.ts +39 -7
  55. package/src/server/tool-registry.ts +3 -3
  56. package/src/tools/actors.ts +44 -19
  57. package/src/tools/assets.ts +3 -3
  58. package/src/tools/blueprint.ts +115 -49
  59. package/src/tools/consolidated-tool-definitions.ts +2 -1
  60. package/src/tools/editor.ts +4 -3
  61. package/src/tools/handlers/actor-handlers.ts +14 -9
  62. package/src/tools/handlers/sequence-handlers.ts +86 -63
  63. package/src/tools/introspection.ts +7 -7
  64. package/src/tools/level.ts +6 -6
  65. package/src/tools/lighting.ts +19 -19
  66. package/src/tools/materials.ts +1 -1
  67. package/src/tools/sequence.ts +1 -1
  68. package/src/tools/ui.ts +1 -1
  69. package/src/types/tool-types.ts +4 -0
  70. package/src/unreal-bridge.ts +71 -26
  71. package/src/utils/command-validator.ts +46 -5
  72. package/src/utils/error-handler.ts +128 -45
  73. package/src/utils/normalize.ts +38 -16
  74. package/src/utils/response-validator.ts +103 -87
  75. package/src/utils/unreal-command-queue.ts +13 -1
@@ -1,7 +1,18 @@
1
1
  import { cleanObject } from '../../utils/safe-json.js';
2
- import { ITools } from '../../types/tool-interfaces.js';
2
+ import { ITools, StandardActionResponse } from '../../types/tool-interfaces.js';
3
3
  import { executeAutomationRequest, requireNonEmptyString } from './common-handlers.js';
4
4
 
5
+ /** Extended response with common sequence fields */
6
+ interface SequenceActionResponse extends StandardActionResponse {
7
+ result?: {
8
+ sequencePath?: string;
9
+ results?: Array<{ success?: boolean; error?: string }>;
10
+ [key: string]: unknown;
11
+ };
12
+ bindings?: Array<{ name?: string;[key: string]: unknown }>;
13
+ message?: string;
14
+ }
15
+
5
16
  const managedSequences = new Set<string>();
6
17
  const deletedSequences = new Set<string>();
7
18
 
@@ -22,37 +33,48 @@ function markSequenceDeleted(path: unknown) {
22
33
  const norm = normalizeSequencePath(path);
23
34
  if (!norm) return;
24
35
  managedSequences.delete(norm);
25
- deletedSequences.add(norm);
36
+ deletedSequences.delete(norm);
37
+ }
38
+
39
+ /** Helper to safely get string from error/message */
40
+ function getErrorString(res: SequenceActionResponse | null | undefined): string {
41
+ if (!res) return '';
42
+ return typeof res.error === 'string' ? res.error : '';
43
+ }
44
+
45
+ function getMessageString(res: SequenceActionResponse | null | undefined): string {
46
+ if (!res) return '';
47
+ return typeof res.message === 'string' ? res.message : '';
26
48
  }
27
49
 
28
50
 
29
51
 
30
- export async function handleSequenceTools(action: string, args: any, tools: ITools) {
52
+ export async function handleSequenceTools(action: string, args: Record<string, unknown>, tools: ITools) {
31
53
  const seqAction = String(action || '').trim();
32
54
  switch (seqAction) {
33
55
  case 'create': {
34
56
  const name = requireNonEmptyString(args.name, 'name', 'Missing required parameter: name');
35
- const res = await tools.sequenceTools.create({ name, path: args.path });
57
+ const res = await tools.sequenceTools.create({ name, path: args.path as string | undefined }) as SequenceActionResponse;
36
58
 
37
59
  let sequencePath: string | undefined;
38
- if (res && (res as any).result && typeof (res as any).result.sequencePath === 'string') {
39
- sequencePath = (res as any).result.sequencePath;
60
+ if (res && res.result && typeof res.result.sequencePath === 'string') {
61
+ sequencePath = res.result.sequencePath;
40
62
  } else if (typeof args.path === 'string' && args.path.trim().length > 0) {
41
63
  const basePath = args.path.trim().replace(/\/$/, '');
42
64
  sequencePath = `${basePath}/${name}`;
43
65
  }
44
- if (sequencePath && res && (res as any).success !== false) {
66
+ if (sequencePath && res && res.success !== false) {
45
67
  markSequenceCreated(sequencePath);
46
68
  }
47
69
 
48
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
49
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
50
- if (res && (res as any).success === false && (errorCode === 'FACTORY_NOT_AVAILABLE' || msgLower.includes('ulevelsequencefactorynew not available'))) {
70
+ const errorCode = getErrorString(res).toUpperCase();
71
+ const msgLower = getMessageString(res).toLowerCase();
72
+ if (res && res.success === false && (errorCode === 'FACTORY_NOT_AVAILABLE' || msgLower.includes('ulevelsequencefactorynew not available'))) {
51
73
  const path = sequencePath || (typeof args.path === 'string' ? args.path : undefined);
52
74
  return cleanObject({
53
75
  success: false,
54
76
  error: 'FACTORY_NOT_AVAILABLE',
55
- message: (res as any).message || 'Sequence creation failed: factory not available',
77
+ message: res.message || 'Sequence creation failed: factory not available',
56
78
  action: 'create',
57
79
  name,
58
80
  path,
@@ -69,7 +91,7 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
69
91
  return cleanObject(res);
70
92
  }
71
93
  case 'add_camera': {
72
- const res = await tools.sequenceTools.addCamera({ spawnable: args.spawnable, path: args.path });
94
+ const res = await tools.sequenceTools.addCamera({ spawnable: args.spawnable as boolean | undefined, path: args.path as string | undefined });
73
95
  return cleanObject(res);
74
96
  }
75
97
  case 'add_actor': {
@@ -82,18 +104,18 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
82
104
  subAction: 'add_actor'
83
105
  };
84
106
 
85
- const res = await executeAutomationRequest(tools, 'manage_sequence', payload);
107
+ const res = await executeAutomationRequest(tools, 'manage_sequence', payload) as SequenceActionResponse;
86
108
 
87
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
88
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
109
+ const errorCode = getErrorString(res).toUpperCase();
110
+ const msgLower = getMessageString(res).toLowerCase();
89
111
 
90
- if (res && (res as any).success === false && path) {
112
+ if (res && res.success === false && path) {
91
113
  const isInvalidSequence = errorCode === 'INVALID_SEQUENCE' || msgLower.includes('sequence_add_actor requires a sequence path') || msgLower.includes('sequence not found');
92
114
  if (isInvalidSequence) {
93
115
  return cleanObject({
94
116
  success: false,
95
117
  error: 'NOT_FOUND',
96
- message: (res as any).message || 'Sequence not found',
118
+ message: res.message || 'Sequence not found',
97
119
  action: 'add_actor',
98
120
  path,
99
121
  actorName
@@ -101,8 +123,8 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
101
123
  }
102
124
  }
103
125
 
104
- const results = res && (res as any).result && Array.isArray((res as any).result.results)
105
- ? (res as any).result.results as any[]
126
+ const results = res && res.result && Array.isArray(res.result.results)
127
+ ? res.result.results
106
128
  : undefined;
107
129
  if (results && results.length) {
108
130
  const failed = results.find((item) => item && item.success === false && typeof item.error === 'string');
@@ -124,24 +146,24 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
124
146
  return cleanObject(res);
125
147
  }
126
148
  case 'add_actors': {
127
- const actorNames: string[] = Array.isArray(args.actorNames) ? args.actorNames : [];
128
- const res = await tools.sequenceTools.addActors({ actorNames, path: args.path });
129
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
130
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
131
- if (actorNames.length === 0 && res && (res as any).success === false && errorCode === 'INVALID_ARGUMENT') {
149
+ const actorNames: string[] = Array.isArray(args.actorNames) ? args.actorNames as string[] : [];
150
+ const res = await tools.sequenceTools.addActors({ actorNames, path: args.path as string | undefined }) as SequenceActionResponse;
151
+ const errorCode = getErrorString(res).toUpperCase();
152
+ const msgLower = getMessageString(res).toLowerCase();
153
+ if (actorNames.length === 0 && res && res.success === false && errorCode === 'INVALID_ARGUMENT') {
132
154
  return cleanObject({
133
155
  success: false,
134
156
  error: 'INVALID_ARGUMENT',
135
- message: (res as any).message || 'Invalid argument: actorNames required',
157
+ message: res.message || 'Invalid argument: actorNames required',
136
158
  action: 'add_actors',
137
159
  actorNames
138
160
  });
139
161
  }
140
- if (res && (res as any).success === false && msgLower.includes('actor not found')) {
162
+ if (res && res.success === false && msgLower.includes('actor not found')) {
141
163
  return cleanObject({
142
164
  success: false,
143
165
  error: 'NOT_FOUND',
144
- message: (res as any).message || 'Actor not found',
166
+ message: res.message || 'Actor not found',
145
167
  action: 'add_actors',
146
168
  actorNames
147
169
  });
@@ -149,8 +171,8 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
149
171
  return cleanObject(res);
150
172
  }
151
173
  case 'remove_actors': {
152
- const actorNames: string[] = Array.isArray(args.actorNames) ? args.actorNames : [];
153
- const res = await tools.sequenceTools.removeActors({ actorNames, path: args.path });
174
+ const actorNames: string[] = Array.isArray(args.actorNames) ? args.actorNames as string[] : [];
175
+ const res = await tools.sequenceTools.removeActors({ actorNames, path: args.path as string | undefined });
154
176
  return cleanObject(res);
155
177
  }
156
178
  case 'get_bindings': {
@@ -164,7 +186,7 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
164
186
  const property = typeof args.property === 'string' ? args.property : undefined;
165
187
  const frame = typeof args.frame === 'number' ? args.frame : Number(args.frame);
166
188
 
167
- const payload = {
189
+ const payload: Record<string, unknown> = {
168
190
  ...args,
169
191
  path: path || args.path,
170
192
  actorName,
@@ -185,16 +207,16 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
185
207
  payload.value = { scale: args.value };
186
208
  }
187
209
 
188
- const res = await executeAutomationRequest(tools, 'manage_sequence', payload);
189
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
190
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
210
+ const res = await executeAutomationRequest(tools, 'manage_sequence', payload) as SequenceActionResponse;
211
+ const errorCode = getErrorString(res).toUpperCase();
212
+ const msgLower = getMessageString(res).toLowerCase();
191
213
 
192
214
  // Keep explicit INVALID_ARGUMENT for missing frame as a real error
193
215
  if (errorCode === 'INVALID_ARGUMENT' || msgLower.includes('frame number is required')) {
194
216
  return cleanObject(res);
195
217
  }
196
218
 
197
- if (res && (res as any).success === false) {
219
+ if (res && res.success === false) {
198
220
  const isBindingIssue = errorCode === 'BINDING_NOT_FOUND' || msgLower.includes('binding not found');
199
221
  const isUnsupported = errorCode === 'UNSUPPORTED_PROPERTY' || msgLower.includes('unsupported property') || msgLower.includes('invalid_sequence_type');
200
222
  const isInvalidSeq = errorCode === 'INVALID_SEQUENCE' || msgLower.includes('sequence not found') || msgLower.includes('requires a sequence path');
@@ -203,7 +225,7 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
203
225
  return cleanObject({
204
226
  success: false,
205
227
  error: 'NOT_FOUND',
206
- message: (res as any).message || 'Sequence not found',
228
+ message: res.message || 'Sequence not found',
207
229
  action: 'add_keyframe',
208
230
  path,
209
231
  actorName,
@@ -222,28 +244,28 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
222
244
  }
223
245
  case 'add_spawnable_from_class': {
224
246
  const className = requireNonEmptyString(args.className, 'className', 'Missing required parameter: className');
225
- const res = await tools.sequenceTools.addSpawnableFromClass({ className, path: args.path });
247
+ const res = await tools.sequenceTools.addSpawnableFromClass({ className, path: args.path as string | undefined });
226
248
  return cleanObject(res);
227
249
  }
228
250
  case 'play': {
229
- const res = await tools.sequenceTools.play({ path: args.path, startTime: args.startTime, loopMode: args.loopMode });
251
+ const res = await tools.sequenceTools.play({ path: args.path as string | undefined, startTime: args.startTime as number | undefined, loopMode: args.loopMode as 'once' | 'loop' | 'pingpong' | undefined });
230
252
  return cleanObject(res);
231
253
  }
232
254
  case 'pause': {
233
- const res = await tools.sequenceTools.pause({ path: args.path });
255
+ const res = await tools.sequenceTools.pause({ path: args.path as string | undefined });
234
256
  return cleanObject(res);
235
257
  }
236
258
  case 'stop': {
237
- const res = await tools.sequenceTools.stop({ path: args.path });
259
+ const res = await tools.sequenceTools.stop({ path: args.path as string | undefined });
238
260
  return cleanObject(res);
239
261
  }
240
262
  case 'set_properties': {
241
263
  const res = await tools.sequenceTools.setSequenceProperties({
242
- path: args.path,
243
- frameRate: args.frameRate,
244
- lengthInFrames: args.lengthInFrames,
245
- playbackStart: args.playbackStart,
246
- playbackEnd: args.playbackEnd
264
+ path: args.path as string | undefined,
265
+ frameRate: args.frameRate as number | undefined,
266
+ lengthInFrames: args.lengthInFrames as number | undefined,
267
+ playbackStart: args.playbackStart as number | undefined,
268
+ playbackEnd: args.playbackEnd as number | undefined
247
269
  });
248
270
  return cleanObject(res);
249
271
  }
@@ -258,31 +280,32 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
258
280
  throw new Error('Invalid speed: must be a positive number');
259
281
  }
260
282
  // Try setting speed
261
- let res = await tools.sequenceTools.setPlaybackSpeed({ speed, path: args.path });
283
+ let res = await tools.sequenceTools.setPlaybackSpeed({ speed, path: args.path as string | undefined }) as SequenceActionResponse;
262
284
 
263
285
  // Fix: Auto-open if editor not open
264
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
286
+ const errorCode = getErrorString(res).toUpperCase();
265
287
  if ((!res || res.success === false) && errorCode === 'EDITOR_NOT_OPEN' && args.path) {
266
288
  // Attempt to open the sequence
267
- await tools.sequenceTools.open({ path: args.path });
289
+ await tools.sequenceTools.open({ path: args.path as string });
268
290
 
269
291
  // Wait a short moment for editor to initialize on game thread
270
292
  await new Promise(resolve => setTimeout(resolve, 1000));
271
293
 
272
294
  // Retry
273
- res = await tools.sequenceTools.setPlaybackSpeed({ speed, path: args.path });
295
+ res = await tools.sequenceTools.setPlaybackSpeed({ speed, path: args.path as string | undefined }) as SequenceActionResponse;
274
296
  }
275
297
 
276
298
  return cleanObject(res);
277
299
  }
278
300
  case 'list': {
279
- const res = await tools.sequenceTools.list({ path: args.path });
301
+ const res = await tools.sequenceTools.list({ path: args.path as string | undefined });
280
302
  return cleanObject(res);
281
303
  }
282
304
  case 'duplicate': {
283
305
  const path = requireNonEmptyString(args.path, 'path', 'Missing required parameter: path');
284
306
  const destDir = requireNonEmptyString(args.destinationPath, 'destinationPath', 'Missing required parameter: destinationPath');
285
- const newName = requireNonEmptyString(args.newName || path.split('/').pop(), 'newName', 'Missing required parameter: newName');
307
+ const defaultNewName = path.split('/').pop() || '';
308
+ const newName = requireNonEmptyString(args.newName || defaultNewName, 'newName', 'Missing required parameter: newName');
286
309
  const baseDir = destDir.replace(/\/$/, '');
287
310
  const destPath = `${baseDir}/${newName}`;
288
311
  const res = await tools.sequenceTools.duplicate({ path, destinationPath: destPath });
@@ -291,15 +314,15 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
291
314
  case 'rename': {
292
315
  const path = requireNonEmptyString(args.path, 'path', 'Missing required parameter: path');
293
316
  const newName = requireNonEmptyString(args.newName, 'newName', 'Missing required parameter: newName');
294
- const res = await tools.sequenceTools.rename({ path, newName });
295
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
296
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
297
- if (res && (res as any).success === false && (errorCode === 'OPERATION_FAILED' || msgLower.includes('failed to rename sequence'))) {
317
+ const res = await tools.sequenceTools.rename({ path, newName }) as SequenceActionResponse;
318
+ const errorCode = getErrorString(res).toUpperCase();
319
+ const msgLower = getMessageString(res).toLowerCase();
320
+ if (res && res.success === false && (errorCode === 'OPERATION_FAILED' || msgLower.includes('failed to rename sequence'))) {
298
321
  // Return actual failure, not best-effort success - rename is a destructive operation
299
322
  return cleanObject({
300
323
  success: false,
301
324
  error: 'OPERATION_FAILED',
302
- message: (res as any).message || 'Failed to rename sequence',
325
+ message: res.message || 'Failed to rename sequence',
303
326
  action: 'rename',
304
327
  path,
305
328
  newName
@@ -309,20 +332,20 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
309
332
  }
310
333
  case 'delete': {
311
334
  const path = requireNonEmptyString(args.path, 'path', 'Missing required parameter: path');
312
- const res = await tools.sequenceTools.deleteSequence({ path });
335
+ const res = await tools.sequenceTools.deleteSequence({ path }) as SequenceActionResponse;
313
336
 
314
- if (res && (res as any).success !== false) {
337
+ if (res && res.success !== false) {
315
338
  markSequenceDeleted(path);
316
339
  }
317
340
  return cleanObject(res);
318
341
  }
319
342
  case 'get_metadata': {
320
- const res = await tools.sequenceTools.getMetadata({ path: args.path });
343
+ const res = await tools.sequenceTools.getMetadata({ path: args.path as string });
321
344
  return cleanObject(res);
322
345
  }
323
346
  case 'set_metadata': {
324
347
  const path = requireNonEmptyString(args.path, 'path', 'Missing required parameter: path');
325
- const metadata = (args.metadata && typeof args.metadata === 'object') ? args.metadata : {};
348
+ const metadata = (args.metadata && typeof args.metadata === 'object') ? args.metadata as Record<string, unknown> : {};
326
349
  const res = await executeAutomationRequest(tools, 'set_metadata', { assetPath: path, metadata });
327
350
  return cleanObject(res);
328
351
  }
@@ -335,10 +358,10 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
335
358
 
336
359
  // Fix: Check if actor is bound before adding track
337
360
  if (actorName) {
338
- const bindingsRes = await tools.sequenceTools.getBindings({ path });
361
+ const bindingsRes = await tools.sequenceTools.getBindings({ path }) as SequenceActionResponse;
339
362
  if (bindingsRes && bindingsRes.success) {
340
- const bindings = (bindingsRes.bindings as any[]) || [];
341
- const isBound = bindings.some((b: any) => b.name === actorName);
363
+ const bindings = bindingsRes.bindings || [];
364
+ const isBound = bindings.some((b) => b.name === actorName);
342
365
  if (!isBound) {
343
366
  return cleanObject({
344
367
  success: false,
@@ -399,7 +422,7 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
399
422
  if (!Number.isFinite(end)) throw new Error('Invalid end: must be a number');
400
423
 
401
424
  const res = await tools.sequenceTools.setWorkRange({
402
- path: args.path,
425
+ path: args.path as string | undefined,
403
426
  start,
404
427
  end
405
428
  });
@@ -35,7 +35,7 @@ export interface PropertyInfo {
35
35
  type: string;
36
36
  value?: any;
37
37
  flags?: string[];
38
- metadata?: Record<string, any>;
38
+ metadata?: Record<string, unknown>;
39
39
  category?: string;
40
40
  tooltip?: string;
41
41
  description?: string;
@@ -141,7 +141,7 @@ export class IntrospectionTools {
141
141
  return value;
142
142
  }
143
143
 
144
- private isPlainObject(value: any): value is Record<string, any> {
144
+ private isPlainObject(value: any): value is Record<string, unknown> {
145
145
  return !!value && typeof value === 'object' && !Array.isArray(value);
146
146
  }
147
147
 
@@ -200,7 +200,7 @@ export class IntrospectionTools {
200
200
  const rawValue = entry.value ?? entry.currentValue ?? entry.defaultValue ?? entry.data ?? entry;
201
201
  const value = this.convertPropertyValue(rawValue, type);
202
202
  const flags: string[] | undefined = entry.flags ?? entry.attributes;
203
- const metadata: Record<string, any> | undefined = entry.metadata ?? entry.annotations;
203
+ const metadata: Record<string, unknown> | undefined = entry.metadata ?? entry.annotations;
204
204
  const filtered = this.shouldFilterProperty(name, value, flags, detailed);
205
205
  const dictionaryEntry = lookupPropertyMetadata(name);
206
206
  const propertyInfo: PropertyInfo = {
@@ -220,12 +220,12 @@ export class IntrospectionTools {
220
220
  return propertyInfo;
221
221
  }
222
222
 
223
- private flattenPropertyMap(source: Record<string, any>, prefix = '', detailed = false): PropertyInfo[] {
223
+ private flattenPropertyMap(source: Record<string, unknown>, prefix = '', detailed = false): PropertyInfo[] {
224
224
  const properties: PropertyInfo[] = [];
225
225
  for (const [rawKey, rawValue] of Object.entries(source)) {
226
226
  const name = prefix ? `${prefix}.${rawKey}` : rawKey;
227
227
  if (this.isLikelyPropertyDescriptor(rawValue)) {
228
- const normalized = this.normalizePropertyEntry({ ...rawValue, name }, detailed);
228
+ const normalized = this.normalizePropertyEntry({ ...(rawValue as Record<string, unknown>), name }, detailed);
229
229
  if (normalized) properties.push(normalized);
230
230
  continue;
231
231
  }
@@ -441,8 +441,8 @@ export class IntrospectionTools {
441
441
  }
442
442
 
443
443
  return res;
444
- } catch (err: any) {
445
- const errorMsg = err?.message || String(err);
444
+ } catch (err: unknown) {
445
+ const errorMsg = (err instanceof Error ? err.message : undefined) || String(err);
446
446
  if (errorMsg.includes('404')) {
447
447
  return { success: false, error: `Property '${params.propertyName}' not found on object '${params.objectPath}'` };
448
448
  }
@@ -50,12 +50,12 @@ export class LevelTools extends BaseTool implements ILevelTools {
50
50
  // Security validation
51
51
  try {
52
52
  formatted = sanitizePath(formatted);
53
- } catch (e: any) {
53
+ } catch (e: unknown) {
54
54
  // If sanitizePath fails, we should probably propagate that error,
55
55
  // but normalizeLevelPath signature expects to return an object.
56
56
  // For now, let's log and rethrow or fallback?
57
57
  // Throwing is safer as it prevents operation on invalid path.
58
- throw new Error(`Security validation failed for level path: ${e.message}`);
58
+ throw new Error(`Security validation failed for level path: ${e instanceof Error ? e.message : String(e)}`);
59
59
  }
60
60
 
61
61
  formatted = formatted.replace(/\.umap$/i, '');
@@ -322,8 +322,8 @@ export class LevelTools extends BaseTool implements ILevelTools {
322
322
  exportPath: params.exportPath,
323
323
  details: res
324
324
  } as StandardActionResponse;
325
- } catch (e: any) {
326
- return { success: false, error: `Export failed: ${e.message}` };
325
+ } catch (e: unknown) {
326
+ return { success: false, error: `Export failed: ${e instanceof Error ? e.message : String(e)}` };
327
327
  }
328
328
  }
329
329
 
@@ -356,8 +356,8 @@ export class LevelTools extends BaseTool implements ILevelTools {
356
356
  streaming: Boolean(params.streaming),
357
357
  details: res
358
358
  } as StandardActionResponse;
359
- } catch (e: any) {
360
- return { success: false, error: `Import failed: ${e.message}` };
359
+ } catch (e: unknown) {
360
+ return { success: false, error: `Import failed: ${e instanceof Error ? e.message : String(e)}` };
361
361
  }
362
362
  }
363
363
 
@@ -40,7 +40,7 @@ export class LightingTools {
40
40
  name: string;
41
41
  location?: [number, number, number];
42
42
  rotation?: [number, number, number] | { pitch: number, yaw: number, roll: number };
43
- properties?: Record<string, any>;
43
+ properties?: Record<string, unknown>;
44
44
  }
45
45
  ) {
46
46
  if (!this.automationBridge) {
@@ -48,7 +48,7 @@ export class LightingTools {
48
48
  }
49
49
 
50
50
  try {
51
- const payload: Record<string, any> = {
51
+ const payload: Record<string, unknown> = {
52
52
  lightClass,
53
53
  name: params.name,
54
54
  };
@@ -98,7 +98,7 @@ export class LightingTools {
98
98
  castShadows?: boolean;
99
99
  temperature?: number;
100
100
  useAsAtmosphereSunLight?: boolean;
101
- properties?: Record<string, any>;
101
+ properties?: Record<string, unknown>;
102
102
  }) {
103
103
  const name = this.normalizeName(params.name);
104
104
  if (!this.automationBridge) {
@@ -149,7 +149,7 @@ export class LightingTools {
149
149
  const rot = params.rotation || [0, 0, 0];
150
150
 
151
151
  // Build properties for the light
152
- const properties: Record<string, any> = params.properties || {};
152
+ const properties: Record<string, unknown> = params.properties || {};
153
153
  if (params.intensity !== undefined) {
154
154
  properties.intensity = params.intensity;
155
155
  }
@@ -175,9 +175,9 @@ export class LightingTools {
175
175
  });
176
176
 
177
177
  return { success: true, message: `Directional light '${name}' spawned` };
178
- } catch (e: any) {
178
+ } catch (e: unknown) {
179
179
  // Don't mask errors as "not implemented" - report the actual error from the bridge
180
- return { success: false, error: `Failed to create directional light: ${e?.message ?? e}` } as any;
180
+ return { success: false, error: `Failed to create directional light: ${(e instanceof Error ? e.message : String(e)) ?? e}` };
181
181
  }
182
182
  }
183
183
 
@@ -247,7 +247,7 @@ export class LightingTools {
247
247
  }
248
248
 
249
249
  // Build properties for the light
250
- const properties: Record<string, any> = {};
250
+ const properties: Record<string, unknown> = {};
251
251
  if (params.intensity !== undefined) {
252
252
  properties.intensity = params.intensity;
253
253
  }
@@ -273,9 +273,9 @@ export class LightingTools {
273
273
  });
274
274
 
275
275
  return { success: true, message: `Point light '${name}' spawned at ${location.join(', ')}` };
276
- } catch (e: any) {
276
+ } catch (e: unknown) {
277
277
  // Don't mask errors as "not implemented" - report the actual error from the bridge
278
- return { success: false, error: `Failed to create point light: ${e?.message ?? e}` } as any;
278
+ return { success: false, error: `Failed to create point light: ${(e instanceof Error ? e.message : String(e)) ?? e}` };
279
279
  }
280
280
  }
281
281
 
@@ -369,7 +369,7 @@ export class LightingTools {
369
369
  }
370
370
  }
371
371
  // Build properties for the light
372
- const properties: Record<string, any> = {};
372
+ const properties: Record<string, unknown> = {};
373
373
  if (params.intensity !== undefined) {
374
374
  properties.intensity = params.intensity;
375
375
  }
@@ -398,9 +398,9 @@ export class LightingTools {
398
398
  });
399
399
 
400
400
  return { success: true, message: `Spot light '${name}' spawned at ${params.location.join(', ')}` };
401
- } catch (e: any) {
401
+ } catch (e: unknown) {
402
402
  // Don't mask errors as "not implemented" - report the actual error from the bridge
403
- return { success: false, error: `Failed to create spot light: ${e?.message ?? e}` } as any;
403
+ return { success: false, error: `Failed to create spot light: ${(e instanceof Error ? e.message : String(e)) ?? e}` };
404
404
  }
405
405
  }
406
406
 
@@ -485,7 +485,7 @@ export class LightingTools {
485
485
  }
486
486
  }
487
487
  // Build properties for the light
488
- const properties: Record<string, any> = {};
488
+ const properties: Record<string, unknown> = {};
489
489
  if (params.intensity !== undefined) {
490
490
  properties.intensity = params.intensity;
491
491
  }
@@ -508,9 +508,9 @@ export class LightingTools {
508
508
  });
509
509
 
510
510
  return { success: true, message: `Rect light '${name}' spawned at ${params.location.join(', ')}` };
511
- } catch (e: any) {
511
+ } catch (e: unknown) {
512
512
  // Don't mask errors as "not implemented" - report the actual error from the bridge
513
- return { success: false, error: `Failed to create rect light: ${e?.message ?? e}` } as any;
513
+ return { success: false, error: `Failed to create rect light: ${(e instanceof Error ? e.message : String(e)) ?? e}` };
514
514
  }
515
515
  }
516
516
 
@@ -581,13 +581,13 @@ export class LightingTools {
581
581
  }
582
582
 
583
583
  try {
584
- const properties: Record<string, any> = {};
584
+ const properties: Record<string, unknown> = {};
585
585
  if (params.intensity !== undefined) properties.Intensity = params.intensity;
586
586
  if (params.castShadows !== undefined) properties.CastShadows = params.castShadows;
587
587
  if (params.realTimeCapture !== undefined) properties.RealTimeCapture = params.realTimeCapture;
588
588
  if (params.color) properties.LightColor = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
589
589
 
590
- const payload: Record<string, any> = {
590
+ const payload: Record<string, unknown> = {
591
591
  name,
592
592
  sourceType: params.sourceType || 'CapturedScene',
593
593
  location: params.location,
@@ -811,12 +811,12 @@ export class LightingTools {
811
811
  success: true,
812
812
  message: response.message || 'Lighting build started',
813
813
  ...(response.result || {})
814
- } as any;
814
+ };
815
815
  } catch (error) {
816
816
  return {
817
817
  success: false,
818
818
  error: `Failed to build lighting: ${error instanceof Error ? error.message : String(error)}`
819
- } as any;
819
+ };
820
820
  }
821
821
  }
822
822
 
@@ -120,7 +120,7 @@ export class MaterialTools {
120
120
  }
121
121
  }
122
122
 
123
- async createMaterialInstance(name: string, path: string, parentMaterial: string, parameters?: Record<string, any>) {
123
+ async createMaterialInstance(name: string, path: string, parentMaterial: string, parameters?: Record<string, unknown>) {
124
124
  try {
125
125
  if (!name || name.trim() === '') {
126
126
  return { success: false, error: 'Material instance name cannot be empty' };
@@ -51,7 +51,7 @@ export class SequenceTools extends BaseTool implements ISequenceTools {
51
51
  const result = response.result ?? response;
52
52
 
53
53
  return { success, message: response.message ?? undefined, error: response.success === false ? (response.error ?? response.message) : undefined, result, requestId: response.requestId } as any;
54
- } catch (err: any) {
54
+ } catch (err: unknown) {
55
55
  return { success: false, error: String(err), message: String(err) } as const;
56
56
  }
57
57
  }
package/src/tools/ui.ts CHANGED
@@ -59,7 +59,7 @@ export class UITools {
59
59
  package_path: path,
60
60
  factory_class: 'WidgetBlueprintFactory',
61
61
  asset_class: 'unreal.WidgetBlueprint'
62
- } as Record<string, any>;
62
+ } as Record<string, unknown>;
63
63
 
64
64
  const resp = await this.bridge.executeEditorFunction('CREATE_ASSET', payload as any);
65
65
  const result = resp && typeof resp === 'object' ? (resp.result ?? resp) : resp;
@@ -9,6 +9,10 @@ export interface BaseToolResponse {
9
9
  message?: string;
10
10
  error?: string;
11
11
  warning?: string;
12
+ /** Whether this error is retriable (e.g., connection failures) */
13
+ retriable?: boolean;
14
+ /** Scope/context for the error (e.g., 'tool-call/manage_asset') */
15
+ scope?: string;
12
16
  }
13
17
 
14
18
  // Asset Management Types