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": {
"blueprint-mcp": {
"command": "/usr/bin/nc",
"args": ["localhost", "9847"]
}
}
}

View File

@@ -15,7 +15,6 @@ public class BlueprintMCP : ModuleRules
"BlueprintGraph",
"Json",
"JsonUtilities",
"HTTPServer",
"Sockets",
"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 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;
}

View File

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

View File

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

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

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