Broad rearrangement of handlers

This commit is contained in:
2026-03-12 00:44:17 -04:00
parent 2e058035f3
commit d69fc4cd1e
98 changed files with 270 additions and 1032 deletions

View File

@@ -40,11 +40,6 @@ is actually fine — but it's a one-way door.
lookup failure instead of silently inserting a blank sample.
Probably better behavior, but different.
## Static Function Violation
- **CompileBlueprint** — has a `static` `CompileAndReport`
method, violating the no-static-functions rule.
## Minor Concerns
- **SetNodePositions** — node search across all graphs could

View File

@@ -1,169 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialExpression.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "UObject/UObjectIterator.h"
#include "UMCPHandler_AddMaterialExpression.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_AddMaterialExpression : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction, not both)"))
FString Material;
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material, not both)"))
FString MaterialFunction;
UPROPERTY(meta=(Description="Expression class name without 'MaterialExpression' prefix (e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp')"))
FString ExpressionClass;
UPROPERTY(meta=(Optional, Description="X position in the material graph editor"))
int32 PosX = 0;
UPROPERTY(meta=(Optional, Description="Y position in the material graph editor"))
int32 PosY = 0;
virtual FString GetDescription() const override
{
return TEXT("Add a new expression node to a material or material function graph.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction'\n"));
return;
}
if (!Material.IsEmpty() && !MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction', not both\n"));
return;
}
// Resolve the expression class
UClass* ExprClass = ResolveExpressionClass(Result);
if (!ExprClass) return;
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
UObject* Owner = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> Assets;
if (!Assets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = Assets.Object();
Owner = MatFunc;
}
else
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = Assets.Object();
Owner = MaterialObj;
}
// Ensure the MaterialGraph exists (commandlet mode doesn't auto-create it)
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
// Create the expression
UMaterialExpression* NewExpr = NewObject<UMaterialExpression>(Owner, ExprClass);
if (!NewExpr)
{
Result.Append(TEXT("ERROR: Failed to create material expression object\n"));
return;
}
NewExpr->MaterialExpressionEditorX = PosX;
NewExpr->MaterialExpressionEditorY = PosY;
if (MaterialObj)
{
MaterialObj->GetExpressionCollection().AddExpression(NewExpr);
if (MaterialObj->MaterialGraph)
MaterialObj->MaterialGraph->RebuildGraph();
MaterialObj->PreEditChange(nullptr);
MaterialObj->PostEditChange();
MaterialObj->MarkPackageDirty();
}
else
{
MatFunc->GetExpressionCollection().AddExpression(NewExpr);
MatFunc->PreEditChange(nullptr);
MatFunc->PostEditChange();
MatFunc->MarkPackageDirty();
}
// Save
bool bSaved = MaterialObj
? MCPUtils::SaveMaterialPackage(MaterialObj)
: MCPUtils::SaveGenericPackage(MatFunc);
// Output
Result.Appendf(TEXT("Added %s\n"), *MCPUtils::FormatName(NewExpr));
// Find the graph node GUID (only for materials with a material graph)
if (MaterialObj && MaterialObj->MaterialGraph)
{
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
{
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node);
if (MatNode && MatNode->MaterialExpression == NewExpr)
{
Result.Appendf(TEXT("NodeId: %s\n"), *Node->NodeGuid.ToString());
break;
}
}
}
if (!bSaved)
Result.Append(TEXT("WARNING: Failed to save package\n"));
}
private:
UClass* ResolveExpressionClass(FStringBuilderBase& Result)
{
// Convenience aliases
static TMap<FString, FString> Aliases = {
{TEXT("Lerp"), TEXT("LinearInterpolate")},
};
FString LookupName = ExpressionClass;
if (const FString* Alias = Aliases.Find(ExpressionClass))
LookupName = *Alias;
FString FullClassName = FString::Printf(TEXT("MaterialExpression%s"), *LookupName);
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == FullClassName && It->IsChildOf(UMaterialExpression::StaticClass()))
{
if (It->HasAnyClassFlags(CLASS_Abstract))
{
Result.Appendf(TEXT("ERROR: Expression class '%s' is abstract\n"), *ExpressionClass);
return nullptr;
}
return *It;
}
}
Result.Appendf(TEXT("ERROR: Unknown expression class '%s'. Use the UMaterialExpression subclass name without the 'MaterialExpression' prefix (e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp')\n"),
*ExpressionClass);
return nullptr;
}
};

View File

@@ -1,188 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialExpression.h"
#include "MaterialGraph/MaterialGraph.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "UMCPHandler_ConnectMaterialExpressionPins.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FConnectMaterialPinsEntry
{
GENERATED_BODY()
UPROPERTY()
FString SourceNode;
UPROPERTY()
FString SourcePin;
UPROPERTY()
FString TargetNode;
UPROPERTY()
FString TargetPin;
};
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ConnectMaterialExpressionPins : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
FString Material;
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
FString MaterialFunction;
UPROPERTY(meta=(Description="Array of {sourceNode, sourcePin, targetNode, targetPin} objects"))
FMCPJsonArray Connections;
virtual FString GetDescription() const override
{
return TEXT("Connect pins between nodes in a material or material function graph. Supports batching.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction'.\n"));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> Assets;
if (!Assets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = Assets.Object();
}
else
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = Assets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
Result.Appendf(TEXT("ERROR: %s has no material graph.\n"),
MaterialObj ? *MCPUtils::FormatName(MaterialObj) : *MCPUtils::FormatName(MatFunc));
return;
}
int32 SuccessCount = 0;
const UEdGraphSchema* Schema = Graph->GetSchema();
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
{
FConnectMaterialPinsEntry Entry;
if (!MCPUtils::PopulateFromJson(FConnectMaterialPinsEntry::StaticStruct(), &Entry, ConnVal, MCPErrorCallback(Result)))
continue;
// Find source node
UEdGraphNode* SrcNode = FindNodeByName(Graph, Entry.SourceNode);
if (!SrcNode)
{
Result.Appendf(TEXT("ERROR: Source node '%s' not found.\n"), *Entry.SourceNode);
continue;
}
// Find target node
UEdGraphNode* TgtNode = FindNodeByName(Graph, Entry.TargetNode);
if (!TgtNode)
{
Result.Appendf(TEXT("ERROR: Target node '%s' not found.\n"), *Entry.TargetNode);
continue;
}
// Find source pin
UEdGraphPin* SrcPin = FindPinByName(SrcNode, Entry.SourcePin);
if (!SrcPin)
{
Result.Appendf(TEXT("ERROR: Pin '%s' not found on %s. Available:"),
*Entry.SourcePin, *MCPUtils::FormatName(SrcNode));
for (UEdGraphPin* P : SrcNode->Pins)
if (P) Result.Appendf(TEXT(" %s"), *MCPUtils::FormatName(P));
Result.Append(TEXT("\n"));
continue;
}
// Find target pin
UEdGraphPin* TgtPin = FindPinByName(TgtNode, Entry.TargetPin);
if (!TgtPin)
{
Result.Appendf(TEXT("ERROR: Pin '%s' not found on %s. Available:"),
*Entry.TargetPin, *MCPUtils::FormatName(TgtNode));
for (UEdGraphPin* P : TgtNode->Pins)
if (P) Result.Appendf(TEXT(" %s"), *MCPUtils::FormatName(P));
Result.Append(TEXT("\n"));
continue;
}
// Check compatibility
const FPinConnectionResponse Response = Schema->CanCreateConnection(SrcPin, TgtPin);
if (Response.Response == CONNECT_RESPONSE_DISALLOW)
{
Result.Appendf(TEXT("ERROR: Cannot connect %s.%s -> %s.%s: %s\n"),
*MCPUtils::FormatName(SrcNode), *MCPUtils::FormatName(SrcPin),
*MCPUtils::FormatName(TgtNode), *MCPUtils::FormatName(TgtPin),
*Response.Message.ToString());
continue;
}
Schema->TryCreateConnection(SrcPin, TgtPin);
SuccessCount++;
}
if (SuccessCount > 0)
{
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
Asset->PostEditChange();
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result.Appendf(TEXT("Connected %d/%d. Saved: %s\n"),
SuccessCount, Connections.Array.Num(), bSaved ? TEXT("yes") : TEXT("no"));
}
else
{
Result.Appendf(TEXT("0/%d connections succeeded.\n"), Connections.Array.Num());
}
}
private:
UEdGraphNode* FindNodeByName(UEdGraph* Graph, const FString& Name)
{
for (UEdGraphNode* Node : Graph->Nodes)
if (Node && MCPUtils::Identifies(Name, Node))
return Node;
return nullptr;
}
UEdGraphPin* FindPinByName(UEdGraphNode* Node, const FString& Name)
{
for (UEdGraphPin* Pin : Node->Pins)
if (Pin && MCPUtils::Identifies(Name, Pin))
return Pin;
return nullptr;
}
};

View File

@@ -1,126 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "UMCPHandler_DeleteMaterialExpression.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DeleteMaterialExpression : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
FString Material;
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
FString MaterialFunction;
UPROPERTY(meta=(Description="Expression name (use FormatName from DumpMaterial output)"))
FString Node;
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
bool DryRun = false;
virtual FString GetDescription() const override
{
return TEXT("Remove an expression node from a material or material function graph.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
MCPErrorCallback(Result).SetError(TEXT("Specify 'material' or 'materialFunction'."));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
MCPErrorCallback(Result).SetError(TEXT("Asset has no material graph."));
return;
}
// Find node by name
UMaterialGraphNode* TargetMatNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(GraphNode);
if (!MatNode || !MatNode->MaterialExpression) continue;
if (MCPUtils::Identifies(Node, MatNode->MaterialExpression))
{
TargetMatNode = MatNode;
break;
}
}
if (!TargetMatNode)
{
MCPErrorCallback(Result).SetError(FString::Printf(TEXT("Expression '%s' not found."), *Node));
return;
}
FString ExprName = MCPUtils::FormatName(TargetMatNode->MaterialExpression);
if (DryRun)
{
Result.Appendf(TEXT("DryRun: would delete %s\n"), *ExprName);
return;
}
// Remove the expression
UMaterialExpression* ExprToRemove = TargetMatNode->MaterialExpression;
if (MaterialObj)
MaterialObj->GetExpressionCollection().RemoveExpression(ExprToRemove);
else
MatFunc->GetExpressionCollection().RemoveExpression(ExprToRemove);
ExprToRemove->MarkAsGarbage();
// Rebuild graph
Graph->NotifyGraphChanged();
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
Asset->PostEditChange();
Asset->MarkPackageDirty();
// Save
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result.Appendf(TEXT("Deleted %s"), *ExprName);
if (!bSaved) Result.Append(TEXT(" (save failed)"));
Result.Append(TEXT("\n"));
}
};

View File

@@ -1,188 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.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/MaterialExpressionMaterialFunctionCall.h"
#include "Materials/MaterialFunction.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "MaterialGraph/MaterialGraphNode_Root.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "UMCPHandler_DescribeMaterialInEnglish.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DescribeMaterialInEnglish : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Material name or package path"))
FString Material;
virtual FString GetDescription() const override
{
return TEXT("Generate a human-readable description of a material by tracing its expression graph from the root node inputs.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterial* MaterialObj = Assets.Object();
// Ensure material graph is built
MCPUtils::EnsureMaterialGraph(MaterialObj);
if (!MaterialObj->MaterialGraph)
{
MCPErrorCallback(Result).SetError(TEXT("Could not build MaterialGraph for this material"));
return;
}
// Find root node
UMaterialGraphNode_Root* RootNode = nullptr;
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
{
RootNode = Cast<UMaterialGraphNode_Root>(Node);
if (RootNode) break;
}
if (!RootNode)
{
MCPErrorCallback(Result).SetError(TEXT("Could not find root node in material graph"));
return;
}
// 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 (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;
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(SourceNode);
if (!MatNode)
{
NodeDesc = MCPUtils::FormatName(SourceNode);
Sources.Add(NodeDesc);
continue;
}
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 ? MCPUtils::FormatName(TP->Texture) : 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 ? MCPUtils::FormatName(TS->Texture) : TEXT("None");
NodeDesc = FString::Printf(TEXT("TextureSample(%s)"), *TexName);
}
else if (auto* MFC = Cast<UMaterialExpressionMaterialFunctionCall>(Expr))
{
FString FuncName = MFC->MaterialFunction ? MFC->MaterialFunction->GetPathName() : TEXT("None");
NodeDesc = FString::Printf(TEXT("FunctionCall(%s)"), *FuncName);
}
else
{
NodeDesc = MCPUtils::FormatName(Expr);
}
// Recurse into input pins
TArray<FString> InputDescs;
for (UEdGraphPin* InputPin : SourceNode->Pins)
{
if (!InputPin || InputPin->Direction != EGPD_Input || InputPin->LinkedTo.Num() == 0) continue;
InputDescs.Add(TracePin(InputPin, Depth + 1));
}
if (InputDescs.Num() > 0)
{
NodeDesc += TEXT(" <- (") + FString::Join(InputDescs, TEXT(", ")) + TEXT(")");
}
Sources.Add(NodeDesc);
}
if (Sources.Num() == 1)
return Sources[0];
return TEXT("(") + FString::Join(Sources, TEXT(", ")) + TEXT(")");
};
// Trace each connected root input and output plain text
Result.Appendf(TEXT("Material: %s\n\n"), *MCPUtils::FormatName(MaterialObj));
for (UEdGraphPin* Pin : RootNode->Pins)
{
if (!Pin || Pin->Direction != EGPD_Input) continue;
if (Pin->LinkedTo.Num() == 0) continue;
FString PinName = MCPUtils::FormatName(Pin);
FString Description = TracePin(Pin, 0);
Result.Appendf(TEXT("%s <- %s\n"), *PinName, *Description);
}
}
};

View File

@@ -1,98 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "MaterialGraph/MaterialGraph.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "UMCPHandler_DisconnectMaterialExpressionPin.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DisconnectMaterialExpressionPin : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
FString Material;
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
FString MaterialFunction;
UPROPERTY(meta=(Description="Node name (use FormatName-style identifier)"))
FString Node;
UPROPERTY(meta=(Description="Pin name to disconnect"))
FString PinName;
virtual FString GetDescription() const override
{
return TEXT("Break all connections on a specific pin in a material or material function graph.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction'.\n"));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> Assets;
if (!Assets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = Assets.Object();
}
else
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = Assets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
Result.Appendf(TEXT("ERROR: %s has no material graph.\n"),
MaterialObj ? *MCPUtils::FormatName(MaterialObj) : *MCPUtils::FormatName(MatFunc));
return;
}
// Find node and pin via MCPFetcher
MCPFetcher F(Result, Graph);
UEdGraphPin* Pin = F.Node(Node).Pin(PinName).Cast<UEdGraphPin>();
if (!Pin) return;
int32 BrokenCount = Pin->LinkedTo.Num();
Pin->BreakAllPinLinks();
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
Asset->PostEditChange();
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result.Appendf(TEXT("Disconnected %d link(s) from %s on %s.\n"),
BrokenCount, *MCPUtils::FormatName(Pin), *MCPUtils::FormatName(Pin->GetOwningNode()));
if (!bSaved)
Result.Append(TEXT("WARNING: Failed to save package.\n"));
}
};

View File

@@ -1,228 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "MaterialDomain.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
#include "Materials/MaterialExpressionTextureSample.h"
#include "Engine/Texture.h"
#include "UMCPHandler_DumpMaterial.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DumpMaterial : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Material or MaterialInstance name or package path"))
FString Material;
virtual FString GetDescription() const override
{
return TEXT("Get detailed info about a material or material instance, including parameters, usage flags, and referenced textures.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPAssets<UMaterialInterface> Assets;
Assets.NoScans();
Assets.Scan<UMaterial>();
Assets.Scan<UMaterialInstanceConstant>();
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterialInterface* LoadedObj = Assets.Object();
if (UMaterial* Mat = Cast<UMaterial>(LoadedObj))
{
EmitMaterial(Mat, Result);
return;
}
if (UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(LoadedObj))
{
EmitMaterialInstance(MI, Result);
return;
}
Result.Appendf(TEXT("ERROR: Loaded object is %s, expected Material or MaterialInstance.\n"),
*LoadedObj->GetClass()->GetName());
}
private:
void EmitMaterial(UMaterial* Mat, FStringBuilderBase& Result)
{
Result.Appendf(TEXT("Material: %s\n"), *MCPUtils::FormatName(Mat));
Result.Appendf(TEXT("Domain: %s\n"), *MCPUtils::EnumToString(Mat->MaterialDomain, TEXT("MD_")));
Result.Appendf(TEXT("BlendMode: %s\n"), *MCPUtils::EnumToString(Mat->BlendMode, TEXT("BLEND_")));
Result.Appendf(TEXT("TwoSided: %s\n"), Mat->IsTwoSided() ? TEXT("true") : TEXT("false"));
// Shading models
FMaterialShadingModelField SMField = Mat->GetShadingModels();
const UEnum* SMEnum = StaticEnum<EMaterialShadingModel>();
TArray<FString> SMNames;
for (int32 i = 0; i < SMEnum->NumEnums() - 1; ++i)
{
EMaterialShadingModel SM = (EMaterialShadingModel)SMEnum->GetValueByIndex(i);
if (SMField.HasShadingModel(SM))
SMNames.Add(SMEnum->GetNameStringByIndex(i));
}
Result.Appendf(TEXT("ShadingModels: %s\n"), *FString::Join(SMNames, TEXT(", ")));
// Parameters
auto Expressions = Mat->GetExpressions();
bool bHasParams = false;
for (UMaterialExpression* Expr : Expressions)
{
if (!Expr) continue;
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
{
if (!bHasParams) { Result.Append(TEXT("\nParameters:\n")); bHasParams = true; }
Result.Appendf(TEXT(" Scalar \"%s\" = %g"), *SP->ParameterName.ToString(), SP->DefaultValue);
if (!SP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *SP->Group.ToString());
Result.Append(TEXT("\n"));
}
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
{
if (!bHasParams) { Result.Append(TEXT("\nParameters:\n")); bHasParams = true; }
Result.Appendf(TEXT(" Vector \"%s\" = (%.3f, %.3f, %.3f, %.3f)"),
*VP->ParameterName.ToString(),
VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
if (!VP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *VP->Group.ToString());
Result.Append(TEXT("\n"));
}
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
{
if (!bHasParams) { Result.Append(TEXT("\nParameters:\n")); bHasParams = true; }
Result.Appendf(TEXT(" Texture \"%s\" = %s"),
*TP->ParameterName.ToString(),
TP->Texture ? *MCPUtils::FormatName(TP->Texture) : TEXT("None"));
if (!TP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *TP->Group.ToString());
Result.Append(TEXT("\n"));
}
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
{
if (!bHasParams) { Result.Append(TEXT("\nParameters:\n")); bHasParams = true; }
Result.Appendf(TEXT(" StaticSwitch \"%s\" = %s"),
*SSP->ParameterName.ToString(),
SSP->DefaultValue ? TEXT("true") : TEXT("false"));
if (!SSP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *SSP->Group.ToString());
Result.Append(TEXT("\n"));
}
}
// Referenced textures
auto RefTexObjs = Mat->GetReferencedTextures();
bool bHasTextures = false;
for (const TObjectPtr<UObject>& TexObj : RefTexObjs)
{
if (!TexObj) continue;
if (!bHasTextures) { Result.Append(TEXT("\nReferenced Textures:\n")); bHasTextures = true; }
if (UTexture* Tex = Cast<UTexture>(TexObj.Get()))
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Tex));
else
Result.Appendf(TEXT(" %s\n"), *TexObj->GetPathName());
}
// Usage flags — only print enabled ones
Result.Append(TEXT("\nUsage Flags:"));
bool bAnyUsage = false;
auto EmitFlag = [&](bool bSet, const TCHAR* Name) {
if (bSet) { Result.Appendf(TEXT(" %s"), Name); bAnyUsage = true; }
};
EmitFlag(Mat->bUsedWithSkeletalMesh, TEXT("SkeletalMesh"));
EmitFlag(Mat->bUsedWithMorphTargets, TEXT("MorphTargets"));
EmitFlag(Mat->bUsedWithNiagaraSprites, TEXT("NiagaraSprites"));
EmitFlag(Mat->bUsedWithParticleSprites, TEXT("ParticleSprites"));
EmitFlag(Mat->bUsedWithStaticLighting, TEXT("StaticLighting"));
if (!bAnyUsage) Result.Append(TEXT(" (none)"));
Result.Append(TEXT("\n"));
// Stats
Result.Appendf(TEXT("Expressions: %d\n"), Expressions.Num());
int32 TextureSampleCount = 0;
for (UMaterialExpression* Expr : Expressions)
if (Expr && Expr->IsA<UMaterialExpressionTextureSample>())
TextureSampleCount++;
Result.Appendf(TEXT("TextureSamples: %d\n"), TextureSampleCount);
if (Mat->MaterialGraph)
Result.Appendf(TEXT("GraphNodes: %d\n"), Mat->MaterialGraph->Nodes.Num());
// Additional settings — only print non-default values
if (Mat->OpacityMaskClipValue != 0.3333f)
Result.Appendf(TEXT("OpacityMaskClipValue: %g\n"), Mat->OpacityMaskClipValue);
if (Mat->DitheredLODTransition)
Result.Append(TEXT("DitheredLODTransition: true\n"));
if (Mat->bAllowNegativeEmissiveColor)
Result.Append(TEXT("AllowNegativeEmissiveColor: true\n"));
}
void EmitMaterialInstance(UMaterialInstanceConstant* MI, FStringBuilderBase& Result)
{
Result.Appendf(TEXT("MaterialInstance: %s\n"), *MCPUtils::FormatName(MI));
if (MI->Parent)
{
if (UMaterial* ParentMat = Cast<UMaterial>(MI->Parent))
Result.Appendf(TEXT("Parent: %s\n"), *MCPUtils::FormatName(ParentMat));
else if (UMaterialInstance* ParentMI = Cast<UMaterialInstance>(MI->Parent))
Result.Appendf(TEXT("Parent: %s\n"), *MCPUtils::FormatName(ParentMI));
else
Result.Appendf(TEXT("Parent: %s\n"), *MI->Parent->GetPathName());
}
// Overridden parameters
bool bHasParams = false;
auto EnsureHeader = [&]() {
if (!bHasParams) { Result.Append(TEXT("\nOverridden Parameters:\n")); bHasParams = true; }
};
for (const FScalarParameterValue& P : MI->ScalarParameterValues)
{
EnsureHeader();
Result.Appendf(TEXT(" Scalar \"%s\" = %g\n"), *P.ParameterInfo.Name.ToString(), P.ParameterValue);
}
for (const FVectorParameterValue& P : MI->VectorParameterValues)
{
EnsureHeader();
Result.Appendf(TEXT(" Vector \"%s\" = (%.3f, %.3f, %.3f, %.3f)\n"),
*P.ParameterInfo.Name.ToString(),
P.ParameterValue.R, P.ParameterValue.G, P.ParameterValue.B, P.ParameterValue.A);
}
for (const FTextureParameterValue& P : MI->TextureParameterValues)
{
EnsureHeader();
if (P.ParameterValue)
{
Result.Appendf(TEXT(" Texture \"%s\" = %s\n"),
*P.ParameterInfo.Name.ToString(), *MCPUtils::FormatName(P.ParameterValue));
}
else
{
Result.Appendf(TEXT(" Texture \"%s\" = None\n"), *P.ParameterInfo.Name.ToString());
}
}
for (const FStaticSwitchParameter& P : MI->GetStaticParameters().StaticSwitchParameters)
{
EnsureHeader();
Result.Appendf(TEXT(" StaticSwitch \"%s\" = %s%s\n"),
*P.ParameterInfo.Name.ToString(),
P.Value ? TEXT("true") : TEXT("false"),
P.bOverride ? TEXT("") : TEXT(" (not overridden)"));
}
}
};

View File

@@ -1,147 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionFunctionInput.h"
#include "Materials/MaterialExpressionFunctionOutput.h"
#include "Materials/MaterialExpressionMaterialFunctionCall.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
#include "Materials/MaterialExpressionConstant.h"
#include "Materials/MaterialExpressionConstant3Vector.h"
#include "Materials/MaterialExpressionConstant4Vector.h"
#include "Materials/MaterialExpressionTextureObjectParameter.h"
#include "Materials/MaterialExpressionTextureSample.h"
#include "Materials/MaterialExpressionTextureCoordinate.h"
#include "Materials/MaterialExpressionComponentMask.h"
#include "Materials/MaterialExpressionCustom.h"
#include "Engine/Texture.h"
#include "UMCPHandler_DumpMaterialFunction.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DumpMaterialFunction : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="MaterialFunction name or package path"))
FString MaterialFunction;
virtual FString GetDescription() const override
{
return TEXT("Get detailed info about a material function, including its inputs, outputs, and expressions.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPAssets<UMaterialFunction> Assets;
if (!Assets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
UMaterialFunction* MF = Assets.Object();
Result.Appendf(TEXT("MaterialFunction: %s\n"), *MCPUtils::FormatName(MF));
FString Desc = MF->GetDescription();
if (!Desc.IsEmpty())
Result.Appendf(TEXT("Description: %s\n"), *Desc);
auto Expressions = MF->GetExpressions();
Result.Appendf(TEXT("Expressions: %d\n"), Expressions.Num());
// Inputs and outputs
bool bHasInputs = false;
bool bHasOutputs = false;
for (UMaterialExpression* Expr : Expressions)
{
if (!Expr) continue;
if (auto* FI = Cast<UMaterialExpressionFunctionInput>(Expr))
{
if (!bHasInputs) { Result.Append(TEXT("\nInputs:\n")); bHasInputs = true; }
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Expr));
}
else if (auto* FO = Cast<UMaterialExpressionFunctionOutput>(Expr))
{
if (!bHasOutputs) { Result.Append(TEXT("\nOutputs:\n")); bHasOutputs = true; }
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Expr));
}
}
// All expressions
Result.Append(TEXT("\nExpression List:\n"));
for (UMaterialExpression* Expr : Expressions)
{
if (!Expr) continue;
Result.Appendf(TEXT(" %s"), *MCPUtils::FormatName(Expr));
EmitExpressionDetails(Expr, Result);
Result.Append(TEXT("\n"));
}
}
private:
void EmitExpressionDetails(UMaterialExpression* Expr, FStringBuilderBase& Result)
{
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
{
Result.Appendf(TEXT(" default=%g"), SP->DefaultValue);
if (!SP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *SP->Group.ToString());
}
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
{
Result.Appendf(TEXT(" default=(%.3f, %.3f, %.3f, %.3f)"),
VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
if (!VP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *VP->Group.ToString());
}
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
{
Result.Appendf(TEXT(" texture=%s"),
TP->Texture ? *MCPUtils::FormatName(TP->Texture) : TEXT("None"));
if (!TP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *TP->Group.ToString());
}
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
{
Result.Appendf(TEXT(" default=%s"), SSP->DefaultValue ? TEXT("true") : TEXT("false"));
if (!SSP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *SSP->Group.ToString());
}
else if (auto* SC = Cast<UMaterialExpressionConstant>(Expr))
{
Result.Appendf(TEXT(" value=%g"), SC->R);
}
else if (auto* C3 = Cast<UMaterialExpressionConstant3Vector>(Expr))
{
Result.Appendf(TEXT(" value=(%.3f, %.3f, %.3f)"), C3->Constant.R, C3->Constant.G, C3->Constant.B);
}
else if (auto* C4 = Cast<UMaterialExpressionConstant4Vector>(Expr))
{
Result.Appendf(TEXT(" value=(%.3f, %.3f, %.3f, %.3f)"),
C4->Constant.R, C4->Constant.G, C4->Constant.B, C4->Constant.A);
}
else if (auto* FC = Cast<UMaterialExpressionMaterialFunctionCall>(Expr))
{
if (FC->MaterialFunction)
Result.Appendf(TEXT(" calls=%s"), *FC->MaterialFunction->GetPathName());
}
else if (auto* TS = Cast<UMaterialExpressionTextureSample>(Expr))
{
if (TS->Texture)
Result.Appendf(TEXT(" texture=%s"), *MCPUtils::FormatName(TS->Texture));
}
else if (auto* TC = Cast<UMaterialExpressionTextureCoordinate>(Expr))
{
Result.Appendf(TEXT(" index=%d tiling=(%.1f, %.1f)"), TC->CoordinateIndex, TC->UTiling, TC->VTiling);
}
else if (auto* Custom = Cast<UMaterialExpressionCustom>(Expr))
{
Result.Appendf(TEXT(" code_len=%d"), Custom->Code.Len());
}
}
};

View File

@@ -1,106 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
#include "UMCPHandler_SearchWithinMaterials.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SearchWithinMaterials : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Search query string to match against material names, expression classes, and parameter names"))
FString Query;
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)"))
int32 MaxResults = 50;
virtual FString GetDescription() const override
{
return TEXT("Search across all materials for matching material names, expression types, and parameter names.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
FString DecodedQuery = MCPUtils::UrlDecode(Query);
MaxResults = FMath::Clamp(MaxResults, 1, 200);
int32 Count = 0;
MCPAssets<UMaterial> AllMaterials;
AllMaterials.Load();
for (UMaterial* MaterialObj : AllMaterials.Objects())
{
if (Count >= MaxResults) break;
if (!MaterialObj) continue;
FString MatName = MCPUtils::FormatName(MaterialObj);
// Check material name
bool bNameMatch = MatName.Contains(DecodedQuery, ESearchCase::IgnoreCase);
if (bNameMatch)
{
Result.Appendf(TEXT("material %s\n"), *MatName);
Count++;
}
// Search expressions
for (UMaterialExpression* Expr : MaterialObj->GetExpressions())
{
if (!Expr || Count >= MaxResults) continue;
FString ExprName = MCPUtils::FormatName(Expr);
FString ExprClass = Expr->GetClass()->GetName();
FString ExprDesc = Expr->GetDescription();
// 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) continue;
Result.Appendf(TEXT("expression %s in %s (%s)"), *ExprName, *MatName, *ExprClass);
if (!ParamName.IsEmpty())
Result.Appendf(TEXT(" param=%s"), *ParamName);
Result.Append(TEXT("\n"));
Count++;
}
}
if (Count == 0)
{
Result.Append(TEXT("No matches found.\n"));
}
else if (Count >= MaxResults)
{
Result.Appendf(TEXT("WARNING: Reached limit of %d results. Specify MaxResults to raise it.\n"), MaxResults);
}
}
};

View File

@@ -1,132 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "UMCPHandler_SetMaterialExpressionPosition.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetMaterialExpressionPosition : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
FString Material;
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
FString MaterialFunction;
UPROPERTY(meta=(Description="Expression name (use FormatName from DumpMaterial output)"))
FString Node;
UPROPERTY(meta=(Description="New X position"))
int32 PosX = 0;
UPROPERTY(meta=(Description="New Y position"))
int32 PosY = 0;
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
bool DryRun = false;
virtual FString GetDescription() const override
{
return TEXT("Reposition a material expression node in the material graph editor.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
MCPErrorCallback(Result).SetError(TEXT("Specify 'material' or 'materialFunction'."));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
MCPErrorCallback(Result).SetError(TEXT("Asset has no material graph."));
return;
}
// Find node by name
UMaterialGraphNode* TargetMatNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(GraphNode);
if (!MatNode || !MatNode->MaterialExpression) continue;
if (MCPUtils::Identifies(Node, MatNode->MaterialExpression))
{
TargetMatNode = MatNode;
break;
}
}
if (!TargetMatNode)
{
MCPErrorCallback(Result).SetError(FString::Printf(TEXT("Expression '%s' not found."), *Node));
return;
}
if (DryRun)
{
Result.Appendf(TEXT("DryRun: would move %s to (%d, %d)\n"),
*MCPUtils::FormatName(TargetMatNode->MaterialExpression), PosX, PosY);
return;
}
// Set position on the graph node
TargetMatNode->NodePosX = PosX;
TargetMatNode->NodePosY = PosY;
// Also update the underlying expression position
if (TargetMatNode->MaterialExpression)
{
TargetMatNode->MaterialExpression->MaterialExpressionEditorX = PosX;
TargetMatNode->MaterialExpression->MaterialExpressionEditorY = PosY;
}
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
Asset->PostEditChange();
// Save
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result.Appendf(TEXT("Moved %s to (%d, %d)"),
*MCPUtils::FormatName(TargetMatNode->MaterialExpression), PosX, PosY);
if (!bSaved) Result.Append(TEXT(" (save failed)"));
Result.Append(TEXT("\n"));
}
};

View File

@@ -1,282 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.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 "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "UMCPHandler_SetMaterialExpressionProperty.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetMaterialExpressionProperty : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
FString Material;
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
FString MaterialFunction;
UPROPERTY(meta=(Description="Expression node name (from FormatName)"))
FString Node;
virtual FString GetDescription() const override
{
return TEXT("Set the value or properties on a material expression node. "
"The 'value' field in the JSON payload provides the new value, whose format depends on the expression type.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction'\n"));
return;
}
if (!Json->HasField(TEXT("value")))
{
Result.Append(TEXT("ERROR: Missing required field: value\n"));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
Result.Appendf(TEXT("ERROR: No material graph found\n"));
return;
}
// Find the node by name using Identifies
UMaterialGraphNode* TargetMatNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
if (!MCPUtils::Identifies(Node, GraphNode)) continue;
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
if (TargetMatNode) break;
}
if (!TargetMatNode)
{
Result.Appendf(TEXT("ERROR: Node '%s' not found in material graph\n"), *Node);
return;
}
UMaterialExpression* Expr = TargetMatNode->MaterialExpression;
if (!Expr)
{
Result.Appendf(TEXT("ERROR: Node '%s' has no material expression\n"), *MCPUtils::FormatName(TargetMatNode));
return;
}
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
FString SetResult;
if (!ApplyValue(Expr, Json, Result, SetResult))
{
Asset->PostEditChange();
return;
}
Asset->PostEditChange();
Asset->MarkPackageDirty();
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result.Appendf(TEXT("%s = %s"), *MCPUtils::FormatName(Expr), *SetResult);
if (!bSaved) Result.Append(TEXT(" (save failed)"));
Result.Append(TEXT("\n"));
}
private:
// Apply the value from JSON to the expression. Returns false on error (with message in Result).
// On success, fills SetResult with a human-readable summary of the new value.
bool ApplyValue(UMaterialExpression* Expr, const FJsonObject* Json, FStringBuilderBase& Result, FString& SetResult)
{
if (auto* E = Cast<UMaterialExpressionConstant>(Expr))
{
double Value = Json->GetNumberField(TEXT("value"));
E->R = (float)Value;
SetResult = FString::Printf(TEXT("%g"), Value);
return true;
}
if (auto* E = Cast<UMaterialExpressionConstant3Vector>(Expr))
{
FLinearColor C;
if (!ParseColorValue(Json, C, false, Result)) return false;
E->Constant = C;
SetResult = FString::Printf(TEXT("(%.3f, %.3f, %.3f)"), C.R, C.G, C.B);
return true;
}
if (auto* E = Cast<UMaterialExpressionConstant4Vector>(Expr))
{
FLinearColor C;
if (!ParseColorValue(Json, C, true, Result)) return false;
E->Constant = C;
SetResult = FString::Printf(TEXT("(%.3f, %.3f, %.3f, %.3f)"), C.R, C.G, C.B, C.A);
return true;
}
if (auto* E = Cast<UMaterialExpressionScalarParameter>(Expr))
{
double Value = Json->GetNumberField(TEXT("value"));
E->DefaultValue = (float)Value;
SetResult = FString::Printf(TEXT("%g"), Value);
ApplyParameterName(E, Json);
return true;
}
if (auto* E = Cast<UMaterialExpressionVectorParameter>(Expr))
{
FLinearColor C;
if (!ParseColorValue(Json, C, true, Result)) return false;
E->DefaultValue = C;
SetResult = FString::Printf(TEXT("(%.3f, %.3f, %.3f, %.3f)"), C.R, C.G, C.B, C.A);
ApplyParameterName(E, Json);
return true;
}
if (auto* E = Cast<UMaterialExpressionTextureCoordinate>(Expr))
{
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
{
Result.Append(TEXT("ERROR: TextureCoordinate requires value as {coordinateIndex, uTiling, vTiling}\n"));
return false;
}
double CoordIndex = 0, UTiling = 1, VTiling = 1;
(*ValueObj)->TryGetNumberField(TEXT("coordinateIndex"), CoordIndex);
(*ValueObj)->TryGetNumberField(TEXT("uTiling"), UTiling);
(*ValueObj)->TryGetNumberField(TEXT("vTiling"), VTiling);
E->CoordinateIndex = (int32)CoordIndex;
E->UTiling = (float)UTiling;
E->VTiling = (float)VTiling;
SetResult = FString::Printf(TEXT("index=%d uTiling=%g vTiling=%g"), (int32)CoordIndex, UTiling, VTiling);
return true;
}
if (auto* E = Cast<UMaterialExpressionCustom>(Expr))
{
FString Code;
if (Json->TryGetStringField(TEXT("code"), Code))
{
E->Code = Code;
}
else if (Json->HasField(TEXT("value")))
{
FString ValueStr = Json->GetStringField(TEXT("value"));
if (!ValueStr.IsEmpty()) E->Code = ValueStr;
}
SetResult = FString::Printf(TEXT("code: %d chars"), E->Code.Len());
FString OutputTypeStr;
if (Json->TryGetStringField(TEXT("outputType"), OutputTypeStr) && !OutputTypeStr.IsEmpty())
{
ECustomMaterialOutputType OutType;
if (MCPUtils::StringToEnum(OutputTypeStr, OutType, MCPErrorCallback(Result)))
E->OutputType = OutType;
}
return true;
}
if (auto* E = Cast<UMaterialExpressionComponentMask>(Expr))
{
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
{
Result.Append(TEXT("ERROR: ComponentMask requires value as {r, g, b, a} (booleans)\n"));
return false;
}
bool bR = false, bG = false, bB = false, bA = false;
(*ValueObj)->TryGetBoolField(TEXT("r"), bR);
(*ValueObj)->TryGetBoolField(TEXT("g"), bG);
(*ValueObj)->TryGetBoolField(TEXT("b"), bB);
(*ValueObj)->TryGetBoolField(TEXT("a"), bA);
E->R = bR ? 1 : 0;
E->G = bG ? 1 : 0;
E->B = bB ? 1 : 0;
E->A = bA ? 1 : 0;
SetResult = FString::Printf(TEXT("R=%s G=%s B=%s A=%s"),
bR ? TEXT("true") : TEXT("false"),
bG ? TEXT("true") : TEXT("false"),
bB ? TEXT("true") : TEXT("false"),
bA ? TEXT("true") : TEXT("false"));
return true;
}
Result.Appendf(TEXT("ERROR: Expression type '%s' does not support value setting. Supported: "
"Constant, Constant3Vector, Constant4Vector, ScalarParameter, VectorParameter, "
"TextureCoordinate, Custom, ComponentMask\n"),
*Expr->GetClass()->GetName());
return false;
}
// Parse {r, g, b[, a]} from the "value" JSON field.
bool ParseColorValue(const FJsonObject* Json, FLinearColor& OutColor, bool bHasAlpha, FStringBuilderBase& Result)
{
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
{
Result.Appendf(TEXT("ERROR: requires value as {r, g, b%s}\n"), bHasAlpha ? TEXT(", a") : TEXT(""));
return false;
}
double R = 0, G = 0, B = 0, A = 1;
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
if (bHasAlpha) (*ValueObj)->TryGetNumberField(TEXT("a"), A);
OutColor = FLinearColor((float)R, (float)G, (float)B, (float)A);
return true;
}
// If the JSON has a "parameterName" field, apply it to a parameter expression.
void ApplyParameterName(UMaterialExpressionParameter* Param, const FJsonObject* Json)
{
FString ParamName;
if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty())
Param->ParameterName = FName(*ParamName);
}
};

View File

@@ -1,116 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraph.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimSequence.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimStateNode.h"
#include "AnimationStateMachineGraph.h"
#include "UMCPHandler_AddAnimStateToMachine.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_AddAnimStateToMachine : 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, FStringBuilderBase& Result) override
{
// Resolve the anim blueprint
MCPFetcher F(Result);
UAnimBlueprint* AnimBP = F.Walk(Blueprint).Cast<UAnimBlueprint>();
if (!AnimBP) return;
// Find the state machine graph
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph)
{
Result.Appendf(TEXT("ERROR: State machine graph '%s' not found in %s\n"), *Graph, *MCPUtils::FormatName(AnimBP));
return;
}
// Check for duplicate state name
if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr))
{
Result.Appendf(TEXT("ERROR: State '%s' already exists in %s\n"), *StateName, *MCPUtils::FormatName(SMGraph));
return;
}
// 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())
{
MCPAssets<UAnimSequence> AnimAssets;
if (!AnimAssets.Exact(AnimationAsset).Errors(Result).ENone().ETwo().Load()) return;
UAnimGraphNode_SequencePlayer* SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(NewState->GetBoundGraph());
SeqNode->CreateNewGuid();
SeqNode->PostPlacedNewNode();
SeqNode->AllocateDefaultPins();
SeqNode->SetAnimationAsset(AnimAssets.Object());
SeqNode->NodePosX = 0;
SeqNode->NodePosY = 0;
NewState->GetBoundGraph()->AddNode(SeqNode, false, false);
}
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
MCPUtils::SaveBlueprintPackage(AnimBP);
Result.Appendf(TEXT("Created state '%s' in %s\n"), *StateName, *MCPUtils::FormatName(SMGraph));
Result.Appendf(TEXT(" node: %s\n"), *MCPUtils::FormatName(NewState));
}
};

View File

@@ -1,107 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
#include "UMCPHandler_AddAnimStateTransition.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_AddAnimStateTransition : 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, FStringBuilderBase& Result) override
{
MCPAssets<UAnimBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UAnimBlueprint* AnimBP = Assets.Object();
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph)
{
Result.Appendf(TEXT("ERROR: State machine graph '%s' not found in '%s'\n"), *Graph, *MCPUtils::FormatName(AnimBP));
return;
}
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);
MCPUtils::SaveBlueprintPackage(AnimBP);
Result.Appendf(TEXT("Created transition %s -> %s: %s\n"),
*FromState, *ToState, *MCPUtils::FormatName(TransNode));
}
};

View File

@@ -1,141 +0,0 @@
#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 "UMCPHandler_AddBlueprintComponent.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
*MCPUtils::FormatName(BP)));
return;
}
// Check for duplicate component names
const TArray<USCS_Node*>& ExistingNodes = SCS->GetAllNodes();
for (USCS_Node* Existing : ExistingNodes)
{
if (Existing && Existing->ComponentTemplate &&
MCPUtils::Identifies(Component, Existing->ComponentTemplate))
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("A component named '%s' already exists in Blueprint '%s'"),
*Component, *MCPUtils::FormatName(BP)));
return;
}
}
// Resolve the component class by name
UClass* ComponentClassObj = MCPUtils::FindClassByName(ComponentClass);
if (!ComponentClassObj || !ComponentClassObj->IsChildOf(UActorComponent::StaticClass()))
{
MCPErrorCallback(Result).SetError(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));
return;
}
// If parent component specified, find its SCS node
USCS_Node* ParentSCSNode = nullptr;
if (!ParentComponent.IsEmpty())
{
for (USCS_Node* Node : ExistingNodes)
{
if (Node && Node->ComponentTemplate &&
MCPUtils::Identifies(ParentComponent, Node->ComponentTemplate))
{
ParentSCSNode = Node;
break;
}
}
if (!ParentSCSNode)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Parent component '%s' not found in Blueprint '%s'"),
*ParentComponent, *MCPUtils::FormatName(BP)));
return;
}
}
// Create the SCS node
USCS_Node* NewNode = SCS->CreateNode(ComponentClassObj, FName(*Component));
if (!NewNode)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Failed to create SCS node for component '%s' with class '%s'"),
*Component, *MCPUtils::FormatName(ComponentClassObj)));
return;
}
// Add to the hierarchy
if (ParentSCSNode)
{
ParentSCSNode->AddChildNode(NewNode);
}
else
{
SCS->AddNode(NewNode);
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Added component %s (%s)"),
*MCPUtils::FormatName(NewNode->ComponentTemplate),
*MCPUtils::FormatName(ComponentClassObj));
if (ParentSCSNode)
{
Result.Appendf(TEXT(" under %s"), *MCPUtils::FormatName(ParentSCSNode->ComponentTemplate));
}
Result.Appendf(TEXT("\nSaved: %s\n"), bSaved ? TEXT("true") : TEXT("false"));
}
};

View File

@@ -1,113 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UObject/UObjectIterator.h"
#include "UMCPHandler_AddBlueprintInterface.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_AddBlueprintInterface : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Interface name (e.g. 'BPI_MyInterface') or native UInterface class name"))
FString InterfaceName;
virtual FString GetDescription() const override
{
return TEXT("Add a Blueprint Interface implementation to a Blueprint. "
"Creates stub function graphs for each interface function.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// Resolve the interface class
UClass* InterfaceClass = FindInterfaceClass(InterfaceName, Result);
if (!InterfaceClass) return;
// Check for duplicates
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (IfaceDesc.Interface == InterfaceClass)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Interface '%s' is already implemented by this Blueprint."),
*MCPUtils::FormatName(InterfaceClass)));
return;
}
}
FTopLevelAssetPath InterfacePath = InterfaceClass->GetClassPathName();
bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath);
if (!bAdded)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("ImplementNewInterface failed for '%s'."),
*MCPUtils::FormatName(InterfaceClass)));
return;
}
// Collect stub function graph names from the newly added interface entry
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
Result.Appendf(TEXT("Added interface %s\n"), *MCPUtils::FormatName(InterfaceClass));
Result.Appendf(TEXT("Function stubs:\n"));
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (IfaceDesc.Interface != InterfaceClass) continue;
for (const UEdGraph* Graph : IfaceDesc.Graphs)
{
if (Graph)
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Graph));
}
break;
}
}
private:
// Resolve an interface name to a UClass. Tries loaded UInterface classes
// first (for native interfaces), then falls back to loading a Blueprint
// Interface asset.
static UClass* FindInterfaceClass(const FString& Name, FStringBuilderBase& Result)
{
// Strategy 1: Search loaded UInterface classes by name
for (TObjectIterator<UClass> It; It; ++It)
{
if (!It->IsChildOf(UInterface::StaticClass())) continue;
if (MCPUtils::Identifies(Name, *It))
return *It;
}
// Strategy 2: Try loading as a Blueprint Interface asset
MCPAssets<UBlueprint> IfaceAssets;
if (!IfaceAssets.Exact(Name).AllContent().Errors(Result).ETwo().Load()) return nullptr;
if (!IfaceAssets.Objects().IsEmpty())
{
UClass* GenClass = IfaceAssets.Object()->GeneratedClass;
if (GenClass && GenClass->IsChildOf(UInterface::StaticClass()))
return GenClass;
}
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Interface '%s' not found. Provide a Blueprint Interface asset name (e.g. 'BPI_MyInterface') or a native UInterface class name."),
*Name));
return nullptr;
}
};

View File

@@ -1,94 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_AddBlueprintVariable.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_AddBlueprintVariable : 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, FStringBuilderBase& Result) override
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// Check for duplicate variable name
FName VarFName(*VariableName);
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName == VarFName)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Variable '%s' already exists in %s"), *VariableName, *MCPUtils::FormatName(BP)));
return;
}
}
// Resolve the type
FEdGraphPinType PinType;
if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, Result))
return;
if (IsArray)
PinType.ContainerType = EPinContainerType::Array;
// Add the variable
if (!FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue))
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Failed to add variable '%s' to %s"), *VariableName, *MCPUtils::FormatName(BP)));
return;
}
if (!Category.IsEmpty())
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Added %s %s to %s\n"),
*VariableType, *VariableName, *MCPUtils::FormatName(BP));
if (IsArray)
Result.Append(TEXT("Container: Array\n"));
if (!Category.IsEmpty())
Result.Appendf(TEXT("Category: %s\n"), *Category);
if (!bSaved)
Result.Append(TEXT("Warning: package save failed\n"));
}
};

View File

@@ -1,147 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.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 "UMCPHandler_AddEventDispatcher.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FDispatcherParamEntry
{
GENERATED_BODY()
UPROPERTY()
FString Name;
UPROPERTY()
FString Type;
};
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_AddEventDispatcher : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Foo/MyBlueprint"))
FString Path;
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, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Path).Cast<UBlueprint>();
if (!BP) return;
FName DispatcherFName(*DispatcherName);
// Check for name uniqueness against existing variables
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName == DispatcherFName)
{
Result.Appendf(TEXT("Error: A variable or dispatcher named '%s' already exists.\n"), *DispatcherName);
return;
}
}
// Check against existing graphs (functions, macros, etc.)
if (!MCPUtils::AllGraphsNamed(BP, DispatcherName).IsEmpty())
{
Result.Appendf(TEXT("Error: A graph named '%s' already exists.\n"), *DispatcherName);
return;
}
// Add a member variable with PC_MCDelegate pin type
FEdGraphPinType DelegateType;
DelegateType.PinCategory = UEdGraphSchema_K2::PC_MCDelegate;
if (!FBlueprintEditorUtils::AddMemberVariable(BP, DispatcherFName, DelegateType))
{
Result.Appendf(TEXT("Error: Failed to add delegate variable for '%s'.\n"), *DispatcherName);
return;
}
// Create the signature graph
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
UEdGraph* SigGraph = FBlueprintEditorUtils::CreateNewGraph(BP, DispatcherFName,
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!SigGraph)
{
Result.Append(TEXT("Error: Failed to create delegate signature graph.\n"));
return;
}
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);
// Add parameters if provided
int32 ParamCount = 0;
if (Parameters.Array.Num() > 0)
{
UK2Node_EditablePinBase* EntryNode = nullptr;
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(SigGraph))
{
EntryNode = FE;
break;
}
if (!EntryNode)
{
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
MCPUtils::SaveBlueprintPackage(BP);
Result.Append(TEXT("Error: Event dispatcher created but entry node not found — parameters could not be added.\n"));
return;
}
for (const TSharedPtr<FJsonValue>& ParamVal : Parameters.Array)
{
FDispatcherParamEntry Entry;
if (!MCPUtils::PopulateFromJson(FDispatcherParamEntry::StaticStruct(), &Entry, ParamVal, Result)) return;
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
FEdGraphPinType PinType;
if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, Result))
return;
EntryNode->CreateUserDefinedPin(FName(*Entry.Name), PinType, EGPD_Output);
ParamCount++;
}
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Created event dispatcher '%s'"), *DispatcherName);
if (ParamCount > 0)
Result.Appendf(TEXT(" with %d parameter(s)"), ParamCount);
Result.Append(TEXT(".\n"));
}
};

View File

@@ -1,145 +0,0 @@
#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 "UMCPHandler_AddFunctionParameter.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& 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;
// Strategy 1: K2Node_FunctionEntry in function graphs
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
UEdGraph* FEGraph = FE->GetGraph();
if (!MCPUtils::Identifies(FunctionName, FEGraph)) 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 (!MCPUtils::Identifies(FunctionName, FEGraph)) 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
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Function/event/dispatcher '%s' not found. Available:\n"), *FunctionName));
for (UEdGraph* Graph : BP->FunctionGraphs)
{
if (Graph) Result.Appendf(TEXT(" function: %s\n"), *MCPUtils::FormatName(Graph));
}
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
Result.Appendf(TEXT(" custom event: %s\n"), *CE->CustomFunctionName.ToString());
}
TSet<FName> DelegateNames;
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNames);
for (const FName& DN : DelegateNames)
{
Result.Appendf(TEXT(" dispatcher: %s\n"), *DN.ToString());
}
return;
}
// Check for duplicate parameter name
for (const TSharedPtr<FUserPinInfo>& Existing : EntryNode->UserDefinedPins)
{
if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName));
return;
}
}
// 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);
Result.Appendf(TEXT("Added %s parameter '%s' to %s '%s'%s\n"),
*ParamType, *ParamName, *NodeType, *FunctionName,
bSaved ? TEXT("") : TEXT(" (WARNING: save failed)"));
}
};

View File

@@ -1,72 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "StructUtils/UserDefinedStruct.h"
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
#include "UMCPHandler_AddStructField.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_AddStructField : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Package path of the struct asset"))
FString Struct;
UPROPERTY(meta=(Description="Name for the new field"))
FString Name;
UPROPERTY(meta=(Description="Type for the new field (e.g. 'int32', 'FString', 'FVector')"))
FString Type;
virtual FString GetDescription() const override
{
return TEXT("Add a new field to a UserDefinedStruct asset.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Find the struct
MCPFetcher F(Result);
UUserDefinedStruct* S = F.Walk(Struct).Cast<UUserDefinedStruct>();
if (!S) return;
// Resolve type
FEdGraphPinType PinType;
if (!MCPUtils::ResolveTypeFromString(Type, PinType, Result))
return;
// Snapshot existing GUIDs so we can find the newly added one
TSet<FGuid> ExistingGuids;
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(S))
ExistingGuids.Add(Var.VarGuid);
if (!FStructureEditorUtils::AddVariable(S, PinType))
{
Result.Append(TEXT("ERROR: Failed to add field to struct.\n"));
return;
}
// Find the new variable by diffing GUID sets and rename it
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(S))
{
if (!ExistingGuids.Contains(Var.VarGuid))
{
FStructureEditorUtils::RenameVariable(S, Var.VarGuid, Name);
break;
}
}
MCPUtils::SaveGenericPackage(S);
Result.Appendf(TEXT("Added field: %s %s\n"), *Type, *Name);
}
};

View File

@@ -1,51 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPUtils.h"
#include "Misc/Paths.h"
#include "Misc/PackageName.h"
#include "HAL/FileManager.h"
#include "UMCPHandler_BackupAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_BackupAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)"))
FString AssetPath;
virtual FString GetDescription() const override
{
return TEXT("Copy an asset's .uasset file to a .uasset.bak backup.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
FString Filename = FPaths::ConvertRelativePathToFull(
FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension()));
if (!IFileManager::Get().FileExists(*Filename))
{
Result.Appendf(TEXT("ERROR: Asset file not found: %s\n"), *Filename);
return;
}
FString BackupFilename = Filename + TEXT(".bak");
uint32 CopyResult = IFileManager::Get().Copy(*BackupFilename, *Filename, true);
if (CopyResult != COPY_OK)
{
Result.Appendf(TEXT("ERROR: Failed to copy %s to %s\n"), *Filename, *BackupFilename);
return;
}
Result.Appendf(TEXT("Backed up to %s\n"), *BackupFilename);
}
};

View File

@@ -1,149 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_ChangeBlueprintVariableType.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ChangeBlueprintVariableType : 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, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Find the variable
FBPVariableDescription* Found = nullptr;
for (FBPVariableDescription& Var : BP->NewVariables)
{
if (MCPUtils::FormatName(Var) == Variable ||
Var.VarName.ToString().Equals(Variable, ESearchCase::IgnoreCase))
{
Found = &Var;
break;
}
}
if (!Found)
{
Result.Appendf(TEXT("ERROR: Variable '%s' not found in %s.\nAvailable variables:\n"),
*Variable, *MCPUtils::FormatName(BP));
for (const FBPVariableDescription& Var : BP->NewVariables)
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Var));
return;
}
// 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;
// List affected nodes (get/set nodes for this variable)
FName VarFName = Found->VarName;
auto AppendAffectedNodes = [&](const auto& NodeArray, const TCHAR* NodeType)
{
for (auto* VarNode : NodeArray)
{
if (VarNode->GetVarName() != VarFName) continue;
Result.Appendf(TEXT(" %s %s in %s\n"), NodeType,
*MCPUtils::FormatName(static_cast<UEdGraphNode*>(VarNode)),
*MCPUtils::FormatName(VarNode->GetGraph()));
for (UEdGraphPin* Pin : VarNode->Pins)
{
if (!Pin || Pin->LinkedTo.Num() == 0) continue;
if (NodeType[0] == 'G' && Pin->Direction != EGPD_Output) continue; // Get nodes: only output pins
Result.Appendf(TEXT(" %s connected to %d pin(s)\n"),
*MCPUtils::FormatName(Pin), Pin->LinkedTo.Num());
}
}
};
auto GetNodes = MCPUtils::AllNodes<UK2Node_VariableGet>(BP);
auto SetNodes = MCPUtils::AllNodes<UK2Node_VariableSet>(BP);
bool bHasAffected = false;
for (auto* VG : GetNodes) if (VG->GetVarName() == VarFName) { bHasAffected = true; break; }
if (!bHasAffected)
for (auto* VS : SetNodes) if (VS->GetVarName() == VarFName) { bHasAffected = true; break; }
if (DryRun)
{
Result.Appendf(TEXT("Dry run: would change %s from %s to %s\n"),
*MCPUtils::FormatName(*Found),
*MCPUtils::FormatPinType(Found->VarType),
*MCPUtils::FormatPinType(NewPinType));
if (bHasAffected)
{
Result.Append(TEXT("Affected nodes:\n"));
AppendAffectedNodes(GetNodes, TEXT("Get"));
AppendAffectedNodes(SetNodes, TEXT("Set"));
}
return;
}
// Apply the type change
BP->PreEditChange(nullptr);
Found->VarType = NewPinType;
BP->PostEditChange();
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Changed %s to %s.%s\n"),
*MCPUtils::FormatName(*Found),
*MCPUtils::FormatPinType(NewPinType),
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
if (bHasAffected)
{
Result.Append(TEXT("Affected nodes:\n"));
AppendAffectedNodes(GetNodes, TEXT("Get"));
AppendAffectedNodes(SetNodes, TEXT("Set"));
}
}
};

View File

@@ -1,159 +0,0 @@
#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 "UMCPHandler_ChangeFunctionParameterType.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ChangeFunctionParameterType : 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, FStringBuilderBase& 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;
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
if (MCPUtils::Identifies(FunctionName, FuncEntry->GetGraph()))
{
EntryNode = FuncEntry;
break;
}
}
// Strategy 2: Search for a K2Node_CustomEvent with matching name
if (!EntryNode)
{
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
if (MCPUtils::Identifies(FunctionName, static_cast<UEdGraphNode*>(CustomEvent)))
{
EntryNode = CustomEvent;
break;
}
}
}
if (!EntryNode)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Function or custom event '%s' not found. Available:"), *FunctionName));
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
Result.Appendf(TEXT(" function: %s\n"), *MCPUtils::FormatName(FE->GetGraph()));
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
Result.Appendf(TEXT(" event: %s\n"), *MCPUtils::FormatName(static_cast<UEdGraphNode*>(CE)));
return;
}
// Find the UserDefinedPin matching ParamName
TSharedPtr<FUserPinInfo>* FoundPinInfo = nullptr;
for (TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
{
if (PinInfo.IsValid() && PinInfo->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
{
FoundPinInfo = &PinInfo;
break;
}
}
if (!FoundPinInfo)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Parameter '%s' not found. Available:"), *ParamName));
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
if (PinInfo.IsValid())
Result.Appendf(TEXT(" %s\n"), *PinInfo->PinName.ToString());
return;
}
// Dry run: report connected pins that may disconnect
if (DryRun)
{
int32 AtRisk = 0;
for (UEdGraphPin* Pin : EntryNode->Pins)
{
if (!Pin || !MCPUtils::Identifies(ParamName, Pin)) continue;
for (UEdGraphPin* Linked : Pin->LinkedTo)
{
if (!Linked || !Linked->GetOwningNode()) continue;
Result.Appendf(TEXT("Connection at risk: %s -> %s on %s\n"),
*MCPUtils::FormatName(Pin),
*MCPUtils::FormatName(Linked),
*MCPUtils::FormatName(Linked->GetOwningNode()));
AtRisk++;
}
}
Result.Appendf(TEXT("Dry run: %d connection(s) at risk.\n"), AtRisk);
return;
}
// Apply the type change
EntryNode->PreEditChange(nullptr);
(*FoundPinInfo)->PinType = NewPinType;
EntryNode->PostEditChange();
// Reconstruct the node to update output pins with the new type
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
{
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
{
Schema->ReconstructNode(*EntryNode);
}
}
// Save
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Changed '%s' to %s. Save %s.\n"),
*ParamName, *MCPUtils::FormatPinType(NewPinType),
bSaved ? TEXT("succeeded") : TEXT("failed"));
}
};

View File

@@ -1,207 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_ChangeStructNodeType.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ChangeStructNodeType : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the node, e.g. /Game/Foo,graph:EventGraph,node:BreakVector"))
FString Node;
UPROPERTY(meta=(Description="New struct type name (e.g. 'Vector', 'FVector', 'Rotator')"))
FString NewType;
virtual FString GetDescription() const override
{
return TEXT("Change the struct type on a BreakStruct or MakeStruct node. "
"Attempts to reconnect matching pins after the type change.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
// Determine what kind of struct node this is
UK2Node_BreakStruct* BreakNode = Cast<UK2Node_BreakStruct>(FoundNode);
UK2Node_MakeStruct* MakeNode = Cast<UK2Node_MakeStruct>(FoundNode);
if (!BreakNode && !MakeNode)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Node %s is not a BreakStruct or MakeStruct (class: %s)"),
*MCPUtils::FormatName(FoundNode), *MCPUtils::FormatName(FoundNode->GetClass())));
return;
}
// Find the new struct type
UScriptStruct* NewStruct = FindStructByName(NewType);
if (!NewStruct)
{
MCPErrorCallback(Result).SetError(FString::Printf(TEXT("Struct type '%s' not found"), *NewType));
return;
}
UEdGraph* Graph = FoundNode->GetGraph();
const UEdGraphSchema* Schema = Graph ? Graph->GetSchema() : nullptr;
if (!Schema)
{
MCPErrorCallback(Result).SetError(TEXT("Graph schema not found"));
return;
}
// Remember existing connections keyed by property base name
struct FPinConnection
{
EEdGraphPinDirection Direction;
TArray<UEdGraphPin*> LinkedPins;
};
TMap<FString, FPinConnection> ConnectionsByBaseName;
for (UEdGraphPin* Pin : FoundNode->Pins)
{
if (!Pin || Pin->LinkedTo.Num() == 0) continue;
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
FString BaseName = ExtractPropertyBaseName(Pin->PinName.ToString());
FPinConnection& Conn = ConnectionsByBaseName.FindOrAdd(BaseName);
Conn.Direction = Pin->Direction;
Conn.LinkedPins = Pin->LinkedTo;
}
// Change the struct type and reconstruct
if (BreakNode) BreakNode->StructType = NewStruct;
else MakeNode->StructType = NewStruct;
FoundNode->BreakAllNodeLinks();
Schema->ReconstructNode(*FoundNode);
// Reconnect pins by matching property base names
int32 Reconnected = 0;
int32 Failed = 0;
for (auto& Pair : ConnectionsByBaseName)
{
const FString& BaseName = Pair.Key;
const FPinConnection& OldConn = Pair.Value;
UEdGraphPin* NewPin = FindMatchingPin(FoundNode, BaseName, OldConn.Direction, NewStruct);
if (NewPin)
{
for (UEdGraphPin* Target : OldConn.LinkedPins)
{
if (Schema->TryCreateConnection(NewPin, Target))
Reconnected++;
else
Failed++;
}
}
else
{
Failed += OldConn.LinkedPins.Num();
}
}
// Get the owning blueprint and mark modified
UBlueprint* BP = FBlueprintEditorUtils::FindBlueprintForGraph(Graph);
if (BP) FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
// Output
Result.Appendf(TEXT("Changed to %s\n"), *MCPUtils::FormatName(NewStruct));
Result.Appendf(TEXT("Reconnected: %d\n"), Reconnected);
if (Failed > 0)
Result.Appendf(TEXT("Failed: %d\n"), Failed);
}
private:
// Find a UScriptStruct by name, trying with and without F prefix.
static UScriptStruct* FindStructByName(const FString& TypeName)
{
// Try stripping F prefix first (Unreal stores structs without it)
FString InternalName = TypeName;
if (TypeName.StartsWith(TEXT("F")))
InternalName = TypeName.Mid(1);
UScriptStruct* Found = FindFirstObject<UScriptStruct>(*InternalName);
if (Found) return Found;
// Try the original name
Found = FindFirstObject<UScriptStruct>(*TypeName);
if (Found) return Found;
// Fallback: iterate all UScriptStructs and use Identifies
for (TObjectIterator<UScriptStruct> It; It; ++It)
{
if (MCPUtils::Identifies(TypeName, *It))
return *It;
}
return nullptr;
}
// Extract the property base name from a pin name, stripping the GUID
// and numeric index suffixes that Unreal appends.
static FString ExtractPropertyBaseName(const FString& PinName)
{
int32 LastUnderscore;
if (!PinName.FindLastChar(TEXT('_'), LastUnderscore) || LastUnderscore <= 0)
return PinName;
FString Suffix = PinName.Mid(LastUnderscore + 1);
if (Suffix.Len() != 32)
return PinName;
FString WithoutGuid = PinName.Left(LastUnderscore);
int32 SecondUnderscore;
if (!WithoutGuid.FindLastChar(TEXT('_'), SecondUnderscore) || SecondUnderscore <= 0)
return WithoutGuid;
FString IndexStr = WithoutGuid.Mid(SecondUnderscore + 1);
if (IndexStr.IsNumeric())
return WithoutGuid.Left(SecondUnderscore);
return WithoutGuid;
}
// Find a pin on the node matching the given base name and direction.
// Falls back to matching the struct-typed pin if no name match is found.
static UEdGraphPin* FindMatchingPin(UEdGraphNode* Node, const FString& BaseName,
EEdGraphPinDirection Direction, UScriptStruct* NewStruct)
{
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->Direction != Direction) continue;
FString NewBaseName = ExtractPropertyBaseName(Pin->PinName.ToString());
if (NewBaseName.Equals(BaseName, ESearchCase::IgnoreCase))
return Pin;
}
// Fallback: match the single struct-typed pin
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->Direction != Direction) continue;
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct &&
Pin->PinType.PinSubCategoryObject == NewStruct)
return Pin;
}
return nullptr;
}
};

View File

@@ -1,84 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "UMCPHandler_CheckPinConnectionCompatibility.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Pre-flight check: can two pins be connected?
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CheckPinConnectionCompatibility : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the source pin, e.g. /Game/Foo,graph:EventGraph,node:MyNode,pin:Output"))
FString SourcePin;
UPROPERTY(meta=(Description="Path to the target pin, e.g. /Game/Foo,graph:EventGraph,node:OtherNode,pin:Input"))
FString TargetPin;
virtual FString GetDescription() const override
{
return TEXT("Check whether two pins can be connected, and what kind of connection would result.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher FS(Result);
UEdGraphPin* SrcPin = FS.Walk(SourcePin).Cast<UEdGraphPin>();
if (!SrcPin) return;
MCPFetcher FT(Result);
UEdGraphPin* TgtPin = FT.Walk(TargetPin).Cast<UEdGraphPin>();
if (!TgtPin) return;
const UEdGraphSchema* Schema = SrcPin->GetOwningNode()->GetGraph()->GetSchema();
const FPinConnectionResponse Response = Schema->CanCreateConnection(SrcPin, TgtPin);
bool bCompatible = (Response.Response != ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW);
// Decode the response type
const TCHAR* ResponseType;
switch (Response.Response)
{
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE:
ResponseType = TEXT("direct");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_A:
ResponseType = TEXT("break-source-connections");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_B:
ResponseType = TEXT("break-target-connections");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_AB:
ResponseType = TEXT("break-both-connections");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE:
ResponseType = TEXT("requires-conversion");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_PROMOTION:
ResponseType = TEXT("requires-promotion");
break;
case ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW:
default:
ResponseType = TEXT("disallowed");
break;
}
Result.Appendf(TEXT("Compatible: %s\n"), bCompatible ? TEXT("yes") : TEXT("no"));
Result.Appendf(TEXT("ConnectionType: %s\n"), ResponseType);
if (!Response.Message.IsEmpty())
Result.Appendf(TEXT("Message: %s\n"), *Response.Message.ToString());
Result.Appendf(TEXT("SourcePinType: %s\n"), *MCPUtils::FormatPinType(SrcPin));
Result.Appendf(TEXT("TargetPinType: %s\n"), *MCPUtils::FormatPinType(TgtPin));
}
};

View File

@@ -1,159 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UMCPHandler_CompileBlueprint.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CompileBlueprint : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Blueprint name or package path. If specified, compile this single blueprint."))
FString Blueprint;
UPROPERTY(meta=(Optional, Description="Substring search query. If specified instead of blueprint, compile all matching blueprints."))
FString Query;
UPROPERTY(meta=(Optional, Description="If true, return the count of matching blueprints without compiling."))
bool CountOnly = false;
UPROPERTY(meta=(Optional, Description="Starting index for pagination (default 0)."))
int32 Offset = 0;
UPROPERTY(meta=(Optional, Description="Maximum number of blueprints to compile (default 0 = no limit)."))
int32 Limit = 0;
virtual FString GetDescription() const override
{
return TEXT("Compile one or more blueprints without saving. "
"Reports errors and warnings. "
"Use 'blueprint' for a single blueprint, or 'query' to bulk-compile matching blueprints.");
}
// Compile a single blueprint and append results to Out.
// Returns true if the blueprint compiled cleanly (no errors).
static bool CompileAndReport(UBlueprint* BP, FStringBuilderBase& Out)
{
FLogCaptureOutputDevice LogCapture;
GLog->AddOutputDevice(&LogCapture);
EBlueprintCompileOptions CompileOpts =
EBlueprintCompileOptions::SkipSave |
EBlueprintCompileOptions::SkipGarbageCollection |
EBlueprintCompileOptions::SkipFiBSearchMetaUpdate;
FKismetEditorUtilities::CompileBlueprint(BP, CompileOpts, nullptr);
GLog->RemoveOutputDevice(&LogCapture);
int32 ErrorCount = 0;
int32 WarningCount = 0;
// Collect compiler messages from nodes
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
{
if (!Node->bHasCompilerMessage) continue;
bool bIsError = (Node->ErrorType == EMessageSeverity::Error);
if (bIsError) ErrorCount++; else WarningCount++;
Out.Appendf(TEXT(" %s: [%s] %s > %s: %s\n"),
bIsError ? TEXT("ERROR") : TEXT("WARNING"),
*MCPUtils::FormatName(Node->GetGraph()),
*MCPUtils::FormatName(Node),
*MCPUtils::FormatName(Node->GetClass()),
*Node->ErrorMsg);
}
// Collect log-captured errors/warnings
for (const FString& Msg : LogCapture.CapturedErrors)
{
ErrorCount++;
Out.Appendf(TEXT(" ERROR: (log) %s\n"), *Msg);
}
for (const FString& Msg : LogCapture.CapturedWarnings)
{
WarningCount++;
Out.Appendf(TEXT(" WARNING: (log) %s\n"), *Msg);
}
FString StatusStr = MCPUtils::EnumToString((EBlueprintStatus)BP->Status, TEXT("BS_"));
bool bIsValid = (BP->Status == BS_UpToDate) && (ErrorCount == 0);
if (bIsValid && WarningCount == 0)
{
Out.Appendf(TEXT(" OK (status: %s)\n"), *StatusStr);
}
else
{
Out.Appendf(TEXT(" status: %s, errors: %d, warnings: %d\n"),
*StatusStr, ErrorCount, WarningCount);
}
return bIsValid;
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPAssets<UBlueprint> Finder;
Finder.Scan<UBlueprint>().Scan<UWorld>().Errors(Result);
if (!Blueprint.IsEmpty())
{
if (!Finder.Exact(Blueprint).ENone().ETwo().Info()) return;
}
else
{
if (!Finder.Substring(Query).Info()) return;
}
const TArray<FAssetData>& MatchingAssets = Finder.AllData();
int32 TotalMatching = MatchingAssets.Num();
// countOnly: return count without compiling anything
if (CountOnly)
{
Result.Appendf(TEXT("Matching blueprints: %d\n"), TotalMatching);
return;
}
// Compute range
int32 StartIdx = FMath::Clamp(Offset, 0, TotalMatching);
int32 EndIdx = (Limit > 0) ? FMath::Min(StartIdx + Limit, TotalMatching) : TotalMatching;
int32 TotalChecked = 0;
int32 TotalPassed = 0;
int32 TotalFailed = 0;
for (int32 Idx = StartIdx; Idx < EndIdx; Idx++)
{
const FAssetData& Asset = MatchingAssets[Idx];
FString PackagePath = Asset.PackageName.ToString();
// Load the Blueprint (handles both regular and level blueprints)
MCPAssets<UBlueprint> Loader;
Loader.Scan<UBlueprint>().Scan<UWorld>();
if (!Loader.Exact(PackagePath).ENone().ETwo().Load())
{
continue;
}
UBlueprint* BP = Loader.Object();
TotalChecked++;
Result.Appendf(TEXT("%s:\n"), *MCPUtils::FormatName(BP));
bool bValid = CompileAndReport(BP, Result);
if (bValid) TotalPassed++; else TotalFailed++;
}
Result.Appendf(TEXT("\nSummary: %d checked, %d passed, %d failed (of %d matching)\n"),
TotalChecked, TotalPassed, TotalFailed, TotalMatching);
}
};

View File

@@ -1,63 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "MaterialDomain.h"
#include "UMCPHandler_CompileMaterial.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CompileMaterial : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Material name or package path"))
FString Material;
virtual FString GetDescription() const override
{
return TEXT("Force recompile a material and check for compilation errors.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Load material
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterial* MaterialObj = Assets.Object();
// Force recompile by triggering PreEditChange/PostEditChange
MaterialObj->PreEditChange(nullptr);
MaterialObj->PostEditChange();
// Check for compilation errors via FMaterialResource on current platform
TArray<FString> Errors;
FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel);
if (Resource)
{
Errors = Resource->GetCompileErrors();
}
if (Errors.IsEmpty())
{
Result.Appendf(TEXT("%s compiled successfully.\n"), *MCPUtils::FormatName(MaterialObj));
Result.Append(TEXT("WARNING: Error detection is not working. GetCompileErrors() returns empty even when shader compilation fails. Do not trust this result.\n"));
}
else
{
Result.Appendf(TEXT("%s compiled with %d error(s):\n"), *MCPUtils::FormatName(MaterialObj), Errors.Num());
for (const FString& Err : Errors)
{
Result.Appendf(TEXT(" %s\n"), *Err);
}
}
}
};

View File

@@ -1,91 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_ConnectPins.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FConnectPinsEntry
{
GENERATED_BODY()
UPROPERTY()
FString SourcePin;
UPROPERTY()
FString TargetPin;
};
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ConnectPins : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Array of {sourcePin, targetPin} objects. Each pin is a path like node:MyNode,pin:Output"))
FMCPJsonArray Connections;
virtual FString GetDescription() const override
{
return TEXT("Connect pins between nodes in a Blueprint graph.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
int32 SuccessCount = 0;
int32 TotalCount = Connections.Array.Num();
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
{
FConnectPinsEntry Entry;
if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, Result))
continue;
MCPFetcher FS(Result, BP);
UEdGraphPin* SourcePin = FS.Walk(Entry.SourcePin).Cast<UEdGraphPin>();
if (!SourcePin) continue;
MCPFetcher FT(Result, BP);
UEdGraphPin* TargetPin = FT.Walk(Entry.TargetPin).Cast<UEdGraphPin>();
if (!TargetPin) continue;
const UEdGraphSchema* Schema = SourcePin->GetOwningNode()->GetGraph()->GetSchema();
const FPinConnectionResponse Response = Schema->CanCreateConnection(SourcePin, TargetPin);
if (Response.Response == CONNECT_RESPONSE_DISALLOW)
{
Result.Appendf(TEXT("error: Cannot connect %s.%s to %s.%s: %s\n"),
*MCPUtils::FormatName(SourcePin->GetOwningNode()), *MCPUtils::FormatName(SourcePin),
*MCPUtils::FormatName(TargetPin->GetOwningNode()), *MCPUtils::FormatName(TargetPin),
*Response.Message.ToString());
continue;
}
Schema->TryCreateConnection(SourcePin, TargetPin);
SuccessCount++;
}
if (SuccessCount > 0)
{
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
}
Result.Appendf(TEXT("Connected %d/%d pins.\n"), SuccessCount, TotalCount);
}
};

View File

@@ -1,126 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimBlueprintGeneratedClass.h"
#include "Animation/AnimInstance.h"
#include "Animation/Skeleton.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UMCPHandler_CreateAnimBlueprintAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CreateAnimBlueprintAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Name for the new Animation Blueprint asset"))
FString Name;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Description="Name or path of the skeleton asset to use"))
FString Skeleton;
UPROPERTY(meta=(Optional, Description="Parent class name (default: AnimInstance)"))
FString ParentClass;
virtual FString GetDescription() const override
{
return TEXT("Create a new Animation Blueprint asset with a specified skeleton.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPErrorCallback CB(Result);
if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty())
{
return CB.SetError(TEXT("Missing required fields: name, packagePath, skeleton"));
}
if (!PackagePath.StartsWith(TEXT("/Game")))
{
return CB.SetError(TEXT("packagePath must start with '/Game'"));
}
// Check if asset already exists
MCPAssets<UBlueprint> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(CB).EAny().Info()) return;
// Resolve skeleton
MCPAssets<USkeleton> SkeletonAssets;
if (!SkeletonAssets.Exact(Skeleton).Errors(CB).ENone().ETwo().Load()) return;
USkeleton* SkeletonObj = SkeletonAssets.Object();
// Resolve parent class (default: UAnimInstance)
UClass* ParentClassObj = UAnimInstance::StaticClass();
if (!ParentClass.IsEmpty() && !ParentClass.Equals(TEXT("AnimInstance"), ESearchCase::IgnoreCase))
{
UClass* Found = nullptr;
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->IsChildOf(UAnimInstance::StaticClass()) && MCPUtils::Identifies(ParentClass, *It))
{
Found = *It;
break;
}
}
if (!Found)
{
return CB.SetError(FString::Printf(TEXT("Parent class '%s' not found (must derive from AnimInstance)"), *ParentClass));
}
ParentClassObj = Found;
}
// Create the package
FString FullPackagePath = PackagePath / Name;
UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package)
{
return CB.SetError(FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
}
// Create the Animation Blueprint
UAnimBlueprint* NewAnimBP = CastChecked<UAnimBlueprint>(
FKismetEditorUtilities::CreateBlueprint(
ParentClassObj,
Package,
FName(*Name),
BPTYPE_Normal,
UAnimBlueprint::StaticClass(),
UAnimBlueprintGeneratedClass::StaticClass()
));
if (!NewAnimBP)
{
return CB.SetError(TEXT("FKismetEditorUtilities::CreateBlueprint returned null"));
}
// Set target skeleton
NewAnimBP->TargetSkeleton = SkeletonObj;
// Compile and save
FKismetEditorUtilities::CompileBlueprint(NewAnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP);
Result.Appendf(TEXT("Created: %s\n"), *FullPackagePath);
Result.Appendf(TEXT("ParentClass: %s\n"), *MCPUtils::FormatName(ParentClassObj));
Result.Appendf(TEXT("Saved: %s\n"), bSaved ? TEXT("true") : TEXT("false"));
TArray<UEdGraph*> Graphs = MCPUtils::AllGraphs(NewAnimBP);
for (UEdGraph* Graph : Graphs)
{
Result.Appendf(TEXT("Graph: %s\n"), *MCPUtils::FormatName(Graph));
}
}
};

View File

@@ -1,82 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Animation/Skeleton.h"
#include "Animation/BlendSpace.h"
#include "UMCPHandler_CreateBlendSpaceAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CreateBlendSpaceAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Name for the new Blend Space asset"))
FString Name;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Description="Name or path of the skeleton asset to use"))
FString Skeleton;
virtual FString GetDescription() const override
{
return TEXT("Create a new 2D Blend Space asset with a specified skeleton.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (!PackagePath.StartsWith(TEXT("/Game")))
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n"));
return;
}
// Check if an asset with this name already exists.
MCPAssets<UBlendSpace> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Resolve skeleton.
MCPAssets<USkeleton> SkeletonAssets;
if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return;
USkeleton* SkeletonObj = SkeletonAssets.Object();
// Create the package.
FString FullPackagePath = PackagePath / Name;
UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package)
{
Result.Appendf(TEXT("ERROR: Failed to create package at '%s'\n"), *FullPackagePath);
return;
}
// Create the Blend Space.
UBlendSpace* NewBS = NewObject<UBlendSpace>(Package, FName(*Name), RF_Public | RF_Standalone);
if (!NewBS)
{
Result.Append(TEXT("ERROR: Failed to create Blend Space object\n"));
return;
}
// Set skeleton.
NewBS->SetSkeleton(SkeletonObj);
// Mark dirty and save.
NewBS->MarkPackageDirty();
bool bSaved = MCPUtils::SaveGenericPackage(NewBS);
Result.Appendf(TEXT("Created %s\n"), *NewBS->GetPathName());
Result.Appendf(TEXT("Skeleton: %s\n"), *SkeletonObj->GetPathName());
if (!bSaved)
Result.Append(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -1,119 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UMCPHandler_CreateBlueprintAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CreateBlueprintAsset : 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, FStringBuilderBase& Result) override
{
MCPErrorCallback Error(Result);
// Validate packagePath starts with /Game
if (!PackagePath.StartsWith(TEXT("/Game")))
{
return Error.SetError(TEXT("PackagePath must start with '/Game'"));
}
// Check if asset already exists
MCPAssets<UBlueprint> ExistCheck;
if (!ExistCheck.Exact(Blueprint).Errors(Error).EAny().Info()) return;
// Resolve parent class — try C++ class first, then Blueprint asset
UClass* ParentClassObj = MCPUtils::FindClassByName(ParentClass);
if (!ParentClassObj)
{
MCPAssets<UBlueprint> ParentAssets;
if (!ParentAssets.Exact(ParentClass).AllContent().Errors(Error).ETwo().Load()) return;
if (!ParentAssets.Objects().IsEmpty() && ParentAssets.Object()->GeneratedClass)
ParentClassObj = ParentAssets.Object()->GeneratedClass;
}
if (!ParentClassObj)
{
return Error.SetError(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 (!MCPUtils::StringToEnum(BlueprintType, BlueprintTypeEnum, Error, TEXT("BPTYPE_"))) return;
}
// For Interface type, parent must be UInterface
if ((BlueprintTypeEnum == BPTYPE_Interface) && !ParentClassObj->IsChildOf(UInterface::StaticClass()))
{
ParentClassObj = UInterface::StaticClass();
}
// Create the package
FString FullPackagePath = PackagePath / Blueprint;
UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package)
{
return Error.SetError(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 Error.SetError(TEXT("FKismetEditorUtilities::CreateBlueprint returned null"));
}
// Compile and save
FKismetEditorUtilities::CompileBlueprint(NewBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP);
// Report result
Result.Appendf(TEXT("Created: %s\n"), *MCPUtils::FormatName(NewBP));
Result.Appendf(TEXT("Parent: %s\n"), *MCPUtils::FormatName(ParentClassObj));
if (!bSaved)
Result.Append(TEXT("Warning: save failed\n"));
for (UEdGraph* Graph : MCPUtils::AllGraphs(NewBP))
Result.Appendf(TEXT("Graph: %s\n"), *MCPUtils::FormatName(Graph));
}
};

View File

@@ -1,120 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.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 "UMCPHandler_CreateBlueprintGraph.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CreateBlueprintGraph : 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, FStringBuilderBase& Result) override
{
if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent"))
{
Result.Appendf(TEXT("ERROR: Invalid GraphType '%s'. Valid values: function, macro, customEvent\n"), *GraphType);
return;
}
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Check graph name uniqueness
if (!MCPUtils::AllGraphsNamed(BP, Graph).IsEmpty())
{
Result.Appendf(TEXT("ERROR: A graph named '%s' already exists in %s\n"), *Graph, *MCPUtils::FormatName(BP));
return;
}
// For custom events, also check for existing custom events with the same name
if (GraphType == TEXT("customEvent"))
{
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
if (CE->CustomFunctionName == FName(*Graph))
{
Result.Appendf(TEXT("ERROR: A custom event named '%s' already exists in %s\n"), *Graph, *MCPUtils::FormatName(BP));
return;
}
}
}
if (GraphType == TEXT("function"))
{
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!NewGraph)
{
Result.Append(TEXT("ERROR: Failed to create function graph\n"));
return;
}
FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast<UClass*>(nullptr));
Result.Appendf(TEXT("Created function graph: %s\n"), *MCPUtils::FormatName(NewGraph));
}
else if (GraphType == TEXT("macro"))
{
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!NewGraph)
{
Result.Append(TEXT("ERROR: Failed to create macro graph\n"));
return;
}
FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr);
Result.Appendf(TEXT("Created macro graph: %s\n"), *MCPUtils::FormatName(NewGraph));
}
else // customEvent
{
UEdGraph* EventGraph = nullptr;
if (BP->UbergraphPages.Num() > 0)
EventGraph = BP->UbergraphPages[0];
if (!EventGraph)
{
Result.Append(TEXT("ERROR: Blueprint has no EventGraph to add a custom event to\n"));
return;
}
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();
Result.Appendf(TEXT("Created custom event: %s\n"), *MCPUtils::FormatName(NewEvent));
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
MCPUtils::SaveBlueprintPackage(BP);
}
};

View File

@@ -1,88 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/UserDefinedEnum.h"
#include "Kismet2/EnumEditorUtils.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Factories/EnumFactory.h"
#include "UMCPHandler_CreateEnumAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CreateEnumAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full package path for the new enum (e.g. '/Game/DataTypes/E_MyEnum')"))
FString AssetPath;
UPROPERTY(meta=(Description="Array of enum value names"))
FMCPJsonArray Values;
virtual FString GetDescription() const override
{
return TEXT("Create a new UserDefinedEnum asset with the specified values.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
FString PackagePath, AssetName;
if (!MCPUtils::SplitAssetPath(AssetPath, PackagePath, AssetName))
{
Result.Append(TEXT("ERROR: AssetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')\n"));
return;
}
TArray<FString> EnumValues;
for (const TSharedPtr<FJsonValue>& Val : Values.Array)
{
FString Str = Val->AsString();
if (!Str.IsEmpty()) EnumValues.Add(Str);
}
if (EnumValues.Num() == 0)
{
Result.Append(TEXT("ERROR: Values must be a non-empty array of strings\n"));
return;
}
// Check that no enum with this name already exists.
MCPAssets<UUserDefinedEnum> ExistCheck;
if (!ExistCheck.Exact(AssetName).Errors(Result).EAny().Info()) return;
// Create the enum using AssetTools.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
UEnumFactory* Factory = NewObject<UEnumFactory>();
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedEnum::StaticClass(), Factory);
UUserDefinedEnum* NewEnum = Cast<UUserDefinedEnum>(NewAsset);
if (!NewEnum)
{
Result.Append(TEXT("ERROR: Failed to create UserDefinedEnum asset\n"));
return;
}
// Add enum values — UUserDefinedEnum starts with a MAX value.
// We need to add entries before MAX.
for (int32 i = 0; i < EnumValues.Num(); ++i)
{
FEnumEditorUtils::AddNewEnumeratorForUserDefinedEnum(NewEnum);
int32 NewIndex = NewEnum->NumEnums() - 2;
FEnumEditorUtils::SetEnumeratorDisplayName(NewEnum, NewIndex, FText::FromString(EnumValues[i]));
}
bool bSaved = MCPUtils::SaveGenericPackage(NewEnum);
Result.Appendf(TEXT("Created %s with %d values\n"), *NewEnum->GetPathName(), EnumValues.Num());
if (!bSaved)
Result.Append(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -1,109 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "MaterialDomain.h"
#include "Factories/MaterialFactoryNew.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "UMCPHandler_CreateMaterialAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CreateMaterialAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Name for the new material asset"))
FString Name;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Optional, Description="Material domain: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"))
FString Domain;
UPROPERTY(meta=(Optional, Description="Blend mode: Opaque, Masked, Translucent, Additive, Modulate"))
FString BlendMode;
UPROPERTY(meta=(Optional, Description="Whether the material is two-sided"))
bool TwoSided = false;
virtual FString GetDescription() const override
{
return TEXT("Create a new UMaterial asset with optional domain, blend mode, and two-sided settings.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (!PackagePath.StartsWith(TEXT("/Game")))
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n"));
return;
}
// Parse optional enum properties before creating the asset.
EMaterialDomain ParsedDomain = MD_Surface;
if (!Domain.IsEmpty())
{
if (!MCPUtils::StringToEnum(Domain, ParsedDomain, Result, TEXT("MD_")))
return;
}
EBlendMode ParsedBlendMode = BLEND_Opaque;
if (!BlendMode.IsEmpty())
{
if (!MCPUtils::StringToEnum(BlendMode, ParsedBlendMode, Result, TEXT("BLEND_")))
return;
}
// Check if an asset with this name already exists.
MCPAssets<UMaterial> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Create via IAssetTools + factory.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
UMaterialFactoryNew* Factory = NewObject<UMaterialFactoryNew>();
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterial::StaticClass(), Factory);
UMaterial* MaterialObj = Cast<UMaterial>(NewAsset);
if (!MaterialObj)
{
Result.Appendf(TEXT("ERROR: Failed to create Material '%s' in '%s'\n"), *Name, *PackagePath);
return;
}
// Apply optional properties.
bool bHasTwoSided = Json->HasField(TEXT("twoSided"));
MaterialObj->PreEditChange(nullptr);
if (!Domain.IsEmpty())
MaterialObj->MaterialDomain = ParsedDomain;
if (!BlendMode.IsEmpty())
MaterialObj->BlendMode = ParsedBlendMode;
if (bHasTwoSided)
MaterialObj->TwoSided = TwoSided;
MaterialObj->PostEditChange();
bool bSaved = MCPUtils::SaveMaterialPackage(MaterialObj);
Result.Appendf(TEXT("Created %s\n"), *MaterialObj->GetPathName());
Result.Appendf(TEXT("Domain: %s\n"), *MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_")));
Result.Appendf(TEXT("BlendMode: %s\n"), *MCPUtils::EnumToString(MaterialObj->BlendMode, TEXT("BLEND_")));
Result.Appendf(TEXT("TwoSided: %s\n"), MaterialObj->TwoSided ? TEXT("true") : TEXT("false"));
if (!bSaved)
Result.Append(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -1,72 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/MaterialFunction.h"
#include "Factories/MaterialFunctionFactoryNew.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "UMCPHandler_CreateMaterialFunctionAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CreateMaterialFunctionAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Name for the new material function asset"))
FString Name;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Optional, Description="Description for the material function"))
FString Description;
virtual FString GetDescription() const override
{
return TEXT("Create a new UMaterialFunction asset with an optional description.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (!PackagePath.StartsWith(TEXT("/Game")))
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n"));
return;
}
// Check if asset already exists.
MCPAssets<UMaterialFunction> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Create via IAssetTools + factory.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
UMaterialFunctionFactoryNew* Factory = NewObject<UMaterialFunctionFactoryNew>();
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialFunction::StaticClass(), Factory);
UMaterialFunction* MF = Cast<UMaterialFunction>(NewAsset);
if (!MF)
{
Result.Appendf(TEXT("ERROR: Failed to create Material Function '%s' in '%s'\n"), *Name, *PackagePath);
return;
}
// Set optional description.
if (!Description.IsEmpty())
MF->Description = Description;
bool bSaved = MCPUtils::SaveGenericPackage(MF);
Result.Appendf(TEXT("Created %s\n"), *MF->GetPathName());
if (!bSaved)
Result.Append(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -1,111 +0,0 @@
#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 "Factories/MaterialInstanceConstantFactoryNew.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "UMCPHandler_CreateMaterialInstanceAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CreateMaterialInstanceAsset : 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, FStringBuilderBase& Result) override
{
if (!PackagePath.StartsWith(TEXT("/Game")))
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n"));
return;
}
// Check if asset already exists.
MCPAssets<UMaterialInstanceConstant> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Load parent material -- try as Material first, then as Material Instance.
UMaterialInterface* ParentMaterialObj = nullptr;
{
MCPAssets<UMaterial> MatAssets;
if (MatAssets.Exact(ParentMaterial).ETwo().Load() && !MatAssets.Objects().IsEmpty())
{
ParentMaterialObj = MatAssets.Object();
}
else
{
MCPAssets<UMaterialInstanceConstant> MIAssets;
if (MIAssets.Exact(ParentMaterial).ETwo().Load() && !MIAssets.Objects().IsEmpty())
{
ParentMaterialObj = MIAssets.Object();
}
}
}
if (!ParentMaterialObj)
{
ParentMaterialObj = LoadObject<UMaterialInterface>(nullptr, *ParentMaterial);
}
if (!ParentMaterialObj)
{
Result.Appendf(TEXT("ERROR: Parent material '%s' not found\n"), *ParentMaterial);
return;
}
// Create via factory + AssetTools.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
UMaterialInstanceConstantFactoryNew* Factory = NewObject<UMaterialInstanceConstantFactoryNew>();
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory);
UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(NewAsset);
if (!MI)
{
Result.Appendf(TEXT("ERROR: Failed to create Material Instance '%s' in '%s'\n"), *Name, *PackagePath);
return;
}
// Set parent.
MI->PreEditChange(nullptr);
MI->Parent = ParentMaterialObj;
MI->PostEditChange();
// Save.
bool bSaved = MCPUtils::SaveGenericPackage(MI);
Result.Appendf(TEXT("Created %s\n"), *MI->GetPathName());
if (UMaterialInstance* ParentMI = Cast<UMaterialInstance>(ParentMaterialObj))
Result.Appendf(TEXT("Parent: %s\n"), *MCPUtils::FormatName(ParentMI));
else if (UMaterial* ParentMat = Cast<UMaterial>(ParentMaterialObj))
Result.Appendf(TEXT("Parent: %s\n"), *MCPUtils::FormatName(ParentMat));
else
Result.Appendf(TEXT("Parent: %s\n"), *ParentMaterialObj->GetPathName());
if (!bSaved)
Result.Append(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -1,116 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "StructUtils/UserDefinedStruct.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Factories/StructureFactory.h"
#include "UMCPHandler_CreateStructAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FStructPropertyEntry
{
GENERATED_BODY()
UPROPERTY()
FString Name;
UPROPERTY()
FString Type;
};
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_CreateStructAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Name for the new struct asset"))
FString Name;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Optional, Description="Array of initial properties, each with 'name' and 'type' fields"))
FMCPJsonArray Properties;
virtual FString GetDescription() const override
{
return TEXT("Create a new UserDefinedStruct asset with optional initial properties.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (!PackagePath.StartsWith(TEXT("/Game")))
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n"));
return;
}
// Check if an asset with this name already exists.
MCPAssets<UUserDefinedStruct> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Create the struct using the AssetTools factory.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
UStructureFactory* Factory = NewObject<UStructureFactory>();
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UUserDefinedStruct::StaticClass(), Factory);
UUserDefinedStruct* NewStruct = Cast<UUserDefinedStruct>(NewAsset);
if (!NewStruct)
{
Result.Appendf(TEXT("ERROR: Failed to create struct '%s' in '%s'\n"), *Name, *PackagePath);
return;
}
// Add properties if specified.
int32 PropsAdded = 0;
for (const TSharedPtr<FJsonValue>& PropVal : Properties.Array)
{
FStructPropertyEntry Entry;
if (!MCPUtils::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal, Result)) return;
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
FEdGraphPinType PinType;
if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, Result))
continue;
// Snapshot existing GUIDs so we can find the newly added one.
TSet<FGuid> ExistingGuids;
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct))
ExistingGuids.Add(Var.VarGuid);
if (!FStructureEditorUtils::AddVariable(NewStruct, PinType))
continue;
// Find the new variable by diffing GUID sets.
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct))
{
if (!ExistingGuids.Contains(Var.VarGuid))
{
FStructureEditorUtils::RenameVariable(NewStruct, Var.VarGuid, Entry.Name);
break;
}
}
PropsAdded++;
}
bool bSaved = MCPUtils::SaveGenericPackage(NewStruct);
Result.Appendf(TEXT("Created %s\n"), *NewStruct->GetPathName());
if (PropsAdded > 0)
Result.Appendf(TEXT("Properties added: %d\n"), PropsAdded);
if (!bSaved)
Result.Append(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -1,127 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Misc/PackageName.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "HAL/FileManager.h"
#include "UMCPHandler_DeleteAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DeleteAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Package path of the asset to delete, e.g. /Game/Foo/Bar"))
FString AssetPath;
UPROPERTY(meta=(Optional, Description="If true, skip reference check and force delete"))
bool Force = false;
virtual FString GetDescription() const override
{
return TEXT("Delete a .uasset after verifying no references. "
"Use force=true to skip the reference check.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Verify the asset file exists on disk
FString PackageFilename = FPackageName::LongPackageNameToFilename(
AssetPath, FPackageName::GetAssetPackageExtension());
PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename);
if (!IFileManager::Get().FileExists(*PackageFilename))
{
Result.Appendf(TEXT("ERROR: Asset file not found on disk: %s\n"), *PackageFilename);
return;
}
// Check references
IAssetRegistry& Registry = *IAssetRegistry::Get();
TArray<FName> Referencers;
Registry.GetReferencers(FName(*AssetPath), Referencers);
// Filter out self-references
Referencers.RemoveAll([this](const FName& Ref) {
return Ref.ToString() == AssetPath;
});
if (Referencers.Num() > 0 && !Force)
{
Result.Appendf(TEXT("ERROR: Asset is still referenced by %d package(s):\n"), Referencers.Num());
for (const FName& Ref : Referencers)
{
FString RefStr = Ref.ToString();
UPackage* RefPackage = FindPackage(nullptr, *RefStr);
Result.Appendf(TEXT(" %s%s\n"), *RefStr,
RefPackage ? TEXT(" (loaded)") : TEXT(" (on-disk only)"));
}
Result.Append(TEXT("Use force=true to skip the reference check.\n"));
return;
}
// Force delete: unload the package from memory first
if (Force && Referencers.Num() > 0)
{
Result.Appendf(TEXT("WARNING: Force-deleting despite %d referencer(s).\n"), Referencers.Num());
}
if (Force)
{
UPackage* Package = FindPackage(nullptr, *AssetPath);
if (Package)
{
// Collect all objects in this package
TArray<UObject*> ObjectsInPackage;
GetObjectsWithPackage(Package, ObjectsInPackage);
// Clear flags and remove from root to allow GC
for (UObject* Obj : ObjectsInPackage)
{
if (Obj)
{
Obj->ClearFlags(RF_Standalone | RF_Public);
Obj->RemoveFromRoot();
}
}
Package->ClearFlags(RF_Standalone | RF_Public);
Package->RemoveFromRoot();
// Reset loaders to release file handles
ResetLoaders(Package);
// Force garbage collection to free the objects
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
}
}
// Delete the file on disk
bool bDeleted = IFileManager::Get().Delete(*PackageFilename, false, true);
if (!bDeleted)
{
Result.Appendf(TEXT("ERROR: Failed to delete file from disk: %s\n"), *PackageFilename);
return;
}
// Trigger an asset registry rescan so it notices the deletion
FString PackageDir;
int32 LastSlash;
if (AssetPath.FindLastChar(TEXT('/'), LastSlash))
{
PackageDir = AssetPath.Left(LastSlash);
Registry.ScanPathsSynchronous({PackageDir}, true);
}
Result.Appendf(TEXT("Deleted %s\n"), *AssetPath);
}
};

View File

@@ -1,96 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_DeleteBlueprintGraph.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DeleteBlueprintGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Foo/Bar"))
FString Path;
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, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
F.Walk(Path);
if (!F.Ok()) return;
UBlueprint* BP = F.Cast<UBlueprint>();
if (!BP) return;
// Search function graphs, then macro graphs
UEdGraph* TargetGraph = nullptr;
FString GraphType;
for (UEdGraph* G : BP->FunctionGraphs)
{
if (G && MCPUtils::Identifies(Graph, G))
{
TargetGraph = G;
GraphType = TEXT("function");
break;
}
}
if (!TargetGraph)
{
for (UEdGraph* G : BP->MacroGraphs)
{
if (G && MCPUtils::Identifies(Graph, G))
{
TargetGraph = G;
GraphType = TEXT("macro");
break;
}
}
}
// Check if it's an UbergraphPage (EventGraph) — disallow deletion
if (!TargetGraph)
{
for (UEdGraph* G : BP->UbergraphPages)
{
if (G && MCPUtils::Identifies(Graph, G))
{
Result.Appendf(TEXT("ERROR: Cannot delete UbergraphPage '%s'. EventGraph pages cannot be deleted.\n"),
*MCPUtils::FormatName(G));
return;
}
}
Result.Appendf(TEXT("ERROR: Graph '%s' not found in blueprint %s\n"),
*Graph, *MCPUtils::FormatName(BP));
return;
}
// Remove the graph
FString GraphName = MCPUtils::FormatName(TargetGraph);
FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Deleted %s graph %s\n"), *GraphType, *GraphName);
if (!bSaved)
Result.Append(TEXT("WARNING: Package save failed.\n"));
}
};

View File

@@ -1,80 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphNode.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_FunctionEntry.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_DeleteNodeFromGraph.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DeleteNodeFromGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the node, e.g. /Game/Foo,graph:EventGraph,node:MyNode"))
FString Node;
virtual FString GetDescription() const override
{
return TEXT("Delete a node from a Blueprint graph. "
"Cannot delete entry nodes (FunctionEntry, Event, CustomEvent).");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
UEdGraph* Graph = FoundNode->GetGraph();
UBlueprint* BP = FBlueprintEditorUtils::FindBlueprintForNodeChecked(FoundNode);
// 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.
MCPErrorCallback Error(Result);
FString NodeTitle = MCPUtils::FormatName(FoundNode);
FString GraphName = MCPUtils::FormatName(Graph);
if (Cast<UK2Node_FunctionEntry>(FoundNode))
{
return Error.SetError(FString::Printf(
TEXT("Cannot delete FunctionEntry node '%s' in graph '%s'. ")
TEXT("This is the root node of the function — removing it would leave an empty, uncompilable graph. ")
TEXT("To remove the entire function, delete it from the Blueprint editor."),
*NodeTitle, *GraphName));
}
if (Cast<UK2Node_Event>(FoundNode))
{
return Error.SetError(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>(FoundNode))
{
return Error.SetError(FString::Printf(
TEXT("Cannot delete CustomEvent entry node '%s' in graph '%s'. ")
TEXT("This is the root node of the custom event — removing it would leave an empty, uncompilable graph."),
*NodeTitle, *GraphName));
}
FoundNode->BreakAllNodeLinks();
Graph->RemoveNode(FoundNode);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
Result.Appendf(TEXT("Deleted node '%s' from graph '%s'.\n"), *NodeTitle, *GraphName);
}
};

View File

@@ -1,241 +0,0 @@
#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 "UMCPHandler_DiffTwoBlueprints.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DiffTwoBlueprints : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="First blueprint name or package path"))
FString BlueprintA;
UPROPERTY(meta=(Description="Second blueprint name or package path"))
FString BlueprintB;
UPROPERTY(meta=(Optional, Description="Filter to a specific graph name"))
FString Graph;
virtual FString GetDescription() const override
{
return TEXT("Structural diff between two different Blueprints. Compares nodes, "
"connections, and variables across graphs. Use for comparing variants, "
"finding divergence after copy-paste, or auditing consistency.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Load both blueprints
MCPAssets<UBlueprint> AssetsA;
if (!AssetsA.Exact(BlueprintA).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BPA = AssetsA.Object();
MCPAssets<UBlueprint> AssetsB;
if (!AssetsB.Exact(BlueprintB).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BPB = AssetsB.Object();
// Gather graphs, optionally filtering by name
auto GatherGraphs = [this](UBlueprint* BP) -> TArray<UEdGraph*>
{
TArray<UEdGraph*> Graphs;
for (UEdGraph* G : BP->UbergraphPages)
{
if (!G) continue;
if (!Graph.IsEmpty() && !MCPUtils::Identifies(Graph, G)) continue;
Graphs.Add(G);
}
for (UEdGraph* G : BP->FunctionGraphs)
{
if (!G) continue;
if (!Graph.IsEmpty() && !MCPUtils::Identifies(Graph, G)) continue;
Graphs.Add(G);
}
return Graphs;
};
TArray<UEdGraph*> GraphsA = GatherGraphs(BPA);
TArray<UEdGraph*> GraphsB = GatherGraphs(BPB);
// Build graph name maps
TMap<FString, UEdGraph*> GraphMapA, GraphMapB;
for (UEdGraph* G : GraphsA) GraphMapA.Add(MCPUtils::FormatName(G), G);
for (UEdGraph* G : GraphsB) GraphMapB.Add(MCPUtils::FormatName(G), G);
// Find all unique graph names
TSet<FString> AllGraphNames;
for (auto& Pair : GraphMapA) AllGraphNames.Add(Pair.Key);
for (auto& Pair : GraphMapB) AllGraphNames.Add(Pair.Key);
int32 TotalDiffs = 0;
for (const FString& GraphName : AllGraphNames)
{
UEdGraph** pGA = GraphMapA.Find(GraphName);
UEdGraph** pGB = GraphMapB.Find(GraphName);
if (!pGA)
{
Result.Appendf(TEXT("Graph %s: only in B (%d nodes)\n"), *GraphName, (*pGB)->Nodes.Num());
TotalDiffs++;
continue;
}
if (!pGB)
{
Result.Appendf(TEXT("Graph %s: only in A (%d nodes)\n"), *GraphName, (*pGA)->Nodes.Num());
TotalDiffs++;
continue;
}
// Both exist -- compare nodes
UEdGraph* GA = *pGA;
UEdGraph* GB = *pGB;
// Build node title maps for matching
TMap<FString, TArray<UEdGraphNode*>> NodesA, NodesB;
for (UEdGraphNode* N : GA->Nodes)
{
if (!N) continue;
NodesA.FindOrAdd(MCPUtils::FormatName(N)).Add(N);
}
for (UEdGraphNode* N : GB->Nodes)
{
if (!N) continue;
NodesB.FindOrAdd(MCPUtils::FormatName(N)).Add(N);
}
// Nodes only in A
TArray<FString> OnlyInA;
for (auto& Pair : NodesA)
{
int32 CountA = Pair.Value.Num();
int32 CountB = 0;
if (auto* pArr = NodesB.Find(Pair.Key)) CountB = pArr->Num();
if (CountA > CountB)
OnlyInA.Add(FString::Printf(TEXT(" %s (x%d)"), *Pair.Key, CountA - CountB));
}
// Nodes only in B
TArray<FString> OnlyInB;
for (auto& Pair : NodesB)
{
int32 CountB = Pair.Value.Num();
int32 CountA = 0;
if (auto* pArr = NodesA.Find(Pair.Key)) CountA = pArr->Num();
if (CountB > CountA)
OnlyInB.Add(FString::Printf(TEXT(" %s (x%d)"), *Pair.Key, CountB - CountA));
}
// Connection diff
auto MakeConnKey = [](UEdGraphPin* SrcPin, UEdGraphPin* TgtPin) -> FString
{
return FString::Printf(TEXT("%s|%s|%s|%s"),
*MCPUtils::FormatName(SrcPin->GetOwningNode()), *MCPUtils::FormatName(SrcPin),
*MCPUtils::FormatName(TgtPin->GetOwningNode()), *MCPUtils::FormatName(TgtPin));
};
auto GatherConnections = [&MakeConnKey](UEdGraph* G) -> TSet<FString>
{
TSet<FString> Conns;
for (UEdGraphNode* N : G->Nodes)
{
if (!N) continue;
for (UEdGraphPin* Pin : N->Pins)
{
if (!Pin || Pin->Direction != EGPD_Output) continue;
for (UEdGraphPin* Linked : Pin->LinkedTo)
{
if (!Linked || !Linked->GetOwningNode()) continue;
Conns.Add(MakeConnKey(Pin, Linked));
}
}
}
return Conns;
};
TSet<FString> ConnectionsA = GatherConnections(GA);
TSet<FString> ConnectionsB = GatherConnections(GB);
TArray<FString> ConnsOnlyInA, ConnsOnlyInB;
for (const FString& Key : ConnectionsA)
if (!ConnectionsB.Contains(Key))
ConnsOnlyInA.Add(FString::Printf(TEXT(" %s"), *Key));
for (const FString& Key : ConnectionsB)
if (!ConnectionsA.Contains(Key))
ConnsOnlyInB.Add(FString::Printf(TEXT(" %s"), *Key));
bool bIdentical = OnlyInA.IsEmpty() && OnlyInB.IsEmpty() && ConnsOnlyInA.IsEmpty() && ConnsOnlyInB.IsEmpty();
if (bIdentical)
{
Result.Appendf(TEXT("Graph %s: identical (%d nodes)\n"), *GraphName, GA->Nodes.Num());
continue;
}
TotalDiffs++;
Result.Appendf(TEXT("Graph %s: different (A=%d nodes, B=%d nodes)\n"), *GraphName, GA->Nodes.Num(), GB->Nodes.Num());
if (!OnlyInA.IsEmpty())
{
Result.Append(TEXT(" Nodes only in A:\n"));
for (const FString& Line : OnlyInA) Result.Appendf(TEXT(" %s\n"), *Line);
}
if (!OnlyInB.IsEmpty())
{
Result.Append(TEXT(" Nodes only in B:\n"));
for (const FString& Line : OnlyInB) Result.Appendf(TEXT(" %s\n"), *Line);
}
if (!ConnsOnlyInA.IsEmpty())
{
Result.Append(TEXT(" Connections only in A:\n"));
for (const FString& Line : ConnsOnlyInA) Result.Appendf(TEXT(" %s\n"), *Line);
}
if (!ConnsOnlyInB.IsEmpty())
{
Result.Append(TEXT(" Connections only in B:\n"));
for (const FString& Line : ConnsOnlyInB) Result.Appendf(TEXT(" %s\n"), *Line);
}
}
// Compare variables
TSet<FString> VarNamesA, VarNamesB;
for (const FBPVariableDescription& V : BPA->NewVariables) VarNamesA.Add(MCPUtils::FormatName(V));
for (const FBPVariableDescription& V : BPB->NewVariables) VarNamesB.Add(MCPUtils::FormatName(V));
TArray<FString> VarsOnlyInA, VarsOnlyInB;
for (const FString& Name : VarNamesA)
if (!VarNamesB.Contains(Name))
VarsOnlyInA.Add(Name);
for (const FString& Name : VarNamesB)
if (!VarNamesA.Contains(Name))
VarsOnlyInB.Add(Name);
if (!VarsOnlyInA.IsEmpty())
{
Result.Append(TEXT("Variables only in A:\n"));
for (const FString& Name : VarsOnlyInA) Result.Appendf(TEXT(" %s\n"), *Name);
TotalDiffs += VarsOnlyInA.Num();
}
if (!VarsOnlyInB.IsEmpty())
{
Result.Append(TEXT("Variables only in B:\n"));
for (const FString& Name : VarsOnlyInB) Result.Appendf(TEXT(" %s\n"), *Name);
TotalDiffs += VarsOnlyInB.Num();
}
Result.Appendf(TEXT("Total differences: %d\n"), TotalDiffs);
}
};

View File

@@ -1,108 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_DisconnectPins.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FDisconnectPinEntry
{
GENERATED_BODY()
UPROPERTY()
FString Pin;
UPROPERTY(meta=(Optional))
FString TargetPin;
};
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DisconnectPins : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Array of {pin, targetPin?} objects. Each pin is a path like node:MyNode,pin:Output. If targetPin is omitted, all connections on the pin are broken."))
FMCPJsonArray Disconnections;
virtual FString GetDescription() const override
{
return TEXT("Disconnect pins in a Blueprint graph. "
"Can disconnect a specific link or all links on a pin.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
int32 SuccessCount = 0;
int32 TotalDisconnected = 0;
for (const TSharedPtr<FJsonValue>& DiscVal : Disconnections.Array)
{
FDisconnectPinEntry Entry;
if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal, Result)) continue;
MCPFetcher FP(Result, BP);
UEdGraphPin* Pin = FP.Walk(Entry.Pin).Cast<UEdGraphPin>();
if (!Pin) continue;
int32 DisconnectedCount = 0;
if (!Entry.TargetPin.IsEmpty())
{
MCPFetcher FT(Result, BP);
UEdGraphPin* Target = FT.Walk(Entry.TargetPin).Cast<UEdGraphPin>();
if (!Target) continue;
if (!Pin->LinkedTo.Contains(Target))
{
Result.Appendf(TEXT("Error: %s.%s is not connected to %s.%s\n"),
*MCPUtils::FormatName(Pin->GetOwningNode()), *MCPUtils::FormatName(Pin),
*MCPUtils::FormatName(Target->GetOwningNode()), *MCPUtils::FormatName(Target));
continue;
}
Pin->BreakLinkTo(Target);
DisconnectedCount = 1;
}
else
{
DisconnectedCount = Pin->LinkedTo.Num();
if (DisconnectedCount > 0)
{
Pin->BreakAllPinLinks(true);
}
}
Result.Appendf(TEXT("Disconnected %d link(s) from %s.%s\n"),
DisconnectedCount,
*MCPUtils::FormatName(Pin->GetOwningNode()), *MCPUtils::FormatName(Pin));
SuccessCount++;
TotalDisconnected += DisconnectedCount;
}
if (TotalDisconnected > 0)
{
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
}
Result.Appendf(TEXT("Done: %d/%d succeeded, %d links broken.\n"),
SuccessCount, Disconnections.Array.Num(), TotalDisconnected);
}
};

View File

@@ -1,106 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/Skeleton.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "UMCPHandler_DumpBlueprint.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DumpBlueprint : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("Dump a Blueprint's structure: variables, interfaces, components, "
"and graph names. Does not include graph contents (use DumpGraphs for that).");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Header
Result.Appendf(TEXT("Blueprint: %s\n"), *MCPUtils::FormatName(BP));
Result.Appendf(TEXT("Parent: %s\n"), BP->ParentClass ? *MCPUtils::FormatName(BP->ParentClass) : TEXT("None"));
Result.Appendf(TEXT("Type: %s\n"),
*MCPUtils::EnumToString(BP->BlueprintType));
// Animation Blueprint
if (UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP))
{
if (AnimBP->TargetSkeleton)
Result.Appendf(TEXT("TargetSkeleton: %s\n"), *AnimBP->TargetSkeleton->GetPathName());
}
// Interfaces
for (const FBPInterfaceDescription& I : BP->ImplementedInterfaces)
{
if (I.Interface)
Result.Appendf(TEXT("Interface: %s\n"), *MCPUtils::FormatName(I.Interface));
}
// Variables
if (!BP->NewVariables.IsEmpty())
{
Result.Append(TEXT("\nVariables:\n"));
for (const FBPVariableDescription& V : BP->NewVariables)
{
Result.Appendf(TEXT(" %s %s"),
*MCPUtils::FormatPinType(V.VarType),
*MCPUtils::FormatName(V));
if (!V.DefaultValue.IsEmpty())
Result.Appendf(TEXT(" = %s"), *V.DefaultValue);
if (!V.Category.IsEmpty() && V.Category.ToString() != TEXT("Default"))
Result.Appendf(TEXT(" [%s]"), *V.Category.ToString());
Result.Append(TEXT("\n"));
}
}
// Components
if (USimpleConstructionScript* SCS = BP->SimpleConstructionScript)
{
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
if (!AllNodes.IsEmpty())
{
Result.Append(TEXT("\nComponents:\n"));
for (USCS_Node* Node : AllNodes)
{
if (!Node || !Node->ComponentTemplate) continue;
Result.Appendf(TEXT(" %s (%s)"),
*MCPUtils::FormatName(Node->ComponentTemplate),
*MCPUtils::FormatName(Node->ComponentClass));
if (Node->ParentComponentOrVariableName != NAME_None)
Result.Appendf(TEXT(" parent=%s"), *Node->ParentComponentOrVariableName.ToString());
Result.Append(TEXT("\n"));
}
}
}
// Graph names (without contents)
TArray<UEdGraph*> Graphs = MCPUtils::AllGraphs(BP);
if (!Graphs.IsEmpty())
{
Result.Append(TEXT("\nGraphs:\n"));
for (UEdGraph* Graph : Graphs)
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Graph));
}
}
};

View File

@@ -1,85 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "BlueprintExporter.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "Materials/Material.h"
#include "MaterialGraph/MaterialGraph.h"
#include "UMCPHandler_DumpGraphs.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DumpGraphs : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, material, or graph, e.g. /Game/Foo or /Game/Foo,graph:EventGraph"))
FString Path;
virtual FString GetDescription() const override
{
return TEXT("Dump blueprint or material graphs as readable text. "
"If given a blueprint or material, dumps all graphs. If given a specific graph, dumps only that one.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
F.Walk(Path);
if (!F.Ok()) return;
if (UEdGraph* Graph = Cast<UEdGraph>(F.Obj))
{
EmitGraph(Graph, Result);
return;
}
if (UBlueprint* BP = Cast<UBlueprint>(F.Obj))
{
TArray<UEdGraph*> Graphs = MCPUtils::AllGraphs(BP);
for (UEdGraph* Graph : Graphs)
{
Result.Appendf(TEXT("\n======== %s ========\n"), *MCPUtils::FormatName(Graph));
EmitGraph(Graph, Result);
}
return;
}
if (UMaterial* Mat = Cast<UMaterial>(F.Obj))
{
MCPUtils::EnsureMaterialGraph(Mat);
if (!Mat->MaterialGraph)
{
Result.Append(TEXT("ERROR: Could not build MaterialGraph for this material\n"));
return;
}
EmitGraph(Mat->MaterialGraph, Result);
return;
}
Result.Appendf(TEXT("ERROR: Expected a blueprint, material, or graph, got %s\n"),
*MCPUtils::FormatName(F.Obj->GetClass()));
}
private:
void EmitGraph(UEdGraph* Graph, FStringBuilderBase& Result)
{
FlxBlueprintExporter Exporter(Graph);
Result.Append(Exporter.GetOutput());
FString Details = Exporter.GetDetails();
if (!Details.IsEmpty())
{
Result.Append(TEXT("\n"));
Result.Append(Details);
}
}
};

View File

@@ -1,168 +0,0 @@
#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 "Engine/Texture.h"
#include "UMCPHandler_DumpMaterialInstanceParameters.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DumpMaterialInstanceParameters : 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, FStringBuilderBase& Result) override
{
MCPAssets<UMaterialInstanceConstant> Assets;
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
UMaterialInstanceConstant* MI = Assets.Object();
Result.Appendf(TEXT("MaterialInstance: %s\n"), *MCPUtils::FormatName(MI));
// Parent chain
if (MI->Parent)
{
Result.Append(TEXT("Parent chain:"));
UMaterialInterface* Current = MI->Parent;
while (Current)
{
if (UMaterial* Mat = Cast<UMaterial>(Current))
{
Result.Appendf(TEXT(" %s"), *MCPUtils::FormatName(Mat));
break;
}
if (UMaterialInstance* ParentMI = Cast<UMaterialInstance>(Current))
{
Result.Appendf(TEXT(" %s ->"), *MCPUtils::FormatName(ParentMI));
UMaterialInstanceConstant* ParentMIC = Cast<UMaterialInstanceConstant>(Current);
Current = ParentMIC ? ParentMIC->Parent : nullptr;
}
else
{
Result.Appendf(TEXT(" %s"), *Current->GetPathName());
break;
}
}
Result.Append(TEXT("\n"));
}
// Collect names of overridden parameters for filtering inherited ones
TSet<FString> OverriddenScalars, OverriddenVectors, OverriddenTextures, OverriddenStaticSwitches;
for (const FScalarParameterValue& P : MI->ScalarParameterValues)
OverriddenScalars.Add(P.ParameterInfo.Name.ToString());
for (const FVectorParameterValue& P : MI->VectorParameterValues)
OverriddenVectors.Add(P.ParameterInfo.Name.ToString());
for (const FTextureParameterValue& P : MI->TextureParameterValues)
OverriddenTextures.Add(P.ParameterInfo.Name.ToString());
{
FStaticParameterSet SP;
MI->GetStaticParameterValues(SP);
for (const FStaticSwitchParameter& P : SP.StaticSwitchParameters)
if (P.bOverride)
OverriddenStaticSwitches.Add(P.ParameterInfo.Name.ToString());
}
// Overridden parameters
bool bHasOverrides = false;
auto EnsureOverrideHeader = [&]() {
if (!bHasOverrides) { Result.Append(TEXT("\nOverridden Parameters:\n")); bHasOverrides = true; }
};
for (const FScalarParameterValue& P : MI->ScalarParameterValues)
{
EnsureOverrideHeader();
Result.Appendf(TEXT(" Scalar \"%s\" = %g\n"), *P.ParameterInfo.Name.ToString(), P.ParameterValue);
}
for (const FVectorParameterValue& P : MI->VectorParameterValues)
{
EnsureOverrideHeader();
Result.Appendf(TEXT(" Vector \"%s\" = (%.3f, %.3f, %.3f, %.3f)\n"),
*P.ParameterInfo.Name.ToString(),
P.ParameterValue.R, P.ParameterValue.G, P.ParameterValue.B, P.ParameterValue.A);
}
for (const FTextureParameterValue& P : MI->TextureParameterValues)
{
EnsureOverrideHeader();
Result.Appendf(TEXT(" Texture \"%s\" = %s\n"),
*P.ParameterInfo.Name.ToString(),
P.ParameterValue ? *MCPUtils::FormatName(P.ParameterValue) : TEXT("None"));
}
{
FStaticParameterSet StaticParams;
MI->GetStaticParameterValues(StaticParams);
for (const FStaticSwitchParameter& P : StaticParams.StaticSwitchParameters)
{
if (!P.bOverride) continue;
EnsureOverrideHeader();
Result.Appendf(TEXT(" StaticSwitch \"%s\" = %s\n"),
*P.ParameterInfo.Name.ToString(), P.Value ? TEXT("true") : TEXT("false"));
}
}
// Inherited (non-overridden) parameters from the base material
UMaterial* BaseMat = MI->GetMaterial();
if (!BaseMat) return;
bool bHasInherited = false;
auto EnsureInheritedHeader = [&]() {
if (!bHasInherited) { Result.Append(TEXT("\nInherited Parameters (not overridden):\n")); bHasInherited = true; }
};
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
{
if (!Expr) continue;
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
{
if (OverriddenScalars.Contains(SP->ParameterName.ToString())) continue;
EnsureInheritedHeader();
Result.Appendf(TEXT(" Scalar \"%s\" default=%g\n"), *SP->ParameterName.ToString(), SP->DefaultValue);
}
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
{
if (OverriddenVectors.Contains(VP->ParameterName.ToString())) continue;
EnsureInheritedHeader();
Result.Appendf(TEXT(" Vector \"%s\" default=(%.3f, %.3f, %.3f, %.3f)\n"),
*VP->ParameterName.ToString(),
VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
}
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
{
if (OverriddenTextures.Contains(TP->ParameterName.ToString())) continue;
EnsureInheritedHeader();
Result.Appendf(TEXT(" Texture \"%s\" default=%s\n"),
*TP->ParameterName.ToString(),
TP->Texture ? *MCPUtils::FormatName(TP->Texture) : TEXT("None"));
}
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
{
if (OverriddenStaticSwitches.Contains(SSP->ParameterName.ToString())) continue;
EnsureInheritedHeader();
Result.Appendf(TEXT(" StaticSwitch \"%s\" default=%s\n"),
*SSP->ParameterName.ToString(), SSP->DefaultValue ? TEXT("true") : TEXT("false"));
}
}
}
};

View File

@@ -1,76 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "UMCPHandler_DumpProperties.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DumpProperties : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="MCPFetcher path to the object (e.g. /Game/Materials/M_Gold or /Game/Tangibles/TAN_Char,component:Mesh0)"))
FString Path;
UPROPERTY(meta=(Optional, Description="Substring filter for property names"))
FString Query;
UPROPERTY(meta=(Optional, Description="Truncate values to 80 characters (default true)"))
bool Truncate = true;
UPROPERTY(meta=(Optional, Description="Only show properties declared on the object's own class, not inherited ones"))
bool Local = false;
virtual FString GetDescription() const override
{
return TEXT("List all blueprint-visible properties on an object resolved via MCPFetcher path, "
"showing current values and which properties are editable.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Resolve the path to an object and get its editable template.
MCPFetcher F(Result);
UObject* Template = F.Walk(Path).Template().Cast<UObject>();
if (!Template) return;
TArray<FProperty*> Props = MCPUtils::SearchProperties(Template, Query, CPF_Edit, Local);
UStruct* CurrentOwner = nullptr;
for (FProperty* Prop : Props)
{
FString PropName = MCPUtils::FormatName(Prop);
// Print section heading when the owning class changes.
UStruct* Owner = Prop->GetOwnerStruct();
if (Owner != CurrentOwner)
{
CurrentOwner = Owner;
Result.Appendf(TEXT("\nFrom %s:\n\n"), *MCPUtils::FormatName(Owner));
}
FString ValueStr = MCPUtils::GetPropertyValueText(Template, Prop);
if (Truncate && (ValueStr.Len() > 80))
ValueStr = ValueStr.Left(80) + TEXT("...");
bool bEditable = !Prop->HasAnyPropertyFlags(CPF_EditConst);
Result.Appendf(TEXT(" %s %s %s = %s\n"),
bEditable ? TEXT("editable") : TEXT("readonly"),
*MCPUtils::FormatPropertyType(Prop),
*PropName,
*ValueStr);
}
if (Props.IsEmpty())
Result.Append(TEXT(" (no blueprint-visible properties found)\n"));
}
};

View File

@@ -1,108 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_DuplicateNodesInGraph.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_DuplicateNodesInGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a graph, e.g. /Game/Foo,graph:EventGraph"))
FString Graph;
UPROPERTY(meta=(Description="Array of node names to duplicate (as returned by FormatName)"))
FMCPJsonArray Nodes;
UPROPERTY(meta=(Optional, Description="X offset for duplicated nodes"))
int32 OffsetX = 50;
UPROPERTY(meta=(Optional, Description="Y offset for duplicated nodes"))
int32 OffsetY = 50;
virtual FString GetDescription() const override
{
return TEXT("Duplicate one or more nodes in a Blueprint graph. "
"Creates copies offset from the originals with new GUIDs. "
"Connections are not preserved on the duplicates.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
if (Nodes.Array.Num() == 0)
{
Result.Append(TEXT("ERROR: Nodes array is empty\n"));
return;
}
// Find all source nodes by name
TArray<UEdGraphNode*> SourceNodes;
for (const TSharedPtr<FJsonValue>& IdVal : Nodes.Array)
{
FString Name = IdVal->AsString();
UEdGraphNode* Found = nullptr;
for (UEdGraphNode* Node : TargetGraph->Nodes)
{
if (MCPUtils::Identifies(Name, Node))
{
Found = Node;
break;
}
}
if (!Found)
{
Result.Appendf(TEXT("ERROR: Node '%s' not found in graph\n"), *Name);
continue;
}
SourceNodes.Add(Found);
}
if (SourceNodes.Num() == 0) return;
// Duplicate each node
for (UEdGraphNode* SourceNode : SourceNodes)
{
UEdGraphNode* NewNode = DuplicateObject<UEdGraphNode>(SourceNode, TargetGraph);
if (!NewNode)
{
Result.Appendf(TEXT("ERROR: Failed to duplicate %s\n"), *MCPUtils::FormatName(SourceNode));
continue;
}
NewNode->CreateNewGuid();
NewNode->NodePosX += OffsetX;
NewNode->NodePosY += OffsetY;
for (UEdGraphPin* Pin : NewNode->Pins)
{
if (Pin)
Pin->LinkedTo.Empty();
}
TargetGraph->AddNode(NewNode, false, false);
Result.Appendf(TEXT("Duplicated: %s -> %s\n"), *MCPUtils::FormatName(SourceNode), *MCPUtils::FormatName(NewNode));
}
UBlueprint* BP = Cast<UBlueprint>(TargetGraph->GetOuter());
if (BP)
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
}
};

View File

@@ -1,67 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPUtils.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "UMCPHandler_FindAssetReferences.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_FindAssetReferences : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Asset package path to find references for, e.g. /Game/Tangibles/TAN_Tree"))
FString AssetPath;
virtual FString GetDescription() const override
{
return TEXT("Find all assets that reference a given asset.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
IAssetRegistry& Registry = *IAssetRegistry::Get();
// Verify the asset exists
FAssetData AssetData = Registry.GetAssetByObjectPath(FSoftObjectPath(AssetPath));
if (!AssetData.IsValid())
{
Result.Appendf(TEXT("ERROR: Asset not found: %s\n"), *AssetPath);
return;
}
TArray<FName> Referencers;
Registry.GetReferencers(FName(*AssetPath), Referencers);
if (Referencers.Num() == 0)
{
Result.Append(TEXT("No referencers found.\n"));
return;
}
// Classify referencers by looking up their asset class
for (const FName& Ref : Referencers)
{
FString RefStr = Ref.ToString();
TArray<FAssetData> RefAssets;
Registry.GetAssetsByPackageName(Ref, RefAssets);
if (RefAssets.Num() > 0)
{
Result.Appendf(TEXT("%s %s\n"),
*MCPUtils::FormatName(RefAssets[0].GetClass()),
*RefStr);
}
else
{
Result.Appendf(TEXT("Unknown %s\n"), *RefStr);
}
}
}
};

View File

@@ -1,71 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "UMCPHandler_FindMaterialReferences.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_FindMaterialReferences : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Material or MaterialInstance name or package path"))
FString Material;
virtual FString GetDescription() const override
{
return TEXT("Find all assets that reference a given material or material instance.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Find the material asset (searching both Material and MaterialInstance)
MCPAssets<UMaterial> Assets;
Assets.Scan<UMaterial>().Scan<UMaterialInstanceConstant>();
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Info()) return;
FString PackagePath = Assets.OneData().PackageName.ToString();
IAssetRegistry& Registry = *IAssetRegistry::Get();
TArray<FName> Referencers;
Registry.GetReferencers(FName(*PackagePath), Referencers);
// Filter out self-references and classify each referencer
int32 Count = 0;
for (const FName& Ref : Referencers)
{
FString RefStr = Ref.ToString();
if (RefStr == PackagePath) continue;
TArray<FAssetData> RefAssets;
Registry.GetAssetsByPackageName(Ref, RefAssets);
if (RefAssets.Num() > 0)
{
Result.Appendf(TEXT("%s %s\n"),
*MCPUtils::FormatName(RefAssets[0].GetClass()),
*RefStr);
}
else
{
Result.Appendf(TEXT("Unknown %s\n"), *RefStr);
}
Count++;
}
if (Count == 0)
{
Result.Append(TEXT("No referencers found.\n"));
}
}
};

View File

@@ -1,39 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphNode.h"
#include "UMCPHandler_GetNodeComment.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_GetNodeComment : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the node, e.g. /Game/Foo,graph:EventGraph,node:MyNode"))
FString Node;
virtual FString GetDescription() const override
{
return TEXT("Get the comment text and bubble visibility of a node.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
Result.Appendf(TEXT("Node: %s\n"), *MCPUtils::FormatName(FoundNode));
Result.Appendf(TEXT("Comment: %s\n"), *FoundNode->NodeComment);
Result.Appendf(TEXT("BubbleVisible: %s\n"), FoundNode->bCommentBubbleVisible ? TEXT("true") : TEXT("false"));
}
};

View File

@@ -1,66 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "UMCPHandler_GetPinDetails.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleGetPinInfo — detailed information about a specific pin
// ============================================================
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_GetPinDetails : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the pin, e.g. /Game/Widgets/WB_Hotkeys,node:MyNode,pin:Result"))
FString Pin;
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, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UEdGraphPin* P = F.Walk(Pin).Cast<UEdGraphPin>();
if (!P) return;
Result.Appendf(TEXT("Direction: %s\n"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
Result.Appendf(TEXT("Type: %s\n"), *MCPUtils::FormatPinType(P));
if (P->PinType.IsArray()) Result.Append(TEXT("Container: Array\n"));
else if (P->PinType.IsSet()) Result.Append(TEXT("Container: Set\n"));
else if (P->PinType.IsMap()) Result.Append(TEXT("Container: Map\n"));
if (P->PinType.bIsReference) Result.Append(TEXT("IsReference: true\n"));
if (P->PinType.bIsConst) Result.Append(TEXT("IsConst: true\n"));
if (!P->DefaultValue.IsEmpty())
Result.Appendf(TEXT("DefaultValue: %s\n"), *P->DefaultValue);
if (!P->DefaultTextValue.IsEmpty())
Result.Appendf(TEXT("DefaultTextValue: %s\n"), *P->DefaultTextValue.ToString());
if (P->DefaultObject)
Result.Appendf(TEXT("DefaultObject: %s\n"), *P->DefaultObject->GetPathName());
// Connected pins
for (UEdGraphPin* Linked : P->LinkedTo)
{
if (!Linked || !Linked->GetOwningNode()) continue;
Result.Appendf(TEXT("Connection: %s :: %s\n"),
*MCPUtils::FormatName(Linked->GetOwningNode()),
*MCPUtils::FormatName(Linked));
}
}
};

View File

@@ -1,63 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Animation/AnimBlueprint.h"
#include "AnimGraphNode_Base.h"
#include "UMCPHandler_ListAnimSlotNames.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ListAnimSlotNames : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("List all animation slot names used in an Animation Blueprint.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPAssets<UAnimBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UAnimBlueprint* AnimBP = Assets.Object();
// Walk all anim nodes to collect slot names
TSet<FString> SlotNames;
for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
{
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
{
if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot")))
{
FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode);
if (!SlotValue.IsNone())
{
SlotNames.Add(SlotValue.ToString());
}
}
}
}
for (const FString& Slot : SlotNames)
{
Result.Appendf(TEXT("%s\n"), *Slot);
}
if (SlotNames.Num() == 0)
{
Result.Append(TEXT("No animation slot names found.\n"));
}
}
};

View File

@@ -1,64 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Animation/AnimBlueprint.h"
#include "AnimGraphNode_Base.h"
#include "UMCPHandler_ListAnimSyncGroups.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ListAnimSyncGroups : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to an Animation Blueprint, e.g. /Game/Foo/ABP_Character"))
FString Path;
virtual FString GetDescription() const override
{
return TEXT("List all sync group names used in an Animation Blueprint.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UAnimBlueprint* AnimBP = F.Walk(Path).Cast<UAnimBlueprint>();
if (!AnimBP) return;
// Walk all anim nodes to collect sync group names
TSet<FString> SyncGroupNames;
for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
{
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
{
if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName")))
{
FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode);
if (!GroupValue.IsNone())
{
SyncGroupNames.Add(GroupValue.ToString());
}
}
}
}
if (SyncGroupNames.Num() == 0)
{
Result.Append(TEXT("No sync groups found.\n"));
return;
}
for (const FString& Group : SyncGroupNames)
{
Result.Appendf(TEXT("%s\n"), *Group);
}
}
};

View File

@@ -1,85 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/World.h"
#include "UMCPHandler_ListBlueprintAssets.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ListBlueprintAssets : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path"))
FString Query;
UPROPERTY(meta=(Optional, Description="Filter by parent class name (exact match, case-insensitive)"))
FString ParentClass;
UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)"))
bool IncludeRegular = true;
UPROPERTY(meta=(Optional, Description="Include level blueprints (default true)"))
bool IncludeLevel = true;
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, FStringBuilderBase& Result) override
{
MCPAssets<UObject> Assets;
Assets.NoScans().Substring(Query).Limit(500).Errors(Result);
if (IncludeRegular) Assets.Scan<UBlueprint>();
if (IncludeLevel) Assets.Scan<UWorld>();
Assets.Info();
int32 Count = 0;
for (const FAssetData& Asset : Assets.AllData())
{
// Extract parent class name from asset tags
FString ParentClassName;
if (Asset.AssetClassPath == UWorld::StaticClass()->GetClassPathName())
{
ParentClassName = TEXT("LevelScriptActor");
}
else
{
Asset.GetTagValue(FName(TEXT("ParentClass")), ParentClassName);
int32 DotIndex;
if (ParentClassName.FindLastChar('.', DotIndex))
{
ParentClassName = ParentClassName.Mid(DotIndex + 1);
}
ParentClassName.RemoveFromEnd(TEXT("'"));
}
// Apply parent class filter
if (!ParentClass.IsEmpty())
{
if (!ParentClassName.Equals(ParentClass, ESearchCase::IgnoreCase))
{
continue;
}
}
Result.Appendf(TEXT("%30s %s\n"), *ParentClassName, *Asset.PackageName.ToString());
Count++;
}
if (Count == 0)
{
Result.Append(TEXT("No blueprint assets found.\n"));
}
}
};

View File

@@ -1,95 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "UMCPHandler_ListBlueprintComponents.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ListBlueprintComponents : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Tangibles/TAN_Tree"))
FString Path;
virtual FString GetDescription() const override
{
return TEXT("List all components in a Blueprint's SimpleConstructionScript, "
"showing hierarchy and component classes.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
F.Walk(Path);
if (!F.Ok()) return;
UBlueprint* BP = F.Cast<UBlueprint>();
if (!BP) return;
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS)
{
Result.Append(TEXT("ERROR: Not an Actor Blueprint (no SimpleConstructionScript)\n"));
return;
}
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
if (AllNodes.Num() == 0)
{
Result.Append(TEXT("No components.\n"));
return;
}
Result.Append(TEXT("WARNING: This only lists components added in this blueprint's SCS. "
"It does not include inherited components from C++ parent classes "
"(available via the CDO's OwnedComponents) or from parent blueprint SCS nodes.\n"));
// Emit components as a tree, starting from root nodes
for (USCS_Node* Root : RootNodes)
{
if (!Root) continue;
EmitNode(Root, 0, Root == RootNodes[0], Result);
}
}
private:
void EmitNode(USCS_Node* Node, int32 Depth, bool bIsSceneRoot, FStringBuilderBase& Result)
{
// Indent to show hierarchy
for (int32 i = 0; i < Depth; i++)
Result.Append(TEXT(" "));
FString ClassName = Node->ComponentClass
? MCPUtils::FormatName(Node->ComponentClass)
: TEXT("None");
Result.Appendf(TEXT("%s %s"),
*ClassName,
*MCPUtils::FormatName(Node->ComponentTemplate));
if (bIsSceneRoot && Depth == 0)
Result.Append(TEXT(" [SceneRoot]"));
Result.Append(TEXT("\n"));
for (USCS_Node* Child : Node->GetChildNodes())
{
if (!Child) continue;
EmitNode(Child, Depth + 1, false, Result);
}
}
};

View File

@@ -1,57 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "UMCPHandler_ListBlueprintInterfaces.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ListBlueprintInterfaces : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Foo/MyBlueprint"))
FString Path;
virtual FString GetDescription() const override
{
return TEXT("List all Blueprint Interfaces implemented by a Blueprint, "
"including their function graphs.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
F.Walk(Path);
if (!F.Ok()) return;
UBlueprint* BP = F.Cast<UBlueprint>();
if (!BP) return;
bool bAny = false;
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (!IfaceDesc.Interface) continue;
bAny = true;
Result.Appendf(TEXT("Interface: %s\n"), *MCPUtils::FormatName(IfaceDesc.Interface));
for (const UEdGraph* Graph : IfaceDesc.Graphs)
{
if (!Graph) continue;
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Graph));
}
}
if (!bAny)
{
Result.Append(TEXT("No interfaces implemented.\n"));
}
}
};

View File

@@ -1,78 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPUtils.h"
#include "UMCPHandler_ListClassProperties.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ListClassProperties : 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 Query;
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, FStringBuilderBase& Result) override
{
UClass* FoundClass = MCPUtils::FindClassByName(ClassName);
if (!FoundClass)
{
Result.Appendf(TEXT("ERROR: Class '%s' not found\n"), *ClassName);
return;
}
Result.Appendf(TEXT("Properties of %s:\n"), *MCPUtils::FormatName(FoundClass));
int32 Count = 0;
for (TFieldIterator<FProperty> PropIt(FoundClass); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
if (!Prop) continue;
FString PropName = Prop->GetName();
if (!Query.IsEmpty() && !PropName.Contains(Query, ESearchCase::IgnoreCase))
continue;
// Build compact flags string
TStringBuilder<256> Flags;
if (Prop->HasAnyPropertyFlags(CPF_BlueprintVisible)) Flags.Append(TEXT(" BlueprintVisible"));
if (Prop->HasAnyPropertyFlags(CPF_BlueprintReadOnly)) Flags.Append(TEXT(" BlueprintReadOnly"));
if (Prop->HasAnyPropertyFlags(CPF_Edit)) Flags.Append(TEXT(" EditAnywhere"));
if (Prop->HasAnyPropertyFlags(CPF_EditConst)) Flags.Append(TEXT(" VisibleOnly"));
if (Prop->HasAnyPropertyFlags(CPF_Config)) Flags.Append(TEXT(" Config"));
if (Prop->HasAnyPropertyFlags(CPF_SaveGame)) Flags.Append(TEXT(" SaveGame"));
if (Prop->HasAnyPropertyFlags(CPF_Transient)) Flags.Append(TEXT(" Transient"));
if (Prop->HasAnyPropertyFlags(CPF_RepNotify)) Flags.Append(TEXT(" RepNotify"));
UClass* OwnerClass = Prop->GetOwnerClass();
Result.Appendf(TEXT(" %s %s"), *MCPUtils::FormatPropertyType(Prop), *PropName);
if (OwnerClass && OwnerClass != FoundClass)
Result.Appendf(TEXT(" [%s]"), *MCPUtils::FormatName(OwnerClass));
if (Flags.Len() > 0)
Result.Appendf(TEXT(" (%s)"), Flags.ToString() + 1); // skip leading space
Result.Append(TEXT("\n"));
Count++;
}
if (Count == 0)
{
Result.Append(TEXT("No properties found.\n"));
}
}
};

View File

@@ -1,74 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.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 "UMCPHandler_ListEventDispatchers.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ListEventDispatchers : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Foo/MyBlueprint"))
FString Path;
virtual FString GetDescription() const override
{
return TEXT("List all event dispatchers on a Blueprint, including their parameter signatures.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Path).Cast<UBlueprint>();
if (!BP) return;
TSet<FName> DelegateNameSet;
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNameSet);
for (const FName& DelegateName : DelegateNameSet)
{
Result.Appendf(TEXT("%s("), *DelegateName.ToString());
UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName);
if (SigGraph)
{
bool bFirst = true;
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(SigGraph))
{
for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;
if (!bFirst) Result.Append(TEXT(", "));
bFirst = false;
Result.Appendf(TEXT("%s %s"),
*MCPUtils::FormatPinType(PinInfo->PinType),
*PinInfo->PinName.ToString());
}
break; // only need the first entry node
}
}
Result.Append(TEXT(")\n"));
}
if (DelegateNameSet.Num() == 0)
{
Result.Append(TEXT("No event dispatchers found.\n"));
}
}
};

View File

@@ -1,49 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "UMCPHandler_ListOpenAssetEditors.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ListOpenAssetEditors : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
virtual FString GetDescription() const override
{
return TEXT("List all currently open asset editors, showing which has focus and whether they have unsaved changes.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
if (!Sub)
{
Result.Append(TEXT("Error: AssetEditorSubsystem not available\n"));
return;
}
TArray<UObject*> EditedAssets = Sub->GetAllEditedAssets();
if (EditedAssets.IsEmpty())
{
Result.Append(TEXT("No asset editors are open.\n"));
return;
}
for (UObject* Asset : EditedAssets)
{
bool bDirty = Asset->GetOutermost()->IsDirty();
Result.Appendf(TEXT(" %s%s\n"),
bDirty ? TEXT("[unsaved] ") : TEXT(""),
*Asset->GetPathName());
}
}
};

View File

@@ -1,46 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "UMCPHandler_OpenAssetEditor.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_OpenAssetEditor : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="MCPFetcher path to the asset to open (e.g. /Game/Materials/M_Gold)"))
FString Path;
virtual FString GetDescription() const override
{
return TEXT("Open an asset in its editor and bring it to focus.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UObject* Obj = F.Walk(Path).Cast<UObject>();
if (!Obj) return;
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
if (!Sub)
{
Result.Append(TEXT("Error: AssetEditorSubsystem not available\n"));
return;
}
if (Sub->OpenEditorForAsset(Obj))
Result.Appendf(TEXT("Opened editor for %s\n"), *Obj->GetPathName());
else
Result.Appendf(TEXT("Error: Could not open editor for %s\n"), *Obj->GetPathName());
}
};

View File

@@ -1,89 +0,0 @@
#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 "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_RefreshAllNodesInGraph.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_RefreshAllNodesInGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("Refresh all nodes in a Blueprint, removing orphaned pins. "
"Reports compiler warnings and errors.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Load Blueprint
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
int32 GraphCount = MCPUtils::AllGraphs(BP).Num();
int32 NodeCount = MCPUtils::AllNodes(BP).Num();
// Refresh all nodes
FBlueprintEditorUtils::RefreshAllNodes(BP);
// Remove orphaned pins from all nodes
int32 OrphanedPinsRemoved = 0;
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
{
for (int32 i = Node->Pins.Num() - 1; i >= 0; --i)
{
UEdGraphPin* Pin = Node->Pins[i];
if (Pin && Pin->bOrphanedPin)
{
Pin->BreakAllPinLinks();
Node->Pins.RemoveAt(i);
OrphanedPinsRemoved++;
}
}
}
// Mark as modified and recompile after orphan removal
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
// Summary
Result.Appendf(TEXT("Refreshed %s: %d graphs, %d nodes"), *MCPUtils::FormatName(BP), GraphCount, NodeCount);
if (OrphanedPinsRemoved > 0)
{
Result.Appendf(TEXT(", %d orphaned pins removed"), OrphanedPinsRemoved);
}
Result.Append(TEXT("\n"));
// Collect compiler warnings and errors
if (BP->Status == BS_Error)
{
Result.Append(TEXT("ERROR: Blueprint has compiler errors after refresh\n"));
}
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
{
if (!Node->bHasCompilerMessage) continue;
const TCHAR* Prefix = (Node->ErrorType == EMessageSeverity::Error) ? TEXT("ERROR") : TEXT("WARNING");
Result.Appendf(TEXT("%s: [%s] %s: %s\n"),
Prefix, *MCPUtils::FormatName(Node->GetGraph()),
*MCPUtils::FormatName(Node), *Node->ErrorMsg);
}
}
};

View File

@@ -1,81 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
#include "UMCPHandler_RemoveAnimStateFromMachine.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_RemoveAnimStateFromMachine : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the state machine graph, e.g. /Game/MyAnimBP,graph:StateMachine"))
FString Path;
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, FStringBuilderBase& Result) override
{
// Fetch the state machine graph via MCPFetcher
MCPFetcher F(Result);
F.Walk(Path);
if (!F.Ok()) return;
UAnimationStateMachineGraph* SMGraph = F.Cast<UAnimationStateMachineGraph>();
if (!SMGraph) return;
// Find the owning AnimBlueprint for compile/save
UBlueprint* BP = Cast<UBlueprint>(SMGraph->GetOuter()->GetOuter());
if (!BP)
{
Result.Append(TEXT("ERROR: Could not find owning blueprint.\n"));
return;
}
// Find the state node
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
// Collect and remove transitions connected to this state
int32 RemovedTransitions = 0;
for (UEdGraphNode* Node : TArray<UEdGraphNode*>(SMGraph->Nodes))
{
UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node);
if (!TransNode) continue;
if (TransNode->GetPreviousState() != StateNode && TransNode->GetNextState() != StateNode) continue;
TransNode->BreakAllNodeLinks();
SMGraph->RemoveNode(TransNode);
RemovedTransitions++;
}
// Remove the state
StateNode->BreakAllNodeLinks();
SMGraph->RemoveNode(StateNode);
// Compile and save
FKismetEditorUtilities::CompileBlueprint(BP);
MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Removed state %s and %d transition(s).\n"),
*MCPUtils::FormatName(StateNode), RemovedTransitions);
}
};

View File

@@ -1,96 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_RemoveBlueprintComponent.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS)
{
Result.Append(TEXT("ERROR: Not an Actor Blueprint (no SimpleConstructionScript).\n"));
return;
}
// Find the node to remove using Identifies for consistent name matching
USCS_Node* NodeToRemove = nullptr;
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
for (USCS_Node* Node : AllNodes)
{
if (Node && Node->ComponentTemplate &&
MCPUtils::Identifies(Component, Node->ComponentTemplate))
{
NodeToRemove = Node;
break;
}
}
if (!NodeToRemove)
{
Result.Appendf(TEXT("ERROR: Component '%s' not found.\nAvailable components:\n"),
*Component);
for (USCS_Node* Node : AllNodes)
{
if (Node && Node->ComponentTemplate)
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Node->ComponentTemplate));
}
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)
{
Result.Appendf(TEXT("ERROR: Cannot remove '%s' — it is a root component with %d child(ren). "
"Remove or re-parent the children first.\n"),
*MCPUtils::FormatName(NodeToRemove->ComponentTemplate),
NodeToRemove->GetChildNodes().Num());
return;
}
FString RemovedName = MCPUtils::FormatName(NodeToRemove->ComponentTemplate);
// 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);
Result.Appendf(TEXT("Removed component %s.%s\n"),
*RemovedName,
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
}
};

View File

@@ -1,75 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_RemoveBlueprintInterface.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_RemoveBlueprintInterface : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Interface name to remove"))
FString InterfaceName;
UPROPERTY(meta=(Optional, Description="If true, keep the function graphs as regular functions"))
bool PreserveFunctions = false;
virtual FString GetDescription() const override
{
return TEXT("Remove a Blueprint Interface implementation from a Blueprint. "
"Optionally preserve the function graphs as regular functions.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// Find the interface by name
UClass* FoundInterface = nullptr;
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (!IfaceDesc.Interface) continue;
if (MCPUtils::Identifies(InterfaceName, IfaceDesc.Interface))
{
FoundInterface = IfaceDesc.Interface;
break;
}
}
if (!FoundInterface)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Interface '%s' not found. Implemented interfaces: "), *InterfaceName));
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (IfaceDesc.Interface)
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(IfaceDesc.Interface));
}
return;
}
FTopLevelAssetPath InterfacePath = FoundInterface->GetClassPathName();
FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
Result.Appendf(TEXT("Removed interface %s\n"), *MCPUtils::FormatName(FoundInterface));
if (PreserveFunctions)
Result.Append(TEXT("Function graphs preserved as regular functions.\n"));
}
};

View File

@@ -1,73 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_RemoveBlueprintVariable.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_RemoveBlueprintVariable : 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, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Find the variable using Identifies for consistent name matching
const FBPVariableDescription* Found = nullptr;
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (MCPUtils::FormatName(Var) == VariableName ||
Var.VarName.ToString().Equals(VariableName, ESearchCase::IgnoreCase))
{
Found = &Var;
break;
}
}
if (!Found)
{
Result.Appendf(TEXT("ERROR: Variable '%s' not found in %s.\nAvailable variables:\n"),
*VariableName, *MCPUtils::FormatName(BP));
for (const FBPVariableDescription& Var : BP->NewVariables)
{
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Var));
}
return;
}
FName VarFName = Found->VarName;
// RemoveMemberVariable also cleans up Get/Set nodes
FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Removed variable %s from %s.%s\n"),
*VarFName.ToString(), *MCPUtils::FormatName(BP),
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
}
};

View File

@@ -1,115 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_EditablePinBase.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_RemoveFunctionParameter.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// Find the entry node (function entry or custom event)
UK2Node_EditablePinBase* EntryNode = nullptr;
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
if (MCPUtils::Identifies(FunctionName, FuncEntry->GetGraph()))
{
EntryNode = FuncEntry;
break;
}
}
if (!EntryNode)
{
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
{
EntryNode = CustomEvent;
break;
}
}
}
if (!EntryNode)
{
Result.Appendf(TEXT("Error: Function or event '%s' not found.\nAvailable:\n"), *FunctionName);
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
Result.Appendf(TEXT(" function: %s\n"), *MCPUtils::FormatName(FE->GetGraph()));
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
Result.Appendf(TEXT(" event: %s\n"), *MCPUtils::FormatName(CE));
return;
}
// Find the parameter to remove
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)
{
Result.Appendf(TEXT("Error: Parameter '%s' not found on %s.\nAvailable:\n"),
*ParamName, *MCPUtils::FormatName(EntryNode));
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
if (PinInfo.IsValid())
Result.Appendf(TEXT(" %s\n"), *PinInfo->PinName.ToString());
return;
}
// Remove the pin
EntryNode->UserDefinedPins.RemoveAt(RemovedIndex);
// Reconstruct the node to update output pins
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
Schema->ReconstructNode(*EntryNode);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Removed parameter '%s' from %s.\n"), *ParamName, *MCPUtils::FormatName(EntryNode));
if (!bSaved)
Result.Append(TEXT("Warning: save failed.\n"));
}
};

View File

@@ -1,75 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "StructUtils/UserDefinedStruct.h"
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
#include "UMCPHandler_RemoveStructField.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_RemoveStructField : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Struct name or package path"))
FString Struct;
UPROPERTY(meta=(Description="Name of the field to remove"))
FString FieldName;
virtual FString GetDescription() const override
{
return TEXT("Remove a field from a UserDefinedStruct asset.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UUserDefinedStruct* S = F.Walk(Struct).Cast<UUserDefinedStruct>();
if (!S) return;
// Find the field GUID by name
FGuid TargetGuid;
bool bFound = false;
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(S))
{
if (Var.FriendlyName.Equals(FieldName, ESearchCase::IgnoreCase) ||
Var.VarName.ToString().Equals(FieldName, ESearchCase::IgnoreCase))
{
TargetGuid = Var.VarGuid;
bFound = true;
break;
}
}
if (!bFound)
{
Result.Appendf(TEXT("ERROR: Field '%s' not found in %s.\nAvailable fields:\n"),
*FieldName, *MCPUtils::FormatName(S));
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(S))
{
Result.Appendf(TEXT(" %s\n"), *Var.FriendlyName);
}
return;
}
if (!FStructureEditorUtils::RemoveVariable(S, TargetGuid))
{
Result.Appendf(TEXT("ERROR: Failed to remove field '%s'."), *FieldName);
return;
}
bool bSaved = MCPUtils::SaveGenericPackage(S);
Result.Appendf(TEXT("Removed field %s from %s.%s\n"),
*FieldName, *MCPUtils::FormatName(S),
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
}
};

View File

@@ -1,70 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "UMCPHandler_RenameAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_RenameAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Current package path of the asset, e.g. /Game/Widgets/WB_Hotkeys"))
FString AssetPath;
UPROPERTY(meta=(Description="New package path (e.g. /Game/Widgets/WB_NewName) or just a new asset name (e.g. WB_NewName)"))
FString NewPath;
virtual FString GetDescription() const override
{
return TEXT("Rename or move an asset with reference fixup.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Load the asset
MCPAssets<UObject> Assets;
if (!Assets.Exact(AssetPath).AllContent().Errors(Result).ENone().ETwo().Load()) return;
UObject* AssetObj = Assets.Object();
// Parse new path into package path and asset name
FString NewPackagePath, NewAssetName;
if (!MCPUtils::SplitAssetPath(NewPath, NewPackagePath, NewAssetName))
{
// No slash — just a new name, keep the same directory
FString OldPackagePath, OldAssetName;
if (!MCPUtils::SplitAssetPath(AssetPath, OldPackagePath, OldAssetName))
{
Result.Appendf(TEXT("ERROR: Cannot determine directory from AssetPath '%s'\n"), *AssetPath);
return;
}
NewPackagePath = OldPackagePath;
NewAssetName = NewPath;
}
// Perform the rename with reference fixup
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
TArray<FAssetRenameData> RenameData;
RenameData.Add(FAssetRenameData(AssetObj, NewPackagePath, NewAssetName));
if (!AssetTools.RenameAssets(RenameData))
{
Result.Append(TEXT("ERROR: Rename failed. The target path may be invalid or a conflicting asset may exist.\n"));
return;
}
Result.Appendf(TEXT("Renamed to %s/%s\n"), *NewPackagePath, *NewAssetName);
}
};

View File

@@ -1,84 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_RenameBlueprintGraph.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_RenameBlueprintGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the graph, e.g. /Game/Foo,graph:MyFunction"))
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, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
UBlueprint* BP = Cast<UBlueprint>(TargetGraph->GetOuter());
if (!BP)
{
Result.Appendf(TEXT("Error: Graph '%s' is not owned by a Blueprint.\n"), *Graph);
return;
}
// Check if it's an UbergraphPage -- disallow rename
if (BP->UbergraphPages.Contains(TargetGraph))
{
Result.Appendf(TEXT("Error: Cannot rename UbergraphPage '%s'. EventGraph pages cannot be renamed.\n"),
*MCPUtils::FormatName(TargetGraph));
return;
}
// Verify it's a function or macro graph
bool bIsFunction = BP->FunctionGraphs.Contains(TargetGraph);
bool bIsMacro = BP->MacroGraphs.Contains(TargetGraph);
if (!bIsFunction && !bIsMacro)
{
Result.Appendf(TEXT("Error: Graph '%s' is not a function or macro graph.\n"),
*MCPUtils::FormatName(TargetGraph));
return;
}
// Check for name collision
for (UEdGraph* Existing : MCPUtils::AllGraphsNamed(BP, NewName))
{
if (Existing != TargetGraph)
{
Result.Appendf(TEXT("Error: A graph named '%s' already exists in '%s'.\n"),
*NewName, *MCPUtils::FormatName(BP));
return;
}
}
FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Renamed to %s %s\n"),
bIsFunction ? TEXT("function") : TEXT("macro"),
*MCPUtils::FormatName(TargetGraph));
}
};

View File

@@ -1,76 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UMCPHandler_ReparentBlueprint.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
// Load Blueprint
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
FString OldParentName = BP->ParentClass ? MCPUtils::FormatName(BP->ParentClass) : TEXT("None");
// Find the new parent class: try C++ classes first, then Blueprint assets
UClass* NewParentClassObj = MCPUtils::FindClassByName(NewParentClass);
if (!NewParentClassObj)
{
MCPAssets<UBlueprint> ParentAssets;
if (!ParentAssets.Exact(NewParentClass).AllContent().Errors(Result).ETwo().Load()) return;
if (!ParentAssets.Objects().IsEmpty() && ParentAssets.Object()->GeneratedClass)
NewParentClassObj = ParentAssets.Object()->GeneratedClass;
}
if (!NewParentClassObj)
{
MCPErrorCallback(Result).SetError(FString::Printf(
TEXT("Could not find class '%s'. Provide a C++ class name or Blueprint name."),
*NewParentClass));
return;
}
// Perform reparent
BP->PreEditChange(nullptr);
BP->ParentClass = NewParentClassObj;
BP->PostEditChange();
FBlueprintEditorUtils::RefreshAllNodes(BP);
FKismetEditorUtilities::CompileBlueprint(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Reparented %s: %s -> %s\n"),
*MCPUtils::FormatName(BP), *OldParentName, *MCPUtils::FormatName(NewParentClassObj));
if (!bSaved)
Result.Append(TEXT("Warning: save failed\n"));
}
};

View File

@@ -1,95 +0,0 @@
#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 "UMCPHandler_ReparentMaterialInstance.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
// Load the Material Instance
MCPAssets<UMaterialInstanceConstant> Assets;
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
UMaterialInstanceConstant* MI = Assets.Object();
// Load new parent (Material or MaterialInstance)
MCPAssets<UMaterialInterface> ParentAssets;
ParentAssets.NoScans();
ParentAssets.Scan<UMaterial>();
ParentAssets.Scan<UMaterialInstanceConstant>();
if (!ParentAssets.Exact(NewParent).Errors(Result).ENone().ETwo().Load()) return;
UMaterialInterface* NewParentObj = ParentAssets.Object();
// Prevent circular parenting
UMaterialInterface* Check = NewParentObj;
while (Check)
{
if (Check == MI)
{
Result.Appendf(TEXT("ERROR: Reparenting to '%s' would create a circular parent chain.\n"),
*NameOf(NewParentObj));
return;
}
UMaterialInstanceConstant* CheckMI = Cast<UMaterialInstanceConstant>(Check);
if (!CheckMI) break;
Check = CheckMI->Parent;
}
FString OldParentName = MI->Parent ? NameOf(MI->Parent) : TEXT("None");
if (DryRun)
{
Result.Appendf(TEXT("[DRY RUN] Would reparent %s: %s -> %s\n"),
*MCPUtils::FormatName(MI), *OldParentName, *NameOf(NewParentObj));
return;
}
MI->PreEditChange(nullptr);
MI->Parent = NewParentObj;
MI->PostEditChange();
MCPUtils::SaveGenericPackage(MI);
Result.Appendf(TEXT("Reparented %s: %s -> %s\n"),
*MCPUtils::FormatName(MI), *OldParentName, *NameOf(NewParentObj));
}
private:
FString NameOf(UMaterialInterface* Obj)
{
if (UMaterial* M = Cast<UMaterial>(Obj))
return MCPUtils::FormatName(M);
if (UMaterialInstance* MI = Cast<UMaterialInstance>(Obj))
return MCPUtils::FormatName(MI);
return Obj->GetPathName();
}
};

View File

@@ -1,162 +0,0 @@
#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 "K2Node_CallFunction.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_ReplaceFunctionCallsInBlueprint.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_ReplaceFunctionCallsInBlueprint : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Old class name to match"))
FString OldClass;
UPROPERTY(meta=(Description="New class name to redirect to"))
FString NewClass;
UPROPERTY(meta=(Optional, Description="If true, report what would change without modifying"))
bool DryRun = false;
virtual FString GetDescription() const override
{
return TEXT("Redirect function call nodes from one class to another. "
"Supports dry-run to preview impact before applying.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Load Blueprint
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// Find the new class
UClass* NewClassPtr = MCPUtils::FindClassByName(NewClass);
if (!NewClassPtr)
{
Result.Appendf(TEXT("ERROR: Could not find class '%s'\n"), *NewClass);
return;
}
int32 ReplacedCount = 0;
for (UK2Node_CallFunction* CallNode : MCPUtils::AllNodes<UK2Node_CallFunction>(BP))
{
UClass* ParentClass = CallNode->FunctionReference.GetMemberParentClass();
if (!ParentClass) continue;
if (!MCPUtils::Identifies(OldClass, ParentClass)) continue;
FName FuncName = CallNode->FunctionReference.GetMemberName();
// Find the matching function in the new class
UFunction* NewFunc = NewClassPtr->FindFunctionByName(FuncName);
if (!NewFunc)
{
Result.Appendf(TEXT("WARNING: Function '%s' not found in %s, skipping %s\n"),
*FuncName.ToString(), *MCPUtils::FormatName(NewClassPtr), *MCPUtils::FormatName(CallNode));
continue;
}
if (DryRun)
{
ReplacedCount++;
// Check which connected pins might not exist in the new function
for (UEdGraphPin* Pin : CallNode->Pins)
{
if (!Pin || Pin->LinkedTo.Num() == 0) continue;
bool bPinExistsInNew = false;
if (Pin->PinName == UEdGraphSchema_K2::PN_Execute ||
Pin->PinName == UEdGraphSchema_K2::PN_Then ||
Pin->PinName == UEdGraphSchema_K2::PN_Self ||
Pin->PinName == UEdGraphSchema_K2::PN_ReturnValue)
{
bPinExistsInNew = true;
}
else
{
for (TFieldIterator<FProperty> PropIt(NewFunc); PropIt; ++PropIt)
{
if (PropIt->GetFName() == Pin->PinName)
{
bPinExistsInNew = true;
break;
}
}
}
if (!bPinExistsInNew)
{
Result.Appendf(TEXT("AT RISK: %s pin %s -> %s\n"),
*MCPUtils::FormatName(CallNode), *MCPUtils::FormatName(Pin),
*MCPUtils::FormatName(Pin->LinkedTo[0]));
}
}
}
else
{
// Record existing pin connections before replacement
TMap<FName, TArray<UEdGraphPin*>> OldPinConnections;
for (UEdGraphPin* Pin : CallNode->Pins)
{
if (Pin->LinkedTo.Num() > 0)
{
OldPinConnections.Add(Pin->PinName, Pin->LinkedTo);
}
}
// Replace the function reference
CallNode->SetFromFunction(NewFunc);
ReplacedCount++;
// Report lost connections
for (auto& Pair : OldPinConnections)
{
UEdGraphPin* NewPin = CallNode->FindPin(Pair.Key);
for (UEdGraphPin* OldLinked : Pair.Value)
{
if (!OldLinked || !OldLinked->GetOwningNode()) continue;
bool bStillConnected = NewPin && NewPin->LinkedTo.Contains(OldLinked);
if (!bStillConnected)
{
Result.Appendf(TEXT("LOST: %s pin %s was connected to %s\n"),
*MCPUtils::FormatName(CallNode), *Pair.Key.ToString(),
*MCPUtils::FormatName(OldLinked));
}
}
}
}
}
if (DryRun)
{
Result.Appendf(TEXT("Dry run: %d node(s) would be replaced\n"), ReplacedCount);
return;
}
if (ReplacedCount > 0)
{
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
}
Result.Appendf(TEXT("Replaced %d node(s)\n"), ReplacedCount);
}
};

View File

@@ -1,73 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPUtils.h"
#include "Misc/PackageName.h"
#include "FileHelpers.h"
#include "HAL/FileManager.h"
#include "UMCPHandler_RestoreAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_RestoreAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)"))
FString AssetPath;
virtual FString GetDescription() const override
{
return TEXT("Restore a .uasset file from its .uasset.bak backup, reloading it in the editor.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
FString Filename = FPaths::ConvertRelativePathToFull(
FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension()));
FString BackupFilename = Filename + TEXT(".bak");
if (!IFileManager::Get().FileExists(*BackupFilename))
{
Result.Appendf(TEXT("ERROR: Backup file not found: %s\n"), *BackupFilename);
return;
}
// Release file handles if the package is loaded
UPackage* Package = FindPackage(nullptr, *AssetPath);
if (Package)
{
ResetLoaders(Package);
}
// Copy backup over the original
uint32 CopyResult = IFileManager::Get().Copy(*Filename, *BackupFilename, true);
if (CopyResult != COPY_OK)
{
Result.Appendf(TEXT("ERROR: Failed to copy backup over %s\n"), *AssetPath);
return;
}
// Reload the package if it was loaded
if (Package)
{
bool bReloaded = false;
FText ErrorMessage;
UEditorLoadingAndSavingUtils::ReloadPackages({Package}, bReloaded, ErrorMessage, EReloadPackagesInteractionMode::AssumePositive);
if (!bReloaded)
{
Result.Appendf(TEXT("WARNING: Restored %s but reload failed: %s\n"),
*AssetPath, *ErrorMessage.ToString());
return;
}
}
Result.Appendf(TEXT("Restored %s from backup\n"), *AssetPath);
}
};

View File

@@ -1,80 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "UMCPHandler_SearchAssets.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SearchAssets : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring to match against asset package paths"))
FString Query;
UPROPERTY(meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
FString Type;
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50)"))
int32 Limit = 50;
virtual FString GetDescription() const override
{
return TEXT("Search for assets by name and/or type. At least one of Query or Type must be specified.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (Query.IsEmpty() && Type.IsEmpty())
{
Result.Append(TEXT("ERROR: At least one of Query or Type must be specified\n"));
return;
}
MCPAssets<UObject> Assets;
// If a type is specified, find the UClass and filter by it
if (!Type.IsEmpty())
{
UClass* TypeClass = MCPUtils::FindClassByName(Type);
if (!TypeClass)
{
Result.Appendf(TEXT("ERROR: Unknown asset type '%s'\n"), *Type);
return;
}
Assets.NoScans().Scan(TypeClass);
}
if (!Query.IsEmpty())
{
Assets.Substring(Query);
}
Assets.AllContent().Limit(Limit).Errors(Result).Info();
const TArray<FAssetData>& AllData = Assets.AllData();
for (const FAssetData& Data : AllData)
{
Result.Appendf(TEXT("%s %s\n"),
*MCPUtils::FormatName(Data.GetClass()),
*Data.PackageName.ToString());
}
if (AllData.Num() == 0)
{
Result.Append(TEXT("No assets found.\n"));
}
else if (AllData.Num() >= Limit)
{
Result.Appendf(TEXT("WARNING: You reached the limit of %d, to raise it, specify the Limit parameter.\n"), Limit);
}
}
};

View File

@@ -1,62 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphSchema.h"
#include "UMCPHandler_SearchSpawnableNodeTypes.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SearchSpawnableNodeTypes : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Search query string"))
FString Query;
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)"))
int32 MaxResults = 50;
UPROPERTY(meta=(Description="MCPFetcher path to a graph, e.g. /Game/Foo,graph:EventGraph or /Game/Materials/M_Gold,graph:"))
FString Graph;
virtual FString GetDescription() const override
{
return TEXT("Search the action database for node types that can be spawned in a graph. "
"Works with any graph type (Blueprint, Material, etc.). "
"Returns full action names for use with SpawnNodesInGraph.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
MCPFetcher F(Result);
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
TArray<TSharedPtr<FEdGraphSchemaAction>> Actions = MCPUtils::SearchGraphActions(TargetGraph, Query, ClampedMax, /*ExactMatch=*/false);
for (const TSharedPtr<FEdGraphSchemaAction>& Action : Actions)
{
Result.Appendf(TEXT("%s\n"), *MCPUtils::ActionFullName(Action));
}
if (Actions.Num() == 0)
{
Result.Append(TEXT("No matching node types found.\n"));
}
else if (Actions.Num() >= ClampedMax)
{
Result.Appendf(TEXT("WARNING: Reached limit of %d results. Refine your query or increase MaxResults.\n"), ClampedMax);
}
}
};

View File

@@ -1,229 +0,0 @@
#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 "UMCPHandler_SearchTypeUsageInBlueprints.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleSearchByType — find all usages of a type across blueprints
// ============================================================
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SearchTypeUsageInBlueprints : 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 Query;
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, FStringBuilderBase& Result) override
{
FString DecodedTypeName = MCPUtils::UrlDecode(TypeName);
FString FilterStr = Query.IsEmpty() ? FString() : MCPUtils::UrlDecode(Query);
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);
};
int32 ResultCount = 0;
// Helper: get subtype name from a pin type
auto GetSubtype = [](const FEdGraphPinType& PinType) -> FString
{
if (PinType.PinSubCategoryObject.IsValid())
return PinType.PinSubCategoryObject->GetName();
return FString();
};
// Helper: check if a pin type matches
auto PinTypeMatches = [&](const FEdGraphPinType& PinType) -> bool
{
return MatchesType(GetSubtype(PinType)) || MatchesType(PinType.PinCategory.ToString());
};
// Lambda that searches a single Blueprint for type usages
auto SearchOneBlueprint = [&](UBlueprint* BP, bool bIsLevel)
{
FString BPName = MCPUtils::FormatName(BP);
// Check variables
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (ResultCount >= EffectiveMaxResults) return;
if (!PinTypeMatches(Var.VarType)) continue;
Result.Appendf(TEXT("variable %s in %s: %s %s\n"),
*MCPUtils::FormatName(Var), *BPName,
*Var.VarType.PinCategory.ToString(), *GetSubtype(Var.VarType));
ResultCount++;
}
// Check graphs for function/event params, struct nodes, and pin connections
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
{
if (ResultCount >= EffectiveMaxResults) return;
// Check FunctionEntry parameters
if (auto* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
{
for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;
if (!PinTypeMatches(PinInfo->PinType)) continue;
Result.Appendf(TEXT("func-param %s.%s in %s: %s %s\n"),
*MCPUtils::FormatName(Node->GetGraph()), *PinInfo->PinName.ToString(),
*BPName,
*PinInfo->PinType.PinCategory.ToString(), *GetSubtype(PinInfo->PinType));
ResultCount++;
}
}
else if (auto* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
{
for (const TSharedPtr<FUserPinInfo>& PinInfo : CustomEvent->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;
if (!PinTypeMatches(PinInfo->PinType)) continue;
Result.Appendf(TEXT("event-param %s.%s in %s: %s %s\n"),
*MCPUtils::FormatName(Node), *PinInfo->PinName.ToString(),
*BPName,
*PinInfo->PinType.PinCategory.ToString(), *GetSubtype(PinInfo->PinType));
ResultCount++;
}
}
// Check Break/Make struct nodes
else if (auto* BreakNode = Cast<UK2Node_BreakStruct>(Node))
{
if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName()))
{
Result.Appendf(TEXT("break-struct %s in %s graph %s\n"),
*BreakNode->StructType->GetName(), *BPName,
*MCPUtils::FormatName(Node->GetGraph()));
ResultCount++;
}
}
else if (auto* MakeNode = Cast<UK2Node_MakeStruct>(Node))
{
if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName()))
{
Result.Appendf(TEXT("make-struct %s in %s graph %s\n"),
*MakeNode->StructType->GetName(), *BPName,
*MCPUtils::FormatName(Node->GetGraph()));
ResultCount++;
}
}
// Check pin connections carrying the type
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden || ResultCount >= EffectiveMaxResults) continue;
if (Pin->LinkedTo.Num() == 0) continue;
if (!PinTypeMatches(Pin->PinType)) continue;
Result.Appendf(TEXT("pin %s.%s in %s graph %s: %s %s (%d connections)\n"),
*MCPUtils::FormatName(Node), *MCPUtils::FormatName(Pin),
*BPName, *MCPUtils::FormatName(Node->GetGraph()),
*Pin->PinType.PinCategory.ToString(), *GetSubtype(Pin->PinType),
Pin->LinkedTo.Num());
ResultCount++;
}
}
};
MCPAssets<UBlueprint> AllBlueprints;
AllBlueprints.Info();
MCPAssets<UWorld> AllWorlds;
AllWorlds.Info();
// Search regular blueprints
for (const FAssetData& Asset : AllBlueprints.AllData())
{
if (ResultCount >= EffectiveMaxResults) break;
FString AssetPath = Asset.PackageName.ToString();
FString AssetName = Asset.AssetName.ToString();
if (!FilterStr.IsEmpty() && !AssetName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
!AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase))
{
continue;
}
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
if (!BP) continue;
SearchOneBlueprint(BP, false);
}
// Search level blueprints from maps
for (const FAssetData& MapAsset : AllWorlds.AllData())
{
if (ResultCount >= 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(LevelBP, true);
}
Result.Appendf(TEXT("\n%d results\n"), ResultCount);
}
};

View File

@@ -1,111 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPUtils.h"
#include "UObject/UObjectIterator.h"
#include "UMCPHandler_SearchUnrealClasses.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleListClasses — discover available UClasses
// ============================================================
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SearchUnrealClasses : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring filter for class names"))
FString Query;
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 names, parent class, package, and flags.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
Limit = FMath::Clamp(Limit, 1, 500);
UClass* ParentClassObj = nullptr;
if (!ParentClass.IsEmpty())
{
for (TObjectIterator<UClass> It; It; ++It)
{
if (MCPUtils::Identifies(ParentClass, *It))
{
ParentClassObj = *It;
break;
}
}
if (!ParentClassObj)
{
Result.Appendf(TEXT("Error: Parent class '%s' not found\n"), *ParentClass);
return;
}
}
TArray<UClass*> Matches;
int32 TotalMatched = 0;
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Class = *It;
if (!Class) continue;
if (Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists)) continue;
if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue;
FString ClassName = MCPUtils::FormatName(Class);
if (!Query.IsEmpty() && !ClassName.Contains(Query, ESearchCase::IgnoreCase)) continue;
TotalMatched++;
if (Matches.Num() < Limit)
{
Matches.Add(Class);
}
}
Result.Appendf(TEXT("Found %d classes"), TotalMatched);
if (TotalMatched > Limit)
{
Result.Appendf(TEXT(" (showing %d)"), Limit);
}
Result.Append(TEXT("\n"));
for (UClass* Class : Matches)
{
Result.Appendf(TEXT(" %s"), *MCPUtils::FormatName(Class));
// Flags
TStringBuilder<64> Flags;
if (Class->HasAnyClassFlags(CLASS_Abstract)) Flags.Append(TEXT(" Abstract"));
if (Class->HasAnyClassFlags(CLASS_Interface)) Flags.Append(TEXT(" Interface"));
if (Class->HasAnyClassFlags(CLASS_MinimalAPI)) Flags.Append(TEXT(" MinimalAPI"));
if (Class->ClassGeneratedBy) Flags.Append(TEXT(" Blueprint"));
if (Flags.Len() > 0)
{
Result.Appendf(TEXT(" [%s]"), Flags.ToString() + 1); // skip leading space
}
if (Class->GetSuperClass())
{
Result.Appendf(TEXT(" : %s"), *MCPUtils::FormatName(Class->GetSuperClass()));
}
Result.Append(TEXT("\n"));
}
}
};

View File

@@ -1,126 +0,0 @@
#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/EdGraphNode.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "UMCPHandler_SearchWithinBlueprints.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SearchWithinBlueprints : 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, FStringBuilderBase& Result) override
{
int32 Limit = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 200) : 50;
int32 Count = 0;
// Search one blueprint's nodes for the query string.
auto SearchBlueprint = [&](UBlueprint* BP, bool bIsLevelBP)
{
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
{
if (Count >= Limit) return;
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) continue;
Count++;
Result.Appendf(TEXT("blueprint: %s\n"), *MCPUtils::FormatName(BP));
Result.Appendf(TEXT(" graph: %s\n"), *MCPUtils::FormatName(Node->GetGraph()));
Result.Appendf(TEXT(" node: %s\n"), *MCPUtils::FormatName(Node));
Result.Appendf(TEXT(" class: %s\n"), *MCPUtils::FormatName(Node->GetClass()));
if (!FuncName.IsEmpty()) Result.Appendf(TEXT(" function: %s\n"), *FuncName);
if (!EventName.IsEmpty()) Result.Appendf(TEXT(" event: %s\n"), *EventName);
if (!VarName.IsEmpty()) Result.Appendf(TEXT(" variable: %s\n"), *VarName);
if (bIsLevelBP) Result.Append(TEXT(" level-blueprint: true\n"));
Result.Append(TEXT("\n"));
}
};
// Search regular blueprints
MCPAssets<UBlueprint> AllBlueprints;
if (!Path.IsEmpty()) AllBlueprints.Substring(Path);
AllBlueprints.Load();
for (UBlueprint* BP : AllBlueprints.Objects())
{
if (Count >= Limit) break;
SearchBlueprint(BP, false);
}
// Search level blueprints
MCPAssets<UWorld> AllWorlds;
if (!Path.IsEmpty()) AllWorlds.Substring(Path);
AllWorlds.Load();
for (UWorld* World : AllWorlds.Objects())
{
if (Count >= Limit) break;
if (!World || !World->PersistentLevel) continue;
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
if (!LevelBP) continue;
SearchBlueprint(LevelBP, true);
}
Result.Appendf(TEXT("Results: %d\n"), Count);
if (Count >= Limit) Result.Appendf(TEXT("(limit %d reached, use MaxResults to increase)\n"), Limit);
}
};

View File

@@ -1,109 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimSequence.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimStateNode.h"
#include "AnimationStateMachineGraph.h"
#include "UMCPHandler_SetAnimStateAnimation.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetAnimStateAnimation : 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, FStringBuilderBase& Result) override
{
// Resolve the anim blueprint
MCPFetcher F(Result);
UAnimBlueprint* AnimBP = F.Walk(Blueprint).Cast<UAnimBlueprint>();
if (!AnimBP) return;
// Find the state machine graph
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph)
{
Result.Appendf(TEXT("ERROR: State machine graph '%s' not found in %s\n"), *Graph, *MCPUtils::FormatName(AnimBP));
return;
}
// Find the target state
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph)
{
Result.Appendf(TEXT("ERROR: State '%s' has no bound graph\n"), *StateName);
return;
}
// Find the animation asset
MCPAssets<UAnimSequence> AnimAssets;
if (!AnimAssets.Exact(AnimationAsset).Errors(Result).ENone().ETwo().Load()) return;
UAnimSequence* AnimSeq = AnimAssets.Object();
// 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);
MCPUtils::SaveBlueprintPackage(AnimBP);
if (bCreatedNew)
Result.Appendf(TEXT("Created sequence player in state '%s', assigned %s\n"), *StateName, *MCPUtils::FormatName(AnimSeq));
else
Result.Appendf(TEXT("Updated sequence player in state '%s' to %s\n"), *StateName, *MCPUtils::FormatName(AnimSeq));
}
};

View File

@@ -1,220 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.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/BlendSpace.h"
#include "AnimGraphNode_BlendSpacePlayer.h"
#include "EdGraphSchema_K2.h"
#include "AnimStateNode.h"
#include "AnimationStateMachineGraph.h"
#include "K2Node_VariableGet.h"
#include "UMCPHandler_SetAnimStateBlendSpace.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetAnimStateBlendSpace : 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, FStringBuilderBase& Result) override
{
// Load the anim blueprint
MCPAssets<UAnimBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UAnimBlueprint* AnimBP = Assets.Object();
// Find the state machine graph and state
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph) { Result.Appendf(TEXT("ERROR: State machine graph '%s' not found\n"), *Graph); return; }
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph) { Result.Appendf(TEXT("ERROR: State '%s' has no bound graph\n"), *StateName); return; }
// Load the blend space asset
MCPAssets<UBlendSpace> BlendSpaceAssets;
if (!BlendSpaceAssets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return;
UBlendSpace* BlendSpaceAsset = BlendSpaceAssets.Object();
// 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
ConnectToOutputPose(BSNode, InnerGraph);
// Wire X and Y variables if provided
WireVariable(AnimBP, InnerGraph, BSNode, XVariable, TEXT("X"), Result);
WireVariable(AnimBP, InnerGraph, BSNode, YVariable, TEXT("Y"), Result);
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result.Appendf(TEXT("BlendSpacePlayer %s placed in state %s\n"),
*MCPUtils::FormatName(BSNode), *StateName);
if (!bSaved)
Result.Append(TEXT("WARNING: Failed to save package\n"));
}
private:
void ConnectToOutputPose(UAnimGraphNode_BlendSpacePlayer* BSNode, UEdGraph* InnerGraph)
{
// Find the result node (AnimGraphNode_Root or AnimGraphNode_StateResult)
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) return;
// Find the pose output pin on BlendSpacePlayer and input 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) return;
ResultInputPin->BreakAllPinLinks();
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
if (Schema)
Schema->TryCreateConnection(BSOutputPin, ResultInputPin);
}
void WireVariable(UAnimBlueprint* AnimBP, UEdGraph* InnerGraph,
UAnimGraphNode_BlendSpacePlayer* BSNode, const FString& VarName,
const TCHAR* PinName, FStringBuilderBase& Result)
{
if (VarName.IsEmpty()) return;
// 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)
{
if (UClass* GenClass = AnimBP->SkeletonGeneratedClass)
{
if (GenClass->FindPropertyByName(VarFName))
bVarFound = true;
}
}
if (!bVarFound)
{
Result.Appendf(TEXT("WARNING: Variable '%s' not found, skipping %s wire\n"), *VarName, PinName);
return;
}
// 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;
}
}
UEdGraphPin* TargetPin = BSNode->FindPin(FName(PinName));
if (VarOutPin && TargetPin)
{
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
if (Schema)
Schema->TryCreateConnection(VarOutPin, TargetPin);
}
}
};

View File

@@ -1,121 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
#include "UMCPHandler_SetAnimTransitionRule.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetAnimTransitionRule : 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, FStringBuilderBase& Result) override
{
MCPAssets<UAnimBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UAnimBlueprint* AnimBP = Assets.Object();
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph)
{
Result.Appendf(TEXT("ERROR: State machine graph '%s' not found in '%s'\n"), *Graph, *MCPUtils::FormatName(AnimBP));
return;
}
UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromState, ToState);
if (!TransNode)
{
Result.Appendf(TEXT("ERROR: Transition from '%s' to '%s' not found in graph '%s'\n"),
*FromState, *ToState, *Graph);
return;
}
// Update properties
int32 ChangedCount = 0;
TransNode->PreEditChange(nullptr);
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)
{
Result.Append(TEXT("ERROR: No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional\n"));
return;
}
TransNode->PostEditChange();
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
MCPUtils::SaveBlueprintPackage(AnimBP);
Result.Appendf(TEXT("Updated %d properties on transition %s -> %s: %s\n"),
ChangedCount, *FromState, *ToState, *MCPUtils::FormatName(TransNode));
}
};

View File

@@ -1,141 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Animation/AnimSequence.h"
#include "Animation/BlendSpace.h"
#include "UMCPHandler_SetBlendSpaceSamplePoints.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FBlendSpaceSampleEntry
{
GENERATED_BODY()
UPROPERTY()
FString AnimationAsset;
UPROPERTY()
float X = 0.0f;
UPROPERTY()
float Y = 0.0f;
};
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetBlendSpaceSamplePoints : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blend Space asset name or package path"))
FString BlendSpace;
UPROPERTY(meta=(Optional, Description="Display name for the X axis"))
FString AxisXName;
UPROPERTY(meta=(Optional, Description="Display name for the Y axis"))
FString AxisYName;
UPROPERTY(meta=(Optional, Description="Minimum value for X axis"))
float AxisXMin = 0.0f;
UPROPERTY(meta=(Optional, Description="Maximum value for X axis"))
float AxisXMax = 0.0f;
UPROPERTY(meta=(Optional, Description="Minimum value for Y axis"))
float AxisYMin = 0.0f;
UPROPERTY(meta=(Optional, Description="Maximum value for Y axis"))
float AxisYMax = 0.0f;
UPROPERTY(meta=(Optional, Description="Array of sample points, each with animationAsset, x, y"))
FMCPJsonArray Samples;
virtual FString GetDescription() const override
{
return TEXT("Set axis parameters and animation sample points on a Blend Space. "
"Replaces all existing samples.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Load the blend space
MCPAssets<UBlendSpace> Assets;
if (!Assets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return;
UBlendSpace* BS = Assets.Object();
// Set axis parameters
BS->PreEditChange(nullptr);
const FBlendParameter& ParamX = BS->GetBlendParameter(0);
const FBlendParameter& ParamY = BS->GetBlendParameter(1);
// We need to modify BlendParameters directly — use const_cast since there's no setter API
FBlendParameter& MutableParamX = const_cast<FBlendParameter&>(ParamX);
FBlendParameter& MutableParamY = const_cast<FBlendParameter&>(ParamY);
if (!AxisXName.IsEmpty()) MutableParamX.DisplayName = AxisXName;
if (Json->HasField(TEXT("axisXMin"))) MutableParamX.Min = AxisXMin;
if (Json->HasField(TEXT("axisXMax"))) MutableParamX.Max = AxisXMax;
if (!AxisYName.IsEmpty()) MutableParamY.DisplayName = AxisYName;
if (Json->HasField(TEXT("axisYMin"))) MutableParamY.Min = AxisYMin;
if (Json->HasField(TEXT("axisYMax"))) MutableParamY.Max = AxisYMax;
// Clear existing samples (delete from end to start)
int32 NumExisting = BS->GetNumberOfBlendSamples();
for (int32 i = NumExisting - 1; i >= 0; --i)
{
BS->DeleteSample(i);
}
// Add new samples
int32 SamplesSet = 0;
for (const TSharedPtr<FJsonValue>& SampleVal : Samples.Array)
{
FBlendSpaceSampleEntry Entry;
if (!MCPUtils::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal, Result)) return;
UAnimSequence* AnimSeq = nullptr;
if (!Entry.AnimationAsset.IsEmpty())
{
MCPAssets<UAnimSequence> AnimAssets;
if (!AnimAssets.Exact(Entry.AnimationAsset).Errors(Result).ENone().Load()) return;
AnimSeq = AnimAssets.Object();
}
FVector SampleValue(Entry.X, Entry.Y, 0.0f);
if (AnimSeq)
{
BS->AddSample(AnimSeq, SampleValue);
}
else
{
BS->AddSample(SampleValue);
}
SamplesSet++;
}
BS->ValidateSampleData();
BS->PostEditChange();
// Save
BS->MarkPackageDirty();
bool bSaved = MCPUtils::SaveGenericPackage(BS);
Result.Appendf(TEXT("Set %d samples on %s\n"), SamplesSet, *MCPUtils::FormatName(BS));
if (!bSaved)
{
Result.Append(TEXT("WARNING: package save failed\n"));
}
}
};

View File

@@ -1,191 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_SetBlueprintVariableMetadata.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetBlueprintVariableMetadata : 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, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Find the variable using Identifies for consistent name matching
FBPVariableDescription* VarDesc = nullptr;
for (FBPVariableDescription& Var : BP->NewVariables)
{
if (MCPUtils::FormatName(Var) == Variable ||
Var.VarName.ToString().Equals(Variable, ESearchCase::IgnoreCase))
{
VarDesc = &Var;
break;
}
}
if (!VarDesc)
{
Result.Appendf(TEXT("ERROR: Variable '%s' not found in %s.\nAvailable variables:\n"),
*Variable, *MCPUtils::FormatName(BP));
for (const FBPVariableDescription& Var : BP->NewVariables)
{
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Var));
}
return;
}
FName VarFName = VarDesc->VarName;
int32 ChangeCount = 0;
// Category
if (Json->HasField(TEXT("category")))
{
VarDesc->Category = FText::FromString(Category);
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
Result.Appendf(TEXT("Set category to '%s'.\n"), *Category);
ChangeCount++;
}
// Tooltip
if (Json->HasField(TEXT("tooltip")))
{
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), Tooltip);
Result.Appendf(TEXT("Set tooltip to '%s'.\n"), *Tooltip);
ChangeCount++;
}
// Replication
if (Json->HasField(TEXT("replication")))
{
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;
VarDesc->RepNotifyFunc = FName(*FString::Printf(TEXT("OnRep_%s"), *Variable));
}
else
{
Result.Appendf(TEXT("ERROR: Invalid replication value '%s'. Valid: none, replicated, repNotify\n"), *Replication);
return;
}
Result.Appendf(TEXT("Set replication to '%s'.\n"), *Replication);
ChangeCount++;
}
// ExposeOnSpawn
if (Json->HasField(TEXT("exposeOnSpawn")))
{
if (ExposeOnSpawn)
VarDesc->PropertyFlags |= CPF_ExposeOnSpawn;
else
VarDesc->PropertyFlags &= ~CPF_ExposeOnSpawn;
Result.Appendf(TEXT("Set exposeOnSpawn to %s.\n"), ExposeOnSpawn ? TEXT("true") : TEXT("false"));
ChangeCount++;
}
// isPrivate
if (Json->HasField(TEXT("isPrivate")))
{
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr,
TEXT("BlueprintPrivate"), IsPrivate ? TEXT("true") : TEXT("false"));
Result.Appendf(TEXT("Set isPrivate to %s.\n"), IsPrivate ? TEXT("true") : TEXT("false"));
ChangeCount++;
}
// Editability
if (Json->HasField(TEXT("editability")))
{
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"))
{
Result.Appendf(TEXT("ERROR: Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none\n"), *Editability);
return;
}
Result.Appendf(TEXT("Set editability to '%s'.\n"), *Editability);
ChangeCount++;
}
if (ChangeCount == 0)
{
Result.Append(TEXT("ERROR: No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability\n"));
return;
}
BP->PreEditChange(nullptr);
BP->PostEditChange();
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("Updated %d field(s) on %s in %s.%s\n"),
ChangeCount, *VarFName.ToString(), *MCPUtils::FormatName(BP),
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
}
};

View File

@@ -1,162 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UObject/UObjectIterator.h"
#include "UMCPHandler_SetClassDefaultValue.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetClassDefaultValue : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Property name on the Class Default Object"))
FString Property;
UPROPERTY(meta=(Description="New value (parsed according to property type)"))
FString Value;
virtual FString GetDescription() const override
{
return TEXT("Set a default property value on a Blueprint's Class Default Object. "
"Handles class references, object references, and simple types.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Load Blueprint
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
if (!BP->GeneratedClass)
{
Result.Append(TEXT("Error: Blueprint has no GeneratedClass\n"));
return;
}
UObject* CDO = BP->GeneratedClass->GetDefaultObject();
if (!CDO)
{
Result.Append(TEXT("Error: Could not get Class Default Object\n"));
return;
}
FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*Property);
if (!Prop)
{
Result.Appendf(TEXT("Error: Property '%s' not found on %s\n"), *Property, *MCPUtils::FormatName(BP));
return;
}
FString OldValue;
Prop->ExportTextItem_Direct(OldValue, Prop->ContainerPtrToValuePtr<void>(CDO), nullptr, CDO, PPF_None);
FString ActualNewValue;
// Handle class/soft-class properties (TSubclassOf, TSoftClassPtr)
FClassProperty* ClassProp = CastField<FClassProperty>(Prop);
FSoftClassProperty* SoftClassProp = CastField<FSoftClassProperty>(Prop);
if (ClassProp || SoftClassProp)
{
UClass* ResolvedClass = ResolveClass(Value, Result);
if (!ResolvedClass) return;
// Validate meta class compatibility
if (ClassProp)
{
UClass* MetaClass = ClassProp->MetaClass;
if (MetaClass && !ResolvedClass->IsChildOf(MetaClass))
{
Result.Appendf(TEXT("Error: %s is not a subclass of %s (required by property '%s')\n"),
*MCPUtils::FormatName(ResolvedClass), *MCPUtils::FormatName(MetaClass), *Property);
return;
}
ClassProp->SetPropertyValue_InContainer(CDO, ResolvedClass);
}
else
{
FSoftObjectPtr SoftPtr(ResolvedClass);
SoftClassProp->SetPropertyValue_InContainer(CDO, SoftPtr);
}
ActualNewValue = MCPUtils::FormatName(ResolvedClass);
}
// Handle object properties (TObjectPtr, UObject*)
else if (FObjectProperty* ObjProp = CastField<FObjectProperty>(Prop))
{
MCPAssets<UBlueprint> ValueAssets;
if (!ValueAssets.Exact(Value).AllContent().Errors(Result).ENone().ETwo().Load()) return;
UObject* ResolvedObj = nullptr;
if (ValueAssets.Object()->GeneratedClass)
ResolvedObj = ValueAssets.Object()->GeneratedClass->GetDefaultObject();
if (!ResolvedObj)
{
Result.Appendf(TEXT("Error: Could not resolve '%s' to an object\n"), *Value);
return;
}
ObjProp->SetPropertyValue_InContainer(CDO, ResolvedObj);
ActualNewValue = MCPUtils::FormatName(ValueAssets.Object());
}
// Handle simple types via ImportText
else
{
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, Prop->ContainerPtrToValuePtr<void>(CDO), CDO, PPF_None);
if (!ImportResult)
{
Result.Appendf(TEXT("Error: Failed to parse '%s' as %s for property '%s'\n"),
*Value, *Prop->GetCPPType(), *Property);
return;
}
Prop->ExportTextItem_Direct(ActualNewValue, Prop->ContainerPtrToValuePtr<void>(CDO), nullptr, CDO, PPF_None);
}
// Mark modified and save
CDO->MarkPackageDirty();
BP->Modify();
FKismetEditorUtilities::CompileBlueprint(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
Result.Appendf(TEXT("%s %s: %s -> %s\n"), *Prop->GetCPPType(), *Property, *OldValue, *ActualNewValue);
if (!bSaved)
Result.Append(TEXT("Warning: Save failed\n"));
}
private:
// Try to resolve a string to a UClass: first as a C++ class name, then as a Blueprint asset.
UClass* ResolveClass(const FString& ClassName, FStringBuilderBase& Result)
{
// Try as a C++ class name
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C"))
return *It;
}
// Try loading as a Blueprint asset
MCPAssets<UBlueprint> ValueAssets;
if (!ValueAssets.Exact(ClassName).AllContent().Errors(Result).ETwo().Load()) return nullptr;
if (!ValueAssets.Objects().IsEmpty() && ValueAssets.Object()->GeneratedClass)
return ValueAssets.Object()->GeneratedClass;
Result.Appendf(TEXT("Error: Could not resolve '%s' to a class\n"), *ClassName);
return nullptr;
}
};

View File

@@ -1,207 +0,0 @@
#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 "Engine/Texture.h"
#include "UMCPHandler_SetMaterialInstanceParameter.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
if (!Json->HasField(TEXT("value")))
{
Result.Append(TEXT("ERROR: Missing required field: value\n"));
return;
}
// Load the Material Instance
MCPAssets<UMaterialInstanceConstant> Assets;
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
UMaterialInstanceConstant* MI = Assets.Object();
// Determine the parameter type -- explicit or auto-detect from parent
FString TypeStr = Type;
if (TypeStr.IsEmpty())
TypeStr = AutoDetectType(MI);
if (TypeStr.IsEmpty())
{
Result.Appendf(TEXT("ERROR: Could not determine parameter type for '%s'. Specify the 'type' field explicitly (scalar, vector, texture, staticSwitch).\n"),
*ParameterName);
return;
}
FMaterialParameterInfo ParamInfo(*ParameterName);
FString NewValueDescription;
if (TypeStr.Equals(TEXT("scalar"), ESearchCase::IgnoreCase))
{
double FloatValue = Json->GetNumberField(TEXT("value"));
if (!DryRun)
MI->SetScalarParameterValueEditorOnly(ParamInfo, (float)FloatValue);
NewValueDescription = FString::Printf(TEXT("%g"), FloatValue);
}
else if (TypeStr.Equals(TEXT("vector"), ESearchCase::IgnoreCase))
{
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
{
Result.Append(TEXT("ERROR: For vector parameters, 'value' must be an object with r, g, b (and optional a) fields.\n"));
return;
}
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("(%.3f, %.3f, %.3f, %.3f)"), R, G, B, A);
}
else if (TypeStr.Equals(TEXT("texture"), ESearchCase::IgnoreCase))
{
FString TexturePath = Json->GetStringField(TEXT("value"));
if (TexturePath.IsEmpty())
{
Result.Append(TEXT("ERROR: For texture parameters, 'value' must be a texture asset path string.\n"));
return;
}
MCPAssets<UTexture> TexAssets;
if (!TexAssets.Exact(TexturePath).AllContent().Errors(Result).ENone().ETwo().Load()) return;
UTexture* TextureObj = TexAssets.Object();
if (!DryRun)
MI->SetTextureParameterValueEditorOnly(ParamInfo, TextureObj);
NewValueDescription = MCPUtils::FormatName(TextureObj);
}
else if (TypeStr.Equals(TEXT("staticSwitch"), ESearchCase::IgnoreCase))
{
bool bSwitchValue = Json->GetBoolField(TEXT("value"));
if (!DryRun)
{
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)
{
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
{
Result.Appendf(TEXT("ERROR: Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch\n"), *TypeStr);
return;
}
if (!DryRun)
{
MI->PreEditChange(nullptr);
MI->PostEditChange();
MI->MarkPackageDirty();
MCPUtils::SaveGenericPackage(MI);
}
if (DryRun)
Result.Appendf(TEXT("[DRY RUN] Would set %s \"%s\" = %s on %s\n"), *TypeStr, *ParameterName, *NewValueDescription, *MCPUtils::FormatName(MI));
else
Result.Appendf(TEXT("Set %s \"%s\" = %s on %s\n"), *TypeStr, *ParameterName, *NewValueDescription, *MCPUtils::FormatName(MI));
}
private:
// Auto-detect parameter type by examining the base material's expressions.
FString AutoDetectType(UMaterialInstanceConstant* MI)
{
UMaterial* BaseMat = MI->GetMaterial();
if (!BaseMat) return FString();
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
{
if (!Expr) continue;
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
{
if (SP->ParameterName.ToString() == ParameterName)
return TEXT("scalar");
}
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
{
if (VP->ParameterName.ToString() == ParameterName)
return TEXT("vector");
}
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
{
if (TP->ParameterName.ToString() == ParameterName)
return TEXT("texture");
}
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
{
if (SSP->ParameterName.ToString() == ParameterName)
return TEXT("staticSwitch");
}
}
return FString();
}
};

View File

@@ -1,138 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "MaterialDomain.h"
#include "UMCPHandler_SetMaterialProperty.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetMaterialProperty : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Material name or package path"))
FString Material;
UPROPERTY(meta=(Description="Property name to set (domain, blendMode, twoSided, shadingModel, opacity, opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, ditheredLODTransition, bAllowNegativeEmissiveColor)"))
FString Property;
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
bool DryRun = false;
virtual FString GetDescription() const override
{
return TEXT("Set a top-level material property such as domain, blend mode, shading model, or usage flags. "
"The 'value' field in the JSON payload provides the new value.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (!Json->HasField(TEXT("value")))
{
Result.Append(TEXT("ERROR: Missing required field: value\n"));
return;
}
// Load material
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterial* Mat = Assets.Object();
FString OldValue;
FString NewValue;
// Helper: apply a bool property change.
auto SetBool = [&](auto Getter, auto Setter) {
bool bValue = Json->GetBoolField(TEXT("value"));
OldValue = Getter() ? TEXT("true") : TEXT("false");
NewValue = bValue ? TEXT("true") : TEXT("false");
if (!DryRun) { Mat->PreEditChange(nullptr); Setter(bValue); Mat->PostEditChange(); }
};
if (Property == TEXT("domain"))
{
FString ValueStr = Json->GetStringField(TEXT("value"));
OldValue = MCPUtils::EnumToString(Mat->MaterialDomain, TEXT("MD_"));
EMaterialDomain NewDomain;
if (!MCPUtils::StringToEnum(ValueStr, NewDomain, Result, TEXT("MD_"))) return;
NewValue = MCPUtils::EnumToString(NewDomain, TEXT("MD_"));
if (!DryRun) { Mat->PreEditChange(nullptr); Mat->MaterialDomain = NewDomain; Mat->PostEditChange(); }
}
else if (Property == TEXT("blendMode"))
{
FString ValueStr = Json->GetStringField(TEXT("value"));
OldValue = MCPUtils::EnumToString(Mat->BlendMode, TEXT("BLEND_"));
EBlendMode NewBlend;
if (!MCPUtils::StringToEnum(ValueStr, NewBlend, Result, TEXT("BLEND_"))) return;
NewValue = MCPUtils::EnumToString(NewBlend, TEXT("BLEND_"));
if (!DryRun) { Mat->PreEditChange(nullptr); Mat->BlendMode = NewBlend; Mat->PostEditChange(); }
}
else if (Property == TEXT("shadingModel"))
{
FString ValueStr = Json->GetStringField(TEXT("value"));
OldValue = MCPUtils::EnumToString(Mat->GetShadingModels().GetFirstShadingModel(), TEXT("MSM_"));
EMaterialShadingModel NewModel;
if (!MCPUtils::StringToEnum(ValueStr, NewModel, Result, TEXT("MSM_"))) return;
NewValue = MCPUtils::EnumToString(NewModel, TEXT("MSM_"));
if (!DryRun) { Mat->PreEditChange(nullptr); Mat->SetShadingModel(NewModel); Mat->PostEditChange(); }
}
else if (Property == TEXT("opacity") || Property == TEXT("opacityMaskClipValue"))
{
double OpacityValue = Json->GetNumberField(TEXT("value"));
OldValue = FString::Printf(TEXT("%g"), Mat->OpacityMaskClipValue);
NewValue = FString::Printf(TEXT("%g"), OpacityValue);
if (!DryRun) { Mat->PreEditChange(nullptr); Mat->OpacityMaskClipValue = (float)OpacityValue; Mat->PostEditChange(); }
}
else if (Property == TEXT("twoSided"))
{
SetBool([&]{ return Mat->TwoSided; }, [&](bool v){ Mat->TwoSided = v ? 1 : 0; });
}
else if (Property == TEXT("bUsedWithSkeletalMesh"))
{
SetBool([&]{ return Mat->bUsedWithSkeletalMesh; }, [&](bool v){ Mat->bUsedWithSkeletalMesh = v ? 1 : 0; });
}
else if (Property == TEXT("bUsedWithMorphTargets"))
{
SetBool([&]{ return Mat->bUsedWithMorphTargets; }, [&](bool v){ Mat->bUsedWithMorphTargets = v ? 1 : 0; });
}
else if (Property == TEXT("bUsedWithNiagaraSprites"))
{
SetBool([&]{ return Mat->bUsedWithNiagaraSprites; }, [&](bool v){ Mat->bUsedWithNiagaraSprites = v ? 1 : 0; });
}
else if (Property == TEXT("ditheredLODTransition") || Property == TEXT("DitheredLODTransition"))
{
SetBool([&]{ return Mat->DitheredLODTransition; }, [&](bool v){ Mat->DitheredLODTransition = v ? 1 : 0; });
}
else if (Property == TEXT("bAllowNegativeEmissiveColor"))
{
SetBool([&]{ return Mat->bAllowNegativeEmissiveColor; }, [&](bool v){ Mat->bAllowNegativeEmissiveColor = v ? 1 : 0; });
}
else
{
Result.Appendf(TEXT("ERROR: Unknown property '%s'. Valid: domain, blendMode, twoSided, shadingModel, "
"opacity, opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, "
"bUsedWithNiagaraSprites, ditheredLODTransition, bAllowNegativeEmissiveColor\n"), *Property);
return;
}
// Save if not dry run
if (!DryRun)
{
if (!MCPUtils::SaveMaterialPackage(Mat))
Result.Append(TEXT("WARNING: Package save failed\n"));
}
Result.Appendf(TEXT("%s%s: %s -> %s\n"),
DryRun ? TEXT("[DRY RUN] ") : TEXT(""),
*Property, *OldValue, *NewValue);
}
};

View File

@@ -1,53 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphNode.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_SetNodeComment.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetNodeComment : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the node, e.g. /Game/Foo,node:MyNode"))
FString Node;
UPROPERTY(meta=(Description="Comment text to set"))
FString Comment;
virtual FString GetDescription() const override
{
return TEXT("Set a node's comment text. Makes the comment bubble visible if non-empty.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
FoundNode->NodeComment = Comment;
// Make the comment bubble visible if setting a non-empty comment
if (!Comment.IsEmpty())
{
FoundNode->bCommentBubbleVisible = true;
FoundNode->bCommentBubblePinned = true;
}
UBlueprint* BP = FBlueprintEditorUtils::FindBlueprintForNodeChecked(FoundNode);
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
Result.Appendf(TEXT("Comment set on %s\n"), *MCPUtils::FormatName(FoundNode));
}
};

View File

@@ -1,76 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraphNode.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_SetNodePositions.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FMoveNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
int32 X = 0;
UPROPERTY()
int32 Y = 0;
};
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetNodePositions : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Array of {node, x, y} objects"))
FMCPJsonArray Nodes;
virtual FString GetDescription() const override
{
return TEXT("Reposition one or more nodes in a Blueprint graph.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
int32 SuccessCount = 0;
for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
{
FMoveNodeEntry Entry;
if (!MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal, Result)) continue;
MCPFetcher FN(Result, BP);
UEdGraphNode* Node = FN.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) continue;
Node->NodePosX = Entry.X;
Node->NodePosY = Entry.Y;
Result.Appendf(TEXT("Moved %s to (%d, %d)\n"), *MCPUtils::FormatName(Node), Entry.X, Entry.Y);
SuccessCount++;
}
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
Result.Appendf(TEXT("Moved %d/%d nodes.\n"), SuccessCount, Nodes.Array.Num());
}
};

View File

@@ -1,85 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_SetPinDefaultValues.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSetPinDefaultEntry
{
GENERATED_BODY()
UPROPERTY()
FString Pin;
UPROPERTY()
FString Value;
};
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetPinDefaultValues : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Array of {pin, value} objects. Pin is a path like /Game/Foo,graph:EventGraph,node:MyNode,pin:InputPin"))
FMCPJsonArray Pins;
virtual FString GetDescription() const override
{
return TEXT("Set the default value of input pins on nodes.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
int32 SuccessCount = 0;
int32 TotalCount = Pins.Array.Num();
TSet<UEdGraphNode*> ModifiedNodes;
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
{
FSetPinDefaultEntry Entry;
if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, Result))
continue;
MCPFetcher F(Result);
UEdGraphPin* Pin = F.Walk(Entry.Pin).Cast<UEdGraphPin>();
if (!Pin) continue;
UEdGraphNode* Node = Pin->GetOwningNode();
if (Pin->Direction != EGPD_Input)
{
Result.Appendf(TEXT("error: %s is an output pin\n"), *MCPUtils::FormatName(Pin));
continue;
}
const UEdGraphSchema* Schema = Node->GetGraph()->GetSchema();
if (Schema)
Schema->TrySetDefaultValue(*Pin, Entry.Value);
else
Pin->DefaultValue = Entry.Value;
SuccessCount++;
ModifiedNodes.Add(Node);
F.PostEdit();
}
for (UEdGraphNode* Node : ModifiedNodes)
{
Node->ReconstructNode();
}
Result.Appendf(TEXT("Set %d/%d pin defaults.\n"), SuccessCount, TotalCount);
}
};

View File

@@ -1,94 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "UMCPHandler_SetProperties.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SetProperties : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="MCPFetcher path to the object (e.g. /Game/Materials/M_Gold or /Game/Tangibles/TAN_Char,component:Mesh0)"))
FString Path;
UPROPERTY(meta=(Description="Object mapping property names to new values in Unreal text format"))
FMCPJsonObject Properties;
virtual FString GetDescription() const override
{
return TEXT("Set one or more editable properties on an object resolved via MCPFetcher path. "
"Properties is a JSON object like {\"TwoSided\": \"true\", \"BlendMode\": \"BLEND_Translucent\"}.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
// Resolve the path to an object and get its editable template.
MCPFetcher F(Result);
UObject* Template = F.Walk(Path).Template().Cast<UObject>();
if (!Template) return;
if (!Properties.Json || Properties.Json->Values.Num() == 0)
{
Result.Append(TEXT("Error: No properties specified\n"));
return;
}
// Validation pass — check all properties before modifying anything.
for (const auto& Pair : Properties.Json->Values)
{
FProperty* Prop = MCPUtils::FindPropertyByName(Template, Pair.Key, Result);
if (!Prop) return;
if (!Prop->HasAnyPropertyFlags(CPF_Edit))
{
Result.Appendf(TEXT("Error: Property '%s' is not editable (no Edit flag)\n"), *Pair.Key);
return;
}
FString ValueStr;
if (!Pair.Value->TryGetString(ValueStr))
{
Result.Appendf(TEXT("Error: Value for '%s' must be a string\n"), *Pair.Key);
return;
}
}
// Apply all changes in a single Pre/PostEditChange bracket.
F.PreEdit();
int32 SuccessCount = 0;
for (const auto& Pair : Properties.Json->Values)
{
FProperty* Prop = MCPUtils::FindPropertyByName(Template, Pair.Key);
FString ValueStr;
Pair.Value->TryGetString(ValueStr);
FString OldValue = MCPUtils::GetPropertyValueText(Template, Prop);
if (!MCPUtils::SetPropertyValueText(Template, Prop, ValueStr, Result))
continue;
FString NewValue = MCPUtils::GetPropertyValueText(Template, Prop);
Result.Appendf(TEXT("%s: %s -> %s\n"), *MCPUtils::FormatName(Prop), *OldValue, *NewValue);
SuccessCount++;
}
F.PostEdit();
// Save.
bool bSaved = MCPUtils::SaveGenericPackage(Template);
Result.Appendf(TEXT("Set %d/%d properties.\n"), SuccessCount, Properties.Json->Values.Num());
if (!bSaved)
Result.Append(TEXT("Warning: Save failed\n"));
}
};

View File

@@ -1,93 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "UMCPHandler_ShowCommands.generated.h"
UCLASS(meta=(Group="Documentation"))
class UMCPHandler_ShowCommands : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring filter for command names"))
FString Query;
UPROPERTY(meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
bool Verbose = false;
virtual FString GetDescription() const override
{
return TEXT("List all available commands with their descriptions.");
}
void EmitGroup(const FString& GroupName, const TArray<UClass*>& Classes, FStringBuilderBase& Result)
{
Result.Appendf(TEXT("\n=== %s ===\n\n"), *GroupName);
for (UClass* Class : Classes)
{
if (Verbose)
{
MCPUtils::FormatCommandHelp(Class, Result);
continue;
}
Result.Append(MCPUtils::GetToolName(Class));
Result.Append(TEXT("("));
bool bFirst = true;
for (TFieldIterator<FProperty> PropIt(Class, EFieldIterationFlags::None); PropIt; ++PropIt)
{
if (!bFirst) Result.Append(TEXT(","));
bFirst = false;
if (PropIt->HasMetaData(TEXT("Optional"))) Result.Append(TEXT("?"));
Result.Append(MCPUtils::FormatPropertyType(*PropIt));
Result.Append(TEXT(" "));
Result.Append(MCPUtils::PropertyNameToJsonKey(PropIt->GetName()));
}
Result.Append(TEXT(")\n"));
}
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
FString QueryLower = Query.ToLower();
// Group handlers by their Group metadata.
TMap<FString, TArray<UClass*>> Groups;
for (UClass* Class : MCPUtils::CollectHandlerClasses())
{
FString ToolName = MCPUtils::GetToolName(Class);
if (!ToolName.ToLower().Contains(QueryLower))
continue;
FString Group = Class->GetMetaData(TEXT("Group"));
if (Group.IsEmpty()) Group = TEXT("Unclassified");
Groups.FindOrAdd(Group).Add(Class);
}
// Emit high-priority groups first, in order.
static const TCHAR* HighPriority[] = { TEXT("Documentation") };
for (const TCHAR* HP : HighPriority)
{
TArray<UClass*> Classes;
if (Groups.RemoveAndCopyValue(HP, Classes))
EmitGroup(HP, Classes, Result);
}
// Emit remaining groups.
for (const auto& Pair : Groups)
EmitGroup(Pair.Key, Pair.Value, Result);
// Append Path documentation.
Result.Append(TEXT("\n"));
Result.Append(TEXT("Some commands take a Path parameter. A Path starts with an asset\n"));
Result.Append(TEXT("package path (e.g. /Game/Widgets/WB_Hotkeys), followed by zero or\n"));
Result.Append(TEXT("more comma-separated steps that navigate into the asset:\n"));
Result.Append(TEXT("\n"));
for (const MCPFetcher::FWalker& W : MCPFetcher::GetWalkerTable())
{
Result.Appendf(TEXT(" %s — %s\n"), W.Key, W.Description);
}
Result.Append(TEXT("\nExample: /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self_Reference_03,pin:Result\n"));
}
};

View File

@@ -1,114 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphSchema.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_SpawnNodes.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSpawnNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString ActionName;
UPROPERTY()
int32 PosX = 0;
UPROPERTY()
int32 PosY = 0;
};
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_SpawnNodes : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a graph, e.g. /Game/Foo,graph:EventGraph"))
FString Graph;
UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use SearchSpawnableNodeTypes to find action names."))
FMCPJsonArray Nodes;
virtual FString GetDescription() const override
{
return TEXT("Create nodes in any graph (Blueprint, Material, etc.) using the editor's action database. "
"Can create ANY node type that appears in the editor's right-click menu. "
"Use SearchSpawnableNodeTypes first to find the exact action name.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
int32 SuccessCount = 0;
int32 TotalCount = Nodes.Array.Num();
for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
{
FSpawnNodeEntry Entry;
if (!MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal, Result))
continue;
// Find the action by exact full name
TArray<TSharedPtr<FEdGraphSchemaAction>> Matches = MCPUtils::SearchGraphActions(TargetGraph, Entry.ActionName, 0, /*ExactMatch=*/true);
if (Matches.Num() == 0)
{
Result.Appendf(TEXT("ERROR: No action found matching '%s'. Use SearchSpawnableNodeTypes to find available actions.\n"),
*Entry.ActionName);
continue;
}
if (Matches.Num() > 1)
{
Result.Appendf(TEXT("ERROR: Ambiguous: %d actions match '%s'.\n"),
Matches.Num(), *Entry.ActionName);
continue;
}
// Perform the action
FVector2D Location(Entry.PosX, Entry.PosY);
UEdGraphNode* NewNode = Matches[0]->PerformAction(TargetGraph, nullptr, Location, /*bSelectNewNode=*/false);
if (!NewNode)
{
Result.Appendf(TEXT("ERROR: PerformAction returned null for '%s'.\n"), *Entry.ActionName);
continue;
}
if (!NewNode->NodeGuid.IsValid())
NewNode->CreateNewGuid();
Result.Appendf(TEXT("Spawned: %s (%s)\n"),
*MCPUtils::FormatName(NewNode), *MCPUtils::FormatName(NewNode->GetClass()));
SuccessCount++;
}
// Mark the owning asset as modified
UBlueprint* BP = Cast<UBlueprint>(TargetGraph->GetOuter());
if (BP)
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
TargetGraph->NotifyGraphChanged();
UObject* Outer = TargetGraph->GetOuter();
if (Outer)
Outer->MarkPackageDirty();
Result.Appendf(TEXT("Spawned %d/%d nodes.\n"), SuccessCount, TotalCount);
}
};

View File

@@ -1,80 +0,0 @@
#include "Handlers/UMCPHandler_AddAnimStateToMachine.h"
#include "Handlers/UMCPHandler_AddAnimStateTransition.h"
#include "Handlers/UMCPHandler_AddBlueprintComponent.h"
#include "Handlers/UMCPHandler_AddBlueprintInterface.h"
#include "Handlers/UMCPHandler_AddBlueprintVariable.h"
#include "Handlers/UMCPHandler_AddEventDispatcher.h"
#include "Handlers/UMCPHandler_AddFunctionParameter.h"
#include "Handlers/UMCPHandler_AddStructField.h"
#include "Handlers/UMCPHandler_BackupAsset.h"
#include "Handlers/UMCPHandler_ChangeBlueprintVariableType.h"
#include "Handlers/UMCPHandler_ChangeFunctionParameterType.h"
#include "Handlers/UMCPHandler_ChangeStructNodeType.h"
#include "Handlers/UMCPHandler_CheckPinConnectionCompatibility.h"
#include "Handlers/UMCPHandler_CompileBlueprint.h"
#include "Handlers/UMCPHandler_CompileMaterial.h"
#include "Handlers/UMCPHandler_ConnectPins.h"
#include "Handlers/UMCPHandler_CreateAnimBlueprintAsset.h"
#include "Handlers/UMCPHandler_CreateBlendSpaceAsset.h"
#include "Handlers/UMCPHandler_CreateBlueprintAsset.h"
#include "Handlers/UMCPHandler_CreateBlueprintGraph.h"
#include "Handlers/UMCPHandler_CreateEnumAsset.h"
#include "Handlers/UMCPHandler_CreateMaterialAsset.h"
#include "Handlers/UMCPHandler_CreateMaterialFunctionAsset.h"
#include "Handlers/UMCPHandler_CreateMaterialInstanceAsset.h"
#include "Handlers/UMCPHandler_CreateStructAsset.h"
#include "Handlers/UMCPHandler_DeleteAsset.h"
#include "Handlers/UMCPHandler_DeleteBlueprintGraph.h"
#include "Handlers/UMCPHandler_DeleteNodeFromGraph.h"
#include "Handlers/UMCPHandler_DiffTwoBlueprints.h"
#include "Handlers/UMCPHandler_DisconnectPins.h"
#include "Handlers/UMCPHandler_DumpBlueprint.h"
#include "Handlers/UMCPHandler_DumpProperties.h"
#include "Handlers/UMCPHandler_DumpGraphs.h"
#include "Handlers/UMCPHandler_DumpMaterialInstanceParameters.h"
#include "Handlers/UMCPHandler_DuplicateNodesInGraph.h"
#include "Handlers/UMCPHandler_FindAssetReferences.h"
#include "Handlers/UMCPHandler_FindMaterialReferences.h"
#include "Handlers/UMCPHandler_GetNodeComment.h"
#include "Handlers/UMCPHandler_GetPinDetails.h"
#include "Handlers/UMCPHandler_ListAnimSlotNames.h"
#include "Handlers/UMCPHandler_ListAnimSyncGroups.h"
#include "Handlers/UMCPHandler_ListBlueprintAssets.h"
#include "Handlers/UMCPHandler_ListOpenAssetEditors.h"
#include "Handlers/UMCPHandler_ListBlueprintComponents.h"
#include "Handlers/UMCPHandler_ListBlueprintInterfaces.h"
#include "Handlers/UMCPHandler_ListClassProperties.h"
#include "Handlers/UMCPHandler_ListEventDispatchers.h"
#include "Handlers/UMCPHandler_OpenAssetEditor.h"
#include "Handlers/UMCPHandler_RefreshAllNodesInGraph.h"
#include "Handlers/UMCPHandler_RemoveAnimStateFromMachine.h"
#include "Handlers/UMCPHandler_RemoveBlueprintComponent.h"
#include "Handlers/UMCPHandler_RemoveBlueprintInterface.h"
#include "Handlers/UMCPHandler_RemoveBlueprintVariable.h"
#include "Handlers/UMCPHandler_RemoveFunctionParameter.h"
#include "Handlers/UMCPHandler_RemoveStructField.h"
#include "Handlers/UMCPHandler_RenameAsset.h"
#include "Handlers/UMCPHandler_RenameBlueprintGraph.h"
#include "Handlers/UMCPHandler_ReparentBlueprint.h"
#include "Handlers/UMCPHandler_ReparentMaterialInstance.h"
#include "Handlers/UMCPHandler_ReplaceFunctionCallsInBlueprint.h"
#include "Handlers/UMCPHandler_RestoreAsset.h"
#include "Handlers/UMCPHandler_SearchAssets.h"
#include "Handlers/UMCPHandler_SearchSpawnableNodeTypes.h"
#include "Handlers/UMCPHandler_SearchTypeUsageInBlueprints.h"
#include "Handlers/UMCPHandler_SearchUnrealClasses.h"
#include "Handlers/UMCPHandler_SearchWithinBlueprints.h"
#include "Handlers/UMCPHandler_SetAnimStateAnimation.h"
#include "Handlers/UMCPHandler_SetAnimStateBlendSpace.h"
#include "Handlers/UMCPHandler_SetAnimTransitionRule.h"
#include "Handlers/UMCPHandler_SetBlendSpaceSamplePoints.h"
#include "Handlers/UMCPHandler_SetBlueprintVariableMetadata.h"
#include "Handlers/UMCPHandler_SetClassDefaultValue.h"
#include "Handlers/UMCPHandler_SetMaterialInstanceParameter.h"
#include "Handlers/UMCPHandler_SetMaterialProperty.h"
#include "Handlers/UMCPHandler_SetNodeComment.h"
#include "Handlers/UMCPHandler_SetProperties.h"
#include "Handlers/UMCPHandler_SetNodePositions.h"
#include "Handlers/UMCPHandler_SetPinDefaultValues.h"
#include "Handlers/UMCPHandler_ShowCommands.h"
#include "Handlers/UMCPHandler_SpawnNodes.h"

View File

@@ -1659,11 +1659,30 @@ TArray<UClass*> MCPUtils::CollectHandlerClasses()
FString MCPUtils::GetToolName(UClass* HandlerClass)
{
FString Name = HandlerClass->GetName();
// Strip "MCP_" prefix
if (Name.StartsWith(TEXT("MCP_")))
Name = Name.Mid(4);
// Strip the remaining underscore between group and action (e.g. "Blueprint_Create" -> "BlueprintCreate")
int32 UnderscoreIdx;
if (Name.FindChar(TEXT('_'), UnderscoreIdx))
{
return Name.Mid(UnderscoreIdx + 1);
}
Name = Name.Left(UnderscoreIdx) + Name.Mid(UnderscoreIdx + 1);
return Name;
}
// ============================================================
// GetToolGroup — derive group name from handler class name
// ============================================================
FString MCPUtils::GetToolGroup(UClass* HandlerClass)
{
FString Name = HandlerClass->GetName();
// Strip "MCP_" prefix
if (Name.StartsWith(TEXT("MCP_")))
Name = Name.Mid(4);
// Everything before the underscore is the group
int32 UnderscoreIdx;
if (Name.FindChar(TEXT('_'), UnderscoreIdx))
return Name.Left(UnderscoreIdx);
return Name;
}