Unified, portable Unreal Engine MCP system plugin
Merge of the two project copies into one self-contained plugin (the superset: variable_op + variables.py, full pcg_op runtime/declarative/preset ops, the CreateWidgetFloatAnimation widget tool, and full Voxel graph authoring). Made fully project- and machine-agnostic — no hardcoded paths: - New src/projectPaths.js auto-detects the host .uproject (walk-up), project name, Editor build target, log file, and engine install (EngineAssociation via launcher manifest/registry, else installed-engine scan). All overridable via UE_* env vars. - Rewired buildOrchestrator/insights/launcher/insightsExporter/server.js and the Python workers (cpp_scaffold, live_coding, apply_graph, console) off the old C:/Github/ihy, IHY*, E:/UE_Versions and UE_5.7 literals onto the resolver. - Voxel made optional: Build.cs auto-detects the Voxel plugin (env UE_MCP_WITH_VOXEL override) and the C++ compiles to stubs under WITH_MCP_VOXEL, so the module builds in projects without Voxel; .uplugin marks Voxel optional. - De-branded the agent-gateway and docs; scrubbed a leaked API key; excluded node_modules/Binaries/Intermediate/__pycache__/secrets from the repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
347
Source/UEBlueprintMCPEditor/Private/UEBlueprintMCPEditor.cpp
Normal file
347
Source/UEBlueprintMCPEditor/Private/UEBlueprintMCPEditor.cpp
Normal file
@ -0,0 +1,347 @@
|
||||
#include "UEBlueprintMCPEditor.h"
|
||||
|
||||
#include "HttpModule.h"
|
||||
#include "Interfaces/IHttpResponse.h"
|
||||
#include "Misc/CoreDelegates.h"
|
||||
#include "Framework/Notifications/NotificationManager.h"
|
||||
#include "Widgets/Notifications/SNotificationList.h"
|
||||
#include "BlueprintEditor.h"
|
||||
#include "BlueprintEditorContext.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "EdGraphNode_Comment.h"
|
||||
#include "Framework/Application/IInputProcessor.h"
|
||||
#include "Framework/Application/SlateApplication.h"
|
||||
#include "Framework/Commands/UIAction.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "ScopedTransaction.h"
|
||||
#include "SGraphPanel.h"
|
||||
#include "ToolMenu.h"
|
||||
#include "ToolMenuEntry.h"
|
||||
#include "ToolMenuSection.h"
|
||||
#include "ToolMenus.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "UEBlueprintMCPEditor"
|
||||
|
||||
namespace
|
||||
{
|
||||
const TCHAR* MCPZoneCommentText = TEXT("MCP Zone");
|
||||
const TCHAR* LegacyMCPZoneCommentText = TEXT("MCP Draft Zone");
|
||||
|
||||
bool IsMCPZoneComment(const UEdGraphNode* Node)
|
||||
{
|
||||
const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node);
|
||||
return CommentNode
|
||||
&& (CommentNode->NodeComment.Equals(MCPZoneCommentText, ESearchCase::IgnoreCase)
|
||||
|| CommentNode->NodeComment.Equals(LegacyMCPZoneCommentText, ESearchCase::IgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
class FMCPZoneInputProcessor : public IInputProcessor
|
||||
{
|
||||
public:
|
||||
explicit FMCPZoneInputProcessor(FUEBlueprintMCPEditorModule& InOwner)
|
||||
: Owner(InOwner)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
||||
{
|
||||
if (!Owner.bMCPZoneModeActive || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<FBlueprintEditor> Editor = Owner.ActiveMCPZoneEditor.Pin();
|
||||
if (!Editor.IsValid() || !Editor->GetFocusedGraph())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Editor->GetFocusedGraph(), false);
|
||||
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
|
||||
if (!GraphPanel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
|
||||
const FVector2f ScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
|
||||
if (!Geometry.IsUnderLocation(ScreenPosition))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bDragging = true;
|
||||
StartScreenPosition = ScreenPosition;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
||||
{
|
||||
return bDragging && Owner.bMCPZoneModeActive;
|
||||
}
|
||||
|
||||
virtual bool HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override
|
||||
{
|
||||
if (!bDragging || MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bDragging = false;
|
||||
const FVector2f EndScreenPosition = UE::Slate::CastToVector2f(MouseEvent.GetScreenSpacePosition());
|
||||
Owner.TryCreateMCPZoneFromScreenDrag(StartScreenPosition, EndScreenPosition);
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual const TCHAR* GetDebugName() const override
|
||||
{
|
||||
return TEXT("MCPZoneInputProcessor");
|
||||
}
|
||||
|
||||
private:
|
||||
FUEBlueprintMCPEditorModule& Owner;
|
||||
bool bDragging = false;
|
||||
FVector2f StartScreenPosition = FVector2f::ZeroVector;
|
||||
};
|
||||
|
||||
IMPLEMENT_MODULE(FUEBlueprintMCPEditorModule, UEBlueprintMCPEditor)
|
||||
|
||||
void FUEBlueprintMCPEditorModule::StartupModule()
|
||||
{
|
||||
UToolMenus::RegisterStartupCallback(
|
||||
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FUEBlueprintMCPEditorModule::RegisterMenus));
|
||||
|
||||
FCoreDelegates::OnPostEngineInit.AddRaw(this, &FUEBlueprintMCPEditorModule::NotifyBridgeStatus);
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::NotifyBridgeStatus()
|
||||
{
|
||||
const FString Url = TEXT("http://127.0.0.1:30010/remote/info");
|
||||
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
|
||||
Request->SetVerb(TEXT("GET"));
|
||||
Request->SetURL(Url);
|
||||
Request->SetTimeout(3.0f);
|
||||
Request->OnProcessRequestComplete().BindLambda(
|
||||
[](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSucceeded)
|
||||
{
|
||||
const bool bOk = bSucceeded && Resp.IsValid() && Resp->GetResponseCode() == 200;
|
||||
FNotificationInfo Info(bOk
|
||||
? LOCTEXT("MCPBridgeUp", "UE Blueprint MCP bridge ready (Remote Control :30010)")
|
||||
: LOCTEXT("MCPBridgeDown", "UE Blueprint MCP bridge NOT reachable on :30010 — check Remote Control plugin"));
|
||||
Info.ExpireDuration = bOk ? 4.0f : 8.0f;
|
||||
Info.bUseSuccessFailIcons = true;
|
||||
Info.bFireAndForget = true;
|
||||
TSharedPtr<SNotificationItem> Item = FSlateNotificationManager::Get().AddNotification(Info);
|
||||
if (Item.IsValid())
|
||||
{
|
||||
Item->SetCompletionState(bOk ? SNotificationItem::CS_Success : SNotificationItem::CS_Fail);
|
||||
}
|
||||
});
|
||||
Request->ProcessRequest();
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::ShutdownModule()
|
||||
{
|
||||
FCoreDelegates::OnPostEngineInit.RemoveAll(this);
|
||||
|
||||
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
||||
{
|
||||
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
|
||||
MCPZoneInputProcessor.Reset();
|
||||
}
|
||||
|
||||
if (UToolMenus::IsToolMenuUIEnabled())
|
||||
{
|
||||
UToolMenus::UnRegisterStartupCallback(this);
|
||||
UToolMenus::UnregisterOwner(this);
|
||||
}
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::RegisterMenus()
|
||||
{
|
||||
FToolMenuOwnerScoped OwnerScoped(this);
|
||||
UToolMenu* Toolbar = UToolMenus::Get()->ExtendMenu("AssetEditor.BlueprintEditor.ToolBar");
|
||||
FToolMenuSection& Section = Toolbar->FindOrAddSection("Settings");
|
||||
|
||||
Section.AddDynamicEntry("MCPDraftZoneCommands", FNewToolMenuSectionDelegate::CreateLambda([this](FToolMenuSection& InSection)
|
||||
{
|
||||
UBlueprintEditorToolMenuContext* Context = InSection.FindContext<UBlueprintEditorToolMenuContext>();
|
||||
if (!Context || !Context->BlueprintEditor.IsValid() || !Context->GetBlueprintObj())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TWeakPtr<FBlueprintEditor> WeakEditor = Context->BlueprintEditor;
|
||||
FUIAction Action(
|
||||
FExecuteAction::CreateLambda([this, WeakEditor]()
|
||||
{
|
||||
ToggleMCPZoneMode(WeakEditor);
|
||||
}),
|
||||
FCanExecuteAction::CreateLambda([WeakEditor]()
|
||||
{
|
||||
return WeakEditor.IsValid() && WeakEditor.Pin()->GetFocusedGraph() != nullptr;
|
||||
}),
|
||||
FIsActionChecked::CreateLambda([this, WeakEditor]()
|
||||
{
|
||||
return IsMCPZoneModeActive(WeakEditor);
|
||||
}));
|
||||
|
||||
FToolMenuEntry Entry = FToolMenuEntry::InitToolBarButton(
|
||||
FName(TEXT("MCPDraftZone")),
|
||||
FToolUIActionChoice(Action),
|
||||
LOCTEXT("MCPDraftZone_Label", "MCP Zone"),
|
||||
LOCTEXT("MCPDraftZone_Tooltip", "Toggle MCP Zone mode. While active, left-mouse drag in the graph to create a generation zone. Toggle off to remove MCP Zone comments while keeping generated nodes."),
|
||||
FSlateIcon(),
|
||||
EUserInterfaceActionType::ToggleButton);
|
||||
InSection.AddEntry(Entry);
|
||||
}));
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::ToggleMCPZoneMode(TWeakPtr<FBlueprintEditor> BlueprintEditor)
|
||||
{
|
||||
TSharedPtr<FBlueprintEditor> Editor = BlueprintEditor.Pin();
|
||||
if (!Editor.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == Editor)
|
||||
{
|
||||
RemoveMCPZones(Editor->GetBlueprintObj());
|
||||
bMCPZoneModeActive = false;
|
||||
ActiveMCPZoneEditor.Reset();
|
||||
if (MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
||||
{
|
||||
FSlateApplication::Get().UnregisterInputPreProcessor(MCPZoneInputProcessor);
|
||||
MCPZoneInputProcessor.Reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bMCPZoneModeActive)
|
||||
{
|
||||
if (TSharedPtr<FBlueprintEditor> PreviousEditor = ActiveMCPZoneEditor.Pin())
|
||||
{
|
||||
RemoveMCPZones(PreviousEditor->GetBlueprintObj());
|
||||
}
|
||||
}
|
||||
|
||||
bMCPZoneModeActive = true;
|
||||
ActiveMCPZoneEditor = Editor;
|
||||
if (!MCPZoneInputProcessor.IsValid() && FSlateApplication::IsInitialized())
|
||||
{
|
||||
MCPZoneInputProcessor = MakeShared<FMCPZoneInputProcessor>(*this);
|
||||
FSlateApplication::Get().RegisterInputPreProcessor(MCPZoneInputProcessor);
|
||||
}
|
||||
}
|
||||
|
||||
bool FUEBlueprintMCPEditorModule::IsMCPZoneModeActive(TWeakPtr<FBlueprintEditor> BlueprintEditor) const
|
||||
{
|
||||
return bMCPZoneModeActive && ActiveMCPZoneEditor.Pin() == BlueprintEditor.Pin();
|
||||
}
|
||||
|
||||
bool FUEBlueprintMCPEditorModule::TryCreateMCPZoneFromScreenDrag(const FVector2f& StartScreenPosition, const FVector2f& EndScreenPosition)
|
||||
{
|
||||
TSharedPtr<FBlueprintEditor> Editor = ActiveMCPZoneEditor.Pin();
|
||||
if (!Editor.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UBlueprint* Blueprint = Editor->GetBlueprintObj();
|
||||
UEdGraph* Graph = Editor->GetFocusedGraph();
|
||||
if (!Blueprint || !Graph)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TSharedPtr<SGraphEditor> GraphEditor = Editor->OpenGraphAndBringToFront(Graph, false);
|
||||
SGraphPanel* GraphPanel = GraphEditor.IsValid() ? GraphEditor->GetGraphPanel() : nullptr;
|
||||
if (!GraphPanel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FGeometry& Geometry = GraphPanel->GetTickSpaceGeometry();
|
||||
const FVector2f LocalStart = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(StartScreenPosition));
|
||||
const FVector2f LocalEnd = UE::Slate::CastToVector2f(Geometry.AbsoluteToLocal(EndScreenPosition));
|
||||
const FVector2f GraphStart = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalStart));
|
||||
const FVector2f GraphEnd = UE::Slate::CastToVector2f(GraphPanel->PanelCoordToGraphCoord(LocalEnd));
|
||||
|
||||
CreateMCPZone(Graph, Blueprint, GraphStart, GraphEnd);
|
||||
return true;
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::CreateMCPZone(UEdGraph* Graph, UBlueprint* Blueprint, const FVector2f& GraphStart, const FVector2f& GraphEnd) const
|
||||
{
|
||||
if (!Blueprint || !Graph)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FScopedTransaction Transaction(LOCTEXT("CreateMCPDraftZone", "Create MCP Draft Zone"));
|
||||
Graph->Modify();
|
||||
Blueprint->Modify();
|
||||
|
||||
constexpr int32 MinWidth = 240;
|
||||
constexpr int32 MinHeight = 160;
|
||||
const int32 MinX = FMath::FloorToInt(FMath::Min(GraphStart.X, GraphEnd.X));
|
||||
const int32 MinY = FMath::FloorToInt(FMath::Min(GraphStart.Y, GraphEnd.Y));
|
||||
const int32 MaxX = FMath::CeilToInt(FMath::Max(GraphStart.X, GraphEnd.X));
|
||||
const int32 MaxY = FMath::CeilToInt(FMath::Max(GraphStart.Y, GraphEnd.Y));
|
||||
|
||||
UEdGraphNode_Comment* ZoneNode = NewObject<UEdGraphNode_Comment>(Graph);
|
||||
ZoneNode->CreateNewGuid();
|
||||
ZoneNode->NodeComment = MCPZoneCommentText;
|
||||
ZoneNode->bCommentBubbleVisible = false;
|
||||
ZoneNode->CommentColor = FLinearColor(0.05f, 0.32f, 0.85f, 0.35f);
|
||||
ZoneNode->NodePosX = MinX;
|
||||
ZoneNode->NodePosY = MinY;
|
||||
ZoneNode->NodeWidth = FMath::Max(MinWidth, MaxX - MinX);
|
||||
ZoneNode->NodeHeight = FMath::Max(MinHeight, MaxY - MinY);
|
||||
|
||||
Graph->AddNode(ZoneNode, true, false);
|
||||
ZoneNode->PostPlacedNewNode();
|
||||
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::RemoveMCPZones(UBlueprint* Blueprint) const
|
||||
{
|
||||
if (!Blueprint)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FScopedTransaction Transaction(LOCTEXT("RemoveMCPDraftZones", "Remove MCP Draft Zones"));
|
||||
Blueprint->Modify();
|
||||
|
||||
TArray<UEdGraph*> Graphs;
|
||||
Blueprint->GetAllGraphs(Graphs);
|
||||
for (UEdGraph* Graph : Graphs)
|
||||
{
|
||||
if (!Graph)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Graph->Modify();
|
||||
TArray<UEdGraphNode*> Nodes = Graph->Nodes;
|
||||
for (UEdGraphNode* Node : Nodes)
|
||||
{
|
||||
if (IsMCPZoneComment(Node))
|
||||
{
|
||||
Graph->RemoveNode(Node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
Reference in New Issue
Block a user