unreal-engine-mcp-server 0.4.0 → 0.4.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 (135) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +3 -2
  4. package/README.md +21 -5
  5. package/dist/index.js +124 -31
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.js +46 -62
  11. package/dist/resources/levels.d.ts +21 -3
  12. package/dist/resources/levels.js +29 -54
  13. package/dist/tools/actors.d.ts +3 -14
  14. package/dist/tools/actors.js +246 -302
  15. package/dist/tools/animation.d.ts +57 -102
  16. package/dist/tools/animation.js +429 -450
  17. package/dist/tools/assets.d.ts +13 -2
  18. package/dist/tools/assets.js +52 -44
  19. package/dist/tools/audio.d.ts +22 -13
  20. package/dist/tools/audio.js +467 -121
  21. package/dist/tools/blueprint.d.ts +32 -13
  22. package/dist/tools/blueprint.js +699 -448
  23. package/dist/tools/build_environment_advanced.d.ts +0 -1
  24. package/dist/tools/build_environment_advanced.js +190 -45
  25. package/dist/tools/consolidated-tool-definitions.js +78 -252
  26. package/dist/tools/consolidated-tool-handlers.js +506 -133
  27. package/dist/tools/debug.d.ts +72 -10
  28. package/dist/tools/debug.js +167 -31
  29. package/dist/tools/editor.d.ts +9 -2
  30. package/dist/tools/editor.js +30 -44
  31. package/dist/tools/foliage.d.ts +34 -15
  32. package/dist/tools/foliage.js +97 -107
  33. package/dist/tools/introspection.js +19 -21
  34. package/dist/tools/landscape.d.ts +1 -2
  35. package/dist/tools/landscape.js +311 -168
  36. package/dist/tools/level.d.ts +3 -28
  37. package/dist/tools/level.js +642 -192
  38. package/dist/tools/lighting.d.ts +14 -3
  39. package/dist/tools/lighting.js +236 -123
  40. package/dist/tools/materials.d.ts +25 -7
  41. package/dist/tools/materials.js +102 -79
  42. package/dist/tools/niagara.d.ts +10 -12
  43. package/dist/tools/niagara.js +74 -94
  44. package/dist/tools/performance.d.ts +12 -4
  45. package/dist/tools/performance.js +38 -79
  46. package/dist/tools/physics.d.ts +34 -10
  47. package/dist/tools/physics.js +364 -292
  48. package/dist/tools/rc.js +97 -23
  49. package/dist/tools/sequence.d.ts +1 -0
  50. package/dist/tools/sequence.js +125 -22
  51. package/dist/tools/ui.d.ts +31 -4
  52. package/dist/tools/ui.js +83 -66
  53. package/dist/tools/visual.d.ts +11 -0
  54. package/dist/tools/visual.js +245 -30
  55. package/dist/types/tool-types.d.ts +0 -6
  56. package/dist/types/tool-types.js +1 -8
  57. package/dist/unreal-bridge.d.ts +32 -2
  58. package/dist/unreal-bridge.js +621 -127
  59. package/dist/utils/elicitation.d.ts +57 -0
  60. package/dist/utils/elicitation.js +104 -0
  61. package/dist/utils/error-handler.d.ts +0 -33
  62. package/dist/utils/error-handler.js +4 -111
  63. package/dist/utils/http.d.ts +2 -22
  64. package/dist/utils/http.js +12 -75
  65. package/dist/utils/normalize.d.ts +4 -4
  66. package/dist/utils/normalize.js +15 -7
  67. package/dist/utils/python-output.d.ts +18 -0
  68. package/dist/utils/python-output.js +290 -0
  69. package/dist/utils/python.d.ts +2 -0
  70. package/dist/utils/python.js +4 -0
  71. package/dist/utils/response-validator.js +28 -2
  72. package/dist/utils/result-helpers.d.ts +27 -0
  73. package/dist/utils/result-helpers.js +147 -0
  74. package/dist/utils/safe-json.d.ts +0 -2
  75. package/dist/utils/safe-json.js +0 -43
  76. package/dist/utils/validation.d.ts +16 -0
  77. package/dist/utils/validation.js +70 -7
  78. package/mcp-config-example.json +2 -2
  79. package/package.json +10 -9
  80. package/server.json +37 -14
  81. package/src/index.ts +130 -33
  82. package/src/prompts/index.ts +211 -13
  83. package/src/resources/actors.ts +59 -44
  84. package/src/resources/assets.ts +48 -51
  85. package/src/resources/levels.ts +35 -45
  86. package/src/tools/actors.ts +269 -313
  87. package/src/tools/animation.ts +556 -539
  88. package/src/tools/assets.ts +53 -43
  89. package/src/tools/audio.ts +507 -113
  90. package/src/tools/blueprint.ts +778 -462
  91. package/src/tools/build_environment_advanced.ts +266 -64
  92. package/src/tools/consolidated-tool-definitions.ts +90 -264
  93. package/src/tools/consolidated-tool-handlers.ts +630 -121
  94. package/src/tools/debug.ts +176 -33
  95. package/src/tools/editor.ts +35 -37
  96. package/src/tools/foliage.ts +110 -104
  97. package/src/tools/introspection.ts +24 -22
  98. package/src/tools/landscape.ts +334 -181
  99. package/src/tools/level.ts +683 -182
  100. package/src/tools/lighting.ts +244 -123
  101. package/src/tools/materials.ts +114 -83
  102. package/src/tools/niagara.ts +87 -81
  103. package/src/tools/performance.ts +49 -88
  104. package/src/tools/physics.ts +393 -299
  105. package/src/tools/rc.ts +102 -24
  106. package/src/tools/sequence.ts +136 -28
  107. package/src/tools/ui.ts +101 -70
  108. package/src/tools/visual.ts +250 -29
  109. package/src/types/tool-types.ts +0 -9
  110. package/src/unreal-bridge.ts +658 -140
  111. package/src/utils/elicitation.ts +129 -0
  112. package/src/utils/error-handler.ts +4 -159
  113. package/src/utils/http.ts +16 -115
  114. package/src/utils/normalize.ts +20 -10
  115. package/src/utils/python-output.ts +351 -0
  116. package/src/utils/python.ts +3 -0
  117. package/src/utils/response-validator.ts +25 -2
  118. package/src/utils/result-helpers.ts +193 -0
  119. package/src/utils/safe-json.ts +0 -50
  120. package/src/utils/validation.ts +94 -7
  121. package/tests/run-unreal-tool-tests.mjs +720 -0
  122. package/tsconfig.json +2 -2
  123. package/dist/python-utils.d.ts +0 -29
  124. package/dist/python-utils.js +0 -54
  125. package/dist/types/index.d.ts +0 -323
  126. package/dist/types/index.js +0 -28
  127. package/dist/utils/cache-manager.d.ts +0 -64
  128. package/dist/utils/cache-manager.js +0 -176
  129. package/dist/utils/errors.d.ts +0 -133
  130. package/dist/utils/errors.js +0 -256
  131. package/src/python/editor_compat.py +0 -181
  132. package/src/python-utils.ts +0 -57
  133. package/src/types/index.ts +0 -414
  134. package/src/utils/cache-manager.ts +0 -213
  135. package/src/utils/errors.ts +0 -312
@@ -1,5 +1,6 @@
1
1
  // Foliage tools for Unreal Engine
2
2
  import { UnrealBridge } from '../unreal-bridge.js';
3
+ import { bestEffortInterpretedText, coerceBoolean, coerceNumber, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
3
4
 
4
5
  export class FoliageTools {
5
6
  constructor(private bridge: UnrealBridge) {}
@@ -205,31 +206,39 @@ print('RESULT:' + json.dumps(res))
205
206
  `.trim();
206
207
 
207
208
  const pyResp = await this.bridge.executePython(py);
208
- let out = '';
209
- if (pyResp?.LogOutput && Array.isArray(pyResp.LogOutput)) out = pyResp.LogOutput.map((l: any) => l.Output || '').join('');
210
- else if (typeof pyResp === 'string') out = pyResp; else out = JSON.stringify(pyResp);
211
- const m = out.match(/RESULT:({.*})/);
212
- if (m) {
213
- try {
214
- const parsed = JSON.parse(m[1]);
215
- if (!parsed.success) {
216
- return { success: false, error: parsed.note || 'Add foliage type failed' };
217
- }
218
- return {
219
- success: true,
220
- created: parsed.created,
221
- exists: parsed.exists_after,
222
- method: parsed.method,
223
- assetPath: parsed.asset_path,
224
- usedMesh: parsed.used_mesh,
225
- note: parsed.note,
226
- message: parsed.exists_after ? `Foliage type '${name}' ready (${parsed.method || 'Unknown'})` : `Created foliage '${name}' but verification did not find it yet`
227
- };
228
- } catch {
229
- return { success: false, error: 'Failed to parse Python result' };
230
- }
231
- }
232
- return { success: false, error: 'No parseable result from Python' };
209
+ const interpreted = interpretStandardResult(pyResp, {
210
+ successMessage: `Foliage type '${name}' processed`,
211
+ failureMessage: 'Add foliage type failed'
212
+ });
213
+
214
+ if (!interpreted.success) {
215
+ return {
216
+ success: false,
217
+ error: coerceString(interpreted.payload.note) ?? interpreted.error ?? 'Add foliage type failed',
218
+ note: coerceString(interpreted.payload.note) ?? bestEffortInterpretedText(interpreted)
219
+ };
220
+ }
221
+
222
+ const payload = interpreted.payload as Record<string, unknown>;
223
+ const created = coerceBoolean(payload.created, false) ?? false;
224
+ const exists = coerceBoolean(payload.exists_after, false) ?? created;
225
+ const method = coerceString(payload.method) ?? 'Unknown';
226
+ const assetPath = coerceString(payload.asset_path);
227
+ const usedMesh = coerceString(payload.used_mesh);
228
+ const note = coerceString(payload.note);
229
+
230
+ return {
231
+ success: true,
232
+ created,
233
+ exists,
234
+ method,
235
+ assetPath,
236
+ usedMesh,
237
+ note,
238
+ message: exists
239
+ ? `Foliage type '${name}' ready (${method})`
240
+ : `Created foliage '${name}' but verification did not find it yet`
241
+ };
233
242
  }
234
243
 
235
244
  // Paint foliage by placing HISM instances (editor-only)
@@ -275,26 +284,34 @@ px, py, pz = ${pos[0]}, ${pos[1]}, ${pos[2]}
275
284
  radius = float(${brush}) / 2.0
276
285
 
277
286
  try:
278
- actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
279
- all_actors = actor_sub.get_all_level_actors() if actor_sub else []
287
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
288
+ if not actor_subsystem:
289
+ raise RuntimeError('EditorActorSubsystem unavailable. Enable Editor Scripting Utilities plugin.')
280
290
 
281
- # Find or create a container actor
282
- label = f"FoliageContainer_{foliage_type_name}"
283
- container = None
284
- for a in all_actors:
285
- try:
286
- if a.get_actor_label() == label:
287
- container = a
288
- break
289
- except Exception:
290
- pass
291
+ all_actors = actor_subsystem.get_all_level_actors()
292
+
293
+ # Find or create a container actor using modern EditorActorSubsystem
294
+ label = f"FoliageContainer_{foliage_type_name}"
295
+ container = None
296
+ for a in all_actors:
297
+ try:
298
+ if a and a.get_actor_label() == label:
299
+ container = a
300
+ break
301
+ except Exception:
302
+ pass
303
+
304
+ if not container:
305
+ container = actor_subsystem.spawn_actor_from_class(
306
+ unreal.StaticMeshActor,
307
+ unreal.Vector(px, py, pz)
308
+ )
291
309
  if not container:
292
- # Spawn actor that can hold components
293
- container = unreal.EditorLevelLibrary.spawn_actor_from_class(unreal.StaticMeshActor, unreal.Vector(px, py, pz))
294
- try:
295
- container.set_actor_label(label)
296
- except Exception:
297
- pass
310
+ raise RuntimeError('Failed to spawn foliage container actor via EditorActorSubsystem')
311
+ try:
312
+ container.set_actor_label(label)
313
+ except Exception:
314
+ pass
298
315
 
299
316
  # Resolve mesh from FoliageType asset
300
317
  mesh = None
@@ -322,12 +339,12 @@ try:
322
339
  r = random.random() * radius
323
340
  x, y, z = px + math.cos(ang) * r, py + math.sin(ang) * r, pz
324
341
  try:
325
- # Spawn static mesh actor at position
326
- inst_actor = unreal.EditorLevelLibrary.spawn_actor_from_class(
327
- unreal.StaticMeshActor,
328
- unreal.Vector(x, y, z),
329
- unreal.Rotator(0, random.random()*360.0, 0)
330
- )
342
+ # Spawn static mesh actor at position using modern subsystem
343
+ inst_actor = actor_subsystem.spawn_actor_from_class(
344
+ unreal.StaticMeshActor,
345
+ unreal.Vector(x, y, z),
346
+ unreal.Rotator(0, random.random()*360.0, 0)
347
+ )
331
348
  if inst_actor and mesh:
332
349
  # Set mesh on the actor's component
333
350
  try:
@@ -355,30 +372,35 @@ print('RESULT:' + json.dumps(res))
355
372
  `.trim();
356
373
 
357
374
  const pyResp = await this.bridge.executePython(py);
358
- let out = '';
359
- if (pyResp?.LogOutput && Array.isArray(pyResp.LogOutput)) out = pyResp.LogOutput.map((l: any) => l.Output || '').join('');
360
- else if (typeof pyResp === 'string') out = pyResp; else out = JSON.stringify(pyResp);
361
- const m = out.match(/RESULT:({.*})/);
362
- if (m) {
363
- try {
364
- const parsed = JSON.parse(m[1]);
365
- if (!parsed.success) {
366
- return { success: false, error: parsed.note || 'Paint foliage failed' };
367
- }
368
- return {
369
- success: true,
370
- added: parsed.added,
371
- actor: parsed.actor,
372
- component: parsed.component,
373
- usedMesh: parsed.used_mesh,
374
- note: parsed.note,
375
- message: `Painted ${parsed.added} instances for '${foliageType}' around (${pos[0]}, ${pos[1]}, ${pos[2]})`
376
- };
377
- } catch {
378
- return { success: false, error: 'Failed to parse Python result' };
379
- }
380
- }
381
- return { success: false, error: 'No parseable result from Python' };
375
+ const interpreted = interpretStandardResult(pyResp, {
376
+ successMessage: `Painted foliage for '${foliageType}'`,
377
+ failureMessage: 'Paint foliage failed'
378
+ });
379
+
380
+ if (!interpreted.success) {
381
+ return {
382
+ success: false,
383
+ error: coerceString(interpreted.payload.note) ?? interpreted.error ?? 'Paint foliage failed',
384
+ note: coerceString(interpreted.payload.note) ?? bestEffortInterpretedText(interpreted)
385
+ };
386
+ }
387
+
388
+ const payload = interpreted.payload as Record<string, unknown>;
389
+ const added = coerceNumber(payload.added) ?? 0;
390
+ const actor = coerceString(payload.actor);
391
+ const component = coerceString(payload.component);
392
+ const usedMesh = coerceString(payload.used_mesh);
393
+ const note = coerceString(payload.note);
394
+
395
+ return {
396
+ success: true,
397
+ added,
398
+ actor,
399
+ component,
400
+ usedMesh,
401
+ note,
402
+ message: `Painted ${added} instances for '${foliageType}' around (${pos[0]}, ${pos[1]}, ${pos[2]})`
403
+ };
382
404
  }
383
405
 
384
406
  // Create instanced mesh
@@ -393,7 +415,7 @@ print('RESULT:' + json.dumps(res))
393
415
  enableCulling?: boolean;
394
416
  cullDistance?: number;
395
417
  }) {
396
- const commands = [];
418
+ const commands: string[] = [];
397
419
 
398
420
  commands.push(`CreateInstancedStaticMesh ${params.name} ${params.meshPath}`);
399
421
 
@@ -411,9 +433,7 @@ print('RESULT:' + json.dumps(res))
411
433
  commands.push(`SetInstanceCullDistance ${params.name} ${params.cullDistance}`);
412
434
  }
413
435
 
414
- for (const cmd of commands) {
415
- await this.bridge.executeConsoleCommand(cmd);
416
- }
436
+ await this.bridge.executeConsoleCommands(commands);
417
437
 
418
438
  return { success: true, message: `Instanced mesh ${params.name} created with ${params.instances.length} instances` };
419
439
  }
@@ -424,7 +444,7 @@ print('RESULT:' + json.dumps(res))
424
444
  lodDistances?: number[];
425
445
  screenSize?: number[];
426
446
  }) {
427
- const commands = [];
447
+ const commands: string[] = [];
428
448
 
429
449
  if (params.lodDistances) {
430
450
  commands.push(`SetFoliageLODDistances ${params.foliageType} ${params.lodDistances.join(' ')}`);
@@ -434,9 +454,7 @@ print('RESULT:' + json.dumps(res))
434
454
  commands.push(`SetFoliageLODScreenSize ${params.foliageType} ${params.screenSize.join(' ')}`);
435
455
  }
436
456
 
437
- for (const cmd of commands) {
438
- await this.bridge.executeConsoleCommand(cmd);
439
- }
457
+ await this.bridge.executeConsoleCommands(commands);
440
458
 
441
459
  return { success: true, message: 'Foliage LOD settings updated' };
442
460
  }
@@ -450,7 +468,7 @@ print('RESULT:' + json.dumps(res))
450
468
  seed?: number;
451
469
  tileSize?: number;
452
470
  }) {
453
- const commands = [];
471
+ const commands: string[] = [];
454
472
 
455
473
  commands.push(`CreateProceduralFoliageVolume ${params.volumeName} ${params.position.join(' ')} ${params.size.join(' ')}`);
456
474
 
@@ -468,9 +486,7 @@ print('RESULT:' + json.dumps(res))
468
486
 
469
487
  commands.push(`GenerateProceduralFoliage ${params.volumeName}`);
470
488
 
471
- for (const cmd of commands) {
472
- await this.bridge.executeConsoleCommand(cmd);
473
- }
489
+ await this.bridge.executeConsoleCommands(commands);
474
490
 
475
491
  return { success: true, message: `Procedural foliage volume ${params.volumeName} created` };
476
492
  }
@@ -482,7 +498,7 @@ print('RESULT:' + json.dumps(res))
482
498
  collisionProfile?: string;
483
499
  generateOverlapEvents?: boolean;
484
500
  }) {
485
- const commands = [];
501
+ const commands: string[] = [];
486
502
 
487
503
  if (params.collisionEnabled !== undefined) {
488
504
  commands.push(`SetFoliageCollision ${params.foliageType} ${params.collisionEnabled}`);
@@ -496,9 +512,7 @@ print('RESULT:' + json.dumps(res))
496
512
  commands.push(`SetFoliageOverlapEvents ${params.foliageType} ${params.generateOverlapEvents}`);
497
513
  }
498
514
 
499
- for (const cmd of commands) {
500
- await this.bridge.executeConsoleCommand(cmd);
501
- }
515
+ await this.bridge.executeConsoleCommands(commands);
502
516
 
503
517
  return { success: true, message: 'Foliage collision settings updated' };
504
518
  }
@@ -515,7 +529,7 @@ print('RESULT:' + json.dumps(res))
515
529
  windStrength?: number;
516
530
  windSpeed?: number;
517
531
  }) {
518
- const commands = [];
532
+ const commands: string[] = [];
519
533
 
520
534
  commands.push(`CreateGrassSystem ${params.name}`);
521
535
 
@@ -533,9 +547,7 @@ print('RESULT:' + json.dumps(res))
533
547
  commands.push(`SetGrassWindSpeed ${params.name} ${params.windSpeed}`);
534
548
  }
535
549
 
536
- for (const cmd of commands) {
537
- await this.bridge.executeConsoleCommand(cmd);
538
- }
550
+ await this.bridge.executeConsoleCommands(commands);
539
551
 
540
552
  return { success: true, message: `Grass system ${params.name} created` };
541
553
  }
@@ -577,7 +589,7 @@ print('RESULT:' + json.dumps(res))
577
589
  updateMesh?: boolean;
578
590
  newMeshPath?: string;
579
591
  }) {
580
- const commands = [];
592
+ const commands: string[] = [];
581
593
 
582
594
  if (params.updateTransforms) {
583
595
  commands.push(`UpdateFoliageTransforms ${params.foliageType}`);
@@ -589,9 +601,7 @@ print('RESULT:' + json.dumps(res))
589
601
 
590
602
  commands.push(`RefreshFoliage ${params.foliageType}`);
591
603
 
592
- for (const cmd of commands) {
593
- await this.bridge.executeConsoleCommand(cmd);
594
- }
604
+ await this.bridge.executeConsoleCommands(commands);
595
605
 
596
606
  return { success: true, message: 'Foliage instances updated' };
597
607
  }
@@ -602,7 +612,7 @@ print('RESULT:' + json.dumps(res))
602
612
  spawnArea: 'Landscape' | 'StaticMesh' | 'BSP' | 'Foliage' | 'All';
603
613
  excludeAreas?: Array<[number, number, number, number]>; // [x, y, z, radius]
604
614
  }) {
605
- const commands = [];
615
+ const commands: string[] = [];
606
616
 
607
617
  commands.push(`CreateFoliageSpawner ${params.name} ${params.spawnArea}`);
608
618
 
@@ -612,9 +622,7 @@ print('RESULT:' + json.dumps(res))
612
622
  }
613
623
  }
614
624
 
615
- for (const cmd of commands) {
616
- await this.bridge.executeConsoleCommand(cmd);
617
- }
625
+ await this.bridge.executeConsoleCommands(commands);
618
626
 
619
627
  return { success: true, message: `Foliage spawner ${params.name} created` };
620
628
  }
@@ -643,9 +651,7 @@ print('RESULT:' + json.dumps(res))
643
651
 
644
652
  commands.push('RebuildFoliageTree');
645
653
 
646
- for (const cmd of commands) {
647
- await this.bridge.executeConsoleCommand(cmd);
648
- }
654
+ await this.bridge.executeConsoleCommands(commands);
649
655
 
650
656
  return { success: true, message: 'Foliage optimized' };
651
657
  }
@@ -1,5 +1,6 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { Logger } from '../utils/logger.js';
3
+ import { bestEffortInterpretedText, interpretStandardResult } from '../utils/result-helpers.js';
3
4
 
4
5
  export interface ObjectInfo {
5
6
  class: string;
@@ -76,33 +77,34 @@ export class IntrospectionTools {
76
77
  * Parse Python execution result with better error handling
77
78
  */
78
79
  private parsePythonResult(resp: any, operationName: string): any {
79
- let out = '';
80
- if (resp?.LogOutput && Array.isArray((resp as any).LogOutput)) {
81
- out = (resp as any).LogOutput.map((l: any) => l.Output || '').join('');
82
- } else if (typeof resp === 'string') {
83
- out = resp;
84
- } else {
85
- out = JSON.stringify(resp);
86
- }
87
-
88
- const m = out.match(/RESULT:({.*})/);
89
- if (m) {
90
- try {
91
- return JSON.parse(m[1]);
92
- } catch (e) {
93
- this.log.error(`Failed to parse ${operationName} result: ${e}`);
94
- }
80
+ const interpreted = interpretStandardResult(resp, {
81
+ successMessage: `${operationName} succeeded`,
82
+ failureMessage: `${operationName} failed`
83
+ });
84
+
85
+ if (interpreted.success) {
86
+ return {
87
+ ...interpreted.payload,
88
+ success: true
89
+ };
90
+ }
91
+
92
+ const output = bestEffortInterpretedText(interpreted) ?? '';
93
+ if (output) {
94
+ this.log.error(`Failed to parse ${operationName} result: ${output}`);
95
95
  }
96
-
97
- // Check for common error patterns
98
- if (out.includes('ModuleNotFoundError')) {
96
+
97
+ if (output.includes('ModuleNotFoundError')) {
99
98
  return { success: false, error: 'Reflection module not available.' };
100
99
  }
101
- if (out.includes('AttributeError')) {
100
+ if (output.includes('AttributeError')) {
102
101
  return { success: false, error: 'Reflection API method not found. Check Unreal Engine version compatibility.' };
103
102
  }
104
-
105
- return { success: false, error: `${operationName} did not return a valid result: ${out.substring(0, 200)}` };
103
+
104
+ return {
105
+ success: false,
106
+ error: `${interpreted.error ?? `${operationName} did not return a valid result`}: ${output.substring(0, 200)}`
107
+ };
106
108
  }
107
109
 
108
110
  /**