ultimate-unreal-engine-mcp 0.1.17 → 0.1.19

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-unreal-engine-mcp",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "MCP server giving AI assistants full access to Unreal Engine 5.7 projects",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,9 +1,11 @@
1
1
  // MCPActorCommands.cpp
2
- // Implements four actor command handlers for the MCP bridge:
3
- // actor.list -- enumerate all actors in the open level
4
- // actor.spawn -- spawn an actor by class name at a given location
5
- // actor.transform -- move/rotate/scale an actor by label
6
- // actor.delete -- remove an actor from the level
2
+ // Implements actor command handlers for the MCP bridge:
3
+ // actor.list -- enumerate all actors in the open level
4
+ // actor.spawn -- spawn an actor by class name at a given location
5
+ // actor.transform -- move/rotate/scale an actor by label
6
+ // actor.delete -- remove an actor from the level
7
+ // actor.setProperty -- set a UPROPERTY on an actor or component
8
+ // actor.componentBounds -- get bounds of individual components on an actor
7
9
  //
8
10
  // All handlers run on the game thread (guaranteed by FMCPCommandRouter::Dispatch).
9
11
  // actor.transform and actor.delete call Actor->Modify() before any state change
@@ -614,6 +616,42 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
614
616
  ObjProp->SetObjectPropertyValue(ValuePtr, Asset);
615
617
  }
616
618
  }
619
+ else if (ValueType == TEXT("vector"))
620
+ {
621
+ // Parse {x, y, z} JSON object for FVector properties (e.g. RelativeLocation).
622
+ const TSharedPtr<FJsonObject>* VecObj;
623
+ if (Payload->TryGetObjectField(TEXT("value"), VecObj))
624
+ {
625
+ double X = 0.0, Y = 0.0, Z = 0.0;
626
+ (*VecObj)->TryGetNumberField(TEXT("x"), X);
627
+ (*VecObj)->TryGetNumberField(TEXT("y"), Y);
628
+ (*VecObj)->TryGetNumberField(TEXT("z"), Z);
629
+
630
+ FStructProperty* StructProp = CastField<FStructProperty>(Prop);
631
+ if (StructProp && StructProp->Struct == TBaseStructure<FVector>::Get())
632
+ {
633
+ *reinterpret_cast<FVector*>(ValuePtr) = FVector(X, Y, Z);
634
+ }
635
+ }
636
+ }
637
+ else if (ValueType == TEXT("rotator"))
638
+ {
639
+ // Parse {pitch, yaw, roll} JSON object for FRotator properties (e.g. RelativeRotation).
640
+ const TSharedPtr<FJsonObject>* RotObj;
641
+ if (Payload->TryGetObjectField(TEXT("value"), RotObj))
642
+ {
643
+ double Pitch = 0.0, Yaw = 0.0, Roll = 0.0;
644
+ (*RotObj)->TryGetNumberField(TEXT("pitch"), Pitch);
645
+ (*RotObj)->TryGetNumberField(TEXT("yaw"), Yaw);
646
+ (*RotObj)->TryGetNumberField(TEXT("roll"), Roll);
647
+
648
+ FStructProperty* StructProp = CastField<FStructProperty>(Prop);
649
+ if (StructProp && StructProp->Struct == TBaseStructure<FRotator>::Get())
650
+ {
651
+ *reinterpret_cast<FRotator*>(ValuePtr) = FRotator(Pitch, Yaw, Roll);
652
+ }
653
+ }
654
+ }
617
655
  else
618
656
  {
619
657
  SendResponse(BuildActorErrorResponse(CorrId, TEXT("unsupported_value_type")) + TEXT("\n"));
@@ -634,4 +672,102 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
634
672
 
635
673
  SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
636
674
  });
675
+
676
+ // -----------------------------------------------------------------------
677
+ // actor.componentBounds
678
+ // Returns bounds of each scene component on an actor, or a specific one.
679
+ // Payload: { actor_label, component_name? }
680
+ // If component_name is omitted, returns all scene components with bounds.
681
+ // -----------------------------------------------------------------------
682
+ Router.RegisterHandler(TEXT("actor.componentBounds"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
683
+ {
684
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
685
+
686
+ TSharedPtr<FJsonObject> Payload;
687
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
688
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
689
+ {
690
+ Payload = (*PayloadVal)->AsObject();
691
+ }
692
+
693
+ FString ActorLabel;
694
+ if (!Payload.IsValid()
695
+ || !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty())
696
+ {
697
+ SendResponse(BuildActorErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
698
+ return;
699
+ }
700
+
701
+ FString FilterComponent;
702
+ Payload->TryGetStringField(TEXT("component_name"), FilterComponent);
703
+
704
+ UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
705
+ if (!World)
706
+ {
707
+ SendResponse(BuildActorErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
708
+ return;
709
+ }
710
+
711
+ AActor* TargetActor = nullptr;
712
+ for (TActorIterator<AActor> It(World); It; ++It)
713
+ {
714
+ if ((*It)->GetActorLabel() == ActorLabel)
715
+ {
716
+ TargetActor = *It;
717
+ break;
718
+ }
719
+ }
720
+ if (!TargetActor)
721
+ {
722
+ SendResponse(BuildActorErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
723
+ return;
724
+ }
725
+
726
+ TArray<TSharedPtr<FJsonValue>> ComponentsArray;
727
+
728
+ TInlineComponentArray<USceneComponent*> Components;
729
+ TargetActor->GetComponents(Components);
730
+
731
+ for (USceneComponent* Comp : Components)
732
+ {
733
+ if (!Comp) continue;
734
+ if (!FilterComponent.IsEmpty() && Comp->GetName() != FilterComponent) continue;
735
+
736
+ FBoxSphereBounds Bounds = Comp->CalcBounds(Comp->GetComponentTransform());
737
+
738
+ TSharedPtr<FJsonObject> CompObj = MakeShared<FJsonObject>();
739
+ CompObj->SetStringField(TEXT("name"), Comp->GetName());
740
+ CompObj->SetStringField(TEXT("class"), Comp->GetClass()->GetName());
741
+
742
+ // World-space bounds
743
+ TSharedPtr<FJsonObject> OriginObj = MakeShared<FJsonObject>();
744
+ OriginObj->SetNumberField(TEXT("x"), Bounds.Origin.X);
745
+ OriginObj->SetNumberField(TEXT("y"), Bounds.Origin.Y);
746
+ OriginObj->SetNumberField(TEXT("z"), Bounds.Origin.Z);
747
+ CompObj->SetObjectField(TEXT("origin"), OriginObj);
748
+
749
+ TSharedPtr<FJsonObject> ExtentObj = MakeShared<FJsonObject>();
750
+ ExtentObj->SetNumberField(TEXT("x"), Bounds.BoxExtent.X);
751
+ ExtentObj->SetNumberField(TEXT("y"), Bounds.BoxExtent.Y);
752
+ ExtentObj->SetNumberField(TEXT("z"), Bounds.BoxExtent.Z);
753
+ CompObj->SetObjectField(TEXT("extent"), ExtentObj);
754
+
755
+ // Relative transform
756
+ FVector RelLoc = Comp->GetRelativeLocation();
757
+ TSharedPtr<FJsonObject> RelLocObj = MakeShared<FJsonObject>();
758
+ RelLocObj->SetNumberField(TEXT("x"), RelLoc.X);
759
+ RelLocObj->SetNumberField(TEXT("y"), RelLoc.Y);
760
+ RelLocObj->SetNumberField(TEXT("z"), RelLoc.Z);
761
+ CompObj->SetObjectField(TEXT("relative_location"), RelLocObj);
762
+
763
+ ComponentsArray.Add(MakeShared<FJsonValueObject>(CompObj));
764
+ }
765
+
766
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
767
+ Data->SetStringField(TEXT("label"), ActorLabel);
768
+ Data->SetArrayField(TEXT("components"), ComponentsArray);
769
+ Data->SetNumberField(TEXT("count"), static_cast<double>(ComponentsArray.Num()));
770
+
771
+ SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
772
+ });
637
773
  }
@@ -32,6 +32,7 @@
32
32
  #include "Misc/DateTime.h"
33
33
  #include "HAL/FileManager.h"
34
34
  #include "Misc/FileHelper.h"
35
+ #include "ImageUtils.h"
35
36
  #include "Engine/World.h"
36
37
  #include "GameFramework/Actor.h"
37
38
  #include "EngineUtils.h"
@@ -186,10 +187,11 @@ static FString GetMCPScreenshotDir()
186
187
  }
187
188
 
188
189
  /**
189
- * Queues an MCP screenshot with the given dimensions and returns the target file path.
190
+ * Captures an MCP screenshot synchronously using FViewport::ReadPixels().
190
191
  *
191
- * Uses the naming convention mcp_screenshot_{timestamp}.png in the MCPScreenshots directory.
192
- * Screenshot capture is asynchronous -- the file may not exist immediately after this call.
192
+ * Unlike FScreenshotRequest (async, one-shot per render, unreliable for repeated calls),
193
+ * this forces a viewport redraw and reads the framebuffer directly.
194
+ * Returns the file path on success, or empty string on failure.
193
195
  *
194
196
  * Threat T-31-05: width clamped 64..7680, height clamped 64..4320 (same as T-12-06).
195
197
  */
@@ -199,13 +201,41 @@ static FString TakeScreenshotToFile(int32 Width, int32 Height)
199
201
  Width = FMath::Clamp(Width, 64, 7680);
200
202
  Height = FMath::Clamp(Height, 64, 4320);
201
203
 
204
+ FLevelEditorViewportClient* ViewportClient = GetActiveViewportClient();
205
+ if (!ViewportClient || !ViewportClient->Viewport)
206
+ {
207
+ return FString();
208
+ }
209
+
210
+ // Force the viewport to redraw so we capture the current frame.
211
+ ViewportClient->Viewport->Invalidate();
212
+ ViewportClient->Viewport->Draw();
213
+
214
+ // Read pixels from the viewport framebuffer.
215
+ TArray<FColor> Pixels;
216
+ if (!ViewportClient->Viewport->ReadPixels(Pixels))
217
+ {
218
+ return FString();
219
+ }
220
+
221
+ const int32 VPWidth = ViewportClient->Viewport->GetSizeXY().X;
222
+ const int32 VPHeight = ViewportClient->Viewport->GetSizeXY().Y;
223
+
224
+ if (Pixels.Num() != VPWidth * VPHeight || VPWidth == 0 || VPHeight == 0)
225
+ {
226
+ return FString();
227
+ }
228
+
229
+ // Compress to PNG.
230
+ TArray64<uint8> PngData;
231
+ FImageUtils::PNGCompressImageArray(VPWidth, VPHeight, Pixels, PngData);
232
+
233
+ // Write to file.
202
234
  const FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S_%s"));
203
235
  const FString FileName = FString::Printf(TEXT("mcp_screenshot_%s.png"), *Timestamp);
204
236
  const FString FilePath = FPaths::Combine(GetMCPScreenshotDir(), FileName);
205
237
 
206
- GScreenshotResolutionX = Width;
207
- GScreenshotResolutionY = Height;
208
- FScreenshotRequest::RequestScreenshot(FilePath, false /* bShowUI */, false /* bAddFilenameSuffix */);
238
+ FFileHelper::SaveArrayToFile(PngData, *FilePath);
209
239
 
210
240
  return FilePath;
211
241
  }
@@ -301,14 +331,20 @@ void RegisterViewportCommands(FMCPCommandRouter& Router)
301
331
  }
302
332
  }
303
333
 
304
- // Use TakeScreenshotToFile which writes to MCPScreenshots/ with a known path.
334
+ // Synchronous capture via ReadPixels works reliably for repeated calls.
305
335
  const FString FilePath = TakeScreenshotToFile(Width, Height);
306
336
 
337
+ if (FilePath.IsEmpty())
338
+ {
339
+ SendResponse(BuildViewportErrorResponse(CorrId, TEXT("screenshot_capture_failed")) + TEXT("\n"));
340
+ return;
341
+ }
342
+
307
343
  TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
308
344
  Data->SetStringField(TEXT("file_path"), FilePath);
309
345
  Data->SetNumberField(TEXT("width"), static_cast<double>(Width));
310
346
  Data->SetNumberField(TEXT("height"), static_cast<double>(Height));
311
- Data->SetBoolField(TEXT("queued"), true);
347
+ Data->SetBoolField(TEXT("saved"), true);
312
348
 
313
349
  SendResponse(BuildViewportSuccessResponse(CorrId, Data) + TEXT("\n"));
314
350
  });