From f831da0f0c827ce59e31acbf250c6c18e034eace Mon Sep 17 00:00:00 2001 From: jyelon Date: Tue, 17 Mar 2026 02:11:54 -0400 Subject: [PATCH] GraphNode_Dump, GraphNode_ShowMenu, GraphNode_ChooseMenu now all functional. --- Content/Testing/BP_VarTest2.uasset | 4 +- .../BlueprintExportSubsystem.cpp | 2 +- .../BlueprintExportSubsystem.h | 0 .../Handlers/GraphNode_ChooseMenu.h | 63 +++++ .../BlueprintMCP/Handlers/GraphNode_Dump.h | 40 +++ .../Handlers/GraphNode_ShowMenu.h | 83 +----- .../Source/BlueprintMCP/Handlers/Graph_Dump.h | 4 +- ...{BlueprintExporter.cpp => GraphExport.cpp} | 45 ++-- .../BlueprintMCP/Private/MCPToolMenu.cpp | 247 +++++++++++++----- .../{BlueprintExporter.h => GraphExport.h} | 5 +- .../Source/BlueprintMCP/Public/MCPToolMenu.h | 41 ++- .../Source/BlueprintMCP/Public/MCPUtils.h | 2 +- 12 files changed, 372 insertions(+), 164 deletions(-) rename Plugins/BlueprintMCP/{Source/BlueprintMCP/Private => Deprecated}/BlueprintExportSubsystem.cpp (98%) rename Plugins/BlueprintMCP/{Source/BlueprintMCP/Public => Deprecated}/BlueprintExportSubsystem.h (100%) create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ChooseMenu.h create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Dump.h rename Plugins/BlueprintMCP/Source/BlueprintMCP/Private/{BlueprintExporter.cpp => GraphExport.cpp} (88%) rename Plugins/BlueprintMCP/Source/BlueprintMCP/Public/{BlueprintExporter.h => GraphExport.h} (96%) diff --git a/Content/Testing/BP_VarTest2.uasset b/Content/Testing/BP_VarTest2.uasset index 69c94f48..ad8c8a00 100644 --- a/Content/Testing/BP_VarTest2.uasset +++ b/Content/Testing/BP_VarTest2.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a426feb7372347c28c835a840f6b890468baa0a272ab022a7efefbb2ecc0d529 -size 49393 +oid sha256:fa63fd28082b228d968772edf2460867625c5dfe36accffb8d6ce9836e462bf5 +size 47196 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExportSubsystem.cpp b/Plugins/BlueprintMCP/Deprecated/BlueprintExportSubsystem.cpp similarity index 98% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExportSubsystem.cpp rename to Plugins/BlueprintMCP/Deprecated/BlueprintExportSubsystem.cpp index 9bcffe50..239b4a03 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExportSubsystem.cpp +++ b/Plugins/BlueprintMCP/Deprecated/BlueprintExportSubsystem.cpp @@ -54,7 +54,7 @@ void UBlueprintExportSubsystem::OnAssetSaved(const FString& PackageFilename, UPa BP->GetAllGraphs(AllGraphs); for (UEdGraph* Graph : AllGraphs) { - FlxBlueprintExporter Exporter(Graph); + MCPGraphExport Exporter(Graph); FString FilePath = BPDir / Graph->GetName() + TEXT(".txt"); FString DetailsPath = BPDir / TEXT("DETAILS") / Graph->GetName() + TEXT(".txt"); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintExportSubsystem.h b/Plugins/BlueprintMCP/Deprecated/BlueprintExportSubsystem.h similarity index 100% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintExportSubsystem.h rename to Plugins/BlueprintMCP/Deprecated/BlueprintExportSubsystem.h diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ChooseMenu.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ChooseMenu.h new file mode 100644 index 00000000..785d1bc0 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ChooseMenu.h @@ -0,0 +1,63 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPFetcher.h" +#include "MCPToolMenu.h" +#include "MCPServer.h" +#include "ToolMenus.h" +#include "GraphNode_ChooseMenu.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCP_GraphNode_ChooseMenu : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Target node")) + FString Node; + + UPROPERTY(meta=(Description="Menu item as shown by GraphNode_ShowMenu")) + FString Item; + + virtual FString GetDescription() const override + { + return TEXT("Execute a context menu action on a node or pin. " + "Supports SplitStructPin, AddPin, AddArrayElementPin, etc. " + "Use GraphNode_ShowMenu to see available actions. "); + } + +private: + virtual void Handle() override + { + MCPFetcher F; + UEdGraphNode* NodeObj = F.Walk(Node).Cast(); + if (!NodeObj) return; + + FToolMenuContext Context; + TArray Entries = MCPToolMenu::GetMenuItems(NodeObj, Context); + for (FToolMenuEntry &Entry : Entries) + { + FString LabelText = Entry.Label.Get().ToString(); + if (!LabelText.Equals(Item, ESearchCase::IgnoreCase)) + continue; + + if (MCPToolMenu::Execute(Entry, Context)) + { + UMCPServer::Printf(TEXT("Executed: %s\n"), *LabelText); + } + else + { + UMCPServer::Printf(TEXT("ERROR: Action '%s' cannot execute (greyed out)\n"), *LabelText); + } + return; + } + + UMCPServer::Printf(TEXT("ERROR: Menu item '%s' not found. Use GraphNode_ShowMenu to see available items.\n"), *Item); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Dump.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Dump.h new file mode 100644 index 00000000..32cbc94f --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_Dump.h @@ -0,0 +1,40 @@ +#pragma once + +#include "CoreMinimal.h" +#include "MCPHandler.h" +#include "MCPServer.h" +#include "MCPFetcher.h" +#include "GraphExport.h" +#include "GraphNode_Dump.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UMCP_GraphNode_Dump : public UObject, public IMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Target node")) + FString Node; + + virtual FString GetDescription() const override + { + return TEXT("Dump a single node as readable text, including all pins and connections."); + } + +private: + virtual void Handle() override + { + MCPFetcher F; + UEdGraphNode* NodeObj = F.Walk(Node).Cast(); + if (!NodeObj) return; + + MCPGraphExport Exporter(NodeObj); + UMCPServer::Print(*Exporter.GetOutput()); + UMCPServer::Print(Exporter.GetDetails()); + } +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ShowMenu.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ShowMenu.h index 153cacc5..d2f73a01 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ShowMenu.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ShowMenu.h @@ -4,11 +4,7 @@ #include "MCPHandler.h" #include "MCPFetcher.h" #include "MCPToolMenu.h" -#include "MCPUtils.h" #include "MCPServer.h" -#include "BlueprintEditor.h" -#include "EdGraph/EdGraphNode.h" -#include "EdGraph/EdGraphSchema.h" #include "ToolMenus.h" #include "GraphNode_ShowMenu.generated.h" @@ -31,82 +27,19 @@ public: return TEXT("Show context menu actions available for a node and its pins."); } +private: virtual void Handle() override { MCPFetcher F; - UEdGraphNode* FoundNode = F.Walk(Node).Cast(); - if (!FoundNode) return; + UEdGraphNode* NodeObj = F.Walk(Node).Cast(); + if (!NodeObj) return; - // Get the blueprint editor's command list to build a proper context. - FBlueprintEditor* Editor = F.CastEditor(); - const TSharedPtr& CommandList = Editor - ? MCPToolMenu::GetGraphEditorCommands(*Editor) : EmptyCommandList; - FToolMenuContext MenuContext(CommandList); - - UEdGraph* Graph = FoundNode->GetGraph(); - if (!Graph) return; - - const UEdGraphSchema* Schema = Graph->GetSchema(); - if (!Schema) return; - - // Print actions for the node itself (no pin). - PrintActions(FoundNode, Graph, Schema, nullptr, MenuContext); - - // Print actions for each pin. - for (UEdGraphPin* Pin : FoundNode->Pins) + FToolMenuContext Context; + TArray Entries = MCPToolMenu::GetMenuItems(NodeObj, Context); + for (FToolMenuEntry &Entry : Entries) { - PrintActions(FoundNode, Graph, Schema, Pin, MenuContext); + FString LabelText = Entry.Label.Get().ToString(); + UMCPServer::Printf(TEXT("%s\n"), *LabelText); } } - -private: - - TSharedPtr EmptyCommandList; - - void PrintMenu(UToolMenu* Menu, const TCHAR* Header, const FToolMenuContext& MenuContext) - { - bool bAnyEntries = false; - for (const FToolMenuSection& Section : Menu->Sections) - { - for (const FToolMenuEntry& Entry : Section.Blocks) - { - FText Label = Entry.Label.Get(); - if (Label.IsEmpty()) - continue; - - bool bCanExecute = MCPToolMenu::CanExecute(Entry, MenuContext); - - if (!bAnyEntries) - UMCPServer::Printf(TEXT(" %s:\n"), Header); - if (bCanExecute) - UMCPServer::Printf(TEXT(" %s (can execute)\n"), *Label.ToString()); - else - UMCPServer::Printf(TEXT(" %s\n"), *Label.ToString()); - bAnyEntries = true; - } - } - } - - void PrintActions(UEdGraphNode* FoundNode, UEdGraph* Graph, const UEdGraphSchema* Schema, - const UEdGraphPin* Pin, const FToolMenuContext& MenuContext) - { - UGraphNodeContextMenuContext* Context = NewObject(); - Context->Init(Graph, FoundNode, Pin, false); - - // Print header. - if (Pin) - UMCPServer::Printf(TEXT("\nPin: %s\n"), *MCPUtils::FormatName(Pin)); - else - UMCPServer::Printf(TEXT("Node: %s\n"), *MCPUtils::FormatName(FoundNode)); - - // Gather and print node-level actions. - UToolMenu* NodeMenu = NewObject(); - FoundNode->GetNodeContextMenuActions(NodeMenu, Context); - PrintMenu(NodeMenu, TEXT("Node Actions"), MenuContext); - - // Gather and print schema-level actions. - UToolMenu* SchemaMenu = NewObject(); - Schema->GetContextMenuActions(SchemaMenu, Context); - PrintMenu(SchemaMenu, TEXT("Schema Actions"), MenuContext); - } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Graph_Dump.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Graph_Dump.h index 5e5243a7..38b82ccd 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Graph_Dump.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/Graph_Dump.h @@ -5,7 +5,7 @@ #include "MCPServer.h" #include "MCPFetcher.h" #include "MCPUtils.h" -#include "BlueprintExporter.h" +#include "GraphExport.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "Materials/Material.h" @@ -40,7 +40,7 @@ public: UEdGraph *G = F.Walk(Graph).ToGraph().Cast(); if (!G) return; - FlxBlueprintExporter Exporter(G); + MCPGraphExport Exporter(G); UMCPServer::Print(*Exporter.GetOutput()); if (bDetails) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/GraphExport.cpp similarity index 88% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp rename to Plugins/BlueprintMCP/Source/BlueprintMCP/Private/GraphExport.cpp index 425f0cf0..063147c4 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintExporter.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/GraphExport.cpp @@ -1,4 +1,4 @@ -#include "BlueprintExporter.h" +#include "GraphExport.h" #include "MCPTypes.h" #include "MCPUtils.h" #include "Engine/Blueprint.h" @@ -13,7 +13,7 @@ #include "K2Node_FunctionEntry.h" #include "MaterialGraph/MaterialGraphNode.h" -FlxBlueprintExporter::FlxBlueprintExporter(UEdGraph* InGraph) +MCPGraphExport::MCPGraphExport(UEdGraph* InGraph) : Graph(InGraph) { SortNodes(); @@ -23,13 +23,24 @@ FlxBlueprintExporter::FlxBlueprintExporter(UEdGraph* InGraph) EmitComments(); } +MCPGraphExport::MCPGraphExport(UEdGraphNode* InNode) + : Graph(InNode->GetGraph()) +{ + SortedNodes.Add(InNode); + Visited.Add(InNode); + EmitLocalVariables(); + EmitDetails(); + EmitGraph(); + EmitComments(); +} + //////////////////////////////////////////////////////// // // General utilities for manipulating UEdGraph nodes. // //////////////////////////////////////////////////////// -UEdGraphPin* FlxBlueprintExporter::GetLinkedTo(UEdGraphPin* Pin) +UEdGraphPin* MCPGraphExport::GetLinkedTo(UEdGraphPin* Pin) { while (true) { @@ -41,7 +52,7 @@ UEdGraphPin* FlxBlueprintExporter::GetLinkedTo(UEdGraphPin* Pin) } } -bool FlxBlueprintExporter::IsDefaultToSelf(UEdGraphPin* Pin) +bool MCPGraphExport::IsDefaultToSelf(UEdGraphPin* Pin) { // Only valid call-function nodes can have default-to-self. UK2Node_CallFunction* CallNode = Cast(Pin->GetOwningNode()); @@ -60,7 +71,7 @@ bool FlxBlueprintExporter::IsDefaultToSelf(UEdGraphPin* Pin) return Pin->PinName.ToString() == DefaultToSelfPinName; } -TArray FlxBlueprintExporter::FilterPins(UEdGraphNode* Node, EEdGraphPinDirection Direction, FName Category) +TArray MCPGraphExport::FilterPins(UEdGraphNode* Node, EEdGraphPinDirection Direction, FName Category) { TArray Result; for (UEdGraphPin* Pin : Node->Pins) @@ -72,12 +83,12 @@ TArray FlxBlueprintExporter::FilterPins(UEdGraphNode* Node, EEdGra return Result; } -bool FlxBlueprintExporter::HasExecPin(UEdGraphNode* Node, EEdGraphPinDirection Direction) +bool MCPGraphExport::HasExecPin(UEdGraphNode* Node, EEdGraphPinDirection Direction) { return FilterPins(Node, Direction, UEdGraphSchema_K2::PC_Exec).Num() > 0; } -UEdGraphPin* FlxBlueprintExporter::FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction) +UEdGraphPin* MCPGraphExport::FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction) { for (UEdGraphPin* Pin : Node->Pins) { @@ -93,7 +104,7 @@ UEdGraphPin* FlxBlueprintExporter::FindFirstPin(UEdGraphNode* Node, EEdGraphPinD // //////////////////////////////////////////////////////// -FString FlxBlueprintExporter::FormatPinSource(UEdGraphPin* Pin) +FString MCPGraphExport::FormatPinSource(UEdGraphPin* Pin) { // If connected, show source node.pin UEdGraphPin* LinkedTo = GetLinkedTo(Pin); @@ -144,7 +155,7 @@ FString FlxBlueprintExporter::FormatPinSource(UEdGraphPin* Pin) } } -void FlxBlueprintExporter::Traverse(UEdGraphNode* Node) +void MCPGraphExport::Traverse(UEdGraphNode* Node) { if (Visited.Contains(Node)) return; Visited.Add(Node); @@ -165,7 +176,7 @@ void FlxBlueprintExporter::Traverse(UEdGraphNode* Node) Traverse(LinkedPin->GetOwningNode()); } -void FlxBlueprintExporter::SortNodes() +void MCPGraphExport::SortNodes() { // Find starter nodes: have exec output but no exec input. TArray Starters; @@ -196,7 +207,7 @@ void FlxBlueprintExporter::SortNodes() } } -void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node) +void MCPGraphExport::EmitNode(UEdGraphNode* Node) { if (Node->IsA()) return; @@ -247,7 +258,7 @@ void FlxBlueprintExporter::EmitNode(UEdGraphNode* Node) } } -void FlxBlueprintExporter::EmitMaterialProperty(UMaterialExpression* Expression, FProperty* Prop, FStringBuilderBase& Out) +void MCPGraphExport::EmitMaterialProperty(UMaterialExpression* Expression, FProperty* Prop, FStringBuilderBase& Out) { FString ValueStr = MCPUtils::GetPropertyValueText(Expression, Prop); ValueStr.ReplaceInline(TEXT("\r\n"), TEXT(" ")); @@ -263,7 +274,7 @@ void FlxBlueprintExporter::EmitMaterialProperty(UMaterialExpression* Expression, *ValueStr); } -void FlxBlueprintExporter::EmitMaterialProperties(UEdGraphNode* Node, FStringBuilderBase& Out, bool bPrimary) +void MCPGraphExport::EmitMaterialProperties(UEdGraphNode* Node, FStringBuilderBase& Out, bool bPrimary) { UMaterialGraphNode* MatNode = Cast(Node); if (!MatNode || !MatNode->MaterialExpression) return; @@ -280,7 +291,7 @@ void FlxBlueprintExporter::EmitMaterialProperties(UEdGraphNode* Node, FStringBui } } -void FlxBlueprintExporter::EmitLocalVariables() +void MCPGraphExport::EmitLocalVariables() { for (UEdGraphNode* Node : Graph->Nodes) { @@ -299,7 +310,7 @@ void FlxBlueprintExporter::EmitLocalVariables() } } -void FlxBlueprintExporter::EmitGraph() +void MCPGraphExport::EmitGraph() { for (UEdGraphNode* Node : SortedNodes) { @@ -310,7 +321,7 @@ void FlxBlueprintExporter::EmitGraph() Output.Append(TEXT("\n")); } -void FlxBlueprintExporter::EmitDetails() +void MCPGraphExport::EmitDetails() { for (UEdGraphNode* Node : SortedNodes) { @@ -324,7 +335,7 @@ void FlxBlueprintExporter::EmitDetails() } } -void FlxBlueprintExporter::EmitComments() +void MCPGraphExport::EmitComments() { for (UEdGraphNode* CommentNode : SortedNodes) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPToolMenu.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPToolMenu.cpp index b701d080..bf70728e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPToolMenu.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPToolMenu.cpp @@ -2,7 +2,10 @@ #include "ToolMenuEntry.h" #include "ToolMenuDelegates.h" #include "ToolMenuContext.h" -#include "BlueprintEditor.h" +#include "ToolMenus.h" +#include "MCPUtils.h" +#include "EdGraph/EdGraphSchema.h" +#include "EdGraphSchema_K2.h" #include "Framework/Commands/UIAction.h" // ============================================================ @@ -27,66 +30,207 @@ struct MCPPrivateAccessor // ----- FToolMenuEntry::Action ----- -struct FToolMenuEntry_Action_Tag +struct Tag_FToolMenuEntry_Action { using type = FToolUIActionChoice FToolMenuEntry::*; - friend type GetPtr(FToolMenuEntry_Action_Tag); + friend type GetPtr(Tag_FToolMenuEntry_Action); }; -template struct MCPPrivateAccessor; +template struct MCPPrivateAccessor; + +static const FToolUIActionChoice& GetAction(const FToolMenuEntry& Entry) +{ + return Entry.*GetPtr(Tag_FToolMenuEntry_Action()); +} // ----- FToolMenuEntry::Command ----- -struct FToolMenuEntry_Command_Tag +struct Tag_FToolMenuEntry_Command { using type = TSharedPtr FToolMenuEntry::*; - friend type GetPtr(FToolMenuEntry_Command_Tag); + friend type GetPtr(Tag_FToolMenuEntry_Command); }; -template struct MCPPrivateAccessor; +template struct MCPPrivateAccessor; -// ----- FBlueprintEditor::GraphEditorCommands ----- - -struct FBlueprintEditor_Commands_Tag +static bool HasCommand(const FToolMenuEntry& Entry) { - using type = TSharedPtr FBlueprintEditor::*; - friend type GetPtr(FBlueprintEditor_Commands_Tag); -}; -template struct MCPPrivateAccessor; - -// ============================================================ -// Helpers to access private fields of a FToolMenuEntry. -// ============================================================ - -static const FToolUIActionChoice& GetChoice(const FToolMenuEntry& Entry) -{ - return Entry.*GetPtr(FToolMenuEntry_Action_Tag()); -} - -static const TSharedPtr& GetCommand(const FToolMenuEntry& Entry) -{ - return Entry.*GetPtr(FToolMenuEntry_Command_Tag()); + return Entry.*GetPtr(Tag_FToolMenuEntry_Command()) != nullptr; } // ============================================================ -// Resolve a menu entry's callback. +// Given a menu entry label, and a pin name (possibly empty), +// generate a better label for an LLM to type. The goal here +// is to have consistent spacing, consistent casing, so that +// the LLM can easily remember what to type. +// ============================================================ + +FText MCPToolMenu::MakeBetterLabel(const UEdGraphPin *Pin, const FText &EntryLabel) +{ + FString Sanitized = EntryLabel.ToString(); + int32 Dst = 0; + bool Upper = true; + for (int32 Src = 0; Src < Sanitized.Len(); Src++) + { + TCHAR c = Sanitized[Src]; + if (FChar::IsAlnum(c)) + { + if (Upper) c = FChar::ToUpper(c); + Sanitized[Dst++] = c; + Upper = false; + } + else + { + Upper = true; + if ((c <= 0x20)||(c == 0x7F)) continue; + if (c == ':') c = '-'; + Sanitized[Dst++] = c; + } + } + Sanitized.LeftInline(Dst); + if (Pin) + { + Sanitized = FString::Printf(TEXT("Pin:%s:%s"), *MCPUtils::FormatName(Pin), *Sanitized); + } + return FText::FromString(Sanitized); +} + +// ============================================================ +// Check if an array of entries contains a specific label. +// ============================================================ + +bool MCPToolMenu::ContainsText(const TArray &Texts, const FText &Value) +{ + for (const FText &Text : Texts) + { + if (Value.IdenticalTo(Text)) + { + return true; + } + } + return false; +} + +// ============================================================ +// AddEntry — create a synthetic menu entry with a direct action. +// ============================================================ + +void MCPToolMenu::AddEntry(TArray& Entries, UEdGraphPin* Pin, + const TCHAR* Label, FCanExecuteAction CanExec, FExecuteAction Exec) +{ + if (!CanExec.Execute()) + return; + FToolMenuEntry Entry = FToolMenuEntry::InitMenuEntry( + NAME_None, + MakeBetterLabel(Pin, FText::FromString(Label)), + FText::GetEmpty(), + FSlateIcon(), + FUIAction(MoveTemp(Exec), MoveTemp(CanExec))); + Entries.Add(MoveTemp(Entry)); +} + +void MCPToolMenu::AddSyntheticEntries(TArray &Entries, UEdGraphNode *NodePtr) +{ + const UEdGraphSchema_K2 *K2Schema = Cast(NodePtr->GetSchema()); + if (K2Schema == nullptr) return; + // TWeakObjectPtr Node(NodePtr); + for (UEdGraphPin *PinPtr : NodePtr->Pins) + { + if (PinPtr->bHidden) continue; + FEdGraphPinReference Pin(PinPtr); + AddEntry(Entries, PinPtr, TEXT("SplitStructPin"), + [=](){ UEdGraphPin *P=Pin.Get(); return P && K2Schema->CanSplitStructPin(*P); }, + [=](){ UEdGraphPin *P=Pin.Get(); if (P) K2Schema->SplitPin(P); }); + AddEntry(Entries, PinPtr, TEXT("RecombineStructPin"), + [=](){ UEdGraphPin *P=Pin.Get(); return P && K2Schema->CanRecombineStructPin(*P); }, + [=](){ UEdGraphPin *P=Pin.Get(); if (P) K2Schema->RecombinePin(P); }); + } +} + +// ============================================================ +// Get the Menu Items for a given node and pin. This doesn't +// do anything with the labels yet. +// ============================================================ + +TArray MCPToolMenu::GetMenuItems( + UGraphNodeContextMenuContext* GNC, const FToolMenuContext &TMC) +{ + TArray Result; + UToolMenu* Menu = NewObject(); + GNC->Node->GetNodeContextMenuActions(Menu, GNC); + //GNC->Node->GetSchema()->GetContextMenuActions(Menu, GNC); + for (FToolMenuSection& Section : Menu->Sections) + { + Result.Append(Section.Blocks); + } + return Result; +} + +TArray MCPToolMenu::GetMenuItems(UEdGraphNode *Node, const FToolMenuContext &Context) +{ + // Create the two context objects. + TArray Result; + UGraphNodeContextMenuContext* GNC = NewObject(); + + // Fetch the menu items for the node. + GNC->Init(Node->GetGraph(), Node, nullptr, false); + TArray NodeEntries = GetMenuItems(GNC, Context); + + // Improve the labels for the node entries, and also + // record the original labels. + TArray OriginalLabels; + for (FToolMenuEntry &Entry: NodeEntries) + { + FText Label = Entry.Label.Get(); + OriginalLabels.Add(Label); + if (!CanExecute(Entry, Context)) continue; + Entry.Label = MakeBetterLabel(nullptr, Label); + Result.Add(Entry); + } + + // Fetch the Menu items for the pins. Discard + // pins whose original label exactly matches the + // original label of a node entry. + for (const UEdGraphPin *Pin : Node->Pins) + { + if (Pin->bHidden) continue; + FString PinName = MCPUtils::FormatName(Pin); + GNC->Init(Node->GetGraph(), Node, Pin, false); + TArray PinEntries = GetMenuItems(GNC, Context); + for (FToolMenuEntry &PinEntry : PinEntries) + { + FText Label = PinEntry.Label.Get(); + if (!ContainsText(OriginalLabels, Label)) + { + if (CanExecute(PinEntry, Context)) + { + PinEntry.Label = MakeBetterLabel(Pin, Label); + Result.Add(PinEntry); + } + } + } + } + + AddSyntheticEntries(Result, Node); + return Result; +} + +// ============================================================ +// Menu entry resolution // -// There are four callback mechanisms we handle: -// 1. Command — looked up in the context's command list -// 2. FToolUIAction — delegates that take a FToolMenuContext -// 3. FToolDynamicUIAction — same, Blueprint-friendly variant -// 4. FUIAction — plain delegates, no context needed +// We only handle the three Action-based callback mechanisms: +// 1. FToolUIAction — delegates that take a FToolMenuContext +// 2. FToolDynamicUIAction — same, Blueprint-friendly variant +// 3. FUIAction — plain delegates, no context needed +// +// Command-based entries are skipped — they depend on editor +// selection/focus state which we can't reliably provide. // ============================================================ bool MCPToolMenu::CanExecute(const FToolMenuEntry& Entry, const FToolMenuContext& Context) { - const TSharedPtr& Command = GetCommand(Entry); - if (Command.IsValid()) - { - TSharedPtr OutCommandList; - const FUIAction* Found = Entry.GetActionForCommand(Context, OutCommandList); - return Found && Found->IsBound() && Found->CanExecute(); - } + if (HasCommand(Entry)) + return false; - const FToolUIActionChoice& Choice = GetChoice(Entry); + const FToolUIActionChoice& Choice = GetAction(Entry); if (const FToolUIAction* ToolAction = Choice.GetToolUIAction()) { @@ -110,20 +254,10 @@ bool MCPToolMenu::CanExecute(const FToolMenuEntry& Entry, const FToolMenuContext bool MCPToolMenu::Execute(const FToolMenuEntry& Entry, const FToolMenuContext& Context) { - if (!CanExecute(Entry, Context)) + if (HasCommand(Entry)) return false; - - const TSharedPtr& Command = GetCommand(Entry); - if (Command.IsValid()) - { - TSharedPtr OutCommandList; - const FUIAction* Found = Entry.GetActionForCommand(Context, OutCommandList); - if (Found) - return Found->Execute(); - return false; - } - - const FToolUIActionChoice& Choice = GetChoice(Entry); + + const FToolUIActionChoice& Choice = GetAction(Entry); if (const FToolUIAction* ToolAction = Choice.GetToolUIAction()) { @@ -142,8 +276,3 @@ bool MCPToolMenu::Execute(const FToolMenuEntry& Entry, const FToolMenuContext& C return false; } - -const TSharedPtr& MCPToolMenu::GetGraphEditorCommands(const FBlueprintEditor& Editor) -{ - return Editor.*GetPtr(FBlueprintEditor_Commands_Tag()); -} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintExporter.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/GraphExport.h similarity index 96% rename from Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintExporter.h rename to Plugins/BlueprintMCP/Source/BlueprintMCP/Public/GraphExport.h index bd20edc8..e66e6f2f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintExporter.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/GraphExport.h @@ -8,10 +8,11 @@ class UMaterialExpression; -class FlxBlueprintExporter +class MCPGraphExport { public: - FlxBlueprintExporter(UEdGraph* InGraph); + MCPGraphExport(UEdGraph* InGraph); + MCPGraphExport(UEdGraphNode* InNode); const FString GetOutput() { return Output.ToString(); } const FString GetDetails() { return Details.ToString(); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPToolMenu.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPToolMenu.h index 544dc58b..46fd3130 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPToolMenu.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPToolMenu.h @@ -1,10 +1,13 @@ #pragma once #include "CoreMinimal.h" +#include "Framework/Commands/UIAction.h" struct FToolMenuEntry; struct FToolMenuContext; -class FBlueprintEditor; +class UEdGraphNode; +class UEdGraphPin; +class UGraphNodeContextMenuContext; // Utilities for manipulating UToolMenu structures. // Uses the C++ template explicit-instantiation loophole to @@ -12,13 +15,41 @@ class FBlueprintEditor; class MCPToolMenu { public: + // Get the menu items for a given Node. This includes + // the menu items for the pins. The labels are updated to be + // nice labels for an LLM. Only includes action-based entries; + // command-based entries (which depend on editor selection/focus + // state) are excluded. + static TArray GetMenuItems(UEdGraphNode *Node, const FToolMenuContext &TMContext); + // Resolve a menu entry to an executable action and check if it can execute. - static bool CanExecute(const FToolMenuEntry& Entry, const FToolMenuContext& Context); + // Entries that use Command-based callbacks are always false. + static bool CanExecute(const FToolMenuEntry& Entry, const FToolMenuContext& TMContext); // Resolve a menu entry to an executable action and execute it. // Returns false if the action is not active or has no bound delegate. - static bool Execute(const FToolMenuEntry& Entry, const FToolMenuContext& Context); + // Entries that use Command-based callbacks are always false. + static bool Execute(const FToolMenuEntry& Entry, const FToolMenuContext& TMContext); - // Get the GraphEditorCommands from a blueprint editor (private field). - static const TSharedPtr& GetGraphEditorCommands(const FBlueprintEditor& Editor); + +private: + // Add a synthetic menu entry. Calls CanExec immediately; if false, + // the entry is not added. Both lambdas are stored in the FUIAction. + static void AddEntry(TArray& Entries, UEdGraphPin* Pin, + const TCHAR* Label, FCanExecuteAction CanExec, FExecuteAction Exec); + + template + static void AddEntry(TArray& Entries, UEdGraphPin* Pin, + const TCHAR* Label, FC&& CanExec, FE&& Exec) + { + AddEntry(Entries, Pin, Label, + FCanExecuteAction::CreateLambda(Forward(CanExec)), + FExecuteAction::CreateLambda(Forward(Exec))); + } + + static void AddSyntheticEntries(TArray &Entries, UEdGraphNode *Node); + static FText MakeBetterLabel(const UEdGraphPin *Pin, const FText &EntryLabel); + static bool ContainsText(const TArray &Texts, const FText &Value); + static TArray GetMenuItems( + UGraphNodeContextMenuContext *GNC, const FToolMenuContext &TMC); }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 78d7272b..6909b60f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -92,6 +92,7 @@ public: //////////////////////////////////////////////////////// + static void SanitizeNameInPlace(FString& Name); static FString FormatNodeTitle(const UEdGraphNode *Node); // ----- Enum helpers ----- @@ -174,7 +175,6 @@ public: static void FormatCommandHelp(UClass* HandlerClass); private: - static void SanitizeNameInPlace(FString& Name); static void AppendNumericSuffix(FString &Name, int32 N); };