Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h
2026-03-10 01:42:43 -04:00

271 lines
9.5 KiB
C++

#pragma once
#include "CoreMinimal.h"
#include "Dom/JsonObject.h"
#include "EdGraph/EdGraphPin.h"
class UBlueprint;
class UEdGraph;
class UEdGraphNode;
class UEdGraphPin;
class UMaterial;
class UMaterialExpression;
class UBlueprintNodeSpawner;
class UAnimationStateMachineGraph;
class UAnimStateNode;
class UAnimStateTransitionNode;
class UActorComponent;
class UWorld;
struct FMemberReference;
struct FBPVariableDescription;
// ----- Log capture -----
class FLogCaptureOutputDevice : public FOutputDevice
{
public:
TArray<FString> CapturedErrors;
TArray<FString> CapturedWarnings;
virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override
{
FString Msg(V);
if (Verbosity == ELogVerbosity::Error || Verbosity == ELogVerbosity::Fatal)
{
CapturedErrors.Add(Msg);
return;
}
if (Verbosity == ELogVerbosity::Warning)
{
if (!Msg.Contains(TEXT("BlueprintMCP:")))
{
CapturedWarnings.Add(Msg);
}
return;
}
static const TCHAR* ErrorPatterns[] = {
TEXT("Can't connect pins"),
TEXT("Fixed up function"),
TEXT("is not compatible with"),
TEXT("could not find a pin"),
TEXT("has an invalid"),
TEXT("orphaned pin"),
TEXT("is deprecated"),
TEXT("does not implement"),
TEXT("Missing function"),
TEXT("Unable to find"),
TEXT("Failed to resolve"),
};
for (const TCHAR* Pattern : ErrorPatterns)
{
if (Msg.Contains(Pattern))
{
CapturedWarnings.Add(Msg);
return;
}
}
}
};
// ----- Error callback -----
struct MCPErrorCallback
{
TFunction<void(const FString&)> Func;
MCPErrorCallback(std::nullptr_t);
MCPErrorCallback(FString& OutError);
MCPErrorCallback(FJsonObject* Result);
MCPErrorCallback(const TSharedRef<FJsonObject>& Result) : MCPErrorCallback(&*Result) {}
MCPErrorCallback(FStringBuilderBase& OutResult);
void SetError(const FString& Msg) const { Func(Msg); }
};
// Stateless utility functions used by MCP handlers and the MCP server.
// This is effectively a namespace — all methods are static.
class MCPUtils
{
public:
////////////////////////////////////////////////////////
//
// Name Formatting
//
// The goal here is to centralize the code that outputs
// names, and have everybody use it, so that names are
// used consistently. The secondary goal is to choose
// names that are as uniquely-identifying as is practical.
// It's not always 100% possible to get perfectly unique
// names, though, so our code needs to check for ambiguity.
//
////////////////////////////////////////////////////////
static FString FormatName(const UWorld *World);
static FString FormatName(const UBlueprint *BP);
static FString FormatName(const UActorComponent *C);
static FString FormatName(const UEdGraph *Graph);
static FString FormatName(const UEdGraphNode* Node);
static FString FormatName(const UEdGraphPin *Pin);
static FString FormatName(const FMemberReference &Ref);
static FString FormatName(const FBPVariableDescription &Var);
static FString FormatName(const UClass *Class);
////////////////////////////////////////////////////////
//
// Identifies
//
// Return true if the name identifies the object. The
// FormatName functions, above, always return names that
// identify the object. However, there may be other
// names that also identify the object. Identifying names
// aren't 100% guaranteed to be unique, but very likely.
//
////////////////////////////////////////////////////////
static bool Identifies(const FString &Name, const UWorld *World);
static bool Identifies(const FString &Name, const UBlueprint *BP);
static bool Identifies(const FString &Name, const UActorComponent *C);
static bool Identifies(const FString &Name, const UEdGraph *Graph);
static bool Identifies(const FString &Name, const UEdGraphNode* Node);
static bool Identifies(const FString &Name, const UEdGraphPin *Pin);
static bool Identifies(const FString &Name, const FMemberReference &Ref);
static bool Identifies(const FString &Name, const UClass *Class);
////////////////////////////////////////////////////////
static FString FormatPinType(const FEdGraphPinType& PinType);
static FString FormatPinType(UEdGraphPin* Pin);
// ----- Asset path helpers -----
// Splits "/Game/Foo/Bar" into PackagePath="/Game/Foo" and AssetName="Bar".
// Returns false if the path has no slash or the asset name is empty.
static bool SplitAssetPath(const FString& AssetPath, FString& OutPackagePath, FString& OutAssetName)
{
int32 LastSlash;
if (!AssetPath.FindLastChar('/', LastSlash))
{
return false;
}
OutPackagePath = AssetPath.Left(LastSlash);
OutAssetName = AssetPath.Mid(LastSlash + 1);
return !OutAssetName.IsEmpty();
}
// ----- JSON helpers -----
static FString JsonToString(TSharedRef<FJsonObject> JsonObj);
static TSharedPtr<FJsonObject> ParseBodyJson(const FString& Body);
static FString MakeErrorJson(const FString& Message);
static void MakeErrorJson(FJsonObject* Result, const FString& Message);
static void CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest);
static FString UrlDecode(const FString& EncodedString);
// ----- Enum helpers -----
// Convert enum value to string. If Prefix is specified, strip "Prefix_" from the front.
template<typename T>
static FString EnumToString(TEnumAsByte<T> Value, const FString& Prefix = FString())
{
return EnumToString<T>((T)Value, Prefix);
}
template<typename T>
static FString EnumToString(T Value, const FString& Prefix = FString())
{
UEnum* Enum = StaticEnum<T>();
FString Full = Enum->GetNameStringByValue((int64)Value);
if (!Prefix.IsEmpty() && Full.StartsWith(Prefix))
return Full.Mid(Prefix.Len());
return Full;
}
// Convert string to enum value. If Prefix is specified, prepend it before lookup.
// Returns false and sets error if the string doesn't match any value.
template<typename T>
static bool StringToEnum(const FString& Str, T& OutValue, MCPErrorCallback Error, const FString& Prefix = FString())
{
UEnum* Enum = StaticEnum<T>();
int64 Value = Enum->GetValueByNameString(Prefix + Str);
if (Value == INDEX_NONE)
{
Error.SetError(FString::Printf(TEXT("Invalid value '%s' for %s"), *Str, *Enum->GetName()));
return false;
}
OutValue = (T)Value;
return true;
}
// ----- Blueprint helpers -----
static TArray<UEdGraph*> AllGraphs(UBlueprint* BP);
static TArray<UEdGraph*> AllGraphsNamed(UBlueprint* BP, const FString& Name);
static TArray<UEdGraphNode*> AllNodes(UBlueprint* BP);
template<class T> static TArray<T*> AllNodes(UBlueprint* BP)
{
TArray<T*> Result;
for (UEdGraph* Graph : AllGraphs(BP))
for (UEdGraphNode* Node : Graph->Nodes)
if (T* Typed = Cast<T>(Node))
Result.Add(Typed);
return Result;
}
template<class T> static TArray<T*> AllNodes(UEdGraph* Graph)
{
TArray<T*> Result;
for (UEdGraphNode* Node : Graph->Nodes)
if (T* Typed = Cast<T>(Node))
Result.Add(Typed);
return Result;
}
static TArray<TSharedPtr<FJsonValue>> AllGraphNamesJson(UBlueprint* BP);
static UEdGraphNode* FindNodeByGuid(UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph = nullptr);
static bool SaveBlueprintPackage(UBlueprint* BP);
// ----- Serialization -----
static TSharedRef<FJsonObject> SerializeBlueprint(UBlueprint* BP);
static TSharedPtr<FJsonObject> SerializeGraph(UEdGraph* Graph);
static TSharedPtr<FJsonObject> SerializeNode(UEdGraphNode* Node);
static TSharedPtr<FJsonObject> SerializePin(UEdGraphPin* Pin);
static TSharedPtr<FJsonObject> SerializeMaterialExpression(UMaterialExpression* Expression);
// ----- Type resolution -----
static UClass* FindClassByName(const FString& ClassName);
static bool ResolveTypeFromString(const FString& TypeName, FEdGraphPinType& OutPinType, MCPErrorCallback Error);
// ----- Material helpers -----
static void EnsureMaterialGraph(UMaterial* Material);
static bool SaveMaterialPackage(UMaterial* Material);
static bool SaveGenericPackage(UObject* Asset);
// ----- Anim blueprint helpers -----
static UAnimationStateMachineGraph* FindStateMachineGraph(UBlueprint* BP, const FString& GraphName);
static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName, MCPErrorCallback Error);
static UAnimStateTransitionNode* FindTransition(UAnimationStateMachineGraph* SMGraph, const FString& FromStateName, const FString& ToStateName);
// ----- Node spawners -----
static FString NodeSpawnerFullName(UBlueprintNodeSpawner* Spawner);
static TArray<UBlueprintNodeSpawner*> SearchNodeSpawners(const FString& Query, int32 MaxResults = 0, bool ExactMatch = false, UEdGraph* GraphFilter = nullptr);
// ----- Property population -----
static FString PropertyNameToJsonKey(const FString& PropName);
static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue, MCPErrorCallback Error);
static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json, MCPErrorCallback Error);
// ----- Handler discovery -----
// Collect all concrete IMCPHandler classes, sorted by tool name.
static TArray<UClass*> CollectHandlerClasses();
// ----- Command help -----
// Derive tool name from handler class name: "MCPHandler_FooBar" → "FooBar"
static FString GetToolName(UClass* HandlerClass);
static FString FormatPropertyType(FProperty* Prop);
static void FormatCommandHelp(UClass* HandlerClass, FStringBuilderBase& Result);
private:
static void SanitizeNameInPlace(FString& Name);
static void AppendNumericSuffix(FString &Name, int32 N);
static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json);
};