From 6f22d62811975cf75a59aba691044bbd86a3dbf4 Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 6 Mar 2026 17:14:25 -0500 Subject: [PATCH] More general cleanup in MCP --- .../Private/MCPHandlerPopulate.cpp | 227 ------------------ .../Source/BlueprintMCP/Private/MCPUtils.cpp | 218 +++++++++++++++++ .../Source/BlueprintMCP/Public/MCPUtils.h | 4 + 3 files changed, 222 insertions(+), 227 deletions(-) delete mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp deleted file mode 100644 index a038e772..00000000 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp +++ /dev/null @@ -1,227 +0,0 @@ -#include "MCPHandler.h" -#include "MCPUtils.h" -#include "Dom/JsonObject.h" -#include "UObject/UnrealType.h" -#include "UObject/EnumProperty.h" - -namespace MCPPopulate -{ - -// Try to set a single FProperty on a handler from a JSON field. -// Returns an empty string on success, or an error message on failure. -FString SetPropertyFromJson( - void* Container, - FProperty* Prop, - const FString& FieldName, - const FJsonObject* Json) -{ - void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); - - // FString - if (FStrProperty* StrProp = CastField(Prop)) - { - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be a string"), *FieldName); - } - StrProp->SetPropertyValue(ValuePtr, Json->GetStringField(FieldName)); - return FString(); - } - - // int32 - if (FIntProperty* IntProp = CastField(Prop)) - { - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be a number"), *FieldName); - } - IntProp->SetPropertyValue(ValuePtr, (int32)Json->GetNumberField(FieldName)); - return FString(); - } - - // float - if (FFloatProperty* FloatProp = CastField(Prop)) - { - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be a number"), *FieldName); - } - FloatProp->SetPropertyValue(ValuePtr, (float)Json->GetNumberField(FieldName)); - return FString(); - } - - // double - if (FDoubleProperty* DoubleProp = CastField(Prop)) - { - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be a number"), *FieldName); - } - DoubleProp->SetPropertyValue(ValuePtr, Json->GetNumberField(FieldName)); - return FString(); - } - - // bool - if (FBoolProperty* BoolProp = CastField(Prop)) - { - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be a boolean"), *FieldName); - } - BoolProp->SetPropertyValue(ValuePtr, Json->GetBoolField(FieldName)); - return FString(); - } - - // Enum (FEnumProperty — C++ enum class) - if (FEnumProperty* EnumProp = CastField(Prop)) - { - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be a string"), *FieldName); - } - FString ValueStr = Json->GetStringField(FieldName); - UEnum* Enum = EnumProp->GetEnum(); - int64 EnumVal = Enum->GetValueByNameString(ValueStr); - if (EnumVal == INDEX_NONE) - { - return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr); - } - FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty(); - UnderlyingProp->SetIntPropertyValue(ValuePtr, EnumVal); - return FString(); - } - - // Enum (FByteProperty with Enum — old-style UENUM) - if (FByteProperty* ByteProp = CastField(Prop)) - { - if (ByteProp->Enum) - { - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be a string"), *FieldName); - } - FString ValueStr = Json->GetStringField(FieldName); - int64 EnumVal = ByteProp->Enum->GetValueByNameString(ValueStr); - if (EnumVal == INDEX_NONE) - { - return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr); - } - ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumVal); - return FString(); - } - // Plain byte without enum — treat as number - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be a number"), *FieldName); - } - ByteProp->SetPropertyValue(ValuePtr, (uint8)Json->GetNumberField(FieldName)); - return FString(); - } - - // FMCPJsonObject — stash a JSON object into the struct - if (FStructProperty* StructProp = CastField(Prop)) - { - if (StructProp->Struct == FMCPJsonObject::StaticStruct()) - { - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be an object"), *FieldName); - } - FMCPJsonObject* Obj = StructProp->ContainerPtrToValuePtr(Container); - Obj->Json = Json->GetObjectField(FieldName); - return FString(); - } - - // FMCPJsonArray — stash a JSON array into the struct - if (StructProp->Struct == FMCPJsonArray::StaticStruct()) - { - if (!Json->HasTypedField(FieldName)) - { - return FString::Printf(TEXT("'%s' must be an array"), *FieldName); - } - FMCPJsonArray* Arr = StructProp->ContainerPtrToValuePtr(Container); - Arr->Array = Json->GetArrayField(FieldName); - return FString(); - } - } - - return FString::Printf(TEXT("'%s': unsupported property type '%s'"), - *FieldName, *Prop->GetCPPType()); -} - -// Convert a property name from PascalCase to camelCase, matching JSON conventions. -// e.g. "BlueprintName" -> "blueprintName", "PosX" -> "posX" -FString PropertyNameToJsonKey(const FString& PropName) -{ - if (PropName.IsEmpty()) - { - return PropName; - } - FString Result = PropName; - Result[0] = FChar::ToLower(Result[0]); - return Result; -} - -} // namespace MCPPopulate - -FString MCPUtils::PopulateFromJson( - UStruct* StructType, - void* Container, - const TSharedPtr& JsonValue) -{ - if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object)) - { - return TEXT("Expected a JSON object"); - } - return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get()); -} - -FString MCPUtils::PopulateFromJson( - UStruct* StructType, - void* Container, - const FJsonObject* Json) -{ - // Build a set of known property names (as JSON keys) for the unknown-field check. - TSet KnownKeys; - TArray Properties; - - for (TFieldIterator It(StructType, EFieldIterationFlags::None); It; ++It) - { - FProperty* Prop = *It; - Properties.Add(Prop); - KnownKeys.Add(MCPPopulate::PropertyNameToJsonKey(Prop->GetName())); - } - - // Check for unknown fields in the JSON - for (const auto& KV : Json->Values) - { - if (!KnownKeys.Contains(KV.Key)) - { - return FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key); - } - } - - // Populate each property from JSON - for (FProperty* Prop : Properties) - { - FString JsonKey = MCPPopulate::PropertyNameToJsonKey(Prop->GetName()); - bool bOptional = Prop->HasMetaData(TEXT("Optional")); - - if (!Json->HasField(JsonKey)) - { - if (!bOptional) - { - return FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey); - } - continue; - } - - FString Error = MCPPopulate::SetPropertyFromJson(Container, Prop, JsonKey, Json); - if (!Error.IsEmpty()) - { - return Error; - } - } - - return FString(); -} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index 789698c4..e30679e3 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -1,4 +1,5 @@ #include "MCPUtils.h" +#include "MCPHandler.h" #include "BlueprintActionDatabase.h" #include "BlueprintNodeSpawner.h" #include "Dom/JsonValue.h" @@ -1220,3 +1221,220 @@ TArray MCPUtils::SearchNodeSpawners(const FString& Query } return Result; } + +// ============================================================ +// PopulateFromJson — fill a USTRUCT from a JSON object +// ============================================================ + +#include "UObject/UnrealType.h" +#include "UObject/EnumProperty.h" + +FString MCPUtils::PropertyNameToJsonKey(const FString& PropName) +{ + if (PropName.IsEmpty()) + { + return PropName; + } + FString Result = PropName; + Result[0] = FChar::ToLower(Result[0]); + return Result; +} + +FString MCPUtils::SetPropertyFromJson( + void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json) +{ + void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); + + // FString + if (FStrProperty* StrProp = CastField(Prop)) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be a string"), *FieldName); + } + StrProp->SetPropertyValue(ValuePtr, Json->GetStringField(FieldName)); + return FString(); + } + + // int32 + if (FIntProperty* IntProp = CastField(Prop)) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be a number"), *FieldName); + } + IntProp->SetPropertyValue(ValuePtr, (int32)Json->GetNumberField(FieldName)); + return FString(); + } + + // float + if (FFloatProperty* FloatProp = CastField(Prop)) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be a number"), *FieldName); + } + FloatProp->SetPropertyValue(ValuePtr, (float)Json->GetNumberField(FieldName)); + return FString(); + } + + // double + if (FDoubleProperty* DoubleProp = CastField(Prop)) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be a number"), *FieldName); + } + DoubleProp->SetPropertyValue(ValuePtr, Json->GetNumberField(FieldName)); + return FString(); + } + + // bool + if (FBoolProperty* BoolProp = CastField(Prop)) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be a boolean"), *FieldName); + } + BoolProp->SetPropertyValue(ValuePtr, Json->GetBoolField(FieldName)); + return FString(); + } + + // Enum (FEnumProperty — C++ enum class) + if (FEnumProperty* EnumProp = CastField(Prop)) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be a string"), *FieldName); + } + FString ValueStr = Json->GetStringField(FieldName); + UEnum* Enum = EnumProp->GetEnum(); + int64 EnumVal = Enum->GetValueByNameString(ValueStr); + if (EnumVal == INDEX_NONE) + { + return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr); + } + FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty(); + UnderlyingProp->SetIntPropertyValue(ValuePtr, EnumVal); + return FString(); + } + + // Enum (FByteProperty with Enum — old-style UENUM) + if (FByteProperty* ByteProp = CastField(Prop)) + { + if (ByteProp->Enum) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be a string"), *FieldName); + } + FString ValueStr = Json->GetStringField(FieldName); + int64 EnumVal = ByteProp->Enum->GetValueByNameString(ValueStr); + if (EnumVal == INDEX_NONE) + { + return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr); + } + ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumVal); + return FString(); + } + // Plain byte without enum — treat as number + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be a number"), *FieldName); + } + ByteProp->SetPropertyValue(ValuePtr, (uint8)Json->GetNumberField(FieldName)); + return FString(); + } + + // FMCPJsonObject — stash a JSON object into the struct + if (FStructProperty* StructProp = CastField(Prop)) + { + if (StructProp->Struct == FMCPJsonObject::StaticStruct()) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be an object"), *FieldName); + } + FMCPJsonObject* Obj = StructProp->ContainerPtrToValuePtr(Container); + Obj->Json = Json->GetObjectField(FieldName); + return FString(); + } + + // FMCPJsonArray — stash a JSON array into the struct + if (StructProp->Struct == FMCPJsonArray::StaticStruct()) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be an array"), *FieldName); + } + FMCPJsonArray* Arr = StructProp->ContainerPtrToValuePtr(Container); + Arr->Array = Json->GetArrayField(FieldName); + return FString(); + } + } + + return FString::Printf(TEXT("'%s': unsupported property type '%s'"), + *FieldName, *Prop->GetCPPType()); +} + +FString MCPUtils::PopulateFromJson( + UStruct* StructType, + void* Container, + const TSharedPtr& JsonValue) +{ + if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object)) + { + return TEXT("Expected a JSON object"); + } + return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get()); +} + +FString MCPUtils::PopulateFromJson( + UStruct* StructType, + void* Container, + const FJsonObject* Json) +{ + // Build a set of known property names (as JSON keys) for the unknown-field check. + TSet KnownKeys; + TArray Properties; + + for (TFieldIterator It(StructType, EFieldIterationFlags::None); It; ++It) + { + FProperty* Prop = *It; + Properties.Add(Prop); + KnownKeys.Add(PropertyNameToJsonKey(Prop->GetName())); + } + + // Check for unknown fields in the JSON + for (const auto& KV : Json->Values) + { + if (!KnownKeys.Contains(KV.Key)) + { + return FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key); + } + } + + // Populate each property from JSON + for (FProperty* Prop : Properties) + { + FString JsonKey = PropertyNameToJsonKey(Prop->GetName()); + bool bOptional = Prop->HasMetaData(TEXT("Optional")); + + if (!Json->HasField(JsonKey)) + { + if (!bOptional) + { + return FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey); + } + continue; + } + + FString Error = SetPropertyFromJson(Container, Prop, JsonKey, Json); + if (!Error.IsEmpty()) + { + return Error; + } + } + + return FString(); +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 939f0c0c..8483c65f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -94,4 +94,8 @@ public: // ----- Property population ----- static FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr& JsonValue); static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json); + +private: + static FString PropertyNameToJsonKey(const FString& PropName); + static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json); };