Blueprint MCP is now a proper MCP server

This commit is contained in:
2026-03-06 19:03:33 -05:00
parent 6f22d62811
commit 7474a0ea91
7 changed files with 689 additions and 502 deletions

View File

@@ -1,4 +1,8 @@
{ {
"mcpServers": { "mcpServers": {
"blueprint-mcp": {
"command": "/usr/bin/nc",
"args": ["localhost", "9847"]
}
} }
} }

View File

@@ -15,7 +15,6 @@ public class BlueprintMCP : ModuleRules
"BlueprintGraph", "BlueprintGraph",
"Json", "Json",
"JsonUtilities", "JsonUtilities",
"HTTPServer",
"Sockets", "Sockets",
"Networking" "Networking"
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -85,7 +85,7 @@ extern int32 TrySavePackageSEH(
FString MCPUtils::JsonToString(TSharedRef<FJsonObject> JsonObj) FString MCPUtils::JsonToString(TSharedRef<FJsonObject> JsonObj)
{ {
FString Output; FString Output;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output); TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> Writer = TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&Output);
FJsonSerializer::Serialize(JsonObj, Writer); FJsonSerializer::Serialize(JsonObj, Writer);
return Output; return Output;
} }

View File

@@ -2,14 +2,17 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "Dom/JsonObject.h" #include "Dom/JsonObject.h"
#include "HttpResultCallback.h"
#include "MCPUtils.h" #include "MCPUtils.h"
class FSocket;
class IMCPHandler; class IMCPHandler;
/** /**
* FBlueprintMCPServer — plain C++ class (not a UCLASS) that owns all HTTP * FMCPServer — plain C++ class (not a UCLASS) that implements the
* serving logic for the Blueprint MCP protocol. * 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 * Both the standalone commandlet (UBlueprintMCPCommandlet) and the in-editor
* subsystem (UBlueprintMCPEditorSubsystem) delegate to an instance of this * subsystem (UBlueprintMCPEditorSubsystem) delegate to an instance of this
@@ -17,55 +20,92 @@ class IMCPHandler;
* - Commandlet: manual FTSTicker loop * - Commandlet: manual FTSTicker loop
* - Editor subsystem: UE editor tick via FTickableEditorObject * - Editor subsystem: UE editor tick via FTickableEditorObject
*/ */
class FBlueprintMCPServer class FMCPServer
{ {
public: public:
/** Get the active server instance via the editor subsystem. */ /** 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. /** Start listening for MCP clients on the given TCP port. */
* Set bEditorMode=true when hosted inside the UE5 editor (disables /api/shutdown). */
bool Start(int32 InPort, bool bEditorMode = false); 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(); 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. * Call this every tick from whichever host owns this server.
* Returns true if a request was processed. * Returns true if a request was processed.
*/ */
bool ProcessOneRequest(); bool ProcessOneRequest();
/** Whether the HTTP server is currently listening. */ /** Whether the server is currently listening. */
bool IsRunning() const { return bRunning; } bool IsRunning() const { return bRunning; }
/** Port the server is listening on. */ /** Port the server is listening on. */
int32 GetPort() const { return Port; } int32 GetPort() const { return Port; }
private: private:
// ----- Request dispatch ----- // ----- Tool dispatch -----
using FRequestHandler = TFunction<void(const FJsonObject* Json, FJsonObject* Result)>; using FRequestHandler = TFunction<void(const FJsonObject* Json, FJsonObject* Result)>;
TMap<FString, FRequestHandler> HandlerMap; // old-style handlers 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; TSet<FString> MutationEndpoints;
void RegisterHandlers(); void RegisterHandlers();
void BuildMCPHandlerRegistry(); 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; int32 Port = 9847;
bool bRunning = false; bool bRunning = false;
bool bIsEditor = 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) ----- // ----- Request handlers (read-only) -----
void HandleList(const FJsonObject* Json, FJsonObject* Result); void HandleList(const FJsonObject* Json, FJsonObject* Result);
void HandleGetBlueprint(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 HandleRemoveVariable(const FJsonObject* Json, FJsonObject* Result);
void HandleSetVariableMetadata(const FJsonObject* Json, FJsonObject* Result); void HandleSetVariableMetadata(const FJsonObject* Json, FJsonObject* Result);
// ----- Event Dispatchers ----- // ----- Event Dispatchers -----
void HandleAddEventDispatcher(const FJsonObject* Json, FJsonObject* Result); void HandleAddEventDispatcher(const FJsonObject* Json, FJsonObject* Result);
void HandleListEventDispatchers(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 HandleFindDisconnectedPins(const FJsonObject* Json, FJsonObject* Result);
void HandleAnalyzeRebuildImpact(const FJsonObject* Json, FJsonObject* Result); void HandleAnalyzeRebuildImpact(const FJsonObject* Json, FJsonObject* Result);
// ----- Material read-only handlers (Phase 1) ----- // ----- Material read-only handlers (Phase 1) -----
void HandleListMaterials(const FJsonObject* Json, FJsonObject* Result); void HandleListMaterials(const FJsonObject* Json, FJsonObject* Result);
void HandleGetMaterial(const FJsonObject* Json, FJsonObject* Result); void HandleGetMaterial(const FJsonObject* Json, FJsonObject* Result);
@@ -191,7 +229,6 @@ private:
void HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result); void HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result);
public: public:
// ----- Snapshot storage ----- // ----- Snapshot storage -----
TMap<FString, FGraphSnapshot> Snapshots; TMap<FString, FGraphSnapshot> Snapshots;
TMap<FString, FGraphSnapshot> MaterialSnapshots; TMap<FString, FGraphSnapshot> MaterialSnapshots;
@@ -206,4 +243,5 @@ public:
}; };
// Transitional alias — old-style handlers use this to access the server instance. // Transitional alias — old-style handlers use this to access the server instance.
using MCPHelper = FBlueprintMCPServer; using FBlueprintMCPServer = FMCPServer;
using MCPHelper = FMCPServer;

View File

@@ -92,10 +92,10 @@ public:
static TArray<UBlueprintNodeSpawner*> SearchNodeSpawners(const FString& Query, int32 MaxResults = 0, bool ExactMatch = false); static TArray<UBlueprintNodeSpawner*> SearchNodeSpawners(const FString& Query, int32 MaxResults = 0, bool ExactMatch = false);
// ----- Property population ----- // ----- 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 TSharedPtr<FJsonValue>& JsonValue);
static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json); static FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json);
private: private:
static FString PropertyNameToJsonKey(const FString& PropName);
static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json); static FString SetPropertyFromJson(void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json);
}; };

2
tools/mcp-bridge.sh Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
exec /usr/bin/nc localhost 9847