Add MCPErrorCallback. Simplify error handling.

This commit is contained in:
2026-03-07 01:55:32 -05:00
parent b7b28e9336
commit 282ee3ef33
13 changed files with 134 additions and 180 deletions

View File

@@ -200,7 +200,7 @@ FAssetData* UMCPAssetFinder::FindAnyAsset(const FString& NameOrPath, FString* Ou
// Load helpers // Load helpers
// ============================================================ // ============================================================
UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(FAssetData& Asset, FString& OutError) UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(FAssetData& Asset, MCPErrorCallback Error)
{ {
// Regular blueprint asset // Regular blueprint asset
UBlueprint* BP = Cast<UBlueprint>(Asset.GetAsset()); UBlueprint* BP = Cast<UBlueprint>(Asset.GetAsset());
@@ -214,20 +214,22 @@ UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(FAssetData& Asset, FS
if (LevelBP) return LevelBP; if (LevelBP) return LevelBP;
} }
OutError = FString::Printf(TEXT("Asset '%s' loaded but its level blueprint could not be retrieved."), Error.SetError(FString::Printf(TEXT("Asset '%s' loaded but its level blueprint could not be retrieved."),
*Asset.AssetName.ToString()); *Asset.AssetName.ToString()));
return nullptr; 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 (!Asset)
{ {
if (OutError.IsEmpty()) if (FindError.IsEmpty())
OutError = FString::Printf(TEXT("Blueprint '%s' not found."), *NameOrPath); FindError = FString::Printf(TEXT("Blueprint '%s' not found."), *NameOrPath);
Error.SetError(FindError);
return nullptr; return nullptr;
} }
return LoadBlueprintOrLevelBlueprint(*Asset, OutError); return LoadBlueprintOrLevelBlueprint(*Asset, Error);
} }

View File

@@ -131,12 +131,8 @@ void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJso
if (ParamName.IsEmpty() || ParamType.IsEmpty()) continue; if (ParamName.IsEmpty() || ParamType.IsEmpty()) continue;
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, TypeError)) return;
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Parameter '%s': %s"), *ParamName, *TypeError));
}
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output); EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);

View File

@@ -145,12 +145,8 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject*
} }
// Load the Material Instance // Load the Material Instance
FString LoadError; UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MIName, Result);
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MIName, LoadError); if (!MI) return;
if (!MI)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Determine the parameter type — explicit or auto-detect from parent // Determine the parameter type — explicit or auto-detect from parent
FString TypeStr; FString TypeStr;
@@ -366,12 +362,8 @@ void FBlueprintMCPServer::HandleGetMaterialInstanceParameters(const FJsonObject*
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required query parameter: name")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required query parameter: name"));
} }
FString LoadError; UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(NameParam, Result);
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(NameParam, LoadError); if (!MI) return;
if (!MI)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
Result->SetStringField(TEXT("name"), MI->GetName()); Result->SetStringField(TEXT("name"), MI->GetName());
Result->SetStringField(TEXT("path"), MI->GetPathName()); Result->SetStringField(TEXT("path"), MI->GetPathName());
@@ -614,12 +606,8 @@ void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json
} }
// Load the Material Instance // Load the Material Instance
FString LoadError; UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MIName, Result);
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MIName, LoadError); if (!MI) return;
if (!MI)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
// Capture old parent // Capture old parent
FString OldParentPath = MI->Parent ? MI->Parent->GetPathName() : TEXT("None"); FString OldParentPath = MI->Parent ? MI->Parent->GetPathName() : TEXT("None");

View File

@@ -218,12 +218,8 @@ void FBlueprintMCPServer::HandleSetMaterialProperty(const FJsonObject* Json, FJs
Json->TryGetBoolField(TEXT("dryRun"), bDryRun); Json->TryGetBoolField(TEXT("dryRun"), bDryRun);
// Load material // Load material
FString LoadError; UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
FString OldValue; FString OldValue;
FString NewValue; 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")); return MCPUtils::MakeErrorJson(Result, TEXT("Specify either 'material' or 'materialFunction', not both"));
} }
FString LoadError; MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, LoadError); if (!MatFunc) return;
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Owner = MatFunc; Owner = MatFunc;
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Owner = Material; Owner = Material;
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
@@ -700,16 +694,14 @@ void FBlueprintMCPServer::HandleDeleteMaterialExpression(const FJsonObject* Json
if (!MaterialFunctionName.IsEmpty()) if (!MaterialFunctionName.IsEmpty())
{ {
FString LoadError; MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, LoadError); if (!MatFunc) return;
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
@@ -827,16 +819,14 @@ void FBlueprintMCPServer::HandleConnectMaterialPins(const FJsonObject* Json, FJs
if (!MaterialFunctionName.IsEmpty()) if (!MaterialFunctionName.IsEmpty())
{ {
FString LoadError; MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, LoadError); if (!MatFunc) return;
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
@@ -978,16 +968,14 @@ void FBlueprintMCPServer::HandleDisconnectMaterialPin(const FJsonObject* Json, F
if (!MaterialFunctionName.IsEmpty()) if (!MaterialFunctionName.IsEmpty())
{ {
FString LoadError; MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, LoadError); if (!MatFunc) return;
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
@@ -1100,16 +1088,14 @@ void FBlueprintMCPServer::HandleSetExpressionValue(const FJsonObject* Json, FJso
if (!MaterialFunctionName.IsEmpty()) if (!MaterialFunctionName.IsEmpty())
{ {
FString LoadError; MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, LoadError); if (!MatFunc) return;
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
@@ -1377,16 +1363,14 @@ void FBlueprintMCPServer::HandleMoveMaterialExpression(const FJsonObject* Json,
if (!MaterialFunctionName.IsEmpty()) if (!MaterialFunctionName.IsEmpty())
{ {
FString LoadError; MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, LoadError); if (!MatFunc) return;
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = MatFunc->GetName(); AssetDisplayName = MatFunc->GetName();
} }
else else
{ {
FString LoadError; Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
AssetDisplayName = Material->GetName(); AssetDisplayName = Material->GetName();
} }
@@ -1550,12 +1534,8 @@ void FBlueprintMCPServer::HandleSnapshotMaterialGraph(const FJsonObject* Json, F
} }
// Load material // Load material
FString LoadError; UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
MCPUtils::EnsureMaterialGraph(Material); MCPUtils::EnsureMaterialGraph(Material);
if (!Material->MaterialGraph) if (!Material->MaterialGraph)
@@ -1649,12 +1629,8 @@ void FBlueprintMCPServer::HandleDiffMaterialGraph(const FJsonObject* Json, FJson
} }
// Load material // Load material
FString LoadError; UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
MCPUtils::EnsureMaterialGraph(Material); MCPUtils::EnsureMaterialGraph(Material);
if (!Material->MaterialGraph) if (!Material->MaterialGraph)
@@ -1815,12 +1791,8 @@ void FBlueprintMCPServer::HandleRestoreMaterialGraph(const FJsonObject* Json, FJ
} }
// Load material // Load material
FString LoadError; UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
MCPUtils::EnsureMaterialGraph(Material); MCPUtils::EnsureMaterialGraph(Material);
if (!Material->MaterialGraph) if (!Material->MaterialGraph)

View File

@@ -361,12 +361,8 @@ void FBlueprintMCPServer::HandleGetMaterialGraph(const FJsonObject* Json, FJsonO
FString DecodedName = MCPUtils::UrlDecode(Name); FString DecodedName = MCPUtils::UrlDecode(Name);
FString LoadError; UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(DecodedName, Result);
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(DecodedName, LoadError); if (!Material) return;
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *Material->GetName()); 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")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
} }
FString LoadError; UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *Material->GetName()); 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 DecodedName = MCPUtils::UrlDecode(Name);
FString LoadError; UMaterialFunction* MF = UMCPAssetFinder::LoadAsset<UMaterialFunction>(DecodedName, Result);
UMaterialFunction* MF = UMCPAssetFinder::LoadAsset<UMaterialFunction>(DecodedName, LoadError); if (!MF) return;
if (!MF)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName()); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName());
@@ -882,12 +870,8 @@ void FBlueprintMCPServer::HandleValidateMaterial(const FJsonObject* Json, FJsonO
} }
// Load material // Load material
FString LoadError; UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError); if (!Material) return;
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *Material->GetName()); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *Material->GetName());

View File

@@ -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) // Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references)
FEdGraphPinType NewPinType; FEdGraphPinType NewPinType;
FString TypeError; if (!MCPUtils::ResolveTypeFromString(NewTypeName, NewPinType, Result))
if (!MCPUtils::ResolveTypeFromString(NewTypeName, NewPinType, TypeError)) return;
{
return MCPUtils::MakeErrorJson(Result, TypeError);
}
// Find the entry node: K2Node_FunctionEntry in a function graph, // Find the entry node: K2Node_FunctionEntry in a function graph,
// or K2Node_CustomEvent in any graph // or K2Node_CustomEvent in any graph
@@ -416,11 +413,8 @@ void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJ
// Resolve param type // Resolve param type
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, TypeError)) return;
{
return MCPUtils::MakeErrorJson(Result, TypeError);
}
// Find the entry node using 3 strategies // Find the entry node using 3 strategies
UK2Node_EditablePinBase* EntryNode = nullptr; UK2Node_EditablePinBase* EntryNode = nullptr;

View File

@@ -121,12 +121,8 @@ void FBlueprintMCPServer::HandleGetBlueprint(const FJsonObject* Json, FJsonObjec
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter")); return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
} }
FString LoadError; UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Name, Result);
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Name, LoadError); if (!BP) return;
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
TSharedRef<FJsonObject> Tmp = MCPUtils::SerializeBlueprint(BP); TSharedRef<FJsonObject> Tmp = MCPUtils::SerializeBlueprint(BP);
MCPUtils::CopyJsonFields(&*Tmp, Result); MCPUtils::CopyJsonFields(&*Tmp, Result);

View File

@@ -238,20 +238,13 @@ public:
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{ {
// Find the struct // Find the struct
FString StructError; UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset<UUserDefinedStruct>(AssetPath, Result);
UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset<UUserDefinedStruct>(AssetPath, StructError); if (!Struct) return;
if (!Struct)
{
return MCPUtils::MakeErrorJson(Result, StructError);
}
// Resolve type // Resolve type
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; if (!MCPUtils::ResolveTypeFromString(Type, PinType, Result))
if (!MCPUtils::ResolveTypeFromString(Type, PinType, TypeError)) return;
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Cannot resolve type '%s': %s"), *Type, *TypeError));
}
// Snapshot existing GUIDs so we can find the newly added one // Snapshot existing GUIDs so we can find the newly added one
TSet<FGuid> ExistingGuids; TSet<FGuid> ExistingGuids;
@@ -311,12 +304,8 @@ public:
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{ {
// Find the struct // Find the struct
FString StructError; UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset<UUserDefinedStruct>(AssetPath, Result);
UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset<UUserDefinedStruct>(AssetPath, StructError); if (!Struct) return;
if (!Struct)
{
return MCPUtils::MakeErrorJson(Result, StructError);
}
// Find the property GUID by name // Find the property GUID by name
FGuid TargetGuid; FGuid TargetGuid;

View File

@@ -68,11 +68,8 @@ void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJso
ResolveInput = TypeCategory + TEXT(":") + NewTypeName; ResolveInput = TypeCategory + TEXT(":") + NewTypeName;
} }
FString TypeError; if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, Result))
if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, TypeError)) return;
{
return MCPUtils::MakeErrorJson(Result, TypeError);
}
// Derive typeCategory from the resolved pin type for the response // Derive typeCategory from the resolved pin type for the response
if (TypeCategory.IsEmpty()) if (TypeCategory.IsEmpty())
@@ -270,11 +267,8 @@ void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject
// Resolve the type using the shared helper // Resolve the type using the shared helper
FEdGraphPinType PinType; FEdGraphPinType PinType;
FString TypeError; if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, Result))
if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, TypeError)) return;
{
return MCPUtils::MakeErrorJson(Result, TypeError);
}
// Set container type for arrays // Set container type for arrays
if (bIsArray) if (bIsArray)

View File

@@ -78,6 +78,22 @@ extern int32 TrySavePackageSEH(
FSavePackageArgs* SaveArgs, ESavePackageResult* OutResult); FSavePackageArgs* SaveArgs, ESavePackageResult* OutResult);
#endif #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 // JSON helpers
// ============================================================ // ============================================================
@@ -703,7 +719,7 @@ UClass* MCPUtils::FindClassByName(const FString& ClassName)
} }
bool MCPUtils::ResolveTypeFromString( bool MCPUtils::ResolveTypeFromString(
const FString& TypeName, FEdGraphPinType& OutPinType, FString& OutError) const FString& TypeName, FEdGraphPinType& OutPinType, MCPErrorCallback Error)
{ {
FString TypeLower = TypeName.ToLower(); FString TypeLower = TypeName.ToLower();
@@ -776,7 +792,7 @@ bool MCPUtils::ResolveTypeFromString(
UClass* FoundClass = FindClassByName(ClassName); UClass* FoundClass = FindClassByName(ClassName);
if (!FoundClass) 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; return false;
} }
OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object; OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object;
@@ -788,7 +804,7 @@ bool MCPUtils::ResolveTypeFromString(
UClass* FoundClass = FindClassByName(ClassName); UClass* FoundClass = FindClassByName(ClassName);
if (!FoundClass) 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; return false;
} }
OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftObject; OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftObject;
@@ -800,7 +816,7 @@ bool MCPUtils::ResolveTypeFromString(
UClass* FoundClass = FindClassByName(ClassName); UClass* FoundClass = FindClassByName(ClassName);
if (!FoundClass) 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; return false;
} }
OutPinType.PinCategory = UEdGraphSchema_K2::PC_Class; OutPinType.PinCategory = UEdGraphSchema_K2::PC_Class;
@@ -812,7 +828,7 @@ bool MCPUtils::ResolveTypeFromString(
UClass* FoundClass = FindClassByName(ClassName); UClass* FoundClass = FindClassByName(ClassName);
if (!FoundClass) 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; return false;
} }
OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftClass; OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftClass;
@@ -824,7 +840,7 @@ bool MCPUtils::ResolveTypeFromString(
UClass* FoundClass = FindClassByName(ClassName); UClass* FoundClass = FindClassByName(ClassName);
if (!FoundClass) 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; return false;
} }
OutPinType.PinCategory = UEdGraphSchema_K2::PC_Interface; OutPinType.PinCategory = UEdGraphSchema_K2::PC_Interface;
@@ -900,9 +916,9 @@ bool MCPUtils::ResolveTypeFromString(
} }
else 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)"), 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; return false;
} }
} }

View File

@@ -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).

View File

@@ -5,6 +5,7 @@
#include "AssetRegistry/AssetData.h" #include "AssetRegistry/AssetData.h"
#include "StructUtils/UserDefinedStruct.h" #include "StructUtils/UserDefinedStruct.h"
#include "Engine/UserDefinedEnum.h" #include "Engine/UserDefinedEnum.h"
#include "MCPUtils.h"
#include "MCPAssetFinder.generated.h" #include "MCPAssetFinder.generated.h"
class UBlueprint; class UBlueprint;
@@ -45,36 +46,38 @@ public:
static TArray<FAssetData*> SearchAssets(FName Class, const FString& NameOrPath, FString* OutError = nullptr); static TArray<FAssetData*> SearchAssets(FName Class, const FString& NameOrPath, FString* OutError = nullptr);
static TArray<FAssetData*> SearchAssets(UClass *Class, const FString& NameOrPath, FString* OutError = nullptr); static TArray<FAssetData*> 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<typename T> template<typename T>
static T* LoadAsset(FAssetData& Asset, FString& OutError) static T* LoadAsset(FAssetData& Asset, MCPErrorCallback Error)
{ {
T* Result = Cast<T>(Asset.GetAsset()); T* Result = Cast<T>(Asset.GetAsset());
if (!Result) if (!Result)
OutError = FString::Printf(TEXT("Asset '%s' found but could not be loaded as %s."), Error.SetError(FString::Printf(TEXT("Asset '%s' found but could not be loaded as %s."),
*Asset.AssetName.ToString(), *T::StaticClass()->GetName()); *Asset.AssetName.ToString(), *T::StaticClass()->GetName()));
return Result; 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<typename T> template<typename T>
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 (!Asset)
{ {
if (OutError.IsEmpty()) if (FindError.IsEmpty())
OutError = FString::Printf(TEXT("'%s' not found."), *NameOrPath); FindError = FString::Printf(TEXT("'%s' not found."), *NameOrPath);
Error.SetError(FindError);
return nullptr; return nullptr;
} }
return LoadAsset<T>(*Asset, OutError); return LoadAsset<T>(*Asset, Error);
} }
static FAssetData* FindAnyAsset(const FString& NameOrPath, FString* OutError = nullptr); static FAssetData* FindAnyAsset(const FString& NameOrPath, FString* OutError = nullptr);
// Load a blueprint or level blueprint from an asset (handles UWorld → level blueprint extraction). // Load a blueprint or level blueprint from an asset (handles UWorld → level blueprint extraction).
static UBlueprint* LoadBlueprintOrLevelBlueprint(FAssetData& Asset, FString& OutError); static UBlueprint* LoadBlueprintOrLevelBlueprint(FAssetData& Asset, MCPErrorCallback Error);
static UBlueprint* LoadBlueprintOrLevelBlueprint(const FString& NameOrPath, FString& OutError); static UBlueprint* LoadBlueprintOrLevelBlueprint(const FString& NameOrPath, MCPErrorCallback Error);
private: private:
// Fetch assets from the Unreal asset registry and store them locally. // Fetch assets from the Unreal asset registry and store them locally.

View File

@@ -100,6 +100,8 @@ public:
} }
}; };
struct MCPErrorCallback;
// Stateless utility functions used by MCP handlers and the MCP server. // Stateless utility functions used by MCP handlers and the MCP server.
// This is effectively a namespace — all methods are static. // This is effectively a namespace — all methods are static.
class MCPUtils class MCPUtils
@@ -141,7 +143,7 @@ public:
// ----- Type resolution ----- // ----- Type resolution -----
static UClass* FindClassByName(const FString& ClassName); 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 ----- // ----- Material helpers -----
static void EnsureMaterialGraph(UMaterial* Material); static void EnsureMaterialGraph(UMaterial* Material);
@@ -166,3 +168,16 @@ public:
private: private:
static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json); static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json);
}; };
// ----- Error callback -----
struct MCPErrorCallback
{
TFunction<void(const FString&)> Func;
MCPErrorCallback(std::nullptr_t);
MCPErrorCallback(FString& OutError);
MCPErrorCallback(FJsonObject* Result);
void SetError(const FString& Msg) const { Func(Msg); }
};