From 7474a0ea91c3cc2b7c0c74a7d7ccf1b392557237 Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 6 Mar 2026 19:03:33 -0500 Subject: [PATCH] Blueprint MCP is now a proper MCP server --- .mcp.json | 4 + .../Source/BlueprintMCP/BlueprintMCP.Build.cs | 1 - .../Source/BlueprintMCP/Private/MCPServer.cpp | 1088 ++++++++++------- .../Source/BlueprintMCP/Private/MCPUtils.cpp | 2 +- .../Source/BlueprintMCP/Public/MCPServer.h | 92 +- .../Source/BlueprintMCP/Public/MCPUtils.h | 2 +- tools/mcp-bridge.sh | 2 + 7 files changed, 689 insertions(+), 502 deletions(-) create mode 100644 tools/mcp-bridge.sh diff --git a/.mcp.json b/.mcp.json index 53f188a2..27fa8451 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,4 +1,8 @@ { "mcpServers": { + "blueprint-mcp": { + "command": "/usr/bin/nc", + "args": ["localhost", "9847"] + } } } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/BlueprintMCP.Build.cs b/Plugins/BlueprintMCP/Source/BlueprintMCP/BlueprintMCP.Build.cs index 37e62214..ee026262 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/BlueprintMCP.Build.cs +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/BlueprintMCP.Build.cs @@ -15,7 +15,6 @@ public class BlueprintMCP : ModuleRules "BlueprintGraph", "Json", "JsonUtilities", - "HTTPServer", "Sockets", "Networking" }); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp index 57189d3d..275ccd46 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPServer.cpp @@ -34,11 +34,11 @@ #include "Serialization/JsonReader.h" #include "Serialization/JsonWriter.h" #include "Serialization/JsonSerializer.h" -#include "HttpServerModule.h" -#include "IHttpRouter.h" -#include "HttpPath.h" +#include "Interfaces/IPv4/IPv4Address.h" +#include "Interfaces/IPv4/IPv4Endpoint.h" #include "SocketSubsystem.h" #include "Sockets.h" +#include "Async/Async.h" #include "UObject/SavePackage.h" #include "Misc/Paths.h" #include "Misc/FileHelper.h" @@ -53,7 +53,6 @@ #include "Materials/Material.h" #include "Materials/MaterialInstanceConstant.h" #include "Materials/MaterialFunction.h" -#include "Materials/MaterialExpression.h" #include "Materials/MaterialExpressionScalarParameter.h" #include "Materials/MaterialExpressionVectorParameter.h" #include "Materials/MaterialExpressionTextureObjectParameter.h" @@ -106,7 +105,7 @@ #include "MCPEditorSubsystem.h" -FBlueprintMCPServer* FBlueprintMCPServer::Get() +FMCPServer* FMCPServer::Get() { if (!GEditor) return nullptr; auto* Sub = GEditor->GetEditorSubsystem(); @@ -250,404 +249,266 @@ int32 TryAddMaterialExpressionSEH( #endif // PLATFORM_WINDOWS // ============================================================ -// Start / Stop / ProcessOneRequest +// JSON-RPC helpers // ============================================================ -bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) +FString FMCPServer::MakeJsonRpcResult(int32 Id, TSharedPtr Result) { - Port = InPort; - bIsEditor = bEditorMode; - - // Start HTTP server - FHttpServerModule& HttpModule = FModuleManager::LoadModuleChecked("HTTPServer"); - TSharedPtr Router = HttpModule.GetHttpRouter(Port); - if (!Router.IsValid()) - { - UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: Failed to create HTTP router on port %d"), Port); - return false; - } - - // Lambda that creates a queued handler — dispatches work to main thread - auto QueuedHandler = [this](const FString& Endpoint) - { - return FHttpRequestHandler::CreateLambda( - [this, Endpoint](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) - { - TSharedPtr Req = MakeShared(); - Req->Endpoint = Endpoint; - Req->QueryParams = Request.QueryParams; - // Capture POST body as UTF-8 string - if (Request.Body.Num() > 0) - { - TArray NullTerminated(Request.Body); - NullTerminated.Add(0); - Req->Body = UTF8_TO_TCHAR((const ANSICHAR*)NullTerminated.GetData()); - } - Req->OnComplete = OnComplete; - RequestQueue.Enqueue(Req); - return true; - }); - }; - - // /api/health — answered directly on HTTP thread (no asset access) - Router->BindRoute(FHttpPath(TEXT("/api/health")), EHttpServerRequestVerbs::VERB_GET, - FHttpRequestHandler::CreateLambda( - [this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) - { - TSharedRef J = MakeShared(); - J->SetStringField(TEXT("status"), TEXT("ok")); - J->SetStringField(TEXT("mode"), bIsEditor ? TEXT("editor") : TEXT("commandlet")); - J->SetNumberField(TEXT("blueprintCount"), UMCPAssetFinder::GetBlueprintAssets().Num()); - J->SetNumberField(TEXT("mapCount"), UMCPAssetFinder::GetMapAssets().Num()); - J->SetNumberField(TEXT("materialCount"), UMCPAssetFinder::GetMaterialAssets().Num()); - J->SetNumberField(TEXT("materialInstanceCount"), UMCPAssetFinder::GetMaterialInstanceAssets().Num()); - J->SetNumberField(TEXT("materialFunctionCount"), UMCPAssetFinder::GetMaterialFunctionAssets().Num()); - TUniquePtr R = FHttpServerResponse::Create( - MCPUtils::JsonToString(J), TEXT("application/json")); - OnComplete(MoveTemp(R)); - return true; - })); - - // /api/shutdown — request graceful engine exit (commandlet only) - Router->BindRoute(FHttpPath(TEXT("/api/shutdown")), EHttpServerRequestVerbs::VERB_POST, - FHttpRequestHandler::CreateLambda( - [this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) - { - TSharedRef J = MakeShared(); - if (bIsEditor) - { - J->SetStringField(TEXT("error"), TEXT("Cannot shut down the editor's MCP server.")); - } - else - { - J->SetStringField(TEXT("status"), TEXT("shutting_down")); - RequestEngineExit(TEXT("BlueprintMCP /api/shutdown")); - } - TUniquePtr R = FHttpServerResponse::Create( - MCPUtils::JsonToString(J), TEXT("application/json")); - OnComplete(MoveTemp(R)); - return true; - })); - - - // /api/list — answered directly (only reads immutable asset list) - Router->BindRoute(FHttpPath(TEXT("/api/list_blueprint_assets")), EHttpServerRequestVerbs::VERB_GET, - FHttpRequestHandler::CreateLambda( - [this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) - { - TSharedRef Params = MakeShared(); - for (const auto& KV : Request.QueryParams) - { - Params->SetStringField(KV.Key, KV.Value); - } - TSharedRef ListResult = MakeShared(); - HandleList(&*Params, &*ListResult); - TUniquePtr R = FHttpServerResponse::Create( - MCPUtils::JsonToString(ListResult), TEXT("application/json")); - OnComplete(MoveTemp(R)); - return true; - })); - - // Old-style handlers (queued, not yet ported to UMCPHandler) - Router->BindRoute(FHttpPath(TEXT("/api/dump_blueprint")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("dump_blueprint"))); - Router->BindRoute(FHttpPath(TEXT("/api/dump_blueprint_graph")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("dump_blueprint_graph"))); - Router->BindRoute(FHttpPath(TEXT("/api/search_within_blueprints")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("search_within_blueprints"))); - Router->BindRoute(FHttpPath(TEXT("/api/find_asset_references")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("find_asset_references"))); - Router->BindRoute(FHttpPath(TEXT("/api/change_blueprint_variable_type")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("change_blueprint_variable_type"))); - Router->BindRoute(FHttpPath(TEXT("/api/change_function_parameter_type")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("change_function_parameter_type"))); - Router->BindRoute(FHttpPath(TEXT("/api/test_save_blueprint_package")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("test_save_blueprint_package"))); - Router->BindRoute(FHttpPath(TEXT("/api/get_pin_details")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("get_pin_details"))); - Router->BindRoute(FHttpPath(TEXT("/api/check_pin_connection_compatibility")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("check_pin_connection_compatibility"))); - Router->BindRoute(FHttpPath(TEXT("/api/search_unreal_classes")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("search_unreal_classes"))); - Router->BindRoute(FHttpPath(TEXT("/api/list_class_functions")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("list_class_functions"))); - Router->BindRoute(FHttpPath(TEXT("/api/list_class_properties")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("list_class_properties"))); - Router->BindRoute(FHttpPath(TEXT("/api/remove_function_parameter")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("remove_function_parameter"))); - Router->BindRoute(FHttpPath(TEXT("/api/search_type_usage_in_blueprints")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("search_type_usage_in_blueprints"))); - Router->BindRoute(FHttpPath(TEXT("/api/compile_blueprint")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("compile_blueprint"))); - Router->BindRoute(FHttpPath(TEXT("/api/compile_all_blueprints")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("compile_all_blueprints"))); - Router->BindRoute(FHttpPath(TEXT("/api/reparent_blueprint")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("reparent_blueprint"))); - Router->BindRoute(FHttpPath(TEXT("/api/create_blueprint_asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("create_blueprint_asset"))); - Router->BindRoute(FHttpPath(TEXT("/api/create_blueprint_graph")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("create_blueprint_graph"))); - Router->BindRoute(FHttpPath(TEXT("/api/create_struct_asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("create_struct_asset"))); - Router->BindRoute(FHttpPath(TEXT("/api/create_enum_asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("create_enum_asset"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_struct_field")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_struct_field"))); - Router->BindRoute(FHttpPath(TEXT("/api/remove_struct_field")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("remove_struct_field"))); - Router->BindRoute(FHttpPath(TEXT("/api/delete_blueprint_graph")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("delete_blueprint_graph"))); - Router->BindRoute(FHttpPath(TEXT("/api/rename_blueprint_graph")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("rename_blueprint_graph"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_blueprint_variable")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_blueprint_variable"))); - Router->BindRoute(FHttpPath(TEXT("/api/remove_blueprint_variable")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("remove_blueprint_variable"))); - Router->BindRoute(FHttpPath(TEXT("/api/set_blueprint_variable_metadata")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("set_blueprint_variable_metadata"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_event_dispatcher")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_event_dispatcher"))); - Router->BindRoute(FHttpPath(TEXT("/api/list_event_dispatchers")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("list_event_dispatchers"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_function_parameter")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_function_parameter"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_blueprint_component")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_blueprint_component"))); - Router->BindRoute(FHttpPath(TEXT("/api/remove_blueprint_component")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("remove_blueprint_component"))); - Router->BindRoute(FHttpPath(TEXT("/api/list_blueprint_components")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("list_blueprint_components"))); - Router->BindRoute(FHttpPath(TEXT("/api/snapshot_blueprint_graph")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("snapshot_blueprint_graph"))); - Router->BindRoute(FHttpPath(TEXT("/api/diff_blueprint_graph_vs_snapshot")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("diff_blueprint_graph_vs_snapshot"))); - Router->BindRoute(FHttpPath(TEXT("/api/restore_blueprint_graph_from_snapshot")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("restore_blueprint_graph_from_snapshot"))); - Router->BindRoute(FHttpPath(TEXT("/api/find_pins_disconnected_since_snapshot")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("find_pins_disconnected_since_snapshot"))); - Router->BindRoute(FHttpPath(TEXT("/api/analyze_cpp_rebuild_impact")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("analyze_cpp_rebuild_impact"))); - - // Material read-only tools (Phase 1) - Router->BindRoute(FHttpPath(TEXT("/api/list_material_assets")), EHttpServerRequestVerbs::VERB_GET, - FHttpRequestHandler::CreateLambda( - [this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) - { - TSharedRef Params = MakeShared(); - for (const auto& KV : Request.QueryParams) - { - Params->SetStringField(KV.Key, KV.Value); - } - TSharedRef ListResult = MakeShared(); - HandleListMaterials(&*Params, &*ListResult); - TUniquePtr R = FHttpServerResponse::Create( - MCPUtils::JsonToString(ListResult), TEXT("application/json")); - OnComplete(MoveTemp(R)); - return true; - })); - Router->BindRoute(FHttpPath(TEXT("/api/material")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("dump_material"))); - Router->BindRoute(FHttpPath(TEXT("/api/material_graph")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("dump_material_expression_graph"))); - Router->BindRoute(FHttpPath(TEXT("/api/describe_material_in_english")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("describe_material_in_english"))); - Router->BindRoute(FHttpPath(TEXT("/api/search_within_materials")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("search_within_materials"))); - Router->BindRoute(FHttpPath(TEXT("/api/material_references")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("find_material_references"))); - - // Material mutation tools (Phase 2) - Router->BindRoute(FHttpPath(TEXT("/api/create_material_asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("create_material_asset"))); - Router->BindRoute(FHttpPath(TEXT("/api/set_material_property")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("set_material_property"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_material_expression")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_material_expression"))); - Router->BindRoute(FHttpPath(TEXT("/api/delete_material_expression")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("delete_material_expression"))); - Router->BindRoute(FHttpPath(TEXT("/api/connect_material_expression_pins")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("connect_material_expression_pins"))); - Router->BindRoute(FHttpPath(TEXT("/api/disconnect_material_expression_pin")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("disconnect_material_expression_pin"))); - Router->BindRoute(FHttpPath(TEXT("/api/set_material_expression_property")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("set_material_expression_property"))); - Router->BindRoute(FHttpPath(TEXT("/api/set_material_expression_position")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("set_material_expression_position"))); - - // Material instance tools (Phase 3) - Router->BindRoute(FHttpPath(TEXT("/api/create_material_instance_asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("create_material_instance_asset"))); - Router->BindRoute(FHttpPath(TEXT("/api/set_material_instance_parameter")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("set_material_instance_parameter"))); - Router->BindRoute(FHttpPath(TEXT("/api/material_instance_params")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("dump_material_instance_parameters"))); - Router->BindRoute(FHttpPath(TEXT("/api/reparent_material_instance")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("reparent_material_instance"))); - - // Material function tools (Phase 4) - Router->BindRoute(FHttpPath(TEXT("/api/list_material_function_assets")), EHttpServerRequestVerbs::VERB_GET, - FHttpRequestHandler::CreateLambda( - [this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) - { - TSharedRef Params = MakeShared(); - for (const auto& KV : Request.QueryParams) - { - Params->SetStringField(KV.Key, KV.Value); - } - TSharedRef ListResult = MakeShared(); - HandleListMaterialFunctions(&*Params, &*ListResult); - TUniquePtr R = FHttpServerResponse::Create( - MCPUtils::JsonToString(ListResult), TEXT("application/json")); - OnComplete(MoveTemp(R)); - return true; - })); - Router->BindRoute(FHttpPath(TEXT("/api/material_function")), EHttpServerRequestVerbs::VERB_GET, - QueuedHandler(TEXT("dump_material_function"))); - Router->BindRoute(FHttpPath(TEXT("/api/create_material_function_asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("create_material_function_asset"))); - - // Material snapshot/diff/restore (Phase 5) - Router->BindRoute(FHttpPath(TEXT("/api/snapshot_material_expression_graph")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("snapshot_material_expression_graph"))); - Router->BindRoute(FHttpPath(TEXT("/api/diff_material_graph_vs_snapshot")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("diff_material_graph_vs_snapshot"))); - Router->BindRoute(FHttpPath(TEXT("/api/restore_material_graph_from_snapshot")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("restore_material_graph_from_snapshot"))); - Router->BindRoute(FHttpPath(TEXT("/api/compile_material")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("compile_material"))); - - // Animation Blueprint tools - Router->BindRoute(FHttpPath(TEXT("/api/create_anim_blueprint_asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("create_anim_blueprint_asset"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_anim_state_to_machine")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_anim_state_to_machine"))); - Router->BindRoute(FHttpPath(TEXT("/api/remove_anim_state_from_machine")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("remove_anim_state_from_machine"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_anim_state_transition")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_anim_state_transition"))); - Router->BindRoute(FHttpPath(TEXT("/api/set_anim_transition_rule")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("set_anim_transition_rule"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_anim_graph_node")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_anim_graph_node"))); - Router->BindRoute(FHttpPath(TEXT("/api/add_anim_state_machine")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("add_anim_state_machine"))); - Router->BindRoute(FHttpPath(TEXT("/api/set_anim_state_animation")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("set_anim_state_animation"))); - Router->BindRoute(FHttpPath(TEXT("/api/list_anim_slot_names")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("list_anim_slot_names"))); - Router->BindRoute(FHttpPath(TEXT("/api/list_anim_sync_groups")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("list_anim_sync_groups"))); - Router->BindRoute(FHttpPath(TEXT("/api/create_blend_space_asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("create_blend_space_asset"))); - Router->BindRoute(FHttpPath(TEXT("/api/set_blend_space_sample_points")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("set_blend_space_sample_points"))); - Router->BindRoute(FHttpPath(TEXT("/api/set_anim_state_blend_space")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("set_anim_state_blend_space"))); - - // Build new-style handler registry from UMCPHandler subclasses and bind routes for each - BuildMCPHandlerRegistry(); - for (const auto& KV : MCPHandlerRegistry) - { - FString Path = FString::Printf(TEXT("/api/%s"), *KV.Key); - Router->BindRoute(FHttpPath(Path), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(KV.Key)); - } - - // Register old-style TMap dispatch handlers - RegisterHandlers(); - - HttpModule.StartAllListeners(); - - // Verify the listener actually bound by attempting a TCP connection - bool bListenerReady = false; - for (int32 Attempt = 0; Attempt < 5; ++Attempt) - { - FSocket* TestSocket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("BlueprintMCP bind test"), false); - if (TestSocket) - { - TSharedRef Addr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); - bool bIsValid = false; - Addr->SetIp(TEXT("127.0.0.1"), bIsValid); - Addr->SetPort(Port); - bool bConnected = TestSocket->Connect(*Addr); - TestSocket->Close(); - ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->DestroySocket(TestSocket); - - if (bConnected) - { - bListenerReady = true; - break; - } - } - UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Bind check attempt %d/5 failed on port %d, retrying..."), Attempt + 1, Port); - FPlatformProcess::Sleep(1.0f); - } - - if (!bListenerReady) - { - UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: Failed to bind HTTP listener on port %d. Port may be in use."), Port); - HttpModule.StopAllListeners(); - return false; - } - - bRunning = true; - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Server listening on http://localhost:%d"), Port); - return true; + TSharedRef Response = MakeShared(); + Response->SetStringField(TEXT("jsonrpc"), TEXT("2.0")); + Response->SetNumberField(TEXT("id"), Id); + Response->SetObjectField(TEXT("result"), Result.IsValid() ? Result.ToSharedRef() : MakeShared()); + return MCPUtils::JsonToString(Response); } -void FBlueprintMCPServer::Stop() +FString FMCPServer::MakeJsonRpcToolResult(int32 Id, TSharedPtr ToolResult, bool bIsError) { - if (!bRunning) return; + TSharedRef ContentItem = MakeShared(); + ContentItem->SetStringField(TEXT("type"), TEXT("text")); + ContentItem->SetStringField(TEXT("text"), ToolResult.IsValid() ? MCPUtils::JsonToString(ToolResult.ToSharedRef()) : TEXT("{}")); - FHttpServerModule& HttpModule = FModuleManager::LoadModuleChecked("HTTPServer"); - HttpModule.StopAllListeners(); - bRunning = false; - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Server stopped.")); -} - -bool FBlueprintMCPServer::ProcessOneRequest() -{ - TSharedPtr Req; - if (!RequestQueue.Dequeue(Req)) - { - return false; - } - - // Build Params JSON: either from POST body or from GET query params - TSharedPtr Params; - if (!Req->Body.IsEmpty()) - { - Params = MCPUtils::ParseBodyJson(Req->Body); - } - if (!Params.IsValid()) - { - Params = MakeShared(); - for (const auto& KV : Req->QueryParams) - { - Params->SetStringField(KV.Key, KV.Value); - } - } + TArray> ContentArray; + ContentArray.Add(MakeShared(ContentItem)); TSharedRef Result = MakeShared(); - - if (UClass** HandlerClass = MCPHandlerRegistry.Find(Req->Endpoint)) + Result->SetArrayField(TEXT("content"), ContentArray); + if (bIsError) { - const bool bIsMutation = MutationEndpoints.Contains(Req->Endpoint); + Result->SetBoolField(TEXT("isError"), true); + } + + return MakeJsonRpcResult(Id, Result); +} + +FString FMCPServer::MakeJsonRpcError(int32 Id, int32 Code, const FString& Message) +{ + TSharedRef ErrObj = MakeShared(); + ErrObj->SetNumberField(TEXT("code"), Code); + ErrObj->SetStringField(TEXT("message"), Message); + + TSharedRef Response = MakeShared(); + Response->SetStringField(TEXT("jsonrpc"), TEXT("2.0")); + Response->SetNumberField(TEXT("id"), Id); + Response->SetObjectField(TEXT("error"), ErrObj); + return MCPUtils::JsonToString(Response); +} + +// ============================================================ +// The Cached Tools List +// ============================================================ + + +TSharedRef FMCPServer::FPropertyToPropSchema(FProperty* Prop) +{ + TSharedRef Schema = MakeShared(); + + if (CastField(Prop)) + { + Schema->SetStringField(TEXT("type"), TEXT("string")); + } + else if (CastField(Prop)) + { + Schema->SetStringField(TEXT("type"), TEXT("integer")); + } + else if (CastField(Prop) || CastField(Prop)) + { + Schema->SetStringField(TEXT("type"), TEXT("number")); + } + else if (CastField(Prop)) + { + Schema->SetStringField(TEXT("type"), TEXT("boolean")); + } + else if (FStructProperty* StructProp = CastField(Prop)) + { + if (StructProp->Struct == FMCPJsonArray::StaticStruct()) + { + Schema->SetStringField(TEXT("type"), TEXT("array")); + } + else + { + Schema->SetStringField(TEXT("type"), TEXT("object")); + } + } + else if (FEnumProperty* EnumProp = CastField(Prop)) + { + Schema->SetStringField(TEXT("type"), TEXT("string")); + UEnum* Enum = EnumProp->GetEnum(); + TArray> EnumValues; + for (int32 i = 0; i < Enum->NumEnums() - 1; ++i) + { + EnumValues.Add(MakeShared(Enum->GetNameStringByIndex(i))); + } + Schema->SetArrayField(TEXT("enum"), EnumValues); + } + else if (FByteProperty* ByteProp = CastField(Prop)) + { + if (ByteProp->Enum) + { + Schema->SetStringField(TEXT("type"), TEXT("string")); + TArray> EnumValues; + for (int32 i = 0; i < ByteProp->Enum->NumEnums() - 1; ++i) + { + EnumValues.Add(MakeShared(ByteProp->Enum->GetNameStringByIndex(i))); + } + Schema->SetArrayField(TEXT("enum"), EnumValues); + } + else + { + Schema->SetStringField(TEXT("type"), TEXT("integer")); + } + } + else + { + Schema->SetStringField(TEXT("type"), TEXT("string")); + } + + return Schema; +} + +TSharedRef FMCPServer::HandlerClassToToolSchema(UClass* HandlerClass) +{ + TSharedRef Tool = MakeShared(); + Tool->SetStringField(TEXT("name"), HandlerClass->GetMetaData(TEXT("ToolName"))); + + // Get description from the handler + UObject* TempObj = NewObject(GetTransientPackage(), HandlerClass); + IMCPHandler* Handler = Cast(TempObj); + if (Handler) + { + Tool->SetStringField(TEXT("description"), Handler->GetDescription()); + } + TempObj->MarkAsGarbage(); + + // Build input schema from UPROPERTY fields + TSharedRef InputSchema = MakeShared(); + InputSchema->SetStringField(TEXT("type"), TEXT("object")); + + TSharedRef Properties = MakeShared(); + TArray> RequiredArray; + + for (TFieldIterator It(HandlerClass, EFieldIterationFlags::None); It; ++It) + { + FProperty* Prop = *It; + FString JsonKey = MCPUtils::PropertyNameToJsonKey(Prop->GetName()); + + Properties->SetObjectField(JsonKey, FPropertyToPropSchema(Prop)); + + if (!Prop->HasMetaData(TEXT("Optional"))) + { + RequiredArray.Add(MakeShared(JsonKey)); + } + } + + InputSchema->SetObjectField(TEXT("properties"), Properties); + if (RequiredArray.Num() > 0) + { + InputSchema->SetArrayField(TEXT("required"), RequiredArray); + } + Tool->SetObjectField(TEXT("inputSchema"), InputSchema); + + return Tool; +} + +void FMCPServer::BuildCachedToolsList() +{ + TArray> ToolsArray; + + // New-style handlers: have UPROPERTY metadata for parameter schemas + for (const auto& KV : MCPHandlerRegistry) + { + ToolsArray.Add(MakeShared(HandlerClassToToolSchema(KV.Value))); + } + + // Old-style handlers: minimal schema (just the tool name) + for (const auto& KV : HandlerMap) + { + // Skip if already registered as new-style + if (MCPHandlerRegistry.Contains(KV.Key)) continue; + + TSharedRef Tool = MakeShared(); + Tool->SetStringField(TEXT("name"), KV.Key); + Tool->SetStringField(TEXT("description"), FString::Printf(TEXT("Tool: %s"), *KV.Key)); + + TSharedRef InputSchema = MakeShared(); + InputSchema->SetStringField(TEXT("type"), TEXT("object")); + Tool->SetObjectField(TEXT("inputSchema"), InputSchema); + + ToolsArray.Add(MakeShared(Tool)); + } + + CachedToolsList = MakeShared(); + CachedToolsList->SetArrayField(TEXT("tools"), ToolsArray); +} + +// ============================================================ +// Handlers for MCP Methods +// ============================================================ + +FString FMCPServer::HandleInitialize(int32 Id, const FJsonObject* Params) +{ + TSharedRef Result = MakeShared(); + Result->SetStringField(TEXT("protocolVersion"), TEXT("2024-11-05")); + + TSharedRef Capabilities = MakeShared(); + TSharedRef ToolsCap = MakeShared(); + Capabilities->SetObjectField(TEXT("tools"), ToolsCap); + Result->SetObjectField(TEXT("capabilities"), Capabilities); + + TSharedRef ServerInfo = MakeShared(); + ServerInfo->SetStringField(TEXT("name"), TEXT("BlueprintMCP")); + ServerInfo->SetStringField(TEXT("version"), TEXT("1.0.0")); + Result->SetObjectField(TEXT("serverInfo"), ServerInfo); + + return MakeJsonRpcResult(Id, Result); +} + +FString FMCPServer::HandleToolsList(int32 Id) +{ + if (!CachedToolsList.IsValid()) + { + BuildCachedToolsList(); + } + return MakeJsonRpcResult(Id, CachedToolsList); +} +FString FMCPServer::HandleToolsCall(int32 Id, const FJsonObject* Params) +{ + FString ToolName; + if (!Params->TryGetStringField(TEXT("name"), ToolName)) + { + return MakeJsonRpcError(Id, -32602, TEXT("Missing 'name' in tools/call params")); + } + + const TSharedPtr* ArgsPtr = nullptr; + Params->TryGetObjectField(TEXT("arguments"), ArgsPtr); + TSharedPtr Args = ArgsPtr ? *ArgsPtr : MakeShared(); + + TSharedRef ToolResult = MakeShared(); + DispatchToolCall(ToolName, Args.Get(), &*ToolResult); + + bool bIsError = ToolResult->HasField(TEXT("error")); + return MakeJsonRpcToolResult(Id, ToolResult, bIsError); +} + +void FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Params, FJsonObject* Result) +{ + if (UClass** HandlerClass = MCPHandlerRegistry.Find(ToolName)) + { + const bool bIsMutation = MutationEndpoints.Contains(ToolName); if (bIsMutation && GEditor) { - GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *Req->Endpoint))); + GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *ToolName))); } TStrongObjectPtr HandlerObj(NewObject(GetTransientPackage(), *HandlerClass)); IMCPHandler* Handler = Cast(HandlerObj.Get()); - FString PopulateError = MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params.Get()); + FString PopulateError = MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params); if (PopulateError.IsEmpty()) { - Handler->Handle(Params.Get(), &*Result); + Handler->Handle(Params, Result); } else { - MCPUtils::MakeErrorJson(&*Result, PopulateError); + MCPUtils::MakeErrorJson(Result, PopulateError); } if (bIsMutation && GEditor) @@ -655,16 +516,15 @@ bool FBlueprintMCPServer::ProcessOneRequest() GEditor->EndTransaction(); } } - else if (FRequestHandler* Handler = HandlerMap.Find(Req->Endpoint)) + else if (FRequestHandler* Handler = HandlerMap.Find(ToolName)) { - // Wrap mutation endpoints in an undo transaction so users can Ctrl+Z - const bool bIsMutation = MutationEndpoints.Contains(Req->Endpoint); + const bool bIsMutation = MutationEndpoints.Contains(ToolName); if (bIsMutation && GEditor) { - GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *Req->Endpoint))); + GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *ToolName))); } - (*Handler)(Params.Get(), &*Result); + (*Handler)(Params, Result); if (bIsMutation && GEditor) { @@ -673,21 +533,302 @@ bool FBlueprintMCPServer::ProcessOneRequest() } else { - MCPUtils::MakeErrorJson(&*Result, FString::Printf(TEXT("Unknown endpoint: %s"), *Req->Endpoint)); + MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Unknown tool: %s"), *ToolName)); + } +} + +// ============================================================ +// HandleJsonRpc — parse a JSON-RPC line and dispatch +// ============================================================ + +FString FMCPServer::HandleJsonRpc(const FString& Line) +{ + TSharedPtr Request = MCPUtils::ParseBodyJson(Line); + if (!Request.IsValid()) + { + return MakeJsonRpcError(0, -32700, TEXT("Parse error")); } - // Send the response back via the HTTP callback (non-blocking) - FString Response = MCPUtils::JsonToString(Result); - TUniquePtr HttpResp = FHttpServerResponse::Create( - Response, TEXT("application/json")); - Req->OnComplete(MoveTemp(HttpResp)); + FString Method; + if (!Request->TryGetStringField(TEXT("method"), Method)) + { + return MakeJsonRpcError(0, -32600, TEXT("Missing 'method' field")); + } + + // Notifications (no id) — we just ignore them + int32 Id = 0; + bool bIsNotification = !Request->HasField(TEXT("id")); + if (!bIsNotification) + { + Id = (int32)Request->GetNumberField(TEXT("id")); + } + + const TSharedPtr* ParamsPtr = nullptr; + Request->TryGetObjectField(TEXT("params"), ParamsPtr); + const FJsonObject* Params = ParamsPtr ? ParamsPtr->Get() : nullptr; + + // Route MCP methods + if (Method == TEXT("initialize")) + { + return HandleInitialize(Id, Params); + } + if (Method == TEXT("notifications/initialized")) + { + return FString(); // notification, no response + } + if (Method == TEXT("tools/list")) + { + return HandleToolsList(Id); + } + if (Method == TEXT("tools/call")) + { + if (!Params) + { + return MakeJsonRpcError(Id, -32602, TEXT("Missing params for tools/call")); + } + return HandleToolsCall(Id, Params); + } + + // Unknown method + return MakeJsonRpcError(Id, -32601, FString::Printf(TEXT("Method not found: %s"), *Method)); +} + +// ============================================================ +// TCP Server: Start / Stop / ProcessOneRequest +// ============================================================ + +bool FMCPServer::Start(int32 InPort, bool bEditorMode) +{ + Port = InPort; + bIsEditor = bEditorMode; + + // Register handlers + BuildMCPHandlerRegistry(); + RegisterHandlers(); + + // Create TCP listen socket + ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM); + ListenSocket = SocketSub->CreateSocket(NAME_Stream, TEXT("MCPServer"), false); + if (!ListenSocket) + { + UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: Failed to create listen socket")); + return false; + } + + ListenSocket->SetReuseAddr(true); + ListenSocket->SetNonBlocking(true); + + TSharedRef Addr = SocketSub->CreateInternetAddr(); + bool bIsValid = false; + Addr->SetIp(TEXT("127.0.0.1"), bIsValid); + Addr->SetPort(Port); + + if (!ListenSocket->Bind(*Addr)) + { + UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: Failed to bind to port %d"), Port); + SocketSub->DestroySocket(ListenSocket); + ListenSocket = nullptr; + return false; + } + + if (!ListenSocket->Listen(4)) + { + UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: Failed to listen on port %d"), Port); + SocketSub->DestroySocket(ListenSocket); + ListenSocket = nullptr; + return false; + } + + bRunning = true; + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MCP server listening on tcp://localhost:%d"), Port); + return true; +} + +void FMCPServer::Stop() +{ + if (!bRunning) return; + + ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM); + + // Set shutdown flag and drain pending messages under lock + { + FScopeLock Lock(&Mutex); + bShuttingDown = true; + for (auto& Msg : PendingMessages) + { + Msg->Response.SetValue(FString()); + } + PendingMessages.Empty(); + } + + // Close all client sockets (unblocks their blocking reads) + for (auto& Client : Clients) + { + if (Client->Socket) + { + Client->Socket->Close(); + } + } + + // Wait for client threads to exit + for (auto& Client : Clients) + { + Client->ThreadFuture.Wait(); + if (Client->Socket) + { + SocketSub->DestroySocket(Client->Socket); + } + } + Clients.Empty(); + + // Close listen socket + if (ListenSocket) + { + ListenSocket->Close(); + SocketSub->DestroySocket(ListenSocket); + ListenSocket = nullptr; + } + + bRunning = false; + bShuttingDown = false; + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Server stopped.")); +} + +void FMCPServer::AcceptNewConnections() +{ + if (!ListenSocket) return; + + bool bHasPending = false; + if (!ListenSocket->HasPendingConnection(bHasPending) || !bHasPending) return; + + FSocket* ClientSocket = ListenSocket->Accept(TEXT("MCPClient")); + if (!ClientSocket) return; + + ClientSocket->SetNonBlocking(false); // client threads use blocking I/O + + TSharedPtr Client = MakeShared(); + Client->Socket = ClientSocket; + Client->ThreadFuture = Async(EAsyncExecution::Thread, [this, Client]() { ClientThreadFunc(this, Client); }); + Clients.Add(Client); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Client connected.")); +} + +void FMCPServer::CleanupFinishedClients() +{ + ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM); + + for (int32 i = Clients.Num() - 1; i >= 0; --i) + { + if (!Clients[i]->bDone) continue; + + Clients[i]->ThreadFuture.Wait(); + if (Clients[i]->Socket) + { + SocketSub->DestroySocket(Clients[i]->Socket); + } + Clients.RemoveAt(i); + } +} + +void FMCPServer::ClientThreadFunc(FMCPServer* Server, TSharedPtr Client) +{ + FSocket* Socket = Client->Socket; + FString LineBuffer; + uint8 RecvBuf[4096]; + + while (true) + { + int32 BytesRead = 0; + if (!Socket->Recv(RecvBuf, sizeof(RecvBuf) - 1, BytesRead)) + { + break; // socket error or closed + } + if (BytesRead <= 0) + { + break; // connection closed + } + + RecvBuf[BytesRead] = 0; + LineBuffer += UTF8_TO_TCHAR((const ANSICHAR*)RecvBuf); + + // Process complete lines + int32 NewlineIdx; + while (LineBuffer.FindChar(TEXT('\n'), NewlineIdx)) + { + FString Line = LineBuffer.Left(NewlineIdx).TrimEnd(); + LineBuffer.RightChopInline(NewlineIdx + 1); + + if (Line.IsEmpty()) continue; + + // Enqueue the line for game-thread processing + TSharedPtr Msg = MakeShared(); + Msg->Line = Line; + TFuture Future = Msg->Response.GetFuture(); + + { + FScopeLock Lock(&Server->Mutex); + if (Server->bShuttingDown) + { + Client->bDone = true; + return; + } + Server->PendingMessages.Add(Msg); + } + + // Block until the game thread processes this message + FString Response = Future.Get(); + + // Empty response means either notification or shutdown + if (Response.IsEmpty()) continue; + + // Write the response line back (blocking) + FString ResponseLine = Response + TEXT("\n"); + FTCHARToUTF8 Utf8(*ResponseLine); + int32 BytesSent = 0; + Socket->Send((const uint8*)Utf8.Get(), Utf8.Length(), BytesSent); + } + } + + Client->bDone = true; + 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 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 = HandleJsonRpc(Msg->Line); + Msg->Response.SetValue(Response); return true; } -void FBlueprintMCPServer::RegisterHandlers() +// ============================================================ +// RegisterHandlers / BuildMCPHandlerRegistry +// ============================================================ + +void FMCPServer::RegisterHandlers() { - // Mutation endpoints — wrapped in undo transactions by ProcessOneRequest() + // Mutation endpoints — wrapped in undo transactions by DispatchToolCall() MutationEndpoints = { TEXT("replace_function_calls_in_blueprint"), TEXT("change_blueprint_variable_type"), @@ -749,89 +890,92 @@ void FBlueprintMCPServer::RegisterHandlers() }; // All handlers have uniform signature: void(const FJsonObject&, FJsonObject&) - auto H = [this](const TCHAR* Name, void(FBlueprintMCPServer::*Fn)(const FJsonObject*, FJsonObject*)) + auto H = [this](const TCHAR* Name, void(FMCPServer::*Fn)(const FJsonObject*, FJsonObject*)) { HandlerMap.Add(Name, [this, Fn](const FJsonObject* Json, FJsonObject* Result) { (this->*Fn)(Json, Result); }); }; - H(TEXT("dump_blueprint"), &FBlueprintMCPServer::HandleGetBlueprint); - H(TEXT("dump_blueprint_graph"), &FBlueprintMCPServer::HandleGetGraph); - H(TEXT("search_within_blueprints"), &FBlueprintMCPServer::HandleSearch); - H(TEXT("find_asset_references"), &FBlueprintMCPServer::HandleFindReferences); - H(TEXT("test_save_blueprint_package"), &FBlueprintMCPServer::HandleTestSave); - H(TEXT("search_type_usage_in_blueprints"), &FBlueprintMCPServer::HandleSearchByType); - H(TEXT("change_blueprint_variable_type"), &FBlueprintMCPServer::HandleChangeVariableType); - H(TEXT("change_function_parameter_type"), &FBlueprintMCPServer::HandleChangeFunctionParamType); - H(TEXT("remove_function_parameter"), &FBlueprintMCPServer::HandleRemoveFunctionParameter); - H(TEXT("get_pin_details"), &FBlueprintMCPServer::HandleGetPinInfo); - H(TEXT("check_pin_connection_compatibility"), &FBlueprintMCPServer::HandleCheckPinCompatibility); - H(TEXT("search_unreal_classes"), &FBlueprintMCPServer::HandleListClasses); - H(TEXT("list_class_functions"), &FBlueprintMCPServer::HandleListFunctions); - H(TEXT("list_class_properties"), &FBlueprintMCPServer::HandleListProperties); - H(TEXT("compile_blueprint"), &FBlueprintMCPServer::HandleValidateBlueprint); - H(TEXT("compile_all_blueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints); - H(TEXT("reparent_blueprint"), &FBlueprintMCPServer::HandleReparentBlueprint); - H(TEXT("create_blueprint_asset"), &FBlueprintMCPServer::HandleCreateBlueprint); - H(TEXT("create_blueprint_graph"), &FBlueprintMCPServer::HandleCreateGraph); - H(TEXT("delete_blueprint_graph"), &FBlueprintMCPServer::HandleDeleteGraph); - H(TEXT("rename_blueprint_graph"), &FBlueprintMCPServer::HandleRenameGraph); - H(TEXT("add_blueprint_variable"), &FBlueprintMCPServer::HandleAddVariable); - H(TEXT("remove_blueprint_variable"), &FBlueprintMCPServer::HandleRemoveVariable); - H(TEXT("set_blueprint_variable_metadata"), &FBlueprintMCPServer::HandleSetVariableMetadata); - H(TEXT("add_event_dispatcher"), &FBlueprintMCPServer::HandleAddEventDispatcher); - H(TEXT("list_event_dispatchers"), &FBlueprintMCPServer::HandleListEventDispatchers); - H(TEXT("add_function_parameter"), &FBlueprintMCPServer::HandleAddFunctionParameter); - H(TEXT("add_blueprint_component"), &FBlueprintMCPServer::HandleAddComponent); - H(TEXT("remove_blueprint_component"), &FBlueprintMCPServer::HandleRemoveComponent); - H(TEXT("list_blueprint_components"), &FBlueprintMCPServer::HandleListComponents); - H(TEXT("snapshot_blueprint_graph"), &FBlueprintMCPServer::HandleSnapshotGraph); - H(TEXT("diff_blueprint_graph_vs_snapshot"), &FBlueprintMCPServer::HandleDiffGraph); - H(TEXT("restore_blueprint_graph_from_snapshot"), &FBlueprintMCPServer::HandleRestoreGraph); - H(TEXT("find_pins_disconnected_since_snapshot"), &FBlueprintMCPServer::HandleFindDisconnectedPins); - H(TEXT("analyze_cpp_rebuild_impact"), &FBlueprintMCPServer::HandleAnalyzeRebuildImpact); - H(TEXT("create_struct_asset"), &FBlueprintMCPServer::HandleCreateStruct); - H(TEXT("create_enum_asset"), &FBlueprintMCPServer::HandleCreateEnum); - H(TEXT("add_struct_field"), &FBlueprintMCPServer::HandleAddStructProperty); - H(TEXT("remove_struct_field"), &FBlueprintMCPServer::HandleRemoveStructProperty); - H(TEXT("dump_material"), &FBlueprintMCPServer::HandleGetMaterial); - H(TEXT("dump_material_expression_graph"), &FBlueprintMCPServer::HandleGetMaterialGraph); - H(TEXT("search_within_materials"), &FBlueprintMCPServer::HandleSearchMaterials); - H(TEXT("dump_material_instance_parameters"),&FBlueprintMCPServer::HandleGetMaterialInstanceParameters); - H(TEXT("dump_material_function"), &FBlueprintMCPServer::HandleGetMaterialFunction); - H(TEXT("describe_material_in_english"), &FBlueprintMCPServer::HandleDescribeMaterial); - H(TEXT("find_material_references"), &FBlueprintMCPServer::HandleFindMaterialReferences); - H(TEXT("create_material_asset"), &FBlueprintMCPServer::HandleCreateMaterial); - H(TEXT("set_material_property"), &FBlueprintMCPServer::HandleSetMaterialProperty); - H(TEXT("add_material_expression"), &FBlueprintMCPServer::HandleAddMaterialExpression); - H(TEXT("delete_material_expression"), &FBlueprintMCPServer::HandleDeleteMaterialExpression); - H(TEXT("connect_material_expression_pins"), &FBlueprintMCPServer::HandleConnectMaterialPins); - H(TEXT("disconnect_material_expression_pin"), &FBlueprintMCPServer::HandleDisconnectMaterialPin); - H(TEXT("set_material_expression_property"), &FBlueprintMCPServer::HandleSetExpressionValue); - H(TEXT("set_material_expression_position"), &FBlueprintMCPServer::HandleMoveMaterialExpression); - H(TEXT("create_material_instance_asset"), &FBlueprintMCPServer::HandleCreateMaterialInstance); - H(TEXT("set_material_instance_parameter"), &FBlueprintMCPServer::HandleSetMaterialInstanceParameter); - H(TEXT("reparent_material_instance"), &FBlueprintMCPServer::HandleReparentMaterialInstance); - H(TEXT("create_material_function_asset"), &FBlueprintMCPServer::HandleCreateMaterialFunction); - H(TEXT("snapshot_material_expression_graph"), &FBlueprintMCPServer::HandleSnapshotMaterialGraph); - H(TEXT("diff_material_graph_vs_snapshot"), &FBlueprintMCPServer::HandleDiffMaterialGraph); - H(TEXT("restore_material_graph_from_snapshot"), &FBlueprintMCPServer::HandleRestoreMaterialGraph); - H(TEXT("compile_material"), &FBlueprintMCPServer::HandleValidateMaterial); - H(TEXT("create_anim_blueprint_asset"), &FBlueprintMCPServer::HandleCreateAnimBlueprint); - H(TEXT("add_anim_state_to_machine"), &FBlueprintMCPServer::HandleAddAnimState); - H(TEXT("remove_anim_state_from_machine"), &FBlueprintMCPServer::HandleRemoveAnimState); - H(TEXT("add_anim_state_transition"), &FBlueprintMCPServer::HandleAddAnimTransition); - H(TEXT("set_anim_transition_rule"), &FBlueprintMCPServer::HandleSetTransitionRule); - H(TEXT("add_anim_graph_node"), &FBlueprintMCPServer::HandleAddAnimNode); - H(TEXT("add_anim_state_machine"), &FBlueprintMCPServer::HandleAddStateMachine); - H(TEXT("set_anim_state_animation"), &FBlueprintMCPServer::HandleSetStateAnimation); - H(TEXT("list_anim_slot_names"), &FBlueprintMCPServer::HandleListAnimSlots); - H(TEXT("list_anim_sync_groups"), &FBlueprintMCPServer::HandleListSyncGroups); - H(TEXT("create_blend_space_asset"), &FBlueprintMCPServer::HandleCreateBlendSpace); - H(TEXT("set_blend_space_sample_points"), &FBlueprintMCPServer::HandleSetBlendSpaceSamples); - H(TEXT("set_anim_state_blend_space"), &FBlueprintMCPServer::HandleSetStateBlendSpace); + H(TEXT("list_blueprint_assets"), &FMCPServer::HandleList); + H(TEXT("dump_blueprint"), &FMCPServer::HandleGetBlueprint); + H(TEXT("dump_blueprint_graph"), &FMCPServer::HandleGetGraph); + H(TEXT("search_within_blueprints"), &FMCPServer::HandleSearch); + H(TEXT("find_asset_references"), &FMCPServer::HandleFindReferences); + H(TEXT("test_save_blueprint_package"), &FMCPServer::HandleTestSave); + H(TEXT("search_type_usage_in_blueprints"), &FMCPServer::HandleSearchByType); + H(TEXT("change_blueprint_variable_type"), &FMCPServer::HandleChangeVariableType); + H(TEXT("change_function_parameter_type"), &FMCPServer::HandleChangeFunctionParamType); + H(TEXT("remove_function_parameter"), &FMCPServer::HandleRemoveFunctionParameter); + H(TEXT("get_pin_details"), &FMCPServer::HandleGetPinInfo); + H(TEXT("check_pin_connection_compatibility"), &FMCPServer::HandleCheckPinCompatibility); + H(TEXT("search_unreal_classes"), &FMCPServer::HandleListClasses); + H(TEXT("list_class_functions"), &FMCPServer::HandleListFunctions); + H(TEXT("list_class_properties"), &FMCPServer::HandleListProperties); + H(TEXT("compile_blueprint"), &FMCPServer::HandleValidateBlueprint); + H(TEXT("compile_all_blueprints"), &FMCPServer::HandleValidateAllBlueprints); + H(TEXT("reparent_blueprint"), &FMCPServer::HandleReparentBlueprint); + H(TEXT("create_blueprint_asset"), &FMCPServer::HandleCreateBlueprint); + H(TEXT("create_blueprint_graph"), &FMCPServer::HandleCreateGraph); + H(TEXT("delete_blueprint_graph"), &FMCPServer::HandleDeleteGraph); + H(TEXT("rename_blueprint_graph"), &FMCPServer::HandleRenameGraph); + H(TEXT("add_blueprint_variable"), &FMCPServer::HandleAddVariable); + H(TEXT("remove_blueprint_variable"), &FMCPServer::HandleRemoveVariable); + H(TEXT("set_blueprint_variable_metadata"), &FMCPServer::HandleSetVariableMetadata); + H(TEXT("add_event_dispatcher"), &FMCPServer::HandleAddEventDispatcher); + H(TEXT("list_event_dispatchers"), &FMCPServer::HandleListEventDispatchers); + H(TEXT("add_function_parameter"), &FMCPServer::HandleAddFunctionParameter); + H(TEXT("add_blueprint_component"), &FMCPServer::HandleAddComponent); + H(TEXT("remove_blueprint_component"), &FMCPServer::HandleRemoveComponent); + H(TEXT("list_blueprint_components"), &FMCPServer::HandleListComponents); + H(TEXT("snapshot_blueprint_graph"), &FMCPServer::HandleSnapshotGraph); + H(TEXT("diff_blueprint_graph_vs_snapshot"), &FMCPServer::HandleDiffGraph); + H(TEXT("restore_blueprint_graph_from_snapshot"), &FMCPServer::HandleRestoreGraph); + H(TEXT("find_pins_disconnected_since_snapshot"), &FMCPServer::HandleFindDisconnectedPins); + H(TEXT("analyze_cpp_rebuild_impact"), &FMCPServer::HandleAnalyzeRebuildImpact); + H(TEXT("create_struct_asset"), &FMCPServer::HandleCreateStruct); + H(TEXT("create_enum_asset"), &FMCPServer::HandleCreateEnum); + H(TEXT("add_struct_field"), &FMCPServer::HandleAddStructProperty); + H(TEXT("remove_struct_field"), &FMCPServer::HandleRemoveStructProperty); + H(TEXT("list_material_assets"), &FMCPServer::HandleListMaterials); + H(TEXT("dump_material"), &FMCPServer::HandleGetMaterial); + H(TEXT("dump_material_expression_graph"), &FMCPServer::HandleGetMaterialGraph); + H(TEXT("search_within_materials"), &FMCPServer::HandleSearchMaterials); + H(TEXT("dump_material_instance_parameters"),&FMCPServer::HandleGetMaterialInstanceParameters); + H(TEXT("list_material_function_assets"), &FMCPServer::HandleListMaterialFunctions); + H(TEXT("dump_material_function"), &FMCPServer::HandleGetMaterialFunction); + H(TEXT("describe_material_in_english"), &FMCPServer::HandleDescribeMaterial); + H(TEXT("find_material_references"), &FMCPServer::HandleFindMaterialReferences); + H(TEXT("create_material_asset"), &FMCPServer::HandleCreateMaterial); + H(TEXT("set_material_property"), &FMCPServer::HandleSetMaterialProperty); + H(TEXT("add_material_expression"), &FMCPServer::HandleAddMaterialExpression); + H(TEXT("delete_material_expression"), &FMCPServer::HandleDeleteMaterialExpression); + H(TEXT("connect_material_expression_pins"), &FMCPServer::HandleConnectMaterialPins); + H(TEXT("disconnect_material_expression_pin"), &FMCPServer::HandleDisconnectMaterialPin); + H(TEXT("set_material_expression_property"), &FMCPServer::HandleSetExpressionValue); + H(TEXT("set_material_expression_position"), &FMCPServer::HandleMoveMaterialExpression); + H(TEXT("create_material_instance_asset"), &FMCPServer::HandleCreateMaterialInstance); + H(TEXT("set_material_instance_parameter"), &FMCPServer::HandleSetMaterialInstanceParameter); + H(TEXT("reparent_material_instance"), &FMCPServer::HandleReparentMaterialInstance); + H(TEXT("create_material_function_asset"), &FMCPServer::HandleCreateMaterialFunction); + H(TEXT("snapshot_material_expression_graph"), &FMCPServer::HandleSnapshotMaterialGraph); + H(TEXT("diff_material_graph_vs_snapshot"), &FMCPServer::HandleDiffMaterialGraph); + H(TEXT("restore_material_graph_from_snapshot"), &FMCPServer::HandleRestoreMaterialGraph); + H(TEXT("compile_material"), &FMCPServer::HandleValidateMaterial); + H(TEXT("create_anim_blueprint_asset"), &FMCPServer::HandleCreateAnimBlueprint); + H(TEXT("add_anim_state_to_machine"), &FMCPServer::HandleAddAnimState); + H(TEXT("remove_anim_state_from_machine"), &FMCPServer::HandleRemoveAnimState); + H(TEXT("add_anim_state_transition"), &FMCPServer::HandleAddAnimTransition); + H(TEXT("set_anim_transition_rule"), &FMCPServer::HandleSetTransitionRule); + H(TEXT("add_anim_graph_node"), &FMCPServer::HandleAddAnimNode); + H(TEXT("add_anim_state_machine"), &FMCPServer::HandleAddStateMachine); + H(TEXT("set_anim_state_animation"), &FMCPServer::HandleSetStateAnimation); + H(TEXT("list_anim_slot_names"), &FMCPServer::HandleListAnimSlots); + H(TEXT("list_anim_sync_groups"), &FMCPServer::HandleListSyncGroups); + H(TEXT("create_blend_space_asset"), &FMCPServer::HandleCreateBlendSpace); + H(TEXT("set_blend_space_sample_points"), &FMCPServer::HandleSetBlendSpaceSamples); + H(TEXT("set_anim_state_blend_space"), &FMCPServer::HandleSetStateBlendSpace); } -void FBlueprintMCPServer::BuildMCPHandlerRegistry() +void FMCPServer::BuildMCPHandlerRegistry() { for (TObjectIterator It; It; ++It) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index e30679e3..bc1d48e3 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -85,7 +85,7 @@ extern int32 TrySavePackageSEH( FString MCPUtils::JsonToString(TSharedRef JsonObj) { FString Output; - TSharedRef> Writer = TJsonWriterFactory<>::Create(&Output); + TSharedRef>> Writer = TJsonWriterFactory>::Create(&Output); FJsonSerializer::Serialize(JsonObj, Writer); return Output; } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h index a293d7e8..2a9d7939 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPServer.h @@ -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; TMap HandlerMap; // old-style handlers - TMap MCPHandlerRegistry; // new-style: tool name → UMCPHandler subclass + TMap MCPHandlerRegistry; // new-style: tool name -> UMCPHandler subclass TSet MutationEndpoints; void RegisterHandlers(); void BuildMCPHandlerRegistry(); - // ----- Queued request model ----- - struct FPendingRequest - { - FString Endpoint; - TMap QueryParams; - FString Body; - FHttpResultCallback OnComplete; - }; - TQueue, 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 CachedToolsList; + static TSharedRef FPropertyToPropSchema(FProperty* Prop); + static TSharedRef HandlerClassToToolSchema(UClass* HandlerClass); + void BuildCachedToolsList(); + + // JSON-RPC helpers + static FString MakeJsonRpcResult(int32 Id, TSharedPtr Result); + static FString MakeJsonRpcToolResult(int32 Id, TSharedPtr 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 ThreadFuture; + bool bDone = false; + }; + TArray> Clients; + void AcceptNewConnections(); + void CleanupFinishedClients(); + static void ClientThreadFunc(FMCPServer* Server, TSharedPtr Client); + + // ----- Thread-safe message queue ----- + struct FPendingMessage + { + FString Line; + TPromise Response; + FPendingMessage() : Response(TPromise()) {} + }; + FCriticalSection Mutex; + TArray> 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 Snapshots; TMap 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; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index 8483c65f..15ab1d63 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -92,10 +92,10 @@ public: static TArray 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& 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); }; diff --git a/tools/mcp-bridge.sh b/tools/mcp-bridge.sh new file mode 100644 index 00000000..80cdccc6 --- /dev/null +++ b/tools/mcp-bridge.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec /usr/bin/nc localhost 9847