Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Discovery.h

596 lines
20 KiB
C++

#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "UObject/UObjectIterator.h"
#include "MCPHandlers_Discovery.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleGetPinInfo — detailed information about a specific pin
// ============================================================
UCLASS(meta=(ToolName="get_pin_details"))
class UMCPHandler_GetPinInfo : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Node to look up (GUID)"))
FString Node;
UPROPERTY(meta=(Description="Pin name on the node"))
FString PinName;
virtual FString GetDescription() const override
{
return TEXT("Get detailed information about a specific pin on a blueprint node, "
"including type, connections, and default values.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UEdGraph* Graph = nullptr;
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
if (!FoundNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
}
UEdGraphPin* Pin = FoundNode->FindPin(FName(*PinName));
if (!Pin)
{
// List available pins
TArray<TSharedPtr<FJsonValue>> AvailPins;
for (UEdGraphPin* P : FoundNode->Pins)
{
if (P)
{
TSharedRef<FJsonObject> PinObj = MakeShared<FJsonObject>();
PinObj->SetStringField(TEXT("name"), P->PinName.ToString());
PinObj->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
PinObj->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString());
AvailPins.Add(MakeShared<FJsonValueObject>(PinObj));
}
}
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *Node));
Result->SetArrayField(TEXT("availablePins"), AvailPins);
return;
}
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), Blueprint);
Result->SetStringField(TEXT("nodeId"), Node);
Result->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
Result->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
Result->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
if (!Pin->PinType.PinSubCategory.IsNone())
{
Result->SetStringField(TEXT("subCategory"), Pin->PinType.PinSubCategory.ToString());
}
if (Pin->PinType.PinSubCategoryObject.IsValid())
{
Result->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName());
}
Result->SetBoolField(TEXT("isArray"), Pin->PinType.IsArray());
Result->SetBoolField(TEXT("isSet"), Pin->PinType.IsSet());
Result->SetBoolField(TEXT("isMap"), Pin->PinType.IsMap());
Result->SetBoolField(TEXT("isReference"), Pin->PinType.bIsReference);
Result->SetBoolField(TEXT("isConst"), Pin->PinType.bIsConst);
if (!Pin->DefaultValue.IsEmpty())
{
Result->SetStringField(TEXT("defaultValue"), Pin->DefaultValue);
}
if (!Pin->DefaultTextValue.IsEmpty())
{
Result->SetStringField(TEXT("defaultTextValue"), Pin->DefaultTextValue.ToString());
}
if (Pin->DefaultObject)
{
Result->SetStringField(TEXT("defaultObject"), Pin->DefaultObject->GetPathName());
}
// Connected pins
if (Pin->LinkedTo.Num() > 0)
{
TArray<TSharedPtr<FJsonValue>> Conns;
for (UEdGraphPin* Linked : Pin->LinkedTo)
{
if (!Linked || !Linked->GetOwningNode()) continue;
TSharedRef<FJsonObject> CJ = MakeShared<FJsonObject>();
CJ->SetStringField(TEXT("nodeId"), Linked->GetOwningNode()->NodeGuid.ToString());
CJ->SetStringField(TEXT("pinName"), Linked->PinName.ToString());
CJ->SetStringField(TEXT("nodeTitle"), Linked->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
Conns.Add(MakeShared<FJsonValueObject>(CJ));
}
Result->SetArrayField(TEXT("connectedTo"), Conns);
}
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleCheckPinCompatibility — pre-flight check for connect_pins
// ============================================================
UCLASS(meta=(ToolName="check_pin_connection_compatibility"))
class UMCPHandler_CheckPinCompatibility : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Source node (GUID)"))
FString SourceNode;
UPROPERTY(meta=(Description="Source pin name"))
FString SourcePinName;
UPROPERTY(meta=(Description="Target node (GUID)"))
FString TargetNode;
UPROPERTY(meta=(Description="Target pin name"))
FString TargetPinName;
virtual FString GetDescription() const override
{
return TEXT("Check whether two pins can be connected, and what kind of connection would result. "
"Use as a pre-flight check before connect_blueprint_pins.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UEdGraph* SourceGraph = nullptr;
UEdGraphNode* FoundSourceNode = MCPUtils::FindNodeByGuid(BP, SourceNode, &SourceGraph);
if (!FoundSourceNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNode));
}
UEdGraphNode* FoundTargetNode = MCPUtils::FindNodeByGuid(BP, TargetNode);
if (!FoundTargetNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNode));
}
UEdGraphPin* SourcePin = FoundSourceNode->FindPin(FName(*SourcePinName));
if (!SourcePin)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *SourcePinName, *SourceNode));
}
UEdGraphPin* TargetPin = FoundTargetNode->FindPin(FName(*TargetPinName));
if (!TargetPin)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *TargetPinName, *TargetNode));
}
const UEdGraphSchema* Schema = SourceGraph ? SourceGraph->GetSchema() : nullptr;
if (!Schema)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found"));
}
// Check compatibility using the schema
const FPinConnectionResponse Response = Schema->CanCreateConnection(SourcePin, TargetPin);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), Blueprint);
bool bCompatible = (Response.Response != ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW);
Result->SetBoolField(TEXT("compatible"), bCompatible);
// Decode the response type
FString ResponseType;
switch (Response.Response)
{
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE:
ResponseType = TEXT("direct");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_A:
ResponseType = TEXT("breakSourceConnections");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_B:
ResponseType = TEXT("breakTargetConnections");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_AB:
ResponseType = TEXT("breakBothConnections");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE:
ResponseType = TEXT("requiresConversion");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_PROMOTION:
ResponseType = TEXT("requiresPromotion");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW:
default:
ResponseType = TEXT("disallowed");
break;
}
Result->SetStringField(TEXT("connectionType"), ResponseType);
if (!Response.Message.IsEmpty())
{
Result->SetStringField(TEXT("message"), Response.Message.ToString());
}
// Include pin type info for context
Result->SetStringField(TEXT("sourcePinType"), SourcePin->PinType.PinCategory.ToString());
if (SourcePin->PinType.PinSubCategoryObject.IsValid())
Result->SetStringField(TEXT("sourcePinSubtype"), SourcePin->PinType.PinSubCategoryObject->GetName());
Result->SetStringField(TEXT("targetPinType"), TargetPin->PinType.PinCategory.ToString());
if (TargetPin->PinType.PinSubCategoryObject.IsValid())
Result->SetStringField(TEXT("targetPinSubtype"), TargetPin->PinType.PinSubCategoryObject->GetName());
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleListClasses — discover available UClasses
// ============================================================
UCLASS(meta=(ToolName="search_unreal_classes"))
class UMCPHandler_ListClasses : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring filter for class names"))
FString Filter;
UPROPERTY(meta=(Optional, Description="Parent class name to restrict results to subclasses"))
FString ParentClass;
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (1-500, default 100)"))
int32 Limit = 100;
virtual FString GetDescription() const override
{
return TEXT("Search for available UClasses by name substring and/or parent class. "
"Returns class metadata including flags, parent class, and package.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
if (Json->HasField(TEXT("limit")))
{
Limit = FMath::Clamp(Limit, 1, 500);
}
UClass* ParentClassObj = nullptr;
if (!ParentClass.IsEmpty())
{
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == ParentClass || It->GetName() == ParentClass + TEXT("_C"))
{
ParentClassObj = *It;
break;
}
}
if (!ParentClassObj)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Parent class '%s' not found"), *ParentClass));
}
}
TArray<TSharedPtr<FJsonValue>> ClassList;
int32 TotalMatched = 0;
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Class = *It;
if (!Class) continue;
// Skip internal/deprecated classes
if (Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists)) continue;
// Apply parent filter
if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue;
FString ClassName = Class->GetName();
// Apply name filter
if (!Filter.IsEmpty() && !ClassName.Contains(Filter, ESearchCase::IgnoreCase))
{
continue;
}
TotalMatched++;
if (ClassList.Num() >= Limit) continue; // Count but don't add beyond limit
TSharedRef<FJsonObject> ClassObj = MakeShared<FJsonObject>();
ClassObj->SetStringField(TEXT("name"), ClassName);
ClassObj->SetStringField(TEXT("fullPath"), Class->GetPathName());
// Determine if it's a Blueprint-generated class
bool bIsBlueprint = Class->ClassGeneratedBy != nullptr;
ClassObj->SetBoolField(TEXT("isBlueprint"), bIsBlueprint);
// Parent class
if (Class->GetSuperClass())
{
ClassObj->SetStringField(TEXT("parentClass"), Class->GetSuperClass()->GetName());
}
// Module/package info
UPackage* Package = Class->GetOuterUPackage();
if (Package)
{
ClassObj->SetStringField(TEXT("package"), Package->GetName());
}
// Flags
TArray<TSharedPtr<FJsonValue>> Flags;
if (Class->HasAnyClassFlags(CLASS_Abstract)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Abstract")));
if (Class->HasAnyClassFlags(CLASS_Interface)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Interface")));
if (Class->HasAnyClassFlags(CLASS_MinimalAPI)) Flags.Add(MakeShared<FJsonValueString>(TEXT("MinimalAPI")));
if (Flags.Num() > 0)
{
ClassObj->SetArrayField(TEXT("flags"), Flags);
}
ClassList.Add(MakeShared<FJsonValueObject>(ClassObj));
}
Result->SetBoolField(TEXT("success"), true);
Result->SetNumberField(TEXT("count"), ClassList.Num());
Result->SetNumberField(TEXT("totalMatched"), TotalMatched);
if (TotalMatched > Limit)
{
Result->SetBoolField(TEXT("truncated"), true);
Result->SetNumberField(TEXT("limit"), Limit);
}
Result->SetArrayField(TEXT("classes"), ClassList);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleListFunctions — list Blueprint-callable functions on a class
// ============================================================
UCLASS(meta=(ToolName="list_class_functions"))
class UMCPHandler_ListFunctions : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Class name to list functions for"))
FString ClassName;
UPROPERTY(meta=(Optional, Description="Substring filter for function names"))
FString Filter;
virtual FString GetDescription() const override
{
return TEXT("List Blueprint-callable functions on a UClass, including parameter info and flags.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
// Find the class
UClass* FoundClass = nullptr;
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C"))
{
FoundClass = *It;
break;
}
}
if (!FoundClass)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName));
}
TArray<TSharedPtr<FJsonValue>> FuncList;
for (TFieldIterator<UFunction> FuncIt(FoundClass); FuncIt; ++FuncIt)
{
UFunction* Func = *FuncIt;
if (!Func) continue;
// Only include Blueprint-callable functions
if (!Func->HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_BlueprintPure | FUNC_BlueprintEvent)) continue;
FString FuncName = Func->GetName();
// Apply filter
if (!Filter.IsEmpty() && !FuncName.Contains(Filter, ESearchCase::IgnoreCase))
{
continue;
}
TSharedRef<FJsonObject> FuncObj = MakeShared<FJsonObject>();
FuncObj->SetStringField(TEXT("name"), FuncName);
// Determine the owning class
UClass* OwnerClass = Func->GetOwnerClass();
if (OwnerClass)
{
FuncObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName());
}
// Function flags
FuncObj->SetBoolField(TEXT("isPure"), Func->HasAnyFunctionFlags(FUNC_BlueprintPure));
FuncObj->SetBoolField(TEXT("isStatic"), Func->HasAnyFunctionFlags(FUNC_Static));
FuncObj->SetBoolField(TEXT("isEvent"), Func->HasAnyFunctionFlags(FUNC_BlueprintEvent));
FuncObj->SetBoolField(TEXT("isConst"), Func->HasAnyFunctionFlags(FUNC_Const));
// Parameters
TArray<TSharedPtr<FJsonValue>> Params;
FString ReturnType;
for (TFieldIterator<FProperty> PropIt(Func); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
if (!Prop) continue;
FString PropType = Prop->GetCPPType();
if (Prop->HasAnyPropertyFlags(CPF_ReturnParm))
{
ReturnType = PropType;
continue;
}
if (Prop->HasAnyPropertyFlags(CPF_Parm))
{
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
ParamObj->SetStringField(TEXT("name"), Prop->GetName());
ParamObj->SetStringField(TEXT("type"), PropType);
ParamObj->SetBoolField(TEXT("isOutput"), Prop->HasAnyPropertyFlags(CPF_OutParm) && !Prop->HasAnyPropertyFlags(CPF_ReferenceParm));
ParamObj->SetBoolField(TEXT("isReference"), Prop->HasAnyPropertyFlags(CPF_ReferenceParm));
Params.Add(MakeShared<FJsonValueObject>(ParamObj));
}
}
FuncObj->SetArrayField(TEXT("parameters"), Params);
if (!ReturnType.IsEmpty())
{
FuncObj->SetStringField(TEXT("returnType"), ReturnType);
}
FuncList.Add(MakeShared<FJsonValueObject>(FuncObj));
}
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("className"), FoundClass->GetName());
Result->SetNumberField(TEXT("count"), FuncList.Num());
Result->SetArrayField(TEXT("functions"), FuncList);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleListProperties — list properties on a class
// ============================================================
UCLASS(meta=(ToolName="list_class_properties"))
class UMCPHandler_ListProperties : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Class name to list properties for"))
FString ClassName;
UPROPERTY(meta=(Optional, Description="Substring filter for property names"))
FString Filter;
virtual FString GetDescription() const override
{
return TEXT("List properties on a UClass, including type, owning class, and property flags.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
// Find the class
UClass* FoundClass = nullptr;
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C"))
{
FoundClass = *It;
break;
}
}
if (!FoundClass)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName));
}
TArray<TSharedPtr<FJsonValue>> PropList;
for (TFieldIterator<FProperty> PropIt(FoundClass); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
if (!Prop) continue;
FString PropName = Prop->GetName();
// Apply filter
if (!Filter.IsEmpty() && !PropName.Contains(Filter, ESearchCase::IgnoreCase))
{
continue;
}
TSharedRef<FJsonObject> PropObj = MakeShared<FJsonObject>();
PropObj->SetStringField(TEXT("name"), PropName);
PropObj->SetStringField(TEXT("type"), Prop->GetCPPType());
// Determine the owning class
UClass* OwnerClass = Prop->GetOwnerClass();
if (OwnerClass)
{
PropObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName());
}
// Property flags
TArray<TSharedPtr<FJsonValue>> Flags;
if (Prop->HasAnyPropertyFlags(CPF_BlueprintVisible)) Flags.Add(MakeShared<FJsonValueString>(TEXT("BlueprintVisible")));
if (Prop->HasAnyPropertyFlags(CPF_BlueprintReadOnly)) Flags.Add(MakeShared<FJsonValueString>(TEXT("BlueprintReadOnly")));
if (Prop->HasAnyPropertyFlags(CPF_Edit)) Flags.Add(MakeShared<FJsonValueString>(TEXT("EditAnywhere")));
if (Prop->HasAnyPropertyFlags(CPF_EditConst)) Flags.Add(MakeShared<FJsonValueString>(TEXT("VisibleOnly")));
if (Prop->HasAnyPropertyFlags(CPF_Config)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Config")));
if (Prop->HasAnyPropertyFlags(CPF_SaveGame)) Flags.Add(MakeShared<FJsonValueString>(TEXT("SaveGame")));
if (Prop->HasAnyPropertyFlags(CPF_Transient)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Transient")));
if (Prop->HasAnyPropertyFlags(CPF_RepNotify)) Flags.Add(MakeShared<FJsonValueString>(TEXT("RepNotify")));
if (Flags.Num() > 0)
{
PropObj->SetArrayField(TEXT("flags"), Flags);
}
PropList.Add(MakeShared<FJsonValueObject>(PropObj));
}
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("className"), FoundClass->GetName());
Result->SetNumberField(TEXT("count"), PropList.Num());
Result->SetArrayField(TEXT("properties"), PropList);
}
};