diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Asset_Rename.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Asset_Rename.h index 2c39a76f..017a29fa 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Asset_Rename.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Asset_Rename.h @@ -38,18 +38,18 @@ public: UObject* AssetObj = Assets.Object(); // Parse new path into package path and asset name - FString NewPackagePath, NewAssetName; - if (!MCPUtils::SplitAssetPath(NewPath, NewPackagePath, NewAssetName)) + FString NewPackagePath = FPackageName::GetLongPackagePath(NewPath); + FString NewAssetName = FPackageName::GetShortName(NewPath); + if (NewPackagePath.IsEmpty()) { // No slash — just a new name, keep the same directory - FString OldPackagePath, OldAssetName; - if (!MCPUtils::SplitAssetPath(AssetPath, OldPackagePath, OldAssetName)) + NewPackagePath = FPackageName::GetLongPackagePath(AssetPath); + NewAssetName = NewPath; + if (NewPackagePath.IsEmpty()) { Result.Appendf(TEXT("ERROR: Cannot determine directory from AssetPath '%s'\n"), *AssetPath); return; } - NewPackagePath = OldPackagePath; - NewAssetName = NewPath; } // Perform the rename with reference fixup diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Create.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Create.h index e3d046f3..d889ece3 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Create.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Material_Create.h @@ -78,7 +78,7 @@ public: MCPUtils::PostEdit(Chain); - bool bSaved = MCPUtils::SaveMaterialPackage(MaterialObj); + bool bSaved = MCPUtils::SaveGenericPackage(MaterialObj); Result.Appendf(TEXT("Created %s\n"), *MaterialObj->GetPathName()); Result.Appendf(TEXT("Domain: %s\n"), *MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_"))); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h index 92c57b3f..37660b38 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h @@ -30,12 +30,12 @@ public: for (UClass* Class : MCPUtils::CollectHandlerClasses()) { - FString ToolName = MCPUtils::GetToolName(Class); + FString ToolName = MCPUtils::GetHandlerName(Class); if (!ToolName.ToLower().Contains(QueryLower)) continue; // Blank line between groups - FString Group = MCPUtils::GetToolGroup(Class); + FString Group = MCPUtils::GetHandlerGroup(Class); if (Group != PrevGroup) { if (!PrevGroup.IsEmpty()) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp index 36b81282..391adfbb 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp @@ -12,38 +12,6 @@ #include "K2Node_FunctionEntry.h" #include "MaterialGraph/MaterialGraphNode.h" -static FString WrapText(const FString& Text, int32 ColLimit, const FString& Prefix) -{ - FString Clean = Text; - Clean.ReplaceInline(TEXT("\r\n"), TEXT("\n")); - TArray Words; - Clean.ParseIntoArrayWS(Words); - - TStringBuilder<1024> Result; - int32 Col = 0; - for (const FString& Word : Words) - { - if (Col > 0 && Col + 1 + Word.Len() > ColLimit) - { - Result.Append(TEXT("\n")); - Col = 0; - } - if (Col == 0) - { - Result.Append(Prefix); - Col = Prefix.Len(); - } - else - { - Result.Append(TEXT(" ")); - Col += 1; - } - Result.Append(Word); - Col += Word.Len(); - } - return Result.ToString(); -} - FlxBlueprintExporter::FlxBlueprintExporter(UEdGraph* InGraph) : Graph(InGraph) { @@ -347,7 +315,7 @@ void FlxBlueprintExporter::EmitDetails() if (Node->IsA()) continue; if (Node->IsA()) continue; if (Node->IsA()) continue; - Details.Appendf(TEXT("details %s\n"), *MCPUtils::FormatName(Node)); + Details.Appendf(TEXT("\ndetails %s\n"), *MCPUtils::FormatName(Node)); Details.Appendf(TEXT(" pos %d, %d\n"), Node->NodePosX, Node->NodePosY); EmitMaterialProperties(Node, Details, false); @@ -369,7 +337,7 @@ void FlxBlueprintExporter::EmitComments() Output.Appendf(TEXT("\ncomment %s:\n"), *MCPUtils::FormatName(CommentNode)); // Emit wrapped, indented body. - Output.Append(WrapText(CommentNode->NodeComment, 70, TEXT(" - "))); + Output.Append(MCPUtils::WrapText(CommentNode->NodeComment, 70, TEXT(" - "))); Output.Append(TEXT("\n")); // Find contained nodes. diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp index c6010171..f2132c72 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp @@ -258,7 +258,9 @@ TStatId UMCPServer::GetStatId() const FString UMCPServer::HandleRequest(const FString& Line) { // Turn the request string into a JSON tree. - TSharedPtr Request = MCPUtils::ParseBodyJson(Line); + TSharedPtr Request; + TSharedRef> Reader = TJsonReaderFactory<>::Create(Line); + FJsonSerializer::Deserialize(Reader, Request); if (!Request.IsValid()) { return TEXT("Request is not valid JSON"); @@ -415,6 +417,6 @@ void UMCPServer::BuildMCPHandlerRegistry() { for (UClass* Class : MCPUtils::CollectHandlerClasses()) { - MCPHandlerRegistry.FindOrAdd(MCPUtils::GetToolName(Class)) = Class; + MCPHandlerRegistry.FindOrAdd(MCPUtils::GetHandlerName(Class)) = Class; } } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index 68afcc35..8378194b 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -295,63 +295,63 @@ FString MCPUtils::FormatNodeTitle(const UEdGraphNode *Node) // JSON helpers // ============================================================ -TSharedPtr MCPUtils::ParseBodyJson(const FString& Body) +// ============================================================ +// Text formatting +// ============================================================ + +FString MCPUtils::WrapText(const FString& Text, int32 ColLimit, const FString& Prefix) { - TSharedPtr JsonObj; - TSharedRef> Reader = TJsonReaderFactory<>::Create(Body); - FJsonSerializer::Deserialize(Reader, JsonObj); - return JsonObj; -} + FString Clean = Text; + Clean.ReplaceInline(TEXT("\r\n"), TEXT("\n")); + TArray Words; + Clean.ParseIntoArrayWS(Words); - -FString MCPUtils::UrlDecode(const FString& EncodedString) -{ - FString Result; - Result.Reserve(EncodedString.Len()); - - for (int32 i = 0; i < EncodedString.Len(); ++i) + TStringBuilder<1024> Result; + int32 Col = 0; + for (const FString& Word : Words) { - TCHAR C = EncodedString[i]; - if (C == TEXT('+')) + if (Col > 0 && Col + 1 + Word.Len() > ColLimit) { - Result += TEXT(' '); + Result.Append(TEXT("\n")); + Col = 0; } - else if (C == TEXT('%') && i + 2 < EncodedString.Len()) + if (Col == 0) { - FString HexStr = EncodedString.Mid(i + 1, 2); - int32 HexVal = 0; - bool bValid = true; - for (TCHAR H : HexStr) - { - HexVal <<= 4; - if (H >= TEXT('0') && H <= TEXT('9')) - HexVal += H - TEXT('0'); - else if (H >= TEXT('a') && H <= TEXT('f')) - HexVal += 10 + H - TEXT('a'); - else if (H >= TEXT('A') && H <= TEXT('F')) - HexVal += 10 + H - TEXT('A'); - else - { - bValid = false; - break; - } - } - if (bValid) - { - Result += (TCHAR)HexVal; - i += 2; - } - else - { - Result += C; - } + Result.Append(Prefix); + Col = Prefix.Len(); } else { - Result += C; + Result.Append(TEXT(" ")); + Col += 1; } + Result.Append(Word); + Col += Word.Len(); } - return Result; + return Result.ToString(); +} + +// ============================================================ +// Enum helpers +// ============================================================ + +FString MCPUtils::EnumToString(UEnum* Enum, int64 Value, const FString& Prefix) +{ + FString Full = Enum->GetNameStringByValue(Value); + if (!Prefix.IsEmpty() && Full.StartsWith(Prefix)) + return Full.Mid(Prefix.Len()); + return Full; +} + +bool MCPUtils::StringToEnum(UEnum* Enum, const FString& Str, int64& OutValue, MCPErrorCallback Error, const FString& Prefix) +{ + OutValue = Enum->GetValueByNameString(Prefix + Str); + if (OutValue == INDEX_NONE) + { + Error.SetError(FString::Printf(TEXT("Invalid value '%s' for %s"), *Str, *Enum->GetName())); + return false; + } + return true; } // ============================================================ @@ -382,37 +382,6 @@ TArray MCPUtils::AllNodes(UBlueprint* BP) return Nodes; } -TArray> MCPUtils::AllGraphNamesJson(UBlueprint* BP) -{ - TArray> Result; - for (UEdGraph* Graph : AllGraphs(BP)) - Result.Add(MakeShared(FormatName(Graph))); - return Result; -} - -UEdGraphNode* MCPUtils::FindNodeByGuid( - UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph) -{ - FGuid TargetGuid; - FGuid::Parse(GuidString, TargetGuid); - - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - for (UEdGraph* Graph : AllGraphs) - { - for (UEdGraphNode* Node : Graph->Nodes) - { - if (Node && Node->NodeGuid == TargetGuid) - { - if (OutGraph) *OutGraph = Graph; - return Node; - } - } - } - return nullptr; -} - bool MCPUtils::SaveBlueprintPackage(UBlueprint* BP) { UPackage* Package = BP->GetPackage(); @@ -946,12 +915,6 @@ void MCPUtils::FormatMaterialParameter(FStringBuilderBase& Result, const FMateri } } -bool MCPUtils::SaveMaterialPackage(UMaterial* Material) -{ - if (!Material) return false; - return SaveGenericPackage(Material); -} - bool MCPUtils::SaveGenericPackage(UObject* Asset) { if (!Asset) return false; @@ -1339,15 +1302,15 @@ TArray MCPUtils::CollectHandlerClasses() if (!Class->ImplementsInterface(UMCPHandler::StaticClass())) continue; Result.Add(Class); } - Result.Sort([](UClass& A, UClass& B) { return GetToolName(&A) < GetToolName(&B); }); + Result.Sort([](UClass& A, UClass& B) { return GetHandlerName(&A) < GetHandlerName(&B); }); return Result; } // ============================================================ -// GetToolName — derive tool name from handler class name +// GetHandlerName — derive tool name from handler class name // ============================================================ -FString MCPUtils::GetToolName(UClass* HandlerClass) +FString MCPUtils::GetHandlerName(UClass* HandlerClass) { FString Name = HandlerClass->GetName(); // Strip "MCP_" prefix @@ -1361,10 +1324,10 @@ FString MCPUtils::GetToolName(UClass* HandlerClass) } // ============================================================ -// GetToolGroup — derive group name from handler class name +// GetHandlerGroup — derive group name from handler class name // ============================================================ -FString MCPUtils::GetToolGroup(UClass* HandlerClass) +FString MCPUtils::GetHandlerGroup(UClass* HandlerClass) { FString Name = HandlerClass->GetName(); // Strip "MCP_" prefix @@ -1400,28 +1363,6 @@ FString MCPUtils::FormatPropertyType(FProperty* Prop) // GetTemplate // ============================================================ -UObject* MCPUtils::GetTemplate(UObject* Obj, MCPErrorCallback Error) -{ - if (!Obj) - { - Error.SetError(TEXT("Object is null")); - return nullptr; - } - - // Blueprint → operate on the Class Default Object. - if (UBlueprint* BP = Cast(Obj)) - { - if (!BP->GeneratedClass) - { - Error.SetError(FString::Printf(TEXT("Blueprint '%s' has no GeneratedClass"), *Obj->GetName())); - return nullptr; - } - return BP->GeneratedClass->GetDefaultObject(); - } - - return Obj; -} - // ============================================================ // FindPropertyByName // ============================================================ @@ -1525,7 +1466,11 @@ void MCPUtils::FormatCommandHelp(UClass* HandlerClass, FStringBuilderBase& Resul const IMCPHandler* Handler = Cast(HandlerClass->GetDefaultObject()); if (!Handler) return; - FString ToolName = GetToolName(HandlerClass); + FString ToolName = GetHandlerName(HandlerClass); + + Result.Append(TEXT("\n")); + Result.Append(WrapText(Handler->GetDescription(), 80, TEXT("// "))); + Result.Append(TEXT("\n")); // Command signature line Result.Append(ToolName); @@ -1540,8 +1485,7 @@ void MCPUtils::FormatCommandHelp(UClass* HandlerClass, FStringBuilderBase& Resul } Result.Append(TEXT(")\n")); - // Description and parameter details - Result.Appendf(TEXT(" %s\n"), *Handler->GetDescription()); + // parameter details for (TFieldIterator PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt) { FProperty* Prop = *PropIt; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 85230dc8..06107ead 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -91,8 +91,6 @@ struct MCPErrorCallback MCPErrorCallback(std::nullptr_t); MCPErrorCallback(FString& OutError); - MCPErrorCallback(FJsonObject* Result); - MCPErrorCallback(const TSharedRef& Result) : MCPErrorCallback(&*Result) {} MCPErrorCallback(FStringBuilderBase& OutResult); void SetError(const FString& Msg) const { Func(Msg); } @@ -165,66 +163,21 @@ public: static FString FormatPinType(UEdGraphPin* Pin); static FString FormatNodeTitle(const UEdGraphNode *Node); - // ----- Asset path helpers ----- - // Splits "/Game/Foo/Bar" into PackagePath="/Game/Foo" and AssetName="Bar". - // Returns false if the path has no slash or the asset name is empty. - static bool SplitAssetPath(const FString& AssetPath, FString& OutPackagePath, FString& OutAssetName) - { - int32 LastSlash; - if (!AssetPath.FindLastChar('/', LastSlash)) - { - return false; - } - OutPackagePath = AssetPath.Left(LastSlash); - OutAssetName = AssetPath.Mid(LastSlash + 1); - return !OutAssetName.IsEmpty(); - } - - // ----- JSON helpers ----- - static TSharedPtr ParseBodyJson(const FString& Body); - static FString UrlDecode(const FString& EncodedString); - // ----- Enum helpers ----- - // Convert enum value to string. If Prefix is specified, strip "Prefix_" from the front. + static FString EnumToString(UEnum* Enum, int64 Value, const FString& Prefix = FString()); + static bool StringToEnum(UEnum* Enum, const FString& Str, int64& OutValue, MCPErrorCallback Error, const FString& Prefix = FString()); + template static FString EnumToString(TEnumAsByte Value, const FString& Prefix = FString()) - { - return EnumToString((T)Value, Prefix); - } + { return EnumToString(StaticEnum(), (int64)Value, Prefix); } 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; - } + { return EnumToString(StaticEnum(), (int64)Value, Prefix); } - // 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; - } - - static bool StringToBool(const FString& Str, bool& OutValue, MCPErrorCallback Error) - { - if (Str.Equals(TEXT("true"), ESearchCase::IgnoreCase)) { OutValue = true; return true; } - if (Str.Equals(TEXT("false"), ESearchCase::IgnoreCase)) { OutValue = false; return true; } - Error.SetError(FString::Printf(TEXT("Invalid bool value '%s' (expected 'true' or 'false')"), *Str)); - return false; - } + { int64 V; if (!StringToEnum(StaticEnum(), Str, V, Error, Prefix)) return false; OutValue = (T)V; return true; } // ----- Blueprint helpers ----- static TArray AllGraphs(UBlueprint* BP); @@ -247,17 +200,15 @@ public: Result.Add(Typed); return Result; } - static TArray> AllGraphNamesJson(UBlueprint* BP); - static UEdGraphNode* FindNodeByGuid(UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph = nullptr); static bool SaveBlueprintPackage(UBlueprint* BP); // ----- Type resolution ----- static UClass* FindClassByName(const FString& ClassName); static bool ResolveTypeFromString(const FString& TypeName, FEdGraphPinType& OutPinType, MCPErrorCallback Error); + static FString FormatPropertyType(FProperty* Prop); // ----- Material helpers ----- static void EnsureMaterialGraph(UMaterial* Material); - static bool SaveMaterialPackage(UMaterial* Material); static bool SaveGenericPackage(UObject* Asset); // If the material editor has a transient preview copy of this material, @@ -287,33 +238,27 @@ public: static void FormatMaterialParameter(FStringBuilderBase& Result, const FMaterialParameterInfo& Info, const FMaterialParameterMetadata& Meta); // ----- Editable template ----- - // Given an object, returns the appropriate template object for generic - // property editing, or nullptr if the type isn't whitelisted. - // UBlueprint → CDO; UMaterial, UActorComponent, etc. → as-is. - static UObject* GetTemplate(UObject* Obj, MCPErrorCallback Error); static TArray SearchProperties(UObject* Obj, const FString& Query, EPropertyFlags Flags, bool bLocal); - static FProperty* FindPropertyByName(UObject* Obj, const FString& Name, MCPErrorCallback Error = nullptr); static FString GetPropertyValueText(UObject* Container, FProperty* Prop); static bool SetPropertyValueText(UObject* Container, FProperty* Prop, const FString& Value, MCPErrorCallback Error = nullptr); static bool SetPropertyValueText(void* Container, FProperty* Prop, const FString& Value, UObject* Owner, MCPErrorCallback Error = nullptr); - // ----- Property population ----- + // ----- JSON helpers ----- static FString PropertyNameToJsonKey(const FString& PropName); static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr& JsonValue, MCPErrorCallback Error); static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json, MCPErrorCallback Error); - // ----- Handler discovery ----- - // Collect all concrete IMCPHandler classes, sorted by tool name. - static TArray CollectHandlerClasses(); + // ----- Text formatting ----- + static FString WrapText(const FString& Text, int32 ColLimit, const FString& Prefix); - // ----- Command help ----- - // Derive tool name from handler class name: "MCPHandler_FooBar" → "FooBar" - static FString GetToolName(UClass* HandlerClass); - static FString GetToolGroup(UClass* HandlerClass); - static FString FormatPropertyType(FProperty* Prop); + // ----- Handler discovery ----- + static TArray CollectHandlerClasses(); + static FString GetHandlerName(UClass* HandlerClass); + static FString GetHandlerGroup(UClass* HandlerClass); static void FormatCommandHelp(UClass* HandlerClass, FStringBuilderBase& Result); + private: static void SanitizeNameInPlace(FString& Name); static void AppendNumericSuffix(FString &Name, int32 N);