From c2b6d80f6feb39c400a22993a696f0f839cfe8ac Mon Sep 17 00:00:00 2001 From: jyelon Date: Mon, 16 Mar 2026 07:30:43 -0400 Subject: [PATCH] Hack around right context menus for nodes. --- .../Handlers/GraphNode_ShowMenu.h | 32 ++-- .../BlueprintMCP/Private/MCPToolMenu.cpp | 149 ++++++++++++++++++ .../Source/BlueprintMCP/Private/MCPUtils.cpp | 57 +------ .../Source/BlueprintMCP/Public/MCPToolMenu.h | 24 +++ .../Source/BlueprintMCP/Public/MCPUtils.h | 2 - 5 files changed, 195 insertions(+), 69 deletions(-) create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPToolMenu.cpp create mode 100644 Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPToolMenu.h diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ShowMenu.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ShowMenu.h index 709a4a12..153cacc5 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ShowMenu.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Handlers/GraphNode_ShowMenu.h @@ -3,8 +3,10 @@ #include "CoreMinimal.h" #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" @@ -35,6 +37,12 @@ public: UEdGraphNode* FoundNode = F.Walk(Node).Cast(); if (!FoundNode) 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; @@ -42,18 +50,20 @@ public: if (!Schema) return; // Print actions for the node itself (no pin). - PrintActions(FoundNode, Graph, Schema, nullptr); + PrintActions(FoundNode, Graph, Schema, nullptr, MenuContext); // Print actions for each pin. for (UEdGraphPin* Pin : FoundNode->Pins) { - PrintActions(FoundNode, Graph, Schema, Pin); + PrintActions(FoundNode, Graph, Schema, Pin, MenuContext); } } private: - void PrintMenu(UToolMenu* Menu, const TCHAR* Header) + TSharedPtr EmptyCommandList; + + void PrintMenu(UToolMenu* Menu, const TCHAR* Header, const FToolMenuContext& MenuContext) { bool bAnyEntries = false; for (const FToolMenuSection& Section : Menu->Sections) @@ -64,15 +74,12 @@ private: if (Label.IsEmpty()) continue; - // Check if this is a command-based entry or a direct action. - TSharedPtr OutCommandList; - bool bIsCommand = (Entry.GetActionForCommand(FToolMenuContext(), OutCommandList) != nullptr) - || OutCommandList.IsValid(); + bool bCanExecute = MCPToolMenu::CanExecute(Entry, MenuContext); if (!bAnyEntries) UMCPServer::Printf(TEXT(" %s:\n"), Header); - if (bIsCommand) - UMCPServer::Printf(TEXT(" %s (command)\n"), *Label.ToString()); + if (bCanExecute) + UMCPServer::Printf(TEXT(" %s (can execute)\n"), *Label.ToString()); else UMCPServer::Printf(TEXT(" %s\n"), *Label.ToString()); bAnyEntries = true; @@ -80,7 +87,8 @@ private: } } - void PrintActions(UEdGraphNode* FoundNode, UEdGraph* Graph, const UEdGraphSchema* Schema, const UEdGraphPin* Pin) + void PrintActions(UEdGraphNode* FoundNode, UEdGraph* Graph, const UEdGraphSchema* Schema, + const UEdGraphPin* Pin, const FToolMenuContext& MenuContext) { UGraphNodeContextMenuContext* Context = NewObject(); Context->Init(Graph, FoundNode, Pin, false); @@ -94,11 +102,11 @@ private: // Gather and print node-level actions. UToolMenu* NodeMenu = NewObject(); FoundNode->GetNodeContextMenuActions(NodeMenu, Context); - PrintMenu(NodeMenu, TEXT("Node Actions")); + PrintMenu(NodeMenu, TEXT("Node Actions"), MenuContext); // Gather and print schema-level actions. UToolMenu* SchemaMenu = NewObject(); Schema->GetContextMenuActions(SchemaMenu, Context); - PrintMenu(SchemaMenu, TEXT("Schema Actions")); + PrintMenu(SchemaMenu, TEXT("Schema Actions"), MenuContext); } }; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPToolMenu.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPToolMenu.cpp new file mode 100644 index 00000000..b701d080 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPToolMenu.cpp @@ -0,0 +1,149 @@ +#include "MCPToolMenu.h" +#include "ToolMenuEntry.h" +#include "ToolMenuDelegates.h" +#include "ToolMenuContext.h" +#include "BlueprintEditor.h" +#include "Framework/Commands/UIAction.h" + +// ============================================================ +// Private member access via template explicit-instantiation loophole. +// +// The C++ standard says "the usual access checking rules do not +// apply to names used to specify explicit instantiations." So +// &FToolMenuEntry::Action is legal as a template argument in an +// explicit instantiation, even though Action is private. +// +// The MCPPrivateAccessor template captures the member pointer and exposes it +// through a friend function that we can call from normal code. +// +// See: https://bloglitb.blogspot.com/2011/12/access-to-private-members-safer.html +// ============================================================ + +template +struct MCPPrivateAccessor +{ + friend typename Tag::type GetPtr(Tag) { return M; } +}; + +// ----- FToolMenuEntry::Action ----- + +struct FToolMenuEntry_Action_Tag +{ + using type = FToolUIActionChoice FToolMenuEntry::*; + friend type GetPtr(FToolMenuEntry_Action_Tag); +}; +template struct MCPPrivateAccessor; + +// ----- FToolMenuEntry::Command ----- + +struct FToolMenuEntry_Command_Tag +{ + using type = TSharedPtr FToolMenuEntry::*; + friend type GetPtr(FToolMenuEntry_Command_Tag); +}; +template struct MCPPrivateAccessor; + +// ----- FBlueprintEditor::GraphEditorCommands ----- + +struct FBlueprintEditor_Commands_Tag +{ + 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()); +} + +// ============================================================ +// Resolve a menu entry's callback. +// +// 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 +// ============================================================ + +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(); + } + + const FToolUIActionChoice& Choice = GetChoice(Entry); + + if (const FToolUIAction* ToolAction = Choice.GetToolUIAction()) + { + if (ToolAction->CanExecuteAction.IsBound()) + return ToolAction->CanExecuteAction.Execute(Context); + return ToolAction->ExecuteAction.IsBound(); + } + + if (const FToolDynamicUIAction* DynamicAction = Choice.GetToolDynamicUIAction()) + { + if (DynamicAction->CanExecuteAction.IsBound()) + return DynamicAction->CanExecuteAction.Execute(Context); + return DynamicAction->ExecuteAction.IsBound(); + } + + if (const FUIAction* Action = Choice.GetUIAction()) + return Action->IsBound() && Action->CanExecute(); + + return false; +} + +bool MCPToolMenu::Execute(const FToolMenuEntry& Entry, const FToolMenuContext& Context) +{ + if (!CanExecute(Entry, Context)) + 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); + + if (const FToolUIAction* ToolAction = Choice.GetToolUIAction()) + { + ToolAction->ExecuteAction.ExecuteIfBound(Context); + return true; + } + + if (const FToolDynamicUIAction* DynamicAction = Choice.GetToolDynamicUIAction()) + { + DynamicAction->ExecuteAction.ExecuteIfBound(Context); + return true; + } + + if (const FUIAction* Action = Choice.GetUIAction()) + return Action->Execute(); + + return false; +} + +const TSharedPtr& MCPToolMenu::GetGraphEditorCommands(const FBlueprintEditor& Editor) +{ + return Editor.*GetPtr(FBlueprintEditor_Commands_Tag()); +} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index 442a5b4d..c3724898 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -3,10 +3,6 @@ #include "MCPTypes.h" #include "MCPServer.h" #include "MCPHandler.h" -#include "Dom/JsonValue.h" -#include "Serialization/JsonReader.h" -#include "Serialization/JsonWriter.h" -#include "Serialization/JsonSerializer.h" #include "Engine/Blueprint.h" #include "Engine/MemberReference.h" #include "Engine/World.h" @@ -14,68 +10,28 @@ #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphPin.h" -#include "EdGraphSchema_K2.h" -#include "K2Node_CallFunction.h" -#include "K2Node_Event.h" -#include "K2Node_CustomEvent.h" -#include "K2Node_FunctionEntry.h" -#include "K2Node_EditablePinBase.h" -#include "K2Node_VariableGet.h" -#include "K2Node_VariableSet.h" -#include "K2Node_BreakStruct.h" -#include "K2Node_MakeStruct.h" -#include "K2Node_MacroInstance.h" -#include "K2Node_DynamicCast.h" -#include "K2Node_CallParentFunction.h" -#include "K2Node_IfThenElse.h" +#include "EdGraph/EdGraphSchema.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" #include "UObject/SavePackage.h" #include "UObject/UObjectIterator.h" +#include "UObject/UnrealType.h" #include "Misc/Paths.h" #include "Misc/PackageName.h" // Animation Blueprint support -#include "Animation/AnimBlueprint.h" -#include "Animation/Skeleton.h" -#include "AnimGraphNode_StateMachine.h" -#include "AnimGraphNode_AssetPlayerBase.h" -#include "AnimGraphNode_SequencePlayer.h" -#include "AnimGraphNode_BlendSpacePlayer.h" -#include "AnimGraphNode_Base.h" #include "AnimStateNode.h" #include "AnimStateTransitionNode.h" -#include "AnimStateConduitNode.h" -#include "AnimStateEntryNode.h" #include "AnimationStateMachineGraph.h" -#include "AnimationGraph.h" -#include "AnimationTransitionGraph.h" // Material support #include "Materials/Material.h" #include "Materials/MaterialExpression.h" -#include "Materials/MaterialExpressionScalarParameter.h" -#include "Materials/MaterialExpressionVectorParameter.h" -#include "Materials/MaterialExpressionTextureObjectParameter.h" -#include "Materials/MaterialExpressionTextureSampleParameter2D.h" -#include "Materials/MaterialExpressionStaticSwitchParameter.h" -#include "Materials/MaterialExpressionConstant.h" -#include "Materials/MaterialExpressionConstant3Vector.h" -#include "Materials/MaterialExpressionConstant4Vector.h" -#include "Materials/MaterialExpressionTextureSample.h" -#include "Materials/MaterialExpressionTextureCoordinate.h" -#include "Materials/MaterialExpressionComponentMask.h" -#include "Materials/MaterialExpressionCustom.h" -#include "Materials/MaterialExpressionFunctionInput.h" -#include "Materials/MaterialExpressionFunctionOutput.h" -#include "Materials/MaterialExpressionMaterialFunctionCall.h" #include "Materials/MaterialFunction.h" #include "Materials/MaterialInstanceConstant.h" #include "MaterialGraph/MaterialGraph.h" -#include "MaterialGraph/MaterialGraphNode.h" #include "MaterialGraph/MaterialGraphSchema.h" #include "IMaterialEditor.h" -#include "MaterialEditingLibrary.h" #include "Subsystems/AssetEditorSubsystem.h" // Mesh, animation, texture support @@ -675,10 +631,6 @@ bool MCPUtils::SaveGenericPackage(UObject* Asset) // Anim blueprint helpers // ============================================================ -#include "AnimStateNode.h" -#include "AnimStateTransitionNode.h" -#include "AnimationStateMachineGraph.h" - UAnimationStateMachineGraph* MCPUtils::FindStateMachineGraph(UBlueprint* BP, const FString& GraphName) { TArray AllGraphs; @@ -736,8 +688,6 @@ UAnimStateTransitionNode* MCPUtils::FindTransition(UAnimationStateMachineGraph* // Graph actions (node spawning) // ============================================================ -#include "EdGraph/EdGraphSchema.h" - FString MCPUtils::ActionFullName(const TSharedPtr& Action) { FString Category = Action->GetCategory().ToString(); @@ -787,9 +737,6 @@ TArray> MCPUtils::SearchGraphActions(UEdGraph* // PopulateFromJson — fill a USTRUCT from a JSON object // ============================================================ -#include "UObject/UnrealType.h" -#include "UObject/EnumProperty.h" - // ============================================================ // CollectHandlerClasses — find all concrete IMCPHandler classes // ============================================================ diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPToolMenu.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPToolMenu.h new file mode 100644 index 00000000..544dc58b --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPToolMenu.h @@ -0,0 +1,24 @@ +#pragma once + +#include "CoreMinimal.h" + +struct FToolMenuEntry; +struct FToolMenuContext; +class FBlueprintEditor; + +// Utilities for manipulating UToolMenu structures. +// Uses the C++ template explicit-instantiation loophole to +// bypass access checks — see MCPToolMenu.cpp for details. +class MCPToolMenu +{ +public: + // Resolve a menu entry to an executable action and check if it can execute. + static bool CanExecute(const FToolMenuEntry& Entry, const FToolMenuContext& Context); + + // 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); + + // Get the GraphEditorCommands from a blueprint editor (private field). + static const TSharedPtr& GetGraphEditorCommands(const FBlueprintEditor& Editor); +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 83031b15..78d7272b 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -29,7 +29,6 @@ class UScriptStruct; class UEnum; struct FMemberReference; struct FBPVariableDescription; - // Stateless utility functions used by MCP handlers and the MCP server. // This is effectively a namespace — all methods are static. class MCPUtils @@ -174,7 +173,6 @@ public: static FString GetHandlerGroup(UClass* HandlerClass); static void FormatCommandHelp(UClass* HandlerClass); - private: static void SanitizeNameInPlace(FString& Name); static void AppendNumericSuffix(FString &Name, int32 N);