From 93d4ed203819f29d20754c6491cf082fc4bc4a5e Mon Sep 17 00:00:00 2001 From: jyelon Date: Sun, 8 Mar 2026 05:00:07 -0400 Subject: [PATCH] More MCP refactoring --- .../BlueprintMCP/Private/MCPHandlers_Graphs.h | 21 +-- .../Private/MCPHandlers_MaterialMutation.h | 140 ++---------------- .../BlueprintMCP/Private/MCPHandlers_Params.h | 2 + .../Private/MCPHandlers_StateMachine.h | 2 + .../Private/MCPHandlers_Validation.h | 19 +-- .../Private/MCPHandlers_Variables.h | 4 + .../Source/BlueprintMCP/Public/MCPUtils.h | 56 +++++-- 7 files changed, 70 insertions(+), 174 deletions(-) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Graphs.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Graphs.h index 14216922..89c2ad40 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Graphs.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Graphs.h @@ -91,7 +91,9 @@ public: *Blueprint, *OldParentName, *NewParentClassObj->GetName()); // Perform reparent + BP->PreEditChange(nullptr); BP->ParentClass = NewParentClassObj; + BP->PostEditChange(); // Refresh all nodes to pick up new parent's functions/variables FBlueprintEditorUtils::RefreshAllNodes(BP); @@ -187,24 +189,7 @@ public: EBlueprintType BlueprintTypeEnum = BPTYPE_Normal; if (!BlueprintType.IsEmpty()) { - if (BlueprintType == TEXT("Interface")) - { - BlueprintTypeEnum = BPTYPE_Interface; - } - else if (BlueprintType == TEXT("FunctionLibrary")) - { - BlueprintTypeEnum = BPTYPE_FunctionLibrary; - } - else if (BlueprintType == TEXT("MacroLibrary")) - { - BlueprintTypeEnum = BPTYPE_MacroLibrary; - } - else if (BlueprintType != TEXT("Normal")) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Invalid blueprintType '%s'. Valid values: Normal, Interface, FunctionLibrary, MacroLibrary"), - *BlueprintType)); - } + if (!MCPUtils::StringToEnum(BlueprintType, BlueprintTypeEnum, Result, TEXT("BPTYPE_"))) return; } // For Interface type, parent must be UInterface diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialMutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialMutation.h index 0236b896..7525b542 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialMutation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialMutation.h @@ -161,40 +161,13 @@ public: bool bSaved = MCPUtils::SaveMaterialPackage(MaterialObj); - // Map domain back to string for response - auto DomainToString = [](EMaterialDomain InDomain) -> FString - { - switch (InDomain) - { - case MD_Surface: return TEXT("Surface"); - case MD_DeferredDecal: return TEXT("DeferredDecal"); - case MD_LightFunction: return TEXT("LightFunction"); - case MD_Volume: return TEXT("Volume"); - case MD_PostProcess: return TEXT("PostProcess"); - case MD_UI: return TEXT("UI"); - default: return TEXT("Surface"); - } - }; - - auto BlendModeToString = [](EBlendMode Mode) -> FString - { - switch (Mode) - { - case BLEND_Opaque: return TEXT("Opaque"); - case BLEND_Masked: return TEXT("Masked"); - case BLEND_Translucent: return TEXT("Translucent"); - case BLEND_Additive: return TEXT("Additive"); - case BLEND_Modulate: return TEXT("Modulate"); - default: return TEXT("Opaque"); - } - }; UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material '%s' (saved: %s)"), *Name, bSaved ? TEXT("true") : TEXT("false")); Result->SetStringField(TEXT("path"), MaterialObj->GetPathName()); - Result->SetStringField(TEXT("domain"), DomainToString(MaterialObj->MaterialDomain)); - Result->SetStringField(TEXT("blendMode"), BlendModeToString(MaterialObj->BlendMode)); + Result->SetStringField(TEXT("domain"), MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_"))); + Result->SetStringField(TEXT("blendMode"), MCPUtils::EnumToString(MaterialObj->BlendMode, TEXT("BLEND_"))); Result->SetBoolField(TEXT("twoSided"), MaterialObj->TwoSided != 0); Result->SetBoolField(TEXT("saved"), bSaved); } @@ -240,72 +213,14 @@ public: FString OldValue; FString NewValue; - // Helper lambdas for converting enum values to strings - auto DomainToString = [](EMaterialDomain Domain) -> FString - { - switch (Domain) - { - case MD_Surface: return TEXT("Surface"); - case MD_DeferredDecal: return TEXT("DeferredDecal"); - case MD_LightFunction: return TEXT("LightFunction"); - case MD_Volume: return TEXT("Volume"); - case MD_PostProcess: return TEXT("PostProcess"); - case MD_UI: return TEXT("UI"); - default: return TEXT("Unknown"); - } - }; - - auto BlendModeToString = [](EBlendMode Mode) -> FString - { - switch (Mode) - { - case BLEND_Opaque: return TEXT("Opaque"); - case BLEND_Masked: return TEXT("Masked"); - case BLEND_Translucent: return TEXT("Translucent"); - case BLEND_Additive: return TEXT("Additive"); - case BLEND_Modulate: return TEXT("Modulate"); - default: return TEXT("Unknown"); - } - }; - - auto ShadingModelToString = [](EMaterialShadingModel Model) -> FString - { - switch (Model) - { - case MSM_Unlit: return TEXT("Unlit"); - case MSM_DefaultLit: return TEXT("DefaultLit"); - case MSM_Subsurface: return TEXT("Subsurface"); - case MSM_PreintegratedSkin: return TEXT("PreintegratedSkin"); - case MSM_ClearCoat: return TEXT("ClearCoat"); - case MSM_SubsurfaceProfile: return TEXT("SubsurfaceProfile"); - case MSM_TwoSidedFoliage: return TEXT("TwoSidedFoliage"); - case MSM_Hair: return TEXT("Hair"); - case MSM_Cloth: return TEXT("Cloth"); - case MSM_Eye: return TEXT("Eye"); - default: return TEXT("DefaultLit"); - } - }; - if (Property == TEXT("domain")) { FString ValueStr = Json->GetStringField(TEXT("value")); - OldValue = DomainToString(MaterialObj->MaterialDomain); + OldValue = MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_")); - EMaterialDomain NewDomain = MaterialObj->MaterialDomain; - if (ValueStr == TEXT("Surface")) NewDomain = MD_Surface; - else if (ValueStr == TEXT("DeferredDecal")) NewDomain = MD_DeferredDecal; - else if (ValueStr == TEXT("LightFunction")) NewDomain = MD_LightFunction; - else if (ValueStr == TEXT("Volume")) NewDomain = MD_Volume; - else if (ValueStr == TEXT("PostProcess")) NewDomain = MD_PostProcess; - else if (ValueStr == TEXT("UI")) NewDomain = MD_UI; - else - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Invalid domain '%s'. Valid values: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"), - *ValueStr)); - } - - NewValue = ValueStr; + EMaterialDomain NewDomain; + if (!MCPUtils::StringToEnum(ValueStr, NewDomain, Result, TEXT("MD_"))) return; + NewValue = MCPUtils::EnumToString(NewDomain, TEXT("MD_")); if (!DryRun) { @@ -317,22 +232,11 @@ public: else if (Property == TEXT("blendMode")) { FString ValueStr = Json->GetStringField(TEXT("value")); - OldValue = BlendModeToString(MaterialObj->BlendMode); + OldValue = MCPUtils::EnumToString(MaterialObj->BlendMode, TEXT("BLEND_")); - EBlendMode NewBlend = MaterialObj->BlendMode; - if (ValueStr == TEXT("Opaque")) NewBlend = BLEND_Opaque; - else if (ValueStr == TEXT("Masked")) NewBlend = BLEND_Masked; - else if (ValueStr == TEXT("Translucent")) NewBlend = BLEND_Translucent; - else if (ValueStr == TEXT("Additive")) NewBlend = BLEND_Additive; - else if (ValueStr == TEXT("Modulate")) NewBlend = BLEND_Modulate; - else - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Invalid blendMode '%s'. Valid values: Opaque, Masked, Translucent, Additive, Modulate"), - *ValueStr)); - } - - NewValue = ValueStr; + EBlendMode NewBlend; + if (!MCPUtils::StringToEnum(ValueStr, NewBlend, Result, TEXT("BLEND_"))) return; + NewValue = MCPUtils::EnumToString(NewBlend, TEXT("BLEND_")); if (!DryRun) { @@ -357,27 +261,11 @@ public: else if (Property == TEXT("shadingModel")) { FString ValueStr = Json->GetStringField(TEXT("value")); - OldValue = ShadingModelToString(MaterialObj->GetShadingModels().GetFirstShadingModel()); + OldValue = MCPUtils::EnumToString(MaterialObj->GetShadingModels().GetFirstShadingModel(), TEXT("MSM_")); - EMaterialShadingModel NewModel = MSM_DefaultLit; - if (ValueStr == TEXT("Unlit")) NewModel = MSM_Unlit; - else if (ValueStr == TEXT("DefaultLit")) NewModel = MSM_DefaultLit; - else if (ValueStr == TEXT("Subsurface")) NewModel = MSM_Subsurface; - else if (ValueStr == TEXT("PreintegratedSkin")) NewModel = MSM_PreintegratedSkin; - else if (ValueStr == TEXT("ClearCoat")) NewModel = MSM_ClearCoat; - else if (ValueStr == TEXT("SubsurfaceProfile")) NewModel = MSM_SubsurfaceProfile; - else if (ValueStr == TEXT("TwoSidedFoliage")) NewModel = MSM_TwoSidedFoliage; - else if (ValueStr == TEXT("Hair")) NewModel = MSM_Hair; - else if (ValueStr == TEXT("Cloth")) NewModel = MSM_Cloth; - else if (ValueStr == TEXT("Eye")) NewModel = MSM_Eye; - else - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Invalid shadingModel '%s'. Valid values: Unlit, DefaultLit, Subsurface, PreintegratedSkin, ClearCoat, SubsurfaceProfile, TwoSidedFoliage, Hair, Cloth, Eye"), - *ValueStr)); - } - - NewValue = ValueStr; + EMaterialShadingModel NewModel; + if (!MCPUtils::StringToEnum(ValueStr, NewModel, Result, TEXT("MSM_"))) return; + NewValue = MCPUtils::EnumToString(NewModel, TEXT("MSM_")); if (!DryRun) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.h index 4ea0aae7..39540b32 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.h @@ -112,7 +112,9 @@ public: { if (PinInfo.IsValid() && PinInfo->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase)) { + EntryNode->PreEditChange(nullptr); PinInfo->PinType = NewPinType; + EntryNode->PostEditChange(); bPinFound = true; break; } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_StateMachine.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_StateMachine.h index ac6b0928..281492ed 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_StateMachine.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_StateMachine.h @@ -332,6 +332,7 @@ public: // Update properties int32 ChangedCount = 0; + TransNode->PreEditChange(nullptr); if (Json->HasField(TEXT("crossfadeDuration"))) { @@ -363,6 +364,7 @@ public: { return MCPUtils::MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional")); } + TransNode->PostEditChange(); // Compile and save FKismetEditorUtilities::CompileBlueprint(AnimBP); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h index 33f45328..505afb6f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Validation.h @@ -103,15 +103,7 @@ public: WarningsArr.Add(MakeShared(Msg)); } - FString StatusStr; - switch (BP->Status) - { - case BS_UpToDate: StatusStr = TEXT("UpToDate"); break; - case BS_Dirty: StatusStr = TEXT("Dirty"); break; - case BS_Error: StatusStr = TEXT("Error"); break; - case BS_Unknown: StatusStr = TEXT("Unknown"); break; - default: StatusStr = FString::Printf(TEXT("Status_%d"), (int32)BP->Status); break; - } + FString StatusStr = MCPUtils::EnumToString((EBlueprintStatus)BP->Status, TEXT("BS_")); bool bIsValid = (BP->Status == BS_UpToDate) && (ErrorsArr.Num() == 0); @@ -129,17 +121,14 @@ public: virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override { MCPAssets Finder; - Finder.Scan().Scan(); + Finder.Scan().Scan().Errors(Result); if (!Blueprint.IsEmpty()) { - if (!Finder.Exact(Blueprint).ETwo().Info() || Finder.AllData().IsEmpty()) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blueprint '%s' not found."), *Blueprint)); - } + if (!Finder.Exact(Blueprint).ENone().ETwo().Info()) return; } else { - Finder.Substring(Query).Info(); + if (!Finder.Substring(Query).Info()) return; } const TArray& MatchingAssets = Finder.AllData(); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.h index 0de731cc..91891a63 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.h @@ -158,6 +158,7 @@ public: } // Directly modify the variable type in the description array. + BP->PreEditChange(nullptr); for (FBPVariableDescription& Var : BP->NewVariables) { if (Var.VarName == FName(*Variable)) @@ -166,6 +167,7 @@ public: break; } } + BP->PostEditChange(); // Save bool bSaved = MCPUtils::SaveBlueprintPackage(BP); @@ -563,6 +565,8 @@ public: UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetVariableMetadata on '%s.%s' — %d field(s) changed"), *Blueprint, *Variable, Changes.Num()); + BP->PreEditChange(nullptr); + BP->PostEditChange(); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); bool bSaved = MCPUtils::SaveBlueprintPackage(BP); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 85e87627..09e4ec07 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -67,7 +67,19 @@ public: } }; -struct MCPErrorCallback; +// ----- Error callback ----- + +struct MCPErrorCallback +{ + TFunction Func; + + + MCPErrorCallback(std::nullptr_t); + MCPErrorCallback(FString& OutError); + MCPErrorCallback(FJsonObject* Result); + + void SetError(const FString& Msg) const { Func(Msg); } +}; // Stateless utility functions used by MCP handlers and the MCP server. // This is effectively a namespace — all methods are static. @@ -97,6 +109,34 @@ public: static void CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest); static FString UrlDecode(const FString& EncodedString); + // ----- Enum helpers ----- + // Convert enum value to string. If Prefix is specified, strip "Prefix_" from the front. + template + static FString EnumToString(T Value, const FString& Prefix = FString()) + { + UEnum* Enum = StaticEnum(); + FString Full = Enum->GetNameStringByValue((int64)Value); + if (!Prefix.IsEmpty() && Full.StartsWith(Prefix)) + return Full.Mid(Prefix.Len()); + return Full; + } + + // Convert string to enum value. If Prefix is specified, prepend it before lookup. + // Returns false and sets error if the string doesn't match any value. + template + static bool StringToEnum(const FString& Str, T& OutValue, MCPErrorCallback Error, const FString& Prefix = FString()) + { + UEnum* Enum = StaticEnum(); + int64 Value = Enum->GetValueByNameString(Prefix + Str); + if (Value == INDEX_NONE) + { + Error.SetError(FString::Printf(TEXT("Invalid value '%s' for %s"), *Str, *Enum->GetName())); + return false; + } + OutValue = (T)Value; + return true; + } + // ----- Blueprint helpers ----- static TArray AllGraphs(UBlueprint* BP); static TArray AllGraphsNamed(UBlueprint* BP, const FString& Name); @@ -155,17 +195,3 @@ public: private: static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json); }; - -// ----- Error callback ----- - -struct MCPErrorCallback -{ - TFunction Func; - - - MCPErrorCallback(std::nullptr_t); - MCPErrorCallback(FString& OutError); - MCPErrorCallback(FJsonObject* Result); - - void SetError(const FString& Msg) const { Func(Msg); } -};