Skip to main content
Glama

UnrealBlueprintMCP

by BestDev
MCPStatusWidget.cpp20 kB
// Copyright Epic Games, Inc. All Rights Reserved. // MCPStatusWidget - UI widget for displaying MCP server connection status and logs #include "MCPStatusWidget.h" #include "MCPSettings.h" #include "MCPClient.h" // Slate includes #include "Widgets/SBoxPanel.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SCheckBox.h" #include "Styling/AppStyle.h" #include "Styling/CoreStyle.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Misc/FileHelper.h" #include "HAL/PlatformFilemanager.h" #include "Misc/Paths.h" #include "Misc/DateTime.h" // Logging DEFINE_LOG_CATEGORY_STATIC(LogMCPStatusWidget, Log, All); #define LOCTEXT_NAMESPACE "MCPStatusWidget" // Color constants const FLinearColor SMCPStatusWidget::InfoColor = FLinearColor::White; const FLinearColor SMCPStatusWidget::WarningColor = FLinearColor::Yellow; const FLinearColor SMCPStatusWidget::ErrorColor = FLinearColor::Red; const FLinearColor SMCPStatusWidget::SuccessColor = FLinearColor::Green; void SMCPStatusWidget::Construct(const FArguments& InArgs) { // Get settings instance MCPSettings = UMCPSettings::Get(); check(MCPSettings); // Initialize cached values CachedConnectionState = MCPSettings->GetConnectionState(); CachedServerAddress = MCPSettings->ServerAddress; CachedServerPort = MCPSettings->ServerPort; // Build the widget hierarchy ChildSlot [ SNew(SVerticalBox) // Header + SVerticalBox::Slot() .AutoHeight() .Padding(5, 5, 5, 10) [ SNew(STextBlock) .Text(LOCTEXT("MCPStatusTitle", "MCP Server Status")) .Font(FCoreStyle::GetDefaultFontStyle("Bold", 16)) .Justification(ETextJustify::Center) ] // Connection Status Section + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ CreateConnectionStatusSection() ] // Separator + SVerticalBox::Slot() .AutoHeight() .Padding(5, 2) [ SNew(SSeparator) ] // Server Settings Section + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ CreateServerSettingsSection() ] // Separator + SVerticalBox::Slot() .AutoHeight() .Padding(5, 2) [ SNew(SSeparator) ] // Connection Controls Section + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ CreateConnectionControlsSection() ] // Separator + SVerticalBox::Slot() .AutoHeight() .Padding(5, 2) [ SNew(SSeparator) ] // Log Display Section + SVerticalBox::Slot() .FillHeight(1.0f) .Padding(5) [ CreateLogDisplaySection() ] ]; // Add initial log entry AddLogEntry(TEXT("Info"), TEXT("MCP Status Widget initialized"), InfoColor); // Register with MCPClient for status updates UMCPClient* MCPClient = UMCPClient::Get(); if (MCPClient) { MCPClient->RegisterStatusWidget(SharedThis(this)); AddLogEntry(TEXT("Info"), TEXT("Registered with MCP Client for updates"), InfoColor); } } SMCPStatusWidget::~SMCPStatusWidget() { // Note: Cannot use SharedThis() in destructor as the object is being destroyed // The MCPClient will automatically clean up weak pointers when they become invalid } TSharedRef<SWidget> SMCPStatusWidget::CreateConnectionStatusSection() { return SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(10) [ SNew(SVerticalBox) // Section title + SVerticalBox::Slot() .AutoHeight() .Padding(0, 0, 0, 5) [ SNew(STextBlock) .Text(LOCTEXT("ConnectionStatusSection", "Connection Status")) .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) ] // Connection state + SVerticalBox::Slot() .AutoHeight() .Padding(0, 2) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("StatusLabel", "Status: ")) ] + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ SAssignNew(ConnectionStatusText, STextBlock) .Text(this, &SMCPStatusWidget::GetConnectionStatusText) .ColorAndOpacity(this, &SMCPStatusWidget::GetConnectionStatusColor) .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) ] ] // Server URL + SVerticalBox::Slot() .AutoHeight() .Padding(0, 2) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("ServerURLLabel", "Server URL: ")) ] + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ SAssignNew(ServerURLText, STextBlock) .Text(this, &SMCPStatusWidget::GetServerURLText) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) ] ] // Last connection time + SVerticalBox::Slot() .AutoHeight() .Padding(0, 2) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("LastConnectionLabel", "Last Connected: ")) ] + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ SAssignNew(LastConnectionTimeText, STextBlock) .Text(this, &SMCPStatusWidget::GetLastConnectionTimeText) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) ] ] ]; } TSharedRef<SWidget> SMCPStatusWidget::CreateServerSettingsSection() { return SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(10) [ SNew(SVerticalBox) // Section title + SVerticalBox::Slot() .AutoHeight() .Padding(0, 0, 0, 5) [ SNew(STextBlock) .Text(LOCTEXT("ServerSettingsSection", "Quick Settings")) .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) ] // Server address + SVerticalBox::Slot() .AutoHeight() .Padding(0, 2) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(0, 0, 10, 0) [ SNew(SBox) .WidthOverride(100) [ SNew(STextBlock) .Text(LOCTEXT("ServerAddressLabel", "Server Address:")) ] ] + SHorizontalBox::Slot() .FillWidth(1.0f) [ SAssignNew(ServerAddressEditBox, SEditableTextBox) .Text(this, &SMCPStatusWidget::GetServerAddressText) .OnTextChanged(this, &SMCPStatusWidget::OnServerAddressChanged) ] ] // Server port + SVerticalBox::Slot() .AutoHeight() .Padding(0, 2) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(0, 0, 10, 0) [ SNew(SBox) .WidthOverride(100) [ SNew(STextBlock) .Text(LOCTEXT("ServerPortLabel", "Server Port:")) ] ] + SHorizontalBox::Slot() .FillWidth(1.0f) [ SAssignNew(ServerPortEditBox, SEditableTextBox) .Text(this, &SMCPStatusWidget::GetServerPortText) .OnTextChanged(this, &SMCPStatusWidget::OnServerPortChanged) ] ] // Auto-connect checkbox + SVerticalBox::Slot() .AutoHeight() .Padding(0, 5, 0, 0) [ SAssignNew(AutoConnectCheckBox, SCheckBox) .IsChecked(this, &SMCPStatusWidget::GetAutoConnectState) .OnCheckStateChanged(this, &SMCPStatusWidget::OnAutoConnectChanged) [ SNew(STextBlock) .Text(LOCTEXT("AutoConnectLabel", "Auto-connect on startup")) ] ] ]; } TSharedRef<SWidget> SMCPStatusWidget::CreateConnectionControlsSection() { return SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(10) [ SNew(SHorizontalBox) // Connect button + SHorizontalBox::Slot() .AutoWidth() .Padding(0, 0, 5, 0) [ SAssignNew(ConnectButton, SButton) .Text(LOCTEXT("ConnectButton", "Connect")) .ToolTipText(LOCTEXT("ConnectButtonTooltip", "Attempt to connect to the MCP server")) .OnClicked(this, &SMCPStatusWidget::OnConnectButtonClicked) .IsEnabled(this, &SMCPStatusWidget::IsConnectButtonEnabled) ] // Disconnect button + SHorizontalBox::Slot() .AutoWidth() .Padding(5, 0) [ SAssignNew(DisconnectButton, SButton) .Text(LOCTEXT("DisconnectButton", "Disconnect")) .ToolTipText(LOCTEXT("DisconnectButtonTooltip", "Disconnect from the MCP server")) .OnClicked(this, &SMCPStatusWidget::OnDisconnectButtonClicked) .IsEnabled(this, &SMCPStatusWidget::IsDisconnectButtonEnabled) ] // Refresh settings button + SHorizontalBox::Slot() .AutoWidth() .Padding(5, 0) [ SNew(SButton) .Text(LOCTEXT("RefreshButton", "Refresh Settings")) .ToolTipText(LOCTEXT("RefreshButtonTooltip", "Reload settings from configuration")) .OnClicked(this, &SMCPStatusWidget::OnRefreshSettingsClicked) ] ]; } TSharedRef<SWidget> SMCPStatusWidget::CreateLogDisplaySection() { return SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(10) [ SNew(SVerticalBox) // Section header + SVerticalBox::Slot() .AutoHeight() .Padding(0, 0, 0, 5) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("LogDisplaySection", "Operation Logs")) .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .Text(LOCTEXT("ClearLogsButton", "Clear")) .ToolTipText(LOCTEXT("ClearLogsTooltip", "Clear all log entries")) .OnClicked(this, &SMCPStatusWidget::OnClearLogsClicked) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(5, 0, 0, 0) [ SNew(SButton) .Text(LOCTEXT("ExportLogsButton", "Export")) .ToolTipText(LOCTEXT("ExportLogsTooltip", "Export logs to file")) .OnClicked(this, &SMCPStatusWidget::OnExportLogsClicked) ] ] // Log scroll area + SVerticalBox::Slot() .FillHeight(1.0f) [ SAssignNew(LogScrollBox, SScrollBox) .Orientation(Orient_Vertical) .ScrollBarAlwaysVisible(true) + SScrollBox::Slot() [ SAssignNew(LogContainer, SVerticalBox) ] ] ]; } void SMCPStatusWidget::AddLogEntry(const FString& Level, const FString& Message, const FLinearColor& Color) { // Create new log entry FMCPLogEntry NewEntry(Level, Message, Color); LogEntries.Add(NewEntry); // Limit the number of stored entries if (LogEntries.Num() > MaxDisplayedLogEntries) { LogEntries.RemoveAt(0, LogEntries.Num() - MaxDisplayedLogEntries); } // Refresh the display RefreshLogDisplay(); // Auto-scroll to bottom if (LogScrollBox.IsValid()) { LogScrollBox->ScrollToEnd(); } UE_LOG(LogMCPStatusWidget, Log, TEXT("[%s] %s"), *Level, *Message); } void SMCPStatusWidget::ClearLogEntries() { LogEntries.Empty(); RefreshLogDisplay(); AddLogEntry(TEXT("Info"), TEXT("Log cleared"), InfoColor); } void SMCPStatusWidget::RefreshLogDisplay() { if (!LogContainer.IsValid()) { return; } // Clear existing widgets LogContainer->ClearChildren(); // Add widgets for all log entries for (const FMCPLogEntry& Entry : LogEntries) { LogContainer->AddSlot() .AutoHeight() .Padding(0, 1) [ CreateLogEntryWidget(Entry) ]; } } TSharedRef<SWidget> SMCPStatusWidget::CreateLogEntryWidget(const FMCPLogEntry& LogEntry) { return SNew(SHorizontalBox) // Timestamp + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(0, 0, 10, 0) [ SNew(STextBlock) .Text(FText::FromString(FormatTimestamp(LogEntry.Timestamp))) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 8)) .ColorAndOpacity(FLinearColor::Gray) ] // Level + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(0, 0, 10, 0) [ SNew(STextBlock) .Text(FText::FromString(LogEntry.Level)) .Font(FCoreStyle::GetDefaultFontStyle("Bold", 8)) .ColorAndOpacity(LogEntry.Color) ] // Message + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(FText::FromString(LogEntry.Message)) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) .ColorAndOpacity(LogEntry.Color) .AutoWrapText(true) ]; } FString SMCPStatusWidget::FormatTimestamp(const FDateTime& Timestamp) const { return Timestamp.ToString(TEXT("%H:%M:%S")); } // Event handlers implementation FReply SMCPStatusWidget::OnConnectButtonClicked() { AddLogEntry(TEXT("Info"), TEXT("Manual connection requested"), InfoColor); // Get MCPClient instance and attempt connection UMCPClient* MCPClient = UMCPClient::Get(); if (MCPClient) { // Initialize the client with current settings if (MCPClient->Initialize(MCPSettings)) { // Attempt to connect if (MCPClient->Connect()) { AddLogEntry(TEXT("Info"), TEXT("Connection attempt started"), InfoColor); } else { AddLogEntry(TEXT("Error"), TEXT("Failed to start connection attempt"), ErrorColor); } } else { AddLogEntry(TEXT("Error"), TEXT("Failed to initialize MCP client"), ErrorColor); } } else { AddLogEntry(TEXT("Error"), TEXT("MCP Client not available"), ErrorColor); } return FReply::Handled(); } FReply SMCPStatusWidget::OnDisconnectButtonClicked() { AddLogEntry(TEXT("Info"), TEXT("Manual disconnection requested"), InfoColor); // Get MCPClient instance and disconnect UMCPClient* MCPClient = UMCPClient::Get(); if (MCPClient) { if (MCPClient->IsConnected()) { MCPClient->Disconnect(true); // Graceful disconnect AddLogEntry(TEXT("Info"), TEXT("Disconnection initiated"), InfoColor); } else { AddLogEntry(TEXT("Warning"), TEXT("Client was not connected"), WarningColor); } } else { AddLogEntry(TEXT("Error"), TEXT("MCP Client not available"), ErrorColor); } return FReply::Handled(); } FReply SMCPStatusWidget::OnRefreshSettingsClicked() { RefreshServerInfo(); AddLogEntry(TEXT("Info"), TEXT("Settings refreshed from configuration"), InfoColor); return FReply::Handled(); } FReply SMCPStatusWidget::OnClearLogsClicked() { ClearLogEntries(); return FReply::Handled(); } FReply SMCPStatusWidget::OnExportLogsClicked() { // Create log file content FString LogContent; for (const FMCPLogEntry& Entry : LogEntries) { LogContent += FString::Printf(TEXT("[%s] [%s] %s\n"), *FormatTimestamp(Entry.Timestamp), *Entry.Level, *Entry.Message); } // Save to file FString FileName = FString::Printf(TEXT("MCP_Logs_%s.txt"), *FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"))); FString FilePath = FPaths::ProjectSavedDir() / TEXT("Logs") / FileName; if (FFileHelper::SaveStringToFile(LogContent, *FilePath)) { AddLogEntry(TEXT("Info"), FString::Printf(TEXT("Logs exported to: %s"), *FilePath), SuccessColor); } else { AddLogEntry(TEXT("Error"), FString::Printf(TEXT("Failed to export logs to: %s"), *FilePath), ErrorColor); } return FReply::Handled(); } void SMCPStatusWidget::OnServerAddressChanged(const FText& NewText) { if (MCPSettings) { MCPSettings->ServerAddress = NewText.ToString(); MCPSettings->SaveConfig(); CachedServerAddress = MCPSettings->ServerAddress; } } void SMCPStatusWidget::OnServerPortChanged(const FText& NewText) { if (MCPSettings) { int32 NewPort = FCString::Atoi(*NewText.ToString()); if (NewPort > 0 && NewPort <= 65535) { MCPSettings->ServerPort = NewPort; MCPSettings->SaveConfig(); CachedServerPort = MCPSettings->ServerPort; } } } void SMCPStatusWidget::OnAutoConnectChanged(ECheckBoxState NewState) { if (MCPSettings) { MCPSettings->bAutoConnectOnStartup = (NewState == ECheckBoxState::Checked); MCPSettings->SaveConfig(); } } // UI state getters FText SMCPStatusWidget::GetConnectionStatusText() const { // Get status from MCPClient first, fallback to MCPSettings UMCPClient* MCPClient = UMCPClient::Get(); EMCPConnectionState CurrentState = EMCPConnectionState::Disconnected; if (MCPClient) { CurrentState = MCPClient->GetConnectionState(); } else if (MCPSettings) { CurrentState = MCPSettings->GetConnectionState(); } else { return LOCTEXT("UnknownStatus", "Unknown"); } switch (CurrentState) { case EMCPConnectionState::Disconnected: return LOCTEXT("DisconnectedStatus", "Disconnected"); case EMCPConnectionState::Connecting: return LOCTEXT("ConnectingStatus", "Connecting..."); case EMCPConnectionState::Connected: return LOCTEXT("ConnectedStatus", "Connected"); case EMCPConnectionState::Failed: return LOCTEXT("FailedStatus", "Connection Failed"); case EMCPConnectionState::Disabled: return LOCTEXT("DisabledStatus", "Disabled"); default: return LOCTEXT("UnknownStatus", "Unknown"); } } FSlateColor SMCPStatusWidget::GetConnectionStatusColor() const { // Get status from MCPClient first, fallback to MCPSettings UMCPClient* MCPClient = UMCPClient::Get(); EMCPConnectionState CurrentState = EMCPConnectionState::Disconnected; if (MCPClient) { CurrentState = MCPClient->GetConnectionState(); } else if (MCPSettings) { CurrentState = MCPSettings->GetConnectionState(); } else { return FSlateColor(FLinearColor::Gray); } return FSlateColor(GetConnectionStateColor(CurrentState)); } FLinearColor SMCPStatusWidget::GetConnectionStateColor(EMCPConnectionState State) const { switch (State) { case EMCPConnectionState::Disconnected: return FLinearColor::Gray; case EMCPConnectionState::Connecting: return FLinearColor::Yellow; case EMCPConnectionState::Connected: return SuccessColor; case EMCPConnectionState::Failed: return ErrorColor; case EMCPConnectionState::Disabled: return FLinearColor::Gray; default: return FLinearColor::White; } } FText SMCPStatusWidget::GetServerURLText() const { if (MCPSettings) { return FText::FromString(MCPSettings->GetWebSocketURL()); } return LOCTEXT("NoURL", "No URL configured"); } FText SMCPStatusWidget::GetLastConnectionTimeText() const { if (MCPSettings) { FDateTime LastTime = MCPSettings->GetLastConnectionTime(); if (LastTime == FDateTime::MinValue()) { return LOCTEXT("NeverConnected", "Never"); } return FText::FromString(LastTime.ToString()); } return LOCTEXT("Unknown", "Unknown"); } bool SMCPStatusWidget::IsConnectButtonEnabled() const { // Get status from MCPClient first, fallback to MCPSettings UMCPClient* MCPClient = UMCPClient::Get(); EMCPConnectionState State = EMCPConnectionState::Disconnected; if (MCPClient) { State = MCPClient->GetConnectionState(); } else if (MCPSettings) { State = MCPSettings->GetConnectionState(); } else { return false; } return (State == EMCPConnectionState::Disconnected || State == EMCPConnectionState::Failed); } bool SMCPStatusWidget::IsDisconnectButtonEnabled() const { // Get status from MCPClient first, fallback to MCPSettings UMCPClient* MCPClient = UMCPClient::Get(); EMCPConnectionState State = EMCPConnectionState::Disconnected; if (MCPClient) { State = MCPClient->GetConnectionState(); } else if (MCPSettings) { State = MCPSettings->GetConnectionState(); } else { return false; } return (State == EMCPConnectionState::Connected || State == EMCPConnectionState::Connecting); } FText SMCPStatusWidget::GetServerAddressText() const { return MCPSettings ? FText::FromString(MCPSettings->ServerAddress) : FText::GetEmpty(); } FText SMCPStatusWidget::GetServerPortText() const { return MCPSettings ? FText::FromString(FString::FromInt(MCPSettings->ServerPort)) : FText::GetEmpty(); } ECheckBoxState SMCPStatusWidget::GetAutoConnectState() const { if (MCPSettings && MCPSettings->bAutoConnectOnStartup) { return ECheckBoxState::Checked; } return ECheckBoxState::Unchecked; } void SMCPStatusWidget::UpdateConnectionStatus(EMCPConnectionState NewState) { CachedConnectionState = NewState; // Widget will automatically update through binding } void SMCPStatusWidget::RefreshServerInfo() { if (MCPSettings) { CachedServerAddress = MCPSettings->ServerAddress; CachedServerPort = MCPSettings->ServerPort; } } #undef LOCTEXT_NAMESPACE

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/BestDev/unreal_bp_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server