Add MCP combo button to the level editor toolbar
A "MCP" combo button in the top toolbar (LevelEditor.LevelEditorToolBar.User) opens a dropdown with: - Status: live Remote Control reachability (:30010, green/red) + re-check, and a "Start Remote Control server" action. - Active tools (N): submenu listing the catalogue from tools.json (name + desc). - Settings: opens the Setup Wizard tab. The bridge ping is refactored into PingRc(bool) so it can update a cached status silently (menu open) or with the startup notification. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -23,6 +23,14 @@
|
||||
#include "Framework/Docking/TabManager.h"
|
||||
#include "Widgets/Docking/SDockTab.h"
|
||||
#include "Styling/AppStyle.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Interfaces/IPluginManager.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "Dom/JsonValue.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "SMCPWizard.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "UEBlueprintMCPEditor"
|
||||
@ -33,6 +41,42 @@ const TCHAR* MCPZoneCommentText = TEXT("MCP Zone");
|
||||
const TCHAR* LegacyMCPZoneCommentText = TEXT("MCP Draft Zone");
|
||||
const FName MCPWizardTabId(TEXT("MCPSetupWizard"));
|
||||
|
||||
FString MCPPluginDir()
|
||||
{
|
||||
TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("UEBlueprintMCP"));
|
||||
return Plugin.IsValid() ? FPaths::ConvertRelativePathToFull(Plugin->GetBaseDir()) : FString();
|
||||
}
|
||||
|
||||
// Reads the shipped tool catalogue (tools.json). Returns name + description pairs.
|
||||
bool MCPReadTools(TArray<TPair<FString, FString>>& Out)
|
||||
{
|
||||
const FString Dir = MCPPluginDir();
|
||||
if (Dir.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FString Json;
|
||||
if (!FFileHelper::LoadFileToString(Json, *(Dir / TEXT("tools.json"))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
TArray<TSharedPtr<FJsonValue>> Items;
|
||||
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Json);
|
||||
if (!FJsonSerializer::Deserialize(Reader, Items))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const TSharedPtr<FJsonValue>& Item : Items)
|
||||
{
|
||||
const TSharedPtr<FJsonObject> Obj = Item.IsValid() ? Item->AsObject() : nullptr;
|
||||
if (Obj.IsValid())
|
||||
{
|
||||
Out.Emplace(Obj->GetStringField(TEXT("name")), Obj->GetStringField(TEXT("description")));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsMCPZoneComment(const UEdGraphNode* Node)
|
||||
{
|
||||
const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node);
|
||||
@ -126,6 +170,12 @@ void FUEBlueprintMCPEditorModule::StartupModule()
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::NotifyBridgeStatus()
|
||||
{
|
||||
// Startup check: ping and show a one-off notification.
|
||||
PingRc(true);
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::PingRc(bool bNotify)
|
||||
{
|
||||
const FString Url = TEXT("http://127.0.0.1:30010/remote/info");
|
||||
TSharedRef<IHttpRequest> Request = FHttpModule::Get().CreateRequest();
|
||||
@ -133,9 +183,14 @@ void FUEBlueprintMCPEditorModule::NotifyBridgeStatus()
|
||||
Request->SetURL(Url);
|
||||
Request->SetTimeout(3.0f);
|
||||
Request->OnProcessRequestComplete().BindLambda(
|
||||
[](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSucceeded)
|
||||
[this, bNotify](FHttpRequestPtr Req, FHttpResponsePtr Resp, bool bSucceeded)
|
||||
{
|
||||
const bool bOk = bSucceeded && Resp.IsValid() && Resp->GetResponseCode() == 200;
|
||||
CachedRcStatus = bOk ? EMcpRcStatus::Up : EMcpRcStatus::Down;
|
||||
if (!bNotify)
|
||||
{
|
||||
return;
|
||||
}
|
||||
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"));
|
||||
@ -214,11 +269,137 @@ TSharedRef<SDockTab> FUEBlueprintMCPEditorModule::SpawnWizardTab(const FSpawnTab
|
||||
];
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::RegisterMCPToolbarButton()
|
||||
{
|
||||
UToolMenu* Toolbar = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.User");
|
||||
if (!Toolbar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FToolMenuSection& Section = Toolbar->FindOrAddSection("MCP");
|
||||
FToolMenuEntry Entry = FToolMenuEntry::InitComboButton(
|
||||
"MCPMenu",
|
||||
FUIAction(),
|
||||
FNewToolMenuChoice(FNewToolMenuDelegate::CreateRaw(this, &FUEBlueprintMCPEditorModule::BuildMCPStatusMenu)),
|
||||
LOCTEXT("MCPToolbarLabel", "MCP"),
|
||||
LOCTEXT("MCPToolbarTip", "UE Blueprint MCP — status, active tools and settings"),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Settings"));
|
||||
Section.AddEntry(Entry);
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::BuildMCPStatusMenu(UToolMenu* Menu)
|
||||
{
|
||||
// Kick a fresh (silent) ping so the next open reflects current state.
|
||||
PingRc(false);
|
||||
|
||||
// --- Status ---
|
||||
{
|
||||
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPStatus", LOCTEXT("MCPStatusHdr", "Status"));
|
||||
|
||||
FText StatusText;
|
||||
FSlateIcon StatusIcon;
|
||||
switch (CachedRcStatus)
|
||||
{
|
||||
case EMcpRcStatus::Up:
|
||||
StatusText = LOCTEXT("RcUp", "Remote Control: connected (:30010)");
|
||||
StatusIcon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.SuccessWithColor");
|
||||
break;
|
||||
case EMcpRcStatus::Down:
|
||||
StatusText = LOCTEXT("RcDown", "Remote Control: not reachable");
|
||||
StatusIcon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.ErrorWithColor");
|
||||
break;
|
||||
default:
|
||||
StatusText = LOCTEXT("RcUnknown", "Remote Control: checking…");
|
||||
StatusIcon = FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.WarningWithColor");
|
||||
break;
|
||||
}
|
||||
|
||||
Sec.AddMenuEntry("MCPRcStatus", StatusText,
|
||||
LOCTEXT("RcStatusTip", "Click to re-check the Remote Control bridge."),
|
||||
StatusIcon,
|
||||
FUIAction(FExecuteAction::CreateRaw(this, &FUEBlueprintMCPEditorModule::PingRc, false)));
|
||||
|
||||
Sec.AddMenuEntry("MCPStartRC",
|
||||
LOCTEXT("StartRC", "Start Remote Control server"),
|
||||
LOCTEXT("StartRCTip", "Run WebControl.StartServer and enable it on startup."),
|
||||
FSlateIcon(),
|
||||
FUIAction(FExecuteAction::CreateLambda([this]()
|
||||
{
|
||||
if (GEngine)
|
||||
{
|
||||
GEngine->Exec(nullptr, TEXT("WebControl.EnableServerOnStartup 1"));
|
||||
GEngine->Exec(nullptr, TEXT("WebControl.StartServer"));
|
||||
}
|
||||
PingRc(false);
|
||||
})));
|
||||
}
|
||||
|
||||
// --- Tools ---
|
||||
{
|
||||
TArray<TPair<FString, FString>> Tools;
|
||||
MCPReadTools(Tools);
|
||||
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPTools", LOCTEXT("MCPToolsHdr", "Tools"));
|
||||
Sec.AddSubMenu("MCPToolsList",
|
||||
FText::Format(LOCTEXT("ActiveTools", "Active tools ({0})"), FText::AsNumber(Tools.Num())),
|
||||
LOCTEXT("ActiveToolsTip", "The MCP tools this server exposes to your client."),
|
||||
FNewToolMenuDelegate::CreateRaw(this, &FUEBlueprintMCPEditorModule::BuildToolsSubMenu),
|
||||
false,
|
||||
FSlateIcon());
|
||||
}
|
||||
|
||||
// --- Settings ---
|
||||
{
|
||||
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPSettings", LOCTEXT("MCPSettingsHdr", "Settings"));
|
||||
Sec.AddMenuEntry("MCPOpenWizard",
|
||||
LOCTEXT("OpenWizardEntry", "Open Setup Wizard…"),
|
||||
LOCTEXT("OpenWizardEntryTip", "Remote Control, API keys and the full tool list."),
|
||||
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Settings"),
|
||||
FUIAction(FExecuteAction::CreateLambda([]()
|
||||
{
|
||||
FGlobalTabmanager::Get()->TryInvokeTab(MCPWizardTabId);
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::BuildToolsSubMenu(UToolMenu* Menu)
|
||||
{
|
||||
TArray<TPair<FString, FString>> Tools;
|
||||
if (!MCPReadTools(Tools) || Tools.Num() == 0)
|
||||
{
|
||||
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPToolsNone");
|
||||
Sec.AddMenuEntry("MCPNoTools",
|
||||
LOCTEXT("NoTools", "tools.json not found"),
|
||||
LOCTEXT("NoToolsTip", "Run `npm run dump-tools` in the plugin folder to generate the catalogue."),
|
||||
FSlateIcon(),
|
||||
FUIAction(FExecuteAction::CreateLambda([]() {}), FCanExecuteAction::CreateLambda([]() { return false; })));
|
||||
return;
|
||||
}
|
||||
|
||||
FToolMenuSection& Sec = Menu->FindOrAddSection("MCPToolsItems");
|
||||
for (const TPair<FString, FString>& Tool : Tools)
|
||||
{
|
||||
// First line of the description as the tooltip — keeps it readable.
|
||||
FString Tip = Tool.Value;
|
||||
int32 Dot;
|
||||
if (Tip.FindChar(TEXT('.'), Dot) && Dot > 20)
|
||||
{
|
||||
Tip = Tip.Left(Dot + 1);
|
||||
}
|
||||
Sec.AddMenuEntry(FName(*Tool.Key),
|
||||
FText::FromString(Tool.Key),
|
||||
FText::FromString(Tip),
|
||||
FSlateIcon(),
|
||||
FUIAction(FExecuteAction::CreateLambda([]() {})));
|
||||
}
|
||||
}
|
||||
|
||||
void FUEBlueprintMCPEditorModule::RegisterMenus()
|
||||
{
|
||||
FToolMenuOwnerScoped OwnerScoped(this);
|
||||
|
||||
RegisterWizardTabAndMenu();
|
||||
RegisterMCPToolbarButton();
|
||||
|
||||
UToolMenu* Toolbar = UToolMenus::Get()->ExtendMenu("AssetEditor.BlueprintEditor.ToolBar");
|
||||
FToolMenuSection& Section = Toolbar->FindOrAddSection("Settings");
|
||||
|
||||
@ -17,8 +17,14 @@ public:
|
||||
private:
|
||||
friend class FMCPZoneInputProcessor;
|
||||
|
||||
enum class EMcpRcStatus : uint8 { Unknown, Up, Down };
|
||||
|
||||
void RegisterMenus();
|
||||
void RegisterWizardTabAndMenu();
|
||||
void RegisterMCPToolbarButton();
|
||||
void BuildMCPStatusMenu(class UToolMenu* Menu);
|
||||
void BuildToolsSubMenu(class UToolMenu* Menu);
|
||||
void PingRc(bool bNotify);
|
||||
TSharedRef<class SDockTab> SpawnWizardTab(const class FSpawnTabArgs& Args);
|
||||
void NotifyBridgeStatus();
|
||||
void ToggleMCPZoneMode(TWeakPtr<FBlueprintEditor> BlueprintEditor);
|
||||
@ -29,6 +35,7 @@ private:
|
||||
|
||||
bool bMCPZoneModeActive = false;
|
||||
bool bWizardTabRegistered = false;
|
||||
EMcpRcStatus CachedRcStatus = EMcpRcStatus::Unknown;
|
||||
TWeakPtr<FBlueprintEditor> ActiveMCPZoneEditor;
|
||||
TSharedPtr<IInputProcessor> MCPZoneInputProcessor;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user