From e33bfefec74e33d34126c6981154a1566c4b8e8d Mon Sep 17 00:00:00 2001 From: jyelon Date: Sat, 14 Mar 2026 01:31:06 -0400 Subject: [PATCH] More refactoring. --- .../BlueprintMCP/Handlers/Property_Dump.h | 2 +- .../BlueprintMCP/Handlers/Property_Get.h | 6 +- .../BlueprintMCP/Handlers/Property_Set.h | 8 +- .../BlueprintMCP/Handlers/ShowCommands.h | 15 +- .../BlueprintMCP/Private/MCPFetcher.cpp | 76 ++++----- .../BlueprintMCP/Private/MCPProperty.cpp | 38 +++-- .../Source/BlueprintMCP/Public/MCPFetcher.h | 151 +++++++++++------- .../Source/BlueprintMCP/Public/MCPProperty.h | 4 + 8 files changed, 166 insertions(+), 134 deletions(-) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Dump.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Dump.h index 3dddd397..2937298b 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Dump.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Dump.h @@ -41,7 +41,7 @@ public: { // Resolve the path to an object and get its editable template. MCPFetcher F; - UObject* Template = F.Walk(Path).Template().Cast(); + UObject* Template = F.Walk(Path).Cast(); if (!Template) return; TArray Props = MCPProperty::GetAllSubstring(Template, CPF_Edit, Query); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Get.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Get.h index acbac0cd..04176229 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Get.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Get.h @@ -33,10 +33,10 @@ public: virtual void Handle() override { MCPFetcher F; - UObject* Template = F.Walk(Path).Template().Cast(); - if (!Template) return; + UObject* Obj = F.Walk(Path).Cast(); + if (!Obj) return; - MCPProperty P = MCPProperty::GetOneExactMatch(Template, CPF_Edit, Property); + MCPProperty P = MCPProperty::GetOneExactMatch(Obj, CPF_Edit, Property); if (!P) return; UMCPServer::Print(P.GetText()); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Set.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Set.h index 361fe147..0bb9c30f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Set.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Property_Set.h @@ -35,8 +35,8 @@ public: { // Resolve the path to an object and get its editable template. MCPFetcher F; - UObject* Template = F.Walk(Path).Template().Cast(); - if (!Template) return; + UObject* Obj = F.Walk(Path).Cast(); + if (!Obj) return; if (!Properties.Json || Properties.Json->Values.Num() == 0) { @@ -48,7 +48,7 @@ public: TArray> Resolved; for (const auto& Pair : Properties.Json->Values) { - MCPProperty P = MCPProperty::GetOneExactMatch(Template, CPF_Edit, Pair.Key); + MCPProperty P = MCPProperty::GetOneExactMatch(Obj, CPF_Edit, Pair.Key); if (!P) return; FString ValueStr; @@ -70,7 +70,7 @@ public: } // Save. - bool bSaved = MCPUtils::SaveGenericPackage(Template); + bool bSaved = MCPUtils::SaveGenericPackage(Obj); UMCPServer::Printf(TEXT("Set %d/%d properties.\n"), SuccessCount, Properties.Json->Values.Num()); if (!bSaved) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h index bdcc3f39..c3fb842c 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/ShowCommands.h @@ -68,19 +68,8 @@ public: virtual void Handle() override { UMCPServer::Printf(TEXT("\n")); - EmitCommandList(); - - // Append Path documentation. - UMCPServer::Print(TEXT("\n")); - UMCPServer::Print(TEXT("Some commands take a Path parameter. A Path starts with an asset\n")); - UMCPServer::Print(TEXT("package path (e.g. /Game/Widgets/WB_Hotkeys), followed by zero or\n")); - UMCPServer::Print(TEXT("more comma-separated steps that navigate into the asset:\n")); - UMCPServer::Print(TEXT("\n")); - for (const MCPFetcher::FWalker& W : MCPFetcher::GetWalkerTable()) - { - UMCPServer::Printf(TEXT(" %s — %s\n"), W.Key, W.Description); - } - UMCPServer::Print(TEXT("\nExample: /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self_Reference_03,pin:Result\n")); + UMCPServer::Printf(TEXT("\n")); + MCPFetcher::PrintDocs(); } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp index 339016ae..94632123 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPFetcher.cpp @@ -15,6 +15,30 @@ #include "Engine/LevelScriptBlueprint.h" #include "Subsystems/AssetEditorSubsystem.h" +MCPFetcher::WalkFunc MCPFetcher::GetWalker(const FString& Step) +{ + if (Step.Equals(TEXT("graph"), ESearchCase::IgnoreCase)) return &MCPFetcher::Graph; + if (Step.Equals(TEXT("node"), ESearchCase::IgnoreCase)) return &MCPFetcher::Node; + if (Step.Equals(TEXT("pin"), ESearchCase::IgnoreCase)) return &MCPFetcher::Pin; + if (Step.Equals(TEXT("component"), ESearchCase::IgnoreCase)) return &MCPFetcher::Component; + if (Step.Equals(TEXT("levelblueprint"), ESearchCase::IgnoreCase)) return &MCPFetcher::LevelBlueprint; + return nullptr; +} + +void MCPFetcher::PrintDocs() +{ + UMCPServer::Print(TEXT("Some commands take a Path parameter. A Path starts with an asset\n")); + UMCPServer::Print(TEXT("package path (e.g. /Game/Widgets/WB_Hotkeys), followed by zero or\n")); + UMCPServer::Print(TEXT("more comma-separated steps that navigate into the asset:\n\n")); + UMCPServer::Print(TEXT(" graph — Find a named UEdGraph (blank name for material graphs)\n")); + UMCPServer::Print(TEXT(" node — Find a named UEdGraphNode within a graph or blueprint\n")); + UMCPServer::Print(TEXT(" pin — Find a named UEdGraphPin on a node\n")); + UMCPServer::Print(TEXT(" component — Find a named component in a Blueprint's SCS\n")); + UMCPServer::Print(TEXT(" levelblueprint — Get the level blueprint from a UWorld\n")); + UMCPServer::Print(TEXT("\nExample: /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self_Reference_03,pin:Result\n")); +} + + void MCPFetcher::SetObj(UObject* InObj) { UMCPServer::AddTouchedObject(InObj); Obj = InObj; ResultPin = nullptr; } void MCPFetcher::SetPin(UEdGraphPin* InPin) { ResultPin = InPin; Obj = nullptr; } @@ -36,17 +60,7 @@ MCPFetcher& MCPFetcher::TypeMismatch(const TCHAR* Walker, const TCHAR* Expected) return *this; } -const TArray& MCPFetcher::GetWalkerTable() -{ - static TArray Table = { - { TEXT("graph"), TEXT("Find a named UEdGraph (blank name for material graphs)"), &MCPFetcher::Graph }, - { TEXT("node"), TEXT("Find a named UEdGraphNode within a graph or blueprint"), &MCPFetcher::Node }, - { TEXT("pin"), TEXT("Find a named UEdGraphPin on a node"), &MCPFetcher::Pin }, - { TEXT("component"), TEXT("Find a named component in a Blueprint's SCS"), &MCPFetcher::Component }, - { TEXT("levelblueprint"), TEXT("Get the level blueprint from a UWorld"), &MCPFetcher::LevelBlueprint }, - }; - return Table; -} + MCPFetcher& MCPFetcher::Walk(const FString& Path) { @@ -73,13 +87,13 @@ MCPFetcher& MCPFetcher::Walk(const FString& Path) if (!Segments[i].Split(TEXT(":"), &Key, &Value)) Key = Segments[i]; - const FWalker* W = GetWalker(Key); - if (!W) + WalkFunc Func = GetWalker(Key); + if (!Func) { UMCPServer::Printf(TEXT("ERROR: Unknown path step '%s'\n"), *Key); return SetError(); } - (this->*W->Func)(Value); + (this->*Func)(Value); if (bError) return *this; } @@ -135,16 +149,6 @@ bool MCPFetcher::CheckAssetIsA(UClass* StaticClass) return true; } -const MCPFetcher::FWalker* MCPFetcher::GetWalker(const FString& Key) -{ - for (const FWalker& W : GetWalkerTable()) - { - if (Key.Equals(W.Key, ESearchCase::IgnoreCase)) - return &W; - } - return nullptr; -} - MCPFetcher& MCPFetcher::Graph(const FString& Value) { if (bError) return *this; @@ -330,30 +334,6 @@ MCPFetcher& MCPFetcher::LevelBlueprint(const FString& Value) return *this; } -MCPFetcher& MCPFetcher::Template() -{ - if (bError) return *this; - if (!Obj) - { - UMCPServer::Print(TEXT("ERROR: Template: object is null\n")); - return SetError(); - } - - if (UBlueprint* BP = ::Cast(Obj)) - { - if (!BP->GeneratedClass) - { - UMCPServer::Printf(TEXT("ERROR: Blueprint '%s' has no GeneratedClass\n"), *Obj->GetName()); - return SetError(); - } - SetObj(BP->GeneratedClass->GetDefaultObject()); - return *this; - } - - // Everything else is its own template — no navigation needed. - return *this; -} - MCPFetcher& MCPFetcher::ToBlueprint() { if (bError) return *this; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp index f5a48c75..ec39fcbe 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPProperty.cpp @@ -35,24 +35,44 @@ bool MCPProperty::SetText(const FString& Value) return true; } -TArray MCPProperty::GetAll(UObject* Obj, EPropertyFlags Flags) +void MCPProperty::Collect(UObject *Obj, TArray &Props, EPropertyFlags Flags) { - TArray Result; - if (!Obj) return Result; for (TFieldIterator It(Obj->GetClass()); It; ++It) { if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue; - Result.Emplace(*It, Obj); + Props.Emplace(*It, Obj); } +} + +TArray MCPProperty::GetAll(UObject* Obj, EPropertyFlags Flags) +{ + if (!Obj) return {}; + TArray Result; + + // Blueprints don't have editable properties. So + // instead, we fetch properties from the generated CDO, + // which is probably what the user intended. + // + if (UBlueprint *BP = ::Cast(Obj)) + { + if (BP->GeneratedClass == nullptr) + { + UMCPServer::Printf(TEXT("ERROR: Blueprint '%s' has no GeneratedClass\n"), *Obj->GetName()); + return {}; + } + Obj = BP->GeneratedClass->GetDefaultObject(); + } + + Collect(Obj, Result, Flags); + + // If it's a Material Graph node, also collect properties from + // the associated material expression. + // if (UMaterialGraphNode* MatNode = Cast(Obj)) { if (UMaterialExpression* Expr = MatNode->MaterialExpression) { - for (TFieldIterator It(Expr->GetClass()); It; ++It) - { - if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue; - Result.Emplace(*It, Expr); - } + Collect(Expr, Result, Flags); } } return Result; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h index 6aabcc7b..c02d2482 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPFetcher.h @@ -5,79 +5,74 @@ class UEdGraphPin; class IAssetEditorInstance; +struct FWalker; -// Resolves a path string into a UObject or UEdGraphPin. +// MCPFetcher: Load an Asset and find an object within it. +// To find an object, you use a path. This is typical: // -// A path starts with a package path (e.g. "/Game/Widgets/WB_Hotkeys"), -// followed by zero or more comma-separated walker segments of the form -// "key:value". Each segment navigates from the current object to a child. +// F.Walk(TEXT("/Game/Mat/M_Test,graph,node:Param_1")) // -// The list of supported walkers is defined by GetWalkerTable(). +// A path always starts from an asset name. The path above +// starts at a material asset, then it walks to the material +// graph, then from there to a specific graph node. // -// Example paths: -// /Game/Widgets/WB_Hotkeys,graph:ReadLuaConfiguration,node:Self_Reference_03,pin:Result -// /Game/Tangibles/TAN_Character,component:CharacterMesh0 +// Instead of specifying the path as a string, you can also +// specify it using a sequence of procedural steps, like +// this: // -// Builder-style usage: -// MCPFetcher F; -// if (!F.Walk(Path).Ok()) return; +// F.Asset(TEXT("/Game/Materials/M_Test")); +// F.Graph(); +// F.Node(TEXT("Param_1")); // -// MCPFetcher F(ExistingObj); -// if (!F.Graph("EventGraph").Node("MyNode").Ok()) return; +// When you're finally at the object you want, you usually +// use the Cast method to get a pointer to the object. // +// If any step fails, the MCPFetcher will print an error +// message that can be seen by the MCP's caller. It will +// also set an error flag. Once the error flag is set, all +// further ops become no-ops. At that point, attempting a +// Cast will return nullptr. +// + class MCPFetcher { -public: - MCPFetcher() {} - MCPFetcher(UObject* O) : Obj(O) {} +public: + // Walk a path from an asset to an object + // within that asset. If you call walk a + // second time, it will walk additional steps. + // + MCPFetcher& Walk(const FString& Path); - // Starting point is always Asset. + // Walk a path using individual path + // steps instead of a path. All these steps generate + // errors if they cannot find the desired element. + // MCPFetcher& Asset(const FString& PackagePath); - - // Walk one step. MCPFetcher& Graph(const FString& Value); MCPFetcher& Node(const FString& Value); MCPFetcher& Pin(const FString& Value); MCPFetcher& Component(const FString& Value); MCPFetcher& LevelBlueprint(const FString& Value); - // Parse string and walk multiple steps. - MCPFetcher& Walk(const FString& Path); - - // C++-only walk step: resolve to the editable template - // (e.g. Blueprint → CDO, everything else → as-is). - MCPFetcher& Template(); - - // C++-only navigation: drill down to a specific type. + // The following walkers cannot be invoked from + // paths, only procedurally. If the current object + // is already the target type, they do nothing. + // Otherwise, they attempt to convert (e.g. World + // to its level blueprint, Material to its graph). + // MCPFetcher& ToBlueprint(); MCPFetcher& ToGraph(); - - // Walker table entry. - struct FWalker - { - const TCHAR* Key; // e.g. "graph", "node", "matexp" - const TCHAR* Description; // brief help text - MCPFetcher& (MCPFetcher::*Func)(const FString& Value); - }; - - // Returns the static table of all supported walkers. - static const TArray& GetWalkerTable(); - + + // Return true if there haven't been any errors. + // Note that errors always automatically generate + // output to MCPServer::Printf. + // bool Ok() const { return !bError; } - UObject* GetObj() const { return Obj; } - UObject* GetAsset() const { return OriginalAsset; } - template T* CastAsset() - { - if (!CheckAssetIsA(T::StaticClass())) return nullptr; - return ::Cast(OriginalAsset); - } - template - EditorType* CastEditor() - { - if (!CheckAssetIsA(AssetType::StaticClass())) return nullptr; - return static_cast(Editor); - } - + + // Try to fetch the current object as a UObject of + // the specified type. If it isn't one, generates an + // error and returns nullptr. + // template T *Cast() { if (bError) return nullptr; @@ -87,12 +82,55 @@ public: return Result; } + // Get the current object as a UObject if it is one, + // otherwise nullptr. Does not generate errors. + // + UObject* GetObj() const { return Obj; } + + // Get the asset from where it all began: the first + // step in the walk path. If the asset couldn't be + // loaded, returns nullptr. Does not generate errors. + // + UObject* GetAsset() const { return OriginalAsset; } + template T* CastAsset() + { + if (!CheckAssetIsA(T::StaticClass())) return nullptr; + return ::Cast(OriginalAsset); + } + + // When an asset is loaded, an editor is automatically + // opened. Get the editor. You must specify the type + // that you expect the asset to be, and the type to cast + // the editor to. Does not generate errors. + // + template + EditorType* CastEditor() + { + if (!CheckAssetIsA(AssetType::StaticClass())) return nullptr; + return static_cast(Editor); + } + + // Initialize empty. You need to call Asset, or walk + // a path that starts with an asset. + // + MCPFetcher() {} + + // Initialize with an object. From there, you can walk + // to sub-objects. + // + MCPFetcher(UObject* O) : Obj(O) {} + + // Print out the documentation for paths, for the LLM. + // + static void PrintDocs(); + private: - // The Current Object or Pin + + // The Current Object. Only one of these can be non-null. UObject* Obj = nullptr; UEdGraphPin* ResultPin = nullptr; - // The Starting Asset and the Editor we Opened + // The Starting Asset and the Editor we Opened. UObject* OriginalAsset = nullptr; IAssetEditorInstance* Editor = nullptr; @@ -100,12 +138,13 @@ private: bool bError = false; // Internal methods. + using WalkFunc = MCPFetcher& (MCPFetcher::*)(const FString&); void SetObj(UObject* InObj); void SetPin(UEdGraphPin* InPin); MCPFetcher& SetError(); MCPFetcher& TypeMismatch(const TCHAR* Walker, const TCHAR* Expected); - const FWalker* GetWalker(const FString& Key); bool CheckAssetIsA(UClass* StaticClass); + WalkFunc GetWalker(const FString &Step); }; template<> inline UEdGraphPin* MCPFetcher::Cast() diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h index ffd26b28..ddebcad0 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPProperty.h @@ -7,6 +7,7 @@ // the value's storage. operator-> forwards to the FProperty. struct MCPProperty { +public: FProperty* Prop = nullptr; void* Container = nullptr; @@ -23,4 +24,7 @@ struct MCPProperty static TArray GetAllSubstring(UObject* Obj, EPropertyFlags Flags, const FString& Substring); static TArray GetAllExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name); static MCPProperty GetOneExactMatch(UObject* Obj, EPropertyFlags Flags, const FString& Name); + +private: + static void Collect(UObject *Obj, TArray &Props, EPropertyFlags Flags); };