Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_MaterialMutation.h
2026-03-08 01:47:15 -05:00

1594 lines
55 KiB
C++

#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/MaterialFunction.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionTextureObjectParameter.h"
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
#include "Materials/MaterialExpressionConstant.h"
#include "Materials/MaterialExpressionConstant3Vector.h"
#include "Materials/MaterialExpressionConstant4Vector.h"
#include "Materials/MaterialExpressionTextureSample.h"
#include "Materials/MaterialExpressionTextureCoordinate.h"
#include "Materials/MaterialExpressionComponentMask.h"
#include "Materials/MaterialExpressionCustom.h"
#include "Materials/MaterialExpressionFunctionInput.h"
#include "Materials/MaterialExpressionFunctionOutput.h"
#include "Materials/MaterialExpressionMaterialFunctionCall.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "Factories/MaterialFactoryNew.h"
#include "Factories/MaterialFunctionFactoryNew.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "Misc/Guid.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "UObject/SavePackage.h"
#include "UObject/UObjectIterator.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "MCPHandlers_MaterialMutation.generated.h"
// SEH wrapper defined in BlueprintMCPServer.cpp — catches crashes from abstract/invalid expression classes.
// Wraps the entire creation + registration + PostEditChange flow so that if the expression crashes
// (e.g. UMaterialExpressionParameter), it cleans up and returns -1 instead of terminating the process.
#if PLATFORM_WINDOWS
extern int32 TryAddMaterialExpressionSEH(
UObject* Owner, UClass* ExprClass, UMaterial* Material, UMaterialFunction* MatFunc,
int32 PosX, int32 PosY, UMaterialExpression** OutExpr);
#endif
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="create_material_asset"))
class UMCPHandler_CreateMaterial : 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, FJsonObject* Result) override
{
if (!PackagePath.StartsWith(TEXT("/Game")))
{
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
}
// Check if asset already exists
MCPAssets<UMaterial> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material '%s' in '%s'"), *Name, *PackagePath);
// Create via IAssetTools + factory
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
UMaterialFactoryNew* Factory = NewObject<UMaterialFactoryNew>();
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterial::StaticClass(), Factory);
if (!NewAsset)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material '%s' in '%s'"), *Name, *PackagePath));
}
UMaterial* MaterialObj = Cast<UMaterial>(NewAsset);
if (!MaterialObj)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterial"));
}
// Apply optional properties
bool bHasTwoSided = Json->HasField(TEXT("twoSided"));
MaterialObj->PreEditChange(nullptr);
// Parse domain
if (!Domain.IsEmpty())
{
if (Domain == TEXT("Surface"))
MaterialObj->MaterialDomain = MD_Surface;
else if (Domain == TEXT("DeferredDecal"))
MaterialObj->MaterialDomain = MD_DeferredDecal;
else if (Domain == TEXT("LightFunction"))
MaterialObj->MaterialDomain = MD_LightFunction;
else if (Domain == TEXT("Volume"))
MaterialObj->MaterialDomain = MD_Volume;
else if (Domain == TEXT("PostProcess"))
MaterialObj->MaterialDomain = MD_PostProcess;
else if (Domain == TEXT("UI"))
MaterialObj->MaterialDomain = MD_UI;
}
// Parse blend mode
if (!BlendMode.IsEmpty())
{
if (BlendMode == TEXT("Opaque"))
MaterialObj->BlendMode = BLEND_Opaque;
else if (BlendMode == TEXT("Masked"))
MaterialObj->BlendMode = BLEND_Masked;
else if (BlendMode == TEXT("Translucent"))
MaterialObj->BlendMode = BLEND_Translucent;
else if (BlendMode == TEXT("Additive"))
MaterialObj->BlendMode = BLEND_Additive;
else if (BlendMode == TEXT("Modulate"))
MaterialObj->BlendMode = BLEND_Modulate;
}
if (bHasTwoSided)
{
MaterialObj->TwoSided = TwoSided;
}
MaterialObj->PostEditChange();
// Save
bool bSaved = MCPUtils::SaveMaterialPackage(MaterialObj);
// Map domain back to string for response
auto DomainToString = [](EMaterialDomain InDomain) -> FString
{
switch (InDomain)
{
case MD_Surface: return TEXT("Surface");
case MD_DeferredDecal: return TEXT("DeferredDecal");
case MD_LightFunction: return TEXT("LightFunction");
case MD_Volume: return TEXT("Volume");
case MD_PostProcess: return TEXT("PostProcess");
case MD_UI: return TEXT("UI");
default: return TEXT("Surface");
}
};
auto BlendModeToString = [](EBlendMode Mode) -> FString
{
switch (Mode)
{
case BLEND_Opaque: return TEXT("Opaque");
case BLEND_Masked: return TEXT("Masked");
case BLEND_Translucent: return TEXT("Translucent");
case BLEND_Additive: return TEXT("Additive");
case BLEND_Modulate: return TEXT("Modulate");
default: return TEXT("Opaque");
}
};
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material '%s' (saved: %s)"),
*Name, bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("path"), MaterialObj->GetPathName());
Result->SetStringField(TEXT("domain"), DomainToString(MaterialObj->MaterialDomain));
Result->SetStringField(TEXT("blendMode"), BlendModeToString(MaterialObj->BlendMode));
Result->SetBoolField(TEXT("twoSided"), MaterialObj->TwoSided != 0);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="set_material_property"))
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, FJsonObject* Result) override
{
if (!Json->HasField(TEXT("value")))
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
}
// Load material
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterial* MaterialObj = Assets.Object();
FString OldValue;
FString NewValue;
// Helper lambdas for converting enum values to strings
auto DomainToString = [](EMaterialDomain Domain) -> FString
{
switch (Domain)
{
case MD_Surface: return TEXT("Surface");
case MD_DeferredDecal: return TEXT("DeferredDecal");
case MD_LightFunction: return TEXT("LightFunction");
case MD_Volume: return TEXT("Volume");
case MD_PostProcess: return TEXT("PostProcess");
case MD_UI: return TEXT("UI");
default: return TEXT("Unknown");
}
};
auto BlendModeToString = [](EBlendMode Mode) -> FString
{
switch (Mode)
{
case BLEND_Opaque: return TEXT("Opaque");
case BLEND_Masked: return TEXT("Masked");
case BLEND_Translucent: return TEXT("Translucent");
case BLEND_Additive: return TEXT("Additive");
case BLEND_Modulate: return TEXT("Modulate");
default: return TEXT("Unknown");
}
};
auto ShadingModelToString = [](EMaterialShadingModel Model) -> FString
{
switch (Model)
{
case MSM_Unlit: return TEXT("Unlit");
case MSM_DefaultLit: return TEXT("DefaultLit");
case MSM_Subsurface: return TEXT("Subsurface");
case MSM_PreintegratedSkin: return TEXT("PreintegratedSkin");
case MSM_ClearCoat: return TEXT("ClearCoat");
case MSM_SubsurfaceProfile: return TEXT("SubsurfaceProfile");
case MSM_TwoSidedFoliage: return TEXT("TwoSidedFoliage");
case MSM_Hair: return TEXT("Hair");
case MSM_Cloth: return TEXT("Cloth");
case MSM_Eye: return TEXT("Eye");
default: return TEXT("DefaultLit");
}
};
if (Property == TEXT("domain"))
{
FString ValueStr = Json->GetStringField(TEXT("value"));
OldValue = DomainToString(MaterialObj->MaterialDomain);
EMaterialDomain NewDomain = MaterialObj->MaterialDomain;
if (ValueStr == TEXT("Surface")) NewDomain = MD_Surface;
else if (ValueStr == TEXT("DeferredDecal")) NewDomain = MD_DeferredDecal;
else if (ValueStr == TEXT("LightFunction")) NewDomain = MD_LightFunction;
else if (ValueStr == TEXT("Volume")) NewDomain = MD_Volume;
else if (ValueStr == TEXT("PostProcess")) NewDomain = MD_PostProcess;
else if (ValueStr == TEXT("UI")) NewDomain = MD_UI;
else
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Invalid domain '%s'. Valid values: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"),
*ValueStr));
}
NewValue = ValueStr;
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->MaterialDomain = NewDomain;
MaterialObj->PostEditChange();
}
}
else if (Property == TEXT("blendMode"))
{
FString ValueStr = Json->GetStringField(TEXT("value"));
OldValue = BlendModeToString(MaterialObj->BlendMode);
EBlendMode NewBlend = MaterialObj->BlendMode;
if (ValueStr == TEXT("Opaque")) NewBlend = BLEND_Opaque;
else if (ValueStr == TEXT("Masked")) NewBlend = BLEND_Masked;
else if (ValueStr == TEXT("Translucent")) NewBlend = BLEND_Translucent;
else if (ValueStr == TEXT("Additive")) NewBlend = BLEND_Additive;
else if (ValueStr == TEXT("Modulate")) NewBlend = BLEND_Modulate;
else
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Invalid blendMode '%s'. Valid values: Opaque, Masked, Translucent, Additive, Modulate"),
*ValueStr));
}
NewValue = ValueStr;
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->BlendMode = NewBlend;
MaterialObj->PostEditChange();
}
}
else if (Property == TEXT("twoSided"))
{
bool bValue = Json->GetBoolField(TEXT("value"));
OldValue = MaterialObj->TwoSided ? TEXT("true") : TEXT("false");
NewValue = bValue ? TEXT("true") : TEXT("false");
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->TwoSided = bValue ? 1 : 0;
MaterialObj->PostEditChange();
}
}
else if (Property == TEXT("shadingModel"))
{
FString ValueStr = Json->GetStringField(TEXT("value"));
OldValue = ShadingModelToString(MaterialObj->GetShadingModels().GetFirstShadingModel());
EMaterialShadingModel NewModel = MSM_DefaultLit;
if (ValueStr == TEXT("Unlit")) NewModel = MSM_Unlit;
else if (ValueStr == TEXT("DefaultLit")) NewModel = MSM_DefaultLit;
else if (ValueStr == TEXT("Subsurface")) NewModel = MSM_Subsurface;
else if (ValueStr == TEXT("PreintegratedSkin")) NewModel = MSM_PreintegratedSkin;
else if (ValueStr == TEXT("ClearCoat")) NewModel = MSM_ClearCoat;
else if (ValueStr == TEXT("SubsurfaceProfile")) NewModel = MSM_SubsurfaceProfile;
else if (ValueStr == TEXT("TwoSidedFoliage")) NewModel = MSM_TwoSidedFoliage;
else if (ValueStr == TEXT("Hair")) NewModel = MSM_Hair;
else if (ValueStr == TEXT("Cloth")) NewModel = MSM_Cloth;
else if (ValueStr == TEXT("Eye")) NewModel = MSM_Eye;
else
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Invalid shadingModel '%s'. Valid values: Unlit, DefaultLit, Subsurface, PreintegratedSkin, ClearCoat, SubsurfaceProfile, TwoSidedFoliage, Hair, Cloth, Eye"),
*ValueStr));
}
NewValue = ValueStr;
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->SetShadingModel(NewModel);
MaterialObj->PostEditChange();
}
}
else if (Property == TEXT("opacity") || Property == TEXT("opacityMaskClipValue"))
{
double OpacityValue = Json->GetNumberField(TEXT("value"));
OldValue = FString::Printf(TEXT("%f"), MaterialObj->OpacityMaskClipValue);
NewValue = FString::Printf(TEXT("%f"), OpacityValue);
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->OpacityMaskClipValue = (float)OpacityValue;
MaterialObj->PostEditChange();
}
}
else if (Property == TEXT("bUsedWithSkeletalMesh"))
{
bool bValue = Json->GetBoolField(TEXT("value"));
OldValue = MaterialObj->bUsedWithSkeletalMesh ? TEXT("true") : TEXT("false");
NewValue = bValue ? TEXT("true") : TEXT("false");
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->bUsedWithSkeletalMesh = bValue ? 1 : 0;
MaterialObj->PostEditChange();
}
}
else if (Property == TEXT("bUsedWithMorphTargets"))
{
bool bValue = Json->GetBoolField(TEXT("value"));
OldValue = MaterialObj->bUsedWithMorphTargets ? TEXT("true") : TEXT("false");
NewValue = bValue ? TEXT("true") : TEXT("false");
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->bUsedWithMorphTargets = bValue ? 1 : 0;
MaterialObj->PostEditChange();
}
}
else if (Property == TEXT("bUsedWithNiagaraSprites"))
{
bool bValue = Json->GetBoolField(TEXT("value"));
OldValue = MaterialObj->bUsedWithNiagaraSprites ? TEXT("true") : TEXT("false");
NewValue = bValue ? TEXT("true") : TEXT("false");
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->bUsedWithNiagaraSprites = bValue ? 1 : 0;
MaterialObj->PostEditChange();
}
}
else if (Property == TEXT("ditheredLODTransition") || Property == TEXT("DitheredLODTransition"))
{
bool bValue = Json->GetBoolField(TEXT("value"));
OldValue = MaterialObj->DitheredLODTransition ? TEXT("true") : TEXT("false");
NewValue = bValue ? TEXT("true") : TEXT("false");
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->DitheredLODTransition = bValue ? 1 : 0;
MaterialObj->PostEditChange();
}
}
else if (Property == TEXT("bAllowNegativeEmissiveColor"))
{
bool bValue = Json->GetBoolField(TEXT("value"));
OldValue = MaterialObj->bAllowNegativeEmissiveColor ? TEXT("true") : TEXT("false");
NewValue = bValue ? TEXT("true") : TEXT("false");
if (!DryRun)
{
MaterialObj->PreEditChange(nullptr);
MaterialObj->bAllowNegativeEmissiveColor = bValue ? 1 : 0;
MaterialObj->PostEditChange();
}
}
else
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Unknown property '%s'. Valid properties: domain, blendMode, twoSided, shadingModel, opacity, "
"opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, "
"ditheredLODTransition, bAllowNegativeEmissiveColor"),
*Property));
}
// Save if not dry run
bool bSaved = false;
if (!DryRun)
{
bSaved = MCPUtils::SaveMaterialPackage(MaterialObj);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %sSet material property '%s' on '%s': '%s' -> '%s'"),
DryRun ? TEXT("[DRY RUN] ") : TEXT(""),
*Property, *Material, *OldValue, *NewValue);
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
Result->SetStringField(TEXT("oldValue"), OldValue);
Result->SetStringField(TEXT("newValue"), NewValue);
Result->SetBoolField(TEXT("dryRun"), DryRun);
if (!DryRun)
{
Result->SetBoolField(TEXT("saved"), bSaved);
}
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="add_material_expression"))
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;
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
bool DryRun = false;
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, FJsonObject* Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
}
// Map string class name to UClass via dynamic lookup
UClass* ExprClass = nullptr;
// Convenience aliases for backward compatibility
static TMap<FString, FString> Aliases = {
{TEXT("Lerp"), TEXT("LinearInterpolate")},
};
FString LookupName = ExpressionClass;
if (const FString* Alias = Aliases.Find(ExpressionClass))
{
LookupName = *Alias;
}
// Dynamic lookup: find UMaterialExpression<Name> via UClass iteration
FString FullClassName = FString::Printf(TEXT("MaterialExpression%s"), *LookupName);
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == FullClassName && It->IsChildOf(UMaterialExpression::StaticClass()))
{
ExprClass = *It;
break;
}
}
if (!ExprClass)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Unknown expression class '%s'. Use the UMaterialExpression subclass name without the 'MaterialExpression' prefix "
"(e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp', 'Subtract', 'Fresnel', 'Comment', etc.)"),
*ExpressionClass));
}
if (ExprClass->HasAnyClassFlags(CLASS_Abstract))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Expression class '%s' is abstract and cannot be instantiated."), *ExpressionClass));
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
UObject* Owner = nullptr;
FString AssetDisplayName;
if (!MaterialFunction.IsEmpty())
{
if (!Material.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Specify either 'material' or 'materialFunction', not both"));
}
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
Owner = MatFunc;
AssetDisplayName = MatFunc->GetName();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
Owner = MaterialObj;
AssetDisplayName = MaterialObj->GetName();
}
if (DryRun)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would add expression '%s' to '%s' at (%d, %d)"),
*ExpressionClass, *AssetDisplayName, PosX, PosY);
Result->SetBoolField(TEXT("dryRun"), true);
Result->SetStringField(TEXT("material"), AssetDisplayName);
return;
}
// Ensure the MaterialGraph exists (commandlet mode doesn't auto-create it)
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
// Create, register, and PostEditChange the expression — all inside an SEH wrapper because
// some classes (e.g. UMaterialExpressionParameter) lack CLASS_Abstract but crash during
// PostEditChange. The SEH wrapper cleans up the bad expression on crash.
UMaterialExpression* NewExpr = nullptr;
#if PLATFORM_WINDOWS
int32 CreateResult = TryAddMaterialExpressionSEH(Owner, ExprClass, MaterialObj, MatFunc, PosX, PosY, &NewExpr);
if (CreateResult != 0 || !NewExpr)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Expression class '%s' cannot be instantiated (may be abstract or have internal errors)."),
*ExpressionClass));
}
#else
NewExpr = NewObject<UMaterialExpression>(Owner, ExprClass);
if (!NewExpr)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create material expression object"));
}
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 if (MatFunc)
{
MatFunc->GetExpressionCollection().AddExpression(NewExpr);
MatFunc->PreEditChange(nullptr);
MatFunc->PostEditChange();
MatFunc->MarkPackageDirty();
}
#endif
// Save
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
// Find the node GUID from the material graph (only for materials)
FString NodeGuid;
if (MaterialObj && MaterialObj->MaterialGraph)
{
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
{
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node);
if (MatNode && MatNode->MaterialExpression == NewExpr)
{
NodeGuid = Node->NodeGuid.ToString();
break;
}
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added expression '%s' to '%s' (nodeId: %s, saved: %s)"),
*ExpressionClass, *AssetDisplayName, *NodeGuid, bSaved ? TEXT("true") : TEXT("false"));
// Serialize the expression details
TSharedPtr<FJsonObject> ExprDetails = MCPUtils::SerializeMaterialExpression(NewExpr);
Result->SetStringField(TEXT("material"), AssetDisplayName);
Result->SetStringField(TEXT("nodeId"), NodeGuid);
if (ExprDetails.IsValid())
{
Result->SetObjectField(TEXT("expression"), ExprDetails);
}
Result->SetBoolField(TEXT("saved"), bSaved);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="delete_material_expression"))
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="Node GUID of the expression to delete"))
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, FJsonObject* Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
FString AssetDisplayName;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
AssetDisplayName = MatFunc->GetName();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
AssetDisplayName = MaterialObj->GetName();
}
// For materials, we need the graph to find nodes by GUID
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
}
// Find the node by GUID
UMaterialGraphNode* TargetMatNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
if (GraphNode->NodeGuid.ToString() == Node)
{
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
break;
}
}
if (!TargetMatNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
}
if (!TargetMatNode->MaterialExpression)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node));
}
// Capture info before deletion
FString DeletedNodeTitle = TargetMatNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
FString DeletedExprClass = TargetMatNode->MaterialExpression->GetClass()->GetName();
if (DryRun)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would delete expression '%s' (nodeId: %s) from '%s'"),
*DeletedExprClass, *Node, *AssetDisplayName);
Result->SetBoolField(TEXT("dryRun"), true);
Result->SetStringField(TEXT("material"), AssetDisplayName);
Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle);
Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass);
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);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted expression '%s' (nodeId: %s) from '%s' (saved: %s)"),
*DeletedExprClass, *Node, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("material"), AssetDisplayName);
Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle);
Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="connect_material_expression_pins"))
class UMCPHandler_ConnectMaterialPins : 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 GUID of the source (output) node"))
FString SourceNode;
UPROPERTY(meta=(Description="Pin name on the source node"))
FString SourcePinName;
UPROPERTY(meta=(Description="Node GUID of the target (input) node"))
FString TargetNode;
UPROPERTY(meta=(Description="Pin name on the target node"))
FString TargetPinName;
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
bool DryRun = false;
virtual FString GetDescription() const override
{
return TEXT("Connect two pins in a material or material function graph.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
FString AssetDisplayName;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
AssetDisplayName = MatFunc->GetName();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
AssetDisplayName = MaterialObj->GetName();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
}
// Find source and target nodes by GUID
UEdGraphNode* SourceGraphNode = nullptr;
UEdGraphNode* TargetGraphNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
if (GraphNode->NodeGuid.ToString() == SourceNode)
SourceGraphNode = GraphNode;
if (GraphNode->NodeGuid.ToString() == TargetNode)
TargetGraphNode = GraphNode;
if (SourceGraphNode && TargetGraphNode)
break;
}
if (!SourceGraphNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found in material graph"), *SourceNode));
}
if (!TargetGraphNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found in material graph"), *TargetNode));
}
// Find pins
UEdGraphPin* SourcePin = SourceGraphNode->FindPin(FName(*SourcePinName));
if (!SourcePin)
{
// List available pins for debugging
TArray<TSharedPtr<FJsonValue>> PinNames;
for (UEdGraphPin* P : SourceGraphNode->Pins)
{
if (P) PinNames.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
}
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"),
*SourcePinName, *SourceNode));
Result->SetArrayField(TEXT("availablePins"), PinNames);
return;
}
UEdGraphPin* TargetPin = TargetGraphNode->FindPin(FName(*TargetPinName));
if (!TargetPin)
{
TArray<TSharedPtr<FJsonValue>> PinNames;
for (UEdGraphPin* P : TargetGraphNode->Pins)
{
if (P) PinNames.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
}
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"),
*TargetPinName, *TargetNode));
Result->SetArrayField(TEXT("availablePins"), PinNames);
return;
}
if (DryRun)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would connect %s.%s -> %s.%s in '%s'"),
*SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName);
Result->SetBoolField(TEXT("dryRun"), true);
Result->SetStringField(TEXT("material"), AssetDisplayName);
return;
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Connecting %s.%s -> %s.%s in '%s'"),
*SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName);
// Try to connect via the schema
const UEdGraphSchema* Schema = Graph->GetSchema();
if (!Schema)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Material graph schema not found"));
}
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
Result->SetStringField(TEXT("material"), AssetDisplayName);
if (!bConnected)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Cannot connect %s.%s to %s.%s — types may be incompatible"),
*SourceNode, *SourcePinName, *TargetNode, *TargetPinName));
}
// Save
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
Asset->PostEditChange();
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="disconnect_material_expression_pin"))
class UMCPHandler_DisconnectMaterialPin : 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 GUID of the node whose pin to disconnect"))
FString Node;
UPROPERTY(meta=(Description="Pin name to disconnect"))
FString PinName;
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
bool DryRun = false;
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, FJsonObject* Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
FString AssetDisplayName;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
AssetDisplayName = MatFunc->GetName();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
AssetDisplayName = MaterialObj->GetName();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
}
// Find node by GUID
UEdGraphNode* TargetGraphNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
if (GraphNode->NodeGuid.ToString() == Node)
{
TargetGraphNode = GraphNode;
break;
}
}
if (!TargetGraphNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
}
// Find pin
UEdGraphPin* Pin = TargetGraphNode->FindPin(FName(*PinName));
if (!Pin)
{
TArray<TSharedPtr<FJsonValue>> PinNames;
for (UEdGraphPin* P : TargetGraphNode->Pins)
{
if (P) PinNames.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
}
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"),
*PinName, *Node));
Result->SetArrayField(TEXT("availablePins"), PinNames);
return;
}
int32 BrokenCount = Pin->LinkedTo.Num();
if (DryRun)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would disconnect pin '%s' on node '%s' in '%s' (%d links)"),
*PinName, *Node, *AssetDisplayName, BrokenCount);
Result->SetBoolField(TEXT("dryRun"), true);
Result->SetStringField(TEXT("material"), AssetDisplayName);
Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount);
return;
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Disconnecting pin '%s' on node '%s' in '%s' (%d links)"),
*PinName, *Node, *AssetDisplayName, BrokenCount);
// Break all links
Pin->BreakAllPinLinks();
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
Asset->PostEditChange();
// Save
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result->SetStringField(TEXT("material"), AssetDisplayName);
Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="set_material_expression_property"))
class UMCPHandler_SetExpressionValue : 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 GUID of the expression to modify"))
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, FJsonObject* Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
}
if (!Json->HasField(TEXT("value")))
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
FString AssetDisplayName;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
AssetDisplayName = MatFunc->GetName();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
AssetDisplayName = MaterialObj->GetName();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
}
// Find the node by GUID
UMaterialGraphNode* TargetMatNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
if (GraphNode->NodeGuid.ToString() == Node)
{
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
break;
}
}
if (!TargetMatNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
}
UMaterialExpression* Expr = TargetMatNode->MaterialExpression;
if (!Expr)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node));
}
FString ExprType;
FString NewValueStr;
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
// Handle based on expression type
if (UMaterialExpressionConstant* ConstExpr = Cast<UMaterialExpressionConstant>(Expr))
{
ExprType = TEXT("Constant");
double Value = Json->GetNumberField(TEXT("value"));
ConstExpr->R = (float)Value;
NewValueStr = FString::Printf(TEXT("%f"), Value);
}
else if (UMaterialExpressionConstant3Vector* C3Expr = Cast<UMaterialExpressionConstant3Vector>(Expr))
{
ExprType = TEXT("Constant3Vector");
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
{
double R = 0, G = 0, B = 0;
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
C3Expr->Constant = FLinearColor((float)R, (float)G, (float)B);
NewValueStr = FString::Printf(TEXT("(%f, %f, %f)"), R, G, B);
}
else
{
Asset->PostEditChange();
return MCPUtils::MakeErrorJson(Result, TEXT("Constant3Vector requires value as object {r, g, b}"));
}
}
else if (UMaterialExpressionConstant4Vector* C4Expr = Cast<UMaterialExpressionConstant4Vector>(Expr))
{
ExprType = TEXT("Constant4Vector");
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
{
double R = 0, G = 0, B = 0, A = 1;
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
(*ValueObj)->TryGetNumberField(TEXT("a"), A);
C4Expr->Constant = FLinearColor((float)R, (float)G, (float)B, (float)A);
NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A);
}
else
{
Asset->PostEditChange();
return MCPUtils::MakeErrorJson(Result, TEXT("Constant4Vector requires value as object {r, g, b, a}"));
}
}
else if (UMaterialExpressionScalarParameter* SPExpr = Cast<UMaterialExpressionScalarParameter>(Expr))
{
ExprType = TEXT("ScalarParameter");
double Value = Json->GetNumberField(TEXT("value"));
SPExpr->DefaultValue = (float)Value;
NewValueStr = FString::Printf(TEXT("%f"), Value);
FString ParamName;
if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty())
{
SPExpr->ParameterName = FName(*ParamName);
}
}
else if (UMaterialExpressionVectorParameter* VPExpr = Cast<UMaterialExpressionVectorParameter>(Expr))
{
ExprType = TEXT("VectorParameter");
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
{
double R = 0, G = 0, B = 0, A = 1;
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
(*ValueObj)->TryGetNumberField(TEXT("a"), A);
VPExpr->DefaultValue = FLinearColor((float)R, (float)G, (float)B, (float)A);
NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A);
}
else
{
Asset->PostEditChange();
return MCPUtils::MakeErrorJson(Result, TEXT("VectorParameter requires value as object {r, g, b, a}"));
}
FString ParamName;
if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty())
{
VPExpr->ParameterName = FName(*ParamName);
}
}
else if (UMaterialExpressionTextureCoordinate* TCExpr = Cast<UMaterialExpressionTextureCoordinate>(Expr))
{
ExprType = TEXT("TextureCoordinate");
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
{
double CoordIndex = 0, UTiling = 1, VTiling = 1;
(*ValueObj)->TryGetNumberField(TEXT("coordinateIndex"), CoordIndex);
(*ValueObj)->TryGetNumberField(TEXT("uTiling"), UTiling);
(*ValueObj)->TryGetNumberField(TEXT("vTiling"), VTiling);
TCExpr->CoordinateIndex = (int32)CoordIndex;
TCExpr->UTiling = (float)UTiling;
TCExpr->VTiling = (float)VTiling;
NewValueStr = FString::Printf(TEXT("(index=%d, uTiling=%f, vTiling=%f)"), (int32)CoordIndex, UTiling, VTiling);
}
else
{
Asset->PostEditChange();
return MCPUtils::MakeErrorJson(Result, TEXT("TextureCoordinate requires value as object {coordinateIndex, uTiling, vTiling}"));
}
}
else if (UMaterialExpressionCustom* CustomExpr = Cast<UMaterialExpressionCustom>(Expr))
{
ExprType = TEXT("Custom");
FString Code;
if (Json->TryGetStringField(TEXT("code"), Code))
{
CustomExpr->Code = Code;
NewValueStr = FString::Printf(TEXT("Code: %d chars"), Code.Len());
}
else if (Json->HasField(TEXT("value")))
{
// Also accept code via value field as string
FString ValueStr = Json->GetStringField(TEXT("value"));
if (!ValueStr.IsEmpty())
{
CustomExpr->Code = ValueStr;
NewValueStr = FString::Printf(TEXT("Code: %d chars"), ValueStr.Len());
}
}
FString OutputTypeStr;
if (Json->TryGetStringField(TEXT("outputType"), OutputTypeStr) && !OutputTypeStr.IsEmpty())
{
int64 EnumVal = StaticEnum<ECustomMaterialOutputType>()->GetValueByNameString(OutputTypeStr);
if (EnumVal != INDEX_NONE)
{
CustomExpr->OutputType = (ECustomMaterialOutputType)EnumVal;
}
}
}
else if (UMaterialExpressionComponentMask* CMExpr = Cast<UMaterialExpressionComponentMask>(Expr))
{
ExprType = TEXT("ComponentMask");
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
{
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);
CMExpr->R = bR ? 1 : 0;
CMExpr->G = bG ? 1 : 0;
CMExpr->B = bB ? 1 : 0;
CMExpr->A = bA ? 1 : 0;
NewValueStr = 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"));
}
else
{
Asset->PostEditChange();
return MCPUtils::MakeErrorJson(Result, TEXT("ComponentMask requires value as object {r, g, b, a} (booleans)"));
}
}
else
{
Asset->PostEditChange();
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Expression type '%s' does not support direct value setting. Supported types: Constant, "
"Constant3Vector, Constant4Vector, ScalarParameter, VectorParameter, TextureCoordinate, "
"Custom, ComponentMask"),
*Expr->GetClass()->GetName()));
}
Asset->PostEditChange();
Asset->MarkPackageDirty();
// Save
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set expression value on node '%s' (%s) in '%s': %s"),
*Node, *ExprType, *AssetDisplayName, *NewValueStr);
Result->SetStringField(TEXT("material"), AssetDisplayName);
Result->SetStringField(TEXT("expressionType"), ExprType);
Result->SetStringField(TEXT("newValue"), NewValueStr);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="set_material_expression_position"))
class UMCPHandler_MoveMaterialExpression : 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 GUID of the expression to reposition"))
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, FJsonObject* Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
FString AssetDisplayName;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
AssetDisplayName = MatFunc->GetName();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
AssetDisplayName = MaterialObj->GetName();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
}
// Find node by GUID
UMaterialGraphNode* TargetMatNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
if (GraphNode->NodeGuid.ToString() == Node)
{
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
break;
}
}
if (!TargetMatNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
}
if (DryRun)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would move node '%s' to (%d, %d) in '%s'"),
*Node, PosX, PosY, *AssetDisplayName);
Result->SetBoolField(TEXT("dryRun"), true);
Result->SetStringField(TEXT("material"), AssetDisplayName);
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);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Moved node '%s' to (%d, %d) in '%s' (saved: %s)"),
*Node, PosX, PosY, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("material"), AssetDisplayName);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="create_material_function_asset"))
class UMCPHandler_CreateMaterialFunction : 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, FJsonObject* Result) override
{
if (!PackagePath.StartsWith(TEXT("/Game")))
{
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
}
// Check if asset already exists
MCPAssets<UMaterialFunction> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Function '%s' in '%s'"), *Name, *PackagePath);
// Create via IAssetTools + factory
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
UMaterialFunctionFactoryNew* Factory = NewObject<UMaterialFunctionFactoryNew>();
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialFunction::StaticClass(), Factory);
if (!NewAsset)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Function '%s' in '%s'"), *Name, *PackagePath));
}
UMaterialFunction* MF = Cast<UMaterialFunction>(NewAsset);
if (!MF)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialFunction"));
}
// Set optional description
if (!Description.IsEmpty())
{
MF->Description = Description;
}
// Save
bool bSaved = MCPUtils::SaveGenericPackage(MF);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Function '%s' (saved: %s)"),
*Name, bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("path"), MF->GetPathName());
Result->SetBoolField(TEXT("saved"), bSaved);
}
};