ue-mcp 1.1.21 → 1.1.23

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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/plugin-cli.js +164 -3
  3. package/dist/plugin-cli.js.map +1 -1
  4. package/dist/tool-counts.json +9 -9
  5. package/dist/tools/asset.js +1 -0
  6. package/dist/tools/asset.js.map +1 -1
  7. package/dist/tools/audio.js +6 -1
  8. package/dist/tools/audio.js.map +1 -1
  9. package/dist/tools/editor.js +16 -5
  10. package/dist/tools/editor.js.map +1 -1
  11. package/dist/tools/gameplay.js +1 -1
  12. package/dist/tools/gameplay.js.map +1 -1
  13. package/dist/tools/landscape.js +4 -0
  14. package/dist/tools/landscape.js.map +1 -1
  15. package/dist/tools/level.js +7 -2
  16. package/dist/tools/level.js.map +1 -1
  17. package/dist/ue-mcp.default.yml +62 -8
  18. package/package.json +2 -2
  19. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +6 -1
  20. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +108 -0
  21. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +3 -0
  22. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers.cpp +127 -17
  23. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers.h +2 -0
  24. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +258 -5
  25. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +7 -0
  26. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_Input.cpp +75 -32
  27. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +100 -0
  28. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +4 -0
  29. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +79 -0
  30. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +2 -0
  31. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +15 -0
  32. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +7 -2
  33. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +15 -2
  34. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +2 -0
  35. package/skills/ue-mcp-blueprint/SKILL.md +13 -0
  36. package/skills/ue-mcp-epic-routing/SKILL.md +37 -0
@@ -490,21 +490,40 @@ TSharedPtr<FJsonValue> FGameplayHandlers::SetMappingModifiers(const TSharedPtr<F
490
490
  }
491
491
 
492
492
  // ── Triggers ──
493
+ // #725: triggers previously accepted only {type:"Hold"}; the more obvious
494
+ // {class:"/Script/EnhancedInput.InputTriggerHold"} was rejected. Worse, an
495
+ // unresolvable shape could leave a NULL entry in Mapping.Triggers, which
496
+ // trips AssetCheck on save. Build into a temp array, accept class OR type
497
+ // (mirroring modifiers, #649), apply nested "properties", and never append
498
+ // a null; report any failed specs instead of silently corrupting the asset.
499
+ TArray<TSharedPtr<FJsonValue>> FailedTriggers;
493
500
  const TArray<TSharedPtr<FJsonValue>>* TriggersArr = nullptr;
494
501
  if (Params->TryGetArrayField(TEXT("triggers"), TriggersArr) && TriggersArr)
495
502
  {
496
- Mapping.Triggers.Empty();
503
+ TArray<TObjectPtr<UInputTrigger>> NewTriggers;
497
504
  for (const auto& TrigVal : *TriggersArr)
498
505
  {
499
506
  const TSharedPtr<FJsonObject>* TrigObj = nullptr;
500
- if (!TrigVal->TryGetObject(TrigObj) || !TrigObj) continue;
507
+ if (!TrigVal->TryGetObject(TrigObj) || !TrigObj)
508
+ {
509
+ FailedTriggers.Add(MakeShared<FJsonValueString>(TEXT("(non-object trigger entry)")));
510
+ continue;
511
+ }
512
+
513
+ FString ClassPath;
514
+ (*TrigObj)->TryGetStringField(TEXT("class"), ClassPath);
515
+ UClass* TrigClass = nullptr;
516
+ if (!ClassPath.IsEmpty())
517
+ {
518
+ TrigClass = LoadClass<UInputTrigger>(nullptr, *ClassPath);
519
+ if (!TrigClass) TrigClass = LoadObject<UClass>(nullptr, *ClassPath);
520
+ if (!TrigClass) TrigClass = FindClassByShortName(ClassPath);
521
+ if (TrigClass && !TrigClass->IsChildOf(UInputTrigger::StaticClass())) TrigClass = nullptr;
522
+ }
501
523
 
502
524
  FString TypeName;
503
525
  (*TrigObj)->TryGetStringField(TEXT("type"), TypeName);
504
- if (TypeName.IsEmpty()) continue;
505
-
506
- // Resolve trigger class: try multiple patterns (#169 fix)
507
- UClass* TrigClass = nullptr;
526
+ if (!TrigClass && !TypeName.IsEmpty())
508
527
  {
509
528
  TArray<FString> Candidates;
510
529
  if (TypeName.StartsWith(TEXT("UInputTrigger")) || TypeName.StartsWith(TEXT("InputTrigger")))
@@ -524,45 +543,63 @@ TSharedPtr<FJsonValue> FGameplayHandlers::SetMappingModifiers(const TSharedPtr<F
524
543
  TrigClass = nullptr;
525
544
  }
526
545
  }
546
+
527
547
  if (!TrigClass)
528
548
  {
549
+ FailedTriggers.Add(MakeShared<FJsonValueString>(ClassPath.IsEmpty() ? TypeName : ClassPath));
529
550
  continue;
530
551
  }
531
552
 
532
553
  UInputTrigger* Trigger = NewObject<UInputTrigger>(IMC, TrigClass);
533
-
534
- // Set properties via reflection (same pattern as modifiers)
535
- for (const auto& Pair : (*TrigObj)->Values)
554
+ if (!Trigger)
536
555
  {
537
- if (Pair.Key == TEXT("type")) continue;
538
-
539
- FProperty* Prop = TrigClass->FindPropertyByName(FName(*Pair.Key));
540
- if (!Prop) continue;
541
-
542
- void* PropAddr = Prop->ContainerPtrToValuePtr<void>(Trigger);
556
+ FailedTriggers.Add(MakeShared<FJsonValueString>(ClassPath.IsEmpty() ? TypeName : ClassPath));
557
+ continue;
558
+ }
543
559
 
544
- if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
545
- {
546
- double Val = 0;
547
- Pair.Value->TryGetNumber(Val);
548
- FloatProp->SetPropertyValue(PropAddr, (float)Val);
549
- }
550
- else if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
551
- {
552
- double Val = 0;
553
- Pair.Value->TryGetNumber(Val);
554
- DoubleProp->SetPropertyValue(PropAddr, Val);
555
- }
556
- else if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
560
+ // Set properties via reflection. Accept both top-level fields and a
561
+ // nested "properties" object (same as modifiers).
562
+ auto ApplyTriggerProps = [&](const TSharedPtr<FJsonObject>& Obj)
563
+ {
564
+ for (const auto& Pair : Obj->Values)
557
565
  {
558
- bool Val = false;
559
- Pair.Value->TryGetBool(Val);
560
- BoolProp->SetPropertyValue(PropAddr, Val);
566
+ if (Pair.Key == TEXT("type") || Pair.Key == TEXT("class") || Pair.Key == TEXT("properties")) continue;
567
+ FProperty* Prop = TrigClass->FindPropertyByName(FName(*Pair.Key));
568
+ if (!Prop) continue;
569
+ void* PropAddr = Prop->ContainerPtrToValuePtr<void>(Trigger);
570
+ if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
571
+ {
572
+ double Val = 0; Pair.Value->TryGetNumber(Val);
573
+ FloatProp->SetPropertyValue(PropAddr, (float)Val);
574
+ }
575
+ else if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
576
+ {
577
+ double Val = 0; Pair.Value->TryGetNumber(Val);
578
+ DoubleProp->SetPropertyValue(PropAddr, Val);
579
+ }
580
+ else if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
581
+ {
582
+ bool Val = false; Pair.Value->TryGetBool(Val);
583
+ BoolProp->SetPropertyValue(PropAddr, Val);
584
+ }
585
+ else if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
586
+ {
587
+ double Val = 0; Pair.Value->TryGetNumber(Val);
588
+ IntProp->SetPropertyValue(PropAddr, (int32)Val);
589
+ }
561
590
  }
591
+ };
592
+ ApplyTriggerProps(*TrigObj);
593
+ const TSharedPtr<FJsonObject>* PropsObj = nullptr;
594
+ if ((*TrigObj)->TryGetObjectField(TEXT("properties"), PropsObj) && PropsObj && (*PropsObj).IsValid())
595
+ {
596
+ ApplyTriggerProps(*PropsObj);
562
597
  }
563
598
 
564
- Mapping.Triggers.Add(Trigger);
599
+ NewTriggers.Add(Trigger);
565
600
  }
601
+
602
+ Mapping.Triggers = NewTriggers;
566
603
  }
567
604
 
568
605
  // Mark dirty — caller can use asset(save) to persist (#197 fix)
@@ -577,6 +614,12 @@ TSharedPtr<FJsonValue> FGameplayHandlers::SetMappingModifiers(const TSharedPtr<F
577
614
  Result->SetNumberField(TEXT("mappingIndex"), MappingIndex);
578
615
  Result->SetNumberField(TEXT("modifierCount"), Mapping.Modifiers.Num());
579
616
  Result->SetNumberField(TEXT("triggerCount"), Mapping.Triggers.Num());
617
+ if (FailedTriggers.Num() > 0)
618
+ {
619
+ // #725: tell the caller which trigger specs did not resolve rather than
620
+ // silently dropping them (or, worse, leaving a null entry behind).
621
+ Result->SetArrayField(TEXT("failedTriggers"), FailedTriggers);
622
+ }
580
623
  return MCPResult(Result);
581
624
  }
582
625
 
@@ -9,6 +9,7 @@
9
9
  #include "Landscape.h"
10
10
  #include "LandscapeEditTypes.h"
11
11
  #include "LandscapeProxy.h"
12
+ #include "LandscapeStreamingProxy.h"
12
13
  #include "LandscapeInfo.h"
13
14
  #include "LandscapeComponent.h"
14
15
  #include "LandscapeSplineActor.h"
@@ -38,6 +39,9 @@ void FLandscapeHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
38
39
  Registry.RegisterHandler(TEXT("create_landscape"), &CreateLandscape);
39
40
  Registry.RegisterHandler(TEXT("create_landscape_layer_info"), &CreateLandscapeLayerInfo);
40
41
  Registry.RegisterHandler(TEXT("get_landscape_material_usage_summary"), &GetMaterialUsageSummary);
42
+ // #733: World Partition landscape streaming-proxy enumeration + spatial lookup.
43
+ Registry.RegisterHandler(TEXT("list_landscape_proxies"), &ListLandscapeProxies);
44
+ Registry.RegisterHandler(TEXT("find_landscape_proxy_at"), &FindLandscapeProxyAt);
41
45
  }
42
46
 
43
47
  TSharedPtr<FJsonValue> FLandscapeHandlers::GetLandscapeInfo(const TSharedPtr<FJsonObject>& Params)
@@ -745,3 +749,99 @@ TSharedPtr<FJsonValue> FLandscapeHandlers::GetMaterialUsageSummary(const TShared
745
749
  Result->SetNumberField(TEXT("totalNaniteComponents"), TotalNanite);
746
750
  return MCPResult(Result);
747
751
  }
752
+
753
+ // #733: enumerate LandscapeStreamingProxy actors currently loaded in the world,
754
+ // with per-proxy world bounds and the parent Landscape count. On a World
755
+ // Partition map, an unloaded proxy silently reads layer weights as 0, so a
756
+ // measurement is only trustworthy once the covering proxy is confirmed loaded.
757
+ // Unloaded proxies are not spawned as actors, so the actor iterator only yields
758
+ // loaded ones - hence loaded is always true for enumerated entries; callers use
759
+ // the count + bounds to reason about coverage.
760
+ TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeProxies(const TSharedPtr<FJsonObject>& Params)
761
+ {
762
+ REQUIRE_EDITOR_WORLD(World);
763
+
764
+ int32 ParentLandscapes = 0;
765
+ TArray<TSharedPtr<FJsonValue>> Proxies;
766
+ for (TActorIterator<AActor> It(World); It; ++It)
767
+ {
768
+ AActor* Actor = *It;
769
+ if (!Actor) continue;
770
+ if (Actor->IsA<ALandscape>())
771
+ {
772
+ ParentLandscapes++;
773
+ continue;
774
+ }
775
+ ALandscapeStreamingProxy* Proxy = Cast<ALandscapeStreamingProxy>(Actor);
776
+ if (!Proxy) continue;
777
+
778
+ FVector Origin, Extent;
779
+ Proxy->GetActorBounds(false, Origin, Extent);
780
+
781
+ TSharedPtr<FJsonObject> ProxyObj = MakeShared<FJsonObject>();
782
+ ProxyObj->SetStringField(TEXT("label"), Proxy->GetActorLabel());
783
+ ProxyObj->SetBoolField(TEXT("loaded"), true);
784
+
785
+ TSharedPtr<FJsonObject> Bounds = MakeShared<FJsonObject>();
786
+ TSharedPtr<FJsonObject> OriginObj = MakeShared<FJsonObject>();
787
+ OriginObj->SetNumberField(TEXT("x"), Origin.X);
788
+ OriginObj->SetNumberField(TEXT("y"), Origin.Y);
789
+ OriginObj->SetNumberField(TEXT("z"), Origin.Z);
790
+ TSharedPtr<FJsonObject> ExtentObj = MakeShared<FJsonObject>();
791
+ ExtentObj->SetNumberField(TEXT("x"), Extent.X);
792
+ ExtentObj->SetNumberField(TEXT("y"), Extent.Y);
793
+ ExtentObj->SetNumberField(TEXT("z"), Extent.Z);
794
+ Bounds->SetObjectField(TEXT("origin"), OriginObj);
795
+ Bounds->SetObjectField(TEXT("extent"), ExtentObj);
796
+ ProxyObj->SetObjectField(TEXT("worldBounds"), Bounds);
797
+
798
+ Proxies.Add(MakeShared<FJsonValueObject>(ProxyObj));
799
+ }
800
+
801
+ auto Result = MCPSuccess();
802
+ Result->SetNumberField(TEXT("loadedProxies"), Proxies.Num());
803
+ Result->SetNumberField(TEXT("parentLandscapes"), ParentLandscapes);
804
+ Result->SetArrayField(TEXT("proxies"), Proxies);
805
+ Result->SetStringField(TEXT("note"), TEXT("World Partition unloaded proxies are not spawned as actors, so only loaded proxies are listed."));
806
+ return MCPResult(Result);
807
+ }
808
+
809
+ // #733: resolve which loaded LandscapeStreamingProxy's world bounds contain a
810
+ // world X/Y. Returns the covering proxy (loaded:true) or loaded:false when no
811
+ // loaded proxy covers the position - which usually means the covering proxy is
812
+ // streamed out, making any 0-weight readback there ambiguous rather than real.
813
+ TSharedPtr<FJsonValue> FLandscapeHandlers::FindLandscapeProxyAt(const TSharedPtr<FJsonObject>& Params)
814
+ {
815
+ REQUIRE_EDITOR_WORLD(World);
816
+
817
+ if (!Params->HasField(TEXT("worldX")) || !Params->HasField(TEXT("worldY")))
818
+ {
819
+ return MCPError(TEXT("Missing 'worldX'/'worldY' world position"));
820
+ }
821
+ const double TargetX = OptionalNumber(Params, TEXT("worldX"), 0.0);
822
+ const double TargetY = OptionalNumber(Params, TEXT("worldY"), 0.0);
823
+
824
+ for (TActorIterator<AActor> It(World); It; ++It)
825
+ {
826
+ ALandscapeStreamingProxy* Proxy = Cast<ALandscapeStreamingProxy>(*It);
827
+ if (!Proxy) continue;
828
+
829
+ FVector Origin, Extent;
830
+ Proxy->GetActorBounds(false, Origin, Extent);
831
+ if (TargetX >= Origin.X - Extent.X && TargetX <= Origin.X + Extent.X &&
832
+ TargetY >= Origin.Y - Extent.Y && TargetY <= Origin.Y + Extent.Y)
833
+ {
834
+ auto Result = MCPSuccess();
835
+ Result->SetBoolField(TEXT("found"), true);
836
+ Result->SetBoolField(TEXT("loaded"), true);
837
+ Result->SetStringField(TEXT("label"), Proxy->GetActorLabel());
838
+ return MCPResult(Result);
839
+ }
840
+ }
841
+
842
+ auto Result = MCPSuccess();
843
+ Result->SetBoolField(TEXT("found"), false);
844
+ Result->SetBoolField(TEXT("loaded"), false);
845
+ Result->SetStringField(TEXT("note"), TEXT("No loaded proxy covers this position; the covering proxy is likely streamed out, so weight/height readbacks here are ambiguous."));
846
+ return MCPResult(Result);
847
+ }
@@ -26,4 +26,8 @@ private:
26
26
  static TSharedPtr<FJsonValue> CreateLandscapeLayerInfo(const TSharedPtr<FJsonObject>& Params);
27
27
  // v0.7.19 issue #150 — concise material + component count summary per proxy
28
28
  static TSharedPtr<FJsonValue> GetMaterialUsageSummary(const TSharedPtr<FJsonObject>& Params);
29
+ // #733: enumerate loaded World Partition landscape streaming proxies with
30
+ // per-proxy world bounds, and resolve which proxy covers a world position.
31
+ static TSharedPtr<FJsonValue> ListLandscapeProxies(const TSharedPtr<FJsonObject>& Params);
32
+ static TSharedPtr<FJsonValue> FindLandscapeProxyAt(const TSharedPtr<FJsonObject>& Params);
29
33
  };
@@ -89,6 +89,8 @@
89
89
  void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
90
90
  {
91
91
  Registry.RegisterHandler(TEXT("get_world_outliner"), &GetOutliner);
92
+ // #717: query/set per-actor editor-only visibility (temporarily hidden).
93
+ Registry.RegisterHandler(TEXT("set_editor_visibility"), &SetEditorVisibility);
92
94
  Registry.RegisterHandler(TEXT("place_actor"), &PlaceActor);
93
95
  Registry.RegisterHandler(TEXT("delete_actor"), &DeleteActor);
94
96
  Registry.RegisterHandler(TEXT("get_actor_details"), &GetActorDetails);
@@ -180,6 +182,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
180
182
  int32 Limit = OptionalInt(Params, TEXT("limit"), 50);
181
183
  bool bIncludeStreaming = OptionalBool(Params, TEXT("includeStreaming"), false);
182
184
 
185
+ // #717: optional tri-state filter on editor-only visibility. When present,
186
+ // only actors whose IsTemporarilyHiddenInEditor() matches are returned. The
187
+ // per-actor editorHidden flag is always reported so callers can find lights
188
+ // that are hidden in the viewport but still render in game.
189
+ bool bEditorHiddenFilterValue = false;
190
+ const bool bHasEditorHiddenFilter = Params->TryGetBoolField(TEXT("editorHidden"), bEditorHiddenFilterValue);
191
+
183
192
  TArray<TSharedPtr<FJsonValue>> ActorsArray;
184
193
  int32 TotalCount = 0;
185
194
  int32 StreamingSkipped = 0;
@@ -213,6 +222,16 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
213
222
  {
214
223
  continue;
215
224
  }
225
+
226
+ #if WITH_EDITOR
227
+ const bool bEditorHidden = Actor->IsTemporarilyHiddenInEditor();
228
+ #else
229
+ const bool bEditorHidden = false;
230
+ #endif
231
+ if (bHasEditorHiddenFilter && bEditorHidden != bEditorHiddenFilterValue)
232
+ {
233
+ continue;
234
+ }
216
235
  if (ActorsArray.Num() >= Limit) break;
217
236
 
218
237
  TSharedPtr<FJsonObject> ActorObj = MakeShared<FJsonObject>();
@@ -220,6 +239,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
220
239
  ActorObj->SetStringField(TEXT("label"), ActorLabel);
221
240
  ActorObj->SetStringField(TEXT("class"), ActorClass);
222
241
  ActorObj->SetStringField(TEXT("path"), Actor->GetPathName());
242
+ ActorObj->SetBoolField(TEXT("editorHidden"), bEditorHidden);
223
243
 
224
244
  FVector Location = Actor->GetActorLocation();
225
245
  TSharedPtr<FJsonObject> LocationObj = MakeShared<FJsonObject>();
@@ -262,6 +282,65 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
262
282
  return MCPResult(Result);
263
283
  }
264
284
 
285
+ // #717: bulk set editor-only visibility (temporarily hidden in editor). Targets
286
+ // either an explicit actorLabels list or every actor (all=true). Editor-hidden
287
+ // actors still render in game, so unhiding them is a common cleanup step.
288
+ TSharedPtr<FJsonValue> FLevelHandlers::SetEditorVisibility(const TSharedPtr<FJsonObject>& Params)
289
+ {
290
+ REQUIRE_EDITOR_WORLD(World);
291
+
292
+ bool bHidden = false;
293
+ if (!Params->TryGetBoolField(TEXT("hidden"), bHidden))
294
+ {
295
+ return MCPError(TEXT("Missing 'hidden' parameter (true = hide in editor, false = show)"));
296
+ }
297
+
298
+ const bool bAll = OptionalBool(Params, TEXT("all"), false);
299
+
300
+ TSet<FString> TargetLabels;
301
+ const TArray<TSharedPtr<FJsonValue>>* LabelsArr = nullptr;
302
+ if (Params->TryGetArrayField(TEXT("actorLabels"), LabelsArr) && LabelsArr)
303
+ {
304
+ for (const TSharedPtr<FJsonValue>& V : *LabelsArr)
305
+ {
306
+ FString S;
307
+ if (V.IsValid() && V->TryGetString(S)) TargetLabels.Add(S);
308
+ }
309
+ }
310
+
311
+ if (!bAll && TargetLabels.Num() == 0)
312
+ {
313
+ return MCPError(TEXT("Provide 'actorLabels' (array) or 'all'=true"));
314
+ }
315
+
316
+ int32 Changed = 0;
317
+ int32 Matched = 0;
318
+ TArray<TSharedPtr<FJsonValue>> Affected;
319
+ for (TActorIterator<AActor> ActorIt(World); ActorIt; ++ActorIt)
320
+ {
321
+ AActor* Actor = *ActorIt;
322
+ if (!Actor) continue;
323
+ const FString Label = Actor->GetActorLabel();
324
+ if (!bAll && !TargetLabels.Contains(Label)) continue;
325
+ Matched++;
326
+ #if WITH_EDITOR
327
+ if (Actor->IsTemporarilyHiddenInEditor() != bHidden)
328
+ {
329
+ Actor->SetIsTemporarilyHiddenInEditor(bHidden);
330
+ Changed++;
331
+ Affected.Add(MakeShared<FJsonValueString>(Label));
332
+ }
333
+ #endif
334
+ }
335
+
336
+ auto Result = MCPSuccess();
337
+ Result->SetBoolField(TEXT("hidden"), bHidden);
338
+ Result->SetNumberField(TEXT("matched"), Matched);
339
+ Result->SetNumberField(TEXT("changed"), Changed);
340
+ Result->SetArrayField(TEXT("affected"), Affected);
341
+ return MCPResult(Result);
342
+ }
343
+
265
344
  TSharedPtr<FJsonValue> FLevelHandlers::PlaceActor(const TSharedPtr<FJsonObject>& Params)
266
345
  {
267
346
  FString ActorClass;
@@ -13,6 +13,8 @@ public:
13
13
  private:
14
14
  // Handler implementations
15
15
  static TSharedPtr<FJsonValue> GetOutliner(const TSharedPtr<FJsonObject>& Params);
16
+ // #717: bulk set editor-only visibility (temporarily hidden in editor)
17
+ static TSharedPtr<FJsonValue> SetEditorVisibility(const TSharedPtr<FJsonObject>& Params);
16
18
  static TSharedPtr<FJsonValue> PlaceActor(const TSharedPtr<FJsonObject>& Params);
17
19
  static TSharedPtr<FJsonValue> DeleteActor(const TSharedPtr<FJsonObject>& Params);
18
20
  static TSharedPtr<FJsonValue> GetActorDetails(const TSharedPtr<FJsonObject>& Params);
@@ -131,6 +131,21 @@ TSharedPtr<FJsonValue> FLevelHandlers::SpawnLight(const TSharedPtr<FJsonObject>&
131
131
  {
132
132
  LightComponent->SetLightColor(LightColor);
133
133
  }
134
+ // #723: attenuationRadius was accepted by the schema but never applied.
135
+ // It lives on the local-light components (point/spot/rect); directional
136
+ // and sky lights have no attenuation radius, so they ignore it.
137
+ double AttenuationRadius = 0.0;
138
+ if (Params->TryGetNumberField(TEXT("attenuationRadius"), AttenuationRadius) && AttenuationRadius > 0.0)
139
+ {
140
+ if (UPointLightComponent* PointComp = Cast<UPointLightComponent>(LightComponent))
141
+ {
142
+ PointComp->SetAttenuationRadius(static_cast<float>(AttenuationRadius));
143
+ }
144
+ else if (URectLightComponent* RectComp = Cast<URectLightComponent>(LightComponent))
145
+ {
146
+ RectComp->SetAttenuationRadius(static_cast<float>(AttenuationRadius));
147
+ }
148
+ }
134
149
  LightComponent->SetVisibility(true);
135
150
  LightComponent->MarkRenderStateDirty();
136
151
  }
@@ -137,10 +137,15 @@ TSharedPtr<FJsonValue> FPhysicsHandlers::SetPhysicsEnabled(const TSharedPtr<FJso
137
137
  FString ActorLabel;
138
138
  if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
139
139
 
140
+ // #721: the published TS schema names this parameter "simulate" while the
141
+ // handler historically read only "enabled", so a schema-conformant call
142
+ // silently no-opped. Accept either spelling (simulate | enabled) and error
143
+ // explicitly when neither is present rather than succeeding silently.
140
144
  bool bEnabled = true;
141
- if (!Params->TryGetBoolField(TEXT("enabled"), bEnabled))
145
+ if (!Params->TryGetBoolField(TEXT("simulate"), bEnabled) &&
146
+ !Params->TryGetBoolField(TEXT("enabled"), bEnabled))
142
147
  {
143
- return MCPError(TEXT("Missing 'enabled' parameter (true/false)"));
148
+ return MCPError(TEXT("Missing 'simulate' (aka 'enabled') parameter (true/false)"));
144
149
  }
145
150
 
146
151
  REQUIRE_EDITOR_WORLD(World);
@@ -571,8 +571,12 @@ TSharedPtr<FJsonValue> FWidgetHandlers::AddWidget(const TSharedPtr<FJsonObject>&
571
571
  }
572
572
  }
573
573
 
574
- // UE 5.4 exposed this map; UE 5.5 removed it from UWidgetBlueprint.
575
- #if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 4
574
+ // #728: the WidgetBlueprintCompiler ensures every added widget has an entry in
575
+ // WidgetVariableNameToGuidMap (UMGEditor WidgetBlueprintCompiler.cpp: "Widget
576
+ // [X] was added but did not get a GUID"). The map was present in 5.4, absent
577
+ // in the 5.5-5.7 window, and present again in 5.8, so register the GUID on
578
+ // 5.4 and on 5.8+ (skipping the versions where the member does not exist).
579
+ #if (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 4) || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 8) || ENGINE_MAJOR_VERSION > 5
576
580
  if (!WidgetBP->WidgetVariableNameToGuidMap.Contains(NewWidget->GetFName()))
577
581
  {
578
582
  WidgetBP->WidgetVariableNameToGuidMap.Add(NewWidget->GetFName(), FGuid::NewGuid());
@@ -906,6 +910,15 @@ TSharedPtr<FJsonValue> FWidgetHandlers::WrapRoot(const TSharedPtr<FJsonObject>&
906
910
  WidgetBP->WidgetTree->RootWidget = Wrapper;
907
911
  Wrapper->AddChild(OldRoot);
908
912
 
913
+ // #728: register the new wrapper's GUID so the WidgetBlueprintCompiler ensure
914
+ // does not fire (see add_widget). Same version window as there.
915
+ #if (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION == 4) || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 8) || ENGINE_MAJOR_VERSION > 5
916
+ if (!WidgetBP->WidgetVariableNameToGuidMap.Contains(Wrapper->GetFName()))
917
+ {
918
+ WidgetBP->WidgetVariableNameToGuidMap.Add(Wrapper->GetFName(), FGuid::NewGuid());
919
+ }
920
+ #endif
921
+
909
922
  WidgetBP->MarkPackageDirty();
910
923
  FKismetEditorUtilities::CompileBlueprint(WidgetBP);
911
924
  UEditorAssetLibrary::SaveAsset(AssetPath);
@@ -62,6 +62,7 @@ public class UE_MCP_Bridge : ModuleRules
62
62
  "Landscape",
63
63
  "LevelEditor",
64
64
  "LevelSequence",
65
+ "LevelSequenceEditor",
65
66
  "MaterialEditor",
66
67
  "MovieScene",
67
68
  "MovieSceneTracks",
@@ -77,6 +78,7 @@ public class UE_MCP_Bridge : ModuleRules
77
78
  "PropertyEditor",
78
79
  "PythonScriptPlugin",
79
80
  "Sequencer",
81
+ "Settings",
80
82
  "Slate",
81
83
  "SlateCore",
82
84
  "StateTreeModule",
@@ -7,6 +7,19 @@ description: Use when authoring or modifying Unreal Blueprint assets through ue-
7
7
 
8
8
  The `blueprint` tool covers reading, authoring, and compiling Blueprints. The default workflow is **read → mutate → compile**, never fire-and-forget.
9
9
 
10
+ ## Authoring a graph body: prefer the Epic DSL (#711)
11
+
12
+ When you are authoring the **contents of a graph** (event graph, function body, macro) - a set of nodes plus their wiring - do **not** default to node-by-node `add_node`/`connect_pins`. Reach for Epic's graph DSL first:
13
+
14
+ 1. `blueprint(action="epic_get_graph_dsl_docs")` - read the S-expression grammar once.
15
+ 2. `blueprint(action="epic_write_graph_dsl", ...)` - author + compile the whole graph body in one call.
16
+
17
+ The DSL authors and compiles an entire graph in a single round-trip, which is materially faster and more reliable than stitching individual K2Nodes together (one correct pass vs several failed iterations). Use it for graph bodies whenever it is available.
18
+
19
+ **Availability / fallback.** The `epic_*` actions come from Epic's ToolsetRegistry and require **UE 5.8+ with the Epic toolset plugins enabled**. Check with `epic(action="status")`. When they are unavailable (pre-5.8, or the registry is off), fall back to the native node path below.
20
+
21
+ **Keep using the native actions for** read/discovery, SCS components, CDO/class defaults, interfaces + event dispatchers, structured `compile`/`validate`, and anything with no Epic equivalent - the native path adds idempotency and rollback the raw tools lack. See the `ue-mcp-epic-routing` skill for the full epic-vs-native decision.
22
+
10
23
  ## Discovery before authoring
11
24
 
12
25
  For any existing Blueprint:
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: ue-mcp-epic-routing
3
+ description: Use when deciding between ue-mcp's native category actions and Epic's wrapped ToolsetRegistry tools (the `epic_*` actions, incl. the Blueprint graph DSL) for a task in Unreal. Pulls in when authoring Blueprint graph bodies, or any time both a native action and an epic_* action could do the job and you need to pick.
4
+ ---
5
+
6
+ # Epic-vs-native tool routing (ue-mcp)
7
+
8
+ ue-mcp exposes two overlapping surfaces. Picking the right one per task avoids slow, failure-prone paths.
9
+
10
+ - **Native category actions** - `blueprint(...)`, `level(...)`, `material(...)`, etc. Hand-written, idempotent, with rollback and structured errors.
11
+ - **Epic `epic_*` actions** - Epic's own MCP toolsets, surfaced in-category when the ToolsetRegistry is available (UE 5.8+ with the Epic toolset plugins enabled). Discover them with `epic(action="status")` and `epic(action="list_toolsets")`.
12
+
13
+ This is **not** a blanket "always use epic" rule. Epic needs 5.8 + plugins, and much of ue-mcp has no epic equivalent or adds idempotency/rollback the raw tools lack.
14
+
15
+ ## The one that matters most: Blueprint graph bodies -> the DSL
16
+
17
+ Authoring the **contents of a graph** (event graph, function body, macro) is the headline case.
18
+
19
+ - Prefer `blueprint(action="epic_write_graph_dsl", ...)`. Call `blueprint(action="epic_get_graph_dsl_docs")` first to get the S-expression grammar.
20
+ - It authors **and compiles the whole graph in one call** - materially faster and more reliable than node-by-node `add_node` + `connect_pins` (typically one correct pass instead of several failed iterations).
21
+
22
+ ## Decision table
23
+
24
+ | Task | Route |
25
+ |------|-------|
26
+ | Author/replace a graph body (nodes + wiring) | `blueprint(epic_write_graph_dsl)` (docs first) |
27
+ | Read / inspect a graph, list graphs/variables/functions | native `blueprint(read*, list_*, get_execution_flow)` |
28
+ | SCS components, reparenting, component properties | native `blueprint(add_component, ...)` |
29
+ | CDO / class defaults, tick settings | native `blueprint(set_class_default, set_actor_tick_settings)` |
30
+ | Interfaces, event dispatchers | native `blueprint(create_interface, add_interface, add_event_dispatcher)` |
31
+ | Structured compile / validate with diagnostics | native `blueprint(compile, validate)` |
32
+ | A task with no native action but an `epic_*` one exists | the `epic_*` action |
33
+ | A task with no epic equivalent | the native action |
34
+
35
+ ## Fallback
36
+
37
+ If `epic(action="status")` reports the registry unavailable (pre-5.8, plugins off, or `available=false`), route **everything** through the native path - the DSL actions will not exist. The native node-by-node path (`add_node` -> `set_node_property` -> `connect_pins` -> `compile`) always works; see the `ue-mcp-blueprint` skill.