From 282ee3ef33b50b72473d295ec799c58d687b545d Mon Sep 17 00:00:00 2001 From: jyelon Date: Sat, 7 Mar 2026 01:55:32 -0500 Subject: [PATCH] Add MCPErrorCallback. Simplify error handling. --- .../BlueprintMCP/Private/MCPAssetFinder.cpp | 18 ++-- .../Private/MCPHandlers_Dispatchers.cpp | 8 +- .../Private/MCPHandlers_MaterialInstance.cpp | 24 ++--- .../Private/MCPHandlers_MaterialMutation.cpp | 92 +++++++------------ .../Private/MCPHandlers_MaterialRead.cpp | 32 ++----- .../Private/MCPHandlers_Params.cpp | 14 +-- .../BlueprintMCP/Private/MCPHandlers_Read.cpp | 8 +- .../Private/MCPHandlers_UserTypes.h | 23 ++--- .../Private/MCPHandlers_Variables.cpp | 14 +-- .../Source/BlueprintMCP/Private/MCPUtils.cpp | 32 +++++-- .../Source/BlueprintMCP/Private/TODO.md | 5 + .../BlueprintMCP/Public/MCPAssetFinder.h | 27 +++--- .../Source/BlueprintMCP/Public/MCPUtils.h | 17 +++- 13 files changed, 134 insertions(+), 180 deletions(-) create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/TODO.md diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp index c0323bb4..ae1cd3c5 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp @@ -200,7 +200,7 @@ FAssetData* UMCPAssetFinder::FindAnyAsset(const FString& NameOrPath, FString* Ou // Load helpers // ============================================================ -UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(FAssetData& Asset, FString& OutError) +UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(FAssetData& Asset, MCPErrorCallback Error) { // Regular blueprint asset UBlueprint* BP = Cast(Asset.GetAsset()); @@ -214,20 +214,22 @@ UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(FAssetData& Asset, FS if (LevelBP) return LevelBP; } - OutError = FString::Printf(TEXT("Asset '%s' loaded but its level blueprint could not be retrieved."), - *Asset.AssetName.ToString()); + Error.SetError(FString::Printf(TEXT("Asset '%s' loaded but its level blueprint could not be retrieved."), + *Asset.AssetName.ToString())); return nullptr; } -UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(const FString& NameOrPath, FString& OutError) +UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(const FString& NameOrPath, MCPErrorCallback Error) { - FAssetData* Asset = FindAsset(BlueprintsAndMaps, NameOrPath, &OutError); + FString FindError; + FAssetData* Asset = FindAsset(BlueprintsAndMaps, NameOrPath, &FindError); if (!Asset) { - if (OutError.IsEmpty()) - OutError = FString::Printf(TEXT("Blueprint '%s' not found."), *NameOrPath); + if (FindError.IsEmpty()) + FindError = FString::Printf(TEXT("Blueprint '%s' not found."), *NameOrPath); + Error.SetError(FindError); return nullptr; } - return LoadBlueprintOrLevelBlueprint(*Asset, OutError); + return LoadBlueprintOrLevelBlueprint(*Asset, Error); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.cpp index 9fd70eca..2fba8bda 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Dispatchers.cpp @@ -131,12 +131,8 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso if (ParamName.IsEmpty() || ParamType.IsEmpty()) continue; FEdGraphPinType PinType; - FString TypeError; - if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, TypeError)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Parameter '%s': %s"), *ParamName, *TypeError)); - } + if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result)) + return; EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialInstance.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialInstance.cpp index 80faa686..b14f65f8 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialInstance.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialInstance.cpp @@ -145,12 +145,8 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject* } // Load the Material Instance - FString LoadError; - UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset(MIName, LoadError); - if (!MI) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset(MIName, Result); + if (!MI) return; // Determine the parameter type — explicit or auto-detect from parent FString TypeStr; @@ -366,12 +362,8 @@ void FBlueprintMCPServer::HandleGetMaterialInstanceParameters(const FJsonObject* return MCPUtils::MakeErrorJson(Result, TEXT("Missing required query parameter: name")); } - FString LoadError; - UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset(NameParam, LoadError); - if (!MI) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset(NameParam, Result); + if (!MI) return; Result->SetStringField(TEXT("name"), MI->GetName()); Result->SetStringField(TEXT("path"), MI->GetPathName()); @@ -614,12 +606,8 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json } // Load the Material Instance - FString LoadError; - UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset(MIName, LoadError); - if (!MI) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset(MIName, Result); + if (!MI) return; // Capture old parent FString OldParentPath = MI->Parent ? MI->Parent->GetPathName() : TEXT("None"); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialMutation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialMutation.cpp index 3e7b21d6..515aba96 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialMutation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialMutation.cpp @@ -218,12 +218,8 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs Json->TryGetBoolField(TEXT("dryRun"), bDryRun); // Load material - FString LoadError; - UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; FString OldValue; FString NewValue; @@ -562,17 +558,15 @@ void FBlueprintMCPServer::HandleAddMaterialExpression(const FJsonObject* Json, F { return MCPUtils::MakeErrorJson(Result, TEXT("Specify either 'material' or 'materialFunction', not both")); } - FString LoadError; - MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, LoadError); - if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, Result); + if (!MatFunc) return; Owner = MatFunc; AssetDisplayName = MatFunc->GetName(); } else { - FString LoadError; - Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; Owner = Material; AssetDisplayName = Material->GetName(); } @@ -700,16 +694,14 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json if (!MaterialFunctionName.IsEmpty()) { - FString LoadError; - MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, LoadError); - if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, Result); + if (!MatFunc) return; AssetDisplayName = MatFunc->GetName(); } else { - FString LoadError; - Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; AssetDisplayName = Material->GetName(); } @@ -827,16 +819,14 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs if (!MaterialFunctionName.IsEmpty()) { - FString LoadError; - MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, LoadError); - if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, Result); + if (!MatFunc) return; AssetDisplayName = MatFunc->GetName(); } else { - FString LoadError; - Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; AssetDisplayName = Material->GetName(); } @@ -978,16 +968,14 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F if (!MaterialFunctionName.IsEmpty()) { - FString LoadError; - MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, LoadError); - if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, Result); + if (!MatFunc) return; AssetDisplayName = MatFunc->GetName(); } else { - FString LoadError; - Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; AssetDisplayName = Material->GetName(); } @@ -1100,16 +1088,14 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso if (!MaterialFunctionName.IsEmpty()) { - FString LoadError; - MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, LoadError); - if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, Result); + if (!MatFunc) return; AssetDisplayName = MatFunc->GetName(); } else { - FString LoadError; - Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; AssetDisplayName = Material->GetName(); } @@ -1377,16 +1363,14 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json, if (!MaterialFunctionName.IsEmpty()) { - FString LoadError; - MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, LoadError); - if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + MatFunc = UMCPAssetFinder::LoadAsset(MaterialFunctionName, Result); + if (!MatFunc) return; AssetDisplayName = MatFunc->GetName(); } else { - FString LoadError; - Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; } + Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; AssetDisplayName = Material->GetName(); } @@ -1550,12 +1534,8 @@ void FBlueprintMCPServer::HandleSnapshotMaterialGraph(const FJsonObject* Json, F } // Load material - FString LoadError; - UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; MCPUtils::EnsureMaterialGraph(Material); if (!Material->MaterialGraph) @@ -1649,12 +1629,8 @@ void FBlueprintMCPServer::HandleDiffMaterialGraph(const FJsonObject* Json, FJson } // Load material - FString LoadError; - UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; MCPUtils::EnsureMaterialGraph(Material); if (!Material->MaterialGraph) @@ -1815,12 +1791,8 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ } // Load material - FString LoadError; - UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; MCPUtils::EnsureMaterialGraph(Material); if (!Material->MaterialGraph) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialRead.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialRead.cpp index d5974215..36529783 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialRead.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialRead.cpp @@ -361,12 +361,8 @@ void FBlueprintMCPServer::HandleGetMaterialGraph(const FJsonObject* Json, FJsonO FString DecodedName = MCPUtils::UrlDecode(Name); - FString LoadError; - UMaterial* Material = UMCPAssetFinder::LoadAsset(DecodedName, LoadError); - if (!Material) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterial* Material = UMCPAssetFinder::LoadAsset(DecodedName, Result); + if (!Material) return; UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *Material->GetName()); @@ -411,12 +407,8 @@ void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonO return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material")); } - FString LoadError; - UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *Material->GetName()); @@ -798,12 +790,8 @@ void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJs FString DecodedName = MCPUtils::UrlDecode(Name); - FString LoadError; - UMaterialFunction* MF = UMCPAssetFinder::LoadAsset(DecodedName, LoadError); - if (!MF) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterialFunction* MF = UMCPAssetFinder::LoadAsset(DecodedName, Result); + if (!MF) return; UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName()); @@ -882,12 +870,8 @@ void FBlueprintMCPServer::HandleValidateMaterial(const FJsonObject* Json, FJsonO } // Load material - FString LoadError; - UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, LoadError); - if (!Material) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UMaterial* Material = UMCPAssetFinder::LoadAsset(MaterialName, Result); + if (!Material) return; UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *Material->GetName()); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.cpp index 6a135ab8..2312ed63 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.cpp @@ -37,11 +37,8 @@ void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json, // Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references) FEdGraphPinType NewPinType; - FString TypeError; - if (!MCPUtils::ResolveTypeFromString(NewTypeName, NewPinType, TypeError)) - { - return MCPUtils::MakeErrorJson(Result, TypeError); - } + if (!MCPUtils::ResolveTypeFromString(NewTypeName, NewPinType, Result)) + return; // Find the entry node: K2Node_FunctionEntry in a function graph, // or K2Node_CustomEvent in any graph @@ -416,11 +413,8 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ // Resolve param type FEdGraphPinType PinType; - FString TypeError; - if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, TypeError)) - { - return MCPUtils::MakeErrorJson(Result, TypeError); - } + if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result)) + return; // Find the entry node using 3 strategies UK2Node_EditablePinBase* EntryNode = nullptr; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Read.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Read.cpp index cf0cbad5..500d00a0 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Read.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Read.cpp @@ -121,12 +121,8 @@ void FBlueprintMCPServer::HandleGetBlueprint(const FJsonObject* Json, FJsonObjec return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Name, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Name, Result); + if (!BP) return; TSharedRef Tmp = MCPUtils::SerializeBlueprint(BP); MCPUtils::CopyJsonFields(&*Tmp, Result); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h index 006e5a34..8c156f57 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_UserTypes.h @@ -238,20 +238,13 @@ public: virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override { // Find the struct - FString StructError; - UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset(AssetPath, StructError); - if (!Struct) - { - return MCPUtils::MakeErrorJson(Result, StructError); - } + UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset(AssetPath, Result); + if (!Struct) return; // Resolve type FEdGraphPinType PinType; - FString TypeError; - if (!MCPUtils::ResolveTypeFromString(Type, PinType, TypeError)) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Cannot resolve type '%s': %s"), *Type, *TypeError)); - } + if (!MCPUtils::ResolveTypeFromString(Type, PinType, Result)) + return; // Snapshot existing GUIDs so we can find the newly added one TSet ExistingGuids; @@ -311,12 +304,8 @@ public: virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override { // Find the struct - FString StructError; - UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset(AssetPath, StructError); - if (!Struct) - { - return MCPUtils::MakeErrorJson(Result, StructError); - } + UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset(AssetPath, Result); + if (!Struct) return; // Find the property GUID by name FGuid TargetGuid; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.cpp index fcc351a4..b3aa6b4e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Variables.cpp @@ -68,11 +68,8 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso ResolveInput = TypeCategory + TEXT(":") + NewTypeName; } - FString TypeError; - if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, TypeError)) - { - return MCPUtils::MakeErrorJson(Result, TypeError); - } + if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, Result)) + return; // Derive typeCategory from the resolved pin type for the response if (TypeCategory.IsEmpty()) @@ -270,11 +267,8 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject // Resolve the type using the shared helper FEdGraphPinType PinType; - FString TypeError; - if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, TypeError)) - { - return MCPUtils::MakeErrorJson(Result, TypeError); - } + if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, Result)) + return; // Set container type for arrays if (bIsArray) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index bc1d48e3..b6407848 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -78,6 +78,22 @@ extern int32 TrySavePackageSEH( FSavePackageArgs* SaveArgs, ESavePackageResult* OutResult); #endif +// ============================================================ +// MCPErrorCallback +// ============================================================ + +MCPErrorCallback::MCPErrorCallback(std::nullptr_t) + : Func([](const FString&) {}) +{} + +MCPErrorCallback::MCPErrorCallback(FString& OutError) + : Func([&OutError](const FString& Msg) { OutError = Msg; }) +{} + +MCPErrorCallback::MCPErrorCallback(FJsonObject* Result) + : Func([Result](const FString& Msg) { MCPUtils::MakeErrorJson(Result, Msg); }) +{} + // ============================================================ // JSON helpers // ============================================================ @@ -703,7 +719,7 @@ UClass* MCPUtils::FindClassByName(const FString& ClassName) } bool MCPUtils::ResolveTypeFromString( - const FString& TypeName, FEdGraphPinType& OutPinType, FString& OutError) + const FString& TypeName, FEdGraphPinType& OutPinType, MCPErrorCallback Error) { FString TypeLower = TypeName.ToLower(); @@ -776,7 +792,7 @@ bool MCPUtils::ResolveTypeFromString( UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { - OutError = FString::Printf(TEXT("Class '%s' not found for object reference type"), *ClassName); + Error.SetError(FString::Printf(TEXT("Class '%s' not found for object reference type"), *ClassName)); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object; @@ -788,7 +804,7 @@ bool MCPUtils::ResolveTypeFromString( UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { - OutError = FString::Printf(TEXT("Class '%s' not found for soft object reference type"), *ClassName); + Error.SetError(FString::Printf(TEXT("Class '%s' not found for soft object reference type"), *ClassName)); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftObject; @@ -800,7 +816,7 @@ bool MCPUtils::ResolveTypeFromString( UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { - OutError = FString::Printf(TEXT("Class '%s' not found for class reference type (TSubclassOf)"), *ClassName); + Error.SetError(FString::Printf(TEXT("Class '%s' not found for class reference type (TSubclassOf)"), *ClassName)); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_Class; @@ -812,7 +828,7 @@ bool MCPUtils::ResolveTypeFromString( UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { - OutError = FString::Printf(TEXT("Class '%s' not found for soft class reference type"), *ClassName); + Error.SetError(FString::Printf(TEXT("Class '%s' not found for soft class reference type"), *ClassName)); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftClass; @@ -824,7 +840,7 @@ bool MCPUtils::ResolveTypeFromString( UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { - OutError = FString::Printf(TEXT("Class '%s' not found for interface reference type"), *ClassName); + Error.SetError(FString::Printf(TEXT("Class '%s' not found for interface reference type"), *ClassName)); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_Interface; @@ -900,9 +916,9 @@ bool MCPUtils::ResolveTypeFromString( } else { - OutError = FString::Printf( + Error.SetError(FString::Printf( TEXT("Unknown type '%s'. Use: bool, int, float, string, name, text, byte, vector, rotator, transform, object, a struct/enum name (e.g. FVector, EMyEnum), or colon syntax for references (object:Actor, softobject:Actor, class:Actor, softclass:Actor, interface:MyInterface)"), - *TypeName); + *TypeName)); return false; } } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/TODO.md b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/TODO.md new file mode 100644 index 00000000..8ced78d4 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/TODO.md @@ -0,0 +1,5 @@ +# BlueprintMCP TODO + +- When compiling blueprints, always check for errors properly. Many handlers call `FKismetEditorUtilities::CompileBlueprint` and assume success without checking. The validation handler (`MCPHandlers_Validation.h`) already has proper error collection logic — all compile sites should follow that pattern. + +- Add `FindBlueprintGraph` to `UMCPAssetFinder`: load blueprint, URL-decode graph name, search all graphs by name, return `UEdGraph*` or nullptr with error. Currently ~10 handlers duplicate this 25-line pattern (see `MCPHandlers_Mutation.h:186-210` for a typical example). diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h index 7b19e507..b2a1ab23 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h @@ -5,6 +5,7 @@ #include "AssetRegistry/AssetData.h" #include "StructUtils/UserDefinedStruct.h" #include "Engine/UserDefinedEnum.h" +#include "MCPUtils.h" #include "MCPAssetFinder.generated.h" class UBlueprint; @@ -45,36 +46,38 @@ public: static TArray SearchAssets(FName Class, const FString& NameOrPath, FString* OutError = nullptr); static TArray SearchAssets(UClass *Class, const FString& NameOrPath, FString* OutError = nullptr); - // Load an asset from an FAssetData. Returns nullptr and sets OutError on failure. + // Load an asset from an FAssetData. Returns nullptr and reports error on failure. template - static T* LoadAsset(FAssetData& Asset, FString& OutError) + static T* LoadAsset(FAssetData& Asset, MCPErrorCallback Error) { T* Result = Cast(Asset.GetAsset()); if (!Result) - OutError = FString::Printf(TEXT("Asset '%s' found but could not be loaded as %s."), - *Asset.AssetName.ToString(), *T::StaticClass()->GetName()); + Error.SetError(FString::Printf(TEXT("Asset '%s' found but could not be loaded as %s."), + *Asset.AssetName.ToString(), *T::StaticClass()->GetName())); return Result; } - // Load an asset by name or path. Returns nullptr and sets OutError on failure. + // Load an asset by name or path. Returns nullptr and reports error on failure. template - static T* LoadAsset(const FString& NameOrPath, FString& OutError) + static T* LoadAsset(const FString& NameOrPath, MCPErrorCallback Error) { - FAssetData* Asset = FindAsset(T::StaticClass(), NameOrPath, &OutError); + FString FindError; + FAssetData* Asset = FindAsset(T::StaticClass(), NameOrPath, &FindError); if (!Asset) { - if (OutError.IsEmpty()) - OutError = FString::Printf(TEXT("'%s' not found."), *NameOrPath); + if (FindError.IsEmpty()) + FindError = FString::Printf(TEXT("'%s' not found."), *NameOrPath); + Error.SetError(FindError); return nullptr; } - return LoadAsset(*Asset, OutError); + return LoadAsset(*Asset, Error); } static FAssetData* FindAnyAsset(const FString& NameOrPath, FString* OutError = nullptr); // Load a blueprint or level blueprint from an asset (handles UWorld → level blueprint extraction). - static UBlueprint* LoadBlueprintOrLevelBlueprint(FAssetData& Asset, FString& OutError); - static UBlueprint* LoadBlueprintOrLevelBlueprint(const FString& NameOrPath, FString& OutError); + static UBlueprint* LoadBlueprintOrLevelBlueprint(FAssetData& Asset, MCPErrorCallback Error); + static UBlueprint* LoadBlueprintOrLevelBlueprint(const FString& NameOrPath, MCPErrorCallback Error); private: // Fetch assets from the Unreal asset registry and store them locally. diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 713a8952..e4a0208c 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -100,6 +100,8 @@ public: } }; +struct MCPErrorCallback; + // Stateless utility functions used by MCP handlers and the MCP server. // This is effectively a namespace — all methods are static. class MCPUtils @@ -141,7 +143,7 @@ public: // ----- Type resolution ----- static UClass* FindClassByName(const FString& ClassName); - static bool ResolveTypeFromString(const FString& TypeName, FEdGraphPinType& OutPinType, FString& OutError); + static bool ResolveTypeFromString(const FString& TypeName, FEdGraphPinType& OutPinType, MCPErrorCallback Error); // ----- Material helpers ----- static void EnsureMaterialGraph(UMaterial* Material); @@ -166,3 +168,16 @@ public: private: static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json); }; + +// ----- Error callback ----- + +struct MCPErrorCallback +{ + TFunction Func; + + MCPErrorCallback(std::nullptr_t); + MCPErrorCallback(FString& OutError); + MCPErrorCallback(FJsonObject* Result); + + void SetError(const FString& Msg) const { Func(Msg); } +};