Files
unreal-engine-mcp-system-pl…/Source/UEBlueprintMCPEditor/Private/UEBlueprintMCPEditor.cpp
Bonchellon eba71c4ca8 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>
2026-06-24 01:07:24 +03:00

348 lines
12 KiB
C++

#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