diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Blueprint_Create.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Blueprint_Create.h similarity index 52% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Blueprint_Create.h rename to Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Blueprint_Create.h index c4ec163d..b08ad7de 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Blueprint_Create.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Blueprint_Create.h @@ -5,8 +5,10 @@ #include "MCPHandler.h" #include "MCPFetcher.h" #include "MCPUtils.h" +#include "MCPTypes.h" #include "MCPPackageMaker.h" #include "Engine/Blueprint.h" +#include "Kismet/BlueprintFunctionLibrary.h" #include "Kismet2/KismetEditorUtilities.h" #include "Blueprint_Create.generated.h" @@ -21,14 +23,14 @@ class UMCP_Blueprint_Create : public UObject, public IMCPHandler GENERATED_BODY() public: - UPROPERTY(meta=(Description="Full asset path for the new Blueprint (e.g. '/Game/Blueprints/BP_MyActor')")) + UPROPERTY(meta=(Description="Full asset path for the new Blueprint")) FString AssetPath; - UPROPERTY(meta=(Description="Parent class: C++ class name or Blueprint package path")) + UPROPERTY(meta=(Description="Parent class, expressed as a type")) FString ParentClass; - UPROPERTY(meta=(Optional, Description="Blueprint type: Normal, Interface, FunctionLibrary, or MacroLibrary (default: Normal)")) - FString BlueprintType; + UPROPERTY(meta=(Optional, Description="Normal, Interface, FunctionLibrary, or MacroLibrary")) + TEnumAsByte BlueprintType = BPTYPE_Normal; virtual FString GetDescription() const override { @@ -40,35 +42,27 @@ public: MCPPackageMaker Maker(AssetPath); if (!Maker.Ok()) return; - // Resolve parent class — try C++ class first, then Blueprint package path - UClass* ParentClassObj = MCPUtils::FindClassByName(ParentClass); - - if (!ParentClassObj) - { - MCPFetcher F; - UBlueprint* ParentBP = F.Asset(ParentClass).Cast(); - if (ParentBP && ParentBP->GeneratedClass) - ParentClassObj = ParentBP->GeneratedClass; - } - - if (!ParentClassObj) - { - UMCPServer::Printf(TEXT("ERROR: Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint package path.\n"), - *ParentClass); - return; - } - - // Map blueprintType string to EBlueprintType - EBlueprintType BlueprintTypeEnum = BPTYPE_Normal; - if (!BlueprintType.IsEmpty()) - { - if (!MCPUtils::StringToEnum(BlueprintType, BlueprintTypeEnum, TEXT("BPTYPE_"))) return; - } - - // For Interface type, parent must be UInterface - if ((BlueprintTypeEnum == BPTYPE_Interface) && !ParentClassObj->IsChildOf(UInterface::StaticClass())) + // Resolve parent class based on blueprint type + UClass* ParentClassObj = nullptr; + switch (BlueprintType) { + case BPTYPE_Normal: + ParentClassObj = UMCPTypes::TextToOneObjectType(ParentClass); + if (!ParentClassObj) return; + break; + case BPTYPE_MacroLibrary: + ParentClassObj = UMCPTypes::TextToOneObjectType(ParentClass); + if (!ParentClassObj) return; + break; + case BPTYPE_Interface: ParentClassObj = UInterface::StaticClass(); + break; + case BPTYPE_FunctionLibrary: + ParentClassObj = UBlueprintFunctionLibrary::StaticClass(); + break; + default: + UMCPServer::Print(TEXT("ERROR: BlueprintType must be Normal, Interface, FunctionLibrary, or MacroLibrary\n")); + return; } // Create the package and Blueprint @@ -78,7 +72,7 @@ public: ParentClassObj, Maker.Package(), FName(*Maker.Name()), - BlueprintTypeEnum, + BlueprintType, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass() ); @@ -95,10 +89,6 @@ public: // Report result UMCPServer::Printf(TEXT("Created: %s\n"), *MCPUtils::FormatName(NewBP)); - UMCPServer::Printf(TEXT("Parent: %s\n"), *MCPUtils::FormatName(ParentClassObj)); - if (!bSaved) - UMCPServer::Print(TEXT("Warning: save failed\n")); - for (UEdGraph* Graph : MCPUtils::AllGraphs(NewBP)) - UMCPServer::Printf(TEXT("Graph: %s\n"), *MCPUtils::FormatName(Graph)); + if (!bSaved) UMCPServer::Print(TEXT("Warning: save failed\n")); } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h index 0365db79..2ba13fc4 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h @@ -82,5 +82,6 @@ public: EmitCommandList(true); UMCPServer::Printf(TEXT("\n")); MCPFetcher::PrintDocs(); + UMCPTypes::PrintDocs(); } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp index 2c8ef47d..c418cbf1 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp @@ -27,15 +27,23 @@ MCPFetcher::WalkFunc MCPFetcher::GetWalker(const FString& Step) void MCPFetcher::PrintDocs() { - UMCPServer::Print(TEXT("Some commands take a Path parameter. A Path starts with an asset\n")); - UMCPServer::Print(TEXT("package path (e.g. /Game/Widgets/WB_Hotkeys), followed by zero or\n")); - UMCPServer::Print(TEXT("more comma-separated steps that navigate into the asset:\n\n")); - UMCPServer::Print(TEXT(" graph — Find a named UEdGraph (blank name for material graphs)\n")); - UMCPServer::Print(TEXT(" node — Find a named UEdGraphNode within a graph or blueprint\n")); - UMCPServer::Print(TEXT(" pin — Find a named UEdGraphPin on a node\n")); - UMCPServer::Print(TEXT(" component — Find a named component in a Blueprint's SCS\n")); - UMCPServer::Print(TEXT(" levelblueprint — Get the level blueprint from a UWorld\n")); - UMCPServer::Print(TEXT("\nExample: /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self_Reference_03,pin:Result\n")); + UMCPServer::Print(TEXT("Most commands require you to specify a path. A path starts\n")); + UMCPServer::Print(TEXT("with an asset name, followed by comma-separated steps that\n")); + UMCPServer::Print(TEXT("navigate into the asset. Example:\n")); + UMCPServer::Print(TEXT("\n")); + UMCPServer::Print(TEXT(" /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self03,pin:Result\n")); + UMCPServer::Print(TEXT("\n")); + UMCPServer::Print(TEXT("The navigation steps supported are:\n")); + UMCPServer::Print(TEXT("\n")); + UMCPServer::Print(TEXT(" graph — move from a blueprint or material to a graph.\n")); + UMCPServer::Print(TEXT(" node — move from a graph to a graph node\n")); + UMCPServer::Print(TEXT(" pin — move from a graph node to a pin\n")); + UMCPServer::Print(TEXT(" component — move from a blueprint to a component\n")); + UMCPServer::Print(TEXT(" levelblueprint — move from a world to a blueprint\n")); + UMCPServer::Print(TEXT("\n")); + UMCPServer::Print(TEXT("It is often useful to use 'dump' commands to see the contents\n")); + UMCPServer::Print(TEXT("of an asset, so you know what objects exist to navigate into.\n")); + UMCPServer::Print(TEXT("\n")); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp index a549af4d..90bde8cd 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp @@ -6,6 +6,7 @@ #include "Materials/MaterialExpression.h" #include "MaterialGraph/MaterialGraphNode.h" #include "EdGraph/EdGraphPin.h" +#include "UObject/EnumProperty.h" static bool IsPinTypeProperty(FProperty* Prop) { @@ -26,18 +27,74 @@ FString MCPProperty::GetText() const return Result; } -bool MCPProperty::SetText(const FString& Value) +bool MCPProperty::TryParseEnum(UEnum* Enum, const FString& Text, int64 &OutValue) +{ + int Index = Enum->GetIndexByNameString(Text); + if (Index == INDEX_NONE) + { + FString Prefix = Enum->GenerateEnumPrefix(); + if (!Prefix.IsEmpty()) + { + Index = Enum->GetIndexByNameString(Prefix + TEXT("_") + Text); + } + } + if (Index == INDEX_NONE) + { + UMCPServer::Printf(TEXT("ERROR: '%s' is not a valid value for %s\n"), + *Text, *Enum->GetName()); + OutValue = 0; + return false; + } + else + { + OutValue = Enum->GetValueByIndex(Index); + return true; + } +} + +bool MCPProperty::TrySetText(const FString &Value) { void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); + + // Pin types get parsed by UMCPTypes. if (IsPinTypeProperty(Prop)) return UMCPTypes::TextToType(Value, *static_cast(ValuePtr)); - const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, nullptr, PPF_None); - if (!ImportResult) + + // Byte Enum types get parsed by TryParseEnum, above. + if (FByteProperty* ByteProp = CastField(Prop)) + { + if (UEnum* Enum = ByteProp->Enum) + { + int64 EnumValue; + if (!TryParseEnum(Enum, Value, EnumValue)) return false; + ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumValue); + return true; + } + } + + // Regular Enum types get parsed by TryParseEnum, above. + if (FEnumProperty* EnumProp = CastField(Prop)) + { + int64 EnumValue; + if (!TryParseEnum(EnumProp->GetEnum(), Value, EnumValue)) return false; + EnumProp->GetUnderlyingProperty()->SetIntPropertyValue(ValuePtr, EnumValue); + return true; + } + + // Non-enum properties use ImportText + const TCHAR* Result = Prop->ImportText_Direct(*Value, ValuePtr, nullptr, PPF_None); + if (!Result) { UMCPServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s' (type: %s)\n"), *Value, *MCPUtils::FormatName(Prop), *Prop->GetCPPType()); return false; } + return true; +} + +bool MCPProperty::SetText(const FString& Value) +{ + if (!TrySetText(Value)) return false; if (Prop->GetOwnerClass()->IsChildOf(UMaterialExpression::StaticClass())) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp index b0fc4335..cd8b6c9b 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPTypes.cpp @@ -494,4 +494,47 @@ bool UMCPTypes::TextToType(const FString& Text, FEdGraphPinType& OutPinType) return false; } +UClass* UMCPTypes::TextToOneObjectType(const FString& Text) +{ + FEdGraphPinType PinType; + if (!TextToType(Text, PinType)) return nullptr; + UClass* Class = Cast(PinType.PinSubCategoryObject.Get()); + if ((!Class) || (PinType.PinCategory != UEdGraphSchema_K2::PC_Object) || + (PinType.IsContainer())) + { + UMCPServer::Printf(TEXT("ERROR: '%s' is not a plain object class\n"), *Text); + return nullptr; + } + return Class; +} +UClass* UMCPTypes::TextToOneInterfaceType(const FString& Text) +{ + FEdGraphPinType PinType; + if (!TextToType(Text, PinType)) return nullptr; + UClass* Class = Cast(PinType.PinSubCategoryObject.Get()); + if ((!Class) || (PinType.PinCategory != UEdGraphSchema_K2::PC_Interface) || + (PinType.IsContainer())) + { + UMCPServer::Printf(TEXT("ERROR: '%s' is not an interface class\n"), *Text); + return nullptr; + } + return Class; +} + +void UMCPTypes::PrintDocs() +{ + UMCPServer::Printf(TEXT("To express types, use case-insensitive short names:\n")); + UMCPServer::Printf(TEXT("\n")); + UMCPServer::Printf(TEXT(" boolean, int64, double, string, etc\n")); + UMCPServer::Printf(TEXT(" vector, rotator, hitresult, etc\n")); + UMCPServer::Printf(TEXT(" actor, character, playercontroller, etc\n")); + UMCPServer::Printf(TEXT(" eblendmode, emovementmode, etc\n")); + UMCPServer::Printf(TEXT("\n")); + UMCPServer::Printf(TEXT("Notice that it's 'actor', not 'AActor'.\n")); + UMCPServer::Printf(TEXT("You can use the following notations for complex types:\n")); + UMCPServer::Printf(TEXT("\n")); + UMCPServer::Printf(TEXT(" soft, class, softclass\n")); + UMCPServer::Printf(TEXT(" array, set, map\n")); + UMCPServer::Printf(TEXT("\n")); +} \ No newline at end of file diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h index 550b1a2b..627227a1 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h @@ -28,5 +28,7 @@ public: static MCPProperty GetOneExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name); private: + bool TryParseEnum(UEnum* Enum, const FString& Text, int64 &OutValue); + bool TrySetText(const FString &Text); static void Collect(UStruct* StructType, void* Container, TArray &Props, EPropertyFlags Flags); }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h index 287b72e9..6e1d5d76 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h @@ -48,9 +48,6 @@ public: /** Track an object that has been modified by the current handler. */ static void AddTouchedObject(UObject* Obj) { GMCPServer->Notifier.AddTouchedObject(Obj); } - /** Send notifications now (also called automatically after the handler returns). */ - static void SendNotifications() { GMCPServer->Notifier.SendNotifications(); } - /** Print text to the handler output buffer. */ static void Print(const TCHAR* Text) { GMCPServer->HandlerOutput.Append(Text); } static void Print(const FString& Text) { GMCPServer->HandlerOutput.Append(Text); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h index 05da34ca..3b583a7d 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPTypes.h @@ -34,6 +34,17 @@ public: // via UMCPServer and returns false. static bool TextToType(const FString& Text, FEdGraphPinType& OutPinType); + // Parse a type string and verify it's a single object class (PC_Object, + // no container, no wrapper). Returns nullptr and prints error on failure. + static UClass* TextToOneObjectType(const FString& Text); + + // Parse a type string and verify it's a single interface class (PC_Interface, + // no container, no wrapper). Returns nullptr and prints error on failure. + static UClass* TextToOneInterfaceType(const FString& Text); + + // Print the documentation for how types are expressed, for the LLM. + static void PrintDocs(); + private: FString TypeToTextInner(FName Category, FName SubCategory, UObject* SubCategoryObject);