ultimate-unreal-engine-mcp 0.1.16 → 0.1.18

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.16",
3
+ "version": "0.1.18",
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
@@ -634,4 +636,102 @@ void RegisterActorCommands(FMCPCommandRouter& Router)
634
636
 
635
637
  SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
636
638
  });
639
+
640
+ // -----------------------------------------------------------------------
641
+ // actor.componentBounds
642
+ // Returns bounds of each scene component on an actor, or a specific one.
643
+ // Payload: { actor_label, component_name? }
644
+ // If component_name is omitted, returns all scene components with bounds.
645
+ // -----------------------------------------------------------------------
646
+ Router.RegisterHandler(TEXT("actor.componentBounds"), [](TSharedPtr<FJsonObject> Cmd, FMCPResponseSender SendResponse)
647
+ {
648
+ const FString CorrId = Cmd->GetStringField(TEXT("correlationId"));
649
+
650
+ TSharedPtr<FJsonObject> Payload;
651
+ const TSharedPtr<FJsonValue>* PayloadVal = Cmd->Values.Find(TEXT("payload"));
652
+ if (PayloadVal && (*PayloadVal)->Type == EJson::Object)
653
+ {
654
+ Payload = (*PayloadVal)->AsObject();
655
+ }
656
+
657
+ FString ActorLabel;
658
+ if (!Payload.IsValid()
659
+ || !Payload->TryGetStringField(TEXT("actor_label"), ActorLabel) || ActorLabel.IsEmpty())
660
+ {
661
+ SendResponse(BuildActorErrorResponse(CorrId, TEXT("missing_actor_label")) + TEXT("\n"));
662
+ return;
663
+ }
664
+
665
+ FString FilterComponent;
666
+ Payload->TryGetStringField(TEXT("component_name"), FilterComponent);
667
+
668
+ UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
669
+ if (!World)
670
+ {
671
+ SendResponse(BuildActorErrorResponse(CorrId, TEXT("no_world_open")) + TEXT("\n"));
672
+ return;
673
+ }
674
+
675
+ AActor* TargetActor = nullptr;
676
+ for (TActorIterator<AActor> It(World); It; ++It)
677
+ {
678
+ if ((*It)->GetActorLabel() == ActorLabel)
679
+ {
680
+ TargetActor = *It;
681
+ break;
682
+ }
683
+ }
684
+ if (!TargetActor)
685
+ {
686
+ SendResponse(BuildActorErrorResponse(CorrId, TEXT("actor_not_found")) + TEXT("\n"));
687
+ return;
688
+ }
689
+
690
+ TArray<TSharedPtr<FJsonValue>> ComponentsArray;
691
+
692
+ TInlineComponentArray<USceneComponent*> Components;
693
+ TargetActor->GetComponents(Components);
694
+
695
+ for (USceneComponent* Comp : Components)
696
+ {
697
+ if (!Comp) continue;
698
+ if (!FilterComponent.IsEmpty() && Comp->GetName() != FilterComponent) continue;
699
+
700
+ FBoxSphereBounds Bounds = Comp->CalcBounds(Comp->GetComponentTransform());
701
+
702
+ TSharedPtr<FJsonObject> CompObj = MakeShared<FJsonObject>();
703
+ CompObj->SetStringField(TEXT("name"), Comp->GetName());
704
+ CompObj->SetStringField(TEXT("class"), Comp->GetClass()->GetName());
705
+
706
+ // World-space bounds
707
+ TSharedPtr<FJsonObject> OriginObj = MakeShared<FJsonObject>();
708
+ OriginObj->SetNumberField(TEXT("x"), Bounds.Origin.X);
709
+ OriginObj->SetNumberField(TEXT("y"), Bounds.Origin.Y);
710
+ OriginObj->SetNumberField(TEXT("z"), Bounds.Origin.Z);
711
+ CompObj->SetObjectField(TEXT("origin"), OriginObj);
712
+
713
+ TSharedPtr<FJsonObject> ExtentObj = MakeShared<FJsonObject>();
714
+ ExtentObj->SetNumberField(TEXT("x"), Bounds.BoxExtent.X);
715
+ ExtentObj->SetNumberField(TEXT("y"), Bounds.BoxExtent.Y);
716
+ ExtentObj->SetNumberField(TEXT("z"), Bounds.BoxExtent.Z);
717
+ CompObj->SetObjectField(TEXT("extent"), ExtentObj);
718
+
719
+ // Relative transform
720
+ FVector RelLoc = Comp->GetRelativeLocation();
721
+ TSharedPtr<FJsonObject> RelLocObj = MakeShared<FJsonObject>();
722
+ RelLocObj->SetNumberField(TEXT("x"), RelLoc.X);
723
+ RelLocObj->SetNumberField(TEXT("y"), RelLoc.Y);
724
+ RelLocObj->SetNumberField(TEXT("z"), RelLoc.Z);
725
+ CompObj->SetObjectField(TEXT("relative_location"), RelLocObj);
726
+
727
+ ComponentsArray.Add(MakeShared<FJsonValueObject>(CompObj));
728
+ }
729
+
730
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
731
+ Data->SetStringField(TEXT("label"), ActorLabel);
732
+ Data->SetArrayField(TEXT("components"), ComponentsArray);
733
+ Data->SetNumberField(TEXT("count"), static_cast<double>(ComponentsArray.Num()));
734
+
735
+ SendResponse(BuildActorSuccessResponse(CorrId, Data) + TEXT("\n"));
736
+ });
637
737
  }
@@ -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,19 +331,20 @@ void RegisterViewportCommands(FMCPCommandRouter& Router)
301
331
  }
302
332
  }
303
333
 
304
- // Set global screenshot resolution and queue capture.
305
- GScreenshotResolutionX = Width;
306
- GScreenshotResolutionY = Height;
307
- FScreenshotRequest::RequestScreenshot(false /* bShowUI */);
334
+ // Synchronous capture via ReadPixels works reliably for repeated calls.
335
+ const FString FilePath = TakeScreenshotToFile(Width, Height);
308
336
 
309
- // Return the directory where UE will write the screenshot file.
310
- const FString ShotDir = FPaths::ScreenShotDir();
337
+ if (FilePath.IsEmpty())
338
+ {
339
+ SendResponse(BuildViewportErrorResponse(CorrId, TEXT("screenshot_capture_failed")) + TEXT("\n"));
340
+ return;
341
+ }
311
342
 
312
343
  TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
313
- Data->SetStringField(TEXT("screenshot_dir"), ShotDir);
344
+ Data->SetStringField(TEXT("file_path"), FilePath);
314
345
  Data->SetNumberField(TEXT("width"), static_cast<double>(Width));
315
346
  Data->SetNumberField(TEXT("height"), static_cast<double>(Height));
316
- Data->SetBoolField(TEXT("queued"), true);
347
+ Data->SetBoolField(TEXT("saved"), true);
317
348
 
318
349
  SendResponse(BuildViewportSuccessResponse(CorrId, Data) + TEXT("\n"));
319
350
  });