Unified, portable Unreal Engine MCP system plugin

Merge of the two project copies into one self-contained plugin (the superset:
variable_op + variables.py, full pcg_op runtime/declarative/preset ops, the
CreateWidgetFloatAnimation widget tool, and full Voxel graph authoring).

Made fully project- and machine-agnostic — no hardcoded paths:
- New src/projectPaths.js auto-detects the host .uproject (walk-up), project
  name, Editor build target, log file, and engine install (EngineAssociation
  via launcher manifest/registry, else installed-engine scan). All overridable
  via UE_* env vars.
- Rewired buildOrchestrator/insights/launcher/insightsExporter/server.js and the
  Python workers (cpp_scaffold, live_coding, apply_graph, console) off the old
  C:/Github/ihy, IHY*, E:/UE_Versions and UE_5.7 literals onto the resolver.
- Voxel made optional: Build.cs auto-detects the Voxel plugin (env
  UE_MCP_WITH_VOXEL override) and the C++ compiles to stubs under WITH_MCP_VOXEL,
  so the module builds in projects without Voxel; .uplugin marks Voxel optional.
- De-branded the agent-gateway and docs; scrubbed a leaked API key; excluded
  node_modules/Binaries/Intermediate/__pycache__/secrets from the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bonchellon
2026-06-24 01:07:24 +03:00
parent d46e03e53e
commit eba71c4ca8
76 changed files with 23838 additions and 1 deletions

View File

@ -0,0 +1,202 @@
#include "MCPCaptureLibrary.h"
#include "CanvasTypes.h"
#include "Components/SceneCaptureComponent2D.h"
#include "Editor.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Engine/World.h"
#include "HAL/PlatformFileManager.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"
#include "Misc/App.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "ObjectTools.h"
#include "RenderingThread.h"
#include "Misc/ObjectThumbnail.h"
namespace
{
// Encode an array of BGRA8 pixels to a PNG on disk.
bool SaveBGRAToPNG(
const uint8* BGRA,
int32 NumBytes,
int32 Width,
int32 Height,
const FString& OutputAbsPath,
const FString& FileNameNoExt,
FString& OutFullPath,
FString& OutError)
{
if (!BGRA || NumBytes < Width * Height * 4)
{
OutError = TEXT("empty / undersized pixel buffer");
return false;
}
IImageWrapperModule& Mod =
FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
TSharedPtr<IImageWrapper> PNG = Mod.CreateImageWrapper(EImageFormat::PNG);
if (!PNG.IsValid() ||
!PNG->SetRaw(BGRA, Width * Height * 4, Width, Height, ERGBFormat::BGRA, 8))
{
OutError = TEXT("PNG SetRaw failed");
return false;
}
const TArray64<uint8>& Compressed = PNG->GetCompressed(100);
IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile();
if (!PF.DirectoryExists(*OutputAbsPath))
{
PF.CreateDirectoryTree(*OutputAbsPath);
}
const FString FullPath = FPaths::Combine(OutputAbsPath, FileNameNoExt + TEXT(".png"));
if (!FFileHelper::SaveArrayToFile(Compressed, *FullPath))
{
OutError = FString::Printf(TEXT("SaveArrayToFile failed: %s"), *FullPath);
return false;
}
OutFullPath = FullPath;
return true;
}
}
bool UMCPCaptureLibrary::RenderAssetThumbnailToPNG(
const FString& AssetPath,
const FString& OutputAbsPath,
const FString& FileNameNoExt,
int32 Width,
int32 Height,
FString& OutFullPath,
FString& OutError)
{
OutFullPath.Reset();
OutError.Reset();
const int32 W = FMath::Clamp(Width, 16, 2048);
const int32 H = FMath::Clamp(Height, 16, 2048);
UObject* Object = StaticLoadObject(UObject::StaticClass(), nullptr, *AssetPath);
if (!Object)
{
OutError = FString::Printf(TEXT("could not load asset: %s"), *AssetPath);
return false;
}
// Render the asset through its registered thumbnail renderer. This is exactly
// what the Content Browser uses, so it works for every asset type and needs
// no open editor. RenderThumbnail allocates its own render target when we
// pass null and fills the thumbnail with uncompressed BGRA8 pixels.
FObjectThumbnail Thumbnail;
ThumbnailTools::RenderThumbnail(
Object,
static_cast<uint32>(W),
static_cast<uint32>(H),
ThumbnailTools::EThumbnailTextureFlushMode::AlwaysFlush,
nullptr,
&Thumbnail);
const TArray<uint8>& Bytes = Thumbnail.GetUncompressedImageData();
if (Bytes.Num() == 0)
{
OutError = FString::Printf(
TEXT("thumbnail renderer produced no pixels for %s (no renderer for this asset type?)"),
*AssetPath);
return false;
}
return SaveBGRAToPNG(
Bytes.GetData(),
Bytes.Num(),
Thumbnail.GetImageWidth(),
Thumbnail.GetImageHeight(),
OutputAbsPath,
FileNameNoExt,
OutFullPath,
OutError);
}
bool UMCPCaptureLibrary::CaptureEditorSceneToPNG(
FVector Location,
FRotator Rotation,
float FOV,
const FString& OutputAbsPath,
const FString& FileNameNoExt,
int32 Width,
int32 Height,
FString& OutFullPath,
FString& OutError)
{
OutFullPath.Reset();
OutError.Reset();
UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
if (!World)
{
OutError = TEXT("no editor world available");
return false;
}
const int32 W = FMath::Clamp(Width, 16, 4096);
const int32 H = FMath::Clamp(Height, 16, 4096);
UTextureRenderTarget2D* RT = NewObject<UTextureRenderTarget2D>(GetTransientPackage());
RT->ClearColor = FLinearColor::Black;
RT->TargetGamma = 2.2f;
RT->InitCustomFormat(W, H, PF_B8G8R8A8, /*bForceLinearGamma=*/false);
RT->UpdateResourceImmediate(true);
// Transient capture component bound to the editor world — no actor, no viewport.
USceneCaptureComponent2D* Capture =
NewObject<USceneCaptureComponent2D>(World->GetWorldSettings());
if (!Capture)
{
OutError = TEXT("failed to create SceneCaptureComponent2D");
return false;
}
Capture->TextureTarget = RT;
Capture->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
Capture->FOVAngle = FMath::Clamp(FOV, 5.f, 170.f);
Capture->bCaptureEveryFrame = false;
Capture->bCaptureOnMovement = false;
Capture->bAlwaysPersistRenderingState = true;
Capture->RegisterComponentWithWorld(World);
Capture->SetWorldLocationAndRotation(Location, Rotation);
Capture->CaptureScene();
FlushRenderingCommands();
FTextureRenderTargetResource* RTResource = RT->GameThread_GetRenderTargetResource();
bool bOk = false;
if (RTResource)
{
TArray<FColor> Pixels;
Pixels.SetNumUninitialized(W * H);
FReadSurfaceDataFlags ReadFlags(RCM_UNorm);
ReadFlags.SetLinearToGamma(false);
if (RTResource->ReadPixels(Pixels, ReadFlags))
{
// FColor is BGRA in memory.
bOk = SaveBGRAToPNG(
reinterpret_cast<const uint8*>(Pixels.GetData()),
Pixels.Num() * sizeof(FColor),
W, H, OutputAbsPath, FileNameNoExt, OutFullPath, OutError);
}
else
{
OutError = TEXT("ReadPixels failed");
}
}
else
{
OutError = TEXT("RT resource null after capture");
}
// Tear down the transient component.
Capture->UnregisterComponent();
Capture->DestroyComponent();
return bOk;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,338 @@
#include "MCPMaterialLibrary.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
namespace
{
FString JsonStringifyMaterial(const TSharedRef<FJsonObject>& Object)
{
FString Out;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
FJsonSerializer::Serialize(Object, Writer);
return Out;
}
FString ExpressionId(const UMaterialExpression* Expression)
{
if (!Expression)
{
return FString();
}
#if WITH_EDITORONLY_DATA
if (Expression->MaterialExpressionGuid.IsValid())
{
return Expression->MaterialExpressionGuid.ToString(EGuidFormats::DigitsWithHyphensInBraces);
}
#endif
return Expression->GetPathName();
}
FString OutputNameForIndex(UMaterialExpression* Expression, int32 OutputIndex)
{
if (!Expression || OutputIndex < 0)
{
return FString();
}
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
if (!Outputs.IsValidIndex(OutputIndex))
{
return FString();
}
return Outputs[OutputIndex].OutputName.ToString();
}
int32 ResolveOutputIndex(UMaterialExpression* Expression, const FString& OutputNameOrIndex)
{
if (!Expression)
{
return INDEX_NONE;
}
FString Needle = OutputNameOrIndex;
Needle.TrimStartAndEndInline();
if (Needle.IsEmpty() || Needle.Equals(TEXT("None"), ESearchCase::IgnoreCase))
{
return 0;
}
int32 NumericIndex = INDEX_NONE;
if (LexTryParseString(NumericIndex, *Needle))
{
return Expression->GetOutputs().IsValidIndex(NumericIndex) ? NumericIndex : INDEX_NONE;
}
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
for (int32 OutputIndex = 0; OutputIndex < Outputs.Num(); ++OutputIndex)
{
if (Outputs[OutputIndex].OutputName.ToString().Equals(Needle, ESearchCase::IgnoreCase))
{
return OutputIndex;
}
}
return Outputs.Num() == 1 ? 0 : INDEX_NONE;
}
int32 ResolveInputIndex(UMaterialExpression* Expression, const FString& InputNameOrIndex)
{
if (!Expression)
{
return INDEX_NONE;
}
FString Needle = InputNameOrIndex;
Needle.TrimStartAndEndInline();
if (Needle.IsEmpty() || Needle.Equals(TEXT("None"), ESearchCase::IgnoreCase))
{
return Expression->CountInputs() == 1 ? 0 : INDEX_NONE;
}
int32 NumericIndex = INDEX_NONE;
if (LexTryParseString(NumericIndex, *Needle))
{
return NumericIndex >= 0 && NumericIndex < Expression->CountInputs() ? NumericIndex : INDEX_NONE;
}
for (int32 InputIndex = 0; InputIndex < Expression->CountInputs(); ++InputIndex)
{
if (Expression->GetInputName(InputIndex).ToString().Equals(Needle, ESearchCase::IgnoreCase))
{
return InputIndex;
}
}
return INDEX_NONE;
}
TSharedRef<FJsonObject> MakeStatusJson(bool bOk, const FString& Error = FString())
{
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetBoolField(TEXT("ok"), bOk);
if (!Error.IsEmpty())
{
Root->SetStringField(TEXT("error"), Error);
}
return Root;
}
void AppendExpressionJson(UMaterialExpression* Expression, TArray<TSharedPtr<FJsonValue>>& Nodes, TArray<TSharedPtr<FJsonValue>>& Edges)
{
if (!Expression)
{
return;
}
TSharedRef<FJsonObject> Node = MakeShared<FJsonObject>();
Node->SetStringField(TEXT("guid"), ExpressionId(Expression));
Node->SetStringField(TEXT("name"), Expression->GetName());
Node->SetStringField(TEXT("class"), Expression->GetClass()->GetPathName());
#if WITH_EDITORONLY_DATA
Node->SetNumberField(TEXT("x"), Expression->MaterialExpressionEditorX);
Node->SetNumberField(TEXT("y"), Expression->MaterialExpressionEditorY);
if (!Expression->Desc.IsEmpty())
{
Node->SetStringField(TEXT("desc"), Expression->Desc);
}
#endif
TArray<TSharedPtr<FJsonValue>> Inputs;
const int32 InputCount = Expression->CountInputs();
for (int32 InputIndex = 0; InputIndex < InputCount; ++InputIndex)
{
const FExpressionInput* Input = Expression->GetInput(InputIndex);
const FName InputName = Expression->GetInputName(InputIndex);
TSharedRef<FJsonObject> InputJson = MakeShared<FJsonObject>();
InputJson->SetStringField(TEXT("name"), InputName.ToString());
InputJson->SetNumberField(TEXT("index"), InputIndex);
InputJson->SetBoolField(TEXT("connected"), Input && Input->Expression);
Inputs.Add(MakeShared<FJsonValueObject>(InputJson));
if (Input && Input->Expression)
{
TSharedRef<FJsonObject> Edge = MakeShared<FJsonObject>();
TArray<TSharedPtr<FJsonValue>> From;
From.Add(MakeShared<FJsonValueString>(ExpressionId(Input->Expression)));
From.Add(MakeShared<FJsonValueString>(OutputNameForIndex(Input->Expression, Input->OutputIndex)));
TArray<TSharedPtr<FJsonValue>> To;
To.Add(MakeShared<FJsonValueString>(ExpressionId(Expression)));
To.Add(MakeShared<FJsonValueString>(InputName.ToString()));
Edge->SetArrayField(TEXT("from"), From);
Edge->SetArrayField(TEXT("to"), To);
Edges.Add(MakeShared<FJsonValueObject>(Edge));
}
}
Node->SetArrayField(TEXT("inputs"), Inputs);
TArray<TSharedPtr<FJsonValue>> OutputsJson;
TArray<FExpressionOutput>& Outputs = Expression->GetOutputs();
for (int32 OutputIndex = 0; OutputIndex < Outputs.Num(); ++OutputIndex)
{
TSharedRef<FJsonObject> OutputJson = MakeShared<FJsonObject>();
OutputJson->SetStringField(TEXT("name"), Outputs[OutputIndex].OutputName.ToString());
OutputJson->SetNumberField(TEXT("index"), OutputIndex);
OutputsJson.Add(MakeShared<FJsonValueObject>(OutputJson));
}
Node->SetArrayField(TEXT("outputs"), OutputsJson);
Nodes.Add(MakeShared<FJsonValueObject>(Node));
}
}
TArray<UMaterialExpression*> UMCPMaterialLibrary::GetMaterialExpressions(UObject* MaterialOrFunction)
{
TArray<UMaterialExpression*> Out;
if (UMaterial* Material = Cast<UMaterial>(MaterialOrFunction))
{
for (TObjectPtr<UMaterialExpression> Expression : Material->GetExpressionCollection().Expressions)
{
if (Expression)
{
Out.Add(Expression.Get());
}
}
}
else if (UMaterialFunction* Function = Cast<UMaterialFunction>(MaterialOrFunction))
{
for (TObjectPtr<UMaterialExpression> Expression : Function->GetExpressionCollection().Expressions)
{
if (Expression)
{
Out.Add(Expression.Get());
}
}
}
return Out;
}
UMaterialExpression* UMCPMaterialLibrary::FindMaterialExpression(UObject* MaterialOrFunction, const FString& NodeId)
{
for (UMaterialExpression* Expression : GetMaterialExpressions(MaterialOrFunction))
{
if (!Expression)
{
continue;
}
if (ExpressionId(Expression) == NodeId || Expression->GetName() == NodeId || Expression->GetPathName() == NodeId)
{
return Expression;
}
}
return nullptr;
}
FString UMCPMaterialLibrary::DumpMaterialGraphToJson(UObject* MaterialOrFunction)
{
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
if (!MaterialOrFunction)
{
Root->SetBoolField(TEXT("ok"), false);
Root->SetStringField(TEXT("error"), TEXT("null material/function"));
return JsonStringifyMaterial(Root);
}
Root->SetBoolField(TEXT("ok"), true);
Root->SetStringField(TEXT("asset"), MaterialOrFunction->GetPathName());
Root->SetStringField(TEXT("class"), MaterialOrFunction->GetClass()->GetPathName());
TArray<TSharedPtr<FJsonValue>> Nodes;
TArray<TSharedPtr<FJsonValue>> Edges;
for (UMaterialExpression* Expression : GetMaterialExpressions(MaterialOrFunction))
{
AppendExpressionJson(Expression, Nodes, Edges);
}
Root->SetArrayField(TEXT("nodes"), Nodes);
Root->SetArrayField(TEXT("edges"), Edges);
if (UMaterial* Material = Cast<UMaterial>(MaterialOrFunction))
{
TSharedRef<FJsonObject> Outputs = MakeShared<FJsonObject>();
for (int32 PropertyIndex = 0; PropertyIndex < static_cast<int32>(MP_MAX); ++PropertyIndex)
{
const EMaterialProperty Property = static_cast<EMaterialProperty>(PropertyIndex);
FExpressionInput* Input = Material->GetExpressionInputForProperty(Property);
if (!Input || !Input->Expression)
{
continue;
}
TSharedRef<FJsonObject> Ref = MakeShared<FJsonObject>();
TArray<TSharedPtr<FJsonValue>> From;
From.Add(MakeShared<FJsonValueString>(ExpressionId(Input->Expression)));
From.Add(MakeShared<FJsonValueString>(OutputNameForIndex(Input->Expression, Input->OutputIndex)));
Ref->SetArrayField(TEXT("from"), From);
Outputs->SetObjectField(UEnum::GetValueAsString(Property), Ref);
}
Root->SetObjectField(TEXT("outputs"), Outputs);
}
return JsonStringifyMaterial(Root);
}
FString UMCPMaterialLibrary::ConnectMaterialExpressionsRaw(UObject* MaterialOrFunction, UMaterialExpression* FromExpression, const FString& FromOutput, UMaterialExpression* ToExpression, const FString& ToInput)
{
if (!MaterialOrFunction)
{
return JsonStringifyMaterial(MakeStatusJson(false, TEXT("null material/function")));
}
if (!FromExpression)
{
return JsonStringifyMaterial(MakeStatusJson(false, TEXT("null source expression")));
}
if (!ToExpression)
{
return JsonStringifyMaterial(MakeStatusJson(false, TEXT("null target expression")));
}
const int32 OutputIndex = ResolveOutputIndex(FromExpression, FromOutput);
if (OutputIndex == INDEX_NONE)
{
return JsonStringifyMaterial(MakeStatusJson(false, FString::Printf(TEXT("source output not found: %s.%s"), *FromExpression->GetName(), *FromOutput)));
}
const int32 InputIndex = ResolveInputIndex(ToExpression, ToInput);
if (InputIndex == INDEX_NONE)
{
return JsonStringifyMaterial(MakeStatusJson(false, FString::Printf(TEXT("target input not found: %s.%s"), *ToExpression->GetName(), *ToInput)));
}
FExpressionInput* Input = ToExpression->GetInput(InputIndex);
if (!Input)
{
return JsonStringifyMaterial(MakeStatusJson(false, FString::Printf(TEXT("target input is null: %s.%s"), *ToExpression->GetName(), *ToInput)));
}
Input->Expression = FromExpression;
Input->OutputIndex = OutputIndex;
if (UMaterial* Material = Cast<UMaterial>(MaterialOrFunction))
{
Material->PreEditChange(nullptr);
Material->PostEditChange();
Material->MarkPackageDirty();
}
else if (UMaterialFunction* Function = Cast<UMaterialFunction>(MaterialOrFunction))
{
Function->PreEditChange(nullptr);
Function->PostEditChange();
Function->MarkPackageDirty();
}
TSharedRef<FJsonObject> Root = MakeStatusJson(true);
Root->SetStringField(TEXT("from"), FromExpression->GetName());
Root->SetStringField(TEXT("from_output"), OutputNameForIndex(FromExpression, OutputIndex));
Root->SetStringField(TEXT("to"), ToExpression->GetName());
Root->SetStringField(TEXT("to_input"), ToExpression->GetInputName(InputIndex).ToString());
return JsonStringifyMaterial(Root);
}

View File

@ -0,0 +1,275 @@
#include "MCPNiagaraLibrary.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "AssetRegistry/AssetData.h"
#include "Modules/ModuleManager.h"
#include "UObject/Package.h"
#include "UObject/UObjectGlobals.h"
#include "NiagaraSystem.h"
#include "NiagaraSystemFactoryNew.h"
#include "NiagaraEmitter.h"
#include "NiagaraEmitterHandle.h"
#include "NiagaraScript.h"
#include "NiagaraRendererProperties.h"
#include "NiagaraScriptSource.h"
#include "NiagaraGraph.h"
#include "NiagaraNodeOutput.h"
#include "NiagaraNodeFunctionCall.h"
#include "NiagaraCommon.h"
#include "ViewModels/Stack/NiagaraStackGraphUtilities.h"
#include "NiagaraEditorUtilities.h"
namespace
{
struct FStackLocation
{
UNiagaraNodeOutput* OutputNode = nullptr;
bool bValid() const { return OutputNode != nullptr; }
};
ENiagaraScriptUsage ResolveUsage(const FString& GroupName, bool& bOutIsSystem)
{
bOutIsSystem = false;
const FString G = GroupName.TrimStartAndEnd();
if (G.Equals(TEXT("EmitterSpawn"), ESearchCase::IgnoreCase)) return ENiagaraScriptUsage::EmitterSpawnScript;
if (G.Equals(TEXT("EmitterUpdate"), ESearchCase::IgnoreCase)) return ENiagaraScriptUsage::EmitterUpdateScript;
if (G.Equals(TEXT("ParticleSpawn"), ESearchCase::IgnoreCase)) return ENiagaraScriptUsage::ParticleSpawnScript;
if (G.Equals(TEXT("ParticleUpdate"), ESearchCase::IgnoreCase)) return ENiagaraScriptUsage::ParticleUpdateScript;
if (G.Equals(TEXT("SystemSpawn"), ESearchCase::IgnoreCase)) { bOutIsSystem = true; return ENiagaraScriptUsage::SystemSpawnScript; }
if (G.Equals(TEXT("SystemUpdate"), ESearchCase::IgnoreCase)) { bOutIsSystem = true; return ENiagaraScriptUsage::SystemUpdateScript; }
return ENiagaraScriptUsage::ParticleSpawnScript;
}
UNiagaraNodeOutput* FindOutputNodeFromScript(UNiagaraScript* Script, ENiagaraScriptUsage Usage)
{
if (!Script) return nullptr;
UNiagaraScriptSourceBase* SourceBase = Script->GetLatestSource();
UNiagaraScriptSource* Source = Cast<UNiagaraScriptSource>(SourceBase);
if (!Source || !Source->NodeGraph) return nullptr;
// FindOutputNode is not NIAGARAEDITOR_API; FindEquivalentOutputNode is.
return Source->NodeGraph->FindEquivalentOutputNode(Usage, FGuid());
}
void GatherFunctionCallNodes(UNiagaraNodeOutput* OutputNode, TArray<UNiagaraNodeFunctionCall*>& Out)
{
if (!OutputNode) return;
UEdGraph* Graph = OutputNode->GetGraph();
if (!Graph) return;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (UNiagaraNodeFunctionCall* FC = Cast<UNiagaraNodeFunctionCall>(Node))
{
// Filter to modules attached to this output's usage. GetCalledUsage()
// returns the usage of the inner module script (usually Module).
// Without param-map walking we list all function-calls in the graph
// tied to the same output usage — good enough for list/remove.
Out.Add(FC);
}
}
}
FNiagaraEmitterHandle* FindEmitterHandleMutable(UNiagaraSystem* System, const FString& EmitterName)
{
if (!System) return nullptr;
TArray<FNiagaraEmitterHandle>& Handles = System->GetEmitterHandles();
for (FNiagaraEmitterHandle& H : Handles)
{
if (H.GetName().ToString().Equals(EmitterName, ESearchCase::IgnoreCase))
{
return &H;
}
}
return nullptr;
}
FStackLocation LocateStack(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName)
{
FStackLocation Loc;
bool bIsSystem = false;
const ENiagaraScriptUsage Usage = ResolveUsage(GroupName, bIsSystem);
if (!System) return Loc;
if (bIsSystem)
{
UNiagaraScript* Script = (Usage == ENiagaraScriptUsage::SystemSpawnScript)
? System->GetSystemSpawnScript()
: System->GetSystemUpdateScript();
Loc.OutputNode = FindOutputNodeFromScript(Script, Usage);
return Loc;
}
FNiagaraEmitterHandle* Handle = FindEmitterHandleMutable(System, EmitterName);
if (!Handle) return Loc;
FVersionedNiagaraEmitter Versioned = Handle->GetInstance();
FVersionedNiagaraEmitterData* Data = Versioned.GetEmitterData();
if (!Data) return Loc;
UNiagaraScript* Script = Data->GetScript(Usage, FGuid());
Loc.OutputNode = FindOutputNodeFromScript(Script, Usage);
return Loc;
}
}
bool UMCPNiagaraLibrary::CreateNiagaraSystem(const FString& PackagePath)
{
if (PackagePath.IsEmpty()) return false;
FString PackageName, AssetName;
if (!PackagePath.Split(TEXT("/"), &PackageName, &AssetName, ESearchCase::IgnoreCase, ESearchDir::FromEnd))
{
return false;
}
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
UNiagaraSystemFactoryNew* Factory = NewObject<UNiagaraSystemFactoryNew>();
UObject* Created = AssetTools.CreateAsset(AssetName, PackageName, UNiagaraSystem::StaticClass(), Factory);
return Created != nullptr;
}
TArray<FString> UMCPNiagaraLibrary::GetEmitterHandleNames(UNiagaraSystem* System)
{
TArray<FString> Out;
if (!System) return Out;
for (const FNiagaraEmitterHandle& H : System->GetEmitterHandles())
{
Out.Add(H.GetName().ToString());
}
return Out;
}
FString UMCPNiagaraLibrary::AddEmitterToSystem(UNiagaraSystem* System, UNiagaraEmitter* SourceEmitter, const FString& EmitterName)
{
if (!System || !SourceEmitter) return FString();
System->Modify();
// Use the editor-utility path that performs full setup (merge, renderer
// initialization, parameter refresh). The raw UNiagaraSystem::AddEmitterHandle
// skips most of this and leaves the emitter non-functional in preview.
const FGuid HandleId = FNiagaraEditorUtilities::AddEmitterToSystem(
*System, *SourceEmitter, FGuid(), /*bCreateCopy=*/true);
if (!HandleId.IsValid()) return FString();
// Find the handle we just added by id and (optionally) rename it.
for (FNiagaraEmitterHandle& H : System->GetEmitterHandles())
{
if (H.GetId() == HandleId)
{
if (!EmitterName.IsEmpty())
{
H.SetName(FName(*EmitterName), *System);
}
return H.GetName().ToString();
}
}
return FString();
}
bool UMCPNiagaraLibrary::RemoveEmitterFromSystem(UNiagaraSystem* System, const FString& EmitterName)
{
if (!System) return false;
FNiagaraEmitterHandle* Handle = FindEmitterHandleMutable(System, EmitterName);
if (!Handle) return false;
System->Modify();
System->RemoveEmitterHandle(*Handle);
return true;
}
TArray<FString> UMCPNiagaraLibrary::ListModules(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName)
{
TArray<FString> Out;
const FStackLocation Loc = LocateStack(System, EmitterName, GroupName);
if (!Loc.bValid()) return Out;
TArray<UNiagaraNodeFunctionCall*> ModuleNodes;
GatherFunctionCallNodes(Loc.OutputNode, ModuleNodes);
for (UNiagaraNodeFunctionCall* N : ModuleNodes)
{
if (N) Out.Add(N->GetFunctionName());
}
return Out;
}
FString UMCPNiagaraLibrary::AddModuleToStack(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName, UNiagaraScript* ModuleScript, int32 Index)
{
if (!ModuleScript) return FString();
const FStackLocation Loc = LocateStack(System, EmitterName, GroupName);
if (!Loc.bValid()) return FString();
System->Modify();
UNiagaraNodeFunctionCall* Added = FNiagaraStackGraphUtilities::AddScriptModuleToStack(
ModuleScript, *Loc.OutputNode, Index, FString(), FGuid());
if (!Added) return FString();
return Added->GetFunctionName();
}
bool UMCPNiagaraLibrary::RemoveModuleFromStack(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName, const FString& ModuleName)
{
const FStackLocation Loc = LocateStack(System, EmitterName, GroupName);
if (!Loc.bValid()) return false;
TArray<UNiagaraNodeFunctionCall*> ModuleNodes;
GatherFunctionCallNodes(Loc.OutputNode, ModuleNodes);
for (UNiagaraNodeFunctionCall* N : ModuleNodes)
{
if (N && N->GetFunctionName().Equals(ModuleName, ESearchCase::IgnoreCase))
{
System->Modify();
UEdGraph* Graph = N->GetGraph();
if (Graph)
{
Graph->Modify();
N->BreakAllNodeLinks();
Graph->RemoveNode(N);
}
return true;
}
}
return false;
}
FString UMCPNiagaraLibrary::AddRendererToEmitter(UNiagaraSystem* System, const FString& EmitterName, UClass* RendererClass)
{
if (!RendererClass || !RendererClass->IsChildOf(UNiagaraRendererProperties::StaticClass())) return FString();
FNiagaraEmitterHandle* Handle = FindEmitterHandleMutable(System, EmitterName);
if (!Handle) return FString();
FVersionedNiagaraEmitter Versioned = Handle->GetInstance();
UNiagaraEmitter* Emitter = Versioned.Emitter;
if (!Emitter) return FString();
Emitter->Modify();
UNiagaraRendererProperties* Renderer = NewObject<UNiagaraRendererProperties>(Emitter, RendererClass, NAME_None, RF_Transactional);
Emitter->AddRenderer(Renderer, Versioned.Version);
return RendererClass->GetName();
}
int32 UMCPNiagaraLibrary::RemoveRenderersFromEmitter(UNiagaraSystem* System, const FString& EmitterName, UClass* RendererClass)
{
if (!RendererClass) return 0;
FNiagaraEmitterHandle* Handle = FindEmitterHandleMutable(System, EmitterName);
if (!Handle) return 0;
FVersionedNiagaraEmitter Versioned = Handle->GetInstance();
UNiagaraEmitter* Emitter = Versioned.Emitter;
FVersionedNiagaraEmitterData* Data = Versioned.GetEmitterData();
if (!Emitter || !Data) return 0;
Emitter->Modify();
TArray<UNiagaraRendererProperties*> ToRemove;
for (UNiagaraRendererProperties* R : Data->GetRenderers())
{
if (R && R->IsA(RendererClass)) ToRemove.Add(R);
}
for (UNiagaraRendererProperties* R : ToRemove)
{
Emitter->RemoveRenderer(R, Versioned.Version);
}
return ToRemove.Num();
}
bool UMCPNiagaraLibrary::RequestCompile(UNiagaraSystem* System)
{
if (!System) return false;
return System->RequestCompile(true);
}

View File

@ -0,0 +1,948 @@
#include "MCPVoxelGraphLibrary.h"
#include "MCPGraphLibrary.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphNode_Comment.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Editor.h"
#include "Dom/JsonObject.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
// WITH_MCP_VOXEL is set by UEBlueprintMCPEditor.Build.cs: 1 when the Voxel plugin
// is present (auto-detected, or forced via env UE_MCP_WITH_VOXEL), 0 otherwise.
// When 0 the whole Voxel integration compiles down to harmless stubs so the
// plugin builds in any project — with or without Voxel installed.
#ifndef WITH_MCP_VOXEL
#define WITH_MCP_VOXEL 0
#endif
#if WITH_MCP_VOXEL
#include "VoxelGraph.h"
#include "VoxelGraphToolkit.h"
#include "VoxelGraphTracker.h"
#include "VoxelTerminalGraph.h"
#include "VoxelTerminalGraphRuntime.h"
#include "VoxelMinimal.h"
#include "VoxelNode.h"
#include "VoxelFunctionLibrary.h"
#include "Nodes/VoxelNode_UFunction.h"
#include "VoxelMinimal/VoxelInstancedStruct.h"
#include "VoxelMinimal/VoxelObjectHelpers.h"
#include "VoxelParameter.h"
#include "VoxelPinType.h"
// Private headers of the VoxelGraphEditor module (reachable via PrivateIncludePaths in Build.cs).
// We only use inline/virtual members from these, so no cross-module link symbols are required.
#include "Nodes/VoxelGraphNode_Struct.h"
namespace
{
static FString VoxelJsonStringify(const TSharedRef<FJsonObject>& Object)
{
FString Out;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Out);
FJsonSerializer::Serialize(Object, Writer);
return Out;
}
static UVoxelGraph* AsVoxelGraph(UObject* Asset)
{
return Cast<UVoxelGraph>(Asset);
}
static void FillTerminalJson(TSharedRef<FJsonObject> Json, const UVoxelTerminalGraph& TerminalGraph)
{
Json->SetStringField(TEXT("guid"), TerminalGraph.GetGuid().ToString());
Json->SetStringField(TEXT("name"), TerminalGraph.GetDisplayName());
Json->SetBoolField(TEXT("is_main"), TerminalGraph.IsMainTerminalGraph());
Json->SetBoolField(TEXT("is_editor"), TerminalGraph.IsEditorTerminalGraph());
const UEdGraph& EdGraph = TerminalGraph.GetEdGraph();
Json->SetStringField(TEXT("ed_graph"), EdGraph.GetName());
Json->SetNumberField(TEXT("node_count"), EdGraph.Nodes.Num());
}
static UVoxelTerminalGraph* FindTerminalGraph(UVoxelGraph* Graph, const FString& Terminal)
{
if (!Graph)
{
return nullptr;
}
if (Terminal.IsEmpty() ||
Terminal.Equals(TEXT("main"), ESearchCase::IgnoreCase) ||
Terminal.Equals(TEXT("Main"), ESearchCase::IgnoreCase))
{
return &Graph->GetMainTerminalGraph();
}
if (Terminal.Equals(TEXT("editor"), ESearchCase::IgnoreCase))
{
return &Graph->GetEditorTerminalGraph();
}
FGuid Guid;
if (FGuid::Parse(Terminal, Guid))
{
return Graph->FindTerminalGraph(Guid);
}
UVoxelTerminalGraph* Result = nullptr;
Graph->ForeachTerminalGraph_NoInheritance([&](UVoxelTerminalGraph& Candidate)
{
if (!Result &&
(Candidate.GetDisplayName().Equals(Terminal, ESearchCase::IgnoreCase) ||
Candidate.GetEdGraph().GetName().Equals(Terminal, ESearchCase::IgnoreCase)))
{
Result = &Candidate;
}
});
return Result;
}
static TSharedRef<FJsonObject> VoxelNodeToJson(UEdGraphNode* Node)
{
TSharedRef<FJsonObject> Json = MakeShared<FJsonObject>();
Json->SetStringField(TEXT("guid"), Node->NodeGuid.ToString());
Json->SetStringField(TEXT("class"), Node->GetClass()->GetName());
Json->SetNumberField(TEXT("x"), Node->NodePosX);
Json->SetNumberField(TEXT("y"), Node->NodePosY);
Json->SetStringField(TEXT("title"), Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
if (!Node->NodeComment.IsEmpty())
{
Json->SetStringField(TEXT("comment"), Node->NodeComment);
}
if (const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node))
{
Json->SetNumberField(TEXT("w"), CommentNode->NodeWidth);
Json->SetNumberField(TEXT("h"), CommentNode->NodeHeight);
}
TArray<TSharedPtr<FJsonValue>> Pins;
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin)
{
continue;
}
TSharedRef<FJsonObject> PinJson = MakeShared<FJsonObject>();
PinJson->SetStringField(TEXT("name"), Pin->PinName.ToString());
PinJson->SetStringField(TEXT("dir"), Pin->Direction == EGPD_Input ? TEXT("in") : TEXT("out"));
PinJson->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
if (!Pin->PinType.PinSubCategory.IsNone())
{
PinJson->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategory.ToString());
}
if (Pin->PinType.PinSubCategoryObject.IsValid())
{
PinJson->SetStringField(TEXT("subobj"), Pin->PinType.PinSubCategoryObject->GetPathName());
}
if (!Pin->DefaultValue.IsEmpty())
{
PinJson->SetStringField(TEXT("default"), Pin->DefaultValue);
}
TArray<TSharedPtr<FJsonValue>> Links;
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
{
if (!LinkedPin || !LinkedPin->GetOwningNode())
{
continue;
}
TSharedRef<FJsonObject> LinkJson = MakeShared<FJsonObject>();
LinkJson->SetStringField(TEXT("node"), LinkedPin->GetOwningNode()->NodeGuid.ToString());
LinkJson->SetStringField(TEXT("pin"), LinkedPin->PinName.ToString());
Links.Add(MakeShared<FJsonValueObject>(LinkJson));
}
if (Links.Num() > 0)
{
PinJson->SetArrayField(TEXT("links"), Links);
}
Pins.Add(MakeShared<FJsonValueObject>(PinJson));
}
Json->SetArrayField(TEXT("pins"), Pins);
return Json;
}
// Lazily-built catalogue of every spawnable Voxel node. Mirrors FVoxelNodeLibrary's
// construction (which lives in VoxelGraphEditor/Private and is not exported), so we
// rebuild it here from the exported reflection helpers.
static const TArray<TSharedRef<const FVoxelNode>>& GetVoxelNodeCatalogue()
{
static TArray<TSharedRef<const FVoxelNode>> Nodes;
static bool bBuilt = false;
if (bBuilt)
{
return Nodes;
}
bBuilt = true;
// Struct nodes (FVoxelNode subclasses): Add, Multiply, noise, output nodes, etc.
for (UScriptStruct* Struct : GetDerivedStructs<FVoxelNode>())
{
if (!Struct)
{
continue;
}
if (Struct->HasMetaData(TEXT("Abstract")) || Struct->HasMetaData(TEXT("Internal")))
{
continue;
}
Nodes.Add(MakeSharedStruct<FVoxelNode>(Struct));
}
// Function-library nodes (UVoxelFunctionLibrary UFUNCTIONs wrapped as FVoxelNode_UFunction):
// most math / position / curve nodes live here.
for (const TSubclassOf<UVoxelFunctionLibrary>& Class : GetDerivedClasses<UVoxelFunctionLibrary>())
{
if (!Class)
{
continue;
}
for (UFunction* Function : GetClassFunctions(Class))
{
if (!Function || Function->HasMetaData(TEXT("Internal")))
{
continue;
}
Nodes.Add(FVoxelNode_UFunction::Make(Function));
}
}
return Nodes;
}
// The UFunction backing a function-library node, or nullptr for plain struct nodes.
static UFunction* GetNodeFunction(const FVoxelNode& Node)
{
if (Node.GetStruct() &&
Node.GetStruct()->IsChildOf(FVoxelNode_UFunction::StaticStruct()))
{
return static_cast<const FVoxelNode_UFunction&>(Node).GetFunction();
}
return nullptr;
}
// Primary, stable identifier used to spawn a node.
static FString GetNodeId(const FVoxelNode& Node)
{
if (const UFunction* Function = GetNodeFunction(Node))
{
const UClass* Owner = Function->GetOwnerClass();
return FString::Printf(TEXT("%s.%s"),
Owner ? *Owner->GetName() : TEXT("?"),
*Function->GetName());
}
return Node.GetStruct() ? Node.GetStruct()->GetName() : FString();
}
// Returns true if NodeId matches the node by any of its accepted aliases.
static bool NodeMatchesId(const FVoxelNode& Node, const FString& NodeId)
{
if (GetNodeId(Node).Equals(NodeId, ESearchCase::IgnoreCase))
{
return true;
}
if (Node.GetDisplayName().Equals(NodeId, ESearchCase::IgnoreCase))
{
return true;
}
if (const UFunction* Function = GetNodeFunction(Node))
{
if (Function->GetName().Equals(NodeId, ESearchCase::IgnoreCase))
{
return true;
}
const UClass* Owner = Function->GetOwnerClass();
if (Owner &&
FString::Printf(TEXT("%s::%s"), *Owner->GetName(), *Function->GetName()).Equals(NodeId, ESearchCase::IgnoreCase))
{
return true;
}
}
else if (const UScriptStruct* Struct = Node.GetStruct())
{
if (Struct->GetPathName().Equals(NodeId, ESearchCase::IgnoreCase))
{
return true;
}
}
return false;
}
static TSharedPtr<const FVoxelNode> FindCatalogueNode(const FString& NodeId)
{
for (const TSharedRef<const FVoxelNode>& Node : GetVoxelNodeCatalogue())
{
if (NodeMatchesId(*Node, NodeId))
{
return Node;
}
}
return nullptr;
}
// Map a friendly string to a Voxel pin type. Defaults to float for unknown input.
static FVoxelPinType PinTypeFromString(const FString& TypeStr)
{
const FString T = TypeStr.ToLower();
if (T == TEXT("double")) return FVoxelPinType::Make<double>();
if (T == TEXT("int") || T == TEXT("int32")) return FVoxelPinType::Make<int32>();
if (T == TEXT("bool")) return FVoxelPinType::Make<bool>();
if (T == TEXT("vector2d") || T == TEXT("vector2"))return FVoxelPinType::Make<FVector2D>();
if (T == TEXT("vector") || T == TEXT("vector3d")) return FVoxelPinType::Make<FVector>();
if (T == TEXT("color") || T == TEXT("linearcolor"))return FVoxelPinType::Make<FLinearColor>();
if (T == TEXT("name")) return FVoxelPinType::Make<FName>();
return FVoxelPinType::Make<float>();
}
// Reflection-based spawn of a (private, non-exported) Voxel graph node class by its script path,
// setting its FGuid "Guid" UPROPERTY. Only inline/virtual calls -> no cross-module link symbols.
static UEdGraphNode* SpawnGuidNodeReflect(UEdGraph* Graph, const TCHAR* ClassPath, const FGuid& Guid, FVector2D Position)
{
UClass* NodeClass = FindObject<UClass>(nullptr, ClassPath);
if (!Graph || !NodeClass)
{
return nullptr;
}
Graph->Modify();
UEdGraphNode* Node = NewObject<UEdGraphNode>(Graph, NodeClass, NAME_None, RF_Transactional);
if (!Node)
{
return nullptr;
}
if (FStructProperty* GuidProp = FindFProperty<FStructProperty>(NodeClass, TEXT("Guid")))
{
*GuidProp->ContainerPtrToValuePtr<FGuid>(Node) = Guid;
}
Node->NodePosX = static_cast<int32>(Position.X);
Node->NodePosY = static_cast<int32>(Position.Y);
Node->CreateNewGuid();
Graph->AddNode(Node, true, false);
Node->PostPlacedNewNode();
if (Node->Pins.Num() == 0)
{
Node->AllocateDefaultPins();
}
return Node;
}
static UVoxelTerminalGraph* OwningTerminal(UEdGraph* Graph)
{
return Graph ? Graph->GetTypedOuter<UVoxelTerminalGraph>() : nullptr;
}
static FGuid ResolveParameterGuid(UVoxelGraph* Graph, const FString& Ref)
{
if (!Graph)
{
return FGuid();
}
FGuid Guid;
if (FGuid::Parse(Ref, Guid) && Graph->FindParameter(Guid))
{
return Guid;
}
for (const FGuid& Candidate : Graph->GetParameters())
{
if (Graph->FindParameterChecked(Candidate).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase))
{
return Candidate;
}
}
return FGuid();
}
// Resolve a terminal-graph property (kind: input/output/local) by guid or name.
static FGuid ResolveTerminalPropGuid(UVoxelTerminalGraph* Terminal, const FString& Kind, const FString& Ref)
{
if (!Terminal)
{
return FGuid();
}
FGuid Guid;
const bool bIsGuid = FGuid::Parse(Ref, Guid);
if (Kind == TEXT("input"))
{
if (bIsGuid && Terminal->FindInput(Guid)) return Guid;
for (const FGuid& G : Terminal->GetFunctionInputs())
if (Terminal->FindInputChecked(G).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase)) return G;
}
else if (Kind == TEXT("output"))
{
if (bIsGuid && Terminal->FindOutput(Guid)) return Guid;
for (const FGuid& G : Terminal->GetFunctionOutputs())
if (Terminal->FindOutputChecked(G).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase)) return G;
}
else if (Kind == TEXT("local"))
{
if (bIsGuid && Terminal->FindLocalVariable(Guid)) return Guid;
for (const FGuid& G : Terminal->GetLocalVariables())
if (Terminal->FindLocalVariableChecked(G).Name.ToString().Equals(Ref, ESearchCase::IgnoreCase)) return G;
}
return FGuid();
}
static TSharedRef<FJsonObject> JsonError(const FString& Message)
{
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetBoolField(TEXT("ok"), false);
Root->SetStringField(TEXT("error"), Message);
return Root;
}
}
bool UMCPVoxelGraphLibrary::IsVoxelGraphAsset(UObject* Asset)
{
return AsVoxelGraph(Asset) != nullptr;
}
FString UMCPVoxelGraphLibrary::ListTerminalGraphsJson(UObject* Asset)
{
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
UVoxelGraph* Graph = AsVoxelGraph(Asset);
if (!Graph)
{
Root->SetBoolField(TEXT("ok"), false);
Root->SetStringField(TEXT("error"), TEXT("asset is not a UVoxelGraph"));
return VoxelJsonStringify(Root);
}
Root->SetBoolField(TEXT("ok"), true);
Root->SetStringField(TEXT("asset"), Graph->GetPathName());
Root->SetStringField(TEXT("class"), Graph->GetClass()->GetPathName());
TArray<TSharedPtr<FJsonValue>> Terminals;
Graph->ForeachTerminalGraph_NoInheritance([&](UVoxelTerminalGraph& TerminalGraph)
{
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
FillTerminalJson(Item, TerminalGraph);
Terminals.Add(MakeShared<FJsonValueObject>(Item));
});
Root->SetArrayField(TEXT("terminals"), Terminals);
Root->SetNumberField(TEXT("terminal_count"), Terminals.Num());
return VoxelJsonStringify(Root);
}
UEdGraph* UMCPVoxelGraphLibrary::FindTerminalEdGraph(UObject* Asset, const FString& Terminal)
{
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(AsVoxelGraph(Asset), Terminal);
return TerminalGraph ? &TerminalGraph->GetEdGraph() : nullptr;
}
FString UMCPVoxelGraphLibrary::DumpTerminalGraphJson(UObject* Asset, const FString& Terminal)
{
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
UVoxelGraph* Graph = AsVoxelGraph(Asset);
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(Graph, Terminal);
if (!Graph || !TerminalGraph)
{
Root->SetBoolField(TEXT("ok"), false);
Root->SetStringField(TEXT("error"), Graph ? TEXT("terminal graph not found") : TEXT("asset is not a UVoxelGraph"));
return VoxelJsonStringify(Root);
}
Root->SetBoolField(TEXT("ok"), true);
Root->SetStringField(TEXT("asset"), Graph->GetPathName());
TSharedRef<FJsonObject> TerminalJson = MakeShared<FJsonObject>();
FillTerminalJson(TerminalJson, *TerminalGraph);
Root->SetObjectField(TEXT("terminal"), TerminalJson);
TArray<TSharedPtr<FJsonValue>> Nodes;
UEdGraph& EdGraph = TerminalGraph->GetEdGraph();
for (UEdGraphNode* Node : EdGraph.Nodes)
{
if (Node)
{
Nodes.Add(MakeShared<FJsonValueObject>(VoxelNodeToJson(Node)));
}
}
Root->SetArrayField(TEXT("nodes"), Nodes);
return VoxelJsonStringify(Root);
}
FString UMCPVoxelGraphLibrary::GetSelectedNodesJson(UObject* Asset, const FString& Terminal)
{
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
UEdGraph* EdGraph = FindTerminalEdGraph(Asset, Terminal);
if (!EdGraph)
{
Root->SetBoolField(TEXT("ok"), false);
Root->SetStringField(TEXT("error"), TEXT("terminal graph not found"));
return VoxelJsonStringify(Root);
}
const TSharedPtr<FVoxelGraphToolkit> Toolkit = FVoxelGraphToolkit::Get(EdGraph);
if (!Toolkit.IsValid())
{
Root->SetBoolField(TEXT("ok"), false);
Root->SetStringField(TEXT("error"), TEXT("voxel graph editor is not open"));
return VoxelJsonStringify(Root);
}
Root->SetBoolField(TEXT("ok"), true);
Root->SetStringField(TEXT("graph"), EdGraph->GetName());
TArray<TSharedPtr<FJsonValue>> Nodes;
for (UEdGraphNode* Node : Toolkit->GetSelectedNodes())
{
if (Node)
{
Nodes.Add(MakeShared<FJsonValueObject>(VoxelNodeToJson(Node)));
}
}
Root->SetArrayField(TEXT("selected"), Nodes);
Root->SetNumberField(TEXT("selected_count"), Nodes.Num());
return VoxelJsonStringify(Root);
}
FString UMCPVoxelGraphLibrary::SpawnCommentNode(UEdGraph* Graph, FVector2D Position, FVector2D Size, const FString& Comment, FLinearColor Color)
{
if (!Graph)
{
return FString();
}
Graph->Modify();
UEdGraphNode_Comment* Node = NewObject<UEdGraphNode_Comment>(Graph);
Node->CreateNewGuid();
Node->NodePosX = static_cast<int32>(Position.X);
Node->NodePosY = static_cast<int32>(Position.Y);
Node->NodeWidth = FMath::Max(64, static_cast<int32>(Size.X));
Node->NodeHeight = FMath::Max(64, static_cast<int32>(Size.Y));
Node->NodeComment = Comment;
Node->CommentColor = Color;
Node->bCommentBubbleVisible = false;
Graph->AddNode(Node, true, false);
Node->PostPlacedNewNode();
NotifyGraphChanged(Graph, false);
return Node->NodeGuid.ToString();
}
FString UMCPVoxelGraphLibrary::ListNodeTypesJson(const FString& Filter, const int32 Limit)
{
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetBoolField(TEXT("ok"), true);
const FString FilterLower = Filter.ToLower();
const int32 EffectiveLimit = Limit > 0 ? Limit : MAX_int32;
TArray<TSharedPtr<FJsonValue>> Items;
int32 Total = 0;
for (const TSharedRef<const FVoxelNode>& Node : GetVoxelNodeCatalogue())
{
const FString Id = GetNodeId(*Node);
const FString DisplayName = Node->GetDisplayName();
const FString Category = Node->GetCategory();
if (!FilterLower.IsEmpty())
{
const bool bMatches =
Id.ToLower().Contains(FilterLower) ||
DisplayName.ToLower().Contains(FilterLower) ||
Category.ToLower().Contains(FilterLower);
if (!bMatches)
{
continue;
}
}
++Total;
if (Items.Num() >= EffectiveLimit)
{
continue;
}
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
Item->SetStringField(TEXT("id"), Id);
Item->SetStringField(TEXT("display_name"), DisplayName);
Item->SetStringField(TEXT("category"), Category);
Item->SetBoolField(TEXT("is_pure"), Node->IsPureNode());
Item->SetStringField(TEXT("kind"), GetNodeFunction(*Node) ? TEXT("function") : TEXT("struct"));
Items.Add(MakeShared<FJsonValueObject>(Item));
}
Root->SetArrayField(TEXT("nodes"), Items);
Root->SetNumberField(TEXT("returned"), Items.Num());
Root->SetNumberField(TEXT("total"), Total);
Root->SetBoolField(TEXT("truncated"), Total > Items.Num());
return VoxelJsonStringify(Root);
}
FString UMCPVoxelGraphLibrary::SpawnStructNode(UEdGraph* Graph, const FString& NodeId, FVector2D Position)
{
if (!Graph)
{
return FString();
}
const TSharedPtr<const FVoxelNode> Chosen = FindCatalogueNode(NodeId);
if (!Chosen)
{
return FString();
}
UClass* NodeClass = FindObject<UClass>(nullptr, TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_Struct"));
if (!NodeClass)
{
return FString();
}
Graph->Modify();
// NewObject with the reflected class avoids referencing the (non-exported) StaticClass()
// symbol of UVoxelGraphNode_Struct; the static_cast is purely compile-time.
UVoxelGraphNode_Struct* Node = static_cast<UVoxelGraphNode_Struct*>(
NewObject<UEdGraphNode>(Graph, NodeClass, NAME_None, RF_Transactional));
if (!Node)
{
return FString();
}
// Mirrors FVoxelGraphSchemaAction_NewStructNode::PerformAction: copy the catalogue
// node into the graph node's instanced Struct, then let it build its pins.
Node->Struct = FVoxelInstancedStruct::Make(*Chosen);
Node->NodePosX = static_cast<int32>(Position.X);
Node->NodePosY = static_cast<int32>(Position.Y);
Node->CreateNewGuid();
Graph->AddNode(Node, true, false);
Node->PostPlacedNewNode();
if (Node->Pins.Num() == 0)
{
Node->AllocateDefaultPins();
}
return Node->NodeGuid.ToString();
}
FString UMCPVoxelGraphLibrary::AddGraphPropertyJson(UObject* Asset, const FString& Terminal, const FString& Kind, const FString& Name, const FString& TypeStr, const FString& Category)
{
UVoxelGraph* Graph = AsVoxelGraph(Asset);
if (!Graph)
{
return VoxelJsonStringify(JsonError(TEXT("asset is not a UVoxelGraph")));
}
const FString KindLower = Kind.ToLower();
const FVoxelPinType Type = PinTypeFromString(TypeStr);
const FName PropName = Name.IsEmpty() ? FName(TEXT("NewProperty")) : FName(*Name);
const FGuid Guid = FGuid::NewGuid();
if (KindLower == TEXT("parameter"))
{
FVoxelParameter Parameter;
Parameter.Name = PropName;
Parameter.Type = Type;
#if WITH_EDITORONLY_DATA
Parameter.Category = Category;
#endif
Parameter.Fixup();
Graph->AddParameter(Guid, Parameter);
Graph->Fixup();
}
else
{
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(Graph, Terminal);
if (!TerminalGraph)
{
return VoxelJsonStringify(JsonError(TEXT("terminal graph not found")));
}
if (KindLower == TEXT("input"))
{
FVoxelGraphFunctionInput Input;
Input.Name = PropName;
Input.Type = Type;
#if WITH_EDITORONLY_DATA
Input.Category = Category;
#endif
Input.Fixup();
TerminalGraph->AddFunctionInput(Guid, Input);
}
else if (KindLower == TEXT("output"))
{
FVoxelGraphFunctionOutput Output;
Output.Name = PropName;
Output.Type = Type;
#if WITH_EDITORONLY_DATA
Output.Category = Category;
#endif
TerminalGraph->AddFunctionOutput(Guid, Output);
}
else if (KindLower == TEXT("local"))
{
FVoxelGraphLocalVariable LocalVariable;
LocalVariable.Name = PropName;
LocalVariable.Type = Type;
#if WITH_EDITORONLY_DATA
LocalVariable.Category = Category;
#endif
TerminalGraph->AddLocalVariable(Guid, LocalVariable);
}
else
{
return VoxelJsonStringify(JsonError(FString::Printf(TEXT("unknown kind %s (use parameter|input|output|local)"), *Kind)));
}
}
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetBoolField(TEXT("ok"), true);
Root->SetStringField(TEXT("guid"), Guid.ToString());
Root->SetStringField(TEXT("name"), PropName.ToString());
Root->SetStringField(TEXT("type"), Type.ToString());
Root->SetStringField(TEXT("kind"), KindLower);
return VoxelJsonStringify(Root);
}
FString UMCPVoxelGraphLibrary::ListGraphPropertiesJson(UObject* Asset, const FString& Terminal, const FString& Kind)
{
UVoxelGraph* Graph = AsVoxelGraph(Asset);
if (!Graph)
{
return VoxelJsonStringify(JsonError(TEXT("asset is not a UVoxelGraph")));
}
const FString KindLower = Kind.ToLower();
TArray<TSharedPtr<FJsonValue>> Items;
const auto AddItem = [&Items](const FGuid& Guid, const FName& PropName, const FVoxelPinType& Type, const FString& Category)
{
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
Item->SetStringField(TEXT("guid"), Guid.ToString());
Item->SetStringField(TEXT("name"), PropName.ToString());
Item->SetStringField(TEXT("type"), Type.ToString());
Item->SetStringField(TEXT("category"), Category);
Items.Add(MakeShared<FJsonValueObject>(Item));
};
if (KindLower == TEXT("parameter"))
{
for (const FGuid& G : Graph->GetParameters())
{
const FVoxelParameter& P = Graph->FindParameterChecked(G);
#if WITH_EDITORONLY_DATA
AddItem(G, P.Name, P.Type, P.Category);
#else
AddItem(G, P.Name, P.Type, FString());
#endif
}
}
else
{
UVoxelTerminalGraph* TerminalGraph = FindTerminalGraph(Graph, Terminal);
if (!TerminalGraph)
{
return VoxelJsonStringify(JsonError(TEXT("terminal graph not found")));
}
if (KindLower == TEXT("input"))
{
for (const FGuid& G : TerminalGraph->GetFunctionInputs())
{
const FVoxelGraphFunctionInput& P = TerminalGraph->FindInputChecked(G);
#if WITH_EDITORONLY_DATA
AddItem(G, P.Name, P.Type, P.Category);
#else
AddItem(G, P.Name, P.Type, FString());
#endif
}
}
else if (KindLower == TEXT("output"))
{
for (const FGuid& G : TerminalGraph->GetFunctionOutputs())
{
const FVoxelGraphFunctionOutput& P = TerminalGraph->FindOutputChecked(G);
#if WITH_EDITORONLY_DATA
AddItem(G, P.Name, P.Type, P.Category);
#else
AddItem(G, P.Name, P.Type, FString());
#endif
}
}
else if (KindLower == TEXT("local"))
{
for (const FGuid& G : TerminalGraph->GetLocalVariables())
{
const FVoxelGraphLocalVariable& P = TerminalGraph->FindLocalVariableChecked(G);
#if WITH_EDITORONLY_DATA
AddItem(G, P.Name, P.Type, P.Category);
#else
AddItem(G, P.Name, P.Type, FString());
#endif
}
}
else
{
return VoxelJsonStringify(JsonError(FString::Printf(TEXT("unknown kind %s (use parameter|input|output|local)"), *Kind)));
}
}
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetBoolField(TEXT("ok"), true);
Root->SetStringField(TEXT("kind"), KindLower);
Root->SetArrayField(TEXT("properties"), Items);
Root->SetNumberField(TEXT("count"), Items.Num());
return VoxelJsonStringify(Root);
}
FString UMCPVoxelGraphLibrary::SpawnPropertyNode(UEdGraph* Graph, const FString& Kind, const FString& Ref, FVector2D Position, bool bDeclaration)
{
if (!Graph)
{
return FString();
}
const FString KindLower = Kind.ToLower();
UVoxelTerminalGraph* Terminal = OwningTerminal(Graph);
FGuid Guid;
const TCHAR* ClassPath = nullptr;
if (KindLower == TEXT("parameter"))
{
UVoxelGraph* OwnerGraph = Terminal ? &Terminal->GetGraph() : nullptr;
Guid = ResolveParameterGuid(OwnerGraph, Ref);
ClassPath = TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_Parameter");
}
else if (KindLower == TEXT("input"))
{
Guid = ResolveTerminalPropGuid(Terminal, KindLower, Ref);
ClassPath = TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_FunctionInput");
}
else if (KindLower == TEXT("output"))
{
Guid = ResolveTerminalPropGuid(Terminal, KindLower, Ref);
ClassPath = TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_FunctionOutput");
}
else if (KindLower == TEXT("local"))
{
Guid = ResolveTerminalPropGuid(Terminal, KindLower, Ref);
ClassPath = bDeclaration
? TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_LocalVariableDeclaration")
: TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_LocalVariableUsage");
}
else
{
return FString();
}
if (!Guid.IsValid())
{
return FString();
}
UEdGraphNode* Node = SpawnGuidNodeReflect(Graph, ClassPath, Guid, Position);
return Node ? Node->NodeGuid.ToString() : FString();
}
FString UMCPVoxelGraphLibrary::SpawnCallFunctionNode(UEdGraph* Graph, const FString& FunctionRef, FVector2D Position)
{
UVoxelTerminalGraph* Terminal = OwningTerminal(Graph);
UVoxelGraph* OwnerGraph = Terminal ? &Terminal->GetGraph() : nullptr;
if (!OwnerGraph)
{
return FString();
}
UVoxelTerminalGraph* Function = FindTerminalGraph(OwnerGraph, FunctionRef);
if (!Function || Function->IsMainTerminalGraph() || Function->IsEditorTerminalGraph())
{
return FString();
}
UEdGraphNode* Node = SpawnGuidNodeReflect(
Graph,
TEXT("/Script/VoxelGraphEditor.VoxelGraphNode_CallMemberFunction"),
Function->GetGuid(),
Position);
return Node ? Node->NodeGuid.ToString() : FString();
}
bool UMCPVoxelGraphLibrary::NotifyGraphChanged(UEdGraph* Graph, const bool bCompile)
{
if (!Graph)
{
return false;
}
UVoxelTerminalGraph* TerminalGraph = Graph->GetTypedOuter<UVoxelTerminalGraph>();
if (!TerminalGraph)
{
Graph->MarkPackageDirty();
return false;
}
TerminalGraph->Modify();
TerminalGraph->GetGraph().Modify();
Graph->Modify();
Graph->MarkPackageDirty();
TerminalGraph->GetGraph().MarkPackageDirty();
if (GVoxelGraphTracker)
{
GVoxelGraphTracker->NotifyEdGraphChanged(*Graph);
GVoxelGraphTracker->NotifyTerminalGraphChanged(TerminalGraph->GetGraph());
}
if (bCompile)
{
TerminalGraph->GetRuntime().EnsureIsCompiled(true);
}
return true;
}
bool UMCPVoxelGraphLibrary::OpenVoxelGraphAsset(UObject* Asset)
{
if (!Asset || !GEditor)
{
return false;
}
UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
return AssetEditorSubsystem && AssetEditorSubsystem->OpenEditorForAsset(Asset);
}
#else // !WITH_MCP_VOXEL
// ---------------------------------------------------------------------------
// Voxel plugin not present in this project. Provide stub implementations so the
// module always links. voxel_graph_op will report this error to the caller.
// Rebuild with the Voxel plugin installed (or env UE_MCP_WITH_VOXEL=1) to enable.
// ---------------------------------------------------------------------------
static const TCHAR* MCPVoxelDisabledJson()
{
return TEXT("{\"ok\":false,\"error\":\"Voxel plugin not enabled in this build. Install the Voxel plugin and rebuild (UE_MCP_WITH_VOXEL=1) to use voxel_graph_op.\"}");
}
bool UMCPVoxelGraphLibrary::IsVoxelGraphAsset(UObject*) { return false; }
FString UMCPVoxelGraphLibrary::ListTerminalGraphsJson(UObject*) { return MCPVoxelDisabledJson(); }
UEdGraph* UMCPVoxelGraphLibrary::FindTerminalEdGraph(UObject*, const FString&) { return nullptr; }
FString UMCPVoxelGraphLibrary::DumpTerminalGraphJson(UObject*, const FString&) { return MCPVoxelDisabledJson(); }
FString UMCPVoxelGraphLibrary::GetSelectedNodesJson(UObject*, const FString&) { return MCPVoxelDisabledJson(); }
FString UMCPVoxelGraphLibrary::SpawnCommentNode(UEdGraph*, FVector2D, FVector2D, const FString&, FLinearColor) { return FString(); }
FString UMCPVoxelGraphLibrary::ListNodeTypesJson(const FString&, int32) { return MCPVoxelDisabledJson(); }
FString UMCPVoxelGraphLibrary::SpawnStructNode(UEdGraph*, const FString&, FVector2D) { return FString(); }
FString UMCPVoxelGraphLibrary::AddGraphPropertyJson(UObject*, const FString&, const FString&, const FString&, const FString&, const FString&) { return MCPVoxelDisabledJson(); }
FString UMCPVoxelGraphLibrary::ListGraphPropertiesJson(UObject*, const FString&, const FString&) { return MCPVoxelDisabledJson(); }
FString UMCPVoxelGraphLibrary::SpawnPropertyNode(UEdGraph*, const FString&, const FString&, FVector2D, bool) { return FString(); }
FString UMCPVoxelGraphLibrary::SpawnCallFunctionNode(UEdGraph*, const FString&, FVector2D) { return FString(); }
bool UMCPVoxelGraphLibrary::NotifyGraphChanged(UEdGraph*, bool) { return false; }
bool UMCPVoxelGraphLibrary::OpenVoxelGraphAsset(UObject*) { return false; }
#endif // WITH_MCP_VOXEL

View File

@ -0,0 +1,126 @@
#include "MCPWidgetRenderLibrary.h"
#include "Blueprint/UserWidget.h"
#include "Blueprint/WidgetTree.h"
#include "Editor.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Engine/World.h"
#include "HAL/PlatformFileManager.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"
#include "ImageUtils.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "RenderingThread.h"
#include "Slate/WidgetRenderer.h"
bool UMCPWidgetRenderLibrary::RenderWidgetToPNG(
TSubclassOf<UUserWidget> WidgetClass,
const FString& OutputAbsPath,
const FString& FileNameNoExt,
int32 Width,
int32 Height,
FString& OutFullPath,
FString& OutError)
{
OutFullPath.Reset();
OutError.Reset();
if (!WidgetClass)
{
OutError = TEXT("WidgetClass is null");
return false;
}
UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
if (!World)
{
OutError = TEXT("No editor world available");
return false;
}
const int32 W = FMath::Clamp(Width, 16, 8192);
const int32 H = FMath::Clamp(Height, 16, 8192);
UUserWidget* Widget = CreateWidget<UUserWidget>(World, WidgetClass);
if (!Widget)
{
OutError = TEXT("CreateWidget returned null");
return false;
}
// FWidgetRenderer wants a transient render target. Use a non-asset one.
UTextureRenderTarget2D* RT = NewObject<UTextureRenderTarget2D>();
RT->ClearColor = FLinearColor(0.f, 0.f, 0.f, 0.f);
RT->TargetGamma = 1.f;
RT->InitCustomFormat(W, H, PF_B8G8R8A8, /*bForceLinearGamma=*/false);
RT->UpdateResourceImmediate(true);
// Use a software widget renderer so we don't need a live Slate window.
FWidgetRenderer* Renderer = new FWidgetRenderer(/*bUseGammaCorrection=*/true);
if (!Renderer)
{
OutError = TEXT("FWidgetRenderer alloc failed");
return false;
}
Renderer->SetIsPrepassNeeded(true);
const FVector2D DrawSize(W, H);
Renderer->DrawWidget(RT, Widget->TakeWidget(), DrawSize, 0.016f, /*bDeferRenderTargetUpdate=*/false);
// Wait for the GPU work to finish so we can read back pixels.
FlushRenderingCommands();
// Read back pixels.
FTextureRenderTargetResource* RTResource = RT->GameThread_GetRenderTargetResource();
if (!RTResource)
{
BeginCleanup(Renderer);
OutError = TEXT("RT resource null after render");
return false;
}
TArray<FColor> Pixels;
Pixels.SetNumUninitialized(W * H);
FReadSurfaceDataFlags ReadFlags(RCM_UNorm);
ReadFlags.SetLinearToGamma(false);
if (!RTResource->ReadPixels(Pixels, ReadFlags))
{
BeginCleanup(Renderer);
OutError = TEXT("ReadPixels failed");
return false;
}
// Encode PNG via ImageWrapper.
IImageWrapperModule& ImageWrapperModule =
FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
TSharedPtr<IImageWrapper> PNG = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
if (!PNG.IsValid() ||
!PNG->SetRaw(Pixels.GetData(), Pixels.Num() * sizeof(FColor), W, H, ERGBFormat::BGRA, 8))
{
BeginCleanup(Renderer);
OutError = TEXT("PNG encode failed");
return false;
}
const TArray64<uint8>& Compressed = PNG->GetCompressed(100);
// Ensure directory.
IPlatformFile& PF = FPlatformFileManager::Get().GetPlatformFile();
if (!PF.DirectoryExists(*OutputAbsPath))
{
PF.CreateDirectoryTree(*OutputAbsPath);
}
const FString FullPath = FPaths::Combine(OutputAbsPath, FileNameNoExt + TEXT(".png"));
if (!FFileHelper::SaveArrayToFile(Compressed, *FullPath))
{
BeginCleanup(Renderer);
OutError = FString::Printf(TEXT("SaveArrayToFile failed: %s"), *FullPath);
return false;
}
BeginCleanup(Renderer);
OutFullPath = FullPath;
return true;
}

View File

@ -0,0 +1,347 @@
#include "UEBlueprintMCPEditor.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"
#include "Misc/CoreDelegates.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "BlueprintEditor.h"
#include "BlueprintEditorContext.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraphNode_Comment.h"
#include "Framework/Application/IInputProcessor.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Commands/UIAction.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "ScopedTransaction.h"
#include "SGraphPanel.h"
#include "ToolMenu.h"
#include "ToolMenuEntry.h"
#include "ToolMenuSection.h"
#include "ToolMenus.h"
#define LOCTEXT_NAMESPACE "UEBlueprintMCPEditor"
namespace
{
const TCHAR* MCPZoneCommentText = TEXT("MCP Zone");
const TCHAR* LegacyMCPZoneCommentText = TEXT("MCP Draft Zone");
bool IsMCPZoneComment(const UEdGraphNode* Node)
{
const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node);
return CommentNode
&& (CommentNode->NodeComment.Equals(MCPZoneCommentText, ESearchCase::IgnoreCase)
|| CommentNode->NodeComment.Equals(LegacyMCPZoneCommentText, ESearchCase::IgnoreCase));
}
}
class FMCPZoneInputProcessor : public IInputProcessor
{
public:
explicit FMCPZoneInputProcessor(FUEBlueprintMCPEditorModule& InOwner)
: Owner(InOwner)
{
}
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override
{
}
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
{
if (!Owner.bMCPZoneModeActive || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
{
return false;
}
TSharedPtr<FBlueprintEditor> Editor = Owner.ActiveMCPZoneEditor.Pin();
if (!Editor.IsValid() || !Editor->GetFocusedGraph())
{
return false;
}
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Editor->GetFocusedGraph(), false);
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
if (!GraphPanel)
{
return false;
}
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
const FVector2f ScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
if (!Geometry.IsUnderLocation(ScreenPosition))
{
return false;
}
bDragging = true;
StartScreenPosition = ScreenPosition;
return true;
}
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
{
return bDragging && Owner.bMCPZoneModeActive;
}
virtual bool HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
{
if (!bDragging || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
{
return false;
}
bDragging = false;
const FVector2f EndScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
Owner.TryCreateMCPZoneFromScreenDrag(StartScreenPosition, EndScreenPosition);
return true;
}
virtual const TCHAR* GetDebugName() const override
{
return TEXT("MCPZoneInputProcessor");
}
private:
FUEBlueprintMCPEditorModule& Owner;
bool bDragging = false;
FVector2f StartScreenPosition = FVector2f::ZeroVector;
};
IMPLEMENT_MODULE(FUEBlueprintMCPEditorModule, UEBlueprintMCPEditor)
void FUEBlueprintMCPEditorModule::StartupModule()
{
UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FUEBlueprintMCPEditorModule::RegisterMenus));
FCoreDelegates::OnPostEngineInit.AddRaw(this, &FUEBlueprintMCPEditorModule::NotifyBridgeStatus);
}
void FUEBlueprintMCPEditorModule::NotifyBridgeStatus()
{
const FString Url = TEXT("http://127.0.0.1:30010/remote/info");
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
Request->SetVerb(TEXT("GET"));
Request->SetURL(Url);
Request->SetTimeout(3.0f);
Request->OnProcessRequestComplete().BindLambda(
[](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSucceeded)
{
const bool bOk = bSucceeded && Resp.IsValid() && Resp->GetResponseCode() == 200;
FNotificationInfo Info(bOk
? LOCTEXT("MCPBridgeUp", "UE Blueprint MCP bridge ready (Remote Control :30010)")
: LOCTEXT("MCPBridgeDown", "UE Blueprint MCP bridge NOT reachable on :30010 — check Remote Control plugin"));
Info.ExpireDuration = bOk ? 4.0f : 8.0f;
Info.bUseSuccessFailIcons = true;
Info.bFireAndForget = true;
TSharedPtr<SNotificationItem> Item = FSlateNotificationManager::Get().AddNotification(Info);
if (Item.IsValid())
{
Item->SetCompletionState(bOk ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail);
}
});
Request->ProcessRequest();
}
void FUEBlueprintMCPEditorModule::ShutdownModule()
{
FCoreDelegates::OnPostEngineInit.RemoveAll(this);
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
{
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
MCPZoneInputProcessor.Reset();
}
if (UToolMenus::IsToolMenuUIEnabled())
{
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);
}
}
void FUEBlueprintMCPEditorModule::RegisterMenus()
{
FToolMenuOwnerScoped OwnerScoped(this);
UToolMenu* Toolbar = UToolMenus::Get()->ExtendMenu("AssetEditor.BlueprintEditor.ToolBar");
FToolMenuSection& Section = Toolbar->FindOrAddSection("Settings");
Section.AddDynamicEntry("MCPDraftZoneCommands", FNewToolMenuSectionDelegate::CreateLambda([this](FToolMenuSection& InSection)
{
UBlueprintEditorToolMenuContext* Context = InSection.FindContext<UBlueprintEditorToolMenuContext>();
if (!Context || !Context->BlueprintEditor.IsValid() || !Context->GetBlueprintObj())
{
return;
}
TWeakPtr<FBlueprintEditor> WeakEditor = Context->BlueprintEditor;
FUIAction Action(
FExecuteAction::CreateLambda([this, WeakEditor]()
{
ToggleMCPZoneMode(WeakEditor);
}),
FCanExecuteAction::CreateLambda([WeakEditor]()
{
return WeakEditor.IsValid() && WeakEditor.Pin()->GetFocusedGraph() != nullptr;
}),
FIsActionChecked::CreateLambda([this, WeakEditor]()
{
return IsMCPZoneModeActive(WeakEditor);
}));
FToolMenuEntry Entry = FToolMenuEntry::InitToolBarButton(
FName(TEXT("MCPDraftZone")),
FToolUIActionChoice(Action),
LOCTEXT("MCPDraftZone_Label", "MCP Zone"),
LOCTEXT("MCPDraftZone_Tooltip", "Toggle MCP Zone mode. While active, left-mouse drag in the graph to create a generation zone. Toggle off to remove MCP Zone comments while keeping generated nodes."),
FSlateIcon(),
EUserInterfaceActionType::ToggleButton);
InSection.AddEntry(Entry);
}));
}
void FUEBlueprintMCPEditorModule::ToggleMCPZoneMode(TWeakPtr<FBlueprintEditor> BlueprintEditor)
{
TSharedPtr<FBlueprintEditor> Editor = BlueprintEditor.Pin();
if (!Editor.IsValid())
{
return;
}
if (bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == Editor)
{
RemoveMCPZones(Editor->GetBlueprintObj());
bMCPZoneModeActive = false;
ActiveMCPZoneEditor.Reset();
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
{
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
MCPZoneInputProcessor.Reset();
}
return;
}
if (bMCPZoneModeActive)
{
if (TSharedPtr<FBlueprintEditor> PreviousEditor = ActiveMCPZoneEditor.Pin())
{
RemoveMCPZones(PreviousEditor->GetBlueprintObj());
}
}
bMCPZoneModeActive = true;
ActiveMCPZoneEditor = Editor;
if (!MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
{
MCPZoneInputProcessor = MakeShared<FMCPZoneInputProcessor>(*this);
FSlateApplication::Get().RegisterInputPreProcessor(MCPZoneInputProcessor);
}
}
bool FUEBlueprintMCPEditorModule::IsMCPZoneModeActive(TWeakPtr<FBlueprintEditor> BlueprintEditor) const
{
return bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == BlueprintEditor.Pin();
}
bool FUEBlueprintMCPEditorModule::TryCreateMCPZoneFromScreenDrag(const FVector2f& StartScreenPosition, const FVector2f& EndScreenPosition)
{
TSharedPtr<FBlueprintEditor> Editor = ActiveMCPZoneEditor.Pin();
if (!Editor.IsValid())
{
return false;
}
UBlueprint* Blueprint = Editor->GetBlueprintObj();
UEdGraph* Graph = Editor->GetFocusedGraph();
if (!Blueprint || !Graph)
{
return false;
}
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Graph, false);
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
if (!GraphPanel)
{
return false;
}
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
const FVector2f LocalStart = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(StartScreenPosition));
const FVector2f LocalEnd = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(EndScreenPosition));
const FVector2f GraphStart = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalStart));
const FVector2f GraphEnd = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalEnd));
CreateMCPZone(Graph, Blueprint, GraphStart, GraphEnd);
return true;
}
void FUEBlueprintMCPEditorModule::CreateMCPZone(UEdGraph* Graph, UBlueprint* Blueprint, const FVector2f& GraphStart, const FVector2f& GraphEnd) const
{
if (!Blueprint || !Graph)
{
return;
}
const FScopedTransaction Transaction(LOCTEXT("CreateMCPDraftZone", "Create MCP Draft Zone"));
Graph->Modify();
Blueprint->Modify();
constexpr int32 MinWidth = 240;
constexpr int32 MinHeight = 160;
const int32 MinX = FMath::FloorToInt(FMath::Min(GraphStart.X, GraphEnd.X));
const int32 MinY = FMath::FloorToInt(FMath::Min(GraphStart.Y, GraphEnd.Y));
const int32 MaxX = FMath::CeilToInt(FMath::Max(GraphStart.X, GraphEnd.X));
const int32 MaxY = FMath::CeilToInt(FMath::Max(GraphStart.Y, GraphEnd.Y));
UEdGraphNode_Comment* ZoneNode = NewObject<UEdGraphNode_Comment>(Graph);
ZoneNode->CreateNewGuid();
ZoneNode->NodeComment = MCPZoneCommentText;
ZoneNode->bCommentBubbleVisible = false;
ZoneNode->CommentColor = FLinearColor(0.05f, 0.32f, 0.85f, 0.35f);
ZoneNode->NodePosX = MinX;
ZoneNode->NodePosY = MinY;
ZoneNode->NodeWidth = FMath::Max(MinWidth, MaxX - MinX);
ZoneNode->NodeHeight = FMath::Max(MinHeight, MaxY - MinY);
Graph->AddNode(ZoneNode, true, false);
ZoneNode->PostPlacedNewNode();
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
}
void FUEBlueprintMCPEditorModule::RemoveMCPZones(UBlueprint* Blueprint) const
{
if (!Blueprint)
{
return;
}
const FScopedTransaction Transaction(LOCTEXT("RemoveMCPDraftZones", "Remove MCP Draft Zones"));
Blueprint->Modify();
TArray<UEdGraph*> Graphs;
Blueprint->GetAllGraphs(Graphs);
for (UEdGraph* Graph : Graphs)
{
if (!Graph)
{
continue;
}
Graph->Modify();
TArray<UEdGraphNode*> Nodes = Graph->Nodes;
for (UEdGraphNode* Node : Nodes)
{
if (IsMCPZoneComment(Node))
{
Graph->RemoveNode(Node);
}
}
}
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,79 @@
// Headless capture helpers for the UEBlueprintMCP plugin.
//
// Lets external agents SEE editor content WITHOUT opening any editor tab/window:
// * RenderAssetThumbnailToPNG — renders ANY asset (StaticMesh, SkeletalMesh,
// Material, Texture, Blueprint, Niagara, AnimSequence, Sound, ...) using the
// editor's per-class thumbnail renderer. No asset editor needs to be open.
// * CaptureEditorSceneToPNG — renders the current editor world from an
// arbitrary camera via an offscreen SceneCaptureComponent2D. No level
// viewport / PIE required.
//
// Both write a PNG to disk and return its absolute path. Called from Python via
// unreal.MCPCaptureLibrary.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MCPCaptureLibrary.generated.h"
UCLASS()
class UEBLUEPRINTMCPEDITOR_API UMCPCaptureLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Render an asset's thumbnail to a PNG file, headlessly.
* Works for every asset type that has a registered thumbnail renderer, which
* is effectively all of them (meshes, materials, textures, blueprints,
* niagara systems, anim sequences, sounds, data assets, ...). The asset
* editor does NOT need to be open.
*
* @param AssetPath Object path, e.g. "/Game/Meshes/SM_Rock" (no class prefix needed).
* @param OutputAbsPath Absolute directory to write into (created if missing).
* @param FileNameNoExt File name WITHOUT extension. ".png" is appended.
* @param Width Pixels, clamped [16, 2048].
* @param Height Pixels, clamped [16, 2048].
* @param OutFullPath On success, absolute path of the written PNG.
* @param OutError On failure, a short description.
*/
UFUNCTION(BlueprintCallable, Category = "MCP|Capture",
meta = (DisplayName = "Render Asset Thumbnail To PNG"))
static bool RenderAssetThumbnailToPNG(
const FString& AssetPath,
const FString& OutputAbsPath,
const FString& FileNameNoExt,
int32 Width,
int32 Height,
FString& OutFullPath,
FString& OutError
);
/**
* Render the active editor world from an arbitrary camera to a PNG, headlessly,
* using an offscreen SceneCaptureComponent2D. No level viewport is needed.
*
* @param Location World-space camera location.
* @param Rotation World-space camera rotation (pitch/yaw/roll).
* @param FOV Horizontal field of view in degrees (clamped [5, 170]).
* @param OutputAbsPath Absolute directory to write into (created if missing).
* @param FileNameNoExt File name WITHOUT extension. ".png" is appended.
* @param Width Pixels, clamped [16, 4096].
* @param Height Pixels, clamped [16, 4096].
* @param OutFullPath On success, absolute path of the written PNG.
* @param OutError On failure, a short description.
*/
UFUNCTION(BlueprintCallable, Category = "MCP|Capture",
meta = (DisplayName = "Capture Editor Scene To PNG"))
static bool CaptureEditorSceneToPNG(
FVector Location,
FRotator Rotation,
float FOV,
const FString& OutputAbsPath,
const FString& FileNameNoExt,
int32 Width,
int32 Height,
FString& OutFullPath,
FString& OutError
);
};

View File

@ -0,0 +1,430 @@
// Exposes K2 graph mutation to Python.
//
// In Python these are reachable as:
// unreal.MCPGraphLibrary.spawn_call_function_node(bp, graph, owner_class, fn_name, pos)
// unreal.MCPGraphLibrary.connect_pins(graph, from_id, "Then", to_id, "Exec")
// etc.
//
// Every spawn returns the node's NodeGuid as a string. Python keeps a local
// id -> NodeGuid map and passes the guid into connect_pins / set_pin_default.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MCPGraphLibrary.generated.h"
class UBlueprint;
class UWidgetBlueprint;
class UWidget;
class UEdGraph;
class UEdGraphNode;
class UEdGraphPin;
class UScriptStruct;
class UEnum;
UCLASS()
class UEBLUEPRINTMCPEDITOR_API UMCPGraphLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// ====================================================================
// Graph lookup
// ====================================================================
UFUNCTION(BlueprintCallable, Category = "MCP|Graph")
static UEdGraph* FindEventGraph(UBlueprint* Blueprint);
UFUNCTION(BlueprintCallable, Category = "MCP|Graph")
static UEdGraph* FindFunctionGraph(UBlueprint* Blueprint, FName FunctionName);
UFUNCTION(BlueprintCallable, Category = "MCP|Graph")
static TArray<FString> ListNodeIds(UEdGraph* Graph);
UFUNCTION(BlueprintCallable, Category = "MCP|Graph")
static TArray<FString> ListPinsOnNode(UEdGraph* Graph, const FString& NodeId);
UFUNCTION(BlueprintCallable, Category = "MCP|Graph")
static int32 ClearGraph(UEdGraph* Graph);
UFUNCTION(BlueprintCallable, Category = "MCP|Graph")
static bool DeleteNode(UEdGraph* Graph, const FString& NodeId);
// ====================================================================
// Spawners — basic
// ====================================================================
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnCallFunctionNode(UBlueprint* Blueprint, UEdGraph* Graph, UClass* FunctionOwnerClass, FName FunctionName, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnEventNode(UBlueprint* Blueprint, UEdGraph* Graph, UClass* OwnerClass, FName EventName, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnCustomEventNode(UBlueprint* Blueprint, UEdGraph* Graph, FName EventName, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Network")
static bool SetCustomEventNetworkFlags(UBlueprint* Blueprint, UEdGraph* Graph, const FString& NodeIdOrEventName, FName Mode, bool bReliable);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnComponentBoundEvent(UBlueprint* Blueprint, UEdGraph* Graph, FName ComponentName, FName DelegateName, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnBranchNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnSequenceNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position, int32 OutputCount);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnVariableGetNode(UBlueprint* Blueprint, UEdGraph* Graph, FName VariableName, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnVariableSetNode(UBlueprint* Blueprint, UEdGraph* Graph, FName VariableName, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnSelfNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnCastNode(UBlueprint* Blueprint, UEdGraph* Graph, UClass* TargetClass, FVector2D Position);
// ====================================================================
// Spawners — flow / collections
// ====================================================================
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnSwitchOnIntNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnSwitchOnStringNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnSwitchOnEnumNode(UBlueprint* Blueprint, UEdGraph* Graph, UEnum* EnumType, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnMakeArrayNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnMakeStructNode(UBlueprint* Blueprint, UEdGraph* Graph, UScriptStruct* StructType, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnBreakStructNode(UBlueprint* Blueprint, UEdGraph* Graph, UScriptStruct* StructType, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnSetFieldsInStructNode(UBlueprint* Blueprint, UEdGraph* Graph, UScriptStruct* StructType, FVector2D Position);
/** Spawn a UK2Node_MacroInstance bound to one of the StandardMacros (ForLoop,
* ForEachLoop, WhileLoop, DoOnce, Gate, FlipFlop, MultiGate, IsValid, DoN). */
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnStandardMacroNode(UBlueprint* Blueprint, UEdGraph* Graph, FName MacroName, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnSpawnActorFromClassNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnFormatTextNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnGetSubsystemNode(UBlueprint* Blueprint, UEdGraph* Graph, UClass* SubsystemClass, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnLoadAssetNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
// ====================================================================
// Spawners — Tier 2 (knot/comment/enum literal/class defaults/return/arrays)
// ====================================================================
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnKnotNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnCommentNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position, FVector2D Size, const FString& Comment);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnEnumLiteralNode(UBlueprint* Blueprint, UEdGraph* Graph, UEnum* EnumType, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnGetClassDefaultsNode(UBlueprint* Blueprint, UEdGraph* Graph, UClass* TargetClass, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnFunctionResultNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnGetArrayItemNode(UBlueprint* Blueprint, UEdGraph* Graph, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Spawn")
static FString SpawnPromotableOperatorNode(UBlueprint* Blueprint, UEdGraph* Graph, FName OperatorName, FVector2D Position);
/** Add a new case pin to a SwitchInt/SwitchString/SwitchEnum node. */
UFUNCTION(BlueprintCallable, Category = "MCP|Edit")
static bool AddCasePinToSwitch(UEdGraph* Graph, const FString& NodeId);
/** Set FormatText template AND reconstruct so {arg} pins appear. */
UFUNCTION(BlueprintCallable, Category = "MCP|Edit")
static bool FormatTextSetFormat(UEdGraph* Graph, const FString& NodeId, const FString& Format);
/** Force a node to ReconstructNode() (e.g. after toggling pure/latent). */
UFUNCTION(BlueprintCallable, Category = "MCP|Edit")
static bool ReconstructNode(UEdGraph* Graph, const FString& NodeId);
// ====================================================================
// Delegates
// ====================================================================
/** Bind an existing event to a component's multicast delegate.
* target = "ComponentName.DelegateName". */
UFUNCTION(BlueprintCallable, Category = "MCP|Delegate")
static FString SpawnBindDelegateNode(UBlueprint* Blueprint, UEdGraph* Graph, FName ComponentName, FName DelegateName, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Delegate")
static FString SpawnUnbindDelegateNode(UBlueprint* Blueprint, UEdGraph* Graph, FName ComponentName, FName DelegateName, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|Delegate")
static FString SpawnCallDelegateNode(UBlueprint* Blueprint, UEdGraph* Graph, FName DispatcherName, FVector2D Position);
/** AssignDelegate = AddDelegate + CustomEvent — a sugar combo node. */
UFUNCTION(BlueprintCallable, Category = "MCP|Delegate")
static FString SpawnAssignDelegateNode(UBlueprint* Blueprint, UEdGraph* Graph, FName ComponentName, FName DelegateName, FVector2D Position);
/** Add an Event Dispatcher (multicast delegate variable) with no params. */
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool AddEventDispatcher(UBlueprint* Blueprint, FName DispatcherName);
// ====================================================================
// Edges & defaults
// ====================================================================
UFUNCTION(BlueprintCallable, Category = "MCP|Edges")
static bool ConnectPins(UEdGraph* Graph, const FString& FromNodeId, FName FromPin, const FString& ToNodeId, FName ToPin);
UFUNCTION(BlueprintCallable, Category = "MCP|Edges")
static bool BreakLink(UEdGraph* Graph, const FString& FromNodeId, FName FromPin, const FString& ToNodeId, FName ToPin);
UFUNCTION(BlueprintCallable, Category = "MCP|Edges")
static bool BreakAllNodeLinks(UEdGraph* Graph, const FString& NodeId);
UFUNCTION(BlueprintCallable, Category = "MCP|Edges")
static bool SetPinDefault(UEdGraph* Graph, const FString& NodeId, FName PinName, const FString& Value);
// ====================================================================
// BP-level authoring
// ====================================================================
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static UEdGraph* AddFunctionGraph(UBlueprint* Blueprint, FName FunctionName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static UEdGraph* AddMacroGraph(UBlueprint* Blueprint, FName MacroName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool AddInterface(UBlueprint* Blueprint, const FString& InterfacePath);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool SetVariableDefaultValue(UBlueprint* Blueprint, FName VariableName, const FString& Value);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool SetVariableMetadata(UBlueprint* Blueprint, FName VariableName, bool bInstanceEditable, bool bExposeOnSpawn, const FString& Category, const FString& Tooltip);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool AddLocalVariable(UBlueprint* Blueprint, FName FunctionName, FName VariableName, const FString& TypeSpec, const FString& DefaultValue);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static FString AddComponentToBlueprint(UBlueprint* Blueprint, UClass* ComponentClass, FName ComponentName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool AddMemberVariable(UBlueprint* Blueprint, FName VariableName, const FString& TypeSpec);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool RemoveComponent(UBlueprint* Blueprint, FName ComponentName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool AttachComponent(UBlueprint* Blueprint, FName ChildName, FName ParentName, FName SocketName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool SetRootComponent(UBlueprint* Blueprint, FName ComponentName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool SetComponentProperty(UBlueprint* Blueprint, FName ComponentName, FName PropertyName, const FString& Value);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool SetComponentTransform(UBlueprint* Blueprint, FName ComponentName, FVector Location, FRotator Rotation, FVector Scale);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool ConfigureBoxComponent(UBlueprint* Blueprint, FName ComponentName, FVector Extent, FName CollisionProfile);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool RenameVariable(UBlueprint* Blueprint, FName OldName, FName NewName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool DeleteVariable(UBlueprint* Blueprint, FName VariableName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool RenameFunction(UBlueprint* Blueprint, FName OldName, FName NewName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool RemoveFunctionGraph(UBlueprint* Blueprint, FName FunctionName);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool RemoveMacroGraph(UBlueprint* Blueprint, FName MacroName);
/** Add an input/output parameter to a user function. Direction: "input"/"output". */
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool AddFunctionParameter(UBlueprint* Blueprint, FName FunctionName, FName ParamName, const FString& TypeSpec, const FString& Direction);
UFUNCTION(BlueprintCallable, Category = "MCP|Authoring")
static bool RemoveFunctionParameter(UBlueprint* Blueprint, FName FunctionName, FName ParamName);
// ====================================================================
// UMG Designer / WidgetTree
// ====================================================================
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static FString DescribeWidgetTreeJson(UWidgetBlueprint* Blueprint);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static FString GetWidgetPropertiesJson(UWidgetBlueprint* Blueprint, FName WidgetName);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static FString GetWidgetSlotPropertiesJson(UWidgetBlueprint* Blueprint, FName WidgetName);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static FString AddWidgetToTree(UWidgetBlueprint* Blueprint, UClass* WidgetClass, FName WidgetName, FName ParentName, int32 Index, bool bIsVariable);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool RemoveWidgetFromTree(UWidgetBlueprint* Blueprint, FName WidgetName);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool RenameWidgetInTree(UWidgetBlueprint* Blueprint, FName OldName, FName NewName);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool MoveWidgetInTree(UWidgetBlueprint* Blueprint, FName WidgetName, FName NewParentName, int32 Index);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool SetRootWidget(UWidgetBlueprint* Blueprint, FName WidgetName);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool SetWidgetIsVariable(UWidgetBlueprint* Blueprint, FName WidgetName, bool bIsVariable);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool SetWidgetProperty(UWidgetBlueprint* Blueprint, FName WidgetName, FName PropertyName, const FString& Value);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool SetWidgetSlotProperty(UWidgetBlueprint* Blueprint, FName WidgetName, FName PropertyName, const FString& Value);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool SetCanvasSlotLayout(UWidgetBlueprint* Blueprint, FName WidgetName, FVector2D Position, FVector2D Size, FVector2D AnchorsMinimum, FVector2D AnchorsMaximum, FVector2D Alignment, bool bAutoSize, int32 ZOrder);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool SetNamedSlotContent(UWidgetBlueprint* Blueprint, FName HostWidgetName, FName SlotName, FName ContentWidgetName);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool ClearNamedSlotContent(UWidgetBlueprint* Blueprint, FName HostWidgetName, FName SlotName);
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool SetDesiredFocusWidget(UWidgetBlueprint* Blueprint, FName WidgetName);
/** Create a UMG WidgetAnimation that keys a single float property (e.g. RenderOpacity)
* on WidgetName. Times (seconds) and Values are parallel arrays — each pair is one
* linear key. The animation is added to the WidgetBlueprint's Animations array; loop
* is controlled at PlayAnimation time (NumLoopsToPlay=0). Returns false on bad input
* or if an animation with AnimName already exists. */
UFUNCTION(BlueprintCallable, Category = "MCP|WidgetTree")
static bool CreateWidgetFloatAnimation(UWidgetBlueprint* Blueprint, FName AnimName, FName WidgetName, FName PropertyName, const TArray<float>& Times, const TArray<float>& Values);
// ====================================================================
// Compile / introspect
// ====================================================================
UFUNCTION(BlueprintCallable, Category = "MCP|Asset")
static bool CompileAndSaveBlueprint(UBlueprint* Blueprint);
UFUNCTION(BlueprintCallable, Category = "MCP|Asset")
static TArray<FString> GetCompileErrors(UBlueprint* Blueprint);
/** Compile + capture FCompilerResultsLog. Returns "SEV|NodeName|Message" per entry. */
UFUNCTION(BlueprintCallable, Category = "MCP|Asset")
static TArray<FString> CompileWithMessages(UBlueprint* Blueprint);
/** Dump entire graph as JSON string round-trippable into apply_graph NodeSpec. */
UFUNCTION(BlueprintCallable, Category = "MCP|Asset")
static FString DumpGraphToJson(UEdGraph* Graph);
// ====================================================================
// Discovery — Pipeline v0.3
// ====================================================================
/** List all graphs on a Blueprint. Returns JSON: {event:[], functions:[], macros:[], delegates:[]}. */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static FString ListGraphsJson(UBlueprint* Blueprint);
/** Full BP overview: parent, interfaces, vars, components (SCS tree), functions, dispatchers. JSON. */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static FString DescribeBlueprintJson(UBlueprint* Blueprint);
/** Currently selected nodes in the open BP editor for `Blueprint`. JSON list of NodeGuid strings + per-node info. */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static FString GetSelectedNodesJson(UBlueprint* Blueprint);
/** All currently-open Blueprint editors (paths). */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static TArray<FString> ListOpenBlueprints();
/** For a UK2Node_CallFunction by guid, return {owner_class, function, source_bp_path?, native?} JSON. */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static FString ResolveFunctionSourceJson(UEdGraph* Graph, const FString& NodeId);
/** All node guids in this BP that reference variable `Name`. JSON list grouped by graph. */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static FString FindVariableReferencesJson(UBlueprint* Blueprint, FName VariableName);
/** All call sites of `FunctionName` (defined on Blueprint or external) within this BP. JSON. */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static FString FindFunctionReferencesJson(UBlueprint* Blueprint, FName FunctionName);
/** AssetRegistry-driven BP search by parent class path. Returns list of BP paths. */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static TArray<FString> FindBlueprintsByParent(const FString& ParentClassPath);
/** Hard-referenced assets of this BP (asset registry). */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static TArray<FString> GetReferencedAssets(const FString& AssetPath);
/** Reverse: assets that reference this BP. */
UFUNCTION(BlueprintCallable, Category = "MCP|Discover")
static TArray<FString> GetReferencersOfAsset(const FString& AssetPath);
// ====================================================================
// Editing — single-node operations
// ====================================================================
/** Move nodes by guid to new positions. positions: array of {guid,x,y} encoded as parallel arrays. */
UFUNCTION(BlueprintCallable, Category = "MCP|Edit")
static int32 MoveNodes(UEdGraph* Graph, const TArray<FString>& NodeIds, const TArray<FVector2D>& Positions);
UFUNCTION(BlueprintCallable, Category = "MCP|Edit")
static bool ResizeCommentNode(UEdGraph* Graph, const FString& NodeId, FVector2D Size);
UFUNCTION(BlueprintCallable, Category = "MCP|Edit")
static bool SetNodeComment(UEdGraph* Graph, const FString& NodeId, const FString& Comment, bool bCommentBubbleVisible);
// ====================================================================
// Navigation — open editors, focus nodes
// ====================================================================
UFUNCTION(BlueprintCallable, Category = "MCP|Navigate")
static bool OpenBlueprintEditor(UBlueprint* Blueprint);
UFUNCTION(BlueprintCallable, Category = "MCP|Navigate")
static bool OpenGraphInEditor(UBlueprint* Blueprint, UEdGraph* Graph);
UFUNCTION(BlueprintCallable, Category = "MCP|Navigate")
static bool JumpToNodeInEditor(UBlueprint* Blueprint, UEdGraph* Graph, const FString& NodeId);
UFUNCTION(BlueprintCallable, Category = "MCP|Introspect")
static TArray<FString> ListFunctionsOnClass(UClass* Class);
UFUNCTION(BlueprintCallable, Category = "MCP|Introspect")
static TArray<FString> ListPropertiesOnClass(UClass* Class);
UFUNCTION(BlueprintCallable, Category = "MCP|Introspect")
static TArray<FString> ListDelegatesOnClass(UClass* Class);
UFUNCTION(BlueprintCallable, Category = "MCP|Introspect")
static FString GetPinType(UClass* OwnerClass, FName FunctionName, FName PinName);
private:
static UEdGraphNode* FindNodeByGuid(UEdGraph* Graph, const FString& NodeId);
static UEdGraphPin* FindPinByName(UEdGraphNode* Node, const FName& PinName);
static class USCS_Node* FindSCSNode(UBlueprint* Blueprint, FName ComponentName);
template <typename TNode>
static TNode* PlaceNode(UEdGraph* Graph, FVector2D Position);
};

View File

@ -0,0 +1,28 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MCPMaterialLibrary.generated.h"
class UMaterialExpression;
class UMaterial;
class UMaterialFunction;
UCLASS()
class UEBLUEPRINTMCPEDITOR_API UMCPMaterialLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "MCP|Material")
static TArray<UMaterialExpression*> GetMaterialExpressions(UObject* MaterialOrFunction);
UFUNCTION(BlueprintCallable, Category = "MCP|Material")
static UMaterialExpression* FindMaterialExpression(UObject* MaterialOrFunction, const FString& NodeId);
UFUNCTION(BlueprintCallable, Category = "MCP|Material")
static FString DumpMaterialGraphToJson(UObject* MaterialOrFunction);
UFUNCTION(BlueprintCallable, Category = "MCP|Material")
static FString ConnectMaterialExpressionsRaw(UObject* MaterialOrFunction, UMaterialExpression* FromExpression, const FString& FromOutput, UMaterialExpression* ToExpression, const FString& ToInput);
};

View File

@ -0,0 +1,69 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MCPNiagaraLibrary.generated.h"
class UNiagaraSystem;
class UNiagaraEmitter;
class UNiagaraScript;
class UNiagaraRendererProperties;
/**
* Editor-only helpers wrapping Niagara authoring APIs for the MCP server.
* All methods are no-ops/return defaults outside the editor.
*/
UCLASS()
class UMCPNiagaraLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Create an empty UNiagaraSystem asset at the given /Game/... path. */
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static bool CreateNiagaraSystem(const FString& PackagePath);
/** List emitter handle names on the system. */
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static TArray<FString> GetEmitterHandleNames(UNiagaraSystem* System);
/** Add an emitter (asset) to the system. Returns the new handle name or empty on failure. */
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static FString AddEmitterToSystem(UNiagaraSystem* System, UNiagaraEmitter* SourceEmitter, const FString& EmitterName);
/** Remove an emitter handle (by handle name) from the system. */
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static bool RemoveEmitterFromSystem(UNiagaraSystem* System, const FString& EmitterName);
/**
* List module nodes (function-call nodes) in a stack group.
* GroupName: EmitterSpawn | EmitterUpdate | ParticleSpawn | ParticleUpdate | SystemSpawn | SystemUpdate
* EmitterName ignored for SystemSpawn/SystemUpdate.
* Returns module names in evaluation order.
*/
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static TArray<FString> ListModules(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName);
/**
* Insert a module script into the stack group. Index = -1 appends.
* Returns the spawned module's function name (used as ModuleName for subsequent ops), or empty on failure.
*/
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static FString AddModuleToStack(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName, UNiagaraScript* ModuleScript, int32 Index);
/** Remove a module node from the stack group by name. */
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static bool RemoveModuleFromStack(UNiagaraSystem* System, const FString& EmitterName, const FString& GroupName, const FString& ModuleName);
/** Add a renderer (UNiagaraRendererProperties subclass) to an emitter. Returns the renderer class name or empty. */
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static FString AddRendererToEmitter(UNiagaraSystem* System, const FString& EmitterName, UClass* RendererClass);
/** Remove all renderers of a given class from an emitter. Returns count removed. */
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static int32 RemoveRenderersFromEmitter(UNiagaraSystem* System, const FString& EmitterName, UClass* RendererClass);
/** Recompile the Niagara system (and its emitters). */
UFUNCTION(BlueprintCallable, Category = "MCP|Niagara")
static bool RequestCompile(UNiagaraSystem* System);
};

View File

@ -0,0 +1,69 @@
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MCPVoxelGraphLibrary.generated.h"
class UEdGraph;
UCLASS()
class UEBLUEPRINTMCPEDITOR_API UMCPVoxelGraphLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static bool IsVoxelGraphAsset(UObject* Asset);
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString ListTerminalGraphsJson(UObject* Asset);
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static UEdGraph* FindTerminalEdGraph(UObject* Asset, const FString& Terminal);
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString DumpTerminalGraphJson(UObject* Asset, const FString& Terminal);
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString GetSelectedNodesJson(UObject* Asset, const FString& Terminal);
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString SpawnCommentNode(UEdGraph* Graph, FVector2D Position, FVector2D Size, const FString& Comment, FLinearColor Color);
// Returns a JSON catalogue of every spawnable Voxel node (struct nodes + function-library nodes).
// Filter is an optional case-insensitive substring matched against id / display name / category.
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString ListNodeTypesJson(const FString& Filter, int32 Limit);
// Spawns a functional Voxel node (UVoxelGraphNode_Struct) identified by NodeId
// (struct name, struct path, display name, or "Library.Function" for function nodes).
// Returns the new node GUID as string, or empty on failure.
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString SpawnStructNode(UEdGraph* Graph, const FString& NodeId, FVector2D Position);
// Registers a graph property and returns its new GUID as JSON {ok, guid, name, type}.
// Kind: "parameter" (graph parameter), "input"/"output" (function in/out), "local" (local variable).
// TypeStr: float|double|int|bool|vector2d|vector|color|name (defaults to float).
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString AddGraphPropertyJson(UObject* Asset, const FString& Terminal, const FString& Kind, const FString& Name, const FString& TypeStr, const FString& Category);
// Lists graph properties of the given Kind as JSON {ok, properties:[{guid,name,type,category}]}.
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString ListGraphPropertiesJson(UObject* Asset, const FString& Terminal, const FString& Kind);
// Spawns a usage node for an existing property (Ref = guid or name). bDeclaration only matters
// for Kind "local" (declaration vs usage). Returns the new node GUID, or empty on failure.
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString SpawnPropertyNode(UEdGraph* Graph, const FString& Kind, const FString& Ref, FVector2D Position, bool bDeclaration);
// Spawns a "Call Function" node for a function terminal graph (FunctionRef = terminal guid or name).
// Returns the new node GUID, or empty on failure.
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static FString SpawnCallFunctionNode(UEdGraph* Graph, const FString& FunctionRef, FVector2D Position);
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static bool NotifyGraphChanged(UEdGraph* Graph, bool bCompile);
UFUNCTION(BlueprintCallable, Category = "MCP|VoxelGraph")
static bool OpenVoxelGraphAsset(UObject* Asset);
};

View File

@ -0,0 +1,42 @@
// Vision-loop helper for the UEBlueprintMCP plugin.
// Renders a UMG WidgetBlueprint to a PNG file so external agents can SEE
// what they just authored. Called from Python via unreal.MCPWidgetRenderLibrary.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MCPWidgetRenderLibrary.generated.h"
class UUserWidget;
UCLASS()
class UEBLUEPRINTMCPEDITOR_API UMCPWidgetRenderLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Render a UserWidget class to a PNG file on disk.
* Returns true on success and fills OutFullPath with the absolute file path.
*
* @param WidgetClass A UUserWidget subclass (e.g. GeneratedClass of a WidgetBlueprint).
* @param OutputAbsPath Absolute directory path where the PNG should be written.
* The folder is created if missing.
* @param FileNameNoExt File name WITHOUT extension. ".png" is appended automatically.
* @param Width Texture width in pixels (clamped to [16, 8192]).
* @param Height Texture height in pixels (clamped to [16, 8192]).
* @param OutFullPath On success, the absolute path of the written PNG.
* @param OutError On failure, a short description of what went wrong.
*/
UFUNCTION(BlueprintCallable, Category = "MCP|UI",
meta = (DisplayName = "Render Widget To PNG"))
static bool RenderWidgetToPNG(
TSubclassOf<UUserWidget> WidgetClass,
const FString& OutputAbsPath,
const FString& FileNameNoExt,
int32 Width,
int32 Height,
FString& OutFullPath,
FString& OutError
);
};

View File

@ -0,0 +1,31 @@
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FBlueprintEditor;
class IInputProcessor;
class UBlueprint;
class UEdGraph;
class FUEBlueprintMCPEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
friend class FMCPZoneInputProcessor;
void RegisterMenus();
void NotifyBridgeStatus();
void ToggleMCPZoneMode(TWeakPtr<FBlueprintEditor> BlueprintEditor);
bool IsMCPZoneModeActive(TWeakPtr<FBlueprintEditor> BlueprintEditor) const;
bool TryCreateMCPZoneFromScreenDrag(const FVector2f& StartScreenPosition, const FVector2f& EndScreenPosition);
void CreateMCPZone(UEdGraph* Graph, UBlueprint* Blueprint, const FVector2f& GraphStart, const FVector2f& GraphEnd) const;
void RemoveMCPZones(UBlueprint* Blueprint) const;
bool bMCPZoneModeActive = false;
TWeakPtr<FBlueprintEditor> ActiveMCPZoneEditor;
TSharedPtr<IInputProcessor> MCPZoneInputProcessor;
};

View File

@ -0,0 +1,129 @@
using System;
using System.IO;
using System.Collections.Generic;
using UnrealBuildTool;
public class UEBlueprintMCPEditor : ModuleRules
{
public UEBlueprintMCPEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"UMG",
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"UnrealEd",
"UMGEditor",
"BlueprintGraph",
"KismetCompiler",
"Kismet",
"GraphEditor",
"InputCore",
"Slate",
"SlateCore",
"EditorSubsystem",
"ToolMenus",
"AssetTools",
"Json",
"JsonUtilities",
"AssetRegistry",
"HTTP",
"Niagara",
"NiagaraCore",
"NiagaraEditor",
"ImageWrapper",
"RenderCore",
"RHI",
"MovieScene",
"MovieSceneTracks",
});
// ---- Optional Voxel integration -----------------------------------
// voxel_graph_op needs Voxel's editor modules. Voxel is a heavy third-party
// plugin most projects don't have, so it is wired in ONLY when present. The
// C++ side guards everything behind WITH_MCP_VOXEL and compiles to stubs at 0.
// env UE_MCP_WITH_VOXEL = "1" -> force on (errors if the plugin is missing)
// env UE_MCP_WITH_VOXEL = "0" -> force off
// unset -> auto-detect
string voxelEnv = Environment.GetEnvironmentVariable("UE_MCP_WITH_VOXEL");
string voxelPrivate = (voxelEnv == "0") ? null : FindVoxelGraphEditorPrivate(Target);
bool voxelEnabled = voxelPrivate != null && voxelEnv != "0";
if (voxelEnabled)
{
// Reach a few VoxelGraphEditor private headers (node classes). We only use
// inline/virtual members, so no private link symbols cross the boundary.
PrivateIncludePaths.Add(voxelPrivate);
PrivateDependencyModuleNames.AddRange(new string[]
{
"VoxelCore",
"VoxelCoreEditor",
"VoxelGraph",
"VoxelGraphEditor",
});
PublicDefinitions.Add("WITH_MCP_VOXEL=1");
}
else
{
if (voxelEnv == "1")
{
throw new BuildException(
"UE_MCP_WITH_VOXEL=1 but the Voxel plugin (Source/VoxelGraphEditor/Private) " +
"could not be located. Install the Voxel plugin into this project or the engine, " +
"or unset UE_MCP_WITH_VOXEL.");
}
PublicDefinitions.Add("WITH_MCP_VOXEL=0");
}
}
// Locate <Voxel>/Source/VoxelGraphEditor/Private by scanning the project's and
// engine's plugin folders for Voxel.uplugin. Returns null if Voxel isn't installed.
private string FindVoxelGraphEditorPrivate(ReadOnlyTargetRules Target)
{
var roots = new List<string>();
if (Target.ProjectFile != null)
{
roots.Add(Path.Combine(Target.ProjectFile.Directory.FullName, "Plugins"));
}
// The Plugins folder this plugin itself lives in (covers <Project>/Plugins/Voxel).
roots.Add(Path.GetFullPath(Path.Combine(ModuleDirectory, "..", "..", "..")));
try { roots.Add(Path.Combine(EngineDirectory, "Plugins")); } catch { }
foreach (string root in roots)
{
string hit = FindVoxelUnder(root, 0);
if (hit != null) return hit;
}
return null;
}
// Bounded, fault-tolerant recursive search (GetFiles+AllDirectories aborts wholesale
// on a single inaccessible subdir, so we walk manually and swallow per-dir errors).
private string FindVoxelUnder(string dir, int depth)
{
if (depth > 5 || string.IsNullOrEmpty(dir) || !Directory.Exists(dir)) return null;
try
{
string upl = Path.Combine(dir, "Voxel.uplugin");
if (File.Exists(upl))
{
string priv = Path.Combine(dir, "Source", "VoxelGraphEditor", "Private");
if (Directory.Exists(priv)) return priv;
}
foreach (string sub in Directory.GetDirectories(dir))
{
string hit = FindVoxelUnder(sub, depth + 1);
if (hit != null) return hit;
}
}
catch { }
return null;
}
}