Blueprint MCP is now a proper MCP server
This commit is contained in:
@@ -15,7 +15,6 @@ public class BlueprintMCP : ModuleRules
|
||||
"BlueprintGraph",
|
||||
"Json",
|
||||
"JsonUtilities",
|
||||
"HTTPServer",
|
||||
"Sockets",
|
||||
"Networking"
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,7 +85,7 @@ extern int32 TrySavePackageSEH(
|
||||
FString MCPUtils::JsonToString(TSharedRef<FJsonObject> JsonObj)
|
||||
{
|
||||
FString Output;
|
||||
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
|
||||
TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> Writer = TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Output);
|
||||
FJsonSerializer::Serialize(JsonObj, Writer);
|
||||
return Output;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -92,10 +92,10 @@ public:
|
||||
static TArray<UBlueprintNodeSpawner*> SearchNodeSpawners(const FString& Query, int32 MaxResults = 0, bool ExactMatch = false);
|
||||
|
||||
// ----- Property population -----
|
||||
static FString PropertyNameToJsonKey(const FString& PropName);
|
||||
static FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue);
|
||||
static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json);
|
||||
|
||||
private:
|
||||
static FString PropertyNameToJsonKey(const FString& PropName);
|
||||
static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user