diff --git a/Source/UEBlueprintMCPEditor/Private/UEBlueprintMCPEditor.cpp b/Source/UEBlueprintMCPEditor/Private/UEBlueprintMCPEditor.cpp index 0c607c9..50b0a17 100644 --- a/Source/UEBlueprintMCPEditor/Private/UEBlueprintMCPEditor.cpp +++ b/Source/UEBlueprintMCPEditor/Private/UEBlueprintMCPEditor.cpp @@ -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 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>& Out) +{ + const FString Dir = MCPPluginDir(); + if (Dir.IsEmpty()) + { + return false; + } + FString Json; + if (!FFileHelper::LoadFileToString(Json, *(Dir / TEXT("tools.json")))) + { + return false; + } + TArray> Items; + TSharedRef> Reader = TJsonReaderFactory<>::Create(Json); + if (!FJsonSerializer::Deserialize(Reader, Items)) + { + return false; + } + for (const TSharedPtr& Item : Items) + { + const TSharedPtr 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(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 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 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> 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> 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& 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"); diff --git a/Source/UEBlueprintMCPEditor/Public/UEBlueprintMCPEditor.h b/Source/UEBlueprintMCPEditor/Public/UEBlueprintMCPEditor.h index 8f9633e..233677e 100644 --- a/Source/UEBlueprintMCPEditor/Public/UEBlueprintMCPEditor.h +++ b/Source/UEBlueprintMCPEditor/Public/UEBlueprintMCPEditor.h @@ -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 SpawnWizardTab(const class FSpawnTabArgs& Args); void NotifyBridgeStatus(); void ToggleMCPZoneMode(TWeakPtr BlueprintEditor); @@ -29,6 +35,7 @@ private: bool bMCPZoneModeActive = false; bool bWizardTabRegistered = false; + EMcpRcStatus CachedRcStatus = EMcpRcStatus::Unknown; TWeakPtr ActiveMCPZoneEditor; TSharedPtr MCPZoneInputProcessor; };