Skip to main content
Glama

UnrealBlueprintMCP

by BestDev
MCPClient.cpp28 kB
// Copyright Epic Games, Inc. All Rights Reserved. // MCPClient - Implementation of WebSocket client for MCP server communication #include "MCPClient.h" #include "MCPStatusWidget.h" #include "MCPBlueprintManager.h" #include "WebSocketsModule.h" #include "Dom/JsonObject.h" #include "Serialization/JsonSerializer.h" #include "Serialization/JsonWriter.h" #include "HAL/PlatformFilemanager.h" #include "Misc/DateTime.h" #include "Misc/Guid.h" #include "Engine/Engine.h" #include "TimerManager.h" #include "Async/Async.h" DEFINE_LOG_CATEGORY_STATIC(LogMCPClient, Log, All); // Static member definitions UMCPClient* UMCPClient::SingletonInstance = nullptr; bool UMCPClient::bIsShuttingDown = false; const float UMCPClient::BaseReconnectDelay = 2.0f; const float UMCPClient::MaxReconnectDelay = 60.0f; const float UMCPClient::ConnectionTimeout = 30.0f; const FString UMCPClient::MCPProtocolVersion = TEXT("2024-11-05"); UMCPClient::UMCPClient() : MCPSettings(nullptr) , BlueprintManager(nullptr) , WebSocket(nullptr) , WebSocketsModule(nullptr) , CurrentConnectionState(EMCPConnectionState::Disconnected) , bAutoReconnectEnabled(true) , ReconnectAttempts(0) , RequestIdCounter(0) { // Initialize WebSockets module WebSocketsModule = &FModuleManager::LoadModuleChecked<FWebSocketsModule>("WebSockets"); } UMCPClient::~UMCPClient() { // Clean up on destruction if (WebSocket.IsValid()) { Disconnect(false); } // Clear singleton reference if (SingletonInstance == this) { SingletonInstance = nullptr; } } void UMCPClient::BeginDestroy() { // Ensure clean disconnection before destruction if (WebSocket.IsValid()) { Disconnect(false); } // Stop any running timers if (GEngine && GEngine->GetWorldFromContextObject(this, EGetWorldErrorMode::LogAndReturnNull)) { if (UWorld* World = GEngine->GetWorldFromContextObject(this, EGetWorldErrorMode::LogAndReturnNull)) { World->GetTimerManager().ClearTimer(AutoReconnectTimerHandle); } } Super::BeginDestroy(); } UMCPClient* UMCPClient::Get(bool bCreateIfNeeded) { // 종료 중이면 nullptr 반환 if (bIsShuttingDown) { return nullptr; } if (!SingletonInstance && bCreateIfNeeded) { // 적절한 Outer 제공 (Transient Package 사용) SingletonInstance = NewObject<UMCPClient>(GetTransientPackage()); SingletonInstance->AddToRoot(); // Prevent garbage collection } return SingletonInstance; } void UMCPClient::Shutdown() { bIsShuttingDown = true; if (SingletonInstance) { if (SingletonInstance->IsConnected()) { SingletonInstance->Disconnect(true); } SingletonInstance->RemoveFromRoot(); SingletonInstance = nullptr; } } bool UMCPClient::Initialize(UMCPSettings* InSettings) { // Use provided settings or get default instance MCPSettings = InSettings ? InSettings : UMCPSettings::Get(); if (!MCPSettings) { LogMessage(TEXT("Failed to initialize MCPClient: No valid settings found"), ELogVerbosity::Error); return false; } // Validate settings if (!ValidateConnectionSettings()) { LogMessage(TEXT("Failed to initialize MCPClient: Invalid connection settings"), ELogVerbosity::Error); return false; } // Initialize Blueprint Manager BlueprintManager = UMCPBlueprintManager::Get(); if (BlueprintManager && !BlueprintManager->Initialize(MCPSettings)) { LogMessage(TEXT("Failed to initialize Blueprint Manager"), ELogVerbosity::Warning); // Don't fail initialization - blueprint features just won't be available } // Initialize connection state SetConnectionState(EMCPConnectionState::Disconnected); ResetReconnectAttempts(); LogMessage(TEXT("MCPClient initialized successfully")); return true; } bool UMCPClient::Connect() { if (!MCPSettings) { LogMessage(TEXT("Cannot connect: MCPClient not initialized"), ELogVerbosity::Error); return false; } if (CurrentConnectionState == EMCPConnectionState::Connected || CurrentConnectionState == EMCPConnectionState::Connecting) { LogMessage(TEXT("Already connected or connecting to MCP server")); return true; } // Validate settings before attempting connection if (!ValidateConnectionSettings()) { SetConnectionState(EMCPConnectionState::Failed, TEXT("Invalid connection settings")); return false; } // Create WebSocket if needed if (!WebSocket.IsValid() && WebSocketsModule) { FString WebSocketURL = GetWebSocketURL(); FString Protocol = TEXT(""); // MCP doesn't require specific sub-protocol WebSocket = WebSocketsModule->CreateWebSocket(WebSocketURL, Protocol); if (!WebSocket.IsValid()) { LogMessage(TEXT("Failed to create WebSocket instance"), ELogVerbosity::Error); SetConnectionState(EMCPConnectionState::Failed, TEXT("Failed to create WebSocket")); return false; } // Bind WebSocket event handlers WebSocket->OnConnected().AddUObject(this, &UMCPClient::OnWebSocketConnected); WebSocket->OnConnectionError().AddUObject(this, &UMCPClient::OnWebSocketConnectionError); WebSocket->OnClosed().AddUObject(this, &UMCPClient::OnWebSocketClosed); WebSocket->OnMessage().AddUObject(this, &UMCPClient::OnWebSocketMessage); WebSocket->OnBinaryMessage().AddUObject(this, &UMCPClient::OnWebSocketBinaryMessage); WebSocket->OnMessageSent().AddUObject(this, &UMCPClient::OnWebSocketMessageSent); } // Update state and attempt connection SetConnectionState(EMCPConnectionState::Connecting); LastConnectionAttempt = FDateTime::Now(); FString WebSocketURL = GetWebSocketURL(); LogMessage(FString::Printf(TEXT("Attempting to connect to MCP server: %s"), *WebSocketURL)); LogMessage(FString::Printf(TEXT("Connection details - Address: %s, Port: %d, Endpoint: %s"), *MCPSettings->ServerAddress, MCPSettings->ServerPort, *MCPSettings->MCPEndpoint)); // Attempt connection WebSocket->Connect(); return true; } void UMCPClient::Disconnect(bool bGraceful) { StopAutoReconnectTimer(); if (WebSocket.IsValid()) { if (bGraceful && CurrentConnectionState == EMCPConnectionState::Connected) { // Send graceful disconnect notification if protocol supports it SendNotification(TEXT("session/end"), TEXT("{}")); } // Close WebSocket connection WebSocket->Close(); WebSocket.Reset(); } // Clear pending requests { FScopeLock Lock(&RequestsMutex); PendingRequests.Empty(); } SetConnectionState(EMCPConnectionState::Disconnected); ResetReconnectAttempts(); LogMessage(TEXT("Disconnected from MCP server")); } bool UMCPClient::IsConnected() const { return CurrentConnectionState == EMCPConnectionState::Connected; } EMCPConnectionState UMCPClient::GetConnectionState() const { return CurrentConnectionState; } FString UMCPClient::SendRequest(const FString& Method, const FString& Params, const FString& RequestId) { if (!IsConnected()) { LogMessage(TEXT("Cannot send request: Not connected to MCP server"), ELogVerbosity::Warning); return TEXT(""); } // Generate request ID if not provided FString ActualRequestId = RequestId.IsEmpty() ? GenerateRequestId() : RequestId; // Create MCP message FMCPMessage Message(ActualRequestId, TEXT("request"), Method); Message.Params = Params; // Store pending request { FScopeLock Lock(&RequestsMutex); PendingRequests.Add(ActualRequestId, Message); } // Convert to JSON and send FString JsonMessage = CreateJsonFromMCPMessage(Message); if (SendRawMessage(JsonMessage)) { LogMessage(FString::Printf(TEXT("Sent MCP request: %s (ID: %s)"), *Method, *ActualRequestId)); return ActualRequestId; } else { // Remove from pending requests if send failed { FScopeLock Lock(&RequestsMutex); PendingRequests.Remove(ActualRequestId); } LogMessage(FString::Printf(TEXT("Failed to send MCP request: %s"), *Method), ELogVerbosity::Error); return TEXT(""); } } bool UMCPClient::SendNotification(const FString& Method, const FString& Params) { if (!IsConnected()) { LogMessage(TEXT("Cannot send notification: Not connected to MCP server"), ELogVerbosity::Warning); return false; } // Create MCP notification message (no ID for notifications) FMCPMessage Message(TEXT(""), TEXT("notification"), Method); Message.Params = Params; // Convert to JSON and send FString JsonMessage = CreateJsonFromMCPMessage(Message); if (SendRawMessage(JsonMessage)) { LogMessage(FString::Printf(TEXT("Sent MCP notification: %s"), *Method)); return true; } else { LogMessage(FString::Printf(TEXT("Failed to send MCP notification: %s"), *Method), ELogVerbosity::Error); return false; } } bool UMCPClient::SendRawMessage(const FString& JsonMessage) { if (!WebSocket.IsValid() || !IsConnected()) { LogMessage(TEXT("Cannot send message: WebSocket not connected"), ELogVerbosity::Warning); return false; } // Send message through WebSocket WebSocket->Send(JsonMessage); return true; } void UMCPClient::RegisterStatusWidget(TSharedPtr<SMCPStatusWidget> StatusWidget) { if (StatusWidget.IsValid()) { FScopeLock Lock(&StatusWidgetsMutex); RegisteredStatusWidgets.AddUnique(StatusWidget); LogMessage(TEXT("Status widget registered for MCP updates")); } } void UMCPClient::UnregisterStatusWidget(TSharedPtr<SMCPStatusWidget> StatusWidget) { FScopeLock Lock(&StatusWidgetsMutex); RegisteredStatusWidgets.RemoveAll([StatusWidget](const TWeakPtr<SMCPStatusWidget>& Widget) { return !Widget.IsValid() || Widget.Pin() == StatusWidget; }); LogMessage(TEXT("Status widget unregistered from MCP updates")); } void UMCPClient::SetAutoReconnectEnabled(bool bEnable) { bAutoReconnectEnabled = bEnable; if (!bEnable) { StopAutoReconnectTimer(); } LogMessage(FString::Printf(TEXT("Auto-reconnect %s"), bEnable ? TEXT("enabled") : TEXT("disabled"))); } bool UMCPClient::IsAutoReconnectEnabled() const { return bAutoReconnectEnabled; } FString UMCPClient::GetLastErrorMessage() const { return LastErrorMessage; } void UMCPClient::ClearLastError() { LastErrorMessage.Empty(); } // WebSocket Event Handlers void UMCPClient::OnWebSocketConnected() { LogMessage(TEXT("WebSocket connected to MCP server")); SetConnectionState(EMCPConnectionState::Connected); ResetReconnectAttempts(); // Update settings with successful connection if (MCPSettings) { MCPSettings->SetConnectionState(EMCPConnectionState::Connected); } } void UMCPClient::OnWebSocketConnectionError(const FString& Error) { FString DetailedError = FString::Printf(TEXT("WebSocket connection error: %s (URL: %s)"), *Error, *GetWebSocketURL()); LogMessage(DetailedError, ELogVerbosity::Error); SetConnectionState(EMCPConnectionState::Failed, DetailedError); // Attempt auto-reconnect if enabled if (bAutoReconnectEnabled && ReconnectAttempts < MaxReconnectAttempts) { StartAutoReconnectTimer(); } } void UMCPClient::OnWebSocketClosed(int32 StatusCode, const FString& Reason, bool bWasClean) { LogMessage(FString::Printf(TEXT("WebSocket connection closed (Code: %d, Reason: %s, Clean: %s)"), StatusCode, *Reason, bWasClean ? TEXT("Yes") : TEXT("No"))); // Reset WebSocket reference if (WebSocket.IsValid()) { WebSocket.Reset(); } // Update connection state if (CurrentConnectionState != EMCPConnectionState::Disconnected) { SetConnectionState(bWasClean ? EMCPConnectionState::Disconnected : EMCPConnectionState::Failed, Reason); // Attempt auto-reconnect if connection was not gracefully closed if (!bWasClean && bAutoReconnectEnabled && ReconnectAttempts < MaxReconnectAttempts) { StartAutoReconnectTimer(); } } } void UMCPClient::OnWebSocketMessage(const FString& Message) { LogMessage(FString::Printf(TEXT("Received WebSocket message: %s"), *Message)); // Process the incoming MCP message ProcessIncomingMessage(Message); } void UMCPClient::OnWebSocketBinaryMessage(const void* Data, uint64 Size, bool bIsLastFragment) { // MCP protocol uses JSON text messages, binary messages are not expected LogMessage(TEXT("Received unexpected binary message from MCP server"), ELogVerbosity::Warning); } void UMCPClient::OnWebSocketMessageSent(const FString& Message) { // Optional: Log sent messages for debugging if (MCPSettings && MCPSettings->bEnableVerboseLogging) { LogMessage(FString::Printf(TEXT("WebSocket message sent: %s"), *Message)); } } // Connection Management void UMCPClient::SetConnectionState(EMCPConnectionState NewState, const FString& ErrorMessage) { if (CurrentConnectionState != NewState) { EMCPConnectionState OldState = CurrentConnectionState; CurrentConnectionState = NewState; // Store error message if provided if (!ErrorMessage.IsEmpty()) { LastErrorMessage = ErrorMessage; } // Update settings if (MCPSettings) { MCPSettings->SetConnectionState(NewState); } // Log state change LogMessage(FString::Printf(TEXT("Connection state changed: %s -> %s"), *StaticEnum<EMCPConnectionState>()->GetValueAsString(OldState), *StaticEnum<EMCPConnectionState>()->GetValueAsString(NewState))); // Notify status widgets on game thread AsyncTask(ENamedThreads::GameThread, [this, NewState, ErrorMessage]() { // Fire delegate OnConnectionStateChanged.Broadcast(NewState, ErrorMessage); // Notify registered status widgets NotifyStatusWidgets(TEXT("Info"), FString::Printf(TEXT("Connection state: %s"), *StaticEnum<EMCPConnectionState>()->GetValueAsString(NewState))); }); } } void UMCPClient::StartAutoReconnectTimer() { if (!bAutoReconnectEnabled || !GEngine) { return; } UWorld* World = GEngine->GetWorldFromContextObject(this, EGetWorldErrorMode::LogAndReturnNull); if (!World) { return; } // Calculate exponential backoff delay float Delay = FMath::Min(BaseReconnectDelay * FMath::Pow(2.0f, ReconnectAttempts), MaxReconnectDelay); LogMessage(FString::Printf(TEXT("Scheduling auto-reconnect in %.1f seconds (attempt %d/%d)"), Delay, ReconnectAttempts + 1, MaxReconnectAttempts)); World->GetTimerManager().SetTimer(AutoReconnectTimerHandle, this, &UMCPClient::AttemptReconnect, Delay, false); } void UMCPClient::StopAutoReconnectTimer() { if (GEngine) { if (UWorld* World = GEngine->GetWorldFromContextObject(this, EGetWorldErrorMode::LogAndReturnNull)) { World->GetTimerManager().ClearTimer(AutoReconnectTimerHandle); } } } void UMCPClient::AttemptReconnect() { if (CurrentConnectionState == EMCPConnectionState::Connected || CurrentConnectionState == EMCPConnectionState::Connecting) { // Already connected or connecting return; } if (ReconnectAttempts >= MaxReconnectAttempts) { LogMessage(FString::Printf(TEXT("Maximum reconnection attempts (%d) reached, giving up"), MaxReconnectAttempts), ELogVerbosity::Error); SetConnectionState(EMCPConnectionState::Failed, TEXT("Maximum reconnection attempts reached")); return; } ReconnectAttempts++; LogMessage(FString::Printf(TEXT("Auto-reconnect attempt %d/%d"), ReconnectAttempts, MaxReconnectAttempts)); // Increment reconnect attempts in settings if (MCPSettings) { MCPSettings->IncrementReconnectAttempts(); } // Attempt to connect if (!Connect()) { // If connection fails, schedule another attempt StartAutoReconnectTimer(); } } void UMCPClient::ResetReconnectAttempts() { ReconnectAttempts = 0; if (MCPSettings) { MCPSettings->ResetReconnectAttempts(); } } // Message Handling void UMCPClient::ProcessIncomingMessage(const FString& JsonMessage) { FMCPMessage Message; if (!ParseMCPMessage(JsonMessage, Message)) { LogMessage(FString::Printf(TEXT("Failed to parse incoming MCP message: %s"), *JsonMessage), ELogVerbosity::Error); return; } // Dispatch to game thread for delegate broadcasting AsyncTask(ENamedThreads::GameThread, [this, Message, JsonMessage]() { // Fire message received delegate OnMessageReceived.Broadcast(Message.Type, JsonMessage); // Handle responses to pending requests if (Message.Type == TEXT("response") && !Message.Id.IsEmpty()) { FMCPMessage OriginalRequest; bool bFoundRequest = false; { FScopeLock Lock(&RequestsMutex); if (FMCPMessage* FoundMessage = PendingRequests.Find(Message.Id)) { OriginalRequest = *FoundMessage; PendingRequests.Remove(Message.Id); bFoundRequest = true; } } if (bFoundRequest) { bool bSuccess = Message.Error.IsEmpty(); FString ResponseData = bSuccess ? Message.Result : Message.Error; // Fire operation complete delegate OnOperationComplete.Broadcast(Message.Id, bSuccess, ResponseData); LogMessage(FString::Printf(TEXT("Received response for request %s: %s"), *Message.Id, bSuccess ? TEXT("Success") : TEXT("Error"))); } } // Handle blueprint commands automatically if (Message.Type == TEXT("request") && !Message.Method.IsEmpty()) { if (Message.Method == TEXT("create_blueprint") || Message.Method == TEXT("set_property") || Message.Method == TEXT("set_blueprint_property") || Message.Method == TEXT("add_component") || Message.Method == TEXT("compile_blueprint") || Message.Method == TEXT("get_server_status")) { // Process blueprint command FString Response = ProcessBlueprintCommand(Message.Method, Message.Params); // Send response back if we have a request ID if (!Message.Id.IsEmpty()) { // Create proper JSON-RPC 2.0 response TSharedPtr<FJsonObject> ResponseObject = MakeShareable(new FJsonObject); ResponseObject->SetStringField(TEXT("jsonrpc"), TEXT("2.0")); ResponseObject->SetStringField(TEXT("id"), Message.Id); // Try to parse the response as JSON object TSharedPtr<FJsonObject> ResultObject; TSharedRef<TJsonReader<>> ResultReader = TJsonReaderFactory<>::Create(Response); if (FJsonSerializer::Deserialize(ResultReader, ResultObject) && ResultObject.IsValid()) { ResponseObject->SetObjectField(TEXT("result"), ResultObject); } else { ResponseObject->SetStringField(TEXT("result"), Response); } FString ResponseString; TSharedRef<TJsonWriter<>> ResponseWriter = TJsonWriterFactory<>::Create(&ResponseString); FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), ResponseWriter); SendRawMessage(ResponseString); } LogMessage(FString::Printf(TEXT("Processed blueprint command: %s"), *Message.Method)); } } // Notify status widgets NotifyStatusWidgets(TEXT("Info"), FString::Printf(TEXT("Received %s: %s"), *Message.Type, *Message.Method)); }); } bool UMCPClient::ParseMCPMessage(const FString& JsonMessage, FMCPMessage& OutMessage) { TSharedPtr<FJsonObject> JsonObject; TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonMessage); if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) { return false; } // Parse basic fields JsonObject->TryGetStringField(TEXT("id"), OutMessage.Id); JsonObject->TryGetStringField(TEXT("method"), OutMessage.Method); // Determine message type based on content if (JsonObject->HasField(TEXT("method"))) { OutMessage.Type = !OutMessage.Id.IsEmpty() ? TEXT("request") : TEXT("notification"); } else if (JsonObject->HasField(TEXT("result")) || JsonObject->HasField(TEXT("error"))) { OutMessage.Type = TEXT("response"); } else { OutMessage.Type = TEXT("unknown"); } // Parse params (can be object or string) if (JsonObject->HasField(TEXT("params"))) { const TSharedPtr<FJsonValue> ParamsValue = JsonObject->TryGetField(TEXT("params")); if (ParamsValue.IsValid()) { if (ParamsValue->Type == EJson::Object) { TSharedPtr<FJsonObject> ParamsObject = ParamsValue->AsObject(); FString ParamsString; TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ParamsString); FJsonSerializer::Serialize(ParamsObject.ToSharedRef(), Writer); OutMessage.Params = ParamsString; } else if (ParamsValue->Type == EJson::String) { OutMessage.Params = ParamsValue->AsString(); } } } // Parse result if (JsonObject->HasField(TEXT("result"))) { const TSharedPtr<FJsonValue> ResultValue = JsonObject->TryGetField(TEXT("result")); if (ResultValue.IsValid()) { if (ResultValue->Type == EJson::Object || ResultValue->Type == EJson::Array) { FString ResultString; TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ResultString); FJsonSerializer::Serialize(ResultValue, TEXT(""), Writer, false); OutMessage.Result = ResultString; } else { OutMessage.Result = ResultValue->AsString(); } } } // Parse error if (JsonObject->HasField(TEXT("error"))) { const TSharedPtr<FJsonValue> ErrorValue = JsonObject->TryGetField(TEXT("error")); if (ErrorValue.IsValid()) { if (ErrorValue->Type == EJson::Object) { TSharedPtr<FJsonObject> ErrorObject = ErrorValue->AsObject(); FString ErrorString; TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ErrorString); FJsonSerializer::Serialize(ErrorObject.ToSharedRef(), Writer); OutMessage.Error = ErrorString; } else { OutMessage.Error = ErrorValue->AsString(); } } } return true; } FString UMCPClient::CreateJsonFromMCPMessage(const FMCPMessage& Message) { TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject); // Set JSON-RPC 2.0 version JsonObject->SetStringField(TEXT("jsonrpc"), TEXT("2.0")); // Set basic fields if (!Message.Id.IsEmpty()) { JsonObject->SetStringField(TEXT("id"), Message.Id); } // For JSON-RPC 2.0, we use method field directly instead of type if (!Message.Method.IsEmpty()) { JsonObject->SetStringField(TEXT("method"), Message.Method); } // Set params if present if (!Message.Params.IsEmpty()) { // Try to parse params as JSON object first TSharedPtr<FJsonObject> ParamsObject; TSharedRef<TJsonReader<>> ParamsReader = TJsonReaderFactory<>::Create(Message.Params); if (FJsonSerializer::Deserialize(ParamsReader, ParamsObject) && ParamsObject.IsValid()) { JsonObject->SetObjectField(TEXT("params"), ParamsObject); } else { // If not valid JSON object, treat as string JsonObject->SetStringField(TEXT("params"), Message.Params); } } // Set result if present if (!Message.Result.IsEmpty()) { // Try to parse result as JSON TSharedPtr<FJsonValue> ResultValue; TSharedRef<TJsonReader<>> ResultReader = TJsonReaderFactory<>::Create(Message.Result); if (FJsonSerializer::Deserialize(ResultReader, ResultValue) && ResultValue.IsValid()) { JsonObject->SetField(TEXT("result"), ResultValue); } else { JsonObject->SetStringField(TEXT("result"), Message.Result); } } // Set error if present if (!Message.Error.IsEmpty()) { // Try to parse error as JSON object TSharedPtr<FJsonObject> ErrorObject; TSharedRef<TJsonReader<>> ErrorReader = TJsonReaderFactory<>::Create(Message.Error); if (FJsonSerializer::Deserialize(ErrorReader, ErrorObject) && ErrorObject.IsValid()) { JsonObject->SetObjectField(TEXT("error"), ErrorObject); } else { JsonObject->SetStringField(TEXT("error"), Message.Error); } } // Serialize to string FString OutputString; TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString); FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer); return OutputString; } FString UMCPClient::GenerateRequestId() { RequestIdCounter++; return FString::Printf(TEXT("req_%d_%s"), RequestIdCounter, *FGuid::NewGuid().ToString(EGuidFormats::Short)); } bool UMCPClient::ValidateMCPMessage(const FMCPMessage& Message) { // Basic validation rules for MCP messages if (Message.Type.IsEmpty()) { return false; } if (Message.Type == TEXT("request") || Message.Type == TEXT("notification")) { if (Message.Method.IsEmpty()) { return false; } } if (Message.Type == TEXT("request") && Message.Id.IsEmpty()) { return false; } return true; } // Utility Functions void UMCPClient::LogMessage(const FString& Message, ELogVerbosity::Type Verbosity) { // Use UE logging system with appropriate log level switch (Verbosity) { case ELogVerbosity::Error: UE_LOG(LogMCPClient, Error, TEXT("[MCPClient] %s"), *Message); break; case ELogVerbosity::Warning: UE_LOG(LogMCPClient, Warning, TEXT("[MCPClient] %s"), *Message); break; default: UE_LOG(LogMCPClient, Log, TEXT("[MCPClient] %s"), *Message); break; } // Also notify status widgets if available FString MessageLogLevel; switch (Verbosity) { case ELogVerbosity::Error: MessageLogLevel = TEXT("Error"); break; case ELogVerbosity::Warning: MessageLogLevel = TEXT("Warning"); break; default: MessageLogLevel = TEXT("Info"); break; } NotifyStatusWidgets(MessageLogLevel, Message); } void UMCPClient::NotifyStatusWidgets(const FString& MessageLogLevel, const FString& LogMessage) { // Must be called from game thread if (!IsInGameThread()) { AsyncTask(ENamedThreads::GameThread, [this, MessageLogLevel, LogMessage]() { NotifyStatusWidgets(MessageLogLevel, LogMessage); }); return; } FScopeLock Lock(&StatusWidgetsMutex); // Clean up invalid widgets and notify valid ones for (int32 i = RegisteredStatusWidgets.Num() - 1; i >= 0; i--) { if (RegisteredStatusWidgets[i].IsValid()) { if (TSharedPtr<SMCPStatusWidget> Widget = RegisteredStatusWidgets[i].Pin()) { Widget->AddLogEntry(MessageLogLevel, LogMessage); Widget->UpdateConnectionStatus(CurrentConnectionState); } } else { // Remove invalid widget references RegisteredStatusWidgets.RemoveAt(i); } } } FString UMCPClient::GetWebSocketURL() const { if (!MCPSettings) { return TEXT(""); } return MCPSettings->GetWebSocketURL(); } bool UMCPClient::ValidateConnectionSettings() const { if (!MCPSettings) { return false; } return MCPSettings->ValidateSettings(); } FString UMCPClient::ProcessBlueprintCommand(const FString& Method, const FString& Params) { // Check if Blueprint Manager is available if (!BlueprintManager) { LogMessage(TEXT("Blueprint Manager not available for command processing"), ELogVerbosity::Error); return TEXT("{\"success\":false,\"error\":\"Blueprint Manager not initialized\"}"); } // Log the command LogMessage(FString::Printf(TEXT("Processing blueprint command: %s"), *Method)); // Process different blueprint commands if (Method == TEXT("create_blueprint")) { return BlueprintManager->ProcessCreateBlueprintCommand(Params); } else if (Method == TEXT("set_property") || Method == TEXT("set_blueprint_property")) { return BlueprintManager->ProcessSetPropertyCommand(Params); } else if (Method == TEXT("add_component")) { return BlueprintManager->ProcessAddComponentCommand(Params); } else if (Method == TEXT("compile_blueprint")) { return BlueprintManager->ProcessCompileBlueprintCommand(Params); } else if (Method == TEXT("get_server_status")) { return BlueprintManager->ProcessGetServerStatusCommand(Params); } else { FString ErrorMsg = FString::Printf(TEXT("Unknown blueprint command: %s"), *Method); LogMessage(ErrorMsg, ELogVerbosity::Error); return FString::Printf(TEXT("{\"success\":false,\"error\":\"%s\"}"), *ErrorMsg); } }

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