From a00058736a944c4647280b4b0a396d4cc858cc35 Mon Sep 17 00:00:00 2001 From: jyelon Date: Sun, 15 Mar 2026 19:03:29 -0400 Subject: [PATCH] Refactoring json code and more --- Content/Testing/M_Test.uasset | 4 +- .../AnimBlueprint_SetBlendSpaceSamples.h | 3 +- .../HalfBaked/Blueprint_AddEventDispatcher.h | 3 +- .../BlueprintMCP/HalfBaked/Struct_Create.h | 3 +- .../BlueprintMCP/Handlers/GraphNode_Create.h | 3 +- .../Handlers/GraphNode_SetDefaults.h | 3 +- .../Handlers/GraphNode_SetPositions.h | 14 +- .../BlueprintMCP/Handlers/GraphPin_Connect.h | 3 +- .../Handlers/GraphPin_Disconnect.h | 3 +- .../Source/BlueprintMCP/Handlers/Graph_Dump.h | 64 ++--- .../BlueprintMCP/Handlers/ShowCommands.h | 3 +- .../Private/BlueprintExporter.cpp | 1 + .../Source/BlueprintMCP/Private/MCPJson.cpp | 140 +++++++++++ .../Source/BlueprintMCP/Private/MCPServer.cpp | 3 +- .../Source/BlueprintMCP/Private/MCPUtils.cpp | 219 +----------------- .../Source/BlueprintMCP/Public/MCPJson.h | 15 ++ .../Source/BlueprintMCP/Public/MCPUtils.h | 6 - 17 files changed, 202 insertions(+), 288 deletions(-) create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPJson.cpp create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPJson.h diff --git a/Content/Testing/M_Test.uasset b/Content/Testing/M_Test.uasset index efea0852..30c46a60 100644 --- a/Content/Testing/M_Test.uasset +++ b/Content/Testing/M_Test.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ff036b0a3e973370df6051f97e44120470ce2574230bd9c8a6e01629c942a5f -size 13931 +oid sha256:11baf80ff08598e08e6d2e290ab08a4f2393328f059c195cbc3f0e03e181aebd +size 13164 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/AnimBlueprint_SetBlendSpaceSamples.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/AnimBlueprint_SetBlendSpaceSamples.h index c63c97de..0acb32f6 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/AnimBlueprint_SetBlendSpaceSamples.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/AnimBlueprint_SetBlendSpaceSamples.h @@ -4,6 +4,7 @@ #include "MCPServer.h" #include "MCPHandler.h" #include "MCPFetcher.h" +#include "MCPJson.h" #include "MCPUtils.h" #include "Animation/AnimSequence.h" #include "Animation/BlendSpace.h" @@ -101,7 +102,7 @@ public: for (const TSharedPtr& SampleVal : Samples.Array) { FBlendSpaceSampleEntry Entry; - if (!MCPUtils::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal)) return; + if (!MCPJson::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal)) return; UAnimSequence* AnimSeq = nullptr; if (!Entry.AnimationAsset.IsEmpty()) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Blueprint_AddEventDispatcher.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Blueprint_AddEventDispatcher.h index 90ed4f48..fe589ec8 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Blueprint_AddEventDispatcher.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Blueprint_AddEventDispatcher.h @@ -5,6 +5,7 @@ #include "MCPHandler.h" #include "MCPTypes.h" #include "MCPFetcher.h" +#include "MCPJson.h" #include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" @@ -125,7 +126,7 @@ public: for (const TSharedPtr& ParamVal : Parameters.Array) { FDispatcherParamEntry Entry; - if (!MCPUtils::PopulateFromJson(FDispatcherParamEntry::StaticStruct(), &Entry, ParamVal)) return; + if (!MCPJson::PopulateFromJson(FDispatcherParamEntry::StaticStruct(), &Entry, ParamVal)) return; if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue; FEdGraphPinType PinType; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Struct_Create.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Struct_Create.h index 964392d5..66222d12 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Struct_Create.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/HalfBaked/Struct_Create.h @@ -4,6 +4,7 @@ #include "MCPServer.h" #include "MCPHandler.h" #include "MCPTypes.h" +#include "MCPJson.h" #include "MCPUtils.h" #include "StructUtils/UserDefinedStruct.h" #include "Kismet2/BlueprintEditorUtils.h" @@ -60,7 +61,7 @@ public: for (const TSharedPtr& PropVal : Properties.Array) { FStructPropertyEntry Entry; - if (!MCPUtils::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal)) return; + if (!MCPJson::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal)) return; if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue; FEdGraphPinType PinType; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Create.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Create.h index 66e85046..92354a89 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Create.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Create.h @@ -4,6 +4,7 @@ #include "MCPServer.h" #include "MCPHandler.h" #include "MCPFetcher.h" +#include "MCPJson.h" #include "MCPUtils.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" @@ -62,7 +63,7 @@ public: for (const TSharedPtr& NodeVal : Nodes.Array) { FSpawnNodeEntry Entry; - if (!MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal)) + if (!MCPJson::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal)) continue; // Find the action by exact full name diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h index 290c7328..38890f8f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetDefaults.h @@ -5,6 +5,7 @@ #include "MCPServer.h" #include "MCPFetcher.h" #include "MCPProperty.h" +#include "MCPJson.h" #include "MCPUtils.h" #include "EdGraph/EdGraphPin.h" #include "EdGraphSchema_K2.h" @@ -124,7 +125,7 @@ public: for (const TSharedPtr& PinVal : Pins.Array) { FSetNodeDefaultEntry Entry; - if (!MCPUtils::PopulateFromJson(FSetNodeDefaultEntry::StaticStruct(), &Entry, PinVal)) + if (!MCPJson::PopulateFromJson(FSetNodeDefaultEntry::StaticStruct(), &Entry, PinVal)) continue; if (K2Schema) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetPositions.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetPositions.h index 0294a6fb..87ee61c7 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetPositions.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_SetPositions.h @@ -4,6 +4,7 @@ #include "MCPServer.h" #include "MCPHandler.h" #include "MCPFetcher.h" +#include "MCPJson.h" #include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraphNode.h" @@ -36,8 +37,8 @@ class UMCP_GraphNode_SetPositions : public UObject, public IMCPHandler GENERATED_BODY() public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; + UPROPERTY(meta=(Description="Path to a graph, e.g. /Game/Foo,graph:EventGraph")) + FString Graph; UPROPERTY(meta=(Description="Array of {node, x, y} objects")) FMCPJsonArray Nodes; @@ -50,23 +51,22 @@ public: virtual void Handle() override { MCPFetcher F; - UBlueprint* BP = F.Walk(Blueprint).Cast(); - if (!BP) return; + UEdGraph* TargetGraph = F.Walk(Graph).Cast(); + if (!TargetGraph) return; int32 SuccessCount = 0; for (const TSharedPtr& NodeVal : Nodes.Array) { FMoveNodeEntry Entry; - if (!MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal)) continue; + if (!MCPJson::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal)) continue; - MCPFetcher FN(BP); + MCPFetcher FN(TargetGraph); UEdGraphNode* Node = FN.Node(Entry.Node).Cast(); if (!Node) continue; Node->NodePosX = Entry.X; Node->NodePosY = Entry.Y; - UMCPServer::Printf(TEXT("Moved %s to (%d, %d)\n"), *MCPUtils::FormatName(Node), Entry.X, Entry.Y); SuccessCount++; } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_Connect.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_Connect.h index 7f6b94d3..505852ef 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_Connect.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_Connect.h @@ -4,6 +4,7 @@ #include "MCPServer.h" #include "MCPHandler.h" #include "MCPFetcher.h" +#include "MCPJson.h" #include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" @@ -58,7 +59,7 @@ public: for (const TSharedPtr& ConnVal : Connections.Array) { FConnectPinsEntry Entry; - if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal)) + if (!MCPJson::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal)) continue; MCPFetcher FS(G); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_Disconnect.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_Disconnect.h index ce180615..cb0c85ea 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_Disconnect.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphPin_Disconnect.h @@ -4,6 +4,7 @@ #include "MCPServer.h" #include "MCPHandler.h" #include "MCPFetcher.h" +#include "MCPJson.h" #include "MCPUtils.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" @@ -58,7 +59,7 @@ public: for (const TSharedPtr& DiscVal : Disconnections.Array) { FDisconnectPinEntry Entry; - if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal)) continue; + if (!MCPJson::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal)) continue; MCPFetcher FP(G); UEdGraphPin* Pin = FP.Walk(Entry.Pin).Cast(); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Graph_Dump.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Graph_Dump.h index f40a98bb..5e5243a7 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Graph_Dump.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Graph_Dump.h @@ -23,64 +23,32 @@ class UMCP_Graph_Dump : public UObject, public IMCPHandler GENERATED_BODY() public: - UPROPERTY(meta=(Description="Path to a blueprint, material, or graph, e.g. /Game/Foo or /Game/Foo,graph:EventGraph")) - FString Object; + UPROPERTY(meta=(Description="Path to graph")) + FString Graph; + + UPROPERTY(meta=(Optional, Description="True to include less-significant details")) + bool bDetails; virtual FString GetDescription() const override { - return TEXT("Dump blueprint or material graphs as readable text. " - "If given a blueprint or material, dumps all graphs. If given a specific graph, dumps only that one."); + return TEXT("Dump blueprint or material graphs as readable text. "); } virtual void Handle() override { MCPFetcher F; - F.Walk(Object); - if (!F.Ok()) return; - - if (UEdGraph* Graph = Cast(F.GetObj())) - { - EmitGraph(Graph); - return; - } - - if (UBlueprint* BP = Cast(F.GetObj())) - { - TArray Graphs = MCPUtils::AllGraphs(BP); - for (UEdGraph* Graph : Graphs) - { - UMCPServer::Printf(TEXT("\n======== %s ========\n"), *MCPUtils::FormatName(Graph)); - EmitGraph(Graph); - } - return; - } - - if (UMaterial* Mat = Cast(F.GetObj())) - { - MCPUtils::EnsureMaterialGraph(Mat); - if (!Mat->MaterialGraph) - { - UMCPServer::Print(TEXT("ERROR: Could not build MaterialGraph for this material\n")); - return; - } - EmitGraph(Mat->MaterialGraph); - return; - } - - UMCPServer::Printf(TEXT("ERROR: Expected a blueprint, material, or graph, got %s\n"), - *MCPUtils::FormatName(F.GetObj()->GetClass())); - } - -private: - void EmitGraph(UEdGraph* Graph) - { - FlxBlueprintExporter Exporter(Graph); + UEdGraph *G = F.Walk(Graph).ToGraph().Cast(); + if (!G) return; + + FlxBlueprintExporter Exporter(G); UMCPServer::Print(*Exporter.GetOutput()); - FString Details = Exporter.GetDetails(); - if (!Details.IsEmpty()) + if (bDetails) { - UMCPServer::Print(TEXT("\n")); - UMCPServer::Print(*Details); + UMCPServer::Print(Exporter.GetDetails()); + } + else + { + UMCPServer::Printf(TEXT("Note: use bDetails=true to see suppressed details.")); } } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h index bbc7e711..a23895de 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h @@ -5,6 +5,7 @@ #include "MCPFetcher.h" #include "MCPServer.h" #include "MCPTypes.h" +#include "MCPJson.h" #include "MCPUtils.h" #include "ShowCommands.generated.h" @@ -40,7 +41,7 @@ public: if (!bFirst) UMCPServer::Print(TEXT(",")); bFirst = false; if (PropIt->HasMetaData(TEXT("Optional"))) UMCPServer::Print(TEXT("?")); - UMCPServer::Print(MCPUtils::PropertyNameToJsonKey(PropIt->GetName())); + UMCPServer::Print(PropIt->GetName()); } UMCPServer::Print(TEXT(")\n")); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp index e29848c5..425f0cf0 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp @@ -307,6 +307,7 @@ void FlxBlueprintExporter::EmitGraph() if (Node->IsA()) continue; EmitNode(Node); } + Output.Append(TEXT("\n")); } void FlxBlueprintExporter::EmitDetails() diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPJson.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPJson.cpp new file mode 100644 index 00000000..223ddc19 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPJson.cpp @@ -0,0 +1,140 @@ +#include "MCPJson.h" +#include "MCPTypes.h" +#include "MCPServer.h" +#include "UObject/UnrealType.h" +#include "UObject/EnumProperty.h" +#include "Dom/JsonValue.h" + + + +bool MCPJson::PopulateFromJson( + UStruct* StructType, void* Container, const FJsonObject* Json) +{ + bool Ok = true; + + // 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(Prop->GetName()); + } + + // Check for unknown fields in the JSON + for (const auto& KV : Json->Values) + { + if (!KnownKeys.Contains(KV.Key)) + { + UMCPServer::Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key); + Ok = false; + } + } + + // Populate each property from JSON + for (FProperty* Property : Properties) + { + if (!PopulateFromJson(Property, Container, Json)) Ok = false; + } + return Ok; +} + +bool MCPJson::PopulateFromJson( + FProperty* Property, void* Container, const FJsonObject* Json) +{ + FString JsonKey = Property->GetName(); + bool bOptional = Property->HasMetaData(TEXT("Optional")); + + if (!Json->HasField(JsonKey)) + { + if (!bOptional) + { + UMCPServer::Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey); + return false; + } + return true; + } + + void* ValuePtr = Property->ContainerPtrToValuePtr(Container); + + // Special handling for FMCPJsonObject and FMCPJsonArray + if (FStructProperty* StructProp = CastField(Property)) + { + if (StructProp->Struct == FMCPJsonObject::StaticStruct()) + { + if (!Json->HasTypedField(JsonKey)) + { + UMCPServer::Printf(TEXT("ERROR: '%s' must be an object\n"), *JsonKey); + return false; + } + static_cast(ValuePtr)->Json = Json->GetObjectField(JsonKey); + return true; + } + + if (StructProp->Struct == FMCPJsonArray::StaticStruct()) + { + if (!Json->HasTypedField(JsonKey)) + { + UMCPServer::Printf(TEXT("ERROR: '%s' must be an array\n"), *JsonKey); + return false; + } + static_cast(ValuePtr)->Array = Json->GetArrayField(JsonKey); + return true; + } + } + + // Handle based on JSON value type. + TSharedPtr JsonValue = Json->TryGetField(JsonKey); + + if (JsonValue->Type == EJson::Number) + { + double D = JsonValue->AsNumber(); + if (FIntProperty* IntProp = CastField(Property)) + { IntProp->SetPropertyValue(ValuePtr, (int32)D); return true; } + if (FFloatProperty* FloatProp = CastField(Property)) + { FloatProp->SetPropertyValue(ValuePtr, (float)D); return true; } + if (FDoubleProperty* DoubleProp = CastField(Property)) + { DoubleProp->SetPropertyValue(ValuePtr, D); return true; } + if (FByteProperty* ByteProp = CastField(Property)) + { ByteProp->SetPropertyValue(ValuePtr, (uint8)D); return true; } + UMCPServer::Printf(TEXT("ERROR: '%s' received a number but expects %s\n"), *JsonKey, *Property->GetCPPType()); + return false; + } + + if (JsonValue->Type == EJson::Boolean) + { + if (FBoolProperty* BoolProp = CastField(Property)) + { BoolProp->SetPropertyValue(ValuePtr, JsonValue->AsBool()); return true; } + UMCPServer::Printf(TEXT("ERROR: '%s' received a boolean but expects %s\n"), *JsonKey, *Property->GetCPPType()); + return false; + } + + if (JsonValue->Type == EJson::String) + { + FString ValueStr = JsonValue->AsString(); + const TCHAR* Result = Property->ImportText_Direct(*ValueStr, ValuePtr, nullptr, PPF_None); + if (!Result) + { + UMCPServer::Printf(TEXT("ERROR: Could not parse '%s' for parameter '%s'\n"), *ValueStr, *JsonKey); + return false; + } + return true; + } + + UMCPServer::Printf(TEXT("ERROR: '%s' must be a string, number, or boolean\n"), *JsonKey); + return false; +} + +bool MCPJson::PopulateFromJson( + UStruct* StructType, void* Container, + const TSharedPtr& JsonValue) +{ + if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object)) + { + UMCPServer::Print(TEXT("ERROR: Expected a JSON object\n")); + return false; + } + return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get()); +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp index 66fcf856..8f70ac68 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp @@ -1,5 +1,6 @@ #include "MCPServer.h" #include "MCPHandler.h" +#include "MCPJson.h" #include "LogCapture.h" #include "MCPUtils.h" #include "MCPAssets.h" @@ -316,7 +317,7 @@ void UMCPServer::TryCallHandler(const FString &Line) IMCPHandler* Handler = Cast(HandlerObj.Get()); // Populate the handler object with the request parameters. - if (!MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), &*Request)) + if (!MCPJson::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), &*Request)) { UMCPServer::Printf(TEXT("\nUsage:\n\n")); MCPUtils::FormatCommandHelp(*HandlerClass); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index f4682624..442a5b4d 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 "MCPJson.h" #include "MCPTypes.h" #include "MCPServer.h" #include "MCPHandler.h" @@ -789,220 +790,6 @@ TArray> MCPUtils::SearchGraphActions(UEdGraph* #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()); -} - -bool MCPUtils::PopulateFromJson( - UStruct* StructType, - void* Container, - const TSharedPtr& JsonValue) -{ - if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object)) - { - UMCPServer::Print(TEXT("ERROR: Expected a JSON object\n")); - return false; - } - return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get()); -} - -bool 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)) - { - UMCPServer::Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key); - return false; - } - } - - // 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) - { - UMCPServer::Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey); - return false; - } - continue; - } - - FString PropError = SetPropertyFromJson(Container, Prop, JsonKey, Json); - if (!PropError.IsEmpty()) - { - UMCPServer::Printf(TEXT("ERROR: %s\n"), *PropError); - return false; - } - } - - return true; -} - // ============================================================ // CollectHandlerClasses — find all concrete IMCPHandler classes // ============================================================ @@ -1173,7 +960,7 @@ void MCPUtils::FormatCommandHelp(UClass* HandlerClass) if (!bFirst) UMCPServer::Print(TEXT(",")); bFirst = false; if (PropIt->HasMetaData(TEXT("Optional"))) UMCPServer::Print(TEXT("?")); - UMCPServer::Print(PropertyNameToJsonKey(PropIt->GetName())); + UMCPServer::Print(PropIt->GetName()); } UMCPServer::Print(TEXT(")\n")); @@ -1181,7 +968,7 @@ void MCPUtils::FormatCommandHelp(UClass* HandlerClass) for (TFieldIterator PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt) { FProperty* Prop = *PropIt; - FString Name = PropertyNameToJsonKey(Prop->GetName()); + FString Name = Prop->GetName(); FString Type = UMCPTypes::TypeToText(Prop); bool bOptional = Prop->HasMetaData(TEXT("Optional")); const FString& Desc = Prop->GetMetaData(TEXT("Description")); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPJson.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPJson.h new file mode 100644 index 00000000..2b24cfe4 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPJson.h @@ -0,0 +1,15 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "Dom/JsonObject.h" + +// JSON utility functions used by MCP handlers. +// This is effectively a namespace — all methods are static. +class MCPJson +{ +public: + static bool PopulateFromJson(FProperty* Property, void* Container, const FJsonObject* Json); + static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr& JsonValue); + static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json); +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 882d140f..83031b15 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -165,11 +165,6 @@ public: static bool SetPropertyValueText(UObject* Container, FProperty* Prop, const FString& Value); static bool SetPropertyValueText(void* Container, FProperty* Prop, const FString& Value, UObject* Owner); - // ----- JSON helpers ----- - static FString PropertyNameToJsonKey(const FString& PropName); - static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr& JsonValue); - static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json); - // ----- Text formatting ----- static FString WrapText(const FString& Text, int32 ColLimit, const FString& Prefix); @@ -183,6 +178,5 @@ public: private: static void SanitizeNameInPlace(FString& Name); static void AppendNumericSuffix(FString &Name, int32 N); - static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json); };