Implemented new framework for MCP Handlers. Made one handler with the new framework.

This commit is contained in:
2026-03-06 00:08:30 -05:00
parent 9b32de024a
commit cf1c2bf8cf
71 changed files with 440 additions and 74415 deletions

View File

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

View File

@@ -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
// ============================================================

View File

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

View File

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

View File

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

View File

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

View 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, );
};