Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Params.h
2026-03-08 03:44:27 -04:00

489 lines
16 KiB
C++

#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/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
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// 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;
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
{
EntryNode = FuncEntry;
FoundNodeType = TEXT("FunctionEntry");
break;
}
}
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
if (!EntryNode)
{
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
{
EntryNode = CustomEvent;
FoundNodeType = TEXT("CustomEvent");
break;
}
}
}
if (!EntryNode)
{
// List available functions/events for debugging
TArray<TSharedPtr<FJsonValue>> Available;
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
}
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
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("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->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
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// Find the entry node
UK2Node_EditablePinBase* EntryNode = nullptr;
FString FoundNodeType;
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
{
EntryNode = FuncEntry;
FoundNodeType = TEXT("FunctionEntry");
break;
}
}
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
if (!EntryNode)
{
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
{
EntryNode = CustomEvent;
FoundNodeType = TEXT("CustomEvent");
break;
}
}
}
if (!EntryNode)
{
// List available functions/events for debugging
TArray<TSharedPtr<FJsonValue>> Available;
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
}
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
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->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
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// 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
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
UEdGraph* FEGraph = FE->GetGraph();
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
// Skip delegate signature graphs (handled in Strategy 3)
if (BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
EntryNode = FE;
NodeType = TEXT("FunctionEntry");
break;
}
// Strategy 2: K2Node_CustomEvent with matching CustomFunctionName
if (!EntryNode)
{
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
{
EntryNode = CE;
NodeType = TEXT("CustomEvent");
break;
}
}
}
// Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs
if (!EntryNode)
{
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
UEdGraph* FEGraph = FE->GetGraph();
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
if (!BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
EntryNode = FE;
NodeType = TEXT("EventDispatcher");
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 (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
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->SetStringField(TEXT("nodeType"), NodeType);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};