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
@@ -2,9 +2,8 @@ import { UnrealBridge } from '../unreal-bridge.js';
2
2
  export declare class LightingTools {
3
3
  private bridge;
4
4
  constructor(bridge: UnrealBridge);
5
- private escapePythonString;
6
5
  private ensurePythonSpawnSucceeded;
7
- private _executeCommand;
6
+ private normalizeName;
8
7
  createDirectionalLight(params: {
9
8
  name: string;
10
9
  intensity?: number;
@@ -62,12 +61,24 @@ export declare class LightingTools {
62
61
  recapture?: boolean;
63
62
  }): Promise<{
64
63
  success: boolean;
64
+ error: string;
65
65
  message: string;
66
+ warnings?: undefined;
67
+ } | {
68
+ success: boolean;
69
+ message: string;
70
+ warnings: any[] | undefined;
66
71
  error?: undefined;
67
72
  } | {
68
73
  success: boolean;
69
- error: any;
74
+ error: string;
75
+ warnings: any[] | undefined;
70
76
  message?: undefined;
77
+ } | {
78
+ success: boolean;
79
+ message: string;
80
+ error?: undefined;
81
+ warnings?: undefined;
71
82
  }>;
72
83
  ensureSingleSkyLight(params?: {
73
84
  name?: string;
@@ -1,13 +1,10 @@
1
+ import { parseStandardResult } from '../utils/python-output.js';
2
+ import { escapePythonString } from '../utils/python.js';
1
3
  export class LightingTools {
2
4
  bridge;
3
5
  constructor(bridge) {
4
6
  this.bridge = bridge;
5
7
  }
6
- // Helper to safely escape strings for Python
7
- escapePythonString(str) {
8
- // Escape backslashes first, then quotes
9
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
10
- }
11
8
  ensurePythonSpawnSucceeded(label, result) {
12
9
  let logs = '';
13
10
  if (Array.isArray(result?.LogOutput)) {
@@ -30,25 +27,25 @@ export class LightingTools {
30
27
  // Otherwise, uncertain
31
28
  throw new Error(`Uncertain spawn result for '${label}'. Engine logs:\n${logs}`);
32
29
  }
33
- // Execute console command
34
- async _executeCommand(command) {
35
- return this.bridge.httpCall('/remote/object/call', 'PUT', {
36
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
37
- functionName: 'ExecuteConsoleCommand',
38
- parameters: {
39
- WorldContextObject: null,
40
- Command: command,
41
- SpecificPlayer: null
42
- },
43
- generateTransaction: false
44
- });
30
+ normalizeName(value, fallback) {
31
+ if (typeof value === 'string') {
32
+ const trimmed = value.trim();
33
+ if (trimmed.length > 0) {
34
+ return trimmed;
35
+ }
36
+ }
37
+ if (typeof fallback === 'string') {
38
+ const trimmedFallback = fallback.trim();
39
+ if (trimmedFallback.length > 0) {
40
+ return trimmedFallback;
41
+ }
42
+ }
43
+ throw new Error('Invalid name: must be a non-empty string');
45
44
  }
46
45
  // Create directional light
47
46
  async createDirectionalLight(params) {
48
- // Validate name
49
- if (!params.name || typeof params.name !== 'string' || params.name.trim() === '') {
50
- throw new Error('Invalid name: must be a non-empty string');
51
- }
47
+ const name = this.normalizeName(params.name);
48
+ const escapedName = escapePythonString(name);
52
49
  // Validate numeric parameters
53
50
  if (params.intensity !== undefined) {
54
51
  if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) {
@@ -115,36 +112,34 @@ spawn_rotation = unreal.Rotator(${rot[0]}, ${rot[1]}, ${rot[2]})
115
112
 
116
113
  # Spawn the actor
117
114
  spawned_light = editor_actor_subsystem.spawn_actor_from_class(
118
- directional_light_class,
119
- spawn_location,
120
- spawn_rotation
115
+ directional_light_class,
116
+ spawn_location,
117
+ spawn_rotation
121
118
  )
122
119
 
123
120
  if spawned_light:
124
- # Set the label/name
125
- spawned_light.set_actor_label("${this.escapePythonString(params.name)}")
121
+ # Set the label/name
122
+ spawned_light.set_actor_label("${escapedName}")
126
123
 
127
- # Get the light component
128
- light_component = spawned_light.get_component_by_class(unreal.DirectionalLightComponent)
124
+ # Get the light component
125
+ light_component = spawned_light.get_component_by_class(unreal.DirectionalLightComponent)
129
126
 
130
- if light_component:
127
+ if light_component:
131
128
  ${propertiesCode}
132
129
 
133
- print("Directional light '${this.escapePythonString(params.name)}' spawned")
130
+ print("Directional light '${escapedName}' spawned")
134
131
  else:
135
- print("Failed to spawn directional light '${this.escapePythonString(params.name)}'")
132
+ print("Failed to spawn directional light '${escapedName}'")
136
133
  `;
137
134
  // Execute the Python script via bridge (UE 5.6-compatible)
138
135
  const result = await this.bridge.executePython(pythonScript);
139
- this.ensurePythonSpawnSucceeded(params.name, result);
140
- return { success: true, message: `Directional light '${params.name}' spawned` };
136
+ this.ensurePythonSpawnSucceeded(name, result);
137
+ return { success: true, message: `Directional light '${name}' spawned` };
141
138
  }
142
139
  // Create point light
143
140
  async createPointLight(params) {
144
- // Validate name
145
- if (!params.name || typeof params.name !== 'string' || params.name.trim() === '') {
146
- throw new Error('Invalid name: must be a non-empty string');
147
- }
141
+ const name = this.normalizeName(params.name);
142
+ const escapedName = escapePythonString(name);
148
143
  // Validate location array
149
144
  if (params.location !== undefined) {
150
145
  if (!Array.isArray(params.location) || params.location.length !== 3) {
@@ -224,36 +219,34 @@ spawn_rotation = unreal.Rotator(0, 0, 0)
224
219
 
225
220
  # Spawn the actor
226
221
  spawned_light = editor_actor_subsystem.spawn_actor_from_class(
227
- point_light_class,
228
- spawn_location,
229
- spawn_rotation
222
+ point_light_class,
223
+ spawn_location,
224
+ spawn_rotation
230
225
  )
231
226
 
232
227
  if spawned_light:
233
- # Set the label/name
234
- spawned_light.set_actor_label("${this.escapePythonString(params.name)}")
228
+ # Set the label/name
229
+ spawned_light.set_actor_label("${escapedName}")
235
230
 
236
- # Get the light component
237
- light_component = spawned_light.get_component_by_class(unreal.PointLightComponent)
231
+ # Get the light component
232
+ light_component = spawned_light.get_component_by_class(unreal.PointLightComponent)
238
233
 
239
- if light_component:
234
+ if light_component:
240
235
  ${propertiesCode}
241
236
 
242
- print(f"Point light '${this.escapePythonString(params.name)}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
237
+ print(f"Point light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
243
238
  else:
244
- print("Failed to spawn point light '${this.escapePythonString(params.name)}'")
239
+ print("Failed to spawn point light '${escapedName}'")
245
240
  `;
246
241
  // Execute the Python script via bridge (UE 5.6-compatible)
247
242
  const result = await this.bridge.executePython(pythonScript);
248
- this.ensurePythonSpawnSucceeded(params.name, result);
249
- return { success: true, message: `Point light '${params.name}' spawned at ${location.join(', ')}` };
243
+ this.ensurePythonSpawnSucceeded(name, result);
244
+ return { success: true, message: `Point light '${name}' spawned at ${location.join(', ')}` };
250
245
  }
251
246
  // Create spot light
252
247
  async createSpotLight(params) {
253
- // Validate name
254
- if (!params.name || typeof params.name !== 'string' || params.name.trim() === '') {
255
- throw new Error('Invalid name: must be a non-empty string');
256
- }
248
+ const name = this.normalizeName(params.name);
249
+ const escapedName = escapePythonString(name);
257
250
  // Validate required location and rotation arrays
258
251
  if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
259
252
  throw new Error('Invalid location: must be an array [x,y,z]');
@@ -358,7 +351,7 @@ spawned_light = editor_actor_subsystem.spawn_actor_from_class(
358
351
 
359
352
  if spawned_light:
360
353
  # Set the label/name
361
- spawned_light.set_actor_label("${this.escapePythonString(params.name)}")
354
+ spawned_light.set_actor_label("${escapedName}")
362
355
 
363
356
  # Get the light component
364
357
  light_component = spawned_light.get_component_by_class(unreal.SpotLightComponent)
@@ -366,21 +359,19 @@ if spawned_light:
366
359
  if light_component:
367
360
  ${propertiesCode}
368
361
 
369
- print(f"Spot light '${this.escapePythonString(params.name)}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
362
+ print(f"Spot light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
370
363
  else:
371
- print("Failed to spawn spot light '${this.escapePythonString(params.name)}'")
364
+ print("Failed to spawn spot light '${escapedName}'")
372
365
  `;
373
366
  // Execute the Python script via bridge (UE 5.6-compatible)
374
367
  const result = await this.bridge.executePython(pythonScript);
375
- this.ensurePythonSpawnSucceeded(params.name, result);
376
- return { success: true, message: `Spot light '${params.name}' spawned at ${params.location.join(', ')}` };
368
+ this.ensurePythonSpawnSucceeded(name, result);
369
+ return { success: true, message: `Spot light '${name}' spawned at ${params.location.join(', ')}` };
377
370
  }
378
371
  // Create rect light
379
372
  async createRectLight(params) {
380
- // Validate name
381
- if (!params.name || typeof params.name !== 'string' || params.name.trim() === '') {
382
- throw new Error('Invalid name: must be a non-empty string');
383
- }
373
+ const name = this.normalizeName(params.name);
374
+ const escapedName = escapePythonString(name);
384
375
  // Validate required location and rotation arrays
385
376
  if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
386
377
  throw new Error('Invalid location: must be an array [x,y,z]');
@@ -470,87 +461,207 @@ spawned_light = editor_actor_subsystem.spawn_actor_from_class(
470
461
  )
471
462
 
472
463
  if spawned_light:
473
- # Set the label/name
474
- spawned_light.set_actor_label("${this.escapePythonString(params.name)}")
464
+ # Set the label/name
465
+ spawned_light.set_actor_label("${escapedName}")
475
466
 
476
- # Get the light component
477
- light_component = spawned_light.get_component_by_class(unreal.RectLightComponent)
467
+ # Get the light component
468
+ light_component = spawned_light.get_component_by_class(unreal.RectLightComponent)
478
469
 
479
- if light_component:
470
+ if light_component:
480
471
  ${propertiesCode}
481
472
 
482
- print(f"Rect light '${this.escapePythonString(params.name)}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
473
+ print(f"Rect light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
483
474
  else:
484
- print("Failed to spawn rect light '${this.escapePythonString(params.name)}'")
475
+ print("Failed to spawn rect light '${escapedName}'")
485
476
  `;
486
477
  // Execute the Python script via bridge (UE 5.6-compatible)
487
478
  const result = await this.bridge.executePython(pythonScript);
488
- this.ensurePythonSpawnSucceeded(params.name, result);
489
- return { success: true, message: `Rect light '${params.name}' spawned at ${params.location.join(', ')}` };
479
+ this.ensurePythonSpawnSucceeded(name, result);
480
+ return { success: true, message: `Rect light '${name}' spawned at ${params.location.join(', ')}` };
490
481
  }
491
482
  // Create sky light
492
483
  async createSkyLight(params) {
493
- const py = `
484
+ const name = this.normalizeName(params.name);
485
+ const escapedName = escapePythonString(name);
486
+ const sourceTypeRaw = typeof params.sourceType === 'string' ? params.sourceType.trim() : undefined;
487
+ const normalizedSourceType = sourceTypeRaw
488
+ ? sourceTypeRaw.toLowerCase() === 'specifiedcubemap'
489
+ ? 'SpecifiedCubemap'
490
+ : sourceTypeRaw.toLowerCase() === 'capturedscene'
491
+ ? 'CapturedScene'
492
+ : undefined
493
+ : undefined;
494
+ const cubemapPath = typeof params.cubemapPath === 'string' ? params.cubemapPath.trim() : undefined;
495
+ if (normalizedSourceType === 'SpecifiedCubemap' && (!cubemapPath || cubemapPath.length === 0)) {
496
+ const message = 'cubemapPath is required when sourceType is SpecifiedCubemap';
497
+ return { success: false, error: message, message };
498
+ }
499
+ const escapedCubemapPath = cubemapPath ? escapePythonString(cubemapPath) : '';
500
+ const python = `
494
501
  import unreal
495
- editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
496
- spawn_location = unreal.Vector(0, 0, 500)
497
- spawn_rotation = unreal.Rotator(0, 0, 0)
498
- # Try to find an existing SkyLight to avoid duplicates
499
- actor = None
502
+ import json
503
+
504
+ result = {
505
+ "success": False,
506
+ "message": "",
507
+ "error": "",
508
+ "warnings": []
509
+ }
510
+
511
+ def add_warning(text):
512
+ if text:
513
+ result["warnings"].append(str(text))
514
+
515
+ def finish():
516
+ if result["success"]:
517
+ if not result["message"]:
518
+ result["message"] = "Sky light ensured"
519
+ result.pop("error", None)
520
+ else:
521
+ if not result["error"]:
522
+ result["error"] = result["message"] or "Failed to ensure sky light"
523
+ if not result["message"]:
524
+ result["message"] = result["error"]
525
+ if not result["warnings"]:
526
+ result.pop("warnings", None)
527
+ print('RESULT:' + json.dumps(result))
528
+
500
529
  try:
501
- actors = editor_actor_subsystem.get_all_level_actors()
502
- for a in actors:
503
- try:
504
- if a.get_class().get_name() == 'SkyLight':
505
- actor = a
506
- break
507
- except Exception: pass
508
- except Exception: pass
509
- # Spawn only if not found
510
- if actor is None:
511
- actor = editor_actor_subsystem.spawn_actor_from_class(unreal.SkyLight, spawn_location, spawn_rotation)
512
- if actor:
530
+ actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
531
+ if not actor_sub:
532
+ result["error"] = "EditorActorSubsystem unavailable"
533
+ finish()
534
+ raise SystemExit(0)
535
+
536
+ spawn_location = unreal.Vector(0.0, 0.0, 500.0)
537
+ spawn_rotation = unreal.Rotator(0.0, 0.0, 0.0)
538
+
539
+ actor = None
540
+ try:
541
+ for candidate in actor_sub.get_all_level_actors():
542
+ try:
543
+ if candidate.get_class().get_name() == 'SkyLight':
544
+ actor = candidate
545
+ break
546
+ except Exception:
547
+ continue
548
+ except Exception:
549
+ pass
550
+
551
+ if actor is None:
552
+ actor = actor_sub.spawn_actor_from_class(unreal.SkyLight, spawn_location, spawn_rotation)
553
+
554
+ if not actor:
555
+ result["error"] = "Failed to spawn SkyLight actor"
556
+ finish()
557
+ raise SystemExit(0)
558
+
559
+ try:
560
+ actor.set_actor_label("${escapedName}")
561
+ except Exception:
562
+ pass
563
+
564
+ comp = actor.get_component_by_class(unreal.SkyLightComponent)
565
+ if not comp:
566
+ result["error"] = "SkyLight component missing"
567
+ finish()
568
+ raise SystemExit(0)
569
+
570
+ ${params.intensity !== undefined ? `
571
+ try:
572
+ comp.set_intensity(${params.intensity})
573
+ except Exception:
513
574
  try:
514
- actor.set_actor_label("${this.escapePythonString(params.name)}")
515
- except Exception: pass
516
- comp = actor.get_component_by_class(unreal.SkyLightComponent)
517
- if comp:
518
- ${params.intensity !== undefined ? `comp.set_intensity(${params.intensity})` : 'pass'}
519
- ${params.sourceType === 'SpecifiedCubemap' && params.cubemapPath ? `
520
- try:
521
- path = r"${params.cubemapPath}"
522
- if unreal.EditorAssetLibrary.does_asset_exist(path):
523
- cube = unreal.EditorAssetLibrary.load_asset(path)
524
- try: comp.set_cubemap(cube)
525
- except Exception: comp.set_editor_property('cubemap', cube)
526
- comp.recapture_sky()
527
- except Exception: pass
528
- ` : 'pass'}
529
- ${params.recapture ? `
530
- try: comp.recapture_sky()
531
- except Exception: pass
532
- ` : 'pass'}
533
- print("RESULT:{'success': True}")
534
- else:
535
- print("RESULT:{'success': False, 'error': 'Failed to spawn SkyLight'}")
575
+ comp.set_editor_property('intensity', ${params.intensity})
576
+ except Exception:
577
+ add_warning('Unable to set intensity property')
578
+ ` : ''}
579
+
580
+ source_type = ${normalizedSourceType ? `'${normalizedSourceType}'` : 'None'}
581
+ if source_type:
582
+ try:
583
+ comp.set_editor_property('source_type', getattr(unreal.SkyLightSourceType, source_type))
584
+ except Exception:
585
+ try:
586
+ comp.source_type = getattr(unreal.SkyLightSourceType, source_type)
587
+ except Exception:
588
+ add_warning(f"Unable to set source type {source_type}")
589
+
590
+ if source_type == 'SpecifiedCubemap':
591
+ path = "${escapedCubemapPath}"
592
+ if not path:
593
+ result["error"] = "cubemapPath is required when sourceType is SpecifiedCubemap"
594
+ finish()
595
+ raise SystemExit(0)
596
+ try:
597
+ exists = unreal.EditorAssetLibrary.does_asset_exist(path)
598
+ except Exception:
599
+ exists = False
600
+ if not exists:
601
+ result["error"] = f"Cubemap asset not found: {path}"
602
+ finish()
603
+ raise SystemExit(0)
604
+ try:
605
+ cube = unreal.EditorAssetLibrary.load_asset(path)
606
+ except Exception as load_err:
607
+ result["error"] = f"Failed to load cubemap asset: {load_err}"
608
+ finish()
609
+ raise SystemExit(0)
610
+ if not cube:
611
+ result["error"] = f"Cubemap asset could not be loaded: {path}"
612
+ finish()
613
+ raise SystemExit(0)
614
+ try:
615
+ if hasattr(comp, 'set_cubemap'):
616
+ comp.set_cubemap(cube)
617
+ else:
618
+ comp.set_editor_property('cubemap', cube)
619
+ except Exception as assign_err:
620
+ result["error"] = f"Failed to assign cubemap: {assign_err}"
621
+ finish()
622
+ raise SystemExit(0)
623
+
624
+ if ${params.recapture ? 'True' : 'False'}:
625
+ try:
626
+ comp.recapture_sky()
627
+ except Exception as recapture_err:
628
+ add_warning(f"Recapture failed: {recapture_err}")
629
+
630
+ result["success"] = True
631
+ result["message"] = "Sky light ensured"
632
+ finish()
633
+
634
+ except SystemExit:
635
+ pass
636
+ except Exception as run_err:
637
+ result["error"] = str(run_err)
638
+ finish()
536
639
  `.trim();
537
- const resp = await this.bridge.executePython(py);
538
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
539
- const m = out.match(/RESULT:({.*})/);
540
- if (m) {
541
- try {
542
- const parsed = JSON.parse(m[1].replace(/'/g, '"'));
543
- return parsed.success ? { success: true, message: 'Sky light ensured' } : { success: false, error: parsed.error };
640
+ const resp = await this.bridge.executePython(python);
641
+ const parsed = parseStandardResult(resp).data;
642
+ if (parsed) {
643
+ if (parsed.success) {
644
+ return {
645
+ success: true,
646
+ message: parsed.message ?? 'Sky light ensured',
647
+ warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined
648
+ };
544
649
  }
545
- catch { }
650
+ return {
651
+ success: false,
652
+ error: parsed.error ?? parsed.message ?? 'Failed to ensure sky light',
653
+ warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined
654
+ };
546
655
  }
547
656
  return { success: true, message: 'Sky light ensured' };
548
657
  }
549
658
  // Remove duplicate SkyLights and keep only one (named target label)
550
659
  async ensureSingleSkyLight(params) {
551
- const name = params?.name || 'MCP_Test_Sky';
660
+ const fallbackName = 'MCP_Test_Sky';
661
+ const name = this.normalizeName(params?.name, fallbackName);
662
+ const escapedName = escapePythonString(name);
552
663
  const recapture = !!params?.recapture;
553
- 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();
664
+ 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();
554
665
  const resp = await this.bridge.executePython(py);
555
666
  let out = '';
556
667
  if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
@@ -1087,6 +1198,8 @@ print('RESULT:' + json.dumps(result))
1087
1198
  }
1088
1199
  // Create lightmass importance volume via Python
1089
1200
  async createLightmassVolume(params) {
1201
+ const name = this.normalizeName(params.name);
1202
+ const escapedName = escapePythonString(name);
1090
1203
  const [lx, ly, lz] = params.location;
1091
1204
  const [sx, sy, sz] = params.size;
1092
1205
  const py = `
@@ -1096,7 +1209,7 @@ loc = unreal.Vector(${lx}, ${ly}, ${lz})
1096
1209
  rot = unreal.Rotator(0,0,0)
1097
1210
  actor = editor_actor_subsystem.spawn_actor_from_class(unreal.LightmassImportanceVolume, loc, rot)
1098
1211
  if actor:
1099
- try: actor.set_actor_label("${this.escapePythonString(params.name)}")
1212
+ try: actor.set_actor_label("${escapedName}")
1100
1213
  except Exception: pass
1101
1214
  # Best-effort: set actor scale to approximate size
1102
1215
  try:
@@ -1112,7 +1225,7 @@ else:
1112
1225
  if (m) {
1113
1226
  try {
1114
1227
  const parsed = JSON.parse(m[1].replace(/'/g, '"'));
1115
- return parsed.success ? { success: true, message: 'LightmassImportanceVolume created' } : { success: false, error: parsed.error };
1228
+ return parsed.success ? { success: true, message: `LightmassImportanceVolume '${name}' created` } : { success: false, error: parsed.error };
1116
1229
  }
1117
1230
  catch { }
1118
1231
  }
@@ -3,23 +3,40 @@ export declare class MaterialTools {
3
3
  private bridge;
4
4
  constructor(bridge: UnrealBridge);
5
5
  createMaterial(name: string, path: string): Promise<{
6
+ success: boolean;
7
+ error: string;
8
+ message?: undefined;
9
+ path?: undefined;
10
+ warnings?: undefined;
11
+ details?: undefined;
12
+ } | {
13
+ success: boolean;
14
+ error: string;
15
+ message: string;
16
+ path?: undefined;
17
+ warnings?: undefined;
18
+ details?: undefined;
19
+ } | {
6
20
  success: boolean;
7
21
  path: string;
8
22
  message: string;
9
23
  error?: undefined;
10
- debug?: undefined;
24
+ warnings?: undefined;
25
+ details?: undefined;
11
26
  } | {
12
27
  success: boolean;
13
- error: any;
14
- path?: undefined;
15
- message?: undefined;
16
- debug?: undefined;
28
+ path: string;
29
+ message: string;
30
+ warnings: string[] | undefined;
31
+ details: string[] | undefined;
32
+ error?: undefined;
17
33
  } | {
18
34
  success: boolean;
19
35
  error: string;
20
- debug: string;
21
- path?: undefined;
36
+ warnings: string[] | undefined;
37
+ details: string[] | undefined;
22
38
  message?: undefined;
39
+ path?: undefined;
23
40
  }>;
24
41
  applyMaterialToActor(actorPath: string, materialPath: string, slotIndex?: number): Promise<{
25
42
  success: boolean;
@@ -30,5 +47,6 @@ export declare class MaterialTools {
30
47
  error: string;
31
48
  message?: undefined;
32
49
  }>;
50
+ private assetExists;
33
51
  }
34
52
  //# sourceMappingURL=materials.d.ts.map