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
// ============================================================
UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(FAssetData& Asset, FString& OutError)
UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(FAssetData& Asset, MCPErrorCallback Error)
{
// Regular blueprint asset
UBlueprint* BP = Cast<UBlueprint>(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);
}

View File

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

View File

@@ -145,12 +145,8 @@ void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject*
}
// Load the Material Instance
FString LoadError;
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MIName, LoadError);
if (!MI)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(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<UMaterialInstanceConstant>(NameParam, LoadError);
if (!MI)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(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<UMaterialInstanceConstant>(MIName, LoadError);
if (!MI)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MIName, Result);
if (!MI) return;
// Capture old parent
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);
// Load material
FString LoadError;
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError);
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterialFunction>(MaterialFunctionName, LoadError);
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
if (!MatFunc) return;
Owner = MatFunc;
AssetDisplayName = MatFunc->GetName();
}
else
{
FString LoadError;
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError);
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterialFunction>(MaterialFunctionName, LoadError);
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
if (!MatFunc) return;
AssetDisplayName = MatFunc->GetName();
}
else
{
FString LoadError;
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError);
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterialFunction>(MaterialFunctionName, LoadError);
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
if (!MatFunc) return;
AssetDisplayName = MatFunc->GetName();
}
else
{
FString LoadError;
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError);
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterialFunction>(MaterialFunctionName, LoadError);
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
if (!MatFunc) return;
AssetDisplayName = MatFunc->GetName();
}
else
{
FString LoadError;
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError);
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterialFunction>(MaterialFunctionName, LoadError);
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
if (!MatFunc) return;
AssetDisplayName = MatFunc->GetName();
}
else
{
FString LoadError;
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError);
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterialFunction>(MaterialFunctionName, LoadError);
if (!MatFunc) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
MatFunc = UMCPAssetFinder::LoadAsset<UMaterialFunction>(MaterialFunctionName, Result);
if (!MatFunc) return;
AssetDisplayName = MatFunc->GetName();
}
else
{
FString LoadError;
Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, LoadError);
if (!Material) { MCPUtils::MakeErrorJson(Result, LoadError); return; }
Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterial>(MaterialName, LoadError);
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterial>(MaterialName, LoadError);
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterial>(MaterialName, LoadError);
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
if (!Material) return;
MCPUtils::EnsureMaterialGraph(Material);
if (!Material->MaterialGraph)

View File

@@ -361,12 +361,8 @@ void FBlueprintMCPServer::HandleGetMaterialGraph(const FJsonObject* Json, FJsonO
FString DecodedName = MCPUtils::UrlDecode(Name);
FString LoadError;
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(DecodedName, LoadError);
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterial>(MaterialName, LoadError);
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(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<UMaterialFunction>(DecodedName, LoadError);
if (!MF)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterialFunction* MF = UMCPAssetFinder::LoadAsset<UMaterialFunction>(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<UMaterial>(MaterialName, LoadError);
if (!Material)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
if (!Material) return;
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)
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;

View File

@@ -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<FJsonObject> Tmp = MCPUtils::SerializeBlueprint(BP);
MCPUtils::CopyJsonFields(&*Tmp, Result);

View File

@@ -238,20 +238,13 @@ public:
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
// Find the struct
FString StructError;
UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset<UUserDefinedStruct>(AssetPath, StructError);
if (!Struct)
{
return MCPUtils::MakeErrorJson(Result, StructError);
}
UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset<UUserDefinedStruct>(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<FGuid> ExistingGuids;
@@ -311,12 +304,8 @@ public:
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
// Find the struct
FString StructError;
UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset<UUserDefinedStruct>(AssetPath, StructError);
if (!Struct)
{
return MCPUtils::MakeErrorJson(Result, StructError);
}
UUserDefinedStruct* Struct = UMCPAssetFinder::LoadAsset<UUserDefinedStruct>(AssetPath, Result);
if (!Struct) return;
// Find the property GUID by name
FGuid TargetGuid;

View File

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

View File

@@ -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;
}
}

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 "StructUtils/UserDefinedStruct.h"
#include "Engine/UserDefinedEnum.h"
#include "MCPUtils.h"
#include "MCPAssetFinder.generated.h"
class UBlueprint;
@@ -45,36 +46,38 @@ public:
static TArray<FAssetData*> SearchAssets(FName 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>
static T* LoadAsset(FAssetData& Asset, FString& OutError)
static T* LoadAsset(FAssetData& Asset, MCPErrorCallback Error)
{
T* Result = Cast<T>(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<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 (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<T>(*Asset, OutError);
return LoadAsset<T>(*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.

View File

@@ -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<void(const FString&)> Func;
MCPErrorCallback(std::nullptr_t);
MCPErrorCallback(FString& OutError);
MCPErrorCallback(FJsonObject* Result);
void SetError(const FString& Msg) const { Func(Msg); }
};