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

963 lines
35 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/MaterialGraphNode_Root.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "MCPHandlers_MaterialRead.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="list_material_assets"))
class UMCPHandler_ListMaterials : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Filter string to match against material name or path"))
FString Filter;
UPROPERTY(meta=(Optional, Description="Type filter: 'all', 'material', or 'instance'"))
FString Type;
virtual FString GetDescription() const override
{
return TEXT("List Material and MaterialInstance assets, optionally filtered by name and type.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
bool bIncludeMaterials = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("material");
bool bIncludeInstances = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("instance");
MCPAssets<UMaterial> Assets;
if (bIncludeMaterials) Assets.Scan(UMaterial::StaticClass());
if (bIncludeInstances) Assets.Scan(UMaterialInstanceConstant::StaticClass());
Assets.Substring(Filter).NoDerived().Info();
TArray<TSharedPtr<FJsonValue>> Entries;
for (const FAssetData& Asset : Assets.AllData())
{
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString());
Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString());
Entry->SetStringField(TEXT("type"),
Asset.AssetClassPath.GetAssetName() == TEXT("MaterialInstanceConstant")
? TEXT("MaterialInstance") : TEXT("Material"));
Entries.Add(MakeShared<FJsonValueObject>(Entry));
}
Result->SetNumberField(TEXT("count"), Entries.Num());
Result->SetArrayField(TEXT("materials"), Entries);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="dump_material"))
class UMCPHandler_GetMaterial : 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, FJsonObject* Result) override
{
FString DecodedName = MCPUtils::UrlDecode(Material);
// Try loading as UMaterial or UMaterialInstanceConstant
MCPAssets<UMaterialInterface> Assets;
Assets.Scan(UMaterial::StaticClass());
Assets.Scan(UMaterialInstanceConstant::StaticClass());
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
UMaterialInterface* LoadedObj = Assets.Object();
if (UMaterial* MaterialObj = Cast<UMaterial>(LoadedObj))
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material '%s'"), *MaterialObj->GetName());
Result->SetStringField(TEXT("name"), MaterialObj->GetName());
Result->SetStringField(TEXT("path"), MaterialObj->GetPathName());
Result->SetStringField(TEXT("type"), TEXT("Material"));
// Material domain
FString DomainStr = TEXT("Unknown");
if (const UEnum* DomainEnum = StaticEnum<EMaterialDomain>())
{
DomainStr = DomainEnum->GetNameStringByValue((int64)MaterialObj->MaterialDomain);
}
Result->SetStringField(TEXT("domain"), DomainStr);
// Blend mode
FString BlendModeStr = TEXT("Unknown");
if (const UEnum* BlendEnum = StaticEnum<EBlendMode>())
{
BlendModeStr = BlendEnum->GetNameStringByValue((int64)MaterialObj->BlendMode);
}
Result->SetStringField(TEXT("blendMode"), BlendModeStr);
// Shading models
TArray<TSharedPtr<FJsonValue>> ShadingModels;
FMaterialShadingModelField SMField = MaterialObj->GetShadingModels();
if (const UEnum* SMEnum = StaticEnum<EMaterialShadingModel>())
{
for (int32 i = 0; i < SMEnum->NumEnums() - 1; ++i)
{
EMaterialShadingModel SM = (EMaterialShadingModel)SMEnum->GetValueByIndex(i);
if (SMField.HasShadingModel(SM))
{
ShadingModels.Add(MakeShared<FJsonValueString>(SMEnum->GetNameStringByIndex(i)));
}
}
}
Result->SetArrayField(TEXT("shadingModels"), ShadingModels);
// Two-sided
Result->SetBoolField(TEXT("twoSided"), MaterialObj->IsTwoSided());
// Expression count
auto Expressions = MaterialObj->GetExpressions();
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
// Parameters — iterate expressions for parameter types
TArray<TSharedPtr<FJsonValue>> Parameters;
for (UMaterialExpression* Expr : Expressions)
{
if (!Expr) continue;
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
bool bIsParam = false;
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
{
bIsParam = true;
ParamObj->SetStringField(TEXT("name"), SP->ParameterName.ToString());
ParamObj->SetStringField(TEXT("type"), TEXT("Scalar"));
ParamObj->SetStringField(TEXT("group"), SP->Group.ToString());
ParamObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
}
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
{
bIsParam = true;
ParamObj->SetStringField(TEXT("name"), VP->ParameterName.ToString());
ParamObj->SetStringField(TEXT("type"), TEXT("Vector"));
ParamObj->SetStringField(TEXT("group"), VP->Group.ToString());
TSharedRef<FJsonObject> DefVal = MakeShared<FJsonObject>();
DefVal->SetNumberField(TEXT("r"), VP->DefaultValue.R);
DefVal->SetNumberField(TEXT("g"), VP->DefaultValue.G);
DefVal->SetNumberField(TEXT("b"), VP->DefaultValue.B);
DefVal->SetNumberField(TEXT("a"), VP->DefaultValue.A);
ParamObj->SetObjectField(TEXT("defaultValue"), DefVal);
}
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
{
bIsParam = true;
ParamObj->SetStringField(TEXT("name"), TP->ParameterName.ToString());
ParamObj->SetStringField(TEXT("type"), TEXT("Texture"));
ParamObj->SetStringField(TEXT("group"), TP->Group.ToString());
if (TP->Texture)
ParamObj->SetStringField(TEXT("defaultValue"), TP->Texture->GetPathName());
}
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
{
bIsParam = true;
ParamObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString());
ParamObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
ParamObj->SetStringField(TEXT("group"), SSP->Group.ToString());
ParamObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
}
if (bIsParam)
{
Parameters.Add(MakeShared<FJsonValueObject>(ParamObj));
}
}
Result->SetArrayField(TEXT("parameters"), Parameters);
// Referenced textures
TArray<TSharedPtr<FJsonValue>> ReferencedTextures;
auto RefTexObjs = MaterialObj->GetReferencedTextures();
for (const TObjectPtr<UObject>& TexObj : RefTexObjs)
{
if (TexObj)
{
ReferencedTextures.Add(MakeShared<FJsonValueString>(TexObj->GetPathName()));
}
}
Result->SetArrayField(TEXT("referencedTextures"), ReferencedTextures);
// Graph node count
int32 GraphNodeCount = 0;
if (MaterialObj->MaterialGraph)
{
GraphNodeCount = MaterialObj->MaterialGraph->Nodes.Num();
}
Result->SetNumberField(TEXT("graphNodeCount"), GraphNodeCount);
// Usage flags
TSharedRef<FJsonObject> UsageFlags = MakeShared<FJsonObject>();
UsageFlags->SetBoolField(TEXT("bUsedWithSkeletalMesh"), MaterialObj->bUsedWithSkeletalMesh != 0);
UsageFlags->SetBoolField(TEXT("bUsedWithMorphTargets"), MaterialObj->bUsedWithMorphTargets != 0);
UsageFlags->SetBoolField(TEXT("bUsedWithNiagaraSprites"), MaterialObj->bUsedWithNiagaraSprites != 0);
UsageFlags->SetBoolField(TEXT("bUsedWithParticleSprites"), MaterialObj->bUsedWithParticleSprites != 0);
UsageFlags->SetBoolField(TEXT("bUsedWithStaticLighting"), MaterialObj->bUsedWithStaticLighting != 0);
Result->SetObjectField(TEXT("usageFlags"), UsageFlags);
// Opacity mask clip value
Result->SetNumberField(TEXT("opacityMaskClipValue"), MaterialObj->OpacityMaskClipValue);
// Additional settings
Result->SetBoolField(TEXT("ditheredLODTransition"), MaterialObj->DitheredLODTransition != 0);
Result->SetBoolField(TEXT("bAllowNegativeEmissiveColor"), MaterialObj->bAllowNegativeEmissiveColor != 0);
// Texture sample count (simple expression scan)
int32 TextureSampleCount = 0;
for (UMaterialExpression* Expr : Expressions)
{
if (Expr && Expr->IsA<UMaterialExpressionTextureSample>())
{
TextureSampleCount++;
}
}
Result->SetNumberField(TEXT("textureSampleCount"), TextureSampleCount);
return;
}
if (UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(LoadedObj))
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material instance '%s'"), *MI->GetName());
Result->SetStringField(TEXT("name"), MI->GetName());
Result->SetStringField(TEXT("path"), MI->GetPathName());
Result->SetStringField(TEXT("type"), TEXT("MaterialInstance"));
if (MI->Parent)
{
Result->SetStringField(TEXT("parent"), MI->Parent->GetName());
Result->SetStringField(TEXT("parentPath"), MI->Parent->GetPathName());
}
// Overridden parameters
TArray<TSharedPtr<FJsonValue>> OverriddenParams;
// Scalar parameters
for (const FScalarParameterValue& Param : MI->ScalarParameterValues)
{
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
PObj->SetStringField(TEXT("type"), TEXT("Scalar"));
PObj->SetNumberField(TEXT("value"), Param.ParameterValue);
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
}
// Vector parameters
for (const FVectorParameterValue& Param : MI->VectorParameterValues)
{
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
PObj->SetStringField(TEXT("type"), TEXT("Vector"));
TSharedRef<FJsonObject> Val = MakeShared<FJsonObject>();
Val->SetNumberField(TEXT("r"), Param.ParameterValue.R);
Val->SetNumberField(TEXT("g"), Param.ParameterValue.G);
Val->SetNumberField(TEXT("b"), Param.ParameterValue.B);
Val->SetNumberField(TEXT("a"), Param.ParameterValue.A);
PObj->SetObjectField(TEXT("value"), Val);
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
}
// Texture parameters
for (const FTextureParameterValue& Param : MI->TextureParameterValues)
{
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
PObj->SetStringField(TEXT("type"), TEXT("Texture"));
if (Param.ParameterValue)
PObj->SetStringField(TEXT("value"), Param.ParameterValue->GetPathName());
else
PObj->SetStringField(TEXT("value"), TEXT("None"));
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
}
// Static switch parameters
for (const FStaticSwitchParameter& Param : MI->GetStaticParameters().StaticSwitchParameters)
{
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
PObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
PObj->SetBoolField(TEXT("value"), Param.Value);
PObj->SetBoolField(TEXT("overridden"), Param.bOverride);
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
}
Result->SetArrayField(TEXT("overriddenParameters"), OverriddenParams);
return;
}
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName));
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="dump_material_expression_graph"))
class UMCPHandler_GetMaterialGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Material name or package path"))
FString Material;
virtual FString GetDescription() const override
{
return TEXT("Get the serialized expression graph for a material, including all nodes and connections.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
FString DecodedName = MCPUtils::UrlDecode(Material);
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
UMaterial* MaterialObj = Assets.Object();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *MaterialObj->GetName());
// Ensure the material graph is built
if (!MaterialObj->MaterialGraph)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — MaterialGraph is null, attempting rebuild"));
// The material graph is built lazily by the material editor; force-create it
MaterialObj->MaterialGraph = CastChecked<UMaterialGraph>(
FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
MaterialObj->MaterialGraph->Material = MaterialObj;
MaterialObj->MaterialGraph->RebuildGraph();
}
if (!MaterialObj->MaterialGraph)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
}
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(MaterialObj->MaterialGraph);
if (!GraphJson.IsValid())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to serialize material graph"));
}
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
// Add material name context
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="describe_material_in_english"))
class UMCPHandler_DescribeMaterial : 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, FJsonObject* Result) override
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterial* MaterialObj = Assets.Object();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *MaterialObj->GetName());
// Ensure material graph is built
if (!MaterialObj->MaterialGraph)
{
MaterialObj->MaterialGraph = CastChecked<UMaterialGraph>(
FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
MaterialObj->MaterialGraph->Material = MaterialObj;
MaterialObj->MaterialGraph->RebuildGraph();
}
if (!MaterialObj->MaterialGraph)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
}
// Recursive helper: trace backwards from a pin and build a description string
TFunction<FString(UEdGraphPin*, int32)> TracePin = [&TracePin](UEdGraphPin* Pin, int32 Depth) -> FString
{
if (!Pin || Depth > 10)
return TEXT("(unknown)");
// If no connections, report as unconnected
if (Pin->LinkedTo.Num() == 0)
{
if (!Pin->DefaultValue.IsEmpty())
return FString::Printf(TEXT("(default: %s)"), *Pin->DefaultValue);
return TEXT("(unconnected)");
}
TArray<FString> Sources;
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
{
if (!LinkedPin || !LinkedPin->GetOwningNode()) continue;
UEdGraphNode* SourceNode = LinkedPin->GetOwningNode();
FString NodeDesc;
// Check if this is a material graph node
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(SourceNode))
{
UMaterialExpression* Expr = MatNode->MaterialExpression;
if (!Expr)
{
NodeDesc = TEXT("(null expression)");
}
else if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
{
NodeDesc = FString::Printf(TEXT("ScalarParam \"%s\" (default: %.4f)"), *SP->ParameterName.ToString(), SP->DefaultValue);
}
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
{
NodeDesc = FString::Printf(TEXT("VectorParam \"%s\" (default: R=%.2f G=%.2f B=%.2f A=%.2f)"),
*VP->ParameterName.ToString(), VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
}
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
{
FString TexName = TP->Texture ? TP->Texture->GetName() : TEXT("None");
NodeDesc = FString::Printf(TEXT("TextureParam \"%s\" (%s)"), *TP->ParameterName.ToString(), *TexName);
}
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
{
NodeDesc = FString::Printf(TEXT("StaticSwitchParam \"%s\" (default: %s)"),
*SSP->ParameterName.ToString(), SSP->DefaultValue ? TEXT("true") : TEXT("false"));
}
else if (auto* SC = Cast<UMaterialExpressionConstant>(Expr))
{
NodeDesc = FString::Printf(TEXT("Constant(%.4f)"), SC->R);
}
else if (auto* C3 = Cast<UMaterialExpressionConstant3Vector>(Expr))
{
NodeDesc = FString::Printf(TEXT("Constant3(R=%.2f G=%.2f B=%.2f)"), C3->Constant.R, C3->Constant.G, C3->Constant.B);
}
else if (auto* C4 = Cast<UMaterialExpressionConstant4Vector>(Expr))
{
NodeDesc = FString::Printf(TEXT("Constant4(R=%.2f G=%.2f B=%.2f A=%.2f)"), C4->Constant.R, C4->Constant.G, C4->Constant.B, C4->Constant.A);
}
else if (auto* TS = Cast<UMaterialExpressionTextureSample>(Expr))
{
FString TexName = TS->Texture ? TS->Texture->GetName() : TEXT("None");
NodeDesc = FString::Printf(TEXT("TextureSample(%s)"), *TexName);
}
else if (auto* MFC = Cast<UMaterialExpressionMaterialFunctionCall>(Expr))
{
FString FuncName = MFC->MaterialFunction ? MFC->MaterialFunction->GetName() : TEXT("None");
NodeDesc = FString::Printf(TEXT("FunctionCall(%s)"), *FuncName);
}
else
{
NodeDesc = Expr->GetClass()->GetName();
}
// If the source node has input pins with connections, recurse
TArray<FString> InputDescs;
for (UEdGraphPin* InputPin : SourceNode->Pins)
{
if (!InputPin || InputPin->Direction != EGPD_Input || InputPin->LinkedTo.Num() == 0) continue;
FString InputDesc = TracePin(InputPin, Depth + 1);
InputDescs.Add(InputDesc);
}
if (InputDescs.Num() > 0)
{
NodeDesc += TEXT(" <- (") + FString::Join(InputDescs, TEXT(", ")) + TEXT(")");
}
}
else
{
// Non-material node (e.g., root, comment), just use title
NodeDesc = SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
}
Sources.Add(NodeDesc);
}
if (Sources.Num() == 1)
return Sources[0];
return TEXT("(") + FString::Join(Sources, TEXT(", ")) + TEXT(")");
};
// Find root node and trace each input
TArray<TSharedPtr<FJsonValue>> InputDescriptions;
UMaterialGraphNode_Root* RootNode = nullptr;
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
{
RootNode = Cast<UMaterialGraphNode_Root>(Node);
if (RootNode) break;
}
if (!RootNode)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Could not find root node in material graph"));
}
for (UEdGraphPin* Pin : RootNode->Pins)
{
if (!Pin || Pin->Direction != EGPD_Input) continue;
FString PinName = Pin->PinName.ToString();
FString Description;
if (Pin->LinkedTo.Num() == 0)
{
Description = TEXT("(unconnected)");
}
else
{
Description = TracePin(Pin, 0);
}
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
InputObj->SetStringField(TEXT("input"), PinName);
InputObj->SetStringField(TEXT("chain"), Description);
InputObj->SetBoolField(TEXT("connected"), Pin->LinkedTo.Num() > 0);
InputDescriptions.Add(MakeShared<FJsonValueObject>(InputObj));
}
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
Result->SetArrayField(TEXT("inputs"), InputDescriptions);
// Also include a compact text description
FString TextDesc;
for (const TSharedPtr<FJsonValue>& Val : InputDescriptions)
{
TSharedPtr<FJsonObject> Obj = Val->AsObject();
if (!Obj.IsValid()) continue;
FString InputName = Obj->GetStringField(TEXT("input"));
FString Chain = Obj->GetStringField(TEXT("chain"));
bool bConnected = Obj->GetBoolField(TEXT("connected"));
if (bConnected)
{
TextDesc += FString::Printf(TEXT("%s <- %s\n"), *InputName, *Chain);
}
}
if (!TextDesc.IsEmpty())
{
Result->SetStringField(TEXT("description"), TextDesc);
}
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="search_within_materials"))
class UMCPHandler_SearchMaterials : 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, FJsonObject* Result) override
{
FString DecodedQuery = MCPUtils::UrlDecode(Query);
MaxResults = FMath::Clamp(MaxResults, 1, 200);
TArray<TSharedPtr<FJsonValue>> Results;
MCPAssets<UMaterial> AllMaterials;
AllMaterials.Info();
for (const FAssetData& Asset : AllMaterials.AllData())
{
if (Results.Num() >= MaxResults) break;
FString MatName = Asset.AssetName.ToString();
// Check material name first
bool bNameMatch = MatName.Contains(DecodedQuery, ESearchCase::IgnoreCase);
UMaterial* MaterialObj = Cast<UMaterial>(const_cast<FAssetData&>(Asset).GetAsset());
if (!MaterialObj) continue;
auto Expressions = MaterialObj->GetExpressions();
if (bNameMatch)
{
// Add a match for the material itself
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
R->SetStringField(TEXT("material"), MatName);
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
R->SetStringField(TEXT("matchType"), TEXT("materialName"));
Results.Add(MakeShared<FJsonValueObject>(R));
}
// Search expressions
for (UMaterialExpression* Expr : Expressions)
{
if (!Expr || Results.Num() >= MaxResults) continue;
FString ExprDesc = Expr->GetDescription();
FString ExprClass = Expr->GetClass()->GetName();
// Check parameter name
FString ParamName;
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
ParamName = SP->ParameterName.ToString();
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
ParamName = VP->ParameterName.ToString();
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
ParamName = TP->ParameterName.ToString();
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
ParamName = SSP->ParameterName.ToString();
bool bExprMatch = ExprDesc.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
ExprClass.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
(!ParamName.IsEmpty() && ParamName.Contains(DecodedQuery, ESearchCase::IgnoreCase));
if (bExprMatch)
{
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
R->SetStringField(TEXT("material"), MatName);
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
R->SetStringField(TEXT("matchType"), TEXT("expression"));
R->SetStringField(TEXT("expressionClass"), ExprClass);
if (!ExprDesc.IsEmpty())
R->SetStringField(TEXT("description"), ExprDesc);
if (!ParamName.IsEmpty())
R->SetStringField(TEXT("parameterName"), ParamName);
Results.Add(MakeShared<FJsonValueObject>(R));
}
}
}
Result->SetStringField(TEXT("query"), DecodedQuery);
Result->SetNumberField(TEXT("resultCount"), Results.Num());
Result->SetArrayField(TEXT("results"), Results);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="find_material_references"))
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, FJsonObject* Result) override
{
// Try to find the material's package path (search 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();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindMaterialReferences — '%s' (package: %s)"), *Material, *PackagePath);
IAssetRegistry& Registry = *IAssetRegistry::Get();
TArray<FName> Referencers;
Registry.GetReferencers(FName(*PackagePath), Referencers);
TArray<TSharedPtr<FJsonValue>> RefArray;
for (const FName& Ref : Referencers)
{
FString RefStr = Ref.ToString();
// Skip self-reference
if (RefStr == PackagePath) continue;
RefArray.Add(MakeShared<FJsonValueString>(RefStr));
}
Result->SetStringField(TEXT("packagePath"), PackagePath);
Result->SetNumberField(TEXT("totalReferencers"), RefArray.Num());
Result->SetArrayField(TEXT("referencers"), RefArray);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="list_material_function_assets"))
class UMCPHandler_ListMaterialFunctions : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Filter string to match against function name or path"))
FString Filter;
virtual FString GetDescription() const override
{
return TEXT("List MaterialFunction assets, optionally filtered by name or path.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
MCPAssets<UMaterialFunction> Assets;
Assets.Substring(Filter).Info();
TArray<TSharedPtr<FJsonValue>> Entries;
for (const FAssetData& Asset : Assets.AllData())
{
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString());
Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString());
Entries.Add(MakeShared<FJsonValueObject>(Entry));
}
Result->SetNumberField(TEXT("count"), Entries.Num());
Result->SetArrayField(TEXT("functions"), Entries);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="dump_material_function"))
class UMCPHandler_GetMaterialFunction : 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, FJsonObject* Result) override
{
FString DecodedName = MCPUtils::UrlDecode(MaterialFunction);
MCPAssets<UMaterialFunction> Assets;
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
UMaterialFunction* MF = Assets.Object();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName());
Result->SetStringField(TEXT("name"), MF->GetName());
Result->SetStringField(TEXT("path"), MF->GetPathName());
Result->SetStringField(TEXT("description"), MF->GetDescription());
// Expression count
auto Expressions = MF->GetExpressions();
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
// List function inputs and outputs from expressions
TArray<TSharedPtr<FJsonValue>> Inputs;
TArray<TSharedPtr<FJsonValue>> Outputs;
TArray<TSharedPtr<FJsonValue>> ExpressionList;
{
for (UMaterialExpression* Expr : Expressions)
{
if (!Expr) continue;
if (auto* FI = Cast<UMaterialExpressionFunctionInput>(Expr))
{
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
InputObj->SetStringField(TEXT("name"), FI->InputName.ToString());
InputObj->SetStringField(TEXT("type"), TEXT("FunctionInput"));
InputObj->SetNumberField(TEXT("posX"), FI->MaterialExpressionEditorX);
InputObj->SetNumberField(TEXT("posY"), FI->MaterialExpressionEditorY);
Inputs.Add(MakeShared<FJsonValueObject>(InputObj));
}
else if (auto* FO = Cast<UMaterialExpressionFunctionOutput>(Expr))
{
TSharedRef<FJsonObject> OutputObj = MakeShared<FJsonObject>();
OutputObj->SetStringField(TEXT("name"), FO->OutputName.ToString());
OutputObj->SetStringField(TEXT("type"), TEXT("FunctionOutput"));
OutputObj->SetNumberField(TEXT("posX"), FO->MaterialExpressionEditorX);
OutputObj->SetNumberField(TEXT("posY"), FO->MaterialExpressionEditorY);
Outputs.Add(MakeShared<FJsonValueObject>(OutputObj));
}
// Serialize every expression
TSharedPtr<FJsonObject> ExprJson = MCPUtils::SerializeMaterialExpression(Expr);
if (ExprJson.IsValid())
{
ExpressionList.Add(MakeShared<FJsonValueObject>(ExprJson.ToSharedRef()));
}
}
}
Result->SetArrayField(TEXT("inputs"), Inputs);
Result->SetArrayField(TEXT("outputs"), Outputs);
Result->SetArrayField(TEXT("expressions"), ExpressionList);
// If the function has an editor graph, serialize it
UEdGraph* FuncGraph = MF->MaterialGraph;
if (FuncGraph)
{
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(FuncGraph);
if (GraphJson.IsValid())
{
Result->SetObjectField(TEXT("graph"), GraphJson);
}
}
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="compile_material"))
class UMCPHandler_ValidateMaterial : 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, FJsonObject* Result) override
{
// Load material
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterial* MaterialObj = Assets.Object();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *MaterialObj->GetName());
// Force recompile by triggering PreEditChange/PostEditChange
MaterialObj->PreEditChange(nullptr);
MaterialObj->PostEditChange();
// Collect compilation errors
TArray<TSharedPtr<FJsonValue>> ErrorArray;
bool bValid = true;
// Check for compilation errors via FMaterialResource on current platform
FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel);
if (Resource)
{
const TArray<FString>& CompileErrors = Resource->GetCompileErrors();
for (const FString& Err : CompileErrors)
{
bValid = false;
ErrorArray.Add(MakeShared<FJsonValueString>(Err));
}
}
// Count expressions and connections
auto Expressions = MaterialObj->GetExpressions();
int32 ExprCount = Expressions.Num();
int32 ConnectionCount = 0;
if (MaterialObj->MaterialGraph)
{
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
{
if (!Node) continue;
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin && Pin->Direction == EGPD_Output)
{
ConnectionCount += Pin->LinkedTo.Num();
}
}
}
}
Result->SetBoolField(TEXT("valid"), bValid);
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
Result->SetNumberField(TEXT("expressionCount"), ExprCount);
Result->SetNumberField(TEXT("connectionCount"), ConnectionCount);
Result->SetArrayField(TEXT("errors"), ErrorArray);
Result->SetNumberField(TEXT("errorCount"), ErrorArray.Num());
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material '%s' validation %s (%d errors)"),
*MaterialObj->GetName(), bValid ? TEXT("passed") : TEXT("failed"), ErrorArray.Num());
}
};