|
|
|
|
@@ -2,14 +2,17 @@
|
|
|
|
|
|
|
|
|
|
#include "CoreMinimal.h"
|
|
|
|
|
#include "Dom/JsonObject.h"
|
|
|
|
|
#include "HttpResultCallback.h"
|
|
|
|
|
#include "MCPUtils.h"
|
|
|
|
|
|
|
|
|
|
class FSocket;
|
|
|
|
|
class IMCPHandler;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* FBlueprintMCPServer — plain C++ class (not a UCLASS) that owns all HTTP
|
|
|
|
|
* serving logic for the Blueprint MCP protocol.
|
|
|
|
|
* FMCPServer — plain C++ class (not a UCLASS) that implements the
|
|
|
|
|
* Model Context Protocol (MCP) over a TCP socket using JSON-RPC.
|
|
|
|
|
*
|
|
|
|
|
* Clients connect via TCP and exchange newline-delimited JSON-RPC messages
|
|
|
|
|
* (the MCP "stdio" protocol). 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
|
|
|
|
|
@@ -17,55 +20,92 @@ class IMCPHandler;
|
|
|
|
|
* - Commandlet: manual FTSTicker loop
|
|
|
|
|
* - Editor subsystem: UE editor tick via FTickableEditorObject
|
|
|
|
|
*/
|
|
|
|
|
class FBlueprintMCPServer
|
|
|
|
|
class FMCPServer
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
/** Get the active server instance via the editor subsystem. */
|
|
|
|
|
static FBlueprintMCPServer* Get();
|
|
|
|
|
static FMCPServer* Get();
|
|
|
|
|
|
|
|
|
|
/** Scan asset registry, bind HTTP routes, start listener on the given port.
|
|
|
|
|
* Set bEditorMode=true when hosted inside the UE5 editor (disables /api/shutdown). */
|
|
|
|
|
/** Start listening for MCP clients on the given TCP port. */
|
|
|
|
|
bool Start(int32 InPort, bool bEditorMode = false);
|
|
|
|
|
|
|
|
|
|
/** Stop the HTTP listener and clean up. */
|
|
|
|
|
/** Stop the server: drain pending requests, close all sockets, join threads. */
|
|
|
|
|
void Stop();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Dequeue and handle ONE pending HTTP request on the calling (game) thread.
|
|
|
|
|
* 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();
|
|
|
|
|
|
|
|
|
|
/** Whether the HTTP server is currently listening. */
|
|
|
|
|
/** Whether the server is currently listening. */
|
|
|
|
|
bool IsRunning() const { return bRunning; }
|
|
|
|
|
|
|
|
|
|
/** Port the server is listening on. */
|
|
|
|
|
int32 GetPort() const { return Port; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
// ----- Request dispatch -----
|
|
|
|
|
// ----- Tool dispatch -----
|
|
|
|
|
using FRequestHandler = TFunction<void(const FJsonObject* Json, FJsonObject* Result)>;
|
|
|
|
|
TMap<FString, FRequestHandler> HandlerMap; // old-style handlers
|
|
|
|
|
TMap<FString, UClass*> MCPHandlerRegistry; // new-style: tool name → UMCPHandler subclass
|
|
|
|
|
TMap<FString, UClass*> MCPHandlerRegistry; // new-style: tool name -> UMCPHandler subclass
|
|
|
|
|
TSet<FString> MutationEndpoints;
|
|
|
|
|
void RegisterHandlers();
|
|
|
|
|
void BuildMCPHandlerRegistry();
|
|
|
|
|
// ----- Queued request model -----
|
|
|
|
|
struct FPendingRequest
|
|
|
|
|
{
|
|
|
|
|
FString Endpoint;
|
|
|
|
|
TMap<FString, FString> QueryParams;
|
|
|
|
|
FString Body;
|
|
|
|
|
FHttpResultCallback OnComplete;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
TQueue<TSharedPtr<FPendingRequest>, EQueueMode::Mpsc> RequestQueue;
|
|
|
|
|
// Dispatch a tool call to the appropriate handler
|
|
|
|
|
void DispatchToolCall(const FString& ToolName, const FJsonObject* Params, FJsonObject* Result);
|
|
|
|
|
|
|
|
|
|
// ----- MCP protocol -----
|
|
|
|
|
// Handle a complete JSON-RPC line and return the response line (or empty for notifications)
|
|
|
|
|
FString HandleJsonRpc(const FString& Line);
|
|
|
|
|
|
|
|
|
|
// MCP method handlers
|
|
|
|
|
FString HandleInitialize(int32 Id, const FJsonObject* Params);
|
|
|
|
|
FString HandleToolsList(int32 Id);
|
|
|
|
|
FString HandleToolsCall(int32 Id, const FJsonObject* Params);
|
|
|
|
|
|
|
|
|
|
// Build the tools/list response (cached after first call)
|
|
|
|
|
TSharedPtr<FJsonObject> CachedToolsList;
|
|
|
|
|
static TSharedRef<FJsonObject> FPropertyToPropSchema(FProperty* Prop);
|
|
|
|
|
static TSharedRef<FJsonObject> HandlerClassToToolSchema(UClass* HandlerClass);
|
|
|
|
|
void BuildCachedToolsList();
|
|
|
|
|
|
|
|
|
|
// JSON-RPC helpers
|
|
|
|
|
static FString MakeJsonRpcResult(int32 Id, TSharedPtr<FJsonObject> Result);
|
|
|
|
|
static FString MakeJsonRpcToolResult(int32 Id, TSharedPtr<FJsonObject> ToolResult, bool bIsError);
|
|
|
|
|
static FString MakeJsonRpcError(int32 Id, int32 Code, const FString& Message);
|
|
|
|
|
|
|
|
|
|
// ----- TCP server -----
|
|
|
|
|
FSocket* ListenSocket = nullptr;
|
|
|
|
|
int32 Port = 9847;
|
|
|
|
|
bool bRunning = false;
|
|
|
|
|
bool bIsEditor = false;
|
|
|
|
|
|
|
|
|
|
// ----- Client connections -----
|
|
|
|
|
struct FClientConnection
|
|
|
|
|
{
|
|
|
|
|
FSocket* Socket = nullptr;
|
|
|
|
|
TFuture<void> ThreadFuture;
|
|
|
|
|
bool bDone = false;
|
|
|
|
|
};
|
|
|
|
|
TArray<TSharedPtr<FClientConnection>> Clients;
|
|
|
|
|
void AcceptNewConnections();
|
|
|
|
|
void CleanupFinishedClients();
|
|
|
|
|
static void ClientThreadFunc(FMCPServer* Server, TSharedPtr<FClientConnection> Client);
|
|
|
|
|
|
|
|
|
|
// ----- Thread-safe message queue -----
|
|
|
|
|
struct FPendingMessage
|
|
|
|
|
{
|
|
|
|
|
FString Line;
|
|
|
|
|
TPromise<FString> Response;
|
|
|
|
|
FPendingMessage() : Response(TPromise<FString>()) {}
|
|
|
|
|
};
|
|
|
|
|
FCriticalSection Mutex;
|
|
|
|
|
TArray<TSharedPtr<FPendingMessage>> PendingMessages;
|
|
|
|
|
bool bShuttingDown = false;
|
|
|
|
|
|
|
|
|
|
// ----- Request handlers (read-only) -----
|
|
|
|
|
void HandleList(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
void HandleGetBlueprint(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
@@ -114,7 +154,6 @@ private:
|
|
|
|
|
void HandleRemoveVariable(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
void HandleSetVariableMetadata(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ----- Event Dispatchers -----
|
|
|
|
|
void HandleAddEventDispatcher(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
void HandleListEventDispatchers(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
@@ -137,7 +176,6 @@ private:
|
|
|
|
|
void HandleFindDisconnectedPins(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
void HandleAnalyzeRebuildImpact(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ----- Material read-only handlers (Phase 1) -----
|
|
|
|
|
void HandleListMaterials(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
void HandleGetMaterial(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
@@ -191,7 +229,6 @@ private:
|
|
|
|
|
void HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result);
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
|
|
|
|
|
// ----- Snapshot storage -----
|
|
|
|
|
TMap<FString, FGraphSnapshot> Snapshots;
|
|
|
|
|
TMap<FString, FGraphSnapshot> MaterialSnapshots;
|
|
|
|
|
@@ -206,4 +243,5 @@ public:
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Transitional alias — old-style handlers use this to access the server instance.
|
|
|
|
|
using MCPHelper = FBlueprintMCPServer;
|
|
|
|
|
using FBlueprintMCPServer = FMCPServer;
|
|
|
|
|
using MCPHelper = FMCPServer;
|
|
|
|
|
|