All MCP handlers converted to new format
This commit is contained in:
@@ -6,3 +6,14 @@
|
||||
#include "MCPHandlers_Validation.h"
|
||||
#include "MCPHandlers_UserTypes.h"
|
||||
#include "MCPHandlers_AnimMutation.h"
|
||||
#include "MCPHandlers_Read.h"
|
||||
#include "MCPHandlers_Discovery.h"
|
||||
#include "MCPHandlers_Graphs.h"
|
||||
#include "MCPHandlers_Variables.h"
|
||||
#include "MCPHandlers_Params.h"
|
||||
#include "MCPHandlers_Dispatchers.h"
|
||||
#include "MCPHandlers_Components.h"
|
||||
#include "MCPHandlers_MaterialRead.h"
|
||||
#include "MCPHandlers_MaterialMutation.h"
|
||||
#include "MCPHandlers_MaterialInstance.h"
|
||||
#include "MCPHandlers_StateMachine.h"
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Engine/SimpleConstructionScript.h"
|
||||
#include "Engine/SCS_Node.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
|
||||
// ============================================================
|
||||
// HandleListComponents — list all components in a Blueprint's SCS
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleListComponents(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
if (BlueprintName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
|
||||
}
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
||||
if (!SCS)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
||||
*BlueprintName));
|
||||
}
|
||||
|
||||
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> ComponentsArr;
|
||||
for (USCS_Node* Node : AllNodes)
|
||||
{
|
||||
if (!Node)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> CompObj = MakeShared<FJsonObject>();
|
||||
CompObj->SetStringField(TEXT("name"), Node->GetVariableName().ToString());
|
||||
|
||||
if (Node->ComponentClass)
|
||||
{
|
||||
CompObj->SetStringField(TEXT("componentClass"), Node->ComponentClass->GetName());
|
||||
}
|
||||
else
|
||||
{
|
||||
CompObj->SetStringField(TEXT("componentClass"), TEXT("None"));
|
||||
}
|
||||
|
||||
// Parent component info
|
||||
USCS_Node* ParentNode = nullptr;
|
||||
for (USCS_Node* Candidate : AllNodes)
|
||||
{
|
||||
if (Candidate && Candidate->GetChildNodes().Contains(Node))
|
||||
{
|
||||
ParentNode = Candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ParentNode)
|
||||
{
|
||||
CompObj->SetStringField(TEXT("parentComponent"), ParentNode->GetVariableName().ToString());
|
||||
}
|
||||
|
||||
// Check if this is a default scene root (first root node with SceneComponent class)
|
||||
bool bIsSceneRoot = false;
|
||||
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
|
||||
if (RootNodes.Num() > 0 && RootNodes[0] == Node)
|
||||
{
|
||||
bIsSceneRoot = true;
|
||||
}
|
||||
CompObj->SetBoolField(TEXT("isSceneRoot"), bIsSceneRoot);
|
||||
|
||||
// List child count for informational purposes
|
||||
CompObj->SetNumberField(TEXT("childCount"), Node->GetChildNodes().Num());
|
||||
|
||||
ComponentsArr.Add(MakeShared<FJsonValueObject>(CompObj));
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetNumberField(TEXT("count"), ComponentsArr.Num());
|
||||
Result->SetArrayField(TEXT("components"), ComponentsArr);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleAddComponent — add a component to a Blueprint's SCS
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleAddComponent(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString ComponentClassName = Json->GetStringField(TEXT("componentClass"));
|
||||
FString ComponentName = Json->GetStringField(TEXT("name"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || ComponentClassName.IsEmpty() || ComponentName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, componentClass, name"));
|
||||
}
|
||||
|
||||
FString ParentComponentName;
|
||||
if (Json->HasField(TEXT("parentComponent")))
|
||||
{
|
||||
ParentComponentName = Json->GetStringField(TEXT("parentComponent"));
|
||||
}
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
||||
if (!SCS)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
||||
*BlueprintName));
|
||||
}
|
||||
|
||||
// Check for duplicate component names
|
||||
const TArray<USCS_Node*>& ExistingNodes = SCS->GetAllNodes();
|
||||
for (USCS_Node* Existing : ExistingNodes)
|
||||
{
|
||||
if (Existing && Existing->GetVariableName().ToString().Equals(ComponentName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A component named '%s' already exists in Blueprint '%s'"),
|
||||
*ComponentName, *BlueprintName));
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the component class by name
|
||||
// Try multiple name variants: exact name, with U prefix, without U prefix
|
||||
UClass* ComponentClass = nullptr;
|
||||
|
||||
TArray<FString> NamesToTry;
|
||||
NamesToTry.Add(ComponentClassName);
|
||||
if (!ComponentClassName.StartsWith(TEXT("U")))
|
||||
{
|
||||
NamesToTry.Add(FString::Printf(TEXT("U%s"), *ComponentClassName));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Also try without U prefix
|
||||
NamesToTry.Add(ComponentClassName.Mid(1));
|
||||
}
|
||||
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
if (!It->IsChildOf(UActorComponent::StaticClass()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString ClassName = It->GetName();
|
||||
for (const FString& NameToTry : NamesToTry)
|
||||
{
|
||||
if (ClassName.Equals(NameToTry, ESearchCase::IgnoreCase))
|
||||
{
|
||||
ComponentClass = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ComponentClass)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ComponentClass)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Component class '%s' not found or is not a subclass of UActorComponent. "
|
||||
"Common classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, "
|
||||
"SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, "
|
||||
"ArrowComponent, ChildActorComponent, SpotLightComponent, PointLightComponent, "
|
||||
"WidgetComponent, BillboardComponent"),
|
||||
*ComponentClassName));
|
||||
}
|
||||
|
||||
// If parent component specified, find its SCS node
|
||||
USCS_Node* ParentSCSNode = nullptr;
|
||||
if (!ParentComponentName.IsEmpty())
|
||||
{
|
||||
for (USCS_Node* Node : ExistingNodes)
|
||||
{
|
||||
if (Node && Node->GetVariableName().ToString().Equals(ParentComponentName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
ParentSCSNode = Node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentSCSNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parent component '%s' not found in Blueprint '%s'"),
|
||||
*ParentComponentName, *BlueprintName));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding component '%s' (%s) to Blueprint '%s'"),
|
||||
*ComponentName, *ComponentClass->GetName(), *BlueprintName);
|
||||
|
||||
// Create the SCS node
|
||||
USCS_Node* NewNode = SCS->CreateNode(ComponentClass, FName(*ComponentName));
|
||||
if (!NewNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Failed to create SCS node for component '%s' with class '%s'"),
|
||||
*ComponentName, *ComponentClass->GetName()));
|
||||
}
|
||||
|
||||
// Add to the hierarchy
|
||||
if (ParentSCSNode)
|
||||
{
|
||||
ParentSCSNode->AddChildNode(NewNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
SCS->AddNode(NewNode);
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added component '%s' (%s) to '%s' (parent: %s, saved: %s)"),
|
||||
*ComponentName, *ComponentClass->GetName(), *BlueprintName,
|
||||
ParentSCSNode ? *ParentComponentName : TEXT("(root)"),
|
||||
bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("name"), NewNode->GetVariableName().ToString());
|
||||
Result->SetStringField(TEXT("componentClass"), ComponentClass->GetName());
|
||||
if (ParentSCSNode)
|
||||
{
|
||||
Result->SetStringField(TEXT("parentComponent"), ParentSCSNode->GetVariableName().ToString());
|
||||
}
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleRemoveComponent — remove a component from a Blueprint's SCS
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleRemoveComponent(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString ComponentName = Json->GetStringField(TEXT("name"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || ComponentName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, name"));
|
||||
}
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
||||
if (!SCS)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
||||
*BlueprintName));
|
||||
}
|
||||
|
||||
// Find the node to remove
|
||||
USCS_Node* NodeToRemove = nullptr;
|
||||
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
|
||||
for (USCS_Node* Node : AllNodes)
|
||||
{
|
||||
if (Node && Node->GetVariableName().ToString().Equals(ComponentName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
NodeToRemove = Node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!NodeToRemove)
|
||||
{
|
||||
// Build list of component names for the error message
|
||||
TArray<TSharedPtr<FJsonValue>> CompList;
|
||||
for (USCS_Node* Node : AllNodes)
|
||||
{
|
||||
if (Node)
|
||||
{
|
||||
CompList.Add(MakeShared<FJsonValueString>(Node->GetVariableName().ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Component '%s' not found in Blueprint '%s'"),
|
||||
*ComponentName, *BlueprintName));
|
||||
Result->SetArrayField(TEXT("existingComponents"), CompList);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent removing the root scene component if it has children
|
||||
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
|
||||
if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot remove component '%s' because it is a root component with %d child(ren). "
|
||||
"Remove or re-parent the children first."),
|
||||
*ComponentName, NodeToRemove->GetChildNodes().Num()));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing component '%s' from Blueprint '%s'"),
|
||||
*ComponentName, *BlueprintName);
|
||||
|
||||
// Remove the node (promotes children to parent if it has any — but we've guarded root above)
|
||||
SCS->RemoveNodeAndPromoteChildren(NodeToRemove);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed component '%s' from '%s' (saved: %s)"),
|
||||
*ComponentName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("name"), ComponentName);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Engine/SimpleConstructionScript.h"
|
||||
#include "Engine/SCS_Node.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
#include "MCPHandlers_Components.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="list_blueprint_components"))
|
||||
class UMCPHandler_ListBlueprintComponents : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("List all components in a Blueprint's SimpleConstructionScript, "
|
||||
"including hierarchy and scene root information.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
||||
if (!SCS)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
||||
*Blueprint));
|
||||
}
|
||||
|
||||
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> ComponentsArr;
|
||||
for (USCS_Node* Node : AllNodes)
|
||||
{
|
||||
if (!Node)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> CompObj = MakeShared<FJsonObject>();
|
||||
CompObj->SetStringField(TEXT("name"), Node->GetVariableName().ToString());
|
||||
|
||||
if (Node->ComponentClass)
|
||||
{
|
||||
CompObj->SetStringField(TEXT("componentClass"), Node->ComponentClass->GetName());
|
||||
}
|
||||
else
|
||||
{
|
||||
CompObj->SetStringField(TEXT("componentClass"), TEXT("None"));
|
||||
}
|
||||
|
||||
// Parent component info
|
||||
USCS_Node* ParentNode = nullptr;
|
||||
for (USCS_Node* Candidate : AllNodes)
|
||||
{
|
||||
if (Candidate && Candidate->GetChildNodes().Contains(Node))
|
||||
{
|
||||
ParentNode = Candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ParentNode)
|
||||
{
|
||||
CompObj->SetStringField(TEXT("parentComponent"), ParentNode->GetVariableName().ToString());
|
||||
}
|
||||
|
||||
// Check if this is a default scene root (first root node with SceneComponent class)
|
||||
bool bIsSceneRoot = false;
|
||||
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
|
||||
if (RootNodes.Num() > 0 && RootNodes[0] == Node)
|
||||
{
|
||||
bIsSceneRoot = true;
|
||||
}
|
||||
CompObj->SetBoolField(TEXT("isSceneRoot"), bIsSceneRoot);
|
||||
|
||||
// List child count for informational purposes
|
||||
CompObj->SetNumberField(TEXT("childCount"), Node->GetChildNodes().Num());
|
||||
|
||||
ComponentsArr.Add(MakeShared<FJsonValueObject>(CompObj));
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetNumberField(TEXT("count"), ComponentsArr.Num());
|
||||
Result->SetArrayField(TEXT("components"), ComponentsArr);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="add_blueprint_component"))
|
||||
class UMCPHandler_AddBlueprintComponent : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Component class name (e.g. StaticMeshComponent, SceneComponent)"))
|
||||
FString ComponentClass;
|
||||
|
||||
UPROPERTY(meta=(Description="Component name for the new component"))
|
||||
FString Component;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Name of the parent component to attach to"))
|
||||
FString ParentComponent;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Add a component to a Blueprint's SimpleConstructionScript. "
|
||||
"Optionally attach it to an existing parent component.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
||||
if (!SCS)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
||||
*Blueprint));
|
||||
}
|
||||
|
||||
// Check for duplicate component names
|
||||
const TArray<USCS_Node*>& ExistingNodes = SCS->GetAllNodes();
|
||||
for (USCS_Node* Existing : ExistingNodes)
|
||||
{
|
||||
if (Existing && Existing->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A component named '%s' already exists in Blueprint '%s'"),
|
||||
*Component, *Blueprint));
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the component class by name
|
||||
// Try multiple name variants: exact name, with U prefix, without U prefix
|
||||
UClass* ComponentClassObj = nullptr;
|
||||
|
||||
TArray<FString> NamesToTry;
|
||||
NamesToTry.Add(ComponentClass);
|
||||
if (!ComponentClass.StartsWith(TEXT("U")))
|
||||
{
|
||||
NamesToTry.Add(FString::Printf(TEXT("U%s"), *ComponentClass));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Also try without U prefix
|
||||
NamesToTry.Add(ComponentClass.Mid(1));
|
||||
}
|
||||
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
if (!It->IsChildOf(UActorComponent::StaticClass()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FString ClassName = It->GetName();
|
||||
for (const FString& NameToTry : NamesToTry)
|
||||
{
|
||||
if (ClassName.Equals(NameToTry, ESearchCase::IgnoreCase))
|
||||
{
|
||||
ComponentClassObj = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ComponentClassObj)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ComponentClassObj)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Component class '%s' not found or is not a subclass of UActorComponent. "
|
||||
"Common classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, "
|
||||
"SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, "
|
||||
"ArrowComponent, ChildActorComponent, SpotLightComponent, PointLightComponent, "
|
||||
"WidgetComponent, BillboardComponent"),
|
||||
*ComponentClass));
|
||||
}
|
||||
|
||||
// If parent component specified, find its SCS node
|
||||
USCS_Node* ParentSCSNode = nullptr;
|
||||
if (!ParentComponent.IsEmpty())
|
||||
{
|
||||
for (USCS_Node* Node : ExistingNodes)
|
||||
{
|
||||
if (Node && Node->GetVariableName().ToString().Equals(ParentComponent, ESearchCase::IgnoreCase))
|
||||
{
|
||||
ParentSCSNode = Node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentSCSNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parent component '%s' not found in Blueprint '%s'"),
|
||||
*ParentComponent, *Blueprint));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding component '%s' (%s) to Blueprint '%s'"),
|
||||
*Component, *ComponentClassObj->GetName(), *Blueprint);
|
||||
|
||||
// Create the SCS node
|
||||
USCS_Node* NewNode = SCS->CreateNode(ComponentClassObj, FName(*Component));
|
||||
if (!NewNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Failed to create SCS node for component '%s' with class '%s'"),
|
||||
*Component, *ComponentClassObj->GetName()));
|
||||
}
|
||||
|
||||
// Add to the hierarchy
|
||||
if (ParentSCSNode)
|
||||
{
|
||||
ParentSCSNode->AddChildNode(NewNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
SCS->AddNode(NewNode);
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added component '%s' (%s) to '%s' (parent: %s, saved: %s)"),
|
||||
*Component, *ComponentClassObj->GetName(), *Blueprint,
|
||||
ParentSCSNode ? *ParentComponent : TEXT("(root)"),
|
||||
bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("component"), NewNode->GetVariableName().ToString());
|
||||
Result->SetStringField(TEXT("componentClass"), ComponentClassObj->GetName());
|
||||
if (ParentSCSNode)
|
||||
{
|
||||
Result->SetStringField(TEXT("parentComponent"), ParentSCSNode->GetVariableName().ToString());
|
||||
}
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="remove_blueprint_component"))
|
||||
class UMCPHandler_RemoveBlueprintComponent : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Component to remove"))
|
||||
FString Component;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Remove a component from a Blueprint's SimpleConstructionScript.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
||||
if (!SCS)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
||||
*Blueprint));
|
||||
}
|
||||
|
||||
// Find the node to remove
|
||||
USCS_Node* NodeToRemove = nullptr;
|
||||
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
|
||||
for (USCS_Node* Node : AllNodes)
|
||||
{
|
||||
if (Node && Node->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase))
|
||||
{
|
||||
NodeToRemove = Node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!NodeToRemove)
|
||||
{
|
||||
// Build list of component names for the error message
|
||||
TArray<TSharedPtr<FJsonValue>> CompList;
|
||||
for (USCS_Node* Node : AllNodes)
|
||||
{
|
||||
if (Node)
|
||||
{
|
||||
CompList.Add(MakeShared<FJsonValueString>(Node->GetVariableName().ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Component '%s' not found in Blueprint '%s'"),
|
||||
*Component, *Blueprint));
|
||||
Result->SetArrayField(TEXT("existingComponents"), CompList);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent removing the root scene component if it has children
|
||||
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
|
||||
if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot remove component '%s' because it is a root component with %d child(ren). "
|
||||
"Remove or re-parent the children first."),
|
||||
*Component, NodeToRemove->GetChildNodes().Num()));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing component '%s' from Blueprint '%s'"),
|
||||
*Component, *Blueprint);
|
||||
|
||||
// Remove the node (promotes children to parent if it has any — but we've guarded root above)
|
||||
SCS->RemoveNodeAndPromoteChildren(NodeToRemove);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed component '%s' from '%s' (saved: %s)"),
|
||||
*Component, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("component"), Component);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
@@ -1,508 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.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 "K2Node.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
|
||||
// ============================================================
|
||||
// HandleGetPinInfo — detailed information about a specific pin
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleGetPinInfo(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString NodeId = Json->GetStringField(TEXT("nodeId"));
|
||||
FString PinName = Json->GetStringField(TEXT("pinName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || NodeId.IsEmpty() || PinName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId, pinName"));
|
||||
}
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
UEdGraph* Graph = nullptr;
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph);
|
||||
if (!Node)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
|
||||
}
|
||||
|
||||
UEdGraphPin* Pin = Node->FindPin(FName(*PinName));
|
||||
if (!Pin)
|
||||
{
|
||||
// List available pins
|
||||
TArray<TSharedPtr<FJsonValue>> AvailPins;
|
||||
for (UEdGraphPin* P : Node->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, *NodeId));
|
||||
Result->SetArrayField(TEXT("availablePins"), AvailPins);
|
||||
return;
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("nodeId"), NodeId);
|
||||
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
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleCheckPinCompatibility(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString SourceNodeId = Json->GetStringField(TEXT("sourceNodeId"));
|
||||
FString SourcePinName = Json->GetStringField(TEXT("sourcePinName"));
|
||||
FString TargetNodeId = Json->GetStringField(TEXT("targetNodeId"));
|
||||
FString TargetPinName = Json->GetStringField(TEXT("targetPinName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || SourceNodeId.IsEmpty() || SourcePinName.IsEmpty() ||
|
||||
TargetNodeId.IsEmpty() || TargetPinName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, sourceNodeId, sourcePinName, targetNodeId, targetPinName"));
|
||||
}
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
UEdGraph* SourceGraph = nullptr;
|
||||
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, SourceNodeId, &SourceGraph);
|
||||
if (!SourceNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNodeId));
|
||||
}
|
||||
|
||||
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, TargetNodeId);
|
||||
if (!TargetNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNodeId));
|
||||
}
|
||||
|
||||
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*SourcePinName));
|
||||
if (!SourcePin)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *SourcePinName, *SourceNodeId));
|
||||
}
|
||||
|
||||
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*TargetPinName));
|
||||
if (!TargetPin)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *TargetPinName, *TargetNodeId));
|
||||
}
|
||||
|
||||
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"), BlueprintName);
|
||||
|
||||
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
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleListClasses(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Filter = Json->GetStringField(TEXT("filter"));
|
||||
FString ParentClassName = Json->GetStringField(TEXT("parentClass"));
|
||||
int32 Limit = 100;
|
||||
if (Json->HasField(TEXT("limit")))
|
||||
{
|
||||
Limit = FMath::Clamp(Json->GetIntegerField(TEXT("limit")), 1, 500);
|
||||
}
|
||||
|
||||
UClass* ParentClass = nullptr;
|
||||
if (!ParentClassName.IsEmpty())
|
||||
{
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
if (It->GetName() == ParentClassName || It->GetName() == ParentClassName + TEXT("_C"))
|
||||
{
|
||||
ParentClass = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ParentClass)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Parent class '%s' not found"), *ParentClassName));
|
||||
}
|
||||
}
|
||||
|
||||
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 (ParentClass && !Class->IsChildOf(ParentClass)) 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
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleListFunctions(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString ClassName = Json->GetStringField(TEXT("className"));
|
||||
FString Filter = Json->GetStringField(TEXT("filter"));
|
||||
|
||||
if (ClassName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: className"));
|
||||
}
|
||||
|
||||
// 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
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleListProperties(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString ClassName = Json->GetStringField(TEXT("className"));
|
||||
FString Filter = Json->GetStringField(TEXT("filter"));
|
||||
|
||||
if (ClassName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: className"));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
#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);
|
||||
}
|
||||
};
|
||||
@@ -1,227 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "K2Node_FunctionEntry.h"
|
||||
#include "K2Node_EditablePinBase.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
|
||||
// ============================================================
|
||||
// HandleAddEventDispatcher — create a multicast delegate on a Blueprint
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleAddEventDispatcher(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString DispatcherName = Json->GetStringField(TEXT("dispatcherName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || DispatcherName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, dispatcherName"));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
FName DispatcherFName(*DispatcherName);
|
||||
|
||||
// Check for name uniqueness against existing variables
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == DispatcherFName)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A variable or dispatcher named '%s' already exists in Blueprint '%s'"),
|
||||
*DispatcherName, *BlueprintName));
|
||||
}
|
||||
}
|
||||
|
||||
// Check against existing graphs (functions, macros, etc.)
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Existing : AllGraphs)
|
||||
{
|
||||
if (Existing && Existing->GetName().Equals(DispatcherName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A graph named '%s' already exists in Blueprint '%s'"),
|
||||
*DispatcherName, *BlueprintName));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding event dispatcher '%s' to Blueprint '%s'"),
|
||||
*DispatcherName, *BlueprintName);
|
||||
|
||||
// Step 1: Add a member variable with PC_MCDelegate pin type
|
||||
FEdGraphPinType DelegateType;
|
||||
DelegateType.PinCategory = UEdGraphSchema_K2::PC_MCDelegate;
|
||||
bool bVarAdded = FBlueprintEditorUtils::AddMemberVariable(BP, DispatcherFName, DelegateType);
|
||||
if (!bVarAdded)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Failed to add delegate variable for '%s'"), *DispatcherName));
|
||||
}
|
||||
|
||||
// Step 2: Create the signature graph
|
||||
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
|
||||
|
||||
UEdGraph* SigGraph = FBlueprintEditorUtils::CreateNewGraph(BP, DispatcherFName,
|
||||
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
||||
if (!SigGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create delegate signature graph"));
|
||||
}
|
||||
|
||||
K2Schema->CreateDefaultNodesForGraph(*SigGraph);
|
||||
K2Schema->CreateFunctionGraphTerminators(*SigGraph, static_cast<UClass*>(nullptr));
|
||||
K2Schema->AddExtraFunctionFlags(SigGraph, FUNC_BlueprintCallable | FUNC_BlueprintEvent | FUNC_Public);
|
||||
K2Schema->MarkFunctionEntryAsEditable(SigGraph, true);
|
||||
|
||||
BP->DelegateSignatureGraphs.Add(SigGraph);
|
||||
|
||||
// Step 3: Add parameters if provided
|
||||
TArray<TSharedPtr<FJsonValue>> ParamsArr;
|
||||
if (Json->HasField(TEXT("parameters")))
|
||||
{
|
||||
ParamsArr = Json->GetArrayField(TEXT("parameters"));
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> AddedParamsJson;
|
||||
|
||||
if (ParamsArr.Num() > 0)
|
||||
{
|
||||
// Find the entry node in the signature graph
|
||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||
for (UEdGraphNode* Node : SigGraph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!EntryNode)
|
||||
{
|
||||
// Still save what we have
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
MCPUtils::SaveBlueprintPackage(BP);
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Event dispatcher created but entry node not found — parameters could not be added"));
|
||||
}
|
||||
|
||||
for (const TSharedPtr<FJsonValue>& ParamVal : ParamsArr)
|
||||
{
|
||||
if (!ParamVal.IsValid() || ParamVal->Type != EJson::Object) continue;
|
||||
TSharedPtr<FJsonObject> ParamObj = ParamVal->AsObject();
|
||||
|
||||
FString ParamName = ParamObj->GetStringField(TEXT("name"));
|
||||
FString ParamType = ParamObj->GetStringField(TEXT("type"));
|
||||
|
||||
if (ParamName.IsEmpty() || ParamType.IsEmpty()) continue;
|
||||
|
||||
FEdGraphPinType PinType;
|
||||
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
|
||||
return;
|
||||
|
||||
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
|
||||
|
||||
TSharedRef<FJsonObject> ParamJson = MakeShared<FJsonObject>();
|
||||
ParamJson->SetStringField(TEXT("name"), ParamName);
|
||||
ParamJson->SetStringField(TEXT("type"), ParamType);
|
||||
AddedParamsJson.Add(MakeShared<FJsonValueObject>(ParamJson));
|
||||
}
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added event dispatcher '%s' to '%s' with %d params (saved: %s)"),
|
||||
*DispatcherName, *BlueprintName, AddedParamsJson.Num(), bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("dispatcherName"), DispatcherName);
|
||||
Result->SetArrayField(TEXT("parameters"), AddedParamsJson);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleListEventDispatchers — list all event dispatchers on a Blueprint
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleListEventDispatchers(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
if (BlueprintName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
|
||||
}
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
TSet<FName> DelegateNameSet;
|
||||
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNameSet);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> DispatchersArr;
|
||||
|
||||
for (const FName& DelegateName : DelegateNameSet)
|
||||
{
|
||||
TSharedRef<FJsonObject> DispObj = MakeShared<FJsonObject>();
|
||||
DispObj->SetStringField(TEXT("name"), DelegateName.ToString());
|
||||
|
||||
// Get parameter info from the signature graph
|
||||
TArray<TSharedPtr<FJsonValue>> ParamsArr;
|
||||
|
||||
UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName);
|
||||
if (SigGraph)
|
||||
{
|
||||
for (UEdGraphNode* Node : SigGraph->Nodes)
|
||||
{
|
||||
UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node);
|
||||
if (!FE) continue;
|
||||
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
|
||||
{
|
||||
if (!PinInfo.IsValid()) continue;
|
||||
|
||||
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
||||
ParamObj->SetStringField(TEXT("name"), PinInfo->PinName.ToString());
|
||||
|
||||
// Build a human-readable type name from the pin type
|
||||
FString TypeStr = PinInfo->PinType.PinCategory.ToString();
|
||||
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
||||
{
|
||||
TypeStr = PinInfo->PinType.PinSubCategoryObject->GetName();
|
||||
}
|
||||
ParamObj->SetStringField(TEXT("type"), TypeStr);
|
||||
|
||||
ParamsArr.Add(MakeShared<FJsonValueObject>(ParamObj));
|
||||
}
|
||||
break; // only need the first entry node
|
||||
}
|
||||
}
|
||||
|
||||
DispObj->SetArrayField(TEXT("parameters"), ParamsArr);
|
||||
DispatchersArr.Add(MakeShared<FJsonValueObject>(DispObj));
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetNumberField(TEXT("count"), DispatchersArr.Num());
|
||||
Result->SetArrayField(TEXT("dispatchers"), DispatchersArr);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "K2Node_FunctionEntry.h"
|
||||
#include "K2Node_EditablePinBase.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "MCPHandlers_Dispatchers.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="add_event_dispatcher"))
|
||||
class UMCPHandler_AddEventDispatcher : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name for the new event dispatcher"))
|
||||
FString DispatcherName;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Array of parameter objects, each with 'name' and 'type' fields"))
|
||||
FMCPJsonArray Parameters;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Create a new multicast event dispatcher on a Blueprint, optionally with parameters.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
FName DispatcherFName(*DispatcherName);
|
||||
|
||||
// Check for name uniqueness against existing variables
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == DispatcherFName)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A variable or dispatcher named '%s' already exists in Blueprint '%s'"),
|
||||
*DispatcherName, *Blueprint));
|
||||
}
|
||||
}
|
||||
|
||||
// Check against existing graphs (functions, macros, etc.)
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Existing : AllGraphs)
|
||||
{
|
||||
if (Existing && Existing->GetName().Equals(DispatcherName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A graph named '%s' already exists in Blueprint '%s'"),
|
||||
*DispatcherName, *Blueprint));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding event dispatcher '%s' to Blueprint '%s'"),
|
||||
*DispatcherName, *Blueprint);
|
||||
|
||||
// Step 1: Add a member variable with PC_MCDelegate pin type
|
||||
FEdGraphPinType DelegateType;
|
||||
DelegateType.PinCategory = UEdGraphSchema_K2::PC_MCDelegate;
|
||||
bool bVarAdded = FBlueprintEditorUtils::AddMemberVariable(BP, DispatcherFName, DelegateType);
|
||||
if (!bVarAdded)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Failed to add delegate variable for '%s'"), *DispatcherName));
|
||||
}
|
||||
|
||||
// Step 2: Create the signature graph
|
||||
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
|
||||
|
||||
UEdGraph* SigGraph = FBlueprintEditorUtils::CreateNewGraph(BP, DispatcherFName,
|
||||
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
||||
if (!SigGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create delegate signature graph"));
|
||||
}
|
||||
|
||||
K2Schema->CreateDefaultNodesForGraph(*SigGraph);
|
||||
K2Schema->CreateFunctionGraphTerminators(*SigGraph, static_cast<UClass*>(nullptr));
|
||||
K2Schema->AddExtraFunctionFlags(SigGraph, FUNC_BlueprintCallable | FUNC_BlueprintEvent | FUNC_Public);
|
||||
K2Schema->MarkFunctionEntryAsEditable(SigGraph, true);
|
||||
|
||||
BP->DelegateSignatureGraphs.Add(SigGraph);
|
||||
|
||||
// Step 3: Add parameters if provided
|
||||
TArray<TSharedPtr<FJsonValue>> AddedParamsJson;
|
||||
|
||||
if (Parameters.Array.Num() > 0)
|
||||
{
|
||||
// Find the entry node in the signature graph
|
||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||
for (UEdGraphNode* Node : SigGraph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!EntryNode)
|
||||
{
|
||||
// Still save what we have
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
MCPUtils::SaveBlueprintPackage(BP);
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Event dispatcher created but entry node not found — parameters could not be added"));
|
||||
}
|
||||
|
||||
for (const TSharedPtr<FJsonValue>& ParamVal : Parameters.Array)
|
||||
{
|
||||
if (!ParamVal.IsValid() || ParamVal->Type != EJson::Object) continue;
|
||||
TSharedPtr<FJsonObject> ParamObj = ParamVal->AsObject();
|
||||
|
||||
FString ParamName = ParamObj->GetStringField(TEXT("name"));
|
||||
FString ParamType = ParamObj->GetStringField(TEXT("type"));
|
||||
|
||||
if (ParamName.IsEmpty() || ParamType.IsEmpty()) continue;
|
||||
|
||||
FEdGraphPinType PinType;
|
||||
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
|
||||
return;
|
||||
|
||||
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
|
||||
|
||||
TSharedRef<FJsonObject> ParamJson = MakeShared<FJsonObject>();
|
||||
ParamJson->SetStringField(TEXT("name"), ParamName);
|
||||
ParamJson->SetStringField(TEXT("type"), ParamType);
|
||||
AddedParamsJson.Add(MakeShared<FJsonValueObject>(ParamJson));
|
||||
}
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added event dispatcher '%s' to '%s' with %d params (saved: %s)"),
|
||||
*DispatcherName, *Blueprint, AddedParamsJson.Num(), bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("dispatcherName"), DispatcherName);
|
||||
Result->SetArrayField(TEXT("parameters"), AddedParamsJson);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="list_event_dispatchers"))
|
||||
class UMCPHandler_ListEventDispatchers : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("List all event dispatchers on a Blueprint, including their parameter signatures.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
TSet<FName> DelegateNameSet;
|
||||
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNameSet);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> DispatchersArr;
|
||||
|
||||
for (const FName& DelegateName : DelegateNameSet)
|
||||
{
|
||||
TSharedRef<FJsonObject> DispObj = MakeShared<FJsonObject>();
|
||||
DispObj->SetStringField(TEXT("name"), DelegateName.ToString());
|
||||
|
||||
// Get parameter info from the signature graph
|
||||
TArray<TSharedPtr<FJsonValue>> ParamsArr;
|
||||
|
||||
UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName);
|
||||
if (SigGraph)
|
||||
{
|
||||
for (UEdGraphNode* Node : SigGraph->Nodes)
|
||||
{
|
||||
UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node);
|
||||
if (!FE) continue;
|
||||
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
|
||||
{
|
||||
if (!PinInfo.IsValid()) continue;
|
||||
|
||||
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
||||
ParamObj->SetStringField(TEXT("name"), PinInfo->PinName.ToString());
|
||||
|
||||
// Build a human-readable type name from the pin type
|
||||
FString TypeStr = PinInfo->PinType.PinCategory.ToString();
|
||||
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
||||
{
|
||||
TypeStr = PinInfo->PinType.PinSubCategoryObject->GetName();
|
||||
}
|
||||
ParamObj->SetStringField(TEXT("type"), TypeStr);
|
||||
|
||||
ParamsArr.Add(MakeShared<FJsonValueObject>(ParamObj));
|
||||
}
|
||||
break; // only need the first entry node
|
||||
}
|
||||
}
|
||||
|
||||
DispObj->SetArrayField(TEXT("parameters"), ParamsArr);
|
||||
DispatchersArr.Add(MakeShared<FJsonValueObject>(DispObj));
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetNumberField(TEXT("count"), DispatchersArr.Num());
|
||||
Result->SetArrayField(TEXT("dispatchers"), DispatchersArr);
|
||||
}
|
||||
};
|
||||
@@ -1,570 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Engine/World.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "EdGraphSchema_K2.h"
|
||||
#include "K2Node_CustomEvent.h"
|
||||
#include "K2Node_FunctionEntry.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
|
||||
// ============================================================
|
||||
// HandleReparentBlueprint — change a Blueprint's parent class
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleReparentBlueprint(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString NewParentName = Json->GetStringField(TEXT("newParentClass"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || NewParentName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, newParentClass"));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None");
|
||||
|
||||
// Find the new parent class
|
||||
// Try C++ class first (e.g. "WebUIHUD" finds /Script/ModuleName.WebUIHUD)
|
||||
UClass* NewParentClass = nullptr;
|
||||
|
||||
// Search across all packages for native classes
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
if (It->GetName() == NewParentName)
|
||||
{
|
||||
NewParentClass = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found as C++ class, try loading as a Blueprint asset
|
||||
if (!NewParentClass)
|
||||
{
|
||||
FString ParentLoadError;
|
||||
UBlueprint* ParentBP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(NewParentName, ParentLoadError);
|
||||
if (ParentBP && ParentBP->GeneratedClass)
|
||||
{
|
||||
NewParentClass = ParentBP->GeneratedClass;
|
||||
}
|
||||
}
|
||||
|
||||
if (!NewParentClass)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Could not find class '%s'. Provide a C++ class name (e.g. 'WebUIHUD') or Blueprint name."),
|
||||
*NewParentName));
|
||||
}
|
||||
|
||||
// Validate: new parent must be compatible
|
||||
if (BP->ParentClass && !NewParentClass->IsChildOf(BP->ParentClass->GetSuperClass()) &&
|
||||
BP->ParentClass != NewParentClass)
|
||||
{
|
||||
// Just warn, don't block — the user may intentionally reparent to a sibling
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s' — classes are not in a direct hierarchy"),
|
||||
*BlueprintName, *OldParentName, *NewParentClass->GetName());
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s'"),
|
||||
*BlueprintName, *OldParentName, *NewParentClass->GetName());
|
||||
|
||||
// Perform reparent
|
||||
BP->ParentClass = NewParentClass;
|
||||
|
||||
// Refresh all nodes to pick up new parent's functions/variables
|
||||
FBlueprintEditorUtils::RefreshAllNodes(BP);
|
||||
|
||||
// Compile
|
||||
FKismetEditorUtilities::CompileBlueprint(BP);
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
FString NewParentActualName = NewParentClass->GetName();
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparent complete, save %s"),
|
||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("oldParentClass"), OldParentName);
|
||||
Result->SetStringField(TEXT("newParentClass"), NewParentActualName);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleCreateBlueprint — create a new Blueprint asset
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleCreateBlueprint(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprintName"));
|
||||
FString PackagePath = Json->GetStringField(TEXT("packagePath"));
|
||||
FString ParentClassName = Json->GetStringField(TEXT("parentClass"));
|
||||
FString BlueprintTypeStr = Json->GetStringField(TEXT("blueprintType"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || PackagePath.IsEmpty() || ParentClassName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprintName, packagePath, parentClass"));
|
||||
}
|
||||
|
||||
// Validate packagePath starts with /Game
|
||||
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||
}
|
||||
|
||||
// Check if asset already exists
|
||||
FString FullAssetPath = PackagePath / BlueprintName;
|
||||
if (UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), BlueprintName) || UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), FullAssetPath))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Blueprint '%s' already exists. Use a different name or delete the existing asset first."),
|
||||
*BlueprintName));
|
||||
}
|
||||
|
||||
// Resolve parent class — try C++ class first, then Blueprint
|
||||
UClass* ParentClass = nullptr;
|
||||
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
if (It->GetName() == ParentClassName)
|
||||
{
|
||||
ParentClass = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentClass)
|
||||
{
|
||||
FString ParentLoadError;
|
||||
UBlueprint* ParentBP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(ParentClassName, ParentLoadError);
|
||||
if (ParentBP && ParentBP->GeneratedClass)
|
||||
{
|
||||
ParentClass = ParentBP->GeneratedClass;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentClass)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint name."),
|
||||
*ParentClassName));
|
||||
}
|
||||
|
||||
// Map blueprintType string to EBlueprintType
|
||||
EBlueprintType BlueprintType = BPTYPE_Normal;
|
||||
if (!BlueprintTypeStr.IsEmpty())
|
||||
{
|
||||
if (BlueprintTypeStr == TEXT("Interface"))
|
||||
{
|
||||
BlueprintType = BPTYPE_Interface;
|
||||
}
|
||||
else if (BlueprintTypeStr == TEXT("FunctionLibrary"))
|
||||
{
|
||||
BlueprintType = BPTYPE_FunctionLibrary;
|
||||
}
|
||||
else if (BlueprintTypeStr == TEXT("MacroLibrary"))
|
||||
{
|
||||
BlueprintType = BPTYPE_MacroLibrary;
|
||||
}
|
||||
else if (BlueprintTypeStr != TEXT("Normal"))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Invalid blueprintType '%s'. Valid values: Normal, Interface, FunctionLibrary, MacroLibrary"),
|
||||
*BlueprintTypeStr));
|
||||
}
|
||||
}
|
||||
|
||||
// For Interface type, parent must be UInterface
|
||||
if (BlueprintType == BPTYPE_Interface && !ParentClass->IsChildOf(UInterface::StaticClass()))
|
||||
{
|
||||
// Use the engine's standard BlueprintInterface parent
|
||||
ParentClass = UInterface::StaticClass();
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blueprint '%s' in '%s' with parent '%s' (type=%s)"),
|
||||
*BlueprintName, *PackagePath, *ParentClass->GetName(), *BlueprintTypeStr);
|
||||
|
||||
// Create the package
|
||||
FString FullPackagePath = PackagePath / BlueprintName;
|
||||
UPackage* Package = CreatePackage(*FullPackagePath);
|
||||
if (!Package)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
|
||||
}
|
||||
|
||||
// Create the Blueprint
|
||||
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(
|
||||
ParentClass,
|
||||
Package,
|
||||
FName(*BlueprintName),
|
||||
BlueprintType,
|
||||
UBlueprint::StaticClass(),
|
||||
UBlueprintGeneratedClass::StaticClass()
|
||||
);
|
||||
|
||||
if (!NewBP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null"));
|
||||
}
|
||||
|
||||
// Compile
|
||||
FKismetEditorUtilities::CompileBlueprint(NewBP);
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP);
|
||||
|
||||
|
||||
// Collect graph names
|
||||
TArray<TSharedPtr<FJsonValue>> GraphNames;
|
||||
for (UEdGraph* Graph : NewBP->UbergraphPages)
|
||||
{
|
||||
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
}
|
||||
for (UEdGraph* Graph : NewBP->FunctionGraphs)
|
||||
{
|
||||
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
}
|
||||
for (UEdGraph* Graph : NewBP->MacroGraphs)
|
||||
{
|
||||
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"),
|
||||
*BlueprintName, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprintName"), BlueprintName);
|
||||
Result->SetStringField(TEXT("packagePath"), PackagePath);
|
||||
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
|
||||
Result->SetStringField(TEXT("parentClass"), ParentClass->GetName());
|
||||
Result->SetStringField(TEXT("blueprintType"), BlueprintTypeStr.IsEmpty() ? TEXT("Normal") : BlueprintTypeStr);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
Result->SetArrayField(TEXT("graphs"), GraphNames);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleCreateGraph — create a new function, macro, or custom event graph
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleCreateGraph(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graphName"));
|
||||
FString GraphType = Json->GetStringField(TEXT("graphType"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || GraphType.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graphName, graphType"));
|
||||
}
|
||||
|
||||
if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent"))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Invalid graphType '%s'. Valid values: function, macro, customEvent"), *GraphType));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Check graph name uniqueness
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Existing : AllGraphs)
|
||||
{
|
||||
if (Existing && Existing->GetName().Equals(GraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *GraphName, *BlueprintName));
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for existing custom events with the same name
|
||||
if (GraphType == TEXT("customEvent"))
|
||||
{
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
if (CE->CustomFunctionName == FName(*GraphName))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *GraphName, *BlueprintName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating %s graph '%s' in Blueprint '%s'"),
|
||||
*GraphType, *GraphName, *BlueprintName);
|
||||
|
||||
FString CreatedNodeId;
|
||||
|
||||
if (GraphType == TEXT("function"))
|
||||
{
|
||||
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*GraphName),
|
||||
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
||||
if (!NewGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create function graph"));
|
||||
}
|
||||
FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast<UClass*>(nullptr));
|
||||
}
|
||||
else if (GraphType == TEXT("macro"))
|
||||
{
|
||||
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*GraphName),
|
||||
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
||||
if (!NewGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create macro graph"));
|
||||
}
|
||||
FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr);
|
||||
}
|
||||
else // customEvent
|
||||
{
|
||||
// Find the EventGraph (first UbergraphPage)
|
||||
UEdGraph* EventGraph = nullptr;
|
||||
if (BP->UbergraphPages.Num() > 0)
|
||||
{
|
||||
EventGraph = BP->UbergraphPages[0];
|
||||
}
|
||||
if (!EventGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no EventGraph to add a custom event to"));
|
||||
}
|
||||
|
||||
// Create a custom event node in the EventGraph
|
||||
UK2Node_CustomEvent* NewEvent = NewObject<UK2Node_CustomEvent>(EventGraph);
|
||||
NewEvent->CustomFunctionName = FName(*GraphName);
|
||||
NewEvent->bIsEditable = true;
|
||||
EventGraph->AddNode(NewEvent, /*bFromUI=*/false, /*bSelectNewNode=*/false);
|
||||
NewEvent->CreateNewGuid();
|
||||
NewEvent->PostPlacedNewNode();
|
||||
NewEvent->AllocateDefaultPins();
|
||||
CreatedNodeId = NewEvent->NodeGuid.ToString();
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created %s graph '%s' in '%s' (saved: %s)"),
|
||||
*GraphType, *GraphName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("graphName"), GraphName);
|
||||
Result->SetStringField(TEXT("graphType"), GraphType);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
if (!CreatedNodeId.IsEmpty())
|
||||
{
|
||||
Result->SetStringField(TEXT("nodeId"), CreatedNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleDeleteGraph — delete a function or macro graph
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleDeleteGraph(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graphName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graphName"));
|
||||
}
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find the graph
|
||||
UEdGraph* TargetGraph = nullptr;
|
||||
FString GraphType;
|
||||
|
||||
for (UEdGraph* Graph : BP->FunctionGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = Graph;
|
||||
GraphType = TEXT("function");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!TargetGraph)
|
||||
{
|
||||
for (UEdGraph* Graph : BP->MacroGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = Graph;
|
||||
GraphType = TEXT("macro");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's an UbergraphPage (EventGraph) — disallow deletion
|
||||
if (!TargetGraph)
|
||||
{
|
||||
for (UEdGraph* Graph : BP->UbergraphPages)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."),
|
||||
*GraphName));
|
||||
}
|
||||
}
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *GraphName, *BlueprintName));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting %s graph '%s' from Blueprint '%s'"),
|
||||
*GraphType, *GraphName, *BlueprintName);
|
||||
|
||||
// Count nodes for reporting
|
||||
int32 NodeCount = TargetGraph->Nodes.Num();
|
||||
|
||||
// Remove the graph
|
||||
FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted graph '%s' (%d nodes), save %s"),
|
||||
*GraphName, NodeCount, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("graphName"), GraphName);
|
||||
Result->SetStringField(TEXT("graphType"), GraphType);
|
||||
Result->SetNumberField(TEXT("nodeCount"), NodeCount);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleRenameGraph — rename a function or macro graph
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleRenameGraph(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graphName"));
|
||||
FString NewName = Json->GetStringField(TEXT("newName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || NewName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graphName, newName"));
|
||||
}
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Check if it's an UbergraphPage — disallow rename
|
||||
for (UEdGraph* Graph : BP->UbergraphPages)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."),
|
||||
*GraphName));
|
||||
}
|
||||
}
|
||||
|
||||
// Find the graph in FunctionGraphs or MacroGraphs
|
||||
UEdGraph* TargetGraph = nullptr;
|
||||
FString GraphType;
|
||||
|
||||
for (UEdGraph* Graph : BP->FunctionGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = Graph;
|
||||
GraphType = TEXT("function");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!TargetGraph)
|
||||
{
|
||||
for (UEdGraph* Graph : BP->MacroGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = Graph;
|
||||
GraphType = TEXT("macro");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!TargetGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *GraphName, *BlueprintName));
|
||||
}
|
||||
|
||||
// Check for name collision
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Existing : AllGraphs)
|
||||
{
|
||||
if (Existing && Existing != TargetGraph && Existing->GetName().Equals(NewName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *BlueprintName));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming %s graph '%s' to '%s' in Blueprint '%s'"),
|
||||
*GraphType, *GraphName, *NewName, *BlueprintName);
|
||||
|
||||
FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renamed graph '%s' to '%s', save %s"),
|
||||
*GraphName, *NewName, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("oldName"), GraphName);
|
||||
Result->SetStringField(TEXT("newName"), TargetGraph->GetName());
|
||||
Result->SetStringField(TEXT("graphType"), GraphType);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
#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 "EdGraphSchema_K2.h"
|
||||
#include "K2Node_CustomEvent.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
#include "MCPHandlers_Graphs.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="reparent_blueprint"))
|
||||
class UMCPHandler_ReparentBlueprint : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the new parent class (C++ class name or Blueprint name)"))
|
||||
FString NewParentClass;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Change a Blueprint's parent class. Accepts C++ class names or Blueprint names.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None");
|
||||
|
||||
// Find the new parent class
|
||||
// Try C++ class first (e.g. "WebUIHUD" finds /Script/ModuleName.WebUIHUD)
|
||||
UClass* NewParentClassObj = nullptr;
|
||||
|
||||
// Search across all packages for native classes
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
if (It->GetName() == NewParentClass)
|
||||
{
|
||||
NewParentClassObj = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found as C++ class, try loading as a Blueprint asset
|
||||
if (!NewParentClassObj)
|
||||
{
|
||||
FString ParentLoadError;
|
||||
UBlueprint* ParentBP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(NewParentClass, ParentLoadError);
|
||||
if (ParentBP && ParentBP->GeneratedClass)
|
||||
{
|
||||
NewParentClassObj = ParentBP->GeneratedClass;
|
||||
}
|
||||
}
|
||||
|
||||
if (!NewParentClassObj)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Could not find class '%s'. Provide a C++ class name (e.g. 'WebUIHUD') or Blueprint name."),
|
||||
*NewParentClass));
|
||||
}
|
||||
|
||||
// Validate: new parent must be compatible
|
||||
if (BP->ParentClass && !NewParentClassObj->IsChildOf(BP->ParentClass->GetSuperClass()) &&
|
||||
BP->ParentClass != NewParentClassObj)
|
||||
{
|
||||
// Just warn, don't block — the user may intentionally reparent to a sibling
|
||||
UE_LOG(LogTemp, Warning,
|
||||
TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s' — classes are not in a direct hierarchy"),
|
||||
*Blueprint, *OldParentName, *NewParentClassObj->GetName());
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s'"),
|
||||
*Blueprint, *OldParentName, *NewParentClassObj->GetName());
|
||||
|
||||
// Perform reparent
|
||||
BP->ParentClass = NewParentClassObj;
|
||||
|
||||
// Refresh all nodes to pick up new parent's functions/variables
|
||||
FBlueprintEditorUtils::RefreshAllNodes(BP);
|
||||
|
||||
// Compile
|
||||
FKismetEditorUtilities::CompileBlueprint(BP);
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
FString NewParentActualName = NewParentClassObj->GetName();
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparent complete, save %s"),
|
||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("oldParentClass"), OldParentName);
|
||||
Result->SetStringField(TEXT("newParentClass"), NewParentActualName);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="create_blueprint_asset"))
|
||||
class UMCPHandler_CreateBlueprint : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="New Blueprint asset name"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
||||
FString PackagePath;
|
||||
|
||||
UPROPERTY(meta=(Description="Parent class name (C++ class name or Blueprint name)"))
|
||||
FString ParentClass;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Blueprint type: Normal, Interface, FunctionLibrary, or MacroLibrary (default: Normal)"))
|
||||
FString BlueprintType;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Create a new Blueprint asset with a specified parent class and type.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Validate packagePath starts with /Game
|
||||
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||
}
|
||||
|
||||
// Check if asset already exists
|
||||
FString FullAssetPath = PackagePath / Blueprint;
|
||||
if (UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), Blueprint) || UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), FullAssetPath))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Blueprint '%s' already exists. Use a different name or delete the existing asset first."),
|
||||
*Blueprint));
|
||||
}
|
||||
|
||||
// Resolve parent class — try C++ class first, then Blueprint
|
||||
UClass* ParentClassObj = nullptr;
|
||||
|
||||
for (TObjectIterator<UClass> It; It; ++It)
|
||||
{
|
||||
if (It->GetName() == ParentClass)
|
||||
{
|
||||
ParentClassObj = *It;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentClassObj)
|
||||
{
|
||||
FString ParentLoadError;
|
||||
UBlueprint* ParentBP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(ParentClass, ParentLoadError);
|
||||
if (ParentBP && ParentBP->GeneratedClass)
|
||||
{
|
||||
ParentClassObj = ParentBP->GeneratedClass;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentClassObj)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint name."),
|
||||
*ParentClass));
|
||||
}
|
||||
|
||||
// Map blueprintType string to EBlueprintType
|
||||
EBlueprintType BlueprintTypeEnum = BPTYPE_Normal;
|
||||
if (!BlueprintType.IsEmpty())
|
||||
{
|
||||
if (BlueprintType == TEXT("Interface"))
|
||||
{
|
||||
BlueprintTypeEnum = BPTYPE_Interface;
|
||||
}
|
||||
else if (BlueprintType == TEXT("FunctionLibrary"))
|
||||
{
|
||||
BlueprintTypeEnum = BPTYPE_FunctionLibrary;
|
||||
}
|
||||
else if (BlueprintType == TEXT("MacroLibrary"))
|
||||
{
|
||||
BlueprintTypeEnum = BPTYPE_MacroLibrary;
|
||||
}
|
||||
else if (BlueprintType != TEXT("Normal"))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Invalid blueprintType '%s'. Valid values: Normal, Interface, FunctionLibrary, MacroLibrary"),
|
||||
*BlueprintType));
|
||||
}
|
||||
}
|
||||
|
||||
// For Interface type, parent must be UInterface
|
||||
if ((BlueprintTypeEnum == BPTYPE_Interface) && !ParentClassObj->IsChildOf(UInterface::StaticClass()))
|
||||
{
|
||||
// Use the engine's standard BlueprintInterface parent
|
||||
ParentClassObj = UInterface::StaticClass();
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blueprint '%s' in '%s' with parent '%s' (type=%s)"),
|
||||
*Blueprint, *PackagePath, *ParentClassObj->GetName(), *BlueprintType);
|
||||
|
||||
// Create the package
|
||||
FString FullPackagePath = PackagePath / Blueprint;
|
||||
UPackage* Package = CreatePackage(*FullPackagePath);
|
||||
if (!Package)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
|
||||
}
|
||||
|
||||
// Create the Blueprint
|
||||
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(
|
||||
ParentClassObj,
|
||||
Package,
|
||||
FName(*Blueprint),
|
||||
BlueprintTypeEnum,
|
||||
UBlueprint::StaticClass(),
|
||||
UBlueprintGeneratedClass::StaticClass()
|
||||
);
|
||||
|
||||
if (!NewBP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null"));
|
||||
}
|
||||
|
||||
// Compile
|
||||
FKismetEditorUtilities::CompileBlueprint(NewBP);
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP);
|
||||
|
||||
|
||||
// Collect graph names
|
||||
TArray<TSharedPtr<FJsonValue>> GraphNames;
|
||||
for (UEdGraph* Graph : NewBP->UbergraphPages)
|
||||
{
|
||||
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
}
|
||||
for (UEdGraph* Graph : NewBP->FunctionGraphs)
|
||||
{
|
||||
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
}
|
||||
for (UEdGraph* Graph : NewBP->MacroGraphs)
|
||||
{
|
||||
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"),
|
||||
*Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprintName"), Blueprint);
|
||||
Result->SetStringField(TEXT("packagePath"), PackagePath);
|
||||
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
|
||||
Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName());
|
||||
Result->SetStringField(TEXT("blueprintType"), BlueprintType.IsEmpty() ? TEXT("Normal") : *BlueprintType);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
Result->SetArrayField(TEXT("graphs"), GraphNames);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="create_blueprint_graph"))
|
||||
class UMCPHandler_CreateGraph : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name for the new graph"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Type of graph: function, macro, or customEvent"))
|
||||
FString GraphType;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Create a new function, macro, or custom event graph in a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent"))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Invalid graphType '%s'. Valid values: function, macro, customEvent"), *GraphType));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Check graph name uniqueness
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Existing : AllGraphs)
|
||||
{
|
||||
if (Existing && Existing->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint));
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for existing custom events with the same name
|
||||
if (GraphType == TEXT("customEvent"))
|
||||
{
|
||||
for (UEdGraph* ExistingGraph : AllGraphs)
|
||||
{
|
||||
if (!ExistingGraph) continue;
|
||||
for (UEdGraphNode* Node : ExistingGraph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
if (CE->CustomFunctionName == FName(*Graph))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating %s graph '%s' in Blueprint '%s'"),
|
||||
*GraphType, *Graph, *Blueprint);
|
||||
|
||||
FString CreatedNodeId;
|
||||
|
||||
if (GraphType == TEXT("function"))
|
||||
{
|
||||
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
|
||||
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
||||
if (!NewGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create function graph"));
|
||||
}
|
||||
FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast<UClass*>(nullptr));
|
||||
}
|
||||
else if (GraphType == TEXT("macro"))
|
||||
{
|
||||
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
|
||||
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
||||
if (!NewGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create macro graph"));
|
||||
}
|
||||
FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr);
|
||||
}
|
||||
else // customEvent
|
||||
{
|
||||
// Find the EventGraph (first UbergraphPage)
|
||||
UEdGraph* EventGraph = nullptr;
|
||||
if (BP->UbergraphPages.Num() > 0)
|
||||
{
|
||||
EventGraph = BP->UbergraphPages[0];
|
||||
}
|
||||
if (!EventGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no EventGraph to add a custom event to"));
|
||||
}
|
||||
|
||||
// Create a custom event node in the EventGraph
|
||||
UK2Node_CustomEvent* NewEvent = NewObject<UK2Node_CustomEvent>(EventGraph);
|
||||
NewEvent->CustomFunctionName = FName(*Graph);
|
||||
NewEvent->bIsEditable = true;
|
||||
EventGraph->AddNode(NewEvent, /*bFromUI=*/false, /*bSelectNewNode=*/false);
|
||||
NewEvent->CreateNewGuid();
|
||||
NewEvent->PostPlacedNewNode();
|
||||
NewEvent->AllocateDefaultPins();
|
||||
CreatedNodeId = NewEvent->NodeGuid.ToString();
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created %s graph '%s' in '%s' (saved: %s)"),
|
||||
*GraphType, *Graph, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("graphName"), Graph);
|
||||
Result->SetStringField(TEXT("graphType"), GraphType);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
if (!CreatedNodeId.IsEmpty())
|
||||
{
|
||||
Result->SetStringField(TEXT("nodeId"), CreatedNodeId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="delete_blueprint_graph"))
|
||||
class UMCPHandler_DeleteGraph : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the graph to delete"))
|
||||
FString Graph;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Delete a function or macro graph from a Blueprint. Cannot delete EventGraph pages.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find the graph
|
||||
UEdGraph* TargetGraph = nullptr;
|
||||
FString GraphType;
|
||||
|
||||
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
|
||||
{
|
||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = CandidateGraph;
|
||||
GraphType = TEXT("function");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!TargetGraph)
|
||||
{
|
||||
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
|
||||
{
|
||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = CandidateGraph;
|
||||
GraphType = TEXT("macro");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's an UbergraphPage (EventGraph) — disallow deletion
|
||||
if (!TargetGraph)
|
||||
{
|
||||
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
|
||||
{
|
||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."),
|
||||
*Graph));
|
||||
}
|
||||
}
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting %s graph '%s' from Blueprint '%s'"),
|
||||
*GraphType, *Graph, *Blueprint);
|
||||
|
||||
// Count nodes for reporting
|
||||
int32 NodeCount = TargetGraph->Nodes.Num();
|
||||
|
||||
// Remove the graph
|
||||
FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted graph '%s' (%d nodes), save %s"),
|
||||
*Graph, NodeCount, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("graphName"), Graph);
|
||||
Result->SetStringField(TEXT("graphType"), GraphType);
|
||||
Result->SetNumberField(TEXT("nodeCount"), NodeCount);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="rename_blueprint_graph"))
|
||||
class UMCPHandler_RenameGraph : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Current name of the graph to rename"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="New name for the graph"))
|
||||
FString NewName;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Rename a function or macro graph in a Blueprint. Cannot rename EventGraph pages.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Check if it's an UbergraphPage — disallow rename
|
||||
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
|
||||
{
|
||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."),
|
||||
*Graph));
|
||||
}
|
||||
}
|
||||
|
||||
// Find the graph in FunctionGraphs or MacroGraphs
|
||||
UEdGraph* TargetGraph = nullptr;
|
||||
FString GraphType;
|
||||
|
||||
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
|
||||
{
|
||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = CandidateGraph;
|
||||
GraphType = TEXT("function");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!TargetGraph)
|
||||
{
|
||||
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
|
||||
{
|
||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TargetGraph = CandidateGraph;
|
||||
GraphType = TEXT("macro");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!TargetGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint));
|
||||
}
|
||||
|
||||
// Check for name collision
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Existing : AllGraphs)
|
||||
{
|
||||
if (Existing && Existing != TargetGraph && Existing->GetName().Equals(NewName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *Blueprint));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming %s graph '%s' to '%s' in Blueprint '%s'"),
|
||||
*GraphType, *Graph, *NewName, *Blueprint);
|
||||
|
||||
FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renamed graph '%s' to '%s', save %s"),
|
||||
*Graph, *NewName, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("oldName"), Graph);
|
||||
Result->SetStringField(TEXT("newName"), TargetGraph->GetName());
|
||||
Result->SetStringField(TEXT("graphType"), GraphType);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
@@ -1,695 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Materials/MaterialInstanceConstant.h"
|
||||
#include "Materials/MaterialExpressionScalarParameter.h"
|
||||
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
||||
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
||||
#include "Factories/MaterialInstanceConstantFactoryNew.h"
|
||||
#include "AssetToolsModule.h"
|
||||
#include "IAssetTools.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
#include "Engine/Texture.h"
|
||||
|
||||
// ============================================================
|
||||
// HandleCreateMaterialInstance — create a new Material Instance Constant
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleCreateMaterialInstance(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Name = Json->GetStringField(TEXT("name"));
|
||||
FString PackagePath = Json->GetStringField(TEXT("packagePath"));
|
||||
FString ParentMaterialName = Json->GetStringField(TEXT("parentMaterial"));
|
||||
|
||||
if (Name.IsEmpty() || PackagePath.IsEmpty() || ParentMaterialName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, parentMaterial"));
|
||||
}
|
||||
|
||||
// Validate packagePath starts with /Game
|
||||
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||
}
|
||||
|
||||
// Check if asset already exists
|
||||
FString FullAssetPath = PackagePath / Name;
|
||||
if (UMCPAssetFinder::FindAsset(UMaterialInstanceConstant::StaticClass(), Name) || UMCPAssetFinder::FindAsset(UMaterialInstanceConstant::StaticClass(), FullAssetPath))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Material Instance '%s' already exists. Use a different name or delete the existing asset first."),
|
||||
*Name));
|
||||
}
|
||||
|
||||
// Load parent material — try as Material first, then as Material Instance
|
||||
UMaterialInterface* ParentMaterial = nullptr;
|
||||
{
|
||||
FString LoadError;
|
||||
UMaterial* ParentMat = UMCPAssetFinder::LoadAsset<UMaterial>(ParentMaterialName, LoadError);
|
||||
if (ParentMat)
|
||||
{
|
||||
ParentMaterial = ParentMat;
|
||||
}
|
||||
else
|
||||
{
|
||||
FString MILoadError;
|
||||
UMaterialInstanceConstant* ParentMI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(ParentMaterialName, MILoadError);
|
||||
if (ParentMI)
|
||||
{
|
||||
ParentMaterial = ParentMI;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentMaterial)
|
||||
{
|
||||
// Also try LoadObject as a fallback with the raw path
|
||||
ParentMaterial = LoadObject<UMaterialInterface>(nullptr, *ParentMaterialName);
|
||||
}
|
||||
|
||||
if (!ParentMaterial)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parent material '%s' not found. Provide a Material or Material Instance name/path."),
|
||||
*ParentMaterialName));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Instance '%s' in '%s' with parent '%s'"),
|
||||
*Name, *PackagePath, *ParentMaterial->GetName());
|
||||
|
||||
// Create via factory + AssetTools
|
||||
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
||||
UMaterialInstanceConstantFactoryNew* Factory = NewObject<UMaterialInstanceConstantFactoryNew>();
|
||||
|
||||
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory);
|
||||
if (!NewAsset)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Instance asset '%s' in '%s'"), *Name, *PackagePath));
|
||||
}
|
||||
|
||||
UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(NewAsset);
|
||||
if (!MI)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialInstanceConstant"));
|
||||
}
|
||||
|
||||
// Set parent
|
||||
MI->PreEditChange(nullptr);
|
||||
MI->Parent = ParentMaterial;
|
||||
MI->PostEditChange();
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveGenericPackage(MI);
|
||||
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Instance '%s' with parent '%s' (saved: %s)"),
|
||||
*Name, *ParentMaterial->GetName(), bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("name"), Name);
|
||||
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
||||
Result->SetStringField(TEXT("parent"), ParentMaterial->GetPathName());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleSetMaterialInstanceParameter — set a parameter override on an MI
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleSetMaterialInstanceParameter(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString MIName = Json->GetStringField(TEXT("materialInstance"));
|
||||
FString ParamName = Json->GetStringField(TEXT("parameterName"));
|
||||
|
||||
if (MIName.IsEmpty() || ParamName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: materialInstance, parameterName"));
|
||||
}
|
||||
|
||||
if (!Json->HasField(TEXT("value")))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
|
||||
}
|
||||
|
||||
bool bDryRun = false;
|
||||
if (Json->HasField(TEXT("dryRun")))
|
||||
{
|
||||
bDryRun = Json->GetBoolField(TEXT("dryRun"));
|
||||
}
|
||||
|
||||
// Load the Material Instance
|
||||
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MIName, Result);
|
||||
if (!MI) return;
|
||||
|
||||
// Determine the parameter type — explicit or auto-detect from parent
|
||||
FString TypeStr;
|
||||
if (Json->HasField(TEXT("type")))
|
||||
{
|
||||
TypeStr = Json->GetStringField(TEXT("type"));
|
||||
}
|
||||
|
||||
// Auto-detect type from parent material's parameters if not provided
|
||||
if (TypeStr.IsEmpty())
|
||||
{
|
||||
UMaterialInterface* ParentMat = MI->Parent;
|
||||
while (ParentMat)
|
||||
{
|
||||
UMaterial* BaseMat = ParentMat->GetMaterial();
|
||||
if (BaseMat)
|
||||
{
|
||||
// Check scalar parameters
|
||||
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
|
||||
{
|
||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||
{
|
||||
if (SP->ParameterName.ToString() == ParamName)
|
||||
{
|
||||
TypeStr = TEXT("scalar");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||
{
|
||||
if (VP->ParameterName.ToString() == ParamName)
|
||||
{
|
||||
TypeStr = TEXT("vector");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||
{
|
||||
if (TP->ParameterName.ToString() == ParamName)
|
||||
{
|
||||
TypeStr = TEXT("texture");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||
{
|
||||
if (SSP->ParameterName.ToString() == ParamName)
|
||||
{
|
||||
TypeStr = TEXT("staticSwitch");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // Only need to check the base material
|
||||
}
|
||||
// Walk up the parent chain if it's an MI parented to another MI
|
||||
UMaterialInstanceConstant* ParentMI = Cast<UMaterialInstanceConstant>(ParentMat);
|
||||
if (ParentMI)
|
||||
{
|
||||
ParentMat = ParentMI->Parent;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TypeStr.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Could not determine parameter type for '%s'. Specify the 'type' field explicitly (scalar, vector, texture, staticSwitch)."),
|
||||
*ParamName));
|
||||
}
|
||||
|
||||
FString NewValueDescription;
|
||||
FMaterialParameterInfo ParamInfo(*ParamName);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' (type=%s) on Material Instance '%s'"),
|
||||
bDryRun ? TEXT("[DRY RUN] Setting") : TEXT("Setting"),
|
||||
*ParamName, *TypeStr, *MIName);
|
||||
|
||||
if (TypeStr.Equals(TEXT("scalar"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
// Scalar parameter — value is a number
|
||||
double FloatValue = Json->GetNumberField(TEXT("value"));
|
||||
|
||||
if (!bDryRun)
|
||||
{
|
||||
MI->SetScalarParameterValueEditorOnly(ParamInfo, (float)FloatValue);
|
||||
}
|
||||
NewValueDescription = FString::Printf(TEXT("%f"), FloatValue);
|
||||
}
|
||||
else if (TypeStr.Equals(TEXT("vector"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
// Vector parameter — value is { r, g, b, a? }
|
||||
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
||||
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("For vector parameters, 'value' must be an object with r, g, b (and optional a) fields."));
|
||||
}
|
||||
|
||||
double R = (*ValueObj)->GetNumberField(TEXT("r"));
|
||||
double G = (*ValueObj)->GetNumberField(TEXT("g"));
|
||||
double B = (*ValueObj)->GetNumberField(TEXT("b"));
|
||||
double A = (*ValueObj)->HasField(TEXT("a")) ? (*ValueObj)->GetNumberField(TEXT("a")) : 1.0;
|
||||
|
||||
FLinearColor Color((float)R, (float)G, (float)B, (float)A);
|
||||
|
||||
if (!bDryRun)
|
||||
{
|
||||
MI->SetVectorParameterValueEditorOnly(ParamInfo, Color);
|
||||
}
|
||||
NewValueDescription = FString::Printf(TEXT("(R=%f, G=%f, B=%f, A=%f)"), R, G, B, A);
|
||||
}
|
||||
else if (TypeStr.Equals(TEXT("texture"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
// Texture parameter — value is a texture path string
|
||||
FString TexturePath = Json->GetStringField(TEXT("value"));
|
||||
if (TexturePath.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("For texture parameters, 'value' must be a texture asset path string."));
|
||||
}
|
||||
|
||||
UTexture* TextureObj = LoadObject<UTexture>(nullptr, *TexturePath);
|
||||
if (!TextureObj)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not load texture at path '%s'"), *TexturePath));
|
||||
}
|
||||
|
||||
if (!bDryRun)
|
||||
{
|
||||
MI->SetTextureParameterValueEditorOnly(ParamInfo, TextureObj);
|
||||
}
|
||||
NewValueDescription = TexturePath;
|
||||
}
|
||||
else if (TypeStr.Equals(TEXT("staticSwitch"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
// Static switch parameter — value is a bool
|
||||
bool bSwitchValue = Json->GetBoolField(TEXT("value"));
|
||||
|
||||
if (!bDryRun)
|
||||
{
|
||||
// Modify static parameters
|
||||
FStaticParameterSet StaticParams;
|
||||
MI->GetStaticParameterValues(StaticParams);
|
||||
|
||||
bool bFound = false;
|
||||
for (FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters)
|
||||
{
|
||||
if (Param.ParameterInfo.Name == FName(*ParamName))
|
||||
{
|
||||
Param.Value = bSwitchValue;
|
||||
Param.bOverride = true;
|
||||
bFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bFound)
|
||||
{
|
||||
// Add new static switch parameter entry
|
||||
FStaticSwitchParameter NewParam;
|
||||
NewParam.ParameterInfo.Name = FName(*ParamName);
|
||||
NewParam.Value = bSwitchValue;
|
||||
NewParam.bOverride = true;
|
||||
StaticParams.StaticSwitchParameters.Add(NewParam);
|
||||
}
|
||||
|
||||
MI->UpdateStaticPermutation(StaticParams);
|
||||
}
|
||||
NewValueDescription = bSwitchValue ? TEXT("true") : TEXT("false");
|
||||
}
|
||||
else
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch"),
|
||||
*TypeStr));
|
||||
}
|
||||
|
||||
if (!bDryRun)
|
||||
{
|
||||
MI->PreEditChange(nullptr);
|
||||
MI->PostEditChange();
|
||||
MI->MarkPackageDirty();
|
||||
MCPUtils::SaveGenericPackage(MI);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' = %s on '%s'"),
|
||||
bDryRun ? TEXT("[DRY RUN] Would set") : TEXT("Set"),
|
||||
*ParamName, *NewValueDescription, *MIName);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("materialInstance"), MIName);
|
||||
Result->SetStringField(TEXT("parameterName"), ParamName);
|
||||
Result->SetStringField(TEXT("type"), TypeStr);
|
||||
Result->SetStringField(TEXT("newValue"), NewValueDescription);
|
||||
if (bDryRun)
|
||||
{
|
||||
Result->SetBoolField(TEXT("dryRun"), true);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleGetMaterialInstanceParameters — list all parameters on an MI
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleGetMaterialInstanceParameters(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString NameParam = Json->GetStringField(TEXT("name"));
|
||||
if (NameParam.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required query parameter: name"));
|
||||
}
|
||||
|
||||
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(NameParam, Result);
|
||||
if (!MI) return;
|
||||
|
||||
Result->SetStringField(TEXT("name"), MI->GetName());
|
||||
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
||||
|
||||
// Parent info
|
||||
if (MI->Parent)
|
||||
{
|
||||
Result->SetStringField(TEXT("parent"), MI->Parent->GetPathName());
|
||||
}
|
||||
|
||||
// Build parent chain
|
||||
TArray<TSharedPtr<FJsonValue>> ParentChainArr;
|
||||
{
|
||||
UMaterialInterface* Current = MI->Parent;
|
||||
while (Current)
|
||||
{
|
||||
TSharedRef<FJsonObject> ParentObj = MakeShared<FJsonObject>();
|
||||
ParentObj->SetStringField(TEXT("name"), Current->GetName());
|
||||
ParentObj->SetStringField(TEXT("path"), Current->GetPathName());
|
||||
ParentObj->SetStringField(TEXT("class"), Current->GetClass()->GetName());
|
||||
ParentChainArr.Add(MakeShared<FJsonValueObject>(ParentObj));
|
||||
|
||||
UMaterialInstanceConstant* ParentMI = Cast<UMaterialInstanceConstant>(Current);
|
||||
if (ParentMI)
|
||||
{
|
||||
Current = ParentMI->Parent;
|
||||
}
|
||||
else
|
||||
{
|
||||
break; // Reached the root Material
|
||||
}
|
||||
}
|
||||
}
|
||||
Result->SetArrayField(TEXT("parentChain"), ParentChainArr);
|
||||
|
||||
// Scalar parameters
|
||||
TArray<TSharedPtr<FJsonValue>> ScalarArr;
|
||||
for (const FScalarParameterValue& Param : MI->ScalarParameterValues)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetNumberField(TEXT("value"), Param.ParameterValue);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), true); // Present in ScalarParameterValues means it's overridden
|
||||
ScalarArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
Result->SetArrayField(TEXT("scalarParameters"), ScalarArr);
|
||||
|
||||
// Vector parameters
|
||||
TArray<TSharedPtr<FJsonValue>> VectorArr;
|
||||
for (const FVectorParameterValue& Param : MI->VectorParameterValues)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetNumberField(TEXT("r"), Param.ParameterValue.R);
|
||||
PObj->SetNumberField(TEXT("g"), Param.ParameterValue.G);
|
||||
PObj->SetNumberField(TEXT("b"), Param.ParameterValue.B);
|
||||
PObj->SetNumberField(TEXT("a"), Param.ParameterValue.A);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), true);
|
||||
VectorArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
Result->SetArrayField(TEXT("vectorParameters"), VectorArr);
|
||||
|
||||
// Texture parameters
|
||||
TArray<TSharedPtr<FJsonValue>> TextureArr;
|
||||
for (const FTextureParameterValue& Param : MI->TextureParameterValues)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
if (Param.ParameterValue)
|
||||
{
|
||||
PObj->SetStringField(TEXT("texture"), Param.ParameterValue->GetPathName());
|
||||
}
|
||||
else
|
||||
{
|
||||
PObj->SetStringField(TEXT("texture"), TEXT("None"));
|
||||
}
|
||||
PObj->SetBoolField(TEXT("isOverridden"), true);
|
||||
TextureArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
Result->SetArrayField(TEXT("textureParameters"), TextureArr);
|
||||
|
||||
// Static switch parameters
|
||||
TArray<TSharedPtr<FJsonValue>> StaticSwitchArr;
|
||||
{
|
||||
FStaticParameterSet StaticParams;
|
||||
MI->GetStaticParameterValues(StaticParams);
|
||||
|
||||
for (const FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetBoolField(TEXT("value"), Param.Value);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), Param.bOverride);
|
||||
StaticSwitchArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr);
|
||||
|
||||
// Also report inherited parameters from the parent material for discoverability
|
||||
TArray<TSharedPtr<FJsonValue>> InheritedScalarArr;
|
||||
TArray<TSharedPtr<FJsonValue>> InheritedVectorArr;
|
||||
TArray<TSharedPtr<FJsonValue>> InheritedTextureArr;
|
||||
TArray<TSharedPtr<FJsonValue>> InheritedStaticSwitchArr;
|
||||
{
|
||||
UMaterial* BaseMat = MI->GetMaterial();
|
||||
if (BaseMat)
|
||||
{
|
||||
// Collect names of already-overridden parameters for filtering
|
||||
TSet<FString> OverriddenScalars;
|
||||
for (const FScalarParameterValue& P : MI->ScalarParameterValues)
|
||||
{
|
||||
OverriddenScalars.Add(P.ParameterInfo.Name.ToString());
|
||||
}
|
||||
TSet<FString> OverriddenVectors;
|
||||
for (const FVectorParameterValue& P : MI->VectorParameterValues)
|
||||
{
|
||||
OverriddenVectors.Add(P.ParameterInfo.Name.ToString());
|
||||
}
|
||||
TSet<FString> OverriddenTextures;
|
||||
for (const FTextureParameterValue& P : MI->TextureParameterValues)
|
||||
{
|
||||
OverriddenTextures.Add(P.ParameterInfo.Name.ToString());
|
||||
}
|
||||
TSet<FString> OverriddenStaticSwitches;
|
||||
{
|
||||
FStaticParameterSet SP;
|
||||
MI->GetStaticParameterValues(SP);
|
||||
for (const FStaticSwitchParameter& P : SP.StaticSwitchParameters)
|
||||
{
|
||||
if (P.bOverride)
|
||||
{
|
||||
OverriddenStaticSwitches.Add(P.ParameterInfo.Name.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
|
||||
{
|
||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||
{
|
||||
if (!OverriddenScalars.Contains(SP->ParameterName.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), SP->ParameterName.ToString());
|
||||
PObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||
InheritedScalarArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||
{
|
||||
if (!OverriddenVectors.Contains(VP->ParameterName.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), VP->ParameterName.ToString());
|
||||
PObj->SetNumberField(TEXT("r"), VP->DefaultValue.R);
|
||||
PObj->SetNumberField(TEXT("g"), VP->DefaultValue.G);
|
||||
PObj->SetNumberField(TEXT("b"), VP->DefaultValue.B);
|
||||
PObj->SetNumberField(TEXT("a"), VP->DefaultValue.A);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||
InheritedVectorArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||
{
|
||||
if (!OverriddenTextures.Contains(TP->ParameterName.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), TP->ParameterName.ToString());
|
||||
if (TP->Texture)
|
||||
{
|
||||
PObj->SetStringField(TEXT("defaultTexture"), TP->Texture->GetPathName());
|
||||
}
|
||||
else
|
||||
{
|
||||
PObj->SetStringField(TEXT("defaultTexture"), TEXT("None"));
|
||||
}
|
||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||
InheritedTextureArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||
{
|
||||
if (!OverriddenStaticSwitches.Contains(SSP->ParameterName.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString());
|
||||
PObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||
InheritedStaticSwitchArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge inherited (non-overridden) params into the arrays
|
||||
for (const TSharedPtr<FJsonValue>& V : InheritedScalarArr)
|
||||
{
|
||||
ScalarArr.Add(V);
|
||||
}
|
||||
for (const TSharedPtr<FJsonValue>& V : InheritedVectorArr)
|
||||
{
|
||||
VectorArr.Add(V);
|
||||
}
|
||||
for (const TSharedPtr<FJsonValue>& V : InheritedTextureArr)
|
||||
{
|
||||
TextureArr.Add(V);
|
||||
}
|
||||
for (const TSharedPtr<FJsonValue>& V : InheritedStaticSwitchArr)
|
||||
{
|
||||
StaticSwitchArr.Add(V);
|
||||
}
|
||||
|
||||
// Update arrays with merged data
|
||||
Result->SetArrayField(TEXT("scalarParameters"), ScalarArr);
|
||||
Result->SetArrayField(TEXT("vectorParameters"), VectorArr);
|
||||
Result->SetArrayField(TEXT("textureParameters"), TextureArr);
|
||||
Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleReparentMaterialInstance — change parent of an MI
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleReparentMaterialInstance(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString MIName = Json->GetStringField(TEXT("materialInstance"));
|
||||
FString NewParentName = Json->GetStringField(TEXT("newParent"));
|
||||
|
||||
if (MIName.IsEmpty() || NewParentName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: materialInstance, newParent"));
|
||||
}
|
||||
|
||||
bool bDryRun = false;
|
||||
if (Json->HasField(TEXT("dryRun")))
|
||||
{
|
||||
bDryRun = Json->GetBoolField(TEXT("dryRun"));
|
||||
}
|
||||
|
||||
// Load the Material Instance
|
||||
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MIName, Result);
|
||||
if (!MI) return;
|
||||
|
||||
// Capture old parent
|
||||
FString OldParentPath = MI->Parent ? MI->Parent->GetPathName() : TEXT("None");
|
||||
|
||||
// Load new parent — try as Material first, then as Material Instance
|
||||
UMaterialInterface* NewParent = nullptr;
|
||||
{
|
||||
FString MatLoadError;
|
||||
UMaterial* NewParentMat = UMCPAssetFinder::LoadAsset<UMaterial>(NewParentName, MatLoadError);
|
||||
if (NewParentMat)
|
||||
{
|
||||
NewParent = NewParentMat;
|
||||
}
|
||||
else
|
||||
{
|
||||
FString MILoadError;
|
||||
UMaterialInstanceConstant* NewParentMI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(NewParentName, MILoadError);
|
||||
if (NewParentMI)
|
||||
{
|
||||
NewParent = NewParentMI;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!NewParent)
|
||||
{
|
||||
// Try LoadObject as a fallback
|
||||
NewParent = LoadObject<UMaterialInterface>(nullptr, *NewParentName);
|
||||
}
|
||||
|
||||
if (!NewParent)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("New parent material '%s' not found. Provide a Material or Material Instance name/path."),
|
||||
*NewParentName));
|
||||
}
|
||||
|
||||
// Prevent circular parenting — check if NewParent is this MI or has this MI in its chain
|
||||
{
|
||||
UMaterialInterface* Check = NewParent;
|
||||
while (Check)
|
||||
{
|
||||
if (Check == MI)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot reparent '%s' to '%s' — this would create a circular parent chain."),
|
||||
*MIName, *NewParentName));
|
||||
}
|
||||
UMaterialInstanceConstant* CheckMI = Cast<UMaterialInstanceConstant>(Check);
|
||||
if (CheckMI)
|
||||
{
|
||||
Check = CheckMI->Parent;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s Material Instance '%s': parent '%s' -> '%s'"),
|
||||
bDryRun ? TEXT("[DRY RUN] Reparenting") : TEXT("Reparenting"),
|
||||
*MIName, *OldParentPath, *NewParent->GetPathName());
|
||||
|
||||
if (!bDryRun)
|
||||
{
|
||||
MI->PreEditChange(nullptr);
|
||||
MI->Parent = NewParent;
|
||||
MI->PostEditChange();
|
||||
|
||||
bool bSaved = MCPUtils::SaveGenericPackage(MI);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparented Material Instance '%s' (saved: %s)"),
|
||||
*MIName, bSaved ? TEXT("true") : TEXT("false"));
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("materialInstance"), MIName);
|
||||
Result->SetStringField(TEXT("oldParent"), OldParentPath);
|
||||
Result->SetStringField(TEXT("newParent"), NewParent->GetPathName());
|
||||
if (bDryRun)
|
||||
{
|
||||
Result->SetBoolField(TEXT("dryRun"), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,733 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Materials/MaterialInstanceConstant.h"
|
||||
#include "Materials/MaterialExpressionScalarParameter.h"
|
||||
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
||||
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
||||
#include "Factories/MaterialInstanceConstantFactoryNew.h"
|
||||
#include "AssetToolsModule.h"
|
||||
#include "IAssetTools.h"
|
||||
#include "Engine/Texture.h"
|
||||
#include "MCPHandlers_MaterialInstance.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="create_material_instance_asset"))
|
||||
class UMCPHandler_CreateMaterialInstance : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Name for the new Material Instance asset"))
|
||||
FString Name;
|
||||
|
||||
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
||||
FString PackagePath;
|
||||
|
||||
UPROPERTY(meta=(Description="Parent material name or path (Material or Material Instance)"))
|
||||
FString ParentMaterial;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Create a new Material Instance Constant asset with a specified parent material.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Validate packagePath starts with /Game
|
||||
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||
}
|
||||
|
||||
// Check if asset already exists
|
||||
FString FullAssetPath = PackagePath / Name;
|
||||
if (UMCPAssetFinder::FindAsset(UMaterialInstanceConstant::StaticClass(), Name) || UMCPAssetFinder::FindAsset(UMaterialInstanceConstant::StaticClass(), FullAssetPath))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Material Instance '%s' already exists. Use a different name or delete the existing asset first."),
|
||||
*Name));
|
||||
}
|
||||
|
||||
// Load parent material — try as Material first, then as Material Instance
|
||||
UMaterialInterface* ParentMaterialObj = nullptr;
|
||||
{
|
||||
FString LoadError;
|
||||
UMaterial* ParentMat = UMCPAssetFinder::LoadAsset<UMaterial>(ParentMaterial, LoadError);
|
||||
if (ParentMat)
|
||||
{
|
||||
ParentMaterialObj = ParentMat;
|
||||
}
|
||||
else
|
||||
{
|
||||
FString MILoadError;
|
||||
UMaterialInstanceConstant* ParentMI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(ParentMaterial, MILoadError);
|
||||
if (ParentMI)
|
||||
{
|
||||
ParentMaterialObj = ParentMI;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ParentMaterialObj)
|
||||
{
|
||||
// Also try LoadObject as a fallback with the raw path
|
||||
ParentMaterialObj = LoadObject<UMaterialInterface>(nullptr, *ParentMaterial);
|
||||
}
|
||||
|
||||
if (!ParentMaterialObj)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parent material '%s' not found. Provide a Material or Material Instance name/path."),
|
||||
*ParentMaterial));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Instance '%s' in '%s' with parent '%s'"),
|
||||
*Name, *PackagePath, *ParentMaterialObj->GetName());
|
||||
|
||||
// Create via factory + AssetTools
|
||||
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
||||
UMaterialInstanceConstantFactoryNew* Factory = NewObject<UMaterialInstanceConstantFactoryNew>();
|
||||
|
||||
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory);
|
||||
if (!NewAsset)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Instance asset '%s' in '%s'"), *Name, *PackagePath));
|
||||
}
|
||||
|
||||
UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(NewAsset);
|
||||
if (!MI)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialInstanceConstant"));
|
||||
}
|
||||
|
||||
// Set parent
|
||||
MI->PreEditChange(nullptr);
|
||||
MI->Parent = ParentMaterialObj;
|
||||
MI->PostEditChange();
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveGenericPackage(MI);
|
||||
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Instance '%s' with parent '%s' (saved: %s)"),
|
||||
*Name, *ParentMaterialObj->GetName(), bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("name"), Name);
|
||||
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
||||
Result->SetStringField(TEXT("parent"), ParentMaterialObj->GetPathName());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="set_material_instance_parameter"))
|
||||
class UMCPHandler_SetMaterialInstanceParameter : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Material Instance name or path"))
|
||||
FString MaterialInstance;
|
||||
|
||||
UPROPERTY(meta=(Description="Parameter name to set"))
|
||||
FString ParameterName;
|
||||
|
||||
UPROPERTY(meta=(Description="Value to set (number for scalar, object with r/g/b/a for vector, string path for texture, bool for staticSwitch)"))
|
||||
FMCPJsonObject Value;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Parameter type: scalar, vector, texture, staticSwitch. Auto-detected from parent if omitted."))
|
||||
FString Type;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, validate without applying changes"))
|
||||
bool DryRun = false;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Set a parameter override on a Material Instance. "
|
||||
"Supports scalar, vector, texture, and static switch parameter types.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
if (!Json->HasField(TEXT("value")))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
|
||||
}
|
||||
|
||||
// Load the Material Instance
|
||||
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MaterialInstance, Result);
|
||||
if (!MI) return;
|
||||
|
||||
// Determine the parameter type — explicit or auto-detect from parent
|
||||
FString TypeStr = Type;
|
||||
|
||||
// Auto-detect type from parent material's parameters if not provided
|
||||
if (TypeStr.IsEmpty())
|
||||
{
|
||||
UMaterialInterface* ParentMat = MI->Parent;
|
||||
while (ParentMat)
|
||||
{
|
||||
UMaterial* BaseMat = ParentMat->GetMaterial();
|
||||
if (BaseMat)
|
||||
{
|
||||
// Check scalar parameters
|
||||
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
|
||||
{
|
||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||
{
|
||||
if (SP->ParameterName.ToString() == ParameterName)
|
||||
{
|
||||
TypeStr = TEXT("scalar");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||
{
|
||||
if (VP->ParameterName.ToString() == ParameterName)
|
||||
{
|
||||
TypeStr = TEXT("vector");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||
{
|
||||
if (TP->ParameterName.ToString() == ParameterName)
|
||||
{
|
||||
TypeStr = TEXT("texture");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||
{
|
||||
if (SSP->ParameterName.ToString() == ParameterName)
|
||||
{
|
||||
TypeStr = TEXT("staticSwitch");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break; // Only need to check the base material
|
||||
}
|
||||
// Walk up the parent chain if it's an MI parented to another MI
|
||||
UMaterialInstanceConstant* ParentMI = Cast<UMaterialInstanceConstant>(ParentMat);
|
||||
if (ParentMI)
|
||||
{
|
||||
ParentMat = ParentMI->Parent;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TypeStr.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Could not determine parameter type for '%s'. Specify the 'type' field explicitly (scalar, vector, texture, staticSwitch)."),
|
||||
*ParameterName));
|
||||
}
|
||||
|
||||
FString NewValueDescription;
|
||||
FMaterialParameterInfo ParamInfo(*ParameterName);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' (type=%s) on Material Instance '%s'"),
|
||||
DryRun ? TEXT("[DRY RUN] Setting") : TEXT("Setting"),
|
||||
*ParameterName, *TypeStr, *MaterialInstance);
|
||||
|
||||
if (TypeStr.Equals(TEXT("scalar"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
// Scalar parameter — value is a number
|
||||
double FloatValue = Json->GetNumberField(TEXT("value"));
|
||||
|
||||
if (!DryRun)
|
||||
{
|
||||
MI->SetScalarParameterValueEditorOnly(ParamInfo, (float)FloatValue);
|
||||
}
|
||||
NewValueDescription = FString::Printf(TEXT("%f"), FloatValue);
|
||||
}
|
||||
else if (TypeStr.Equals(TEXT("vector"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
// Vector parameter — value is { r, g, b, a? }
|
||||
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
||||
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("For vector parameters, 'value' must be an object with r, g, b (and optional a) fields."));
|
||||
}
|
||||
|
||||
double R = (*ValueObj)->GetNumberField(TEXT("r"));
|
||||
double G = (*ValueObj)->GetNumberField(TEXT("g"));
|
||||
double B = (*ValueObj)->GetNumberField(TEXT("b"));
|
||||
double A = (*ValueObj)->HasField(TEXT("a")) ? (*ValueObj)->GetNumberField(TEXT("a")) : 1.0;
|
||||
|
||||
FLinearColor Color((float)R, (float)G, (float)B, (float)A);
|
||||
|
||||
if (!DryRun)
|
||||
{
|
||||
MI->SetVectorParameterValueEditorOnly(ParamInfo, Color);
|
||||
}
|
||||
NewValueDescription = FString::Printf(TEXT("(R=%f, G=%f, B=%f, A=%f)"), R, G, B, A);
|
||||
}
|
||||
else if (TypeStr.Equals(TEXT("texture"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
// Texture parameter — value is a texture path string
|
||||
FString TexturePath = Json->GetStringField(TEXT("value"));
|
||||
if (TexturePath.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("For texture parameters, 'value' must be a texture asset path string."));
|
||||
}
|
||||
|
||||
UTexture* TextureObj = LoadObject<UTexture>(nullptr, *TexturePath);
|
||||
if (!TextureObj)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not load texture at path '%s'"), *TexturePath));
|
||||
}
|
||||
|
||||
if (!DryRun)
|
||||
{
|
||||
MI->SetTextureParameterValueEditorOnly(ParamInfo, TextureObj);
|
||||
}
|
||||
NewValueDescription = TexturePath;
|
||||
}
|
||||
else if (TypeStr.Equals(TEXT("staticSwitch"), ESearchCase::IgnoreCase))
|
||||
{
|
||||
// Static switch parameter — value is a bool
|
||||
bool bSwitchValue = Json->GetBoolField(TEXT("value"));
|
||||
|
||||
if (!DryRun)
|
||||
{
|
||||
// Modify static parameters
|
||||
FStaticParameterSet StaticParams;
|
||||
MI->GetStaticParameterValues(StaticParams);
|
||||
|
||||
bool bFound = false;
|
||||
for (FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters)
|
||||
{
|
||||
if (Param.ParameterInfo.Name == FName(*ParameterName))
|
||||
{
|
||||
Param.Value = bSwitchValue;
|
||||
Param.bOverride = true;
|
||||
bFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bFound)
|
||||
{
|
||||
// Add new static switch parameter entry
|
||||
FStaticSwitchParameter NewParam;
|
||||
NewParam.ParameterInfo.Name = FName(*ParameterName);
|
||||
NewParam.Value = bSwitchValue;
|
||||
NewParam.bOverride = true;
|
||||
StaticParams.StaticSwitchParameters.Add(NewParam);
|
||||
}
|
||||
|
||||
MI->UpdateStaticPermutation(StaticParams);
|
||||
}
|
||||
NewValueDescription = bSwitchValue ? TEXT("true") : TEXT("false");
|
||||
}
|
||||
else
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch"),
|
||||
*TypeStr));
|
||||
}
|
||||
|
||||
if (!DryRun)
|
||||
{
|
||||
MI->PreEditChange(nullptr);
|
||||
MI->PostEditChange();
|
||||
MI->MarkPackageDirty();
|
||||
MCPUtils::SaveGenericPackage(MI);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' = %s on '%s'"),
|
||||
DryRun ? TEXT("[DRY RUN] Would set") : TEXT("Set"),
|
||||
*ParameterName, *NewValueDescription, *MaterialInstance);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("materialInstance"), MaterialInstance);
|
||||
Result->SetStringField(TEXT("parameterName"), ParameterName);
|
||||
Result->SetStringField(TEXT("type"), TypeStr);
|
||||
Result->SetStringField(TEXT("newValue"), NewValueDescription);
|
||||
if (DryRun)
|
||||
{
|
||||
Result->SetBoolField(TEXT("dryRun"), true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="dump_material_instance_parameters"))
|
||||
class UMCPHandler_GetMaterialInstanceParameters : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Material Instance name or path to inspect"))
|
||||
FString MaterialInstance;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("List all parameters on a Material Instance, including overridden and inherited parameters.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MaterialInstance, Result);
|
||||
if (!MI) return;
|
||||
|
||||
Result->SetStringField(TEXT("name"), MI->GetName());
|
||||
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
||||
|
||||
// Parent info
|
||||
if (MI->Parent)
|
||||
{
|
||||
Result->SetStringField(TEXT("parent"), MI->Parent->GetPathName());
|
||||
}
|
||||
|
||||
// Build parent chain
|
||||
TArray<TSharedPtr<FJsonValue>> ParentChainArr;
|
||||
{
|
||||
UMaterialInterface* Current = MI->Parent;
|
||||
while (Current)
|
||||
{
|
||||
TSharedRef<FJsonObject> ParentObj = MakeShared<FJsonObject>();
|
||||
ParentObj->SetStringField(TEXT("name"), Current->GetName());
|
||||
ParentObj->SetStringField(TEXT("path"), Current->GetPathName());
|
||||
ParentObj->SetStringField(TEXT("class"), Current->GetClass()->GetName());
|
||||
ParentChainArr.Add(MakeShared<FJsonValueObject>(ParentObj));
|
||||
|
||||
UMaterialInstanceConstant* ParentMI = Cast<UMaterialInstanceConstant>(Current);
|
||||
if (ParentMI)
|
||||
{
|
||||
Current = ParentMI->Parent;
|
||||
}
|
||||
else
|
||||
{
|
||||
break; // Reached the root Material
|
||||
}
|
||||
}
|
||||
}
|
||||
Result->SetArrayField(TEXT("parentChain"), ParentChainArr);
|
||||
|
||||
// Scalar parameters
|
||||
TArray<TSharedPtr<FJsonValue>> ScalarArr;
|
||||
for (const FScalarParameterValue& Param : MI->ScalarParameterValues)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetNumberField(TEXT("value"), Param.ParameterValue);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), true); // Present in ScalarParameterValues means it's overridden
|
||||
ScalarArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
Result->SetArrayField(TEXT("scalarParameters"), ScalarArr);
|
||||
|
||||
// Vector parameters
|
||||
TArray<TSharedPtr<FJsonValue>> VectorArr;
|
||||
for (const FVectorParameterValue& Param : MI->VectorParameterValues)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetNumberField(TEXT("r"), Param.ParameterValue.R);
|
||||
PObj->SetNumberField(TEXT("g"), Param.ParameterValue.G);
|
||||
PObj->SetNumberField(TEXT("b"), Param.ParameterValue.B);
|
||||
PObj->SetNumberField(TEXT("a"), Param.ParameterValue.A);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), true);
|
||||
VectorArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
Result->SetArrayField(TEXT("vectorParameters"), VectorArr);
|
||||
|
||||
// Texture parameters
|
||||
TArray<TSharedPtr<FJsonValue>> TextureArr;
|
||||
for (const FTextureParameterValue& Param : MI->TextureParameterValues)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
if (Param.ParameterValue)
|
||||
{
|
||||
PObj->SetStringField(TEXT("texture"), Param.ParameterValue->GetPathName());
|
||||
}
|
||||
else
|
||||
{
|
||||
PObj->SetStringField(TEXT("texture"), TEXT("None"));
|
||||
}
|
||||
PObj->SetBoolField(TEXT("isOverridden"), true);
|
||||
TextureArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
Result->SetArrayField(TEXT("textureParameters"), TextureArr);
|
||||
|
||||
// Static switch parameters
|
||||
TArray<TSharedPtr<FJsonValue>> StaticSwitchArr;
|
||||
{
|
||||
FStaticParameterSet StaticParams;
|
||||
MI->GetStaticParameterValues(StaticParams);
|
||||
|
||||
for (const FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetBoolField(TEXT("value"), Param.Value);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), Param.bOverride);
|
||||
StaticSwitchArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr);
|
||||
|
||||
// Also report inherited parameters from the parent material for discoverability
|
||||
TArray<TSharedPtr<FJsonValue>> InheritedScalarArr;
|
||||
TArray<TSharedPtr<FJsonValue>> InheritedVectorArr;
|
||||
TArray<TSharedPtr<FJsonValue>> InheritedTextureArr;
|
||||
TArray<TSharedPtr<FJsonValue>> InheritedStaticSwitchArr;
|
||||
{
|
||||
UMaterial* BaseMat = MI->GetMaterial();
|
||||
if (BaseMat)
|
||||
{
|
||||
// Collect names of already-overridden parameters for filtering
|
||||
TSet<FString> OverriddenScalars;
|
||||
for (const FScalarParameterValue& P : MI->ScalarParameterValues)
|
||||
{
|
||||
OverriddenScalars.Add(P.ParameterInfo.Name.ToString());
|
||||
}
|
||||
TSet<FString> OverriddenVectors;
|
||||
for (const FVectorParameterValue& P : MI->VectorParameterValues)
|
||||
{
|
||||
OverriddenVectors.Add(P.ParameterInfo.Name.ToString());
|
||||
}
|
||||
TSet<FString> OverriddenTextures;
|
||||
for (const FTextureParameterValue& P : MI->TextureParameterValues)
|
||||
{
|
||||
OverriddenTextures.Add(P.ParameterInfo.Name.ToString());
|
||||
}
|
||||
TSet<FString> OverriddenStaticSwitches;
|
||||
{
|
||||
FStaticParameterSet SP;
|
||||
MI->GetStaticParameterValues(SP);
|
||||
for (const FStaticSwitchParameter& P : SP.StaticSwitchParameters)
|
||||
{
|
||||
if (P.bOverride)
|
||||
{
|
||||
OverriddenStaticSwitches.Add(P.ParameterInfo.Name.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
|
||||
{
|
||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||
{
|
||||
if (!OverriddenScalars.Contains(SP->ParameterName.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), SP->ParameterName.ToString());
|
||||
PObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||
InheritedScalarArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||
{
|
||||
if (!OverriddenVectors.Contains(VP->ParameterName.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), VP->ParameterName.ToString());
|
||||
PObj->SetNumberField(TEXT("r"), VP->DefaultValue.R);
|
||||
PObj->SetNumberField(TEXT("g"), VP->DefaultValue.G);
|
||||
PObj->SetNumberField(TEXT("b"), VP->DefaultValue.B);
|
||||
PObj->SetNumberField(TEXT("a"), VP->DefaultValue.A);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||
InheritedVectorArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||
{
|
||||
if (!OverriddenTextures.Contains(TP->ParameterName.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), TP->ParameterName.ToString());
|
||||
if (TP->Texture)
|
||||
{
|
||||
PObj->SetStringField(TEXT("defaultTexture"), TP->Texture->GetPathName());
|
||||
}
|
||||
else
|
||||
{
|
||||
PObj->SetStringField(TEXT("defaultTexture"), TEXT("None"));
|
||||
}
|
||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||
InheritedTextureArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||
{
|
||||
if (!OverriddenStaticSwitches.Contains(SSP->ParameterName.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString());
|
||||
PObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
|
||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||
InheritedStaticSwitchArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge inherited (non-overridden) params into the arrays
|
||||
for (const TSharedPtr<FJsonValue>& V : InheritedScalarArr)
|
||||
{
|
||||
ScalarArr.Add(V);
|
||||
}
|
||||
for (const TSharedPtr<FJsonValue>& V : InheritedVectorArr)
|
||||
{
|
||||
VectorArr.Add(V);
|
||||
}
|
||||
for (const TSharedPtr<FJsonValue>& V : InheritedTextureArr)
|
||||
{
|
||||
TextureArr.Add(V);
|
||||
}
|
||||
for (const TSharedPtr<FJsonValue>& V : InheritedStaticSwitchArr)
|
||||
{
|
||||
StaticSwitchArr.Add(V);
|
||||
}
|
||||
|
||||
// Update arrays with merged data
|
||||
Result->SetArrayField(TEXT("scalarParameters"), ScalarArr);
|
||||
Result->SetArrayField(TEXT("vectorParameters"), VectorArr);
|
||||
Result->SetArrayField(TEXT("textureParameters"), TextureArr);
|
||||
Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="reparent_material_instance"))
|
||||
class UMCPHandler_ReparentMaterialInstance : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Material Instance name or path to reparent"))
|
||||
FString MaterialInstance;
|
||||
|
||||
UPROPERTY(meta=(Description="New parent material name or path (Material or Material Instance)"))
|
||||
FString NewParent;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, validate without applying changes"))
|
||||
bool DryRun = false;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Change the parent material of a Material Instance. "
|
||||
"Validates against circular parent chains.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load the Material Instance
|
||||
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(MaterialInstance, Result);
|
||||
if (!MI) return;
|
||||
|
||||
// Capture old parent
|
||||
FString OldParentPath = MI->Parent ? MI->Parent->GetPathName() : TEXT("None");
|
||||
|
||||
// Load new parent — try as Material first, then as Material Instance
|
||||
UMaterialInterface* NewParentObj = nullptr;
|
||||
{
|
||||
FString MatLoadError;
|
||||
UMaterial* NewParentMat = UMCPAssetFinder::LoadAsset<UMaterial>(NewParent, MatLoadError);
|
||||
if (NewParentMat)
|
||||
{
|
||||
NewParentObj = NewParentMat;
|
||||
}
|
||||
else
|
||||
{
|
||||
FString MILoadError;
|
||||
UMaterialInstanceConstant* NewParentMI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(NewParent, MILoadError);
|
||||
if (NewParentMI)
|
||||
{
|
||||
NewParentObj = NewParentMI;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!NewParentObj)
|
||||
{
|
||||
// Try LoadObject as a fallback
|
||||
NewParentObj = LoadObject<UMaterialInterface>(nullptr, *NewParent);
|
||||
}
|
||||
|
||||
if (!NewParentObj)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("New parent material '%s' not found. Provide a Material or Material Instance name/path."),
|
||||
*NewParent));
|
||||
}
|
||||
|
||||
// Prevent circular parenting — check if NewParent is this MI or has this MI in its chain
|
||||
{
|
||||
UMaterialInterface* Check = NewParentObj;
|
||||
while (Check)
|
||||
{
|
||||
if (Check == MI)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot reparent '%s' to '%s' — this would create a circular parent chain."),
|
||||
*MaterialInstance, *NewParent));
|
||||
}
|
||||
UMaterialInstanceConstant* CheckMI = Cast<UMaterialInstanceConstant>(Check);
|
||||
if (CheckMI)
|
||||
{
|
||||
Check = CheckMI->Parent;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s Material Instance '%s': parent '%s' -> '%s'"),
|
||||
DryRun ? TEXT("[DRY RUN] Reparenting") : TEXT("Reparenting"),
|
||||
*MaterialInstance, *OldParentPath, *NewParentObj->GetPathName());
|
||||
|
||||
if (!DryRun)
|
||||
{
|
||||
MI->PreEditChange(nullptr);
|
||||
MI->Parent = NewParentObj;
|
||||
MI->PostEditChange();
|
||||
|
||||
bool bSaved = MCPUtils::SaveGenericPackage(MI);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparented Material Instance '%s' (saved: %s)"),
|
||||
*MaterialInstance, bSaved ? TEXT("true") : TEXT("false"));
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("materialInstance"), MaterialInstance);
|
||||
Result->SetStringField(TEXT("oldParent"), OldParentPath);
|
||||
Result->SetStringField(TEXT("newParent"), NewParentObj->GetPathName());
|
||||
if (DryRun)
|
||||
{
|
||||
Result->SetBoolField(TEXT("dryRun"), true);
|
||||
}
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,927 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPUtils.h"
|
||||
#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"
|
||||
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
||||
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
||||
#include "Materials/MaterialExpressionConstant.h"
|
||||
#include "Materials/MaterialExpressionConstant3Vector.h"
|
||||
#include "Materials/MaterialExpressionConstant4Vector.h"
|
||||
#include "Materials/MaterialExpressionTextureSample.h"
|
||||
#include "Materials/MaterialExpressionTextureCoordinate.h"
|
||||
#include "Materials/MaterialExpressionComponentMask.h"
|
||||
#include "Materials/MaterialExpressionCustom.h"
|
||||
#include "Materials/MaterialExpressionFunctionInput.h"
|
||||
#include "Materials/MaterialExpressionFunctionOutput.h"
|
||||
#include "Materials/MaterialExpressionMaterialFunctionCall.h"
|
||||
#include "MaterialGraph/MaterialGraph.h"
|
||||
#include "MaterialGraph/MaterialGraphNode.h"
|
||||
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
|
||||
// ============================================================
|
||||
// HandleListMaterials — list Material and MaterialInstance assets
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleListMaterials(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Filter = Json->GetStringField(TEXT("filter"));
|
||||
FString TypeFilter = Json->GetStringField(TEXT("type"));
|
||||
|
||||
bool bIncludeMaterials = TypeFilter.IsEmpty() || TypeFilter == TEXT("all") || TypeFilter == TEXT("material");
|
||||
bool bIncludeInstances = TypeFilter.IsEmpty() || TypeFilter == TEXT("all") || TypeFilter == TEXT("instance");
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Entries;
|
||||
|
||||
if (bIncludeMaterials)
|
||||
{
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UMaterial::StaticClass()))
|
||||
{
|
||||
FString Name = Asset.AssetName.ToString();
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
|
||||
if (!Filter.IsEmpty())
|
||||
{
|
||||
if (!Name.Contains(Filter, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("name"), Name);
|
||||
Entry->SetStringField(TEXT("path"), Path);
|
||||
Entry->SetStringField(TEXT("type"), TEXT("Material"));
|
||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
}
|
||||
}
|
||||
|
||||
if (bIncludeInstances)
|
||||
{
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UMaterialInstanceConstant::StaticClass()))
|
||||
{
|
||||
FString Name = Asset.AssetName.ToString();
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
|
||||
if (!Filter.IsEmpty())
|
||||
{
|
||||
if (!Name.Contains(Filter, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("name"), Name);
|
||||
Entry->SetStringField(TEXT("path"), Path);
|
||||
Entry->SetStringField(TEXT("type"), TEXT("MaterialInstance"));
|
||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
}
|
||||
}
|
||||
|
||||
int32 Total = UMCPAssetFinder::GetAssets(UMaterial::StaticClass()).Num() + UMCPAssetFinder::GetAssets(UMaterialInstanceConstant::StaticClass()).Num();
|
||||
|
||||
Result->SetNumberField(TEXT("count"), Entries.Num());
|
||||
Result->SetNumberField(TEXT("total"), Total);
|
||||
Result->SetArrayField(TEXT("materials"), Entries);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleGetMaterial — detailed info about a material or instance
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleGetMaterial(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Name = Json->GetStringField(TEXT("name"));
|
||||
if (Name.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
|
||||
}
|
||||
|
||||
FString DecodedName = MCPUtils::UrlDecode(Name);
|
||||
|
||||
// Try loading as UMaterial first
|
||||
FString LoadError;
|
||||
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(DecodedName, LoadError);
|
||||
if (Material)
|
||||
{
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material '%s'"), *Material->GetName());
|
||||
|
||||
Result->SetStringField(TEXT("name"), Material->GetName());
|
||||
Result->SetStringField(TEXT("path"), Material->GetPathName());
|
||||
Result->SetStringField(TEXT("type"), TEXT("Material"));
|
||||
|
||||
// Material domain
|
||||
FString DomainStr = TEXT("Unknown");
|
||||
if (const UEnum* DomainEnum = StaticEnum<EMaterialDomain>())
|
||||
{
|
||||
DomainStr = DomainEnum->GetNameStringByValue((int64)Material->MaterialDomain);
|
||||
}
|
||||
Result->SetStringField(TEXT("domain"), DomainStr);
|
||||
|
||||
// Blend mode
|
||||
FString BlendModeStr = TEXT("Unknown");
|
||||
if (const UEnum* BlendEnum = StaticEnum<EBlendMode>())
|
||||
{
|
||||
BlendModeStr = BlendEnum->GetNameStringByValue((int64)Material->BlendMode);
|
||||
}
|
||||
Result->SetStringField(TEXT("blendMode"), BlendModeStr);
|
||||
|
||||
// Shading models
|
||||
TArray<TSharedPtr<FJsonValue>> ShadingModels;
|
||||
FMaterialShadingModelField SMField = Material->GetShadingModels();
|
||||
if (const UEnum* SMEnum = StaticEnum<EMaterialShadingModel>())
|
||||
{
|
||||
for (int32 i = 0; i < SMEnum->NumEnums() - 1; ++i)
|
||||
{
|
||||
EMaterialShadingModel SM = (EMaterialShadingModel)SMEnum->GetValueByIndex(i);
|
||||
if (SMField.HasShadingModel(SM))
|
||||
{
|
||||
ShadingModels.Add(MakeShared<FJsonValueString>(SMEnum->GetNameStringByIndex(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Result->SetArrayField(TEXT("shadingModels"), ShadingModels);
|
||||
|
||||
// Two-sided
|
||||
Result->SetBoolField(TEXT("twoSided"), Material->IsTwoSided());
|
||||
|
||||
// Expression count
|
||||
auto Expressions = Material->GetExpressions();
|
||||
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
|
||||
|
||||
// Parameters — iterate expressions for parameter types
|
||||
TArray<TSharedPtr<FJsonValue>> Parameters;
|
||||
for (UMaterialExpression* Expr : Expressions)
|
||||
{
|
||||
if (!Expr) continue;
|
||||
|
||||
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
||||
bool bIsParam = false;
|
||||
|
||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||
{
|
||||
bIsParam = true;
|
||||
ParamObj->SetStringField(TEXT("name"), SP->ParameterName.ToString());
|
||||
ParamObj->SetStringField(TEXT("type"), TEXT("Scalar"));
|
||||
ParamObj->SetStringField(TEXT("group"), SP->Group.ToString());
|
||||
ParamObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
|
||||
}
|
||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||
{
|
||||
bIsParam = true;
|
||||
ParamObj->SetStringField(TEXT("name"), VP->ParameterName.ToString());
|
||||
ParamObj->SetStringField(TEXT("type"), TEXT("Vector"));
|
||||
ParamObj->SetStringField(TEXT("group"), VP->Group.ToString());
|
||||
TSharedRef<FJsonObject> DefVal = MakeShared<FJsonObject>();
|
||||
DefVal->SetNumberField(TEXT("r"), VP->DefaultValue.R);
|
||||
DefVal->SetNumberField(TEXT("g"), VP->DefaultValue.G);
|
||||
DefVal->SetNumberField(TEXT("b"), VP->DefaultValue.B);
|
||||
DefVal->SetNumberField(TEXT("a"), VP->DefaultValue.A);
|
||||
ParamObj->SetObjectField(TEXT("defaultValue"), DefVal);
|
||||
}
|
||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||
{
|
||||
bIsParam = true;
|
||||
ParamObj->SetStringField(TEXT("name"), TP->ParameterName.ToString());
|
||||
ParamObj->SetStringField(TEXT("type"), TEXT("Texture"));
|
||||
ParamObj->SetStringField(TEXT("group"), TP->Group.ToString());
|
||||
if (TP->Texture)
|
||||
ParamObj->SetStringField(TEXT("defaultValue"), TP->Texture->GetPathName());
|
||||
}
|
||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||
{
|
||||
bIsParam = true;
|
||||
ParamObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString());
|
||||
ParamObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
|
||||
ParamObj->SetStringField(TEXT("group"), SSP->Group.ToString());
|
||||
ParamObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
|
||||
}
|
||||
|
||||
if (bIsParam)
|
||||
{
|
||||
Parameters.Add(MakeShared<FJsonValueObject>(ParamObj));
|
||||
}
|
||||
}
|
||||
Result->SetArrayField(TEXT("parameters"), Parameters);
|
||||
|
||||
// Referenced textures
|
||||
TArray<TSharedPtr<FJsonValue>> ReferencedTextures;
|
||||
auto RefTexObjs = Material->GetReferencedTextures();
|
||||
for (const TObjectPtr<UObject>& TexObj : RefTexObjs)
|
||||
{
|
||||
if (TexObj)
|
||||
{
|
||||
ReferencedTextures.Add(MakeShared<FJsonValueString>(TexObj->GetPathName()));
|
||||
}
|
||||
}
|
||||
Result->SetArrayField(TEXT("referencedTextures"), ReferencedTextures);
|
||||
|
||||
// Graph node count
|
||||
int32 GraphNodeCount = 0;
|
||||
if (Material->MaterialGraph)
|
||||
{
|
||||
GraphNodeCount = Material->MaterialGraph->Nodes.Num();
|
||||
}
|
||||
Result->SetNumberField(TEXT("graphNodeCount"), GraphNodeCount);
|
||||
|
||||
// Usage flags
|
||||
TSharedRef<FJsonObject> UsageFlags = MakeShared<FJsonObject>();
|
||||
UsageFlags->SetBoolField(TEXT("bUsedWithSkeletalMesh"), Material->bUsedWithSkeletalMesh != 0);
|
||||
UsageFlags->SetBoolField(TEXT("bUsedWithMorphTargets"), Material->bUsedWithMorphTargets != 0);
|
||||
UsageFlags->SetBoolField(TEXT("bUsedWithNiagaraSprites"), Material->bUsedWithNiagaraSprites != 0);
|
||||
UsageFlags->SetBoolField(TEXT("bUsedWithParticleSprites"), Material->bUsedWithParticleSprites != 0);
|
||||
UsageFlags->SetBoolField(TEXT("bUsedWithStaticLighting"), Material->bUsedWithStaticLighting != 0);
|
||||
Result->SetObjectField(TEXT("usageFlags"), UsageFlags);
|
||||
|
||||
// Opacity mask clip value
|
||||
Result->SetNumberField(TEXT("opacityMaskClipValue"), Material->OpacityMaskClipValue);
|
||||
|
||||
// Additional settings
|
||||
Result->SetBoolField(TEXT("ditheredLODTransition"), Material->DitheredLODTransition != 0);
|
||||
Result->SetBoolField(TEXT("bAllowNegativeEmissiveColor"), Material->bAllowNegativeEmissiveColor != 0);
|
||||
|
||||
// Texture sample count (simple expression scan)
|
||||
int32 TextureSampleCount = 0;
|
||||
for (UMaterialExpression* Expr : Expressions)
|
||||
{
|
||||
if (Expr && Expr->IsA<UMaterialExpressionTextureSample>())
|
||||
{
|
||||
TextureSampleCount++;
|
||||
}
|
||||
}
|
||||
Result->SetNumberField(TEXT("textureSampleCount"), TextureSampleCount);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Try loading as MaterialInstance
|
||||
FString MILoadError;
|
||||
UMaterialInstanceConstant* MI = UMCPAssetFinder::LoadAsset<UMaterialInstanceConstant>(DecodedName, MILoadError);
|
||||
if (MI)
|
||||
{
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material instance '%s'"), *MI->GetName());
|
||||
|
||||
Result->SetStringField(TEXT("name"), MI->GetName());
|
||||
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
||||
Result->SetStringField(TEXT("type"), TEXT("MaterialInstance"));
|
||||
|
||||
if (MI->Parent)
|
||||
{
|
||||
Result->SetStringField(TEXT("parent"), MI->Parent->GetName());
|
||||
Result->SetStringField(TEXT("parentPath"), MI->Parent->GetPathName());
|
||||
}
|
||||
|
||||
// Overridden parameters
|
||||
TArray<TSharedPtr<FJsonValue>> OverriddenParams;
|
||||
|
||||
// Scalar parameters
|
||||
for (const FScalarParameterValue& Param : MI->ScalarParameterValues)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetStringField(TEXT("type"), TEXT("Scalar"));
|
||||
PObj->SetNumberField(TEXT("value"), Param.ParameterValue);
|
||||
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
|
||||
// Vector parameters
|
||||
for (const FVectorParameterValue& Param : MI->VectorParameterValues)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetStringField(TEXT("type"), TEXT("Vector"));
|
||||
TSharedRef<FJsonObject> Val = MakeShared<FJsonObject>();
|
||||
Val->SetNumberField(TEXT("r"), Param.ParameterValue.R);
|
||||
Val->SetNumberField(TEXT("g"), Param.ParameterValue.G);
|
||||
Val->SetNumberField(TEXT("b"), Param.ParameterValue.B);
|
||||
Val->SetNumberField(TEXT("a"), Param.ParameterValue.A);
|
||||
PObj->SetObjectField(TEXT("value"), Val);
|
||||
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
|
||||
// Texture parameters
|
||||
for (const FTextureParameterValue& Param : MI->TextureParameterValues)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetStringField(TEXT("type"), TEXT("Texture"));
|
||||
if (Param.ParameterValue)
|
||||
PObj->SetStringField(TEXT("value"), Param.ParameterValue->GetPathName());
|
||||
else
|
||||
PObj->SetStringField(TEXT("value"), TEXT("None"));
|
||||
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
|
||||
// Static switch parameters
|
||||
for (const FStaticSwitchParameter& Param : MI->GetStaticParameters().StaticSwitchParameters)
|
||||
{
|
||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||
PObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
|
||||
PObj->SetBoolField(TEXT("value"), Param.Value);
|
||||
PObj->SetBoolField(TEXT("overridden"), Param.bOverride);
|
||||
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
||||
}
|
||||
|
||||
Result->SetArrayField(TEXT("overriddenParameters"), OverriddenParams);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleGetMaterialGraph — serialized graph for a material
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleGetMaterialGraph(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Name = Json->GetStringField(TEXT("name"));
|
||||
if (Name.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
|
||||
}
|
||||
|
||||
FString DecodedName = MCPUtils::UrlDecode(Name);
|
||||
|
||||
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(DecodedName, Result);
|
||||
if (!Material) return;
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *Material->GetName());
|
||||
|
||||
// Ensure the material graph is built
|
||||
if (!Material->MaterialGraph)
|
||||
{
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — MaterialGraph is null, attempting rebuild"));
|
||||
// The material graph is built lazily by the material editor; force-create it
|
||||
Material->MaterialGraph = CastChecked<UMaterialGraph>(
|
||||
FBlueprintEditorUtils::CreateNewGraph(Material, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
|
||||
Material->MaterialGraph->Material = Material;
|
||||
Material->MaterialGraph->RebuildGraph();
|
||||
}
|
||||
|
||||
if (!Material->MaterialGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
|
||||
}
|
||||
|
||||
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(Material->MaterialGraph);
|
||||
if (!GraphJson.IsValid())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to serialize material graph"));
|
||||
}
|
||||
|
||||
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
|
||||
|
||||
// Add material name context
|
||||
Result->SetStringField(TEXT("material"), Material->GetName());
|
||||
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleDescribeMaterial — human-readable material description
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleDescribeMaterial(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString MaterialName = Json->GetStringField(TEXT("material"));
|
||||
if (MaterialName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
|
||||
}
|
||||
|
||||
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
|
||||
if (!Material) return;
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *Material->GetName());
|
||||
|
||||
// Ensure material graph is built
|
||||
if (!Material->MaterialGraph)
|
||||
{
|
||||
Material->MaterialGraph = CastChecked<UMaterialGraph>(
|
||||
FBlueprintEditorUtils::CreateNewGraph(Material, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
|
||||
Material->MaterialGraph->Material = Material;
|
||||
Material->MaterialGraph->RebuildGraph();
|
||||
}
|
||||
|
||||
if (!Material->MaterialGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
|
||||
}
|
||||
|
||||
// Recursive helper: trace backwards from a pin and build a description string
|
||||
TFunction<FString(UEdGraphPin*, int32)> TracePin = [&TracePin](UEdGraphPin* Pin, int32 Depth) -> FString
|
||||
{
|
||||
if (!Pin || Depth > 10)
|
||||
return TEXT("(unknown)");
|
||||
|
||||
// If no connections, report as unconnected
|
||||
if (Pin->LinkedTo.Num() == 0)
|
||||
{
|
||||
if (!Pin->DefaultValue.IsEmpty())
|
||||
return FString::Printf(TEXT("(default: %s)"), *Pin->DefaultValue);
|
||||
return TEXT("(unconnected)");
|
||||
}
|
||||
|
||||
TArray<FString> Sources;
|
||||
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
|
||||
{
|
||||
if (!LinkedPin || !LinkedPin->GetOwningNode()) continue;
|
||||
|
||||
UEdGraphNode* SourceNode = LinkedPin->GetOwningNode();
|
||||
FString NodeDesc;
|
||||
|
||||
// Check if this is a material graph node
|
||||
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(SourceNode))
|
||||
{
|
||||
UMaterialExpression* Expr = MatNode->MaterialExpression;
|
||||
if (!Expr)
|
||||
{
|
||||
NodeDesc = TEXT("(null expression)");
|
||||
}
|
||||
else if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||
{
|
||||
NodeDesc = FString::Printf(TEXT("ScalarParam \"%s\" (default: %.4f)"), *SP->ParameterName.ToString(), SP->DefaultValue);
|
||||
}
|
||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||
{
|
||||
NodeDesc = FString::Printf(TEXT("VectorParam \"%s\" (default: R=%.2f G=%.2f B=%.2f A=%.2f)"),
|
||||
*VP->ParameterName.ToString(), VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
|
||||
}
|
||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||
{
|
||||
FString TexName = TP->Texture ? TP->Texture->GetName() : TEXT("None");
|
||||
NodeDesc = FString::Printf(TEXT("TextureParam \"%s\" (%s)"), *TP->ParameterName.ToString(), *TexName);
|
||||
}
|
||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||
{
|
||||
NodeDesc = FString::Printf(TEXT("StaticSwitchParam \"%s\" (default: %s)"),
|
||||
*SSP->ParameterName.ToString(), SSP->DefaultValue ? TEXT("true") : TEXT("false"));
|
||||
}
|
||||
else if (auto* SC = Cast<UMaterialExpressionConstant>(Expr))
|
||||
{
|
||||
NodeDesc = FString::Printf(TEXT("Constant(%.4f)"), SC->R);
|
||||
}
|
||||
else if (auto* C3 = Cast<UMaterialExpressionConstant3Vector>(Expr))
|
||||
{
|
||||
NodeDesc = FString::Printf(TEXT("Constant3(R=%.2f G=%.2f B=%.2f)"), C3->Constant.R, C3->Constant.G, C3->Constant.B);
|
||||
}
|
||||
else if (auto* C4 = Cast<UMaterialExpressionConstant4Vector>(Expr))
|
||||
{
|
||||
NodeDesc = FString::Printf(TEXT("Constant4(R=%.2f G=%.2f B=%.2f A=%.2f)"), C4->Constant.R, C4->Constant.G, C4->Constant.B, C4->Constant.A);
|
||||
}
|
||||
else if (auto* TS = Cast<UMaterialExpressionTextureSample>(Expr))
|
||||
{
|
||||
FString TexName = TS->Texture ? TS->Texture->GetName() : TEXT("None");
|
||||
NodeDesc = FString::Printf(TEXT("TextureSample(%s)"), *TexName);
|
||||
}
|
||||
else if (auto* MFC = Cast<UMaterialExpressionMaterialFunctionCall>(Expr))
|
||||
{
|
||||
FString FuncName = MFC->MaterialFunction ? MFC->MaterialFunction->GetName() : TEXT("None");
|
||||
NodeDesc = FString::Printf(TEXT("FunctionCall(%s)"), *FuncName);
|
||||
}
|
||||
else
|
||||
{
|
||||
NodeDesc = Expr->GetClass()->GetName();
|
||||
}
|
||||
|
||||
// If the source node has input pins with connections, recurse
|
||||
TArray<FString> InputDescs;
|
||||
for (UEdGraphPin* InputPin : SourceNode->Pins)
|
||||
{
|
||||
if (!InputPin || InputPin->Direction != EGPD_Input || InputPin->LinkedTo.Num() == 0) continue;
|
||||
FString InputDesc = TracePin(InputPin, Depth + 1);
|
||||
InputDescs.Add(InputDesc);
|
||||
}
|
||||
|
||||
if (InputDescs.Num() > 0)
|
||||
{
|
||||
NodeDesc += TEXT(" <- (") + FString::Join(InputDescs, TEXT(", ")) + TEXT(")");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-material node (e.g., root, comment), just use title
|
||||
NodeDesc = SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||
}
|
||||
|
||||
Sources.Add(NodeDesc);
|
||||
}
|
||||
|
||||
if (Sources.Num() == 1)
|
||||
return Sources[0];
|
||||
|
||||
return TEXT("(") + FString::Join(Sources, TEXT(", ")) + TEXT(")");
|
||||
};
|
||||
|
||||
// Find root node and trace each input
|
||||
TArray<TSharedPtr<FJsonValue>> InputDescriptions;
|
||||
|
||||
UMaterialGraphNode_Root* RootNode = nullptr;
|
||||
for (UEdGraphNode* Node : Material->MaterialGraph->Nodes)
|
||||
{
|
||||
RootNode = Cast<UMaterialGraphNode_Root>(Node);
|
||||
if (RootNode) break;
|
||||
}
|
||||
|
||||
if (!RootNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Could not find root node in material graph"));
|
||||
}
|
||||
|
||||
for (UEdGraphPin* Pin : RootNode->Pins)
|
||||
{
|
||||
if (!Pin || Pin->Direction != EGPD_Input) continue;
|
||||
|
||||
FString PinName = Pin->PinName.ToString();
|
||||
FString Description;
|
||||
|
||||
if (Pin->LinkedTo.Num() == 0)
|
||||
{
|
||||
Description = TEXT("(unconnected)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Description = TracePin(Pin, 0);
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
|
||||
InputObj->SetStringField(TEXT("input"), PinName);
|
||||
InputObj->SetStringField(TEXT("chain"), Description);
|
||||
InputObj->SetBoolField(TEXT("connected"), Pin->LinkedTo.Num() > 0);
|
||||
InputDescriptions.Add(MakeShared<FJsonValueObject>(InputObj));
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("material"), Material->GetName());
|
||||
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
||||
Result->SetArrayField(TEXT("inputs"), InputDescriptions);
|
||||
|
||||
// Also include a compact text description
|
||||
FString TextDesc;
|
||||
for (const TSharedPtr<FJsonValue>& Val : InputDescriptions)
|
||||
{
|
||||
TSharedPtr<FJsonObject> Obj = Val->AsObject();
|
||||
if (!Obj.IsValid()) continue;
|
||||
FString InputName = Obj->GetStringField(TEXT("input"));
|
||||
FString Chain = Obj->GetStringField(TEXT("chain"));
|
||||
bool bConnected = Obj->GetBoolField(TEXT("connected"));
|
||||
if (bConnected)
|
||||
{
|
||||
TextDesc += FString::Printf(TEXT("%s <- %s\n"), *InputName, *Chain);
|
||||
}
|
||||
}
|
||||
if (!TextDesc.IsEmpty())
|
||||
{
|
||||
Result->SetStringField(TEXT("description"), TextDesc);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleSearchMaterials — search expressions and parameters
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleSearchMaterials(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Query = Json->GetStringField(TEXT("query"));
|
||||
if (Query.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'query' parameter"));
|
||||
}
|
||||
|
||||
FString DecodedQuery = MCPUtils::UrlDecode(Query);
|
||||
|
||||
int32 MaxResults = 50;
|
||||
if (Json->HasField(TEXT("maxResults")))
|
||||
{
|
||||
MaxResults = FMath::Clamp((int32)Json->GetNumberField(TEXT("maxResults")), 1, 200);
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Results;
|
||||
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UMaterial::StaticClass()))
|
||||
{
|
||||
if (Results.Num() >= MaxResults) break;
|
||||
|
||||
FString MatName = Asset.AssetName.ToString();
|
||||
|
||||
// Check material name first
|
||||
bool bNameMatch = MatName.Contains(DecodedQuery, ESearchCase::IgnoreCase);
|
||||
|
||||
UMaterial* Material = Cast<UMaterial>(const_cast<FAssetData&>(Asset).GetAsset());
|
||||
if (!Material) continue;
|
||||
|
||||
auto Expressions = Material->GetExpressions();
|
||||
|
||||
if (bNameMatch)
|
||||
{
|
||||
// Add a match for the material itself
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("material"), MatName);
|
||||
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
|
||||
R->SetStringField(TEXT("matchType"), TEXT("materialName"));
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
|
||||
// Search expressions
|
||||
for (UMaterialExpression* Expr : Expressions)
|
||||
{
|
||||
if (!Expr || Results.Num() >= MaxResults) continue;
|
||||
|
||||
FString ExprDesc = Expr->GetDescription();
|
||||
FString ExprClass = Expr->GetClass()->GetName();
|
||||
|
||||
// Check parameter name
|
||||
FString ParamName;
|
||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||
ParamName = SP->ParameterName.ToString();
|
||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||
ParamName = VP->ParameterName.ToString();
|
||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||
ParamName = TP->ParameterName.ToString();
|
||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||
ParamName = SSP->ParameterName.ToString();
|
||||
|
||||
bool bExprMatch = ExprDesc.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
|
||||
ExprClass.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
|
||||
(!ParamName.IsEmpty() && ParamName.Contains(DecodedQuery, ESearchCase::IgnoreCase));
|
||||
|
||||
if (bExprMatch)
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("material"), MatName);
|
||||
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
|
||||
R->SetStringField(TEXT("matchType"), TEXT("expression"));
|
||||
R->SetStringField(TEXT("expressionClass"), ExprClass);
|
||||
if (!ExprDesc.IsEmpty())
|
||||
R->SetStringField(TEXT("description"), ExprDesc);
|
||||
if (!ParamName.IsEmpty())
|
||||
R->SetStringField(TEXT("parameterName"), ParamName);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("query"), DecodedQuery);
|
||||
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
||||
Result->SetArrayField(TEXT("results"), Results);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleFindMaterialReferences — find assets referencing a material
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleFindMaterialReferences(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString MaterialName = Json->GetStringField(TEXT("material"));
|
||||
if (MaterialName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
|
||||
}
|
||||
|
||||
// Try to find the material's package path
|
||||
FString PackagePath;
|
||||
FAssetData* MatAsset = UMCPAssetFinder::FindAsset(UMaterial::StaticClass(), MaterialName);
|
||||
if (MatAsset)
|
||||
{
|
||||
PackagePath = MatAsset->PackageName.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try material instance
|
||||
FAssetData* MIAsset = UMCPAssetFinder::FindAsset(UMaterialInstanceConstant::StaticClass(), MaterialName);
|
||||
if (MIAsset)
|
||||
{
|
||||
PackagePath = MIAsset->PackageName.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
if (PackagePath.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material '%s' not found. Use list_materials to see available assets."), *MaterialName));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindMaterialReferences — '%s' (package: %s)"), *MaterialName, *PackagePath);
|
||||
|
||||
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
||||
|
||||
TArray<FName> Referencers;
|
||||
Registry.GetReferencers(FName(*PackagePath), Referencers);
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> RefArray;
|
||||
for (const FName& Ref : Referencers)
|
||||
{
|
||||
FString RefStr = Ref.ToString();
|
||||
// Skip self-reference
|
||||
if (RefStr == PackagePath) continue;
|
||||
RefArray.Add(MakeShared<FJsonValueString>(RefStr));
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("material"), MaterialName);
|
||||
Result->SetStringField(TEXT("packagePath"), PackagePath);
|
||||
Result->SetNumberField(TEXT("totalReferencers"), RefArray.Num());
|
||||
Result->SetArrayField(TEXT("referencers"), RefArray);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleListMaterialFunctions — list MaterialFunction assets
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleListMaterialFunctions(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Filter = Json->GetStringField(TEXT("filter"));
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Entries;
|
||||
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UMaterialFunction::StaticClass()))
|
||||
{
|
||||
FString Name = Asset.AssetName.ToString();
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
|
||||
if (!Filter.IsEmpty())
|
||||
{
|
||||
if (!Name.Contains(Filter, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("name"), Name);
|
||||
Entry->SetStringField(TEXT("path"), Path);
|
||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
}
|
||||
|
||||
Result->SetNumberField(TEXT("count"), Entries.Num());
|
||||
Result->SetNumberField(TEXT("total"), UMCPAssetFinder::GetAssets(UMaterialFunction::StaticClass()).Num());
|
||||
Result->SetArrayField(TEXT("functions"), Entries);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleGetMaterialFunction — detailed info about a material function
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleGetMaterialFunction(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Name = Json->GetStringField(TEXT("name"));
|
||||
if (Name.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
|
||||
}
|
||||
|
||||
FString DecodedName = MCPUtils::UrlDecode(Name);
|
||||
|
||||
UMaterialFunction* MF = UMCPAssetFinder::LoadAsset<UMaterialFunction>(DecodedName, Result);
|
||||
if (!MF) return;
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName());
|
||||
|
||||
Result->SetStringField(TEXT("name"), MF->GetName());
|
||||
Result->SetStringField(TEXT("path"), MF->GetPathName());
|
||||
Result->SetStringField(TEXT("description"), MF->GetDescription());
|
||||
|
||||
// Expression count
|
||||
auto Expressions = MF->GetExpressions();
|
||||
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
|
||||
|
||||
// List function inputs and outputs from expressions
|
||||
TArray<TSharedPtr<FJsonValue>> Inputs;
|
||||
TArray<TSharedPtr<FJsonValue>> Outputs;
|
||||
TArray<TSharedPtr<FJsonValue>> ExpressionList;
|
||||
|
||||
{
|
||||
for (UMaterialExpression* Expr : Expressions)
|
||||
{
|
||||
if (!Expr) continue;
|
||||
|
||||
if (auto* FI = Cast<UMaterialExpressionFunctionInput>(Expr))
|
||||
{
|
||||
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
|
||||
InputObj->SetStringField(TEXT("name"), FI->InputName.ToString());
|
||||
InputObj->SetStringField(TEXT("type"), TEXT("FunctionInput"));
|
||||
InputObj->SetNumberField(TEXT("posX"), FI->MaterialExpressionEditorX);
|
||||
InputObj->SetNumberField(TEXT("posY"), FI->MaterialExpressionEditorY);
|
||||
Inputs.Add(MakeShared<FJsonValueObject>(InputObj));
|
||||
}
|
||||
else if (auto* FO = Cast<UMaterialExpressionFunctionOutput>(Expr))
|
||||
{
|
||||
TSharedRef<FJsonObject> OutputObj = MakeShared<FJsonObject>();
|
||||
OutputObj->SetStringField(TEXT("name"), FO->OutputName.ToString());
|
||||
OutputObj->SetStringField(TEXT("type"), TEXT("FunctionOutput"));
|
||||
OutputObj->SetNumberField(TEXT("posX"), FO->MaterialExpressionEditorX);
|
||||
OutputObj->SetNumberField(TEXT("posY"), FO->MaterialExpressionEditorY);
|
||||
Outputs.Add(MakeShared<FJsonValueObject>(OutputObj));
|
||||
}
|
||||
|
||||
// Serialize every expression
|
||||
TSharedPtr<FJsonObject> ExprJson = MCPUtils::SerializeMaterialExpression(Expr);
|
||||
if (ExprJson.IsValid())
|
||||
{
|
||||
ExpressionList.Add(MakeShared<FJsonValueObject>(ExprJson.ToSharedRef()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetArrayField(TEXT("inputs"), Inputs);
|
||||
Result->SetArrayField(TEXT("outputs"), Outputs);
|
||||
Result->SetArrayField(TEXT("expressions"), ExpressionList);
|
||||
|
||||
// If the function has an editor graph, serialize it
|
||||
UEdGraph* FuncGraph = MF->MaterialGraph;
|
||||
if (FuncGraph)
|
||||
{
|
||||
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(FuncGraph);
|
||||
if (GraphJson.IsValid())
|
||||
{
|
||||
Result->SetObjectField(TEXT("graph"), GraphJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleValidateMaterial — force recompile and check for errors
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleValidateMaterial(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString MaterialName = Json->GetStringField(TEXT("material"));
|
||||
if (MaterialName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: material"));
|
||||
}
|
||||
|
||||
// Load material
|
||||
UMaterial* Material = UMCPAssetFinder::LoadAsset<UMaterial>(MaterialName, Result);
|
||||
if (!Material) return;
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *Material->GetName());
|
||||
|
||||
// Force recompile by triggering PreEditChange/PostEditChange
|
||||
Material->PreEditChange(nullptr);
|
||||
Material->PostEditChange();
|
||||
|
||||
// Collect compilation errors
|
||||
TArray<TSharedPtr<FJsonValue>> ErrorArray;
|
||||
bool bValid = true;
|
||||
|
||||
// Check for compilation errors via FMaterialResource on current platform
|
||||
FMaterialResource* Resource = Material->GetMaterialResource(GMaxRHIFeatureLevel);
|
||||
if (Resource)
|
||||
{
|
||||
const TArray<FString>& CompileErrors = Resource->GetCompileErrors();
|
||||
for (const FString& Err : CompileErrors)
|
||||
{
|
||||
bValid = false;
|
||||
ErrorArray.Add(MakeShared<FJsonValueString>(Err));
|
||||
}
|
||||
}
|
||||
|
||||
// Count expressions and connections
|
||||
auto Expressions = Material->GetExpressions();
|
||||
int32 ExprCount = Expressions.Num();
|
||||
int32 ConnectionCount = 0;
|
||||
if (Material->MaterialGraph)
|
||||
{
|
||||
for (UEdGraphNode* Node : Material->MaterialGraph->Nodes)
|
||||
{
|
||||
if (!Node) continue;
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (Pin && Pin->Direction == EGPD_Output)
|
||||
{
|
||||
ConnectionCount += Pin->LinkedTo.Num();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("valid"), bValid);
|
||||
Result->SetStringField(TEXT("material"), Material->GetName());
|
||||
Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
|
||||
Result->SetNumberField(TEXT("expressionCount"), ExprCount);
|
||||
Result->SetNumberField(TEXT("connectionCount"), ConnectionCount);
|
||||
Result->SetArrayField(TEXT("errors"), ErrorArray);
|
||||
Result->SetNumberField(TEXT("errorCount"), ErrorArray.Num());
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material '%s' validation %s (%d errors)"),
|
||||
*Material->GetName(), bValid ? TEXT("passed") : TEXT("failed"), ErrorArray.Num());
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -60,7 +60,7 @@ struct FMoveNodeEntry
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
FString NodeId;
|
||||
FString Node;
|
||||
|
||||
UPROPERTY()
|
||||
int32 X = 0;
|
||||
@@ -113,12 +113,12 @@ public:
|
||||
continue;
|
||||
}
|
||||
|
||||
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
|
||||
EntryResult->SetStringField(TEXT("nodeId"), Entry.Node);
|
||||
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId);
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node);
|
||||
if (!Node)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ public:
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Array of node GUIDs to duplicate"))
|
||||
FMCPJsonArray NodeIds;
|
||||
FMCPJsonArray Nodes;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="X offset for duplicated nodes"))
|
||||
int32 OffsetX = 50;
|
||||
@@ -209,7 +209,7 @@ public:
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||
}
|
||||
|
||||
if (NodeIds.Array.Num() == 0)
|
||||
if (Nodes.Array.Num() == 0)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("nodeIds array is empty"));
|
||||
}
|
||||
@@ -218,24 +218,24 @@ public:
|
||||
TArray<UEdGraphNode*> SourceNodes;
|
||||
TArray<FString> NotFound;
|
||||
|
||||
for (const TSharedPtr<FJsonValue>& IdVal : NodeIds.Array)
|
||||
for (const TSharedPtr<FJsonValue>& IdVal : Nodes.Array)
|
||||
{
|
||||
FString NodeId = IdVal->AsString();
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId);
|
||||
if (Node)
|
||||
FString Node = IdVal->AsString();
|
||||
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
|
||||
if (FoundNode)
|
||||
{
|
||||
if (Node->GetGraph() == TargetGraph)
|
||||
if (FoundNode->GetGraph() == TargetGraph)
|
||||
{
|
||||
SourceNodes.Add(Node);
|
||||
SourceNodes.Add(FoundNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotFound.Add(FString::Printf(TEXT("%s (in different graph)"), *NodeId));
|
||||
NotFound.Add(FString::Printf(TEXT("%s (in different graph)"), *Node));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
NotFound.Add(NodeId);
|
||||
NotFound.Add(Node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +494,7 @@ public:
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Node GUID"))
|
||||
FString NodeId;
|
||||
FString Node;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
@@ -511,17 +511,17 @@ public:
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId);
|
||||
if (!Node)
|
||||
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
|
||||
if (!FoundNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("nodeId"), NodeId);
|
||||
Result->SetStringField(TEXT("comment"), Node->NodeComment);
|
||||
Result->SetBoolField(TEXT("commentBubbleVisible"), Node->bCommentBubbleVisible);
|
||||
Result->SetStringField(TEXT("nodeId"), Node);
|
||||
Result->SetStringField(TEXT("comment"), FoundNode->NodeComment);
|
||||
Result->SetBoolField(TEXT("commentBubbleVisible"), FoundNode->bCommentBubbleVisible);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -539,7 +539,7 @@ public:
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Node GUID"))
|
||||
FString NodeId;
|
||||
FString Node;
|
||||
|
||||
UPROPERTY(meta=(Description="Comment text to set"))
|
||||
FString Comment;
|
||||
@@ -559,30 +559,30 @@ public:
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId);
|
||||
if (!Node)
|
||||
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
|
||||
if (!FoundNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
||||
}
|
||||
|
||||
FString OldComment = Node->NodeComment;
|
||||
Node->NodeComment = Comment;
|
||||
FString OldComment = FoundNode->NodeComment;
|
||||
FoundNode->NodeComment = Comment;
|
||||
|
||||
// Make the comment bubble visible if setting a non-empty comment
|
||||
if (!Comment.IsEmpty())
|
||||
{
|
||||
Node->bCommentBubbleVisible = true;
|
||||
Node->bCommentBubblePinned = true;
|
||||
FoundNode->bCommentBubbleVisible = true;
|
||||
FoundNode->bCommentBubblePinned = true;
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s' in '%s'"),
|
||||
*NodeId, *Blueprint);
|
||||
*Node, *Blueprint);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("nodeId"), NodeId);
|
||||
Result->SetStringField(TEXT("nodeId"), Node);
|
||||
Result->SetStringField(TEXT("oldComment"), OldComment);
|
||||
Result->SetStringField(TEXT("newComment"), Comment);
|
||||
}
|
||||
@@ -602,7 +602,7 @@ public:
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Node GUID"))
|
||||
FString NodeId;
|
||||
FString Node;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
@@ -621,24 +621,24 @@ public:
|
||||
}
|
||||
|
||||
UEdGraph* Graph = nullptr;
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph);
|
||||
if (!Node)
|
||||
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
|
||||
if (!FoundNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
||||
}
|
||||
if (!Graph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *NodeId));
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *Node));
|
||||
}
|
||||
|
||||
FString NodeClass = Node->GetClass()->GetName();
|
||||
FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||
FString NodeClass = FoundNode->GetClass()->GetName();
|
||||
FString NodeTitle = FoundNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||
FString GraphName = Graph->GetName();
|
||||
|
||||
// Protect root/entry nodes — deleting these leaves the graph in an invalid
|
||||
// state with no root node, causing compiler errors that can't be fixed
|
||||
// without recreating the entire function/event.
|
||||
if (Cast<UK2Node_FunctionEntry>(Node))
|
||||
if (Cast<UK2Node_FunctionEntry>(FoundNode))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot delete FunctionEntry node '%s' in graph '%s'. ")
|
||||
@@ -646,14 +646,14 @@ public:
|
||||
TEXT("To remove the entire function, delete it from the Blueprint editor."),
|
||||
*NodeTitle, *GraphName));
|
||||
}
|
||||
if (Cast<UK2Node_Event>(Node))
|
||||
if (Cast<UK2Node_Event>(FoundNode))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot delete event entry node '%s' in graph '%s'. ")
|
||||
TEXT("This is the root node of the event handler — removing it would leave an empty, uncompilable graph."),
|
||||
*NodeTitle, *GraphName));
|
||||
}
|
||||
if (Cast<UK2Node_CustomEvent>(Node))
|
||||
if (Cast<UK2Node_CustomEvent>(FoundNode))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Cannot delete CustomEvent entry node '%s' in graph '%s'. ")
|
||||
@@ -662,10 +662,10 @@ public:
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting node '%s' (%s) from graph '%s' in '%s'"),
|
||||
*NodeId, *NodeTitle, *GraphName, *Blueprint);
|
||||
*Node, *NodeTitle, *GraphName, *Blueprint);
|
||||
|
||||
Node->BreakAllNodeLinks();
|
||||
Graph->RemoveNode(Node);
|
||||
FoundNode->BreakAllNodeLinks();
|
||||
Graph->RemoveNode(FoundNode);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
|
||||
@@ -673,7 +673,7 @@ public:
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("nodeId"), NodeId);
|
||||
Result->SetStringField(TEXT("nodeId"), Node);
|
||||
Result->SetStringField(TEXT("nodeClass"), NodeClass);
|
||||
Result->SetStringField(TEXT("nodeTitle"), NodeTitle);
|
||||
Result->SetStringField(TEXT("graph"), GraphName);
|
||||
@@ -1077,7 +1077,7 @@ public:
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Node GUID of the BreakStruct or MakeStruct node"))
|
||||
FString NodeId;
|
||||
FString Node;
|
||||
|
||||
UPROPERTY(meta=(Description="New struct type name (e.g. 'FVector', 'Vector')"))
|
||||
FString NewType;
|
||||
@@ -1101,20 +1101,20 @@ public:
|
||||
|
||||
// Find node
|
||||
UEdGraph* Graph = nullptr;
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, NodeId, &Graph);
|
||||
if (!Node)
|
||||
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
|
||||
if (!FoundNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
||||
}
|
||||
|
||||
// Determine what kind of struct node this is
|
||||
UK2Node_BreakStruct* BreakNode = Cast<UK2Node_BreakStruct>(Node);
|
||||
UK2Node_MakeStruct* MakeNode = Cast<UK2Node_MakeStruct>(Node);
|
||||
UK2Node_BreakStruct* BreakNode = Cast<UK2Node_BreakStruct>(FoundNode);
|
||||
UK2Node_MakeStruct* MakeNode = Cast<UK2Node_MakeStruct>(FoundNode);
|
||||
|
||||
if (!BreakNode && !MakeNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"),
|
||||
*NodeId, *Node->GetClass()->GetName()));
|
||||
*Node, *FoundNode->GetClass()->GetName()));
|
||||
}
|
||||
|
||||
// Find the new struct type
|
||||
@@ -1136,7 +1136,7 @@ public:
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing struct node '%s' to type '%s'"),
|
||||
*NodeId, *NewStruct->GetName());
|
||||
*Node, *NewStruct->GetName());
|
||||
|
||||
// Helper: extract property base name from a BreakStruct pin name
|
||||
auto ExtractPropertyBaseName = [](const FString& PinName) -> FString
|
||||
@@ -1172,7 +1172,7 @@ public:
|
||||
};
|
||||
TMap<FString, FPinConnection> ConnectionsByBaseName;
|
||||
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
for (UEdGraphPin* Pin : FoundNode->Pins)
|
||||
{
|
||||
if (!Pin || Pin->LinkedTo.Num() == 0) continue;
|
||||
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
|
||||
@@ -1196,7 +1196,7 @@ public:
|
||||
}
|
||||
|
||||
// Break all existing links before reconstruction
|
||||
Node->BreakAllNodeLinks();
|
||||
FoundNode->BreakAllNodeLinks();
|
||||
|
||||
// Reconnect pins by matching property base names
|
||||
const UEdGraphSchema* Schema = Graph->GetSchema();
|
||||
@@ -1206,7 +1206,7 @@ public:
|
||||
}
|
||||
|
||||
// Reconstruct to rebuild pins for the new struct type (use schema version for MinimalAPI compat)
|
||||
Schema->ReconstructNode(*Node);
|
||||
Schema->ReconstructNode(*FoundNode);
|
||||
|
||||
int32 Reconnected = 0;
|
||||
int32 Failed = 0;
|
||||
@@ -1219,7 +1219,7 @@ public:
|
||||
|
||||
// Find matching new pin
|
||||
UEdGraphPin* NewPin = nullptr;
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
for (UEdGraphPin* Pin : FoundNode->Pins)
|
||||
{
|
||||
if (!Pin || Pin->Direction != OldConn.Direction) continue;
|
||||
FString NewBaseName = ExtractPropertyBaseName(Pin->PinName.ToString());
|
||||
@@ -1233,7 +1233,7 @@ public:
|
||||
// Also try matching the struct input/output pin (single struct pin)
|
||||
if (!NewPin)
|
||||
{
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
for (UEdGraphPin* Pin : FoundNode->Pins)
|
||||
{
|
||||
if (!Pin || Pin->Direction != OldConn.Direction) continue;
|
||||
if ((Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) &&
|
||||
@@ -1279,13 +1279,13 @@ public:
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
|
||||
// Return updated node state
|
||||
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(Node);
|
||||
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(FoundNode);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("nodeId"), NodeId);
|
||||
Result->SetStringField(TEXT("nodeId"), Node);
|
||||
Result->SetStringField(TEXT("newStructType"), NewStruct->GetName());
|
||||
Result->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
|
||||
Result->SetStringField(TEXT("nodeClass"), FoundNode->GetClass()->GetName());
|
||||
Result->SetNumberField(TEXT("reconnected"), Reconnected);
|
||||
Result->SetNumberField(TEXT("failed"), Failed);
|
||||
Result->SetArrayField(TEXT("reconnectDetails"), ReconnectDetails);
|
||||
|
||||
@@ -1,567 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "K2Node.h"
|
||||
#include "K2Node_FunctionEntry.h"
|
||||
#include "K2Node_CustomEvent.h"
|
||||
#include "K2Node_EditablePinBase.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
|
||||
void FBlueprintMCPServer::HandleChangeFunctionParamType(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString FunctionName = Json->GetStringField(TEXT("functionName"));
|
||||
FString ParamName = Json->GetStringField(TEXT("paramName"));
|
||||
FString NewTypeName = Json->GetStringField(TEXT("newType"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty() || NewTypeName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, newType"));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references)
|
||||
FEdGraphPinType NewPinType;
|
||||
if (!MCPUtils::ResolveTypeFromString(NewTypeName, NewPinType, Result))
|
||||
return;
|
||||
|
||||
// Find the entry node: K2Node_FunctionEntry in a function graph,
|
||||
// or K2Node_CustomEvent in any graph
|
||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||
FString FoundNodeType;
|
||||
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
// Strategy 1: Look for a function graph matching the name
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FuncEntry;
|
||||
FoundNodeType = TEXT("FunctionEntry");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
||||
if (!EntryNode)
|
||||
{
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
EntryNode = CustomEvent;
|
||||
FoundNodeType = TEXT("CustomEvent");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!EntryNode)
|
||||
{
|
||||
// List available functions/events for debugging
|
||||
TArray<TSharedPtr<FJsonValue>> Available;
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
Available.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("function:%s"), *Graph->GetName())));
|
||||
}
|
||||
else if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
Available.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
||||
*FunctionName, *BlueprintName));
|
||||
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the UserDefinedPin matching paramName
|
||||
bool bPinFound = false;
|
||||
for (TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
||||
{
|
||||
if (PinInfo.IsValid() && PinInfo->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
PinInfo->PinType = NewPinType;
|
||||
bPinFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bPinFound)
|
||||
{
|
||||
// List available params for debugging
|
||||
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
||||
{
|
||||
if (PinInfo.IsValid())
|
||||
{
|
||||
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parameter '%s' not found in %s '%s'"),
|
||||
*ParamName, *FoundNodeType, *FunctionName));
|
||||
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for dry run
|
||||
bool bDryRun = false;
|
||||
if (Json->HasField(TEXT("dryRun")))
|
||||
{
|
||||
bDryRun = Json->GetBoolField(TEXT("dryRun"));
|
||||
}
|
||||
|
||||
if (bDryRun)
|
||||
{
|
||||
// Analyze what would change: report connected pins that may disconnect
|
||||
TArray<TSharedPtr<FJsonValue>> AffectedPins;
|
||||
for (UEdGraphPin* Pin : EntryNode->Pins)
|
||||
{
|
||||
if (Pin && Pin->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase) && Pin->LinkedTo.Num() > 0)
|
||||
{
|
||||
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
||||
{
|
||||
if (Linked && Linked->GetOwningNode())
|
||||
{
|
||||
TSharedRef<FJsonObject> AffPin = MakeShared<FJsonObject>();
|
||||
AffPin->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
|
||||
AffPin->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString());
|
||||
AffPin->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString());
|
||||
AffPin->SetStringField(TEXT("currentType"), Pin->PinType.PinCategory.ToString());
|
||||
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
||||
AffPin->SetStringField(TEXT("currentSubtype"), Pin->PinType.PinSubCategoryObject->GetName());
|
||||
AffectedPins.Add(MakeShared<FJsonValueObject>(AffPin));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("dryRun"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("functionName"), FunctionName);
|
||||
Result->SetStringField(TEXT("paramName"), ParamName);
|
||||
Result->SetStringField(TEXT("newType"), NewTypeName);
|
||||
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
||||
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
||||
Result->SetNumberField(TEXT("connectionsAtRisk"), AffectedPins.Num());
|
||||
Result->SetArrayField(TEXT("affectedPins"), AffectedPins);
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing param '%s' in %s '%s' of '%s' to %s"),
|
||||
*ParamName, *FoundNodeType, *FunctionName, *BlueprintName, *NewTypeName);
|
||||
|
||||
// Reconstruct the node to update output pins with the new type (use schema for MinimalAPI compat)
|
||||
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
||||
{
|
||||
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
||||
{
|
||||
Schema->ReconstructNode(*EntryNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"),
|
||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||
|
||||
// Serialize the updated entry node state
|
||||
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(EntryNode);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("functionName"), FunctionName);
|
||||
Result->SetStringField(TEXT("paramName"), ParamName);
|
||||
Result->SetStringField(TEXT("newType"), NewTypeName);
|
||||
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
||||
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
if (UpdatedNodeState.IsValid())
|
||||
{
|
||||
Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleRemoveFunctionParameter
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString FunctionName = Json->GetStringField(TEXT("functionName"));
|
||||
FString ParamName = Json->GetStringField(TEXT("paramName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName"));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find the entry node
|
||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||
FString FoundNodeType;
|
||||
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
// Strategy 1: Look for a function graph matching the name
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FuncEntry;
|
||||
FoundNodeType = TEXT("FunctionEntry");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
||||
if (!EntryNode)
|
||||
{
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
EntryNode = CustomEvent;
|
||||
FoundNodeType = TEXT("CustomEvent");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!EntryNode)
|
||||
{
|
||||
// List available functions/events for debugging
|
||||
TArray<TSharedPtr<FJsonValue>> Available;
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
Available.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("function:%s"), *Graph->GetName())));
|
||||
}
|
||||
else if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
Available.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
||||
*FunctionName, *BlueprintName));
|
||||
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find and remove the UserDefinedPin matching paramName
|
||||
int32 RemovedIndex = INDEX_NONE;
|
||||
for (int32 i = 0; i < EntryNode->UserDefinedPins.Num(); ++i)
|
||||
{
|
||||
if (EntryNode->UserDefinedPins[i].IsValid() &&
|
||||
EntryNode->UserDefinedPins[i]->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
RemovedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (RemovedIndex == INDEX_NONE)
|
||||
{
|
||||
// List available params for debugging
|
||||
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
||||
{
|
||||
if (PinInfo.IsValid())
|
||||
{
|
||||
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parameter '%s' not found in %s '%s'"),
|
||||
*ParamName, *FoundNodeType, *FunctionName));
|
||||
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing param '%s' from %s '%s' in '%s'"),
|
||||
*ParamName, *FoundNodeType, *FunctionName, *BlueprintName);
|
||||
|
||||
// Remove the pin
|
||||
EntryNode->UserDefinedPins.RemoveAt(RemovedIndex);
|
||||
|
||||
// Reconstruct the node to update output pins (use schema for MinimalAPI compat)
|
||||
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
||||
{
|
||||
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
||||
{
|
||||
Schema->ReconstructNode(*EntryNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"),
|
||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("functionName"), FunctionName);
|
||||
Result->SetStringField(TEXT("paramName"), ParamName);
|
||||
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
||||
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleAddFunctionParameter
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleAddFunctionParameter(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString FunctionName = Json->GetStringField(TEXT("functionName"));
|
||||
FString ParamName = Json->GetStringField(TEXT("paramName"));
|
||||
FString ParamType = Json->GetStringField(TEXT("paramType"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || FunctionName.IsEmpty() || ParamName.IsEmpty() || ParamType.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, functionName, paramName, paramType"));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Resolve param type
|
||||
FEdGraphPinType PinType;
|
||||
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
|
||||
return;
|
||||
|
||||
// Find the entry node using 3 strategies
|
||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||
FString NodeType;
|
||||
|
||||
FName FuncFName(*FunctionName);
|
||||
|
||||
// Strategy 1: K2Node_FunctionEntry in function graphs
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph || !Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip delegate signature graphs (handled in Strategy 3)
|
||||
if (BP->DelegateSignatureGraphs.Contains(Graph))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FE;
|
||||
NodeType = TEXT("FunctionEntry");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
|
||||
// Strategy 2: K2Node_CustomEvent with matching CustomFunctionName
|
||||
if (!EntryNode)
|
||||
{
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
EntryNode = CE;
|
||||
NodeType = TEXT("CustomEvent");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs
|
||||
if (!EntryNode)
|
||||
{
|
||||
for (UEdGraph* SigGraph : BP->DelegateSignatureGraphs)
|
||||
{
|
||||
if (!SigGraph || !SigGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (UEdGraphNode* Node : SigGraph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FE;
|
||||
NodeType = TEXT("EventDispatcher");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!EntryNode)
|
||||
{
|
||||
// Build a helpful error listing available functions, events, and dispatchers
|
||||
TArray<TSharedPtr<FJsonValue>> AvailFuncs;
|
||||
|
||||
for (UEdGraph* Graph : BP->FunctionGraphs)
|
||||
{
|
||||
if (Graph) AvailFuncs.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
}
|
||||
|
||||
// Custom events
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatchers
|
||||
TSet<FName> DelegateNames;
|
||||
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNames);
|
||||
for (const FName& DN : DelegateNames)
|
||||
{
|
||||
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("%s (event dispatcher)"), *DN.ToString())));
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"),
|
||||
*FunctionName, *BlueprintName));
|
||||
Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate parameter name
|
||||
for (const TSharedPtr<FUserPinInfo>& Existing : EntryNode->UserDefinedPins)
|
||||
{
|
||||
if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding parameter '%s' (type=%s) to %s '%s' in Blueprint '%s'"),
|
||||
*ParamName, *ParamType, *NodeType, *FunctionName, *BlueprintName);
|
||||
|
||||
// Add the parameter pin (EGPD_Output on entry = input to callers)
|
||||
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"),
|
||||
*ParamName, *FunctionName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("functionName"), FunctionName);
|
||||
Result->SetStringField(TEXT("paramName"), ParamName);
|
||||
Result->SetStringField(TEXT("paramType"), ParamType);
|
||||
Result->SetStringField(TEXT("nodeType"), NodeType);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
@@ -0,0 +1,606 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "K2Node_FunctionEntry.h"
|
||||
#include "K2Node_CustomEvent.h"
|
||||
#include "K2Node_EditablePinBase.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "MCPHandlers_Params.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="change_function_parameter_type"))
|
||||
class UMCPHandler_ChangeFunctionParamType : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the function or custom event"))
|
||||
FString FunctionName;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the parameter to change"))
|
||||
FString ParamName;
|
||||
|
||||
UPROPERTY(meta=(Description="New type for the parameter (e.g. 'Float', 'Vector', 'MyStruct')"))
|
||||
FString NewType;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, analyze impact without making changes"))
|
||||
bool DryRun = false;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Change the type of an existing parameter on a function or custom event in a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references)
|
||||
FEdGraphPinType NewPinType;
|
||||
if (!MCPUtils::ResolveTypeFromString(NewType, NewPinType, Result))
|
||||
return;
|
||||
|
||||
// Find the entry node: K2Node_FunctionEntry in a function graph,
|
||||
// or K2Node_CustomEvent in any graph
|
||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||
FString FoundNodeType;
|
||||
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
// Strategy 1: Look for a function graph matching the name
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FuncEntry;
|
||||
FoundNodeType = TEXT("FunctionEntry");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
||||
if (!EntryNode)
|
||||
{
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
EntryNode = CustomEvent;
|
||||
FoundNodeType = TEXT("CustomEvent");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!EntryNode)
|
||||
{
|
||||
// List available functions/events for debugging
|
||||
TArray<TSharedPtr<FJsonValue>> Available;
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
Available.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("function:%s"), *Graph->GetName())));
|
||||
}
|
||||
else if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
Available.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
||||
*FunctionName, *Blueprint));
|
||||
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the UserDefinedPin matching paramName
|
||||
bool bPinFound = false;
|
||||
for (TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
||||
{
|
||||
if (PinInfo.IsValid() && PinInfo->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
PinInfo->PinType = NewPinType;
|
||||
bPinFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bPinFound)
|
||||
{
|
||||
// List available params for debugging
|
||||
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
||||
{
|
||||
if (PinInfo.IsValid())
|
||||
{
|
||||
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parameter '%s' not found in %s '%s'"),
|
||||
*ParamName, *FoundNodeType, *FunctionName));
|
||||
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for dry run
|
||||
if (DryRun)
|
||||
{
|
||||
// Analyze what would change: report connected pins that may disconnect
|
||||
TArray<TSharedPtr<FJsonValue>> AffectedPins;
|
||||
for (UEdGraphPin* Pin : EntryNode->Pins)
|
||||
{
|
||||
if (Pin && Pin->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase) && Pin->LinkedTo.Num() > 0)
|
||||
{
|
||||
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
||||
{
|
||||
if (Linked && Linked->GetOwningNode())
|
||||
{
|
||||
TSharedRef<FJsonObject> AffPin = MakeShared<FJsonObject>();
|
||||
AffPin->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
|
||||
AffPin->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString());
|
||||
AffPin->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString());
|
||||
AffPin->SetStringField(TEXT("currentType"), Pin->PinType.PinCategory.ToString());
|
||||
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
||||
AffPin->SetStringField(TEXT("currentSubtype"), Pin->PinType.PinSubCategoryObject->GetName());
|
||||
AffectedPins.Add(MakeShared<FJsonValueObject>(AffPin));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("dryRun"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("functionName"), FunctionName);
|
||||
Result->SetStringField(TEXT("paramName"), ParamName);
|
||||
Result->SetStringField(TEXT("newType"), NewType);
|
||||
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
||||
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
||||
Result->SetNumberField(TEXT("connectionsAtRisk"), AffectedPins.Num());
|
||||
Result->SetArrayField(TEXT("affectedPins"), AffectedPins);
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing param '%s' in %s '%s' of '%s' to %s"),
|
||||
*ParamName, *FoundNodeType, *FunctionName, *Blueprint, *NewType);
|
||||
|
||||
// Reconstruct the node to update output pins with the new type (use schema for MinimalAPI compat)
|
||||
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
||||
{
|
||||
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
||||
{
|
||||
Schema->ReconstructNode(*EntryNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"),
|
||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||
|
||||
// Serialize the updated entry node state
|
||||
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(EntryNode);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("functionName"), FunctionName);
|
||||
Result->SetStringField(TEXT("paramName"), ParamName);
|
||||
Result->SetStringField(TEXT("newType"), NewType);
|
||||
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
||||
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
if (UpdatedNodeState.IsValid())
|
||||
{
|
||||
Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="remove_function_parameter"))
|
||||
class UMCPHandler_RemoveFunctionParameter : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the function or custom event"))
|
||||
FString FunctionName;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the parameter to remove"))
|
||||
FString ParamName;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Remove a parameter from a function or custom event in a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find the entry node
|
||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||
FString FoundNodeType;
|
||||
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
// Strategy 1: Look for a function graph matching the name
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FuncEntry;
|
||||
FoundNodeType = TEXT("FunctionEntry");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
||||
if (!EntryNode)
|
||||
{
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
EntryNode = CustomEvent;
|
||||
FoundNodeType = TEXT("CustomEvent");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!EntryNode)
|
||||
{
|
||||
// List available functions/events for debugging
|
||||
TArray<TSharedPtr<FJsonValue>> Available;
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
Available.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("function:%s"), *Graph->GetName())));
|
||||
}
|
||||
else if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
Available.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
||||
*FunctionName, *Blueprint));
|
||||
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find and remove the UserDefinedPin matching paramName
|
||||
int32 RemovedIndex = INDEX_NONE;
|
||||
for (int32 i = 0; i < EntryNode->UserDefinedPins.Num(); ++i)
|
||||
{
|
||||
if (EntryNode->UserDefinedPins[i].IsValid() &&
|
||||
EntryNode->UserDefinedPins[i]->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
RemovedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (RemovedIndex == INDEX_NONE)
|
||||
{
|
||||
// List available params for debugging
|
||||
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
||||
{
|
||||
if (PinInfo.IsValid())
|
||||
{
|
||||
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parameter '%s' not found in %s '%s'"),
|
||||
*ParamName, *FoundNodeType, *FunctionName));
|
||||
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing param '%s' from %s '%s' in '%s'"),
|
||||
*ParamName, *FoundNodeType, *FunctionName, *Blueprint);
|
||||
|
||||
// Remove the pin
|
||||
EntryNode->UserDefinedPins.RemoveAt(RemovedIndex);
|
||||
|
||||
// Reconstruct the node to update output pins (use schema for MinimalAPI compat)
|
||||
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
||||
{
|
||||
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
||||
{
|
||||
Schema->ReconstructNode(*EntryNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"),
|
||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("functionName"), FunctionName);
|
||||
Result->SetStringField(TEXT("paramName"), ParamName);
|
||||
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
||||
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="add_function_parameter"))
|
||||
class UMCPHandler_AddFunctionParameter : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the function, custom event, or event dispatcher"))
|
||||
FString FunctionName;
|
||||
|
||||
UPROPERTY(meta=(Description="Name for the new parameter"))
|
||||
FString ParamName;
|
||||
|
||||
UPROPERTY(meta=(Description="Type for the new parameter (e.g. 'Float', 'Vector', 'MyStruct')"))
|
||||
FString ParamType;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Add a new parameter to a function, custom event, or event dispatcher in a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Resolve param type
|
||||
FEdGraphPinType PinType;
|
||||
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
|
||||
return;
|
||||
|
||||
// Find the entry node using 3 strategies
|
||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||
FString NodeType;
|
||||
|
||||
FName FuncFName(*FunctionName);
|
||||
|
||||
// Strategy 1: K2Node_FunctionEntry in function graphs
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph || !Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip delegate signature graphs (handled in Strategy 3)
|
||||
if (BP->DelegateSignatureGraphs.Contains(Graph))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FE;
|
||||
NodeType = TEXT("FunctionEntry");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
|
||||
// Strategy 2: K2Node_CustomEvent with matching CustomFunctionName
|
||||
if (!EntryNode)
|
||||
{
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
EntryNode = CE;
|
||||
NodeType = TEXT("CustomEvent");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs
|
||||
if (!EntryNode)
|
||||
{
|
||||
for (UEdGraph* SigGraph : BP->DelegateSignatureGraphs)
|
||||
{
|
||||
if (!SigGraph || !SigGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (UEdGraphNode* Node : SigGraph->Nodes)
|
||||
{
|
||||
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
EntryNode = FE;
|
||||
NodeType = TEXT("EventDispatcher");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (EntryNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!EntryNode)
|
||||
{
|
||||
// Build a helpful error listing available functions, events, and dispatchers
|
||||
TArray<TSharedPtr<FJsonValue>> AvailFuncs;
|
||||
|
||||
for (UEdGraph* Graph : BP->FunctionGraphs)
|
||||
{
|
||||
if (Graph) AvailFuncs.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
}
|
||||
|
||||
// Custom events
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatchers
|
||||
TSet<FName> DelegateNames;
|
||||
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNames);
|
||||
for (const FName& DN : DelegateNames)
|
||||
{
|
||||
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("%s (event dispatcher)"), *DN.ToString())));
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"),
|
||||
*FunctionName, *Blueprint));
|
||||
Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate parameter name
|
||||
for (const TSharedPtr<FUserPinInfo>& Existing : EntryNode->UserDefinedPins)
|
||||
{
|
||||
if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName));
|
||||
}
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding parameter '%s' (type=%s) to %s '%s' in Blueprint '%s'"),
|
||||
*ParamName, *ParamType, *NodeType, *FunctionName, *Blueprint);
|
||||
|
||||
// Add the parameter pin (EGPD_Output on entry = input to callers)
|
||||
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"),
|
||||
*ParamName, *FunctionName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("functionName"), FunctionName);
|
||||
Result->SetStringField(TEXT("paramName"), ParamName);
|
||||
Result->SetStringField(TEXT("paramType"), ParamType);
|
||||
Result->SetStringField(TEXT("nodeType"), NodeType);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
@@ -24,7 +24,7 @@ struct FSetPinDefaultEntry
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY()
|
||||
FString NodeId;
|
||||
FString Node;
|
||||
|
||||
UPROPERTY()
|
||||
FString PinName;
|
||||
@@ -40,7 +40,7 @@ class UMCPHandler_SetPinDefaultValues : public UObject, public IMCPHandler
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Array of {blueprint, nodeId, pinName, value} objects"))
|
||||
UPROPERTY(meta=(Description="Array of {blueprint, node, pinName, value} objects"))
|
||||
FMCPJsonArray Pins;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
@@ -70,7 +70,7 @@ public:
|
||||
}
|
||||
|
||||
EntryResult->SetStringField(TEXT("blueprint"), Entry.Blueprint);
|
||||
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
|
||||
EntryResult->SetStringField(TEXT("nodeId"), Entry.Node);
|
||||
EntryResult->SetStringField(TEXT("pinName"), Entry.PinName);
|
||||
|
||||
FString LoadError;
|
||||
@@ -82,17 +82,17 @@ public:
|
||||
}
|
||||
|
||||
UEdGraph* Graph = nullptr;
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId, &Graph);
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node, &Graph);
|
||||
if (!Node)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
|
||||
continue;
|
||||
}
|
||||
|
||||
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
|
||||
if (!Pin)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.NodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -155,13 +155,13 @@ struct FConnectPinsEntry
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
FString SourceNodeId;
|
||||
FString SourceNode;
|
||||
|
||||
UPROPERTY()
|
||||
FString SourcePinName;
|
||||
|
||||
UPROPERTY()
|
||||
FString TargetNodeId;
|
||||
FString TargetNode;
|
||||
|
||||
UPROPERTY()
|
||||
FString TargetPinName;
|
||||
@@ -177,7 +177,7 @@ public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Array of {sourceNodeId, sourcePinName, targetNodeId, targetPinName} objects"))
|
||||
UPROPERTY(meta=(Description="Array of {sourceNode, sourcePinName, targetNode, targetPinName} objects"))
|
||||
FMCPJsonArray Connections;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
@@ -211,37 +211,37 @@ public:
|
||||
continue;
|
||||
}
|
||||
|
||||
EntryResult->SetStringField(TEXT("sourceNodeId"), Entry.SourceNodeId);
|
||||
EntryResult->SetStringField(TEXT("sourceNodeId"), Entry.SourceNode);
|
||||
EntryResult->SetStringField(TEXT("sourcePinName"), Entry.SourcePinName);
|
||||
EntryResult->SetStringField(TEXT("targetNodeId"), Entry.TargetNodeId);
|
||||
EntryResult->SetStringField(TEXT("targetNodeId"), Entry.TargetNode);
|
||||
EntryResult->SetStringField(TEXT("targetPinName"), Entry.TargetPinName);
|
||||
|
||||
UEdGraph* SourceGraph = nullptr;
|
||||
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph);
|
||||
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph);
|
||||
if (!SourceNode)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNode));
|
||||
continue;
|
||||
}
|
||||
|
||||
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId);
|
||||
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode);
|
||||
if (!TargetNode)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode));
|
||||
continue;
|
||||
}
|
||||
|
||||
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Entry.SourcePinName));
|
||||
if (!SourcePin)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *Entry.SourcePinName, *Entry.SourceNodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *Entry.SourcePinName, *Entry.SourceNode));
|
||||
continue;
|
||||
}
|
||||
|
||||
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
|
||||
if (!TargetPin)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -292,13 +292,13 @@ struct FDisconnectPinEntry
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
FString NodeId;
|
||||
FString Node;
|
||||
|
||||
UPROPERTY()
|
||||
FString PinName;
|
||||
|
||||
UPROPERTY(meta=(Optional))
|
||||
FString TargetNodeId;
|
||||
FString TargetNode;
|
||||
|
||||
UPROPERTY(meta=(Optional))
|
||||
FString TargetPinName;
|
||||
@@ -314,7 +314,7 @@ public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Array of {nodeId, pinName, targetNodeId?, targetPinName?} objects. If target is omitted, all connections on the pin are broken."))
|
||||
UPROPERTY(meta=(Description="Array of {node, pinName, targetNode?, targetPinName?} objects. If target is omitted, all connections on the pin are broken."))
|
||||
FMCPJsonArray Disconnections;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
@@ -350,38 +350,38 @@ public:
|
||||
continue;
|
||||
}
|
||||
|
||||
EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
|
||||
EntryResult->SetStringField(TEXT("nodeId"), Entry.Node);
|
||||
EntryResult->SetStringField(TEXT("pinName"), Entry.PinName);
|
||||
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.NodeId);
|
||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node);
|
||||
if (!Node)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
|
||||
continue;
|
||||
}
|
||||
|
||||
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
|
||||
if (!Pin)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.NodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node));
|
||||
continue;
|
||||
}
|
||||
|
||||
int32 DisconnectedCount = 0;
|
||||
|
||||
if (!Entry.TargetNodeId.IsEmpty() && !Entry.TargetPinName.IsEmpty())
|
||||
if (!Entry.TargetNode.IsEmpty() && !Entry.TargetPinName.IsEmpty())
|
||||
{
|
||||
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNodeId);
|
||||
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode);
|
||||
if (!TargetNode)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode));
|
||||
continue;
|
||||
}
|
||||
|
||||
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
|
||||
if (!TargetPin)
|
||||
{
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNodeId));
|
||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,630 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Level.h"
|
||||
#include "Engine/LevelScriptBlueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "K2Node_CallFunction.h"
|
||||
#include "K2Node_Event.h"
|
||||
#include "K2Node_CustomEvent.h"
|
||||
#include "K2Node_VariableGet.h"
|
||||
#include "K2Node_VariableSet.h"
|
||||
#include "K2Node_BreakStruct.h"
|
||||
#include "K2Node_MakeStruct.h"
|
||||
#include "K2Node_FunctionEntry.h"
|
||||
#include "K2Node_EditablePinBase.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "UObject/SavePackage.h"
|
||||
|
||||
// ============================================================
|
||||
// Request handlers
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleList(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Filter = Json->GetStringField(TEXT("filter"));
|
||||
FString ParentClassFilter = Json->GetStringField(TEXT("parentClass"));
|
||||
FString TypeFilter = Json->GetStringField(TEXT("type"));
|
||||
// type: "all" (default), "regular", "level"
|
||||
bool bIncludeRegular = TypeFilter.IsEmpty() || TypeFilter == TEXT("all") || TypeFilter == TEXT("regular");
|
||||
bool bIncludeLevel = TypeFilter.IsEmpty() || TypeFilter == TEXT("all") || TypeFilter == TEXT("level");
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Entries;
|
||||
if (bIncludeRegular)
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()))
|
||||
{
|
||||
FString Name = Asset.AssetName.ToString();
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
|
||||
if (!Filter.IsEmpty())
|
||||
{
|
||||
if (!Name.Contains(Filter, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
FString ParentClass;
|
||||
Asset.GetTagValue(FName(TEXT("ParentClass")), ParentClass);
|
||||
// Tag stores full path — extract short name
|
||||
int32 DotIndex;
|
||||
if (ParentClass.FindLastChar('.', DotIndex))
|
||||
{
|
||||
ParentClass = ParentClass.Mid(DotIndex + 1);
|
||||
}
|
||||
|
||||
if (!ParentClassFilter.IsEmpty())
|
||||
{
|
||||
if (!ParentClass.Contains(ParentClassFilter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("name"), Name);
|
||||
Entry->SetStringField(TEXT("path"), Path);
|
||||
Entry->SetStringField(TEXT("parentClass"), ParentClass);
|
||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
}
|
||||
|
||||
// Also include level blueprints from maps
|
||||
if (bIncludeLevel)
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UWorld::StaticClass()))
|
||||
{
|
||||
FString Name = Asset.AssetName.ToString();
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
|
||||
if (!Filter.IsEmpty())
|
||||
{
|
||||
if (!Name.Contains(Filter, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// No parent class filter for level blueprints
|
||||
if (!ParentClassFilter.IsEmpty())
|
||||
{
|
||||
if (!FString(TEXT("LevelScriptActor")).Contains(ParentClassFilter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("name"), Name);
|
||||
Entry->SetStringField(TEXT("path"), Path);
|
||||
Entry->SetStringField(TEXT("parentClass"), TEXT("LevelScriptActor"));
|
||||
Entry->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
}
|
||||
|
||||
Result->SetNumberField(TEXT("count"), Entries.Num());
|
||||
Result->SetNumberField(TEXT("total"), UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()).Num() + UMCPAssetFinder::GetAssets(UWorld::StaticClass()).Num());
|
||||
Result->SetArrayField(TEXT("blueprints"), Entries);
|
||||
}
|
||||
|
||||
void FBlueprintMCPServer::HandleGetBlueprint(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Name = Json->GetStringField(TEXT("name"));
|
||||
if (Name.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' parameter"));
|
||||
}
|
||||
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Name, Result);
|
||||
if (!BP) return;
|
||||
|
||||
TSharedRef<FJsonObject> Tmp = MCPUtils::SerializeBlueprint(BP);
|
||||
MCPUtils::CopyJsonFields(&*Tmp, Result);
|
||||
}
|
||||
|
||||
void FBlueprintMCPServer::HandleGetGraph(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Name = Json->GetStringField(TEXT("name"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graph"));
|
||||
if (Name.IsEmpty() || GraphName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' or 'graph' parameter"));
|
||||
}
|
||||
|
||||
// URL-decode graph name to handle spaces encoded as '+' or '%20'
|
||||
FString DecodedGraphName = MCPUtils::UrlDecode(GraphName);
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Name, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (Graph && Graph->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(Graph);
|
||||
if (GraphJson.IsValid())
|
||||
{
|
||||
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found — list available graphs
|
||||
TArray<TSharedPtr<FJsonValue>> GraphNames;
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (Graph)
|
||||
{
|
||||
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||
}
|
||||
}
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
|
||||
}
|
||||
|
||||
void FBlueprintMCPServer::HandleSearch(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Query = Json->GetStringField(TEXT("query"));
|
||||
if (Query.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'query' parameter"));
|
||||
}
|
||||
|
||||
FString PathFilter = Json->GetStringField(TEXT("path"));
|
||||
|
||||
int32 MaxResults = 50;
|
||||
if (Json->HasField(TEXT("maxResults")))
|
||||
{
|
||||
MaxResults = FMath::Clamp(FCString::Atoi(*Json->GetStringField(TEXT("maxResults"))), 1, 200);
|
||||
}
|
||||
|
||||
// Build a combined list of all searchable blueprints (regular + level)
|
||||
auto SearchBlueprint = [&](const FString& AssetName, const FString& Path, UBlueprint* BP, TArray<TSharedPtr<FJsonValue>>& OutResults)
|
||||
{
|
||||
TArray<UEdGraph*> Graphs;
|
||||
BP->GetAllGraphs(Graphs);
|
||||
|
||||
for (UEdGraph* Graph : Graphs)
|
||||
{
|
||||
if (!Graph || OutResults.Num() >= MaxResults) break;
|
||||
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (!Node || OutResults.Num() >= MaxResults) break;
|
||||
|
||||
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||
|
||||
FString FuncName, EventName, VarName;
|
||||
if (auto* CF = Cast<UK2Node_CallFunction>(Node))
|
||||
{
|
||||
FuncName = CF->FunctionReference.GetMemberName().ToString();
|
||||
}
|
||||
else if (auto* Ev = Cast<UK2Node_Event>(Node))
|
||||
{
|
||||
EventName = Ev->EventReference.GetMemberName().ToString();
|
||||
}
|
||||
else if (auto* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
EventName = CE->CustomFunctionName.ToString();
|
||||
}
|
||||
else if (auto* VG = Cast<UK2Node_VariableGet>(Node))
|
||||
{
|
||||
VarName = VG->GetVarName().ToString();
|
||||
}
|
||||
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
|
||||
{
|
||||
VarName = VS->GetVarName().ToString();
|
||||
}
|
||||
|
||||
bool bMatch = Title.Contains(Query, ESearchCase::IgnoreCase) ||
|
||||
(!FuncName.IsEmpty() && FuncName.Contains(Query, ESearchCase::IgnoreCase)) ||
|
||||
(!EventName.IsEmpty() && EventName.Contains(Query, ESearchCase::IgnoreCase)) ||
|
||||
(!VarName.IsEmpty() && VarName.Contains(Query, ESearchCase::IgnoreCase));
|
||||
|
||||
if (bMatch)
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), AssetName);
|
||||
R->SetStringField(TEXT("blueprintPath"), Path);
|
||||
R->SetStringField(TEXT("graph"), Graph->GetName());
|
||||
R->SetStringField(TEXT("nodeTitle"), Title);
|
||||
R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
|
||||
if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName);
|
||||
if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName);
|
||||
if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName);
|
||||
OutResults.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Results;
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()))
|
||||
{
|
||||
if (Results.Num() >= MaxResults) break;
|
||||
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
if (!PathFilter.IsEmpty() && !Path.Contains(PathFilter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
|
||||
if (!BP) continue;
|
||||
|
||||
SearchBlueprint(Asset.AssetName.ToString(), Path, BP, Results);
|
||||
}
|
||||
|
||||
// Also search level blueprints
|
||||
for (const FAssetData& MapAsset : UMCPAssetFinder::GetAssets(UWorld::StaticClass()))
|
||||
{
|
||||
if (Results.Num() >= MaxResults) break;
|
||||
|
||||
FString Path = MapAsset.PackageName.ToString();
|
||||
if (!PathFilter.IsEmpty() && !Path.Contains(PathFilter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UWorld* World = Cast<UWorld>(MapAsset.GetAsset());
|
||||
if (!World || !World->PersistentLevel) continue;
|
||||
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
|
||||
if (!LevelBP) continue;
|
||||
|
||||
int32 BeforeCount = Results.Num();
|
||||
SearchBlueprint(MapAsset.AssetName.ToString(), Path, LevelBP, Results);
|
||||
// Tag newly-added entries as level blueprint results
|
||||
for (int32 i = BeforeCount; i < Results.Num(); ++i)
|
||||
{
|
||||
Results[i]->AsObject()->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("query"), Query);
|
||||
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
||||
Result->SetArrayField(TEXT("results"), Results);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleTestSave — load a Blueprint and save it unmodified (diagnostic)
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleTestSave(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString Name = Json->GetStringField(TEXT("name"));
|
||||
if (Name.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'name' query parameter"));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save requested for '%s'"), *Name);
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Name, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save — loaded '%s', GeneratedClass=%s"),
|
||||
*BP->GetName(),
|
||||
BP->GeneratedClass ? *BP->GeneratedClass->GetName() : TEXT("null"));
|
||||
|
||||
// Attempt save with NO modifications
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
Result->SetStringField(TEXT("blueprint"), Name);
|
||||
Result->SetStringField(TEXT("packagePath"), BP->GetPackage()->GetName());
|
||||
Result->SetBoolField(TEXT("hasGeneratedClass"), BP->GeneratedClass != nullptr);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleFindReferences — find all Blueprints referencing an asset
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleFindReferences(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString AssetPath = Json->GetStringField(TEXT("assetPath"));
|
||||
if (AssetPath.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'assetPath' query parameter"));
|
||||
}
|
||||
|
||||
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
||||
|
||||
TArray<FName> Referencers;
|
||||
Registry.GetReferencers(FName(*AssetPath), Referencers);
|
||||
|
||||
// Build set of known Blueprint package names for filtering
|
||||
TSet<FString> BlueprintPackages;
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()))
|
||||
{
|
||||
BlueprintPackages.Add(Asset.PackageName.ToString());
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> BPRefs;
|
||||
TArray<TSharedPtr<FJsonValue>> OtherRefs;
|
||||
for (const FName& Ref : Referencers)
|
||||
{
|
||||
FString RefStr = Ref.ToString();
|
||||
if (BlueprintPackages.Contains(RefStr))
|
||||
{
|
||||
BPRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
||||
}
|
||||
else
|
||||
{
|
||||
OtherRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
||||
Result->SetNumberField(TEXT("totalReferencers"), Referencers.Num());
|
||||
Result->SetNumberField(TEXT("blueprintReferencerCount"), BPRefs.Num());
|
||||
Result->SetArrayField(TEXT("blueprintReferencers"), BPRefs);
|
||||
Result->SetNumberField(TEXT("otherReferencerCount"), OtherRefs.Num());
|
||||
Result->SetArrayField(TEXT("otherReferencers"), OtherRefs);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleSearchByType — find all usages of a type across blueprints
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleSearchByType(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString TypeNameRaw = Json->GetStringField(TEXT("typeName"));
|
||||
if (TypeNameRaw.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing 'typeName' query parameter"));
|
||||
}
|
||||
|
||||
FString TypeName = MCPUtils::UrlDecode(TypeNameRaw);
|
||||
FString FilterRaw = Json->GetStringField(TEXT("filter"));
|
||||
FString FilterStr = FilterRaw.IsEmpty() ? FString() : MCPUtils::UrlDecode(FilterRaw);
|
||||
|
||||
int32 MaxResults = 200;
|
||||
if (Json->HasField(TEXT("maxResults")))
|
||||
{
|
||||
MaxResults = FMath::Clamp(FCString::Atoi(*Json->GetStringField(TEXT("maxResults"))), 1, 500);
|
||||
}
|
||||
|
||||
// Strip F/E/U prefix for comparison
|
||||
FString TypeNameNoPrefix = TypeName;
|
||||
if (TypeNameNoPrefix.StartsWith(TEXT("F")) || TypeNameNoPrefix.StartsWith(TEXT("E")) || TypeNameNoPrefix.StartsWith(TEXT("U")))
|
||||
{
|
||||
TypeNameNoPrefix = TypeNameNoPrefix.Mid(1);
|
||||
}
|
||||
|
||||
auto MatchesType = [&TypeName, &TypeNameNoPrefix](const FString& TestType) -> bool
|
||||
{
|
||||
return TestType.Equals(TypeName, ESearchCase::IgnoreCase) ||
|
||||
TestType.Equals(TypeNameNoPrefix, ESearchCase::IgnoreCase);
|
||||
};
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Results;
|
||||
|
||||
// Lambda that searches a single Blueprint for type usages
|
||||
auto SearchOneBlueprint = [&](const FString& BPName, const FString& Path, UBlueprint* BP, bool bIsLevel)
|
||||
{
|
||||
// Check variables
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Results.Num() >= MaxResults) break;
|
||||
|
||||
FString VarSubtype;
|
||||
if (Var.VarType.PinSubCategoryObject.IsValid())
|
||||
{
|
||||
VarSubtype = Var.VarType.PinSubCategoryObject->GetName();
|
||||
}
|
||||
|
||||
if (MatchesType(VarSubtype) || MatchesType(Var.VarType.PinCategory.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), Path);
|
||||
R->SetStringField(TEXT("usage"), TEXT("variable"));
|
||||
R->SetStringField(TEXT("location"), Var.VarName.ToString());
|
||||
R->SetStringField(TEXT("currentType"), Var.VarType.PinCategory.ToString());
|
||||
if (!VarSubtype.IsEmpty())
|
||||
R->SetStringField(TEXT("currentSubtype"), VarSubtype);
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
|
||||
// Check graphs for function/event params, struct nodes, and pin connections
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph || Results.Num() >= MaxResults) break;
|
||||
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (!Node || Results.Num() >= MaxResults) break;
|
||||
|
||||
// Check FunctionEntry/CustomEvent parameters
|
||||
if (auto* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins)
|
||||
{
|
||||
if (!PinInfo.IsValid()) continue;
|
||||
FString ParamSubtype;
|
||||
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
||||
ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
|
||||
|
||||
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), Path);
|
||||
R->SetStringField(TEXT("usage"), TEXT("functionParameter"));
|
||||
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
||||
*Graph->GetName(), *PinInfo->PinName.ToString()));
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
|
||||
if (!ParamSubtype.IsEmpty())
|
||||
R->SetStringField(TEXT("currentSubtype"), ParamSubtype);
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (auto* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : CustomEvent->UserDefinedPins)
|
||||
{
|
||||
if (!PinInfo.IsValid()) continue;
|
||||
FString ParamSubtype;
|
||||
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
||||
ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
|
||||
|
||||
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), Path);
|
||||
R->SetStringField(TEXT("usage"), TEXT("eventParameter"));
|
||||
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
||||
*CustomEvent->CustomFunctionName.ToString(), *PinInfo->PinName.ToString()));
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
|
||||
if (!ParamSubtype.IsEmpty())
|
||||
R->SetStringField(TEXT("currentSubtype"), ParamSubtype);
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check Break/Make struct nodes
|
||||
else if (auto* BreakNode = Cast<UK2Node_BreakStruct>(Node))
|
||||
{
|
||||
if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), Path);
|
||||
R->SetStringField(TEXT("usage"), TEXT("breakStruct"));
|
||||
R->SetStringField(TEXT("location"), Graph->GetName());
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("structType"), BreakNode->StructType->GetName());
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
else if (auto* MakeNode = Cast<UK2Node_MakeStruct>(Node))
|
||||
{
|
||||
if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), Path);
|
||||
R->SetStringField(TEXT("usage"), TEXT("makeStruct"));
|
||||
R->SetStringField(TEXT("location"), Graph->GetName());
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("structType"), MakeNode->StructType->GetName());
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
|
||||
// Check pin connections carrying the type
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (!Pin || Pin->bHidden || Results.Num() >= MaxResults) continue;
|
||||
|
||||
FString PinSubtype;
|
||||
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
||||
PinSubtype = Pin->PinType.PinSubCategoryObject->GetName();
|
||||
|
||||
if (Pin->LinkedTo.Num() > 0 &&
|
||||
(MatchesType(PinSubtype) || MatchesType(Pin->PinType.PinCategory.ToString())))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), Path);
|
||||
R->SetStringField(TEXT("usage"), TEXT("pinConnection"));
|
||||
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
||||
*Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(),
|
||||
*Pin->PinName.ToString()));
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("graph"), Graph->GetName());
|
||||
R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString());
|
||||
if (!PinSubtype.IsEmpty())
|
||||
R->SetStringField(TEXT("pinSubtype"), PinSubtype);
|
||||
R->SetNumberField(TEXT("connectionCount"), Pin->LinkedTo.Num());
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Search regular blueprints
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()))
|
||||
{
|
||||
if (Results.Num() >= MaxResults) break;
|
||||
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
FString BPName = Asset.AssetName.ToString();
|
||||
|
||||
if (!FilterStr.IsEmpty() && !BPName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(FilterStr, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
|
||||
if (!BP) continue;
|
||||
|
||||
SearchOneBlueprint(BPName, Path, BP, false);
|
||||
}
|
||||
|
||||
// Search level blueprints from maps
|
||||
for (const FAssetData& MapAsset : UMCPAssetFinder::GetAssets(UWorld::StaticClass()))
|
||||
{
|
||||
if (Results.Num() >= MaxResults) break;
|
||||
|
||||
FString Path = MapAsset.PackageName.ToString();
|
||||
FString MapName = MapAsset.AssetName.ToString();
|
||||
|
||||
if (!FilterStr.IsEmpty() && !MapName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(FilterStr, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UWorld* World = Cast<UWorld>(MapAsset.GetAsset());
|
||||
if (!World || !World->PersistentLevel) continue;
|
||||
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
|
||||
if (!LevelBP) continue;
|
||||
|
||||
SearchOneBlueprint(MapName, Path, LevelBP, true);
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("typeName"), TypeName);
|
||||
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
||||
Result->SetArrayField(TEXT("results"), Results);
|
||||
}
|
||||
@@ -0,0 +1,729 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Level.h"
|
||||
#include "Engine/LevelScriptBlueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "K2Node_CallFunction.h"
|
||||
#include "K2Node_Event.h"
|
||||
#include "K2Node_CustomEvent.h"
|
||||
#include "K2Node_VariableGet.h"
|
||||
#include "K2Node_VariableSet.h"
|
||||
#include "K2Node_BreakStruct.h"
|
||||
#include "K2Node_MakeStruct.h"
|
||||
#include "K2Node_FunctionEntry.h"
|
||||
#include "K2Node_EditablePinBase.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
#include "MCPHandlers_Read.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="list_blueprint_assets"))
|
||||
class UMCPHandler_List : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path"))
|
||||
FString Filter;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Filter by parent class name (substring match)"))
|
||||
FString ParentClass;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Type filter: 'all' (default), 'regular', or 'level'"))
|
||||
FString Type;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("List all Blueprint assets in the project, with optional filtering by name, parent class, or type.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// type: "all" (default), "regular", "level"
|
||||
bool bIncludeRegular = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("regular");
|
||||
bool bIncludeLevel = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("level");
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Entries;
|
||||
if (bIncludeRegular)
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()))
|
||||
{
|
||||
FString Name = Asset.AssetName.ToString();
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
|
||||
if (!Filter.IsEmpty())
|
||||
{
|
||||
if (!Name.Contains(Filter, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
FString ParentClassName;
|
||||
Asset.GetTagValue(FName(TEXT("ParentClass")), ParentClassName);
|
||||
// Tag stores full path — extract short name
|
||||
int32 DotIndex;
|
||||
if (ParentClassName.FindLastChar('.', DotIndex))
|
||||
{
|
||||
ParentClassName = ParentClassName.Mid(DotIndex + 1);
|
||||
}
|
||||
|
||||
if (!ParentClass.IsEmpty())
|
||||
{
|
||||
if (!ParentClassName.Contains(ParentClass, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("name"), Name);
|
||||
Entry->SetStringField(TEXT("path"), Path);
|
||||
Entry->SetStringField(TEXT("parentClass"), ParentClassName);
|
||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
}
|
||||
|
||||
// Also include level blueprints from maps
|
||||
if (bIncludeLevel)
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UWorld::StaticClass()))
|
||||
{
|
||||
FString Name = Asset.AssetName.ToString();
|
||||
FString Path = Asset.PackageName.ToString();
|
||||
|
||||
if (!Filter.IsEmpty())
|
||||
{
|
||||
if (!Name.Contains(Filter, ESearchCase::IgnoreCase) &&
|
||||
!Path.Contains(Filter, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// No parent class filter for level blueprints
|
||||
if (!ParentClass.IsEmpty())
|
||||
{
|
||||
if (!FString(TEXT("LevelScriptActor")).Contains(ParentClass, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||
Entry->SetStringField(TEXT("name"), Name);
|
||||
Entry->SetStringField(TEXT("path"), Path);
|
||||
Entry->SetStringField(TEXT("parentClass"), TEXT("LevelScriptActor"));
|
||||
Entry->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||
}
|
||||
|
||||
Result->SetNumberField(TEXT("count"), Entries.Num());
|
||||
Result->SetNumberField(TEXT("total"), UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()).Num() + UMCPAssetFinder::GetAssets(UWorld::StaticClass()).Num());
|
||||
Result->SetArrayField(TEXT("blueprints"), Entries);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="dump_blueprint"))
|
||||
class UMCPHandler_GetBlueprint : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Load and serialize a Blueprint, returning its full structure including graphs, variables, and components.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, Result);
|
||||
if (!BP) return;
|
||||
|
||||
TSharedRef<FJsonObject> Tmp = MCPUtils::SerializeBlueprint(BP);
|
||||
MCPUtils::CopyJsonFields(&*Tmp, Result);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="dump_blueprint_graph"))
|
||||
class UMCPHandler_GetGraph : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Graph name to dump"))
|
||||
FString Graph;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Dump the detailed node/pin structure of a specific graph within a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// URL-decode graph name to handle spaces encoded as '+' or '%20'
|
||||
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
for (UEdGraph* GraphObj : AllGraphs)
|
||||
{
|
||||
if (GraphObj && GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(GraphObj);
|
||||
if (GraphJson.IsValid())
|
||||
{
|
||||
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found — list available graphs
|
||||
TArray<TSharedPtr<FJsonValue>> GraphNames;
|
||||
for (UEdGraph* GraphObj : AllGraphs)
|
||||
{
|
||||
if (GraphObj)
|
||||
{
|
||||
GraphNames.Add(MakeShared<FJsonValueString>(GraphObj->GetName()));
|
||||
}
|
||||
}
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="search_within_blueprints"))
|
||||
class UMCPHandler_Search : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Search query string to match against node titles, function names, event names, and variable names"))
|
||||
FString Query;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Filter results to blueprints whose path contains this substring"))
|
||||
FString Path;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)"))
|
||||
int32 MaxResults = 0;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Search across all Blueprint graphs for nodes matching a query string.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 200) : 50;
|
||||
|
||||
// Build a combined list of all searchable blueprints (regular + level)
|
||||
auto SearchBlueprint = [&](const FString& AssetName, const FString& AssetPath, UBlueprint* BP, TArray<TSharedPtr<FJsonValue>>& OutResults)
|
||||
{
|
||||
TArray<UEdGraph*> Graphs;
|
||||
BP->GetAllGraphs(Graphs);
|
||||
|
||||
for (UEdGraph* GraphObj : Graphs)
|
||||
{
|
||||
if (!GraphObj || OutResults.Num() >= EffectiveMaxResults) break;
|
||||
|
||||
for (UEdGraphNode* Node : GraphObj->Nodes)
|
||||
{
|
||||
if (!Node || OutResults.Num() >= EffectiveMaxResults) break;
|
||||
|
||||
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||
|
||||
FString FuncName, EventName, VarName;
|
||||
if (auto* CF = Cast<UK2Node_CallFunction>(Node))
|
||||
{
|
||||
FuncName = CF->FunctionReference.GetMemberName().ToString();
|
||||
}
|
||||
else if (auto* Ev = Cast<UK2Node_Event>(Node))
|
||||
{
|
||||
EventName = Ev->EventReference.GetMemberName().ToString();
|
||||
}
|
||||
else if (auto* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
EventName = CE->CustomFunctionName.ToString();
|
||||
}
|
||||
else if (auto* VG = Cast<UK2Node_VariableGet>(Node))
|
||||
{
|
||||
VarName = VG->GetVarName().ToString();
|
||||
}
|
||||
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
|
||||
{
|
||||
VarName = VS->GetVarName().ToString();
|
||||
}
|
||||
|
||||
bool bMatch = Title.Contains(Query, ESearchCase::IgnoreCase) ||
|
||||
(!FuncName.IsEmpty() && FuncName.Contains(Query, ESearchCase::IgnoreCase)) ||
|
||||
(!EventName.IsEmpty() && EventName.Contains(Query, ESearchCase::IgnoreCase)) ||
|
||||
(!VarName.IsEmpty() && VarName.Contains(Query, ESearchCase::IgnoreCase));
|
||||
|
||||
if (bMatch)
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), AssetName);
|
||||
R->SetStringField(TEXT("blueprintPath"), AssetPath);
|
||||
R->SetStringField(TEXT("graph"), GraphObj->GetName());
|
||||
R->SetStringField(TEXT("nodeTitle"), Title);
|
||||
R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
|
||||
if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName);
|
||||
if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName);
|
||||
if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName);
|
||||
OutResults.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Results;
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()))
|
||||
{
|
||||
if (Results.Num() >= EffectiveMaxResults) break;
|
||||
|
||||
FString AssetPath = Asset.PackageName.ToString();
|
||||
if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
|
||||
if (!BP) continue;
|
||||
|
||||
SearchBlueprint(Asset.AssetName.ToString(), AssetPath, BP, Results);
|
||||
}
|
||||
|
||||
// Also search level blueprints
|
||||
for (const FAssetData& MapAsset : UMCPAssetFinder::GetAssets(UWorld::StaticClass()))
|
||||
{
|
||||
if (Results.Num() >= EffectiveMaxResults) break;
|
||||
|
||||
FString AssetPath = MapAsset.PackageName.ToString();
|
||||
if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UWorld* World = Cast<UWorld>(MapAsset.GetAsset());
|
||||
if (!World || !World->PersistentLevel) continue;
|
||||
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
|
||||
if (!LevelBP) continue;
|
||||
|
||||
int32 BeforeCount = Results.Num();
|
||||
SearchBlueprint(MapAsset.AssetName.ToString(), AssetPath, LevelBP, Results);
|
||||
// Tag newly-added entries as level blueprint results
|
||||
for (int32 i = BeforeCount; i < Results.Num(); ++i)
|
||||
{
|
||||
Results[i]->AsObject()->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("query"), Query);
|
||||
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
||||
Result->SetArrayField(TEXT("results"), Results);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ============================================================
|
||||
// HandleTestSave — load a Blueprint and save it unmodified (diagnostic)
|
||||
// ============================================================
|
||||
|
||||
UCLASS(meta=(ToolName="test_save_blueprint_package"))
|
||||
class UMCPHandler_TestSave : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Load a Blueprint and save it unmodified as a diagnostic test.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save requested for '%s'"), *Blueprint);
|
||||
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save — loaded '%s', GeneratedClass=%s"),
|
||||
*BP->GetName(),
|
||||
BP->GeneratedClass ? *BP->GeneratedClass->GetName() : TEXT("null"));
|
||||
|
||||
// Attempt save with NO modifications
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("packagePath"), BP->GetPackage()->GetName());
|
||||
Result->SetBoolField(TEXT("hasGeneratedClass"), BP->GeneratedClass != nullptr);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ============================================================
|
||||
// HandleFindReferences — find all Blueprints referencing an asset
|
||||
// ============================================================
|
||||
|
||||
UCLASS(meta=(ToolName="find_asset_references"))
|
||||
class UMCPHandler_FindReferences : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Asset package path to find references for"))
|
||||
FString AssetPath;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Find all assets that reference a given asset, categorized into Blueprint and non-Blueprint referencers.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
||||
|
||||
TArray<FName> Referencers;
|
||||
Registry.GetReferencers(FName(*AssetPath), Referencers);
|
||||
|
||||
// Build set of known Blueprint package names for filtering
|
||||
TSet<FString> BlueprintPackages;
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()))
|
||||
{
|
||||
BlueprintPackages.Add(Asset.PackageName.ToString());
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> BPRefs;
|
||||
TArray<TSharedPtr<FJsonValue>> OtherRefs;
|
||||
for (const FName& Ref : Referencers)
|
||||
{
|
||||
FString RefStr = Ref.ToString();
|
||||
if (BlueprintPackages.Contains(RefStr))
|
||||
{
|
||||
BPRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
||||
}
|
||||
else
|
||||
{
|
||||
OtherRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
||||
Result->SetNumberField(TEXT("totalReferencers"), Referencers.Num());
|
||||
Result->SetNumberField(TEXT("blueprintReferencerCount"), BPRefs.Num());
|
||||
Result->SetArrayField(TEXT("blueprintReferencers"), BPRefs);
|
||||
Result->SetNumberField(TEXT("otherReferencerCount"), OtherRefs.Num());
|
||||
Result->SetArrayField(TEXT("otherReferencers"), OtherRefs);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ============================================================
|
||||
// HandleSearchByType — find all usages of a type across blueprints
|
||||
// ============================================================
|
||||
|
||||
UCLASS(meta=(ToolName="search_type_usage_in_blueprints"))
|
||||
class UMCPHandler_SearchByType : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Type name to search for (e.g. 'FVector', 'MyStruct'). F/E/U prefix is stripped for matching."))
|
||||
FString TypeName;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Filter to blueprints whose name or path contains this substring"))
|
||||
FString Filter;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 200, max 500)"))
|
||||
int32 MaxResults = 0;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Search all Blueprints for usages of a specific type in variables, function parameters, struct nodes, and pin connections.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
FString DecodedTypeName = MCPUtils::UrlDecode(TypeName);
|
||||
FString FilterStr = Filter.IsEmpty() ? FString() : MCPUtils::UrlDecode(Filter);
|
||||
|
||||
int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 500) : 200;
|
||||
|
||||
// Strip F/E/U prefix for comparison
|
||||
FString TypeNameNoPrefix = DecodedTypeName;
|
||||
if (TypeNameNoPrefix.StartsWith(TEXT("F")) || TypeNameNoPrefix.StartsWith(TEXT("E")) || TypeNameNoPrefix.StartsWith(TEXT("U")))
|
||||
{
|
||||
TypeNameNoPrefix = TypeNameNoPrefix.Mid(1);
|
||||
}
|
||||
|
||||
auto MatchesType = [&DecodedTypeName, &TypeNameNoPrefix](const FString& TestType) -> bool
|
||||
{
|
||||
return TestType.Equals(DecodedTypeName, ESearchCase::IgnoreCase) ||
|
||||
TestType.Equals(TypeNameNoPrefix, ESearchCase::IgnoreCase);
|
||||
};
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Results;
|
||||
|
||||
// Lambda that searches a single Blueprint for type usages
|
||||
auto SearchOneBlueprint = [&](const FString& BPName, const FString& BPPath, UBlueprint* BP, bool bIsLevel)
|
||||
{
|
||||
// Check variables
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Results.Num() >= EffectiveMaxResults) break;
|
||||
|
||||
FString VarSubtype;
|
||||
if (Var.VarType.PinSubCategoryObject.IsValid())
|
||||
{
|
||||
VarSubtype = Var.VarType.PinSubCategoryObject->GetName();
|
||||
}
|
||||
|
||||
if (MatchesType(VarSubtype) || MatchesType(Var.VarType.PinCategory.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||
R->SetStringField(TEXT("usage"), TEXT("variable"));
|
||||
R->SetStringField(TEXT("location"), Var.VarName.ToString());
|
||||
R->SetStringField(TEXT("currentType"), Var.VarType.PinCategory.ToString());
|
||||
if (!VarSubtype.IsEmpty())
|
||||
R->SetStringField(TEXT("currentSubtype"), VarSubtype);
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
|
||||
// Check graphs for function/event params, struct nodes, and pin connections
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
|
||||
for (UEdGraph* GraphObj : AllGraphs)
|
||||
{
|
||||
if (!GraphObj || Results.Num() >= EffectiveMaxResults) break;
|
||||
|
||||
for (UEdGraphNode* Node : GraphObj->Nodes)
|
||||
{
|
||||
if (!Node || Results.Num() >= EffectiveMaxResults) break;
|
||||
|
||||
// Check FunctionEntry/CustomEvent parameters
|
||||
if (auto* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
|
||||
{
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins)
|
||||
{
|
||||
if (!PinInfo.IsValid()) continue;
|
||||
FString ParamSubtype;
|
||||
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
||||
ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
|
||||
|
||||
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||
R->SetStringField(TEXT("usage"), TEXT("functionParameter"));
|
||||
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
||||
*GraphObj->GetName(), *PinInfo->PinName.ToString()));
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
|
||||
if (!ParamSubtype.IsEmpty())
|
||||
R->SetStringField(TEXT("currentSubtype"), ParamSubtype);
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (auto* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
|
||||
{
|
||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : CustomEvent->UserDefinedPins)
|
||||
{
|
||||
if (!PinInfo.IsValid()) continue;
|
||||
FString ParamSubtype;
|
||||
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
||||
ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
|
||||
|
||||
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||
R->SetStringField(TEXT("usage"), TEXT("eventParameter"));
|
||||
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
||||
*CustomEvent->CustomFunctionName.ToString(), *PinInfo->PinName.ToString()));
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
|
||||
if (!ParamSubtype.IsEmpty())
|
||||
R->SetStringField(TEXT("currentSubtype"), ParamSubtype);
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check Break/Make struct nodes
|
||||
else if (auto* BreakNode = Cast<UK2Node_BreakStruct>(Node))
|
||||
{
|
||||
if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||
R->SetStringField(TEXT("usage"), TEXT("breakStruct"));
|
||||
R->SetStringField(TEXT("location"), GraphObj->GetName());
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("structType"), BreakNode->StructType->GetName());
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
else if (auto* MakeNode = Cast<UK2Node_MakeStruct>(Node))
|
||||
{
|
||||
if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName()))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||
R->SetStringField(TEXT("usage"), TEXT("makeStruct"));
|
||||
R->SetStringField(TEXT("location"), GraphObj->GetName());
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("structType"), MakeNode->StructType->GetName());
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
|
||||
// Check pin connections carrying the type
|
||||
for (UEdGraphPin* Pin : Node->Pins)
|
||||
{
|
||||
if (!Pin || Pin->bHidden || Results.Num() >= EffectiveMaxResults) continue;
|
||||
|
||||
FString PinSubtype;
|
||||
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
||||
PinSubtype = Pin->PinType.PinSubCategoryObject->GetName();
|
||||
|
||||
if (Pin->LinkedTo.Num() > 0 &&
|
||||
(MatchesType(PinSubtype) || MatchesType(Pin->PinType.PinCategory.ToString())))
|
||||
{
|
||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||
R->SetStringField(TEXT("blueprint"), BPName);
|
||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||
R->SetStringField(TEXT("usage"), TEXT("pinConnection"));
|
||||
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
||||
*Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(),
|
||||
*Pin->PinName.ToString()));
|
||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||
R->SetStringField(TEXT("graph"), GraphObj->GetName());
|
||||
R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString());
|
||||
if (!PinSubtype.IsEmpty())
|
||||
R->SetStringField(TEXT("pinSubtype"), PinSubtype);
|
||||
R->SetNumberField(TEXT("connectionCount"), Pin->LinkedTo.Num());
|
||||
if (bIsLevel)
|
||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Search regular blueprints
|
||||
for (const FAssetData& Asset : UMCPAssetFinder::GetAssets(UBlueprint::StaticClass()))
|
||||
{
|
||||
if (Results.Num() >= EffectiveMaxResults) break;
|
||||
|
||||
FString AssetPath = Asset.PackageName.ToString();
|
||||
FString BPName = Asset.AssetName.ToString();
|
||||
|
||||
if (!FilterStr.IsEmpty() && !BPName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
|
||||
!AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
|
||||
if (!BP) continue;
|
||||
|
||||
SearchOneBlueprint(BPName, AssetPath, BP, false);
|
||||
}
|
||||
|
||||
// Search level blueprints from maps
|
||||
for (const FAssetData& MapAsset : UMCPAssetFinder::GetAssets(UWorld::StaticClass()))
|
||||
{
|
||||
if (Results.Num() >= EffectiveMaxResults) break;
|
||||
|
||||
FString AssetPath = MapAsset.PackageName.ToString();
|
||||
FString MapName = MapAsset.AssetName.ToString();
|
||||
|
||||
if (!FilterStr.IsEmpty() && !MapName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
|
||||
!AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UWorld* World = Cast<UWorld>(MapAsset.GetAsset());
|
||||
if (!World || !World->PersistentLevel) continue;
|
||||
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
|
||||
if (!LevelBP) continue;
|
||||
|
||||
SearchOneBlueprint(MapName, AssetPath, LevelBP, true);
|
||||
}
|
||||
|
||||
Result->SetStringField(TEXT("typeName"), DecodedTypeName);
|
||||
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
||||
Result->SetArrayField(TEXT("results"), Results);
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,540 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "Animation/AnimBlueprint.h"
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Animation/BlendSpace.h"
|
||||
#include "AnimGraphNode_SequencePlayer.h"
|
||||
#include "AnimGraphNode_BlendSpacePlayer.h"
|
||||
#include "AnimStateNode.h"
|
||||
#include "AnimStateTransitionNode.h"
|
||||
#include "AnimationStateMachineGraph.h"
|
||||
#include "K2Node_VariableGet.h"
|
||||
|
||||
// ============================================================
|
||||
// Tier 2: State Machine Mutation
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graph"));
|
||||
FString StateName = Json->GetStringField(TEXT("stateName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName"));
|
||||
}
|
||||
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
// Check for duplicate state name
|
||||
if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *GraphName));
|
||||
}
|
||||
|
||||
// Get position
|
||||
int32 PosX = Json->HasField(TEXT("posX")) ? (int32)Json->GetNumberField(TEXT("posX")) : 200;
|
||||
int32 PosY = Json->HasField(TEXT("posY")) ? (int32)Json->GetNumberField(TEXT("posY")) : 0;
|
||||
|
||||
// Create the state node
|
||||
UAnimStateNode* NewState = NewObject<UAnimStateNode>(SMGraph);
|
||||
NewState->CreateNewGuid();
|
||||
NewState->NodePosX = PosX;
|
||||
NewState->NodePosY = PosY;
|
||||
|
||||
// Set the state name via the bound graph
|
||||
NewState->PostPlacedNewNode();
|
||||
NewState->AllocateDefaultPins();
|
||||
|
||||
// Rename the bound graph to set the state name
|
||||
if (NewState->GetBoundGraph())
|
||||
{
|
||||
NewState->GetBoundGraph()->Rename(*StateName, nullptr);
|
||||
}
|
||||
|
||||
SMGraph->AddNode(NewState, false, false);
|
||||
NewState->SetFlags(RF_Transactional);
|
||||
|
||||
// Optionally set animation asset
|
||||
FString AnimAssetName = Json->GetStringField(TEXT("animationAsset"));
|
||||
if (!AnimAssetName.IsEmpty() && NewState->GetBoundGraph())
|
||||
{
|
||||
// Try to find the animation asset and create a sequence player in the state's inner graph
|
||||
FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimAssetName);
|
||||
UAnimSequence* AnimSeq = FoundAnimAsset ? Cast<UAnimSequence>(FoundAnimAsset->GetAsset()) : nullptr;
|
||||
|
||||
if (AnimSeq)
|
||||
{
|
||||
UAnimGraphNode_SequencePlayer* SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(NewState->GetBoundGraph());
|
||||
SeqNode->CreateNewGuid();
|
||||
SeqNode->PostPlacedNewNode();
|
||||
SeqNode->AllocateDefaultPins();
|
||||
SeqNode->SetAnimationAsset(AnimSeq);
|
||||
SeqNode->NodePosX = 0;
|
||||
SeqNode->NodePosY = 0;
|
||||
NewState->GetBoundGraph()->AddNode(SeqNode, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("stateName"), StateName);
|
||||
Result->SetStringField(TEXT("graph"), GraphName);
|
||||
Result->SetStringField(TEXT("nodeId"), NewState->NodeGuid.ToString());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
void FBlueprintMCPServer::HandleRemoveAnimState(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graph"));
|
||||
FString StateName = Json->GetStringField(TEXT("stateName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName"));
|
||||
}
|
||||
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
||||
if (!StateNode) return;
|
||||
|
||||
// Collect and remove transitions connected to this state
|
||||
TArray<UAnimStateTransitionNode*> TransitionsToRemove;
|
||||
for (UEdGraphNode* Node : SMGraph->Nodes)
|
||||
{
|
||||
if (UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node))
|
||||
{
|
||||
if (TransNode->GetPreviousState() == StateNode || TransNode->GetNextState() == StateNode)
|
||||
{
|
||||
TransitionsToRemove.Add(TransNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32 RemovedTransitions = TransitionsToRemove.Num();
|
||||
for (UAnimStateTransitionNode* Trans : TransitionsToRemove)
|
||||
{
|
||||
Trans->BreakAllNodeLinks();
|
||||
SMGraph->RemoveNode(Trans);
|
||||
}
|
||||
|
||||
// Remove the state
|
||||
StateNode->BreakAllNodeLinks();
|
||||
SMGraph->RemoveNode(StateNode);
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("removedState"), StateName);
|
||||
Result->SetNumberField(TEXT("removedTransitions"), RemovedTransitions);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
void FBlueprintMCPServer::HandleAddAnimTransition(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graph"));
|
||||
FString FromStateName = Json->GetStringField(TEXT("fromState"));
|
||||
FString ToStateName = Json->GetStringField(TEXT("toState"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || FromStateName.IsEmpty() || ToStateName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState"));
|
||||
}
|
||||
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateNode* FromState = MCPUtils::FindStateByName(SMGraph, FromStateName, Result);
|
||||
if (!FromState) return;
|
||||
|
||||
UAnimStateNode* ToState = MCPUtils::FindStateByName(SMGraph, ToStateName, Result);
|
||||
if (!ToState) return;
|
||||
|
||||
// Create transition node
|
||||
UAnimStateTransitionNode* TransNode = NewObject<UAnimStateTransitionNode>(SMGraph);
|
||||
TransNode->CreateNewGuid();
|
||||
TransNode->PostPlacedNewNode();
|
||||
TransNode->AllocateDefaultPins();
|
||||
|
||||
// Position between the two states
|
||||
TransNode->NodePosX = (FromState->NodePosX + ToState->NodePosX) / 2;
|
||||
TransNode->NodePosY = (FromState->NodePosY + ToState->NodePosY) / 2;
|
||||
|
||||
SMGraph->AddNode(TransNode, false, false);
|
||||
TransNode->SetFlags(RF_Transactional);
|
||||
|
||||
// Connect: FromState output -> Transition input, Transition output -> ToState input
|
||||
TransNode->CreateConnections(FromState, ToState);
|
||||
|
||||
// Set optional properties
|
||||
if (Json->HasField(TEXT("crossfadeDuration")))
|
||||
{
|
||||
TransNode->CrossfadeDuration = (float)Json->GetNumberField(TEXT("crossfadeDuration"));
|
||||
}
|
||||
if (Json->HasField(TEXT("priority")))
|
||||
{
|
||||
TransNode->PriorityOrder = (int32)Json->GetNumberField(TEXT("priority"));
|
||||
}
|
||||
if (Json->HasField(TEXT("bBidirectional")))
|
||||
{
|
||||
TransNode->Bidirectional = Json->GetBoolField(TEXT("bBidirectional"));
|
||||
}
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("fromState"), FromStateName);
|
||||
Result->SetStringField(TEXT("toState"), ToStateName);
|
||||
Result->SetStringField(TEXT("nodeId"), TransNode->NodeGuid.ToString());
|
||||
Result->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration);
|
||||
Result->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder);
|
||||
Result->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
void FBlueprintMCPServer::HandleSetTransitionRule(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graph"));
|
||||
FString FromStateName = Json->GetStringField(TEXT("fromState"));
|
||||
FString ToStateName = Json->GetStringField(TEXT("toState"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || FromStateName.IsEmpty() || ToStateName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState"));
|
||||
}
|
||||
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromStateName, ToStateName);
|
||||
if (!TransNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Transition from '%s' to '%s' not found in graph '%s'"),
|
||||
*FromStateName, *ToStateName, *GraphName));
|
||||
}
|
||||
|
||||
// Update properties
|
||||
int32 ChangedCount = 0;
|
||||
|
||||
if (Json->HasField(TEXT("crossfadeDuration")))
|
||||
{
|
||||
TransNode->CrossfadeDuration = (float)Json->GetNumberField(TEXT("crossfadeDuration"));
|
||||
ChangedCount++;
|
||||
}
|
||||
if (Json->HasField(TEXT("blendMode")))
|
||||
{
|
||||
TransNode->BlendMode = (EAlphaBlendOption)(int32)Json->GetNumberField(TEXT("blendMode"));
|
||||
ChangedCount++;
|
||||
}
|
||||
if (Json->HasField(TEXT("priorityOrder")))
|
||||
{
|
||||
TransNode->PriorityOrder = (int32)Json->GetNumberField(TEXT("priorityOrder"));
|
||||
ChangedCount++;
|
||||
}
|
||||
if (Json->HasField(TEXT("logicType")))
|
||||
{
|
||||
TransNode->LogicType = (ETransitionLogicType::Type)(int32)Json->GetNumberField(TEXT("logicType"));
|
||||
ChangedCount++;
|
||||
}
|
||||
if (Json->HasField(TEXT("bBidirectional")))
|
||||
{
|
||||
TransNode->Bidirectional = Json->GetBoolField(TEXT("bBidirectional"));
|
||||
ChangedCount++;
|
||||
}
|
||||
|
||||
if (ChangedCount == 0)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional"));
|
||||
}
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("fromState"), FromStateName);
|
||||
Result->SetStringField(TEXT("toState"), ToStateName);
|
||||
Result->SetNumberField(TEXT("propertiesChanged"), ChangedCount);
|
||||
Result->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration);
|
||||
Result->SetNumberField(TEXT("blendMode"), (int32)TransNode->BlendMode);
|
||||
Result->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder);
|
||||
Result->SetNumberField(TEXT("logicType"), (int32)TransNode->LogicType.GetValue());
|
||||
Result->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graph"));
|
||||
FString StateName = Json->GetStringField(TEXT("stateName"));
|
||||
FString AnimAssetName = Json->GetStringField(TEXT("animationAsset"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty() || AnimAssetName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, animationAsset"));
|
||||
}
|
||||
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
||||
if (!StateNode) return;
|
||||
|
||||
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
|
||||
if (!InnerGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
|
||||
}
|
||||
|
||||
// Find the animation asset
|
||||
UAnimSequence* AnimSeq = UMCPAssetFinder::LoadAsset<UAnimSequence>(AnimAssetName, Result);
|
||||
if (!AnimSeq) return;
|
||||
|
||||
// Find existing SequencePlayer or create one
|
||||
UAnimGraphNode_SequencePlayer* SeqNode = nullptr;
|
||||
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
||||
{
|
||||
SeqNode = Cast<UAnimGraphNode_SequencePlayer>(Node);
|
||||
if (SeqNode) break;
|
||||
}
|
||||
|
||||
bool bCreatedNew = false;
|
||||
if (!SeqNode)
|
||||
{
|
||||
SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(InnerGraph);
|
||||
SeqNode->CreateNewGuid();
|
||||
SeqNode->PostPlacedNewNode();
|
||||
SeqNode->AllocateDefaultPins();
|
||||
SeqNode->NodePosX = 0;
|
||||
SeqNode->NodePosY = 0;
|
||||
InnerGraph->AddNode(SeqNode, false, false);
|
||||
bCreatedNew = true;
|
||||
}
|
||||
|
||||
SeqNode->SetAnimationAsset(AnimSeq);
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("stateName"), StateName);
|
||||
Result->SetStringField(TEXT("animationAsset"), AnimSeq->GetName());
|
||||
Result->SetBoolField(TEXT("createdNewNode"), bCreatedNew);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleSetStateBlendSpace — place a BlendSpacePlayer in a state
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString GraphName = Json->GetStringField(TEXT("graph"));
|
||||
FString StateName = Json->GetStringField(TEXT("stateName"));
|
||||
FString BlendSpaceName = Json->GetStringField(TEXT("blendSpace"));
|
||||
FString XVariable = Json->GetStringField(TEXT("xVariable"));
|
||||
FString YVariable = Json->GetStringField(TEXT("yVariable"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty() || BlendSpaceName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, blendSpace"));
|
||||
}
|
||||
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
||||
if (!StateNode) return;
|
||||
|
||||
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
|
||||
if (!InnerGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
|
||||
}
|
||||
|
||||
// Find the blend space asset
|
||||
UBlendSpace* BlendSpaceAsset = UMCPAssetFinder::LoadAsset<UBlendSpace>(BlendSpaceName, Result);
|
||||
if (!BlendSpaceAsset) return;
|
||||
|
||||
// Find existing BlendSpacePlayer or create one
|
||||
UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr;
|
||||
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
||||
{
|
||||
BSNode = Cast<UAnimGraphNode_BlendSpacePlayer>(Node);
|
||||
if (BSNode) break;
|
||||
}
|
||||
|
||||
if (!BSNode)
|
||||
{
|
||||
BSNode = NewObject<UAnimGraphNode_BlendSpacePlayer>(InnerGraph);
|
||||
BSNode->CreateNewGuid();
|
||||
BSNode->PostPlacedNewNode();
|
||||
BSNode->AllocateDefaultPins();
|
||||
BSNode->NodePosX = 0;
|
||||
BSNode->NodePosY = 0;
|
||||
InnerGraph->AddNode(BSNode, false, false);
|
||||
}
|
||||
|
||||
BSNode->SetAnimationAsset(BlendSpaceAsset);
|
||||
|
||||
// Connect BlendSpacePlayer output to the Output Animation Pose node
|
||||
{
|
||||
// Find the AnimGraphNode_Root (Output Pose) in the inner graph
|
||||
UEdGraphNode* ResultNode = nullptr;
|
||||
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
||||
{
|
||||
if (Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_Root")) ||
|
||||
Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_StateResult")))
|
||||
{
|
||||
ResultNode = Node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ResultNode)
|
||||
{
|
||||
// Find output pose pin on BlendSpacePlayer and input pose pin on result node
|
||||
UEdGraphPin* BSOutputPin = nullptr;
|
||||
for (UEdGraphPin* Pin : BSNode->Pins)
|
||||
{
|
||||
if (Pin && Pin->Direction == EGPD_Output && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
|
||||
{
|
||||
BSOutputPin = Pin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UEdGraphPin* ResultInputPin = nullptr;
|
||||
for (UEdGraphPin* Pin : ResultNode->Pins)
|
||||
{
|
||||
if (Pin && Pin->Direction == EGPD_Input && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
|
||||
{
|
||||
ResultInputPin = Pin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (BSOutputPin && ResultInputPin)
|
||||
{
|
||||
// Break existing connections on the result input
|
||||
ResultInputPin->BreakAllPinLinks();
|
||||
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
|
||||
if (Schema)
|
||||
{
|
||||
Schema->TryCreateConnection(BSOutputPin, ResultInputPin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wire X and Y variables if provided
|
||||
auto WireVariable = [&](const FString& VarName, const FString& PinName) -> bool
|
||||
{
|
||||
if (VarName.IsEmpty()) return false;
|
||||
|
||||
// Verify the variable exists in the blueprint
|
||||
FName VarFName(*VarName);
|
||||
bool bVarFound = false;
|
||||
for (FBPVariableDescription& Var : AnimBP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == VarFName)
|
||||
{
|
||||
bVarFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bVarFound)
|
||||
{
|
||||
// Also check parent class properties
|
||||
if (UClass* GenClass = AnimBP->SkeletonGeneratedClass)
|
||||
{
|
||||
if (FProperty* Prop = GenClass->FindPropertyByName(VarFName))
|
||||
{
|
||||
bVarFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bVarFound)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Variable '%s' not found in '%s', skipping wire"),
|
||||
*VarName, *BlueprintName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a VariableGet node
|
||||
UK2Node_VariableGet* GetNode = NewObject<UK2Node_VariableGet>(InnerGraph);
|
||||
GetNode->VariableReference.SetSelfMember(VarFName);
|
||||
GetNode->NodePosX = BSNode->NodePosX - 250;
|
||||
GetNode->NodePosY = BSNode->NodePosY;
|
||||
InnerGraph->AddNode(GetNode, false, false);
|
||||
GetNode->AllocateDefaultPins();
|
||||
|
||||
// Find the variable output pin
|
||||
UEdGraphPin* VarOutPin = nullptr;
|
||||
for (UEdGraphPin* Pin : GetNode->Pins)
|
||||
{
|
||||
if (Pin && Pin->Direction == EGPD_Output && Pin->PinName == VarFName)
|
||||
{
|
||||
VarOutPin = Pin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the target pin on the BlendSpacePlayer
|
||||
UEdGraphPin* TargetPin = BSNode->FindPin(FName(*PinName));
|
||||
|
||||
if (VarOutPin && TargetPin)
|
||||
{
|
||||
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
|
||||
if (Schema)
|
||||
{
|
||||
Schema->TryCreateConnection(VarOutPin, TargetPin);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
WireVariable(XVariable, TEXT("X"));
|
||||
WireVariable(YVariable, TEXT("Y"));
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("stateName"), StateName);
|
||||
Result->SetStringField(TEXT("blendSpace"), BlendSpaceAsset->GetName());
|
||||
Result->SetStringField(TEXT("nodeId"), BSNode->NodeGuid.ToString());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphNode.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "Animation/AnimBlueprint.h"
|
||||
#include "Animation/AnimSequence.h"
|
||||
#include "Animation/BlendSpace.h"
|
||||
#include "AnimGraphNode_SequencePlayer.h"
|
||||
#include "AnimGraphNode_BlendSpacePlayer.h"
|
||||
#include "AnimStateNode.h"
|
||||
#include "AnimStateTransitionNode.h"
|
||||
#include "AnimationStateMachineGraph.h"
|
||||
#include "K2Node_VariableGet.h"
|
||||
#include "MCPHandlers_StateMachine.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="add_anim_state_to_machine"))
|
||||
class UMCPHandler_AddAnimState : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="State machine graph name"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Name for the new state"))
|
||||
FString StateName;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="X position of the new state node"))
|
||||
int32 PosX = 200;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Y position of the new state node"))
|
||||
int32 PosY = 0;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Animation asset name to assign to the state"))
|
||||
FString AnimationAsset;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Add a new state to an animation state machine graph. "
|
||||
"Optionally assign an animation asset to the state.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(Blueprint, Graph, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
// Check for duplicate state name
|
||||
if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr))
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *Graph));
|
||||
}
|
||||
|
||||
// Create the state node
|
||||
UAnimStateNode* NewState = NewObject<UAnimStateNode>(SMGraph);
|
||||
NewState->CreateNewGuid();
|
||||
NewState->NodePosX = PosX;
|
||||
NewState->NodePosY = PosY;
|
||||
|
||||
// Set the state name via the bound graph
|
||||
NewState->PostPlacedNewNode();
|
||||
NewState->AllocateDefaultPins();
|
||||
|
||||
// Rename the bound graph to set the state name
|
||||
if (NewState->GetBoundGraph())
|
||||
{
|
||||
NewState->GetBoundGraph()->Rename(*StateName, nullptr);
|
||||
}
|
||||
|
||||
SMGraph->AddNode(NewState, false, false);
|
||||
NewState->SetFlags(RF_Transactional);
|
||||
|
||||
// Optionally set animation asset
|
||||
if (!AnimationAsset.IsEmpty() && NewState->GetBoundGraph())
|
||||
{
|
||||
// Try to find the animation asset and create a sequence player in the state's inner graph
|
||||
FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimationAsset);
|
||||
UAnimSequence* AnimSeq = FoundAnimAsset ? Cast<UAnimSequence>(FoundAnimAsset->GetAsset()) : nullptr;
|
||||
|
||||
if (AnimSeq)
|
||||
{
|
||||
UAnimGraphNode_SequencePlayer* SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(NewState->GetBoundGraph());
|
||||
SeqNode->CreateNewGuid();
|
||||
SeqNode->PostPlacedNewNode();
|
||||
SeqNode->AllocateDefaultPins();
|
||||
SeqNode->SetAnimationAsset(AnimSeq);
|
||||
SeqNode->NodePosX = 0;
|
||||
SeqNode->NodePosY = 0;
|
||||
NewState->GetBoundGraph()->AddNode(SeqNode, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("stateName"), StateName);
|
||||
Result->SetStringField(TEXT("graph"), Graph);
|
||||
Result->SetStringField(TEXT("nodeId"), NewState->NodeGuid.ToString());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="remove_anim_state_from_machine"))
|
||||
class UMCPHandler_RemoveAnimState : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="State machine graph name"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the state to remove"))
|
||||
FString StateName;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Remove a state and its connected transitions from an animation state machine graph.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(Blueprint, Graph, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
||||
if (!StateNode) return;
|
||||
|
||||
// Collect and remove transitions connected to this state
|
||||
TArray<UAnimStateTransitionNode*> TransitionsToRemove;
|
||||
for (UEdGraphNode* Node : SMGraph->Nodes)
|
||||
{
|
||||
if (UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node))
|
||||
{
|
||||
if ((TransNode->GetPreviousState() == StateNode) || (TransNode->GetNextState() == StateNode))
|
||||
{
|
||||
TransitionsToRemove.Add(TransNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32 RemovedTransitions = TransitionsToRemove.Num();
|
||||
for (UAnimStateTransitionNode* Trans : TransitionsToRemove)
|
||||
{
|
||||
Trans->BreakAllNodeLinks();
|
||||
SMGraph->RemoveNode(Trans);
|
||||
}
|
||||
|
||||
// Remove the state
|
||||
StateNode->BreakAllNodeLinks();
|
||||
SMGraph->RemoveNode(StateNode);
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("removedState"), StateName);
|
||||
Result->SetNumberField(TEXT("removedTransitions"), RemovedTransitions);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="add_anim_state_transition"))
|
||||
class UMCPHandler_AddAnimTransition : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="State machine graph name"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the source state"))
|
||||
FString FromState;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the target state"))
|
||||
FString ToState;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds"))
|
||||
float CrossfadeDuration = 0.0f;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Transition priority order"))
|
||||
int32 Priority = 0;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional"))
|
||||
bool BBidirectional = false;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Add a transition between two states in an animation state machine graph.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(Blueprint, Graph, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateNode* FromStateNode = MCPUtils::FindStateByName(SMGraph, FromState, Result);
|
||||
if (!FromStateNode) return;
|
||||
|
||||
UAnimStateNode* ToStateNode = MCPUtils::FindStateByName(SMGraph, ToState, Result);
|
||||
if (!ToStateNode) return;
|
||||
|
||||
// Create transition node
|
||||
UAnimStateTransitionNode* TransNode = NewObject<UAnimStateTransitionNode>(SMGraph);
|
||||
TransNode->CreateNewGuid();
|
||||
TransNode->PostPlacedNewNode();
|
||||
TransNode->AllocateDefaultPins();
|
||||
|
||||
// Position between the two states
|
||||
TransNode->NodePosX = (FromStateNode->NodePosX + ToStateNode->NodePosX) / 2;
|
||||
TransNode->NodePosY = (FromStateNode->NodePosY + ToStateNode->NodePosY) / 2;
|
||||
|
||||
SMGraph->AddNode(TransNode, false, false);
|
||||
TransNode->SetFlags(RF_Transactional);
|
||||
|
||||
// Connect: FromState output -> Transition input, Transition output -> ToState input
|
||||
TransNode->CreateConnections(FromStateNode, ToStateNode);
|
||||
|
||||
// Set optional properties
|
||||
if (Json->HasField(TEXT("crossfadeDuration")))
|
||||
{
|
||||
TransNode->CrossfadeDuration = CrossfadeDuration;
|
||||
}
|
||||
if (Json->HasField(TEXT("priority")))
|
||||
{
|
||||
TransNode->PriorityOrder = Priority;
|
||||
}
|
||||
if (Json->HasField(TEXT("bBidirectional")))
|
||||
{
|
||||
TransNode->Bidirectional = BBidirectional;
|
||||
}
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("fromState"), FromState);
|
||||
Result->SetStringField(TEXT("toState"), ToState);
|
||||
Result->SetStringField(TEXT("nodeId"), TransNode->NodeGuid.ToString());
|
||||
Result->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration);
|
||||
Result->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder);
|
||||
Result->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="set_anim_transition_rule"))
|
||||
class UMCPHandler_SetTransitionRule : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="State machine graph name"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the source state"))
|
||||
FString FromState;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the target state"))
|
||||
FString ToState;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds"))
|
||||
float CrossfadeDuration = 0.0f;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Blend mode (as integer enum value)"))
|
||||
int32 BlendMode = 0;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Transition priority order"))
|
||||
int32 PriorityOrder = 0;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Logic type (as integer enum value)"))
|
||||
int32 LogicType = 0;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional"))
|
||||
bool BBidirectional = false;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Update properties on an existing transition between two states in an animation state machine.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(Blueprint, Graph, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromState, ToState);
|
||||
if (!TransNode)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Transition from '%s' to '%s' not found in graph '%s'"),
|
||||
*FromState, *ToState, *Graph));
|
||||
}
|
||||
|
||||
// Update properties
|
||||
int32 ChangedCount = 0;
|
||||
|
||||
if (Json->HasField(TEXT("crossfadeDuration")))
|
||||
{
|
||||
TransNode->CrossfadeDuration = CrossfadeDuration;
|
||||
ChangedCount++;
|
||||
}
|
||||
if (Json->HasField(TEXT("blendMode")))
|
||||
{
|
||||
TransNode->BlendMode = (EAlphaBlendOption)BlendMode;
|
||||
ChangedCount++;
|
||||
}
|
||||
if (Json->HasField(TEXT("priorityOrder")))
|
||||
{
|
||||
TransNode->PriorityOrder = PriorityOrder;
|
||||
ChangedCount++;
|
||||
}
|
||||
if (Json->HasField(TEXT("logicType")))
|
||||
{
|
||||
TransNode->LogicType = (ETransitionLogicType::Type)LogicType;
|
||||
ChangedCount++;
|
||||
}
|
||||
if (Json->HasField(TEXT("bBidirectional")))
|
||||
{
|
||||
TransNode->Bidirectional = BBidirectional;
|
||||
ChangedCount++;
|
||||
}
|
||||
|
||||
if (ChangedCount == 0)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional"));
|
||||
}
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("fromState"), FromState);
|
||||
Result->SetStringField(TEXT("toState"), ToState);
|
||||
Result->SetNumberField(TEXT("propertiesChanged"), ChangedCount);
|
||||
Result->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration);
|
||||
Result->SetNumberField(TEXT("blendMode"), (int32)TransNode->BlendMode);
|
||||
Result->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder);
|
||||
Result->SetNumberField(TEXT("logicType"), (int32)TransNode->LogicType.GetValue());
|
||||
Result->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="set_anim_state_animation"))
|
||||
class UMCPHandler_SetStateAnimation : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="State machine graph name"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the state to modify"))
|
||||
FString StateName;
|
||||
|
||||
UPROPERTY(meta=(Description="Animation asset name to assign"))
|
||||
FString AnimationAsset;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Set or replace the animation sequence played by a state in an animation state machine.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(Blueprint, Graph, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
||||
if (!StateNode) return;
|
||||
|
||||
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
|
||||
if (!InnerGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
|
||||
}
|
||||
|
||||
// Find the animation asset
|
||||
UAnimSequence* AnimSeq = UMCPAssetFinder::LoadAsset<UAnimSequence>(AnimationAsset, Result);
|
||||
if (!AnimSeq) return;
|
||||
|
||||
// Find existing SequencePlayer or create one
|
||||
UAnimGraphNode_SequencePlayer* SeqNode = nullptr;
|
||||
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
||||
{
|
||||
SeqNode = Cast<UAnimGraphNode_SequencePlayer>(Node);
|
||||
if (SeqNode) break;
|
||||
}
|
||||
|
||||
bool bCreatedNew = false;
|
||||
if (!SeqNode)
|
||||
{
|
||||
SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(InnerGraph);
|
||||
SeqNode->CreateNewGuid();
|
||||
SeqNode->PostPlacedNewNode();
|
||||
SeqNode->AllocateDefaultPins();
|
||||
SeqNode->NodePosX = 0;
|
||||
SeqNode->NodePosY = 0;
|
||||
InnerGraph->AddNode(SeqNode, false, false);
|
||||
bCreatedNew = true;
|
||||
}
|
||||
|
||||
SeqNode->SetAnimationAsset(AnimSeq);
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("stateName"), StateName);
|
||||
Result->SetStringField(TEXT("animationAsset"), AnimSeq->GetName());
|
||||
Result->SetBoolField(TEXT("createdNewNode"), bCreatedNew);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="set_anim_state_blend_space"))
|
||||
class UMCPHandler_SetStateBlendSpace : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="State machine graph name"))
|
||||
FString Graph;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the state to modify"))
|
||||
FString StateName;
|
||||
|
||||
UPROPERTY(meta=(Description="Blend Space asset name or path"))
|
||||
FString BlendSpace;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the X axis input"))
|
||||
FString XVariable;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the Y axis input"))
|
||||
FString YVariable;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Place a BlendSpacePlayer in a state's inner graph, connect it to the output pose, "
|
||||
"and optionally wire blueprint variables to the X and Y axis inputs.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(Blueprint, Graph, Result);
|
||||
if (!SMGraph) return;
|
||||
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
|
||||
|
||||
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
||||
if (!StateNode) return;
|
||||
|
||||
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
|
||||
if (!InnerGraph)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
|
||||
}
|
||||
|
||||
// Find the blend space asset
|
||||
UBlendSpace* BlendSpaceAsset = UMCPAssetFinder::LoadAsset<UBlendSpace>(BlendSpace, Result);
|
||||
if (!BlendSpaceAsset) return;
|
||||
|
||||
// Find existing BlendSpacePlayer or create one
|
||||
UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr;
|
||||
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
||||
{
|
||||
BSNode = Cast<UAnimGraphNode_BlendSpacePlayer>(Node);
|
||||
if (BSNode) break;
|
||||
}
|
||||
|
||||
if (!BSNode)
|
||||
{
|
||||
BSNode = NewObject<UAnimGraphNode_BlendSpacePlayer>(InnerGraph);
|
||||
BSNode->CreateNewGuid();
|
||||
BSNode->PostPlacedNewNode();
|
||||
BSNode->AllocateDefaultPins();
|
||||
BSNode->NodePosX = 0;
|
||||
BSNode->NodePosY = 0;
|
||||
InnerGraph->AddNode(BSNode, false, false);
|
||||
}
|
||||
|
||||
BSNode->SetAnimationAsset(BlendSpaceAsset);
|
||||
|
||||
// Connect BlendSpacePlayer output to the Output Animation Pose node
|
||||
{
|
||||
// Find the AnimGraphNode_Root (Output Pose) in the inner graph
|
||||
UEdGraphNode* ResultNode = nullptr;
|
||||
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
||||
{
|
||||
if (Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_Root")) ||
|
||||
Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_StateResult")))
|
||||
{
|
||||
ResultNode = Node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ResultNode)
|
||||
{
|
||||
// Find output pose pin on BlendSpacePlayer and input pose pin on result node
|
||||
UEdGraphPin* BSOutputPin = nullptr;
|
||||
for (UEdGraphPin* Pin : BSNode->Pins)
|
||||
{
|
||||
if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct))
|
||||
{
|
||||
BSOutputPin = Pin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UEdGraphPin* ResultInputPin = nullptr;
|
||||
for (UEdGraphPin* Pin : ResultNode->Pins)
|
||||
{
|
||||
if (Pin && (Pin->Direction == EGPD_Input) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct))
|
||||
{
|
||||
ResultInputPin = Pin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (BSOutputPin && ResultInputPin)
|
||||
{
|
||||
// Break existing connections on the result input
|
||||
ResultInputPin->BreakAllPinLinks();
|
||||
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
|
||||
if (Schema)
|
||||
{
|
||||
Schema->TryCreateConnection(BSOutputPin, ResultInputPin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wire X and Y variables if provided
|
||||
auto WireVariable = [&](const FString& VarName, const FString& PinName) -> bool
|
||||
{
|
||||
if (VarName.IsEmpty()) return false;
|
||||
|
||||
// Verify the variable exists in the blueprint
|
||||
FName VarFName(*VarName);
|
||||
bool bVarFound = false;
|
||||
for (FBPVariableDescription& Var : AnimBP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == VarFName)
|
||||
{
|
||||
bVarFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bVarFound)
|
||||
{
|
||||
// Also check parent class properties
|
||||
if (UClass* GenClass = AnimBP->SkeletonGeneratedClass)
|
||||
{
|
||||
if (FProperty* Prop = GenClass->FindPropertyByName(VarFName))
|
||||
{
|
||||
bVarFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bVarFound)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Variable '%s' not found in '%s', skipping wire"),
|
||||
*VarName, *Blueprint);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a VariableGet node
|
||||
UK2Node_VariableGet* GetNode = NewObject<UK2Node_VariableGet>(InnerGraph);
|
||||
GetNode->VariableReference.SetSelfMember(VarFName);
|
||||
GetNode->NodePosX = BSNode->NodePosX - 250;
|
||||
GetNode->NodePosY = BSNode->NodePosY;
|
||||
InnerGraph->AddNode(GetNode, false, false);
|
||||
GetNode->AllocateDefaultPins();
|
||||
|
||||
// Find the variable output pin
|
||||
UEdGraphPin* VarOutPin = nullptr;
|
||||
for (UEdGraphPin* Pin : GetNode->Pins)
|
||||
{
|
||||
if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinName == VarFName))
|
||||
{
|
||||
VarOutPin = Pin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the target pin on the BlendSpacePlayer
|
||||
UEdGraphPin* TargetPin = BSNode->FindPin(FName(*PinName));
|
||||
|
||||
if (VarOutPin && TargetPin)
|
||||
{
|
||||
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
|
||||
if (Schema)
|
||||
{
|
||||
Schema->TryCreateConnection(VarOutPin, TargetPin);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
WireVariable(XVariable, TEXT("X"));
|
||||
WireVariable(YVariable, TEXT("Y"));
|
||||
|
||||
// Compile and save
|
||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("stateName"), StateName);
|
||||
Result->SetStringField(TEXT("blendSpace"), BlendSpaceAsset->GetName());
|
||||
Result->SetStringField(TEXT("nodeId"), BSNode->NodeGuid.ToString());
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
@@ -1,583 +0,0 @@
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPServer.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "K2Node_VariableGet.h"
|
||||
#include "K2Node_VariableSet.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "Kismet2/KismetEditorUtilities.h"
|
||||
#include "Serialization/JsonReader.h"
|
||||
#include "Serialization/JsonWriter.h"
|
||||
#include "Serialization/JsonSerializer.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
|
||||
// ============================================================
|
||||
// HandleChangeVariableType — change a Blueprint member variable's type
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleChangeVariableType(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString VariableName = Json->GetStringField(TEXT("variable"));
|
||||
FString NewTypeName = Json->GetStringField(TEXT("newType"));
|
||||
FString TypeCategory; // now optional
|
||||
if (Json->HasField(TEXT("typeCategory")))
|
||||
{
|
||||
TypeCategory = Json->GetStringField(TEXT("typeCategory"));
|
||||
}
|
||||
|
||||
if (BlueprintName.IsEmpty() || VariableName.IsEmpty() || NewTypeName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variable, newType"));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Verify variable exists
|
||||
bool bVarFound = false;
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName.ToString() == VariableName)
|
||||
{
|
||||
bVarFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bVarFound)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName));
|
||||
}
|
||||
|
||||
// Build the new pin type using shared resolver
|
||||
FEdGraphPinType NewPinType;
|
||||
FString ResolveInput = NewTypeName;
|
||||
|
||||
// If typeCategory is an object reference variant, use colon syntax for the resolver
|
||||
if (TypeCategory == TEXT("object") || TypeCategory == TEXT("softobject") ||
|
||||
TypeCategory == TEXT("class") || TypeCategory == TEXT("softclass") ||
|
||||
TypeCategory == TEXT("interface"))
|
||||
{
|
||||
ResolveInput = TypeCategory + TEXT(":") + NewTypeName;
|
||||
}
|
||||
|
||||
if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, Result))
|
||||
return;
|
||||
|
||||
// Derive typeCategory from the resolved pin type for the response
|
||||
if (TypeCategory.IsEmpty())
|
||||
{
|
||||
if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
|
||||
TypeCategory = TEXT("struct");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Enum || NewPinType.PinCategory == UEdGraphSchema_K2::PC_Byte)
|
||||
TypeCategory = TEXT("enum");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Object)
|
||||
TypeCategory = TEXT("object");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftObject)
|
||||
TypeCategory = TEXT("softobject");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Class)
|
||||
TypeCategory = TEXT("class");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass)
|
||||
TypeCategory = TEXT("softclass");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Interface)
|
||||
TypeCategory = TEXT("interface");
|
||||
else
|
||||
TypeCategory = NewPinType.PinCategory.ToString();
|
||||
}
|
||||
|
||||
// Check for dry run
|
||||
bool bDryRun = false;
|
||||
if (Json->HasField(TEXT("dryRun")))
|
||||
{
|
||||
bDryRun = Json->GetBoolField(TEXT("dryRun"));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s variable '%s' in '%s' to %s (%s)"),
|
||||
bDryRun ? TEXT("[DRY RUN] Analyzing change of") : TEXT("Changing"),
|
||||
*VariableName, *BlueprintName, *NewTypeName, *TypeCategory);
|
||||
|
||||
// Analyze affected nodes (get/set nodes for this variable)
|
||||
TArray<TSharedPtr<FJsonValue>> AffectedNodes;
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (!Node) continue;
|
||||
if (auto* VG = Cast<UK2Node_VariableGet>(Node))
|
||||
{
|
||||
if (VG->GetVarName().ToString() == VariableName)
|
||||
{
|
||||
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
|
||||
AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString());
|
||||
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet"));
|
||||
AffNode->SetStringField(TEXT("graph"), Graph->GetName());
|
||||
// Check which pins would be affected
|
||||
TArray<TSharedPtr<FJsonValue>> AffPins;
|
||||
for (UEdGraphPin* Pin : VG->Pins)
|
||||
{
|
||||
if (Pin && Pin->LinkedTo.Num() > 0 && Pin->Direction == EGPD_Output)
|
||||
{
|
||||
AffPins.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("%s (connected to %d pin(s))"),
|
||||
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
|
||||
}
|
||||
}
|
||||
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
|
||||
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
|
||||
}
|
||||
}
|
||||
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
|
||||
{
|
||||
if (VS->GetVarName().ToString() == VariableName)
|
||||
{
|
||||
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
|
||||
AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString());
|
||||
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet"));
|
||||
AffNode->SetStringField(TEXT("graph"), Graph->GetName());
|
||||
TArray<TSharedPtr<FJsonValue>> AffPins;
|
||||
for (UEdGraphPin* Pin : VS->Pins)
|
||||
{
|
||||
if (Pin && Pin->LinkedTo.Num() > 0)
|
||||
{
|
||||
AffPins.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("%s (connected to %d pin(s))"),
|
||||
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
|
||||
}
|
||||
}
|
||||
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
|
||||
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bDryRun)
|
||||
{
|
||||
Result->SetBoolField(TEXT("dryRun"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("variable"), VariableName);
|
||||
Result->SetStringField(TEXT("newType"), NewTypeName);
|
||||
Result->SetStringField(TEXT("typeCategory"), TypeCategory);
|
||||
Result->SetNumberField(TEXT("affectedNodeCount"), AffectedNodes.Num());
|
||||
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Directly modify the variable type in the description array.
|
||||
for (FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == FName(*VariableName))
|
||||
{
|
||||
Var.VarType = NewPinType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"),
|
||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||
|
||||
// Return updated variable state
|
||||
TSharedRef<FJsonObject> UpdatedVar = MakeShared<FJsonObject>();
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == FName(*VariableName))
|
||||
{
|
||||
UpdatedVar->SetStringField(TEXT("name"), Var.VarName.ToString());
|
||||
UpdatedVar->SetStringField(TEXT("type"), Var.VarType.PinCategory.ToString());
|
||||
if (Var.VarType.PinSubCategoryObject.IsValid())
|
||||
UpdatedVar->SetStringField(TEXT("subtype"), Var.VarType.PinSubCategoryObject->GetName());
|
||||
UpdatedVar->SetBoolField(TEXT("isArray"), Var.VarType.IsArray());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("variable"), VariableName);
|
||||
Result->SetStringField(TEXT("newType"), NewTypeName);
|
||||
Result->SetStringField(TEXT("typeCategory"), TypeCategory);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
Result->SetObjectField(TEXT("updatedVariable"), UpdatedVar);
|
||||
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleAddVariable — add a new member variable to a Blueprint
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleAddVariable(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString VariableName = Json->GetStringField(TEXT("variableName"));
|
||||
FString VariableType = Json->GetStringField(TEXT("variableType"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || VariableName.IsEmpty() || VariableType.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variableName, variableType"));
|
||||
}
|
||||
|
||||
FString Category;
|
||||
if (Json->HasField(TEXT("category")))
|
||||
{
|
||||
Category = Json->GetStringField(TEXT("category"));
|
||||
}
|
||||
|
||||
bool bIsArray = false;
|
||||
if (Json->HasField(TEXT("isArray")))
|
||||
{
|
||||
bIsArray = Json->GetBoolField(TEXT("isArray"));
|
||||
}
|
||||
|
||||
FString DefaultValue;
|
||||
if (Json->HasField(TEXT("defaultValue")))
|
||||
{
|
||||
DefaultValue = Json->GetStringField(TEXT("defaultValue"));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Check for duplicate variable name
|
||||
FName VarFName(*VariableName);
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == VarFName)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Variable '%s' already exists in Blueprint '%s'"), *VariableName, *BlueprintName));
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the type using the shared helper
|
||||
FEdGraphPinType PinType;
|
||||
if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, Result))
|
||||
return;
|
||||
|
||||
// Set container type for arrays
|
||||
if (bIsArray)
|
||||
{
|
||||
PinType.ContainerType = EPinContainerType::Array;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding variable '%s' (type=%s, array=%s) to Blueprint '%s'"),
|
||||
*VariableName, *VariableType, bIsArray ? TEXT("true") : TEXT("false"), *BlueprintName);
|
||||
|
||||
// Add the variable using the editor utility function
|
||||
bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue);
|
||||
if (!bSuccess)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName));
|
||||
}
|
||||
|
||||
// Set category if provided
|
||||
if (!Category.IsEmpty())
|
||||
{
|
||||
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added variable '%s' to '%s' (saved: %s)"),
|
||||
*VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("variableName"), VariableName);
|
||||
Result->SetStringField(TEXT("variableType"), VariableType);
|
||||
if (!Category.IsEmpty())
|
||||
{
|
||||
Result->SetStringField(TEXT("category"), Category);
|
||||
}
|
||||
Result->SetBoolField(TEXT("isArray"), bIsArray);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleRemoveVariable — remove a member variable from a Blueprint
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleRemoveVariable(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString VariableName = Json->GetStringField(TEXT("variableName"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || VariableName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variableName"));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find variable by name (case-insensitive)
|
||||
FName VarFName(*VariableName);
|
||||
bool bVarFound = false;
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName.ToString().Equals(VariableName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
VarFName = Var.VarName; // Use the exact name found
|
||||
bVarFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bVarFound)
|
||||
{
|
||||
// Build available variables list for helpful error message
|
||||
TArray<TSharedPtr<FJsonValue>> AvailVars;
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
AvailVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName));
|
||||
Result->SetArrayField(TEXT("availableVariables"), AvailVars);
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing variable '%s' from Blueprint '%s'"),
|
||||
*VariableName, *BlueprintName);
|
||||
|
||||
// Use the editor utility to remove the variable (also cleans up Get/Set nodes)
|
||||
FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed variable '%s' from '%s' (saved: %s)"),
|
||||
*VariableName, *BlueprintName, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("variableName"), VariableName);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HandleSetVariableMetadata — set variable properties (category, tooltip, replication, etc.)
|
||||
// ============================================================
|
||||
|
||||
void FBlueprintMCPServer::HandleSetVariableMetadata(const FJsonObject* Json, FJsonObject* Result)
|
||||
{
|
||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||
FString VariableName = Json->GetStringField(TEXT("variable"));
|
||||
|
||||
if (BlueprintName.IsEmpty() || VariableName.IsEmpty())
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, variable"));
|
||||
}
|
||||
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find the variable
|
||||
FName VarFName(*VariableName);
|
||||
FBPVariableDescription* VarDesc = nullptr;
|
||||
for (FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == VarFName)
|
||||
{
|
||||
VarDesc = &Var;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!VarDesc)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> AvailableVars;
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
AvailableVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
|
||||
}
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *BlueprintName));
|
||||
Result->SetArrayField(TEXT("availableVariables"), AvailableVars);
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Changes;
|
||||
|
||||
// Category
|
||||
if (Json->HasField(TEXT("category")))
|
||||
{
|
||||
FString OldCategory = VarDesc->Category.ToString();
|
||||
FString NewCategory = Json->GetStringField(TEXT("category"));
|
||||
VarDesc->Category = FText::FromString(NewCategory);
|
||||
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(NewCategory));
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("category"));
|
||||
Change->SetStringField(TEXT("oldValue"), OldCategory);
|
||||
Change->SetStringField(TEXT("newValue"), NewCategory);
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
if (Json->HasField(TEXT("tooltip")))
|
||||
{
|
||||
FString OldTooltip;
|
||||
FBlueprintEditorUtils::GetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), OldTooltip);
|
||||
FString NewTooltip = Json->GetStringField(TEXT("tooltip"));
|
||||
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), NewTooltip);
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("tooltip"));
|
||||
Change->SetStringField(TEXT("oldValue"), OldTooltip);
|
||||
Change->SetStringField(TEXT("newValue"), NewTooltip);
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// Replication
|
||||
if (Json->HasField(TEXT("replication")))
|
||||
{
|
||||
FString ReplicationStr = Json->GetStringField(TEXT("replication"));
|
||||
uint64 OldFlags = VarDesc->PropertyFlags;
|
||||
|
||||
if (ReplicationStr == TEXT("none"))
|
||||
{
|
||||
VarDesc->PropertyFlags &= ~CPF_Net;
|
||||
VarDesc->PropertyFlags &= ~CPF_RepNotify;
|
||||
VarDesc->RepNotifyFunc = NAME_None;
|
||||
}
|
||||
else if (ReplicationStr == TEXT("replicated"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Net;
|
||||
VarDesc->PropertyFlags &= ~CPF_RepNotify;
|
||||
VarDesc->RepNotifyFunc = NAME_None;
|
||||
}
|
||||
else if (ReplicationStr == TEXT("repNotify"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Net | CPF_RepNotify;
|
||||
// Auto-generate RepNotify function name
|
||||
VarDesc->RepNotifyFunc = FName(*FString::Printf(TEXT("OnRep_%s"), *VariableName));
|
||||
}
|
||||
else
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *ReplicationStr));
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("replication"));
|
||||
Change->SetStringField(TEXT("newValue"), ReplicationStr);
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// ExposeOnSpawn
|
||||
if (Json->HasField(TEXT("exposeOnSpawn")))
|
||||
{
|
||||
bool bOld = (VarDesc->PropertyFlags & CPF_ExposeOnSpawn) != 0;
|
||||
bool bNew = Json->GetBoolField(TEXT("exposeOnSpawn"));
|
||||
if (bNew)
|
||||
VarDesc->PropertyFlags |= CPF_ExposeOnSpawn;
|
||||
else
|
||||
VarDesc->PropertyFlags &= ~CPF_ExposeOnSpawn;
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("exposeOnSpawn"));
|
||||
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
|
||||
Change->SetStringField(TEXT("newValue"), bNew ? TEXT("true") : TEXT("false"));
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// isPrivate
|
||||
if (Json->HasField(TEXT("isPrivate")))
|
||||
{
|
||||
bool bOld = (VarDesc->PropertyFlags & CPF_DisableEditOnInstance) != 0;
|
||||
bool bNew = Json->GetBoolField(TEXT("isPrivate"));
|
||||
// In UE5, "private" for Blueprint variables is represented via metadata
|
||||
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr,
|
||||
TEXT("BlueprintPrivate"), bNew ? TEXT("true") : TEXT("false"));
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("isPrivate"));
|
||||
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
|
||||
Change->SetStringField(TEXT("newValue"), bNew ? TEXT("true") : TEXT("false"));
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// Editability (EditAnywhere, EditDefaultsOnly, EditInstanceOnly)
|
||||
if (Json->HasField(TEXT("editability")))
|
||||
{
|
||||
FString Editability = Json->GetStringField(TEXT("editability"));
|
||||
|
||||
// Clear all edit flags first
|
||||
VarDesc->PropertyFlags &= ~(CPF_Edit | CPF_DisableEditOnInstance | CPF_DisableEditOnTemplate);
|
||||
|
||||
if (Editability == TEXT("editAnywhere"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Edit;
|
||||
}
|
||||
else if (Editability == TEXT("editDefaultsOnly"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnInstance;
|
||||
}
|
||||
else if (Editability == TEXT("editInstanceOnly"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnTemplate;
|
||||
}
|
||||
else if (Editability == TEXT("none"))
|
||||
{
|
||||
// All edit flags already cleared
|
||||
}
|
||||
else
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"),
|
||||
*Editability));
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("editability"));
|
||||
Change->SetStringField(TEXT("newValue"), Editability);
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
if (Changes.Num() == 0)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability"));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetVariableMetadata on '%s.%s' — %d field(s) changed"),
|
||||
*BlueprintName, *VariableName, Changes.Num());
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), BlueprintName);
|
||||
Result->SetStringField(TEXT("variable"), VariableName);
|
||||
Result->SetArrayField(TEXT("changes"), Changes);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "MCPHandler.h"
|
||||
#include "MCPAssetFinder.h"
|
||||
#include "MCPUtils.h"
|
||||
#include "Engine/Blueprint.h"
|
||||
#include "EdGraph/EdGraph.h"
|
||||
#include "EdGraph/EdGraphPin.h"
|
||||
#include "K2Node_VariableGet.h"
|
||||
#include "K2Node_VariableSet.h"
|
||||
#include "Kismet2/BlueprintEditorUtils.h"
|
||||
#include "MCPHandlers_Variables.generated.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="change_blueprint_variable_type"))
|
||||
class UMCPHandler_ChangeVariableType : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the variable to change"))
|
||||
FString Variable;
|
||||
|
||||
UPROPERTY(meta=(Description="New type name for the variable"))
|
||||
FString NewType;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Type category: object, softobject, class, softclass, interface, struct, enum"))
|
||||
FString TypeCategory;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, analyze the change without applying it"))
|
||||
bool DryRun = false;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Change the type of a Blueprint member variable. "
|
||||
"Supports dry-run mode to preview affected nodes before committing.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Verify variable exists
|
||||
bool bVarFound = false;
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName.ToString() == Variable)
|
||||
{
|
||||
bVarFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bVarFound)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint));
|
||||
}
|
||||
|
||||
// Build the new pin type using shared resolver
|
||||
FEdGraphPinType NewPinType;
|
||||
FString ResolveInput = NewType;
|
||||
|
||||
// If typeCategory is an object reference variant, use colon syntax for the resolver
|
||||
if (TypeCategory == TEXT("object") || TypeCategory == TEXT("softobject") ||
|
||||
TypeCategory == TEXT("class") || TypeCategory == TEXT("softclass") ||
|
||||
TypeCategory == TEXT("interface"))
|
||||
{
|
||||
ResolveInput = TypeCategory + TEXT(":") + NewType;
|
||||
}
|
||||
|
||||
if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, Result))
|
||||
return;
|
||||
|
||||
// Derive typeCategory from the resolved pin type for the response
|
||||
FString ResolvedTypeCategory = TypeCategory;
|
||||
if (ResolvedTypeCategory.IsEmpty())
|
||||
{
|
||||
if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
|
||||
ResolvedTypeCategory = TEXT("struct");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Enum || NewPinType.PinCategory == UEdGraphSchema_K2::PC_Byte)
|
||||
ResolvedTypeCategory = TEXT("enum");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Object)
|
||||
ResolvedTypeCategory = TEXT("object");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftObject)
|
||||
ResolvedTypeCategory = TEXT("softobject");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Class)
|
||||
ResolvedTypeCategory = TEXT("class");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass)
|
||||
ResolvedTypeCategory = TEXT("softclass");
|
||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Interface)
|
||||
ResolvedTypeCategory = TEXT("interface");
|
||||
else
|
||||
ResolvedTypeCategory = NewPinType.PinCategory.ToString();
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s variable '%s' in '%s' to %s (%s)"),
|
||||
DryRun ? TEXT("[DRY RUN] Analyzing change of") : TEXT("Changing"),
|
||||
*Variable, *Blueprint, *NewType, *ResolvedTypeCategory);
|
||||
|
||||
// Analyze affected nodes (get/set nodes for this variable)
|
||||
TArray<TSharedPtr<FJsonValue>> AffectedNodes;
|
||||
TArray<UEdGraph*> AllGraphs;
|
||||
BP->GetAllGraphs(AllGraphs);
|
||||
for (UEdGraph* Graph : AllGraphs)
|
||||
{
|
||||
if (!Graph) continue;
|
||||
for (UEdGraphNode* Node : Graph->Nodes)
|
||||
{
|
||||
if (!Node) continue;
|
||||
if (auto* VG = Cast<UK2Node_VariableGet>(Node))
|
||||
{
|
||||
if (VG->GetVarName().ToString() == Variable)
|
||||
{
|
||||
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
|
||||
AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString());
|
||||
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet"));
|
||||
AffNode->SetStringField(TEXT("graph"), Graph->GetName());
|
||||
// Check which pins would be affected
|
||||
TArray<TSharedPtr<FJsonValue>> AffPins;
|
||||
for (UEdGraphPin* Pin : VG->Pins)
|
||||
{
|
||||
if (Pin && Pin->LinkedTo.Num() > 0 && Pin->Direction == EGPD_Output)
|
||||
{
|
||||
AffPins.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("%s (connected to %d pin(s))"),
|
||||
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
|
||||
}
|
||||
}
|
||||
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
|
||||
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
|
||||
}
|
||||
}
|
||||
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
|
||||
{
|
||||
if (VS->GetVarName().ToString() == Variable)
|
||||
{
|
||||
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
|
||||
AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString());
|
||||
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet"));
|
||||
AffNode->SetStringField(TEXT("graph"), Graph->GetName());
|
||||
TArray<TSharedPtr<FJsonValue>> AffPins;
|
||||
for (UEdGraphPin* Pin : VS->Pins)
|
||||
{
|
||||
if (Pin && Pin->LinkedTo.Num() > 0)
|
||||
{
|
||||
AffPins.Add(MakeShared<FJsonValueString>(
|
||||
FString::Printf(TEXT("%s (connected to %d pin(s))"),
|
||||
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
|
||||
}
|
||||
}
|
||||
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
|
||||
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DryRun)
|
||||
{
|
||||
Result->SetBoolField(TEXT("dryRun"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("variable"), Variable);
|
||||
Result->SetStringField(TEXT("newType"), NewType);
|
||||
Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory);
|
||||
Result->SetNumberField(TEXT("affectedNodeCount"), AffectedNodes.Num());
|
||||
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Directly modify the variable type in the description array.
|
||||
for (FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == FName(*Variable))
|
||||
{
|
||||
Var.VarType = NewPinType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"),
|
||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||
|
||||
// Return updated variable state
|
||||
TSharedRef<FJsonObject> UpdatedVar = MakeShared<FJsonObject>();
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == FName(*Variable))
|
||||
{
|
||||
UpdatedVar->SetStringField(TEXT("name"), Var.VarName.ToString());
|
||||
UpdatedVar->SetStringField(TEXT("type"), Var.VarType.PinCategory.ToString());
|
||||
if (Var.VarType.PinSubCategoryObject.IsValid())
|
||||
UpdatedVar->SetStringField(TEXT("subtype"), Var.VarType.PinSubCategoryObject->GetName());
|
||||
UpdatedVar->SetBoolField(TEXT("isArray"), Var.VarType.IsArray());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("variable"), Variable);
|
||||
Result->SetStringField(TEXT("newType"), NewType);
|
||||
Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
Result->SetObjectField(TEXT("updatedVariable"), UpdatedVar);
|
||||
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="add_blueprint_variable"))
|
||||
class UMCPHandler_AddVariable : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the new variable"))
|
||||
FString VariableName;
|
||||
|
||||
UPROPERTY(meta=(Description="Type of the new variable"))
|
||||
FString VariableType;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Category to assign the variable to"))
|
||||
FString Category;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, make the variable an array"))
|
||||
bool IsArray = false;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Default value for the variable"))
|
||||
FString DefaultValue;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Add a new member variable to a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Check for duplicate variable name
|
||||
FName VarFName(*VariableName);
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == VarFName)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Variable '%s' already exists in Blueprint '%s'"), *VariableName, *Blueprint));
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the type using the shared helper
|
||||
FEdGraphPinType PinType;
|
||||
if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, Result))
|
||||
return;
|
||||
|
||||
// Set container type for arrays
|
||||
if (IsArray)
|
||||
{
|
||||
PinType.ContainerType = EPinContainerType::Array;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding variable '%s' (type=%s, array=%s) to Blueprint '%s'"),
|
||||
*VariableName, *VariableType, IsArray ? TEXT("true") : TEXT("false"), *Blueprint);
|
||||
|
||||
// Add the variable using the editor utility function
|
||||
bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue);
|
||||
if (!bSuccess)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName));
|
||||
}
|
||||
|
||||
// Set category if provided
|
||||
if (!Category.IsEmpty())
|
||||
{
|
||||
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
|
||||
}
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added variable '%s' to '%s' (saved: %s)"),
|
||||
*VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("variableName"), VariableName);
|
||||
Result->SetStringField(TEXT("variableType"), VariableType);
|
||||
if (!Category.IsEmpty())
|
||||
{
|
||||
Result->SetStringField(TEXT("category"), Category);
|
||||
}
|
||||
Result->SetBoolField(TEXT("isArray"), IsArray);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="remove_blueprint_variable"))
|
||||
class UMCPHandler_RemoveVariable : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the variable to remove"))
|
||||
FString VariableName;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Remove a member variable from a Blueprint.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find variable by name (case-insensitive)
|
||||
FName VarFName(*VariableName);
|
||||
bool bVarFound = false;
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName.ToString().Equals(VariableName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
VarFName = Var.VarName; // Use the exact name found
|
||||
bVarFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bVarFound)
|
||||
{
|
||||
// Build available variables list for helpful error message
|
||||
TArray<TSharedPtr<FJsonValue>> AvailVars;
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
AvailVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
|
||||
}
|
||||
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *Blueprint));
|
||||
Result->SetArrayField(TEXT("availableVariables"), AvailVars);
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing variable '%s' from Blueprint '%s'"),
|
||||
*VariableName, *Blueprint);
|
||||
|
||||
// Use the editor utility to remove the variable (also cleans up Get/Set nodes)
|
||||
FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName);
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed variable '%s' from '%s' (saved: %s)"),
|
||||
*VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("variableName"), VariableName);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
UCLASS(meta=(ToolName="set_blueprint_variable_metadata"))
|
||||
class UMCPHandler_SetVariableMetadata : public UObject, public IMCPHandler
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||
FString Blueprint;
|
||||
|
||||
UPROPERTY(meta=(Description="Name of the variable to modify"))
|
||||
FString Variable;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Category to assign the variable to"))
|
||||
FString Category;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Tooltip text for the variable"))
|
||||
FString Tooltip;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Replication mode: none, replicated, or repNotify"))
|
||||
FString Replication;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, expose this variable on spawn"))
|
||||
bool ExposeOnSpawn = false;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="If true, mark the variable as private"))
|
||||
bool IsPrivate = false;
|
||||
|
||||
UPROPERTY(meta=(Optional, Description="Editability mode: editAnywhere, editDefaultsOnly, editInstanceOnly, or none"))
|
||||
FString Editability;
|
||||
|
||||
virtual FString GetDescription() const override
|
||||
{
|
||||
return TEXT("Set variable metadata properties such as category, tooltip, "
|
||||
"replication, editability, and visibility flags.");
|
||||
}
|
||||
|
||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||
{
|
||||
// Load Blueprint
|
||||
FString LoadError;
|
||||
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(Blueprint, LoadError);
|
||||
if (!BP)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, LoadError);
|
||||
}
|
||||
|
||||
// Find the variable
|
||||
FName VarFName(*Variable);
|
||||
FBPVariableDescription* VarDesc = nullptr;
|
||||
for (FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
if (Var.VarName == VarFName)
|
||||
{
|
||||
VarDesc = &Var;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!VarDesc)
|
||||
{
|
||||
TArray<TSharedPtr<FJsonValue>> AvailableVars;
|
||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||
{
|
||||
AvailableVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
|
||||
}
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint));
|
||||
Result->SetArrayField(TEXT("availableVariables"), AvailableVars);
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<TSharedPtr<FJsonValue>> Changes;
|
||||
|
||||
// Category
|
||||
if (Json->HasField(TEXT("category")))
|
||||
{
|
||||
FString OldCategory = VarDesc->Category.ToString();
|
||||
VarDesc->Category = FText::FromString(Category);
|
||||
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("category"));
|
||||
Change->SetStringField(TEXT("oldValue"), OldCategory);
|
||||
Change->SetStringField(TEXT("newValue"), Category);
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
if (Json->HasField(TEXT("tooltip")))
|
||||
{
|
||||
FString OldTooltip;
|
||||
FBlueprintEditorUtils::GetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), OldTooltip);
|
||||
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), Tooltip);
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("tooltip"));
|
||||
Change->SetStringField(TEXT("oldValue"), OldTooltip);
|
||||
Change->SetStringField(TEXT("newValue"), Tooltip);
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// Replication
|
||||
if (Json->HasField(TEXT("replication")))
|
||||
{
|
||||
uint64 OldFlags = VarDesc->PropertyFlags;
|
||||
|
||||
if (Replication == TEXT("none"))
|
||||
{
|
||||
VarDesc->PropertyFlags &= ~CPF_Net;
|
||||
VarDesc->PropertyFlags &= ~CPF_RepNotify;
|
||||
VarDesc->RepNotifyFunc = NAME_None;
|
||||
}
|
||||
else if (Replication == TEXT("replicated"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Net;
|
||||
VarDesc->PropertyFlags &= ~CPF_RepNotify;
|
||||
VarDesc->RepNotifyFunc = NAME_None;
|
||||
}
|
||||
else if (Replication == TEXT("repNotify"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Net | CPF_RepNotify;
|
||||
// Auto-generate RepNotify function name
|
||||
VarDesc->RepNotifyFunc = FName(*FString::Printf(TEXT("OnRep_%s"), *Variable));
|
||||
}
|
||||
else
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *Replication));
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("replication"));
|
||||
Change->SetStringField(TEXT("newValue"), Replication);
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// ExposeOnSpawn
|
||||
if (Json->HasField(TEXT("exposeOnSpawn")))
|
||||
{
|
||||
bool bOld = (VarDesc->PropertyFlags & CPF_ExposeOnSpawn) != 0;
|
||||
if (ExposeOnSpawn)
|
||||
VarDesc->PropertyFlags |= CPF_ExposeOnSpawn;
|
||||
else
|
||||
VarDesc->PropertyFlags &= ~CPF_ExposeOnSpawn;
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("exposeOnSpawn"));
|
||||
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
|
||||
Change->SetStringField(TEXT("newValue"), ExposeOnSpawn ? TEXT("true") : TEXT("false"));
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// isPrivate
|
||||
if (Json->HasField(TEXT("isPrivate")))
|
||||
{
|
||||
bool bOld = (VarDesc->PropertyFlags & CPF_DisableEditOnInstance) != 0;
|
||||
// In UE5, "private" for Blueprint variables is represented via metadata
|
||||
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr,
|
||||
TEXT("BlueprintPrivate"), IsPrivate ? TEXT("true") : TEXT("false"));
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("isPrivate"));
|
||||
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
|
||||
Change->SetStringField(TEXT("newValue"), IsPrivate ? TEXT("true") : TEXT("false"));
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
// Editability (EditAnywhere, EditDefaultsOnly, EditInstanceOnly)
|
||||
if (Json->HasField(TEXT("editability")))
|
||||
{
|
||||
// Clear all edit flags first
|
||||
VarDesc->PropertyFlags &= ~(CPF_Edit | CPF_DisableEditOnInstance | CPF_DisableEditOnTemplate);
|
||||
|
||||
if (Editability == TEXT("editAnywhere"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Edit;
|
||||
}
|
||||
else if (Editability == TEXT("editDefaultsOnly"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnInstance;
|
||||
}
|
||||
else if (Editability == TEXT("editInstanceOnly"))
|
||||
{
|
||||
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnTemplate;
|
||||
}
|
||||
else if (Editability == TEXT("none"))
|
||||
{
|
||||
// All edit flags already cleared
|
||||
}
|
||||
else
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||
TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"),
|
||||
*Editability));
|
||||
}
|
||||
|
||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||
Change->SetStringField(TEXT("field"), TEXT("editability"));
|
||||
Change->SetStringField(TEXT("newValue"), Editability);
|
||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||
}
|
||||
|
||||
if (Changes.Num() == 0)
|
||||
{
|
||||
return MCPUtils::MakeErrorJson(Result, TEXT("No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability"));
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetVariableMetadata on '%s.%s' — %d field(s) changed"),
|
||||
*Blueprint, *Variable, Changes.Num());
|
||||
|
||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||
|
||||
Result->SetBoolField(TEXT("success"), true);
|
||||
Result->SetStringField(TEXT("blueprint"), Blueprint);
|
||||
Result->SetStringField(TEXT("variable"), Variable);
|
||||
Result->SetArrayField(TEXT("changes"), Changes);
|
||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||
}
|
||||
};
|
||||
@@ -419,23 +419,6 @@ void FMCPServer::BuildCachedToolsList()
|
||||
ToolsArray.Add(MakeShared<FJsonValueObject>(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<FJsonObject> Tool = MakeShared<FJsonObject>();
|
||||
Tool->SetStringField(TEXT("name"), KV.Key);
|
||||
Tool->SetStringField(TEXT("description"), FString::Printf(TEXT("Tool: %s"), *KV.Key));
|
||||
|
||||
TSharedRef<FJsonObject> InputSchema = MakeShared<FJsonObject>();
|
||||
InputSchema->SetStringField(TEXT("type"), TEXT("object"));
|
||||
Tool->SetObjectField(TEXT("inputSchema"), InputSchema);
|
||||
|
||||
ToolsArray.Add(MakeShared<FJsonValueObject>(Tool));
|
||||
}
|
||||
|
||||
CachedToolsList = MakeShared<FJsonObject>();
|
||||
CachedToolsList->SetArrayField(TEXT("tools"), ToolsArray);
|
||||
}
|
||||
@@ -517,21 +500,6 @@ void FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Pa
|
||||
GEditor->EndTransaction();
|
||||
}
|
||||
}
|
||||
else if (FRequestHandler* Handler = HandlerMap.Find(ToolName))
|
||||
{
|
||||
const bool bIsMutation = MutationEndpoints.Contains(ToolName);
|
||||
if (bIsMutation && GEditor)
|
||||
{
|
||||
GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *ToolName)));
|
||||
}
|
||||
|
||||
(*Handler)(Params, Result);
|
||||
|
||||
if (bIsMutation && GEditor)
|
||||
{
|
||||
GEditor->EndTransaction();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Unknown tool: %s"), *ToolName));
|
||||
@@ -862,7 +830,6 @@ void FMCPServer::RegisterHandlers()
|
||||
TEXT("add_function_parameter"),
|
||||
TEXT("add_blueprint_component"),
|
||||
TEXT("remove_blueprint_component"),
|
||||
TEXT("restore_blueprint_graph_from_snapshot"),
|
||||
TEXT("create_material_asset"),
|
||||
TEXT("set_material_property"),
|
||||
TEXT("add_material_expression"),
|
||||
@@ -875,7 +842,6 @@ void FMCPServer::RegisterHandlers()
|
||||
TEXT("set_material_instance_parameter"),
|
||||
TEXT("reparent_material_instance"),
|
||||
TEXT("create_material_function_asset"),
|
||||
TEXT("restore_material_graph_from_snapshot"),
|
||||
TEXT("add_anim_state_to_machine"),
|
||||
TEXT("remove_anim_state_from_machine"),
|
||||
TEXT("add_anim_state_transition"),
|
||||
@@ -883,77 +849,6 @@ void FMCPServer::RegisterHandlers()
|
||||
TEXT("set_anim_state_animation"),
|
||||
};
|
||||
|
||||
// All handlers have uniform signature: void(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("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("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("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("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("set_anim_state_animation"), &FMCPServer::HandleSetStateAnimation);
|
||||
H(TEXT("set_anim_state_blend_space"), &FMCPServer::HandleSetStateBlendSpace);
|
||||
}
|
||||
|
||||
void FMCPServer::BuildMCPHandlerRegistry()
|
||||
|
||||
@@ -47,9 +47,7 @@ public:
|
||||
|
||||
private:
|
||||
// ----- Tool dispatch -----
|
||||
using FRequestHandler = TFunction<void(const FJsonObject* Json, FJsonObject* Result)>;
|
||||
TMap<FString, FRequestHandler> HandlerMap; // old-style handlers
|
||||
TMap<FString, UClass*> MCPHandlerRegistry; // new-style: tool name -> UMCPHandler subclass
|
||||
TMap<FString, UClass*> MCPHandlerRegistry; // tool name -> UMCPHandler subclass
|
||||
TSet<FString> MutationEndpoints;
|
||||
void RegisterHandlers();
|
||||
void BuildMCPHandlerRegistry();
|
||||
@@ -105,126 +103,7 @@ private:
|
||||
FCriticalSection Mutex;
|
||||
TArray<TSharedPtr<FPendingMessage>> PendingMessages;
|
||||
bool bShuttingDown = false;
|
||||
|
||||
// ----- Request handlers (read-only) -----
|
||||
void HandleList(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleGetBlueprint(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleGetGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSearch(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleFindReferences(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSearchByType(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Request handlers (write) -----
|
||||
void HandleChangeVariableType(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleChangeFunctionParamType(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Pin introspection (read-only) -----
|
||||
void HandleGetPinInfo(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleCheckPinCompatibility(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Class/function discovery (read-only) -----
|
||||
void HandleListClasses(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleListFunctions(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleListProperties(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Reparent -----
|
||||
void HandleReparentBlueprint(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Create -----
|
||||
void HandleCreateBlueprint(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleCreateGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Graph manipulation -----
|
||||
void HandleDeleteGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleRenameGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Variables -----
|
||||
void HandleAddVariable(const FJsonObject* Json, FJsonObject* Result);
|
||||
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);
|
||||
|
||||
// ----- Function Parameters -----
|
||||
void HandleAddFunctionParameter(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Components -----
|
||||
void HandleAddComponent(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleRemoveComponent(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleListComponents(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Diagnostic -----
|
||||
void HandleTestSave(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Snapshot / Safety tools (write) -----
|
||||
void HandleSnapshotGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleDiffGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleRestoreGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
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);
|
||||
void HandleGetMaterialGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleDescribeMaterial(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSearchMaterials(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleFindMaterialReferences(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Material mutation handlers (Phase 2) -----
|
||||
void HandleCreateMaterial(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSetMaterialProperty(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleAddMaterialExpression(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleDeleteMaterialExpression(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleConnectMaterialPins(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleDisconnectMaterialPin(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSetExpressionValue(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleMoveMaterialExpression(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Material instance handlers (Phase 3) -----
|
||||
void HandleCreateMaterialInstance(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSetMaterialInstanceParameter(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleGetMaterialInstanceParameters(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleReparentMaterialInstance(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Material function handlers (Phase 4) -----
|
||||
void HandleListMaterialFunctions(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleGetMaterialFunction(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleCreateMaterialFunction(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Material validation -----
|
||||
void HandleValidateMaterial(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Material snapshot/diff/restore (Phase 5) -----
|
||||
void HandleSnapshotMaterialGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleDiffMaterialGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleRestoreMaterialGraph(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
// ----- Animation Blueprint handlers (state machine, still old-style) -----
|
||||
void HandleAddAnimState(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleRemoveAnimState(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleAddAnimTransition(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSetTransitionRule(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSetStateAnimation(const FJsonObject* Json, FJsonObject* Result);
|
||||
void HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result);
|
||||
|
||||
public:
|
||||
// ----- Snapshot storage -----
|
||||
TMap<FString, FGraphSnapshot> Snapshots;
|
||||
TMap<FString, FGraphSnapshot> MaterialSnapshots;
|
||||
static const int32 MaxSnapshots = 50;
|
||||
|
||||
// Snapshot helpers
|
||||
FString GenerateSnapshotId(const FString& BlueprintName);
|
||||
FGraphSnapshotData CaptureGraphSnapshot(UEdGraph* Graph);
|
||||
void PruneOldSnapshots();
|
||||
bool SaveSnapshotToDisk(const FString& SnapshotId, const FGraphSnapshot& Snapshot);
|
||||
bool LoadSnapshotFromDisk(const FString& SnapshotId, FGraphSnapshot& OutSnapshot);
|
||||
};
|
||||
|
||||
// Transitional alias — old-style handlers use this to access the server instance.
|
||||
using FBlueprintMCPServer = FMCPServer;
|
||||
using MCPHelper = FMCPServer;
|
||||
|
||||
|
||||
@@ -15,39 +15,6 @@ class UAnimationStateMachineGraph;
|
||||
class UAnimStateNode;
|
||||
class UAnimStateTransitionNode;
|
||||
|
||||
// ----- Snapshot data structures -----
|
||||
|
||||
struct FPinConnectionRecord
|
||||
{
|
||||
FString SourceNodeGuid;
|
||||
FString SourcePinName;
|
||||
FString TargetNodeGuid;
|
||||
FString TargetPinName;
|
||||
};
|
||||
|
||||
struct FNodeRecord
|
||||
{
|
||||
FString NodeGuid;
|
||||
FString NodeClass;
|
||||
FString NodeTitle;
|
||||
FString StructType; // for Break/Make nodes
|
||||
};
|
||||
|
||||
struct FGraphSnapshotData
|
||||
{
|
||||
TArray<FNodeRecord> Nodes;
|
||||
TArray<FPinConnectionRecord> Connections;
|
||||
};
|
||||
|
||||
struct FGraphSnapshot
|
||||
{
|
||||
FString SnapshotId;
|
||||
FString BlueprintName;
|
||||
FString BlueprintPath;
|
||||
FDateTime CreatedAt;
|
||||
TMap<FString, FGraphSnapshotData> Graphs; // graphName -> data
|
||||
};
|
||||
|
||||
// ----- Log capture -----
|
||||
|
||||
class FLogCaptureOutputDevice : public FOutputDevice
|
||||
|
||||
Reference in New Issue
Block a user