Implemented new framework for MCP Handlers. Made one handler with the new framework.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
#include "BlueprintMCPHandlers_Mutation.h"
|
||||
#include "BlueprintMCPServer.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Materials/Material.h"
|
||||
@@ -2594,42 +2595,29 @@ void FBlueprintMCPServer::HandleSearchNodeActions(const FJsonObject* Json, FJson
|
||||
// Takes a full action name, finds the spawner, and calls Invoke().
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject* Result)
|
||||
void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graph"));
|
||||
FString ActionName = Json->GetStringField(TEXT("actionName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || ActionName.IsEmpty())
|
||||
{
|
||||
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, actionName"));
|
||||
}
|
||||
|
||||
int32 PosX = 0, PosY = 0;
|
||||
if (Json->HasField(TEXT("posX")))
|
||||
PosX = (int32)Json->GetNumberField(TEXT("posX"));
|
||||
if (Json->HasField(TEXT("posY")))
|
||||
PosY = (int32)Json->GetNumberField(TEXT("posY"));
|
||||
MCPHelper* Helper = MCPHelper::Get();
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
|
||||
UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MakeErrorJson(Result, LoadError);
|
||||
return Helper->MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find the target graph
|
||||
FString DecodedGraphName = UrlDecode(GraphName);
|
||||
FString DecodedGraphName = MCPHelper::UrlDecode(Graph);
|
||||
UEdGraph* TargetGraph = nullptr;
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
for (UEdGraph* G : AllGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
||||
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = Graph;
|
||||
TargetGraph = G;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2637,11 +2625,11 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
if (!TargetGraph)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> GraphNames;
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
for (UEdGraph* G : AllGraphs)
|
||||
{
|
||||
if (Graph) GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
if (G) GraphNames.Add(MakeShared<FJsonValueString>(G->GetName()));
|
||||
}
|
||||
MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||
Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
|
||||
return;
|
||||
}
|
||||
@@ -2650,13 +2638,13 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
TArray<UBlueprintNodeSpawner*> Matches = FNodeActionSearch::FindSpawner(ActionName);
|
||||
if (Matches.Num() == 0)
|
||||
{
|
||||
return MakeErrorJson(Result, FString::Printf(
|
||||
return Helper->MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("No action found matching '%s'. Use search_node_actions to find available actions."),
|
||||
*ActionName));
|
||||
}
|
||||
if (Matches.Num() > 1)
|
||||
{
|
||||
return MakeErrorJson(Result, FString::Printf(
|
||||
return Helper->MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Ambiguous: %d spawners match '%s'. Cannot determine which one to use."),
|
||||
Matches.Num(), *ActionName));
|
||||
}
|
||||
@@ -2669,7 +2657,7 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
|
||||
if (!NewNode)
|
||||
{
|
||||
return MakeErrorJson(Result, TEXT("Spawner Invoke() returned null — node creation failed."));
|
||||
return Helper->MakeErrorJson(Result, TEXT("Spawner Invoke() returned null — node creation failed."));
|
||||
}
|
||||
|
||||
// Ensure valid GUID
|
||||
@@ -2678,22 +2666,20 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
NewNode->CreateNewGuid();
|
||||
}
|
||||
|
||||
// Mark as modified and save
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Spawned node '%s' (class %s) via action '%s' in graph '%s' of '%s'"),
|
||||
*NewNode->NodeGuid.ToString(),
|
||||
*NewNode->GetClass()->GetName(),
|
||||
*ActionName,
|
||||
*DecodedGraphName,
|
||||
*BlueprintName);
|
||||
*Blueprint);
|
||||
|
||||
// Serialize result
|
||||
TSharedPtr<FJsonObject> NodeState = SerializeNode(NewNode);
|
||||
TSharedPtr<FJsonObject> NodeState = Helper->SerializeNode(NewNode);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("graph"), DecodedGraphName);
|
||||
Result->SetStringField(TEXT("actionName"), ActionName);
|
||||
Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
|
||||
@@ -2703,5 +2689,5 @@ void FBlueprintMCPServer::HandleSpawnNode(const FJsonObject* Json, FJsonObject*
|
||||
{
|
||||
Result->SetObjectField(TEXT("node"), NodeState);
|
||||
}
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "BlueprintMCPServer.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "Materials/MaterialExpression.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
@@ -96,6 +97,19 @@
|
||||
#include "AnimationGraph.h"
|
||||
#include "AnimationTransitionGraph.h"
|
||||
|
||||
// ============================================================
|
||||
// Get() — retrieve the active server via the editor subsystem
|
||||
// ============================================================
|
||||
|
||||
#include "BlueprintMCPEditorSubsystem.h"
|
||||
|
||||
FBlueprintMCPServer* FBlueprintMCPServer::Get()
|
||||
{
|
||||
if (!GEditor) return nullptr;
|
||||
auto* Sub = GEditor->GetEditorSubsystem<UBlueprintMCPEditorSubsystem>();
|
||||
return Sub ? Sub->GetServer() : nullptr;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helpers
|
||||
// ============================================================
|
||||
@@ -638,7 +652,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode)
|
||||
Router->BindRoute(FHttpPath(TEXT("/api/search-node-actions")), EHttpServerRequestVerbs::VERB_POST,
|
||||
QueuedHandler(TEXT("searchNodeActions")));
|
||||
Router->BindRoute(FHttpPath(TEXT("/api/spawn-node")), EHttpServerRequestVerbs::VERB_POST,
|
||||
QueuedHandler(TEXT("spawnNode")));
|
||||
QueuedHandler(TEXT("spawn_node")));
|
||||
Router->BindRoute(FHttpPath(TEXT("/api/rename-asset")), EHttpServerRequestVerbs::VERB_POST,
|
||||
QueuedHandler(TEXT("renameAsset")));
|
||||
Router->BindRoute(FHttpPath(TEXT("/api/reparent-blueprint")), EHttpServerRequestVerbs::VERB_POST,
|
||||
@@ -827,6 +841,9 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode)
|
||||
// Register TMap dispatch handlers
|
||||
RegisterHandlers();
|
||||
|
||||
// Build new-style handler registry from UMCPHandler subclasses
|
||||
BuildMCPHandlerRegistry();
|
||||
|
||||
HttpModule.StartAllListeners();
|
||||
|
||||
// Verify the listener actually bound by attempting a TCP connection
|
||||
@@ -901,7 +918,26 @@ bool FBlueprintMCPServer::ProcessOneRequest()
|
||||
|
||||
TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
|
||||
|
||||
if (FRequestHandler* Handler = HandlerMap.Find(Req->Endpoint))
|
||||
if (UClass** HandlerClass = MCPHandlerRegistry.Find(Req->Endpoint))
|
||||
{
|
||||
const bool bIsMutation = MutationEndpoints.Contains(Req->Endpoint);
|
||||
if (bIsMutation && GEditor)
|
||||
{
|
||||
GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *Req->Endpoint)));
|
||||
}
|
||||
|
||||
UMCPHandler* Handler = NewObject<UMCPHandler>(GetTransientPackage(), *HandlerClass);
|
||||
if (PopulateHandlerFromJson(Handler, Params.Get(), &*Result))
|
||||
{
|
||||
Handler->Handle(Params.Get(), &*Result);
|
||||
}
|
||||
|
||||
if (bIsMutation && GEditor)
|
||||
{
|
||||
GEditor->EndTransaction();
|
||||
}
|
||||
}
|
||||
else if (FRequestHandler* Handler = HandlerMap.Find(Req->Endpoint))
|
||||
{
|
||||
// Wrap mutation endpoints in an undo transaction so users can Ctrl+Z
|
||||
const bool bIsMutation = MutationEndpoints.Contains(Req->Endpoint);
|
||||
@@ -949,7 +985,7 @@ void FBlueprintMCPServer::RegisterHandlers()
|
||||
TEXT("deleteNode"),
|
||||
TEXT("duplicateNodes"),
|
||||
TEXT("addNode"),
|
||||
TEXT("spawnNode"),
|
||||
TEXT("spawn_node"),
|
||||
TEXT("setNodeComment"),
|
||||
TEXT("renameAsset"),
|
||||
TEXT("reparentBlueprint"),
|
||||
@@ -1032,7 +1068,7 @@ void FBlueprintMCPServer::RegisterHandlers()
|
||||
H(TEXT("validateAllBlueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints);
|
||||
H(TEXT("addNode"), &FBlueprintMCPServer::HandleAddNode);
|
||||
H(TEXT("searchNodeActions"), &FBlueprintMCPServer::HandleSearchNodeActions);
|
||||
H(TEXT("spawnNode"), &FBlueprintMCPServer::HandleSpawnNode);
|
||||
// spawn_node is now handled by UMCPHandler_SpawnNode (new-style registry)
|
||||
H(TEXT("renameAsset"), &FBlueprintMCPServer::HandleRenameAsset);
|
||||
H(TEXT("reparentBlueprint"), &FBlueprintMCPServer::HandleReparentBlueprint);
|
||||
H(TEXT("setBlueprintDefault"), &FBlueprintMCPServer::HandleSetBlueprintDefault);
|
||||
@@ -1100,6 +1136,35 @@ void FBlueprintMCPServer::RegisterHandlers()
|
||||
H(TEXT("setStateBlendSpace"), &FBlueprintMCPServer::HandleSetStateBlendSpace);
|
||||
}
|
||||
|
||||
void FBlueprintMCPServer::BuildMCPHandlerRegistry()
|
||||
{
|
||||
TArray<UClass*> HandlerClasses;
|
||||
GetDerivedClasses(UMCPHandler::StaticClass(), HandlerClasses);
|
||||
|
||||
for (UClass* Class : HandlerClasses)
|
||||
{
|
||||
if (Class->HasAnyClassFlags(CLASS_Abstract))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FString& ToolName = Class->GetMetaData(TEXT("ToolName"));
|
||||
if (ToolName.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: %s has no ToolName meta — skipping."), *Class->GetName());
|
||||
continue;
|
||||
}
|
||||
if (MCPHandlerRegistry.Contains(ToolName))
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Duplicate ToolName '%s' on %s — skipping."), *ToolName, *Class->GetName());
|
||||
continue;
|
||||
}
|
||||
MCPHandlerRegistry.Add(ToolName, Class);
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Registered handler '%s' → %s"), *ToolName, *Class->GetName());
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %d new-style handlers registered."), MCPHandlerRegistry.Num());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleRescan — refresh cached asset lists from asset registry
|
||||
// ============================================================
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#include "MCPHandler.h"
|
||||
#include "BlueprintMCPServer.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
#include "UObject/EnumProperty.h"
|
||||
|
||||
namespace MCPPopulate
|
||||
{
|
||||
|
||||
// Try to set a single FProperty on a handler from a JSON field.
|
||||
// Returns an empty string on success, or an error message on failure.
|
||||
FString SetPropertyFromJson(
|
||||
UMCPHandler* Handler,
|
||||
FProperty* Prop,
|
||||
const FString& FieldName,
|
||||
const FJsonObject* Json)
|
||||
{
|
||||
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Handler);
|
||||
|
||||
// FString
|
||||
if (FStrProperty* StrProp = CastField<FStrProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::String>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
||||
}
|
||||
StrProp->SetPropertyValue(ValuePtr, Json->GetStringField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// int32
|
||||
if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||
}
|
||||
IntProp->SetPropertyValue(ValuePtr, (int32)Json->GetNumberField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// float
|
||||
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||
}
|
||||
FloatProp->SetPropertyValue(ValuePtr, (float)Json->GetNumberField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// double
|
||||
if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||
}
|
||||
DoubleProp->SetPropertyValue(ValuePtr, Json->GetNumberField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// bool
|
||||
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Boolean>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a boolean"), *FieldName);
|
||||
}
|
||||
BoolProp->SetPropertyValue(ValuePtr, Json->GetBoolField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// Enum (FEnumProperty — C++ enum class)
|
||||
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::String>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
||||
}
|
||||
FString ValueStr = Json->GetStringField(FieldName);
|
||||
UEnum* Enum = EnumProp->GetEnum();
|
||||
int64 EnumVal = Enum->GetValueByNameString(ValueStr);
|
||||
if (EnumVal == INDEX_NONE)
|
||||
{
|
||||
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
|
||||
}
|
||||
FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty();
|
||||
UnderlyingProp->SetIntPropertyValue(ValuePtr, EnumVal);
|
||||
return FString();
|
||||
}
|
||||
|
||||
// Enum (FByteProperty with Enum — old-style UENUM)
|
||||
if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
|
||||
{
|
||||
if (ByteProp->Enum)
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::String>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
|
||||
}
|
||||
FString ValueStr = Json->GetStringField(FieldName);
|
||||
int64 EnumVal = ByteProp->Enum->GetValueByNameString(ValueStr);
|
||||
if (EnumVal == INDEX_NONE)
|
||||
{
|
||||
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
|
||||
}
|
||||
ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumVal);
|
||||
return FString();
|
||||
}
|
||||
// Plain byte without enum — treat as number
|
||||
if (!Json->HasTypedField<EJson::Number>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
|
||||
}
|
||||
ByteProp->SetPropertyValue(ValuePtr, (uint8)Json->GetNumberField(FieldName));
|
||||
return FString();
|
||||
}
|
||||
|
||||
// FMCPSubtree — stash the JSON subtree into the struct
|
||||
if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
|
||||
{
|
||||
if (StructProp->Struct == FMCPSubtree::StaticStruct())
|
||||
{
|
||||
if (!Json->HasTypedField<EJson::Object>(FieldName))
|
||||
{
|
||||
return FString::Printf(TEXT("'%s' must be an object"), *FieldName);
|
||||
}
|
||||
FMCPSubtree* Subtree = StructProp->ContainerPtrToValuePtr<FMCPSubtree>(Handler);
|
||||
Subtree->Json = Json->GetObjectField(FieldName);
|
||||
return FString();
|
||||
}
|
||||
}
|
||||
|
||||
return FString::Printf(TEXT("'%s': unsupported property type '%s'"),
|
||||
*FieldName, *Prop->GetCPPType());
|
||||
}
|
||||
|
||||
// Convert a property name from PascalCase to camelCase, matching JSON conventions.
|
||||
// e.g. "BlueprintName" -> "blueprintName", "PosX" -> "posX"
|
||||
FString PropertyNameToJsonKey(const FString& PropName)
|
||||
{
|
||||
if (PropName.IsEmpty())
|
||||
{
|
||||
return PropName;
|
||||
}
|
||||
FString Result = PropName;
|
||||
Result[0] = FChar::ToLower(Result[0]);
|
||||
return Result;
|
||||
}
|
||||
|
||||
} // namespace MCPPopulate
|
||||
|
||||
bool FBlueprintMCPServer::PopulateHandlerFromJson(
|
||||
UMCPHandler* Handler,
|
||||
const FJsonObject* Json,
|
||||
FJsonObject* Result)
|
||||
{
|
||||
UClass* HandlerClass = Handler->GetClass();
|
||||
|
||||
// Build a set of known property names (as JSON keys) for the unknown-field check.
|
||||
TSet<FString> KnownKeys;
|
||||
TArray<FProperty*> Properties;
|
||||
|
||||
for (TFieldIterator<FProperty> It(HandlerClass, EFieldIterationFlags::None); It; ++It)
|
||||
{
|
||||
FProperty* Prop = *It;
|
||||
Properties.Add(Prop);
|
||||
KnownKeys.Add(MCPPopulate::PropertyNameToJsonKey(Prop->GetName()));
|
||||
}
|
||||
|
||||
// Check for unknown fields in the JSON
|
||||
for (const auto& KV : Json->Values)
|
||||
{
|
||||
if (!KnownKeys.Contains(KV.Key))
|
||||
{
|
||||
MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Unknown parameter '%s'"), *KV.Key));
|
||||
Result->SetArrayField(TEXT("validParameters"),
|
||||
[&]() {
|
||||
TArray<TSharedPtr<FJsonValue>> Arr;
|
||||
for (const FString& Key : KnownKeys)
|
||||
{
|
||||
Arr.Add(MakeShared<FJsonValueString>(Key));
|
||||
}
|
||||
return Arr;
|
||||
}());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate each property from JSON
|
||||
for (FProperty* Prop : Properties)
|
||||
{
|
||||
FString JsonKey = MCPPopulate::PropertyNameToJsonKey(Prop->GetName());
|
||||
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
|
||||
|
||||
if (!Json->HasField(JsonKey))
|
||||
{
|
||||
if (!bOptional)
|
||||
{
|
||||
MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Missing required parameter '%s'"), *JsonKey));
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
FString Error = MCPPopulate::SetPropertyFromJson(Handler, Prop, JsonKey, Json);
|
||||
if (!Error.IsEmpty())
|
||||
{
|
||||
MakeErrorJson(Result, Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -29,6 +29,8 @@ public:
|
||||
virtual bool IsTickable() const override;
|
||||
virtual TStatId GetStatId() const override;
|
||||
|
||||
FBlueprintMCPServer* GetServer() const { return Server.Get(); }
|
||||
|
||||
private:
|
||||
TUniquePtr<FBlueprintMCPServer> Server;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "BlueprintMCPHandlers_Mutation.generated.h"
|
||||
|
||||
UCLASS(meta=(ToolName="spawn_node"))
|
||||
class UMCPHandler_SpawnNode : public UMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Graph name (e.g. 'EventGraph')"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Full action name from search_node_actions (e.g. 'Luprex|Lua|Read Lua Values')"))
|
||||
FString ActionName;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="X position in the graph"))
|
||||
int32 PosX = 0;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Y position in the graph"))
|
||||
int32 PosY = 0;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Create a node in a Blueprint graph using the editor's action database. "
|
||||
"Unlike add_node which only supports a fixed set of node types, spawn_node can create "
|
||||
"ANY node type that appears in the editor's right-click menu, including custom K2 nodes. "
|
||||
"Use search_node_actions first to find the exact action name.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
|
||||
};
|
||||
@@ -14,6 +14,7 @@ class UMaterial;
|
||||
class UMaterialInstanceConstant;
|
||||
class UMaterialFunction;
|
||||
class UMaterialExpression;
|
||||
class UMCPHandler;
|
||||
|
||||
// ----- Snapshot data structures -----
|
||||
|
||||
@@ -61,6 +62,9 @@ struct FGraphSnapshot
|
||||
class FBlueprintMCPServer
|
||||
{
|
||||
public:
|
||||
/** Get the active server instance via the editor subsystem. */
|
||||
static FBlueprintMCPServer* 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). */
|
||||
bool Start(int32 InPort, bool bEditorMode = false);
|
||||
@@ -96,9 +100,11 @@ public:
|
||||
private:
|
||||
// ----- Request dispatch -----
|
||||
using FRequestHandler = TFunction<void(const FJsonObject* Json, FJsonObject* Result)>;
|
||||
TMap<FString, FRequestHandler> HandlerMap;
|
||||
TMap<FString, FRequestHandler> HandlerMap; // old-style handlers
|
||||
TMap<FString, UClass*> MCPHandlerRegistry; // new-style: tool name → UMCPHandler subclass
|
||||
TSet<FString> MutationEndpoints;
|
||||
void RegisterHandlers();
|
||||
void BuildMCPHandlerRegistry();
|
||||
// ----- Queued request model -----
|
||||
struct FPendingRequest
|
||||
{
|
||||
@@ -209,7 +215,6 @@ private:
|
||||
|
||||
// ----- Generic node spawning via action database -----
|
||||
void HandleSearchNodeActions(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSpawnNode(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Diagnostic -----
|
||||
void HandleTestSave(const FJsonObject* Json, FJsonObject* Result);
|
||||
@@ -276,6 +281,7 @@ private:
|
||||
void HandleSetBlendSpaceSamples(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
public:
|
||||
// ----- Serialization -----
|
||||
TSharedRef<FJsonObject> SerializeBlueprint(UBlueprint* BP);
|
||||
TSharedPtr<FJsonObject> SerializeGraph(UEdGraph* Graph);
|
||||
@@ -297,6 +303,10 @@ private:
|
||||
static void CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest);
|
||||
static FString UrlDecode(const FString& EncodedString);
|
||||
|
||||
// Populate a handler's UPROPERTY fields from JSON.
|
||||
// Returns true on success, or sets error on Result and returns false.
|
||||
bool PopulateHandlerFromJson(UMCPHandler* Handler, const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Material helpers -----
|
||||
/** Ensure that Material->MaterialGraph exists (creates it on demand for commandlet mode). */
|
||||
void EnsureMaterialGraph(UMaterial* Material);
|
||||
@@ -325,3 +335,6 @@ private:
|
||||
bool SaveSnapshotToDisk(const FString& SnapshotId, const FGraphSnapshot& Snapshot);
|
||||
bool LoadSnapshotFromDisk(const FString& SnapshotId, FGraphSnapshot& OutSnapshot);
|
||||
};
|
||||
|
||||
// Transitional alias — eventually MCPHelper will be its own class.
|
||||
using MCPHelper = FBlueprintMCPServer;
|
||||
|
||||
44
Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPHandler.h
Normal file
44
Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPHandler.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UObject/Object.h"
|
||||
#include "Dom/JsonObject.h"
|
||||
#include "MCPHandler.generated.h"
|
||||
|
||||
// Marker struct for handler parameters that accept a JSON subtree.
|
||||
// The parameter name is included in the tool schema as "type": "object".
|
||||
// PopulateHandlerFromJson stashes the actual JSON object into the Json field,
|
||||
// so the handler can read it directly without parsing from the raw request.
|
||||
USTRUCT()
|
||||
struct FMCPSubtree
|
||||
{
|
||||
GENERATED_BODY()
|
||||
TSharedPtr<FJsonObject> Json;
|
||||
};
|
||||
|
||||
// Base class for self-registering MCP tool handlers.
|
||||
//
|
||||
// Subclasses declare their parameters as UPROPERTY fields, which are
|
||||
// automatically populated from the incoming JSON request and used to
|
||||
// generate the tool's JSON Schema for MCP tools/list.
|
||||
//
|
||||
// Class metadata:
|
||||
// ToolName - the MCP tool name (e.g. "spawn_node")
|
||||
//
|
||||
// Property metadata:
|
||||
// Optional - marks a parameter as not required
|
||||
//
|
||||
UCLASS(Abstract)
|
||||
class BLUEPRINTMCP_API UMCPHandler : public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Human-readable tool description for MCP tools/list.
|
||||
// Override in each subclass.
|
||||
virtual FString GetDescription() const PURE_VIRTUAL(UMCPHandler::GetDescription, return FString(););
|
||||
|
||||
// Called after parameter fields have been populated from JSON.
|
||||
// Override in each subclass.
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) PURE_VIRTUAL(UMCPHandler::Handle, );
|
||||
};
|
||||
Reference in New Issue
Block a user