Simplify MCPServer
This commit is contained in:
@@ -2,21 +2,9 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "EdGraphSchema_K2.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
#include "UMCPHandler_ShowCommands.generated.h"
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS()
|
||||
class UMCPHandler_ShowCommands : public UObject, public IMCPHandler
|
||||
{
|
||||
@@ -31,49 +19,28 @@ public:
|
||||
return TEXT("List all available commands with their descriptions.");
|
||||
}
|
||||
|
||||
// Collect all handler classes sorted by tool name.
|
||||
TArray<TPair<FString, UClass*>> CollectHandlers() const
|
||||
{
|
||||
TArray<TPair<FString, UClass*>> Handlers;
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
UClass* Class = *It;
|
||||
if (Class->HasAnyClassFlags(CLASS_Abstract)) continue;
|
||||
const IMCPHandler* Handler = Cast<IMCPHandler>(Class->GetDefaultObject());
|
||||
if (!Handler) continue;
|
||||
FString ToolName = MCPUtils::GetToolName(Class);
|
||||
Handlers.Add({ToolName, Class});
|
||||
}
|
||||
Handlers.Sort();
|
||||
return Handlers;
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||
{
|
||||
auto Handlers = CollectHandlers();
|
||||
Result.Appendf(TEXT("%d commands:\n"), Handlers.Num());
|
||||
|
||||
for (const auto& Pair : Handlers)
|
||||
for (UClass* Class : MCPUtils::CollectHandlerClasses())
|
||||
{
|
||||
if (Verbose)
|
||||
{
|
||||
MCPUtils::FormatCommandHelp(Pair.Value, Result);
|
||||
continue;
|
||||
MCPUtils::FormatCommandHelp(Class, Result);
|
||||
}
|
||||
|
||||
// Non-verbose: just the signature line
|
||||
UClass* Class = Pair.Value;
|
||||
Result.Append(Pair.Key);
|
||||
Result.Append(TEXT("("));
|
||||
bool bFirst = true;
|
||||
for (TFieldIterator<FProperty> PropIt(Class, EFieldIterationFlags::None); PropIt; ++PropIt)
|
||||
else
|
||||
{
|
||||
if (!bFirst) Result.Append(TEXT(","));
|
||||
bFirst = false;
|
||||
if (PropIt->HasMetaData(TEXT("Optional"))) Result.Append(TEXT("?"));
|
||||
Result.Append(MCPUtils::PropertyNameToJsonKey(PropIt->GetName()));
|
||||
Result.Append(MCPUtils::GetToolName(Class));
|
||||
Result.Append(TEXT("("));
|
||||
bool bFirst = true;
|
||||
for (TFieldIterator<FProperty> PropIt(Class, EFieldIterationFlags::None); PropIt; ++PropIt)
|
||||
{
|
||||
if (!bFirst) Result.Append(TEXT(","));
|
||||
bFirst = false;
|
||||
if (PropIt->HasMetaData(TEXT("Optional"))) Result.Append(TEXT("?"));
|
||||
Result.Append(MCPUtils::PropertyNameToJsonKey(PropIt->GetName()));
|
||||
}
|
||||
Result.Append(TEXT(")\n"));
|
||||
}
|
||||
Result.Append(TEXT(")\n"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,50 +12,26 @@ UBlueprintMCPCommandlet::UBlueprintMCPCommandlet()
|
||||
|
||||
int32 UBlueprintMCPCommandlet::Main(const FString& Params)
|
||||
{
|
||||
// Parse port from command-line params
|
||||
TArray<FString> Tokens;
|
||||
TArray<FString> Switches;
|
||||
TMap<FString, FString> ParamMap;
|
||||
ParseCommandLine(*Params, Tokens, Switches, ParamMap);
|
||||
|
||||
int32 Port = 9847;
|
||||
if (ParamMap.Contains(TEXT("port")))
|
||||
{
|
||||
Port = FCString::Atoi(*ParamMap[TEXT("port")]);
|
||||
}
|
||||
|
||||
// Create and start the shared server
|
||||
Server = MakeUnique<FBlueprintMCPServer>();
|
||||
if (!Server->Start(Port))
|
||||
// The UMCPServer editor subsystem starts the server automatically.
|
||||
// We just need to tick it, since FTickableEditorObject doesn't tick in commandlet mode.
|
||||
UMCPServer* Server = UMCPServer::Get();
|
||||
if (!Server)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: Could not find MCP server subsystem"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Main loop — tick the engine systems and process queued requests one at a time
|
||||
double LastTime = FPlatformTime::Seconds();
|
||||
|
||||
auto TickEngine = [&LastTime]()
|
||||
while (!IsEngineExitRequested())
|
||||
{
|
||||
double CurrentTime = FPlatformTime::Seconds();
|
||||
double DeltaTime = CurrentTime - LastTime;
|
||||
LastTime = CurrentTime;
|
||||
FTSTicker::GetCoreTicker().Tick(DeltaTime);
|
||||
};
|
||||
|
||||
while (!IsEngineExitRequested())
|
||||
{
|
||||
TickEngine();
|
||||
|
||||
if (Server->ProcessOneRequest())
|
||||
{
|
||||
// Tick again immediately after completing a request so pending
|
||||
// HTTP responses get flushed.
|
||||
TickEngine();
|
||||
}
|
||||
|
||||
Server->Tick(DeltaTime);
|
||||
FPlatformProcess::Sleep(0.01f);
|
||||
}
|
||||
|
||||
Server->Stop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "MCPEditorSubsystem.h"
|
||||
#include "MCPServer.h"
|
||||
#include "BlueprintExporter.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
@@ -11,59 +10,16 @@ void UBlueprintMCPEditorSubsystem::Initialize(FSubsystemCollectionBase& Collecti
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
// Don't start in commandlet mode — the commandlet has its own server instance.
|
||||
if (IsRunningCommandlet())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnAssetSavedHandle = UPackage::PackageSavedWithContextEvent.AddUObject(
|
||||
this, &UBlueprintMCPEditorSubsystem::OnAssetSaved);
|
||||
|
||||
Server = MakeUnique<FBlueprintMCPServer>();
|
||||
if (Server->Start(9847, /*bEditorMode=*/true))
|
||||
{
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Editor subsystem started — MCP server on port %d"), Server->GetPort());
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Editor subsystem failed to start MCP server (port may be in use)"));
|
||||
Server.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void UBlueprintMCPEditorSubsystem::Deinitialize()
|
||||
{
|
||||
UPackage::PackageSavedWithContextEvent.Remove(OnAssetSavedHandle);
|
||||
|
||||
if (Server)
|
||||
{
|
||||
Server->Stop();
|
||||
Server.Reset();
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Editor subsystem stopped."));
|
||||
}
|
||||
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void UBlueprintMCPEditorSubsystem::Tick(float DeltaTime)
|
||||
{
|
||||
if (Server)
|
||||
{
|
||||
Server->ProcessOneRequest();
|
||||
}
|
||||
}
|
||||
|
||||
bool UBlueprintMCPEditorSubsystem::IsTickable() const
|
||||
{
|
||||
return Server.IsValid() && Server->IsRunning();
|
||||
}
|
||||
|
||||
TStatId UBlueprintMCPEditorSubsystem::GetStatId() const
|
||||
{
|
||||
RETURN_QUICK_DECLARE_CYCLE_STAT(UBlueprintMCPEditorSubsystem, STATGROUP_Tickables);
|
||||
}
|
||||
|
||||
void UBlueprintMCPEditorSubsystem::OnAssetSaved(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext Context)
|
||||
{
|
||||
if (!Package) return;
|
||||
|
||||
@@ -103,90 +103,19 @@
|
||||
// Get() — retrieve the active server via the editor subsystem
|
||||
// ============================================================
|
||||
|
||||
#include "MCPEditorSubsystem.h"
|
||||
|
||||
FMCPServer* FMCPServer::Get()
|
||||
UMCPServer* UMCPServer::Get()
|
||||
{
|
||||
if (!GEditor) return nullptr;
|
||||
auto* Sub = GEditor->GetEditorSubsystem<UBlueprintMCPEditorSubsystem>();
|
||||
return Sub ? Sub->GetServer() : nullptr;
|
||||
}
|
||||
|
||||
FString FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Params)
|
||||
{
|
||||
UClass** HandlerClass = MCPHandlerRegistry.Find(ToolName);
|
||||
if (!HandlerClass)
|
||||
{
|
||||
return FString::Printf(TEXT("Unknown tool: %s"), *ToolName);
|
||||
}
|
||||
|
||||
TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass));
|
||||
IMCPHandler* Handler = Cast<IMCPHandler>(HandlerObj.Get());
|
||||
|
||||
TStringBuilder<4096> PopulateError;
|
||||
if (!MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params, PopulateError))
|
||||
{
|
||||
PopulateError.Append(TEXT("\nUsage:\n"));
|
||||
MCPUtils::FormatCommandHelp(*HandlerClass, PopulateError);
|
||||
return PopulateError.ToString();
|
||||
}
|
||||
|
||||
// Try text handler first; fall back to JSON if nothing was written.
|
||||
TStringBuilder<32768> TextResult;
|
||||
Handler->Handle(Params, TextResult);
|
||||
if (TextResult.Len() > 0)
|
||||
{
|
||||
FString Result = TextResult.ToString();
|
||||
for (int32 i = 0; i < Result.Len(); ++i)
|
||||
{
|
||||
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
// Invoke the Json handler.
|
||||
TSharedRef<FJsonObject> JsonResult = MakeShared<FJsonObject>();
|
||||
Handler->Handle(Params, &*JsonResult);
|
||||
return MCPUtils::JsonToString(JsonResult);
|
||||
return GEditor->GetEditorSubsystem<UMCPServer>();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleRequest — parse a JSON command and dispatch
|
||||
// Initialization and Shutdown
|
||||
// ============================================================
|
||||
|
||||
FString FMCPServer::HandleRequest(const FString& Line)
|
||||
void UMCPServer::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
TSharedPtr<FJsonObject> Request = MCPUtils::ParseBodyJson(Line);
|
||||
if (!Request.IsValid())
|
||||
{
|
||||
TSharedRef<FJsonObject> ErrResult = MakeShared<FJsonObject>();
|
||||
MCPUtils::MakeErrorJson(&*ErrResult, TEXT("JSON parse error"));
|
||||
return MCPUtils::JsonToString(ErrResult);
|
||||
}
|
||||
|
||||
FString Command;
|
||||
if (!Request->TryGetStringField(TEXT("command"), Command))
|
||||
{
|
||||
TSharedRef<FJsonObject> ErrResult = MakeShared<FJsonObject>();
|
||||
MCPUtils::MakeErrorJson(&*ErrResult, TEXT("Missing 'command' field"));
|
||||
return MCPUtils::JsonToString(ErrResult);
|
||||
}
|
||||
Request->RemoveField(TEXT("command"));
|
||||
|
||||
return DispatchToolCall(Command, Request.Get());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// TCP Server: Start / Stop / ProcessOneRequest
|
||||
// ============================================================
|
||||
|
||||
bool FMCPServer::Start(int32 InPort, bool bEditorMode)
|
||||
{
|
||||
Port = InPort;
|
||||
bIsEditor = bEditorMode;
|
||||
|
||||
// Register handlers
|
||||
BuildMCPHandlerRegistry();
|
||||
Super::Initialize(Collection);
|
||||
|
||||
// Create TCP listen socket
|
||||
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
||||
@@ -194,7 +123,7 @@ bool FMCPServer::Start(int32 InPort, bool bEditorMode)
|
||||
if (!ListenSocket)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: Failed to create listen socket"));
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
ListenSocket->SetReuseAddr(true);
|
||||
@@ -210,7 +139,7 @@ bool FMCPServer::Start(int32 InPort, bool bEditorMode)
|
||||
UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: Failed to bind to port %d"), Port);
|
||||
SocketSub->DestroySocket(ListenSocket);
|
||||
ListenSocket = nullptr;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ListenSocket->Listen(4))
|
||||
@@ -218,17 +147,21 @@ bool FMCPServer::Start(int32 InPort, bool bEditorMode)
|
||||
UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: Failed to listen on port %d"), Port);
|
||||
SocketSub->DestroySocket(ListenSocket);
|
||||
ListenSocket = nullptr;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
BuildMCPHandlerRegistry();
|
||||
bRunning = true;
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MCP server listening on tcp://localhost:%d"), Port);
|
||||
return true;
|
||||
}
|
||||
|
||||
void FMCPServer::Stop()
|
||||
void UMCPServer::Deinitialize()
|
||||
{
|
||||
if (!bRunning) return;
|
||||
if (!bRunning)
|
||||
{
|
||||
Super::Deinitialize();
|
||||
return;
|
||||
}
|
||||
|
||||
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
||||
|
||||
@@ -274,9 +207,117 @@ void FMCPServer::Stop()
|
||||
bRunning = false;
|
||||
bShuttingDown = false;
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Server stopped."));
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void FMCPServer::AcceptNewConnections()
|
||||
// ============================================================
|
||||
// FTickableEditorObject interface
|
||||
// ============================================================
|
||||
|
||||
void UMCPServer::Tick(float DeltaTime)
|
||||
{
|
||||
// Accept new connections (non-blocking)
|
||||
AcceptNewConnections();
|
||||
|
||||
// Clean up finished client threads
|
||||
CleanupFinishedClients();
|
||||
|
||||
// Dequeue one pending message
|
||||
TSharedPtr<FPendingMessage> Request;
|
||||
{
|
||||
FScopeLock Lock(&Mutex);
|
||||
if (PendingMessages.Num() > 0)
|
||||
{
|
||||
Request = PendingMessages[0];
|
||||
PendingMessages.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a request, process it.
|
||||
if (Request.IsValid())
|
||||
{
|
||||
FString Response = HandleRequest(Request->Line);
|
||||
Request->Response.SetValue(Response);
|
||||
}
|
||||
}
|
||||
|
||||
bool UMCPServer::IsTickable() const
|
||||
{
|
||||
return bRunning;
|
||||
}
|
||||
|
||||
TStatId UMCPServer::GetStatId() const
|
||||
{
|
||||
RETURN_QUICK_DECLARE_CYCLE_STAT(UMCPServer, STATGROUP_Tickables);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleRequest — Given a command, execute it.
|
||||
// ============================================================
|
||||
|
||||
FString UMCPServer::HandleRequest(const FString& Line)
|
||||
{
|
||||
// Turn the request string into a JSON tree.
|
||||
TSharedPtr<FJsonObject> Request = MCPUtils::ParseBodyJson(Line);
|
||||
if (!Request.IsValid())
|
||||
{
|
||||
return TEXT("Request is not valid JSON");
|
||||
}
|
||||
|
||||
// Extract the command from the request.
|
||||
FString Command;
|
||||
if (!Request->TryGetStringField(TEXT("command"), Command))
|
||||
{
|
||||
return TEXT("Request does not contain 'command' parameter");
|
||||
}
|
||||
Request->RemoveField(TEXT("command"));
|
||||
|
||||
// Find the handler UClass for the specified command.
|
||||
UClass** HandlerClass = MCPHandlerRegistry.Find(Command);
|
||||
if (!HandlerClass)
|
||||
{
|
||||
return FString::Printf(TEXT("Unknown command: %s"), *Command);
|
||||
}
|
||||
|
||||
// Make an object of the handler class.
|
||||
TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass));
|
||||
IMCPHandler* Handler = Cast<IMCPHandler>(HandlerObj.Get());
|
||||
|
||||
// Populate the handler object with the request parameters.
|
||||
TStringBuilder<4096> PopulateError;
|
||||
if (!MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), &*Request, PopulateError))
|
||||
{
|
||||
PopulateError.Append(TEXT("\nUsage:\n"));
|
||||
MCPUtils::FormatCommandHelp(*HandlerClass, PopulateError);
|
||||
return PopulateError.ToString();
|
||||
}
|
||||
|
||||
// Call the text handler. This may be a no-op, in which case
|
||||
// the string builder will remain empty. That indicates that
|
||||
// the json handler should be used instead.
|
||||
TStringBuilder<32768> TextResult;
|
||||
Handler->Handle(&*Request, TextResult);
|
||||
if (TextResult.Len() > 0)
|
||||
{
|
||||
FString Result = TextResult.ToString();
|
||||
for (int32 i = 0; i < Result.Len(); ++i)
|
||||
{
|
||||
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
// Invoke the Json handler.
|
||||
TSharedRef<FJsonObject> JsonResult = MakeShared<FJsonObject>();
|
||||
Handler->Handle(&*Request, &*JsonResult);
|
||||
return MCPUtils::JsonToString(JsonResult);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Connection Maintenance
|
||||
// ============================================================
|
||||
|
||||
void UMCPServer::AcceptNewConnections()
|
||||
{
|
||||
if (!ListenSocket) return;
|
||||
|
||||
@@ -296,7 +337,7 @@ void FMCPServer::AcceptNewConnections()
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Client connected."));
|
||||
}
|
||||
|
||||
void FMCPServer::CleanupFinishedClients()
|
||||
void UMCPServer::CleanupFinishedClients()
|
||||
{
|
||||
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
|
||||
|
||||
@@ -313,7 +354,7 @@ void FMCPServer::CleanupFinishedClients()
|
||||
}
|
||||
}
|
||||
|
||||
void FMCPServer::ClientThreadFunc(FMCPServer* Server, TSharedPtr<FClientConnection> Client)
|
||||
void UMCPServer::ClientThreadFunc(UMCPServer* Server, TSharedPtr<FClientConnection> Client)
|
||||
{
|
||||
FSocket* Socket = Client->Socket;
|
||||
FString LineBuffer;
|
||||
@@ -344,7 +385,7 @@ void FMCPServer::ClientThreadFunc(FMCPServer* Server, TSharedPtr<FClientConnecti
|
||||
if (Line.IsEmpty()) continue;
|
||||
|
||||
// Enqueue the line for game-thread processing
|
||||
TSharedPtr<FMCPServer::FPendingMessage> Msg = MakeShared<FMCPServer::FPendingMessage>();
|
||||
TSharedPtr<UMCPServer::FPendingMessage> Msg = MakeShared<UMCPServer::FPendingMessage>();
|
||||
Msg->Line = Line;
|
||||
TFuture<FString> Future = Msg->Response.GetFuture();
|
||||
|
||||
@@ -375,62 +416,15 @@ void FMCPServer::ClientThreadFunc(FMCPServer* Server, TSharedPtr<FClientConnecti
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Client disconnected."));
|
||||
}
|
||||
|
||||
bool FMCPServer::ProcessOneRequest()
|
||||
{
|
||||
// Accept new connections (non-blocking)
|
||||
AcceptNewConnections();
|
||||
|
||||
// Clean up finished client threads
|
||||
CleanupFinishedClients();
|
||||
|
||||
// Dequeue one pending message
|
||||
TSharedPtr<FPendingMessage> Msg;
|
||||
{
|
||||
FScopeLock Lock(&Mutex);
|
||||
if (PendingMessages.Num() > 0)
|
||||
{
|
||||
Msg = PendingMessages[0];
|
||||
PendingMessages.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Msg.IsValid()) return false;
|
||||
|
||||
// Process on game thread
|
||||
FString Response = HandleRequest(Msg->Line);
|
||||
Msg->Response.SetValue(Response);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// BuildMCPHandlerRegistry
|
||||
// ============================================================
|
||||
|
||||
void FMCPServer::BuildMCPHandlerRegistry()
|
||||
void UMCPServer::BuildMCPHandlerRegistry()
|
||||
{
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
for (UClass* Class : MCPUtils::CollectHandlerClasses())
|
||||
{
|
||||
UClass* Class = *It;
|
||||
if (!Class->ImplementsInterface(UMCPHandler::StaticClass()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (Class->HasAnyClassFlags(CLASS_Abstract))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FString ToolName = MCPUtils::GetToolName(Class);
|
||||
if (MCPHandlerRegistry.Contains(ToolName))
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Duplicate tool name '%s' on %s — skipping."), *ToolName, *Class->GetName());
|
||||
continue;
|
||||
}
|
||||
MCPHandlerRegistry.Add(ToolName, Class);
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Registered handler '%s' → %s"), *ToolName, *Class->GetName());
|
||||
MCPHandlerRegistry.FindOrAdd(MCPUtils::GetToolName(Class)) = Class;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %d new-style handlers registered."), MCPHandlerRegistry.Num());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1499,6 +1499,24 @@ bool MCPUtils::PopulateFromJson(
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CollectHandlerClasses — find all concrete IMCPHandler classes
|
||||
// ============================================================
|
||||
|
||||
TArray<UClass*> MCPUtils::CollectHandlerClasses()
|
||||
{
|
||||
TArray<UClass*> Result;
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
UClass* Class = *It;
|
||||
if (Class->HasAnyClassFlags(CLASS_Abstract)) continue;
|
||||
if (!Class->ImplementsInterface(UMCPHandler::StaticClass())) continue;
|
||||
Result.Add(Class);
|
||||
}
|
||||
Result.Sort([](UClass& A, UClass& B) { return GetToolName(&A) < GetToolName(&B); });
|
||||
return Result;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GetToolName — derive tool name from handler class name
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user