From 730396753c2c84af4589afcfb7ddbac66eeaa772 Mon Sep 17 00:00:00 2001 From: jyelon Date: Sun, 8 Mar 2026 03:58:06 -0400 Subject: [PATCH] More MCP refactoring --- .../Private/MCPHandlers_AnimMutation.h | 29 ++++++++++++------ .../Private/MCPHandlers_Dispatchers.h | 30 ++++++++++++------- .../Private/MCPHandlers_Mutation.h | 14 ++------- .../Private/MCPHandlers_PinMutation.h | 21 ++----------- .../Private/MCPHandlers_UserTypes.h | 27 +++++++++++------ .../Source/BlueprintMCP/Private/MCPServer.cpp | 7 +---- .../Source/BlueprintMCP/Private/MCPUtils.cpp | 30 +++++++++++-------- .../Source/BlueprintMCP/Public/MCPUtils.h | 4 +-- 8 files changed, 83 insertions(+), 79 deletions(-) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.h index e8035b9e..7f2fb97c 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.h @@ -332,6 +332,21 @@ public: // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- +USTRUCT() +struct FBlendSpaceSampleEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString AnimationAsset; + + UPROPERTY() + float X = 0.0f; + + UPROPERTY() + float Y = 0.0f; +}; + UCLASS(meta=(ToolName="set_blend_space_sample_points")) class UMCPHandler_SetBlendSpaceSamples : public UObject, public IMCPHandler { @@ -410,22 +425,18 @@ public: for (const TSharedPtr& SampleVal : Samples.Array) { - const TSharedPtr& SampleObj = SampleVal->AsObject(); - if (!SampleObj.IsValid()) continue; - - FString AnimAssetName = SampleObj->GetStringField(TEXT("animationAsset")); - float X = (float)SampleObj->GetNumberField(TEXT("x")); - float Y = (float)SampleObj->GetNumberField(TEXT("y")); + FBlendSpaceSampleEntry Entry; + if (!MCPUtils::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal, Result)) return; UAnimSequence* AnimSeq = nullptr; - if (!AnimAssetName.IsEmpty()) + if (!Entry.AnimationAsset.IsEmpty()) { MCPAssets AnimAssets; - if (AnimAssets.Exact(AnimAssetName).Load()) + if (AnimAssets.Exact(Entry.AnimationAsset).Load()) AnimSeq = AnimAssets.Object(); } - FVector SampleValue(X, Y, 0.0f); + FVector SampleValue(Entry.X, Entry.Y, 0.0f); if (AnimSeq) { BS->AddSample(AnimSeq, SampleValue); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.h index 2de1c8d8..632a1293 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.h @@ -16,6 +16,18 @@ // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- +USTRUCT() +struct FDispatcherParamEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString Name; + + UPROPERTY() + FString Type; +}; + UCLASS(meta=(ToolName="add_event_dispatcher")) class UMCPHandler_AddEventDispatcher : public UObject, public IMCPHandler { @@ -117,23 +129,19 @@ public: for (const TSharedPtr& ParamVal : Parameters.Array) { - if (!ParamVal.IsValid() || ParamVal->Type != EJson::Object) continue; - TSharedPtr ParamObj = ParamVal->AsObject(); - - FString ParamName = ParamObj->GetStringField(TEXT("name")); - FString ParamType = ParamObj->GetStringField(TEXT("type")); - - if (ParamName.IsEmpty() || ParamType.IsEmpty()) continue; + FDispatcherParamEntry Entry; + if (!MCPUtils::PopulateFromJson(FDispatcherParamEntry::StaticStruct(), &Entry, ParamVal, Result)) return; + if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue; FEdGraphPinType PinType; - if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result)) + if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, Result)) return; - EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output); + EntryNode->CreateUserDefinedPin(FName(*Entry.Name), PinType, EGPD_Output); TSharedRef ParamJson = MakeShared(); - ParamJson->SetStringField(TEXT("name"), ParamName); - ParamJson->SetStringField(TEXT("type"), ParamType); + ParamJson->SetStringField(TEXT("name"), Entry.Name); + ParamJson->SetStringField(TEXT("type"), Entry.Type); AddedParamsJson.Add(MakeShared(ParamJson)); } } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Mutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Mutation.h index cb5fb559..4c7948a2 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Mutation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Mutation.h @@ -103,12 +103,7 @@ public: Results.Add(MakeShared(EntryResult)); FMoveNodeEntry Entry; - FString PopulateError = MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal); - if (!PopulateError.IsEmpty()) - { - EntryResult->SetStringField(TEXT("error"), PopulateError); - continue; - } + if (!MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal, &*EntryResult)) continue; UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node); if (!Node) @@ -362,12 +357,7 @@ public: Results.Add(MakeShared(EntryResult)); FSpawnNodeEntry Entry; - FString PopulateError = MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal); - if (!PopulateError.IsEmpty()) - { - EntryResult->SetStringField(TEXT("error"), PopulateError); - continue; - } + if (!MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal, &*EntryResult)) continue; // Find the spawner by exact full name TArray Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true, TargetGraph); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_PinMutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_PinMutation.h index d5411633..36ac29a0 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_PinMutation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_PinMutation.h @@ -63,12 +63,7 @@ public: Results.Add(MakeShared(EntryResult)); FSetPinDefaultEntry Entry; - FString PopulateError = MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal); - if (!PopulateError.IsEmpty()) - { - EntryResult->SetStringField(TEXT("error"), PopulateError); - continue; - } + if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, &*EntryResult)) continue; MCPAssets Assets; if (!Assets.Scan().Scan().Exact(Entry.Blueprint).Errors(&*EntryResult).ENone().ETwo().Load()) @@ -192,12 +187,7 @@ public: Results.Add(MakeShared(EntryResult)); FConnectPinsEntry Entry; - FString PopulateError = MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal); - if (!PopulateError.IsEmpty()) - { - EntryResult->SetStringField(TEXT("error"), PopulateError); - continue; - } + if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, &*EntryResult)) continue; UEdGraph* SourceGraph = nullptr; UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph); @@ -319,12 +309,7 @@ public: Results.Add(MakeShared(EntryResult)); FDisconnectPinEntry Entry; - FString PopulateError = MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal); - if (!PopulateError.IsEmpty()) - { - EntryResult->SetStringField(TEXT("error"), PopulateError); - continue; - } + if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal, &*EntryResult)) continue; UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node); if (!Node) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h index ba5f4a7d..a26a2d26 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h @@ -21,6 +21,18 @@ // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- +USTRUCT() +struct FStructPropertyEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString Name; + + UPROPERTY() + FString Type; +}; + UCLASS(meta=(ToolName="create_struct_asset")) class UMCPHandler_CreateStruct : public UObject, public IMCPHandler { @@ -77,18 +89,15 @@ public: int32 PropsAdded = 0; for (const TSharedPtr& PropVal : Properties.Array) { - TSharedPtr PropObj = PropVal->AsObject(); - if (!PropObj) continue; - - FString PropName = PropObj->GetStringField(TEXT("name")); - FString PropType = PropObj->GetStringField(TEXT("type")); - if (PropName.IsEmpty() || PropType.IsEmpty()) continue; + FStructPropertyEntry Entry; + if (!MCPUtils::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal, Result)) return; + if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue; FEdGraphPinType PinType; FString TypeError; - if (!MCPUtils::ResolveTypeFromString(PropType, PinType, TypeError)) + if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, TypeError)) { - UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *PropType, *PropName, *TypeError); + UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *Entry.Type, *Entry.Name, *TypeError); continue; } @@ -114,7 +123,7 @@ public: } if (NewPropGuid.IsValid()) { - FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, PropName); + FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, Entry.Name); } PropsAdded++; } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp index 80f76d89..d23b1b8a 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp @@ -261,15 +261,10 @@ void FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Pa TStrongObjectPtr HandlerObj(NewObject(GetTransientPackage(), *HandlerClass)); IMCPHandler* Handler = Cast(HandlerObj.Get()); - FString PopulateError = MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params); - if (PopulateError.IsEmpty()) + if (MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params, Result)) { Handler->Handle(Params, Result); } - else - { - MCPUtils::MakeErrorJson(Result, PopulateError); - } if (bIsMutation && GEditor) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index 4a1dd0ba..1345176d 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -1427,22 +1427,25 @@ FString MCPUtils::SetPropertyFromJson( *FieldName, *Prop->GetCPPType()); } -FString MCPUtils::PopulateFromJson( +bool MCPUtils::PopulateFromJson( UStruct* StructType, void* Container, - const TSharedPtr& JsonValue) + const TSharedPtr& JsonValue, + MCPErrorCallback Error) { if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object)) { - return TEXT("Expected a JSON object"); + Error.SetError(TEXT("Expected a JSON object")); + return false; } - return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get()); + return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get(), Error); } -FString MCPUtils::PopulateFromJson( +bool MCPUtils::PopulateFromJson( UStruct* StructType, void* Container, - const FJsonObject* Json) + const FJsonObject* Json, + MCPErrorCallback Error) { // Build a set of known property names (as JSON keys) for the unknown-field check. TSet KnownKeys; @@ -1460,7 +1463,8 @@ FString MCPUtils::PopulateFromJson( { if (!KnownKeys.Contains(KV.Key)) { - return FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key); + Error.SetError(FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key)); + return false; } } @@ -1474,17 +1478,19 @@ FString MCPUtils::PopulateFromJson( { if (!bOptional) { - return FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey); + Error.SetError(FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey)); + return false; } continue; } - FString Error = SetPropertyFromJson(Container, Prop, JsonKey, Json); - if (!Error.IsEmpty()) + FString PropError = SetPropertyFromJson(Container, Prop, JsonKey, Json); + if (!PropError.IsEmpty()) { - return Error; + Error.SetError(PropError); + return false; } } - return FString(); + return true; } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 196d9b8a..85e87627 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -149,8 +149,8 @@ public: // ----- Property population ----- static FString PropertyNameToJsonKey(const FString& PropName); - static FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr& JsonValue); - static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json); + static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr& JsonValue, MCPErrorCallback Error); + static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json, MCPErrorCallback Error); private: static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json);