unreal-engine-mcp-server 0.4.0 → 0.4.3

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 +1 -1
  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,15 +1,11 @@
1
1
  // Lighting tools for Unreal Engine
2
2
  import { UnrealBridge } from '../unreal-bridge.js';
3
+ import { parseStandardResult } from '../utils/python-output.js';
4
+ import { escapePythonString } from '../utils/python.js';
3
5
 
4
6
  export class LightingTools {
5
7
  constructor(private bridge: UnrealBridge) {}
6
8
 
7
- // Helper to safely escape strings for Python
8
- private escapePythonString(str: string): string {
9
- // Escape backslashes first, then quotes
10
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
11
- }
12
-
13
9
  private ensurePythonSpawnSucceeded(label: string, result: any) {
14
10
  let logs = '';
15
11
  if (Array.isArray(result?.LogOutput)) {
@@ -34,18 +30,22 @@ export class LightingTools {
34
30
  throw new Error(`Uncertain spawn result for '${label}'. Engine logs:\n${logs}`);
35
31
  }
36
32
 
37
- // Execute console command
38
- private async _executeCommand(command: string) {
39
- return this.bridge.httpCall('/remote/object/call', 'PUT', {
40
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
41
- functionName: 'ExecuteConsoleCommand',
42
- parameters: {
43
- WorldContextObject: null,
44
- Command: command,
45
- SpecificPlayer: null
46
- },
47
- generateTransaction: false
48
- });
33
+ private normalizeName(value: unknown, fallback?: string): string {
34
+ if (typeof value === 'string') {
35
+ const trimmed = value.trim();
36
+ if (trimmed.length > 0) {
37
+ return trimmed;
38
+ }
39
+ }
40
+
41
+ if (typeof fallback === 'string') {
42
+ const trimmedFallback = fallback.trim();
43
+ if (trimmedFallback.length > 0) {
44
+ return trimmedFallback;
45
+ }
46
+ }
47
+
48
+ throw new Error('Invalid name: must be a non-empty string');
49
49
  }
50
50
 
51
51
  // Create directional light
@@ -57,10 +57,8 @@ export class LightingTools {
57
57
  castShadows?: boolean;
58
58
  temperature?: number;
59
59
  }) {
60
- // Validate name
61
- if (!params.name || typeof params.name !== 'string' || params.name.trim() === '') {
62
- throw new Error('Invalid name: must be a non-empty string');
63
- }
60
+ const name = this.normalizeName(params.name);
61
+ const escapedName = escapePythonString(name);
64
62
 
65
63
  // Validate numeric parameters
66
64
  if (params.intensity !== undefined) {
@@ -135,31 +133,31 @@ spawn_rotation = unreal.Rotator(${rot[0]}, ${rot[1]}, ${rot[2]})
135
133
 
136
134
  # Spawn the actor
137
135
  spawned_light = editor_actor_subsystem.spawn_actor_from_class(
138
- directional_light_class,
139
- spawn_location,
140
- spawn_rotation
136
+ directional_light_class,
137
+ spawn_location,
138
+ spawn_rotation
141
139
  )
142
140
 
143
141
  if spawned_light:
144
- # Set the label/name
145
- spawned_light.set_actor_label("${this.escapePythonString(params.name)}")
142
+ # Set the label/name
143
+ spawned_light.set_actor_label("${escapedName}")
146
144
 
147
- # Get the light component
148
- light_component = spawned_light.get_component_by_class(unreal.DirectionalLightComponent)
145
+ # Get the light component
146
+ light_component = spawned_light.get_component_by_class(unreal.DirectionalLightComponent)
149
147
 
150
- if light_component:
148
+ if light_component:
151
149
  ${propertiesCode}
152
150
 
153
- print("Directional light '${this.escapePythonString(params.name)}' spawned")
151
+ print("Directional light '${escapedName}' spawned")
154
152
  else:
155
- print("Failed to spawn directional light '${this.escapePythonString(params.name)}'")
153
+ print("Failed to spawn directional light '${escapedName}'")
156
154
  `;
157
155
 
158
156
  // Execute the Python script via bridge (UE 5.6-compatible)
159
157
  const result = await this.bridge.executePython(pythonScript);
160
158
 
161
- this.ensurePythonSpawnSucceeded(params.name, result);
162
- return { success: true, message: `Directional light '${params.name}' spawned` };
159
+ this.ensurePythonSpawnSucceeded(name, result);
160
+ return { success: true, message: `Directional light '${name}' spawned` };
163
161
  }
164
162
 
165
163
  // Create point light
@@ -172,10 +170,8 @@ else:
172
170
  falloffExponent?: number;
173
171
  castShadows?: boolean;
174
172
  }) {
175
- // Validate name
176
- if (!params.name || typeof params.name !== 'string' || params.name.trim() === '') {
177
- throw new Error('Invalid name: must be a non-empty string');
178
- }
173
+ const name = this.normalizeName(params.name);
174
+ const escapedName = escapePythonString(name);
179
175
 
180
176
  // Validate location array
181
177
  if (params.location !== undefined) {
@@ -262,31 +258,31 @@ spawn_rotation = unreal.Rotator(0, 0, 0)
262
258
 
263
259
  # Spawn the actor
264
260
  spawned_light = editor_actor_subsystem.spawn_actor_from_class(
265
- point_light_class,
266
- spawn_location,
267
- spawn_rotation
261
+ point_light_class,
262
+ spawn_location,
263
+ spawn_rotation
268
264
  )
269
265
 
270
266
  if spawned_light:
271
- # Set the label/name
272
- spawned_light.set_actor_label("${this.escapePythonString(params.name)}")
267
+ # Set the label/name
268
+ spawned_light.set_actor_label("${escapedName}")
273
269
 
274
- # Get the light component
275
- light_component = spawned_light.get_component_by_class(unreal.PointLightComponent)
270
+ # Get the light component
271
+ light_component = spawned_light.get_component_by_class(unreal.PointLightComponent)
276
272
 
277
- if light_component:
273
+ if light_component:
278
274
  ${propertiesCode}
279
275
 
280
- print(f"Point light '${this.escapePythonString(params.name)}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
276
+ print(f"Point light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
281
277
  else:
282
- print("Failed to spawn point light '${this.escapePythonString(params.name)}'")
278
+ print("Failed to spawn point light '${escapedName}'")
283
279
  `;
284
280
 
285
281
  // Execute the Python script via bridge (UE 5.6-compatible)
286
282
  const result = await this.bridge.executePython(pythonScript);
287
283
 
288
- this.ensurePythonSpawnSucceeded(params.name, result);
289
- return { success: true, message: `Point light '${params.name}' spawned at ${location.join(', ')}` };
284
+ this.ensurePythonSpawnSucceeded(name, result);
285
+ return { success: true, message: `Point light '${name}' spawned at ${location.join(', ')}` };
290
286
  }
291
287
 
292
288
  // Create spot light
@@ -301,10 +297,8 @@ else:
301
297
  color?: [number, number, number];
302
298
  castShadows?: boolean;
303
299
  }) {
304
- // Validate name
305
- if (!params.name || typeof params.name !== 'string' || params.name.trim() === '') {
306
- throw new Error('Invalid name: must be a non-empty string');
307
- }
300
+ const name = this.normalizeName(params.name);
301
+ const escapedName = escapePythonString(name);
308
302
 
309
303
  // Validate required location and rotation arrays
310
304
  if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
@@ -418,7 +412,7 @@ spawned_light = editor_actor_subsystem.spawn_actor_from_class(
418
412
 
419
413
  if spawned_light:
420
414
  # Set the label/name
421
- spawned_light.set_actor_label("${this.escapePythonString(params.name)}")
415
+ spawned_light.set_actor_label("${escapedName}")
422
416
 
423
417
  # Get the light component
424
418
  light_component = spawned_light.get_component_by_class(unreal.SpotLightComponent)
@@ -426,16 +420,16 @@ if spawned_light:
426
420
  if light_component:
427
421
  ${propertiesCode}
428
422
 
429
- print(f"Spot light '${this.escapePythonString(params.name)}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
423
+ print(f"Spot light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
430
424
  else:
431
- print("Failed to spawn spot light '${this.escapePythonString(params.name)}'")
425
+ print("Failed to spawn spot light '${escapedName}'")
432
426
  `;
433
427
 
434
428
  // Execute the Python script via bridge (UE 5.6-compatible)
435
- const result = await this.bridge.executePython(pythonScript);
429
+ const result = await this.bridge.executePython(pythonScript);
436
430
 
437
- this.ensurePythonSpawnSucceeded(params.name, result);
438
- return { success: true, message: `Spot light '${params.name}' spawned at ${params.location.join(', ')}` };
431
+ this.ensurePythonSpawnSucceeded(name, result);
432
+ return { success: true, message: `Spot light '${name}' spawned at ${params.location.join(', ')}` };
439
433
  }
440
434
 
441
435
  // Create rect light
@@ -448,11 +442,9 @@ else:
448
442
  intensity?: number;
449
443
  color?: [number, number, number];
450
444
  }) {
451
- // Validate name
452
- if (!params.name || typeof params.name !== 'string' || params.name.trim() === '') {
453
- throw new Error('Invalid name: must be a non-empty string');
454
- }
455
-
445
+ const name = this.normalizeName(params.name);
446
+ const escapedName = escapePythonString(name);
447
+
456
448
  // Validate required location and rotation arrays
457
449
  if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
458
450
  throw new Error('Invalid location: must be an array [x,y,z]');
@@ -549,25 +541,25 @@ spawned_light = editor_actor_subsystem.spawn_actor_from_class(
549
541
  )
550
542
 
551
543
  if spawned_light:
552
- # Set the label/name
553
- spawned_light.set_actor_label("${this.escapePythonString(params.name)}")
544
+ # Set the label/name
545
+ spawned_light.set_actor_label("${escapedName}")
554
546
 
555
- # Get the light component
556
- light_component = spawned_light.get_component_by_class(unreal.RectLightComponent)
547
+ # Get the light component
548
+ light_component = spawned_light.get_component_by_class(unreal.RectLightComponent)
557
549
 
558
- if light_component:
550
+ if light_component:
559
551
  ${propertiesCode}
560
552
 
561
- print(f"Rect light '${this.escapePythonString(params.name)}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
553
+ print(f"Rect light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
562
554
  else:
563
- print("Failed to spawn rect light '${this.escapePythonString(params.name)}'")
555
+ print("Failed to spawn rect light '${escapedName}'")
564
556
  `;
565
557
 
566
558
  // Execute the Python script via bridge (UE 5.6-compatible)
567
559
  const result = await this.bridge.executePython(pythonScript);
568
560
 
569
- this.ensurePythonSpawnSucceeded(params.name, result);
570
- return { success: true, message: `Rect light '${params.name}' spawned at ${params.location.join(', ')}` };
561
+ this.ensurePythonSpawnSucceeded(name, result);
562
+ return { success: true, message: `Rect light '${name}' spawned at ${params.location.join(', ')}` };
571
563
  }
572
564
 
573
565
  // Create sky light
@@ -578,62 +570,189 @@ else:
578
570
  intensity?: number;
579
571
  recapture?: boolean;
580
572
  }) {
581
- const py = `
573
+ const name = this.normalizeName(params.name);
574
+ const escapedName = escapePythonString(name);
575
+ const sourceTypeRaw = typeof params.sourceType === 'string' ? params.sourceType.trim() : undefined;
576
+ const normalizedSourceType = sourceTypeRaw
577
+ ? sourceTypeRaw.toLowerCase() === 'specifiedcubemap'
578
+ ? 'SpecifiedCubemap'
579
+ : sourceTypeRaw.toLowerCase() === 'capturedscene'
580
+ ? 'CapturedScene'
581
+ : undefined
582
+ : undefined;
583
+ const cubemapPath = typeof params.cubemapPath === 'string' ? params.cubemapPath.trim() : undefined;
584
+
585
+ if (normalizedSourceType === 'SpecifiedCubemap' && (!cubemapPath || cubemapPath.length === 0)) {
586
+ const message = 'cubemapPath is required when sourceType is SpecifiedCubemap';
587
+ return { success: false, error: message, message };
588
+ }
589
+ const escapedCubemapPath = cubemapPath ? escapePythonString(cubemapPath) : '';
590
+ const python = `
582
591
  import unreal
583
- editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
584
- spawn_location = unreal.Vector(0, 0, 500)
585
- spawn_rotation = unreal.Rotator(0, 0, 0)
586
- # Try to find an existing SkyLight to avoid duplicates
587
- actor = None
592
+ import json
593
+
594
+ result = {
595
+ "success": False,
596
+ "message": "",
597
+ "error": "",
598
+ "warnings": []
599
+ }
600
+
601
+ def add_warning(text):
602
+ if text:
603
+ result["warnings"].append(str(text))
604
+
605
+ def finish():
606
+ if result["success"]:
607
+ if not result["message"]:
608
+ result["message"] = "Sky light ensured"
609
+ result.pop("error", None)
610
+ else:
611
+ if not result["error"]:
612
+ result["error"] = result["message"] or "Failed to ensure sky light"
613
+ if not result["message"]:
614
+ result["message"] = result["error"]
615
+ if not result["warnings"]:
616
+ result.pop("warnings", None)
617
+ print('RESULT:' + json.dumps(result))
618
+
588
619
  try:
589
- actors = editor_actor_subsystem.get_all_level_actors()
590
- for a in actors:
591
- try:
592
- if a.get_class().get_name() == 'SkyLight':
593
- actor = a
594
- break
595
- except Exception: pass
596
- except Exception: pass
597
- # Spawn only if not found
598
- if actor is None:
599
- actor = editor_actor_subsystem.spawn_actor_from_class(unreal.SkyLight, spawn_location, spawn_rotation)
600
- if actor:
620
+ actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
621
+ if not actor_sub:
622
+ result["error"] = "EditorActorSubsystem unavailable"
623
+ finish()
624
+ raise SystemExit(0)
625
+
626
+ spawn_location = unreal.Vector(0.0, 0.0, 500.0)
627
+ spawn_rotation = unreal.Rotator(0.0, 0.0, 0.0)
628
+
629
+ actor = None
630
+ try:
631
+ for candidate in actor_sub.get_all_level_actors():
632
+ try:
633
+ if candidate.get_class().get_name() == 'SkyLight':
634
+ actor = candidate
635
+ break
636
+ except Exception:
637
+ continue
638
+ except Exception:
639
+ pass
640
+
641
+ if actor is None:
642
+ actor = actor_sub.spawn_actor_from_class(unreal.SkyLight, spawn_location, spawn_rotation)
643
+
644
+ if not actor:
645
+ result["error"] = "Failed to spawn SkyLight actor"
646
+ finish()
647
+ raise SystemExit(0)
648
+
649
+ try:
650
+ actor.set_actor_label("${escapedName}")
651
+ except Exception:
652
+ pass
653
+
654
+ comp = actor.get_component_by_class(unreal.SkyLightComponent)
655
+ if not comp:
656
+ result["error"] = "SkyLight component missing"
657
+ finish()
658
+ raise SystemExit(0)
659
+
660
+ ${params.intensity !== undefined ? `
661
+ try:
662
+ comp.set_intensity(${params.intensity})
663
+ except Exception:
601
664
  try:
602
- actor.set_actor_label("${this.escapePythonString(params.name)}")
603
- except Exception: pass
604
- comp = actor.get_component_by_class(unreal.SkyLightComponent)
605
- if comp:
606
- ${params.intensity !== undefined ? `comp.set_intensity(${params.intensity})` : 'pass'}
607
- ${params.sourceType === 'SpecifiedCubemap' && params.cubemapPath ? `
608
- try:
609
- path = r"${params.cubemapPath}"
610
- if unreal.EditorAssetLibrary.does_asset_exist(path):
611
- cube = unreal.EditorAssetLibrary.load_asset(path)
612
- try: comp.set_cubemap(cube)
613
- except Exception: comp.set_editor_property('cubemap', cube)
614
- comp.recapture_sky()
615
- except Exception: pass
616
- ` : 'pass'}
617
- ${params.recapture ? `
618
- try: comp.recapture_sky()
619
- except Exception: pass
620
- ` : 'pass'}
621
- print("RESULT:{'success': True}")
622
- else:
623
- print("RESULT:{'success': False, 'error': 'Failed to spawn SkyLight'}")
665
+ comp.set_editor_property('intensity', ${params.intensity})
666
+ except Exception:
667
+ add_warning('Unable to set intensity property')
668
+ ` : ''}
669
+
670
+ source_type = ${normalizedSourceType ? `'${normalizedSourceType}'` : 'None'}
671
+ if source_type:
672
+ try:
673
+ comp.set_editor_property('source_type', getattr(unreal.SkyLightSourceType, source_type))
674
+ except Exception:
675
+ try:
676
+ comp.source_type = getattr(unreal.SkyLightSourceType, source_type)
677
+ except Exception:
678
+ add_warning(f"Unable to set source type {source_type}")
679
+
680
+ if source_type == 'SpecifiedCubemap':
681
+ path = "${escapedCubemapPath}"
682
+ if not path:
683
+ result["error"] = "cubemapPath is required when sourceType is SpecifiedCubemap"
684
+ finish()
685
+ raise SystemExit(0)
686
+ try:
687
+ exists = unreal.EditorAssetLibrary.does_asset_exist(path)
688
+ except Exception:
689
+ exists = False
690
+ if not exists:
691
+ result["error"] = f"Cubemap asset not found: {path}"
692
+ finish()
693
+ raise SystemExit(0)
694
+ try:
695
+ cube = unreal.EditorAssetLibrary.load_asset(path)
696
+ except Exception as load_err:
697
+ result["error"] = f"Failed to load cubemap asset: {load_err}"
698
+ finish()
699
+ raise SystemExit(0)
700
+ if not cube:
701
+ result["error"] = f"Cubemap asset could not be loaded: {path}"
702
+ finish()
703
+ raise SystemExit(0)
704
+ try:
705
+ if hasattr(comp, 'set_cubemap'):
706
+ comp.set_cubemap(cube)
707
+ else:
708
+ comp.set_editor_property('cubemap', cube)
709
+ except Exception as assign_err:
710
+ result["error"] = f"Failed to assign cubemap: {assign_err}"
711
+ finish()
712
+ raise SystemExit(0)
713
+
714
+ if ${params.recapture ? 'True' : 'False'}:
715
+ try:
716
+ comp.recapture_sky()
717
+ except Exception as recapture_err:
718
+ add_warning(f"Recapture failed: {recapture_err}")
719
+
720
+ result["success"] = True
721
+ result["message"] = "Sky light ensured"
722
+ finish()
723
+
724
+ except SystemExit:
725
+ pass
726
+ except Exception as run_err:
727
+ result["error"] = str(run_err)
728
+ finish()
624
729
  `.trim();
625
- const resp = await this.bridge.executePython(py);
626
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
627
- const m = out.match(/RESULT:({.*})/);
628
- if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: 'Sky light ensured' } : { success: false, error: parsed.error }; } catch {} }
629
- return { success: true, message: 'Sky light ensured' };
730
+ const resp = await this.bridge.executePython(python);
731
+ const parsed = parseStandardResult(resp).data;
732
+ if (parsed) {
733
+ if (parsed.success) {
734
+ return {
735
+ success: true,
736
+ message: parsed.message ?? 'Sky light ensured',
737
+ warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined
738
+ };
739
+ }
740
+ return {
741
+ success: false,
742
+ error: parsed.error ?? parsed.message ?? 'Failed to ensure sky light',
743
+ warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined
744
+ };
745
+ }
746
+ return { success: true, message: 'Sky light ensured' };
630
747
  }
631
748
 
632
749
  // Remove duplicate SkyLights and keep only one (named target label)
633
750
  async ensureSingleSkyLight(params?: { name?: string; recapture?: boolean }) {
634
- const name = params?.name || 'MCP_Test_Sky';
751
+ const fallbackName = 'MCP_Test_Sky';
752
+ const name = this.normalizeName(params?.name, fallbackName);
753
+ const escapedName = escapePythonString(name);
635
754
  const recapture = !!params?.recapture;
636
- const py = `\nimport unreal, json\nactor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\nactors = actor_sub.get_all_level_actors() if actor_sub else []\nskies = []\nfor a in actors:\n try:\n if a.get_class().get_name() == 'SkyLight':\n skies.append(a)\n except Exception: pass\nkeep = None\n# Prefer one with matching label; otherwise keep the first\nfor a in skies:\n try:\n label = a.get_actor_label()\n if label == r"${this.escapePythonString(name)}":\n keep = a\n break\n except Exception: pass\nif keep is None and len(skies) > 0:\n keep = skies[0]\n# Rename the kept one if needed\nif keep is not None:\n try: keep.set_actor_label(r"${this.escapePythonString(name)}")\n except Exception: pass\n# Destroy all others using the correct non-deprecated API\nremoved = 0\nfor a in skies:\n if keep is not None and a == keep:\n continue\n try:\n # Use EditorActorSubsystem.destroy_actor instead of deprecated EditorLevelLibrary\n actor_sub.destroy_actor(a)\n removed += 1\n except Exception: pass\n# Optionally recapture\nif keep is not None and ${recapture ? 'True' : 'False'}:\n try:\n comp = keep.get_component_by_class(unreal.SkyLightComponent)\n if comp: comp.recapture_sky()\n except Exception: pass\nprint('RESULT:' + json.dumps({'success': True, 'removed': removed, 'kept': True if keep else False}))\n`.trim();
755
+ const py = `\nimport unreal, json\nactor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\nactors = actor_sub.get_all_level_actors() if actor_sub else []\nskies = []\nfor a in actors:\n try:\n if a.get_class().get_name() == 'SkyLight':\n skies.append(a)\n except Exception: pass\nkeep = None\n# Prefer one with matching label; otherwise keep the first\nfor a in skies:\n try:\n label = a.get_actor_label()\n if label == "${escapedName}":\n keep = a\n break\n except Exception: pass\nif keep is None and len(skies) > 0:\n keep = skies[0]\n# Rename the kept one if needed\nif keep is not None:\n try: keep.set_actor_label("${escapedName}")\n except Exception: pass\n# Destroy all others using the correct non-deprecated API\nremoved = 0\nfor a in skies:\n if keep is not None and a == keep:\n continue\n try:\n # Use EditorActorSubsystem.destroy_actor instead of deprecated EditorLevelLibrary\n actor_sub.destroy_actor(a)\n removed += 1\n except Exception: pass\n# Optionally recapture\nif keep is not None and ${recapture ? 'True' : 'False'}:\n try:\n comp = keep.get_component_by_class(unreal.SkyLightComponent)\n if comp: comp.recapture_sky()\n except Exception: pass\nprint('RESULT:' + json.dumps({'success': True, 'removed': removed, 'kept': True if keep else False}))\n`.trim();
637
756
 
638
757
  const resp = await this.bridge.executePython(py);
639
758
  let out = '';
@@ -1205,6 +1324,8 @@ print('RESULT:' + json.dumps(result))
1205
1324
  location: [number, number, number];
1206
1325
  size: [number, number, number];
1207
1326
  }) {
1327
+ const name = this.normalizeName(params.name);
1328
+ const escapedName = escapePythonString(name);
1208
1329
  const [lx, ly, lz] = params.location;
1209
1330
  const [sx, sy, sz] = params.size;
1210
1331
  const py = `
@@ -1214,7 +1335,7 @@ loc = unreal.Vector(${lx}, ${ly}, ${lz})
1214
1335
  rot = unreal.Rotator(0,0,0)
1215
1336
  actor = editor_actor_subsystem.spawn_actor_from_class(unreal.LightmassImportanceVolume, loc, rot)
1216
1337
  if actor:
1217
- try: actor.set_actor_label("${this.escapePythonString(params.name)}")
1338
+ try: actor.set_actor_label("${escapedName}")
1218
1339
  except Exception: pass
1219
1340
  # Best-effort: set actor scale to approximate size
1220
1341
  try:
@@ -1227,7 +1348,7 @@ else:
1227
1348
  const resp = await this.bridge.executePython(py);
1228
1349
  const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
1229
1350
  const m = out.match(/RESULT:({.*})/);
1230
- if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: 'LightmassImportanceVolume created' } : { success: false, error: parsed.error }; } catch {} }
1351
+ if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: `LightmassImportanceVolume '${name}' created` } : { success: false, error: parsed.error }; } catch {} }
1231
1352
  return { success: true, message: 'LightmassImportanceVolume creation attempted' };
1232
1353
  }
1233
1354