Simplify MCPServer

This commit is contained in:
2026-03-09 03:44:35 -04:00
parent aceec452d6
commit 642e3aca0a
9 changed files with 201 additions and 313 deletions

View File

@@ -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"));
}
}
};

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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
// ============================================================

View File

@@ -2,14 +2,13 @@
#include "CoreMinimal.h"
#include "Commandlets/Commandlet.h"
#include "MCPServer.h"
#include "MCPCommandlet.generated.h"
/**
* Standalone commandlet that hosts the Blueprint MCP HTTP server.
* Delegates all logic to FBlueprintMCPServer and runs a manual engine tick loop.
* Commandlet that keeps the engine alive so the BlueprintMCP editor subsystem
* can serve MCP requests without the full editor UI.
*
* Usage: UnrealEditor-Cmd.exe Project.uproject -run=BlueprintMCP [-port=9847]
* Usage: UnrealEditor-Cmd.exe Project.uproject -run=BlueprintMCP
*/
UCLASS()
class UBlueprintMCPCommandlet : public UCommandlet
@@ -19,7 +18,4 @@ class UBlueprintMCPCommandlet : public UCommandlet
public:
UBlueprintMCPCommandlet();
virtual int32 Main(const FString& Params) override;
private:
TUniquePtr<FBlueprintMCPServer> Server;
};

View File

@@ -2,38 +2,22 @@
#include "CoreMinimal.h"
#include "EditorSubsystem.h"
#include "Tickable.h"
#include "UObject/ObjectSaveContext.h"
#include "MCPServer.h"
#include "MCPEditorSubsystem.generated.h"
/**
* Editor subsystem that hosts the Blueprint MCP HTTP server inside the running
* UE5 editor. When active, the MCP TypeScript wrapper connects instantly
* (no commandlet spawn, no extra RAM).
*
* Requests are dequeued and processed on the editor's game thread via
* FTickableEditorObject::Tick().
* Editor subsystem that exports blueprint text files whenever an asset is saved.
*/
UCLASS()
class UBlueprintMCPEditorSubsystem : public UEditorSubsystem, public FTickableEditorObject
class UBlueprintMCPEditorSubsystem : public UEditorSubsystem
{
GENERATED_BODY()
public:
// UEditorSubsystem
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// FTickableEditorObject
virtual void Tick(float DeltaTime) override;
virtual bool IsTickable() const override;
virtual TStatId GetStatId() const override;
FBlueprintMCPServer* GetServer() const { return Server.Get(); }
private:
void OnAssetSaved(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext Context);
FDelegateHandle OnAssetSavedHandle;
TUniquePtr<FBlueprintMCPServer> Server;
};

View File

@@ -1,15 +1,18 @@
#pragma once
#include "CoreMinimal.h"
#include "EditorSubsystem.h"
#include "Tickable.h"
#include "Async/Future.h"
#include "Dom/JsonObject.h"
#include "MCPUtils.h"
#include "MCPServer.generated.h"
class FSocket;
class IMCPHandler;
/**
* FMCPServer — plain C++ class (not a UCLASS) that listens on a TCP
* socket and dispatches JSON commands to blueprint editing handlers.
* UMCPServer — editor subsystem that listens on a TCP socket and dispatches
* JSON commands to blueprint editing handlers.
*
* Clients connect via TCP and exchange newline-delimited JSON messages.
* Request format: {"command": "tool_name", "param1": "value1", ...}
@@ -18,30 +21,26 @@ class IMCPHandler;
* Each connected client gets its own thread for blocking I/O;
* tool calls are dispatched on the game thread.
*
* Both the standalone commandlet (UBlueprintMCPCommandlet) and the in-editor
* subsystem (UBlueprintMCPEditorSubsystem) delegate to an instance of this
* class. The only difference is *who ticks the engine*:
* - Commandlet: manual FTSTicker loop
* - Editor subsystem: UE editor tick via FTickableEditorObject
* In the editor, FTickableEditorObject drives the tick.
* In commandlet mode, the commandlet ticks us directly.
*/
class FMCPServer
UCLASS()
class UMCPServer : public UEditorSubsystem, public FTickableEditorObject
{
GENERATED_BODY()
public:
/** Get the active server instance via the editor subsystem. */
static FMCPServer* Get();
/** Get the active server instance via GEditor. */
static UMCPServer* Get();
/** Start listening for MCP clients on the given TCP port. */
bool Start(int32 InPort, bool bEditorMode = false);
// UEditorSubsystem
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
/** Stop the server: drain pending requests, close all sockets, join threads. */
void Stop();
/**
* Process pending MCP requests on the game thread.
* Call this every tick from whichever host owns this server.
* Returns true if a request was processed.
*/
bool ProcessOneRequest();
// FTickableEditorObject
virtual void Tick(float DeltaTime) override;
virtual bool IsTickable() const override;
virtual TStatId GetStatId() const override;
/** Whether the server is currently listening. */
bool IsRunning() const { return bRunning; }
@@ -50,13 +49,11 @@ public:
int32 GetPort() const { return Port; }
private:
// ----- Tool dispatch -----
TMap<FString, UClass*> MCPHandlerRegistry; // tool name -> UMCPHandler subclass
void BuildMCPHandlerRegistry();
// Dispatch a tool call to the appropriate handler, returning the response string.
FString DispatchToolCall(const FString& ToolName, const FJsonObject* Params);
// Handle a complete JSON line and return the response JSON
FString HandleRequest(const FString& Line);
@@ -64,7 +61,6 @@ private:
FSocket* ListenSocket = nullptr;
int32 Port = 9847;
bool bRunning = false;
bool bIsEditor = false;
// ----- Client connections -----
struct FClientConnection
@@ -76,7 +72,7 @@ private:
TArray<TSharedPtr<FClientConnection>> Clients;
void AcceptNewConnections();
void CleanupFinishedClients();
static void ClientThreadFunc(FMCPServer* Server, TSharedPtr<FClientConnection> Client);
static void ClientThreadFunc(UMCPServer* Server, TSharedPtr<FClientConnection> Client);
// ----- Thread-safe message queue -----
struct FPendingMessage
@@ -89,6 +85,3 @@ private:
TArray<TSharedPtr<FPendingMessage>> PendingMessages;
bool bShuttingDown = false;
};
using FBlueprintMCPServer = FMCPServer;

View File

@@ -199,6 +199,10 @@ public:
static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue, MCPErrorCallback Error);
static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json, MCPErrorCallback Error);
// ----- Handler discovery -----
// Collect all concrete IMCPHandler classes, sorted by tool name.
static TArray<UClass*> CollectHandlerClasses();
// ----- Command help -----
// Derive tool name from handler class name: "MCPHandler_FooBar" → "FooBar"
static FString GetToolName(UClass* HandlerClass);