More work on MCP

This commit is contained in:
2026-03-10 20:15:59 -04:00
parent d5fb9cd224
commit 0e79b02307
23 changed files with 617 additions and 307 deletions

1
.clangd-query.pid Normal file
View File

@@ -0,0 +1 @@
540424

BIN
Content/Testing/M_Test.uasset LFS Normal file

Binary file not shown.

View File

@@ -54,3 +54,11 @@ is actually fine — but it's a one-way door.
recreated during the replacement recreated during the replacement
- **DumpMaterialInstanceParameters** — parent chain lost class - **DumpMaterialInstanceParameters** — parent chain lost class
type info (Material vs MaterialInstance) type info (Material vs MaterialInstance)
## Design changes
- Saving assets is being done at somewhat unpredictable
points. It's not entirely clear that we *should* be
saving things every time an edit is made. It might be
better to have an explicit "Save" MCP command.

View File

@@ -7,6 +7,8 @@
#include "BlueprintExporter.h" #include "BlueprintExporter.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "Materials/Material.h"
#include "MaterialGraph/MaterialGraph.h"
#include "UMCPHandler_DumpGraphs.generated.h" #include "UMCPHandler_DumpGraphs.generated.h"
@@ -20,13 +22,13 @@ class UMCPHandler_DumpGraphs : public UObject, public IMCPHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(meta=(Description="Path to a blueprint or graph, e.g. /Game/Foo or /Game/Foo,graph:EventGraph")) UPROPERTY(meta=(Description="Path to a blueprint, material, or graph, e.g. /Game/Foo or /Game/Foo,graph:EventGraph"))
FString Path; FString Path;
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
return TEXT("Dump blueprint graphs as readable text. " return TEXT("Dump blueprint or material graphs as readable text. "
"If given a blueprint, dumps all graphs. If given a specific graph, dumps only that one."); "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 virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
@@ -52,7 +54,19 @@ public:
return; return;
} }
Result.Appendf(TEXT("ERROR: Expected a blueprint or graph, got %s\n"), 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())); *MCPUtils::FormatName(F.Obj->GetClass()));
} }

View File

@@ -1,82 +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 "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "MaterialGraph/MaterialGraphNode_Root.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "UMCPHandler_DumpMaterialExpressionGraph.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UMCPHandler_DumpMaterialExpressionGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Material name or package path"))
FString Material;
virtual FString GetDescription() const override
{
return TEXT("Dump the expression graph for a material, showing all nodes and connections as readable text.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterial* Mat = Assets.Object();
// Ensure the material graph is built (it's created lazily by the material editor)
MCPUtils::EnsureMaterialGraph(Mat);
if (!Mat->MaterialGraph)
{
Result.Append(TEXT("ERROR: Could not build MaterialGraph for this material\n"));
return;
}
Result.Appendf(TEXT("Material: %s\n"), *MCPUtils::FormatName(Mat));
Result.Appendf(TEXT("Expressions: %d\n\n"), Mat->GetExpressions().Num());
UMaterialGraph* Graph = Mat->MaterialGraph;
for (UEdGraphNode* Node : Graph->Nodes)
{
Result.Appendf(TEXT("--- %s ---\n"), *MCPUtils::FormatName(Node));
// Show the material expression type for material graph nodes
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node))
{
if (MatNode->MaterialExpression)
Result.Appendf(TEXT(" Expression: %s\n"), *MCPUtils::FormatName(MatNode->MaterialExpression));
}
// Output pins and their connections
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->Direction == EGPD_Output && Pin->LinkedTo.Num() > 0)
{
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
{
Result.Appendf(TEXT(" %s -> %s.%s\n"),
*MCPUtils::FormatName(Pin),
*MCPUtils::FormatName(LinkedPin->GetOwningNode()),
*MCPUtils::FormatName(LinkedPin));
}
}
}
}
}
};

View File

@@ -0,0 +1,77 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "UMCPHandler_DumpProperties.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
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;
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.
MCPFetcher F(Result);
UObject* Obj = F.Walk(Path).Cast<UObject>();
if (!Obj) return;
// Get the editable template (e.g. Blueprint → CDO).
UObject* Template = MCPUtils::GetEditableTemplate(Obj, Result);
if (!Template) return;
TArray<FProperty*> Props = MCPUtils::BlueprintVisibleProperties(Template, Query);
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_Edit);
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

@@ -20,7 +20,7 @@ class UMCPHandler_ListBlueprintAssets : public UObject, public IMCPHandler
public: public:
UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path")) UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path"))
FString Filter; FString Query;
UPROPERTY(meta=(Optional, Description="Filter by parent class name (exact match, case-insensitive)")) UPROPERTY(meta=(Optional, Description="Filter by parent class name (exact match, case-insensitive)"))
FString ParentClass; FString ParentClass;
@@ -39,7 +39,7 @@ public:
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{ {
MCPAssets<UObject> Assets; MCPAssets<UObject> Assets;
Assets.NoScans().Substring(Filter).Limit(500).Errors(Result); Assets.NoScans().Substring(Query).Limit(500).Errors(Result);
if (IncludeRegular) Assets.Scan<UBlueprint>(); if (IncludeRegular) Assets.Scan<UBlueprint>();
if (IncludeLevel) Assets.Scan<UWorld>(); if (IncludeLevel) Assets.Scan<UWorld>();
Assets.Info(); Assets.Info();

View File

@@ -20,7 +20,7 @@ public:
FString ClassName; FString ClassName;
UPROPERTY(meta=(Optional, Description="Substring filter for property names")) UPROPERTY(meta=(Optional, Description="Substring filter for property names"))
FString Filter; FString Query;
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
@@ -46,7 +46,7 @@ public:
FString PropName = Prop->GetName(); FString PropName = Prop->GetName();
if (!Filter.IsEmpty() && !PropName.Contains(Filter, ESearchCase::IgnoreCase)) if (!Query.IsEmpty() && !PropName.Contains(Query, ESearchCase::IgnoreCase))
continue; continue;
// Build compact flags string // Build compact flags string

View File

@@ -0,0 +1,49 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "UMCPHandler_ListOpenAssetEditors.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
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

@@ -0,0 +1,46 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "UMCPHandler_OpenAssetEditor.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
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

@@ -18,7 +18,7 @@ class UMCPHandler_SearchAssets : public UObject, public IMCPHandler
public: public:
UPROPERTY(meta=(Optional, Description="Substring to match against asset package paths")) UPROPERTY(meta=(Optional, Description="Substring to match against asset package paths"))
FString Search; FString Query;
UPROPERTY(meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh")) UPROPERTY(meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
FString Type; FString Type;
@@ -28,14 +28,14 @@ public:
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
return TEXT("Search for assets by name and/or type. At least one of Search or Type must be specified."); 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 virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{ {
if (Search.IsEmpty() && Type.IsEmpty()) if (Query.IsEmpty() && Type.IsEmpty())
{ {
Result.Append(TEXT("ERROR: At least one of Search or Type must be specified\n")); Result.Append(TEXT("ERROR: At least one of Query or Type must be specified\n"));
return; return;
} }
@@ -53,9 +53,9 @@ public:
Assets.NoScans().Scan(TypeClass); Assets.NoScans().Scan(TypeClass);
} }
if (!Search.IsEmpty()) if (!Query.IsEmpty())
{ {
Assets.Substring(Search); Assets.Substring(Query);
} }
Assets.AllContent().Limit(Limit).Errors(Result).Info(); Assets.AllContent().Limit(Limit).Errors(Result).Info();

View File

@@ -2,10 +2,10 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "MCPHandler.h" #include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h" #include "MCPFetcher.h"
#include "MCPUtils.h" #include "MCPUtils.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphSchema.h"
#include "UMCPHandler_SearchSpawnableNodeTypes.generated.h" #include "UMCPHandler_SearchSpawnableNodeTypes.generated.h"
@@ -25,50 +25,36 @@ public:
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)")) UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)"))
int32 MaxResults = 50; int32 MaxResults = 50;
UPROPERTY(meta=(Optional, Description="Blueprint path. If specified with Graph, only returns nodes compatible with that graph.")) UPROPERTY(meta=(Description="MCPFetcher path to a graph, e.g. /Game/Foo,graph:EventGraph or /Game/Materials/M_Gold,graph:"))
FString Blueprint;
UPROPERTY(meta=(Optional, Description="Graph name to filter by compatibility. Requires Blueprint."))
FString Graph; FString Graph;
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
return TEXT("Search the Blueprint action database for node spawners matching a query. " return TEXT("Search the action database for node types that can be spawned in a graph. "
"Returns full action names for use with spawn_node. " "Works with any graph type (Blueprint, Material, etc.). "
"Optionally filter by blueprint+graph to only show compatible node types."); "Returns full action names for use with SpawnNodesInGraph.");
} }
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{ {
int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500); int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
// Optionally resolve a graph to filter by compatibility
UEdGraph* GraphFilter = nullptr;
if (!Blueprint.IsEmpty() && !Graph.IsEmpty())
{
MCPFetcher F(Result); MCPFetcher F(Result);
F.Walk(Blueprint).Graph(Graph); UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!F.Ok()) return; if (!TargetGraph) return;
GraphFilter = Cast<UEdGraph>(F.Obj);
if (!GraphFilter) TArray<TSharedPtr<FEdGraphSchemaAction>> Actions = MCPUtils::SearchGraphActions(TargetGraph, Query, ClampedMax, /*ExactMatch=*/false);
for (const TSharedPtr<FEdGraphSchemaAction>& Action : Actions)
{ {
Result.Appendf(TEXT("ERROR: '%s' is not a graph\n"), *Graph); Result.Appendf(TEXT("%s\n"), *MCPUtils::ActionFullName(Action));
return;
}
} }
TArray<UBlueprintNodeSpawner*> Spawners = MCPUtils::SearchNodeSpawners(Query, ClampedMax, /*ExactMatch=*/false, GraphFilter); if (Actions.Num() == 0)
for (UBlueprintNodeSpawner* Spawner : Spawners)
{
Result.Appendf(TEXT("%s\n"), *MCPUtils::NodeSpawnerFullName(Spawner));
}
if (Spawners.Num() == 0)
{ {
Result.Append(TEXT("No matching node types found.\n")); Result.Append(TEXT("No matching node types found.\n"));
} }
else if (Spawners.Num() >= ClampedMax) else if (Actions.Num() >= ClampedMax)
{ {
Result.Appendf(TEXT("WARNING: Reached limit of %d results. Refine your query or increase MaxResults.\n"), ClampedMax); Result.Appendf(TEXT("WARNING: Reached limit of %d results. Refine your query or increase MaxResults.\n"), ClampedMax);
} }

View File

@@ -41,7 +41,7 @@ public:
FString TypeName; FString TypeName;
UPROPERTY(meta=(Optional, Description="Filter to blueprints whose name or path contains this substring")) UPROPERTY(meta=(Optional, Description="Filter to blueprints whose name or path contains this substring"))
FString Filter; FString Query;
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 200, max 500)")) UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 200, max 500)"))
int32 MaxResults = 0; int32 MaxResults = 0;
@@ -54,7 +54,7 @@ public:
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{ {
FString DecodedTypeName = MCPUtils::UrlDecode(TypeName); FString DecodedTypeName = MCPUtils::UrlDecode(TypeName);
FString FilterStr = Filter.IsEmpty() ? FString() : MCPUtils::UrlDecode(Filter); FString FilterStr = Query.IsEmpty() ? FString() : MCPUtils::UrlDecode(Query);
int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 500) : 200; int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 500) : 200;

View File

@@ -22,7 +22,7 @@ class UMCPHandler_SearchUnrealClasses : public UObject, public IMCPHandler
public: public:
UPROPERTY(meta=(Optional, Description="Substring filter for class names")) UPROPERTY(meta=(Optional, Description="Substring filter for class names"))
FString Filter; FString Query;
UPROPERTY(meta=(Optional, Description="Parent class name to restrict results to subclasses")) UPROPERTY(meta=(Optional, Description="Parent class name to restrict results to subclasses"))
FString ParentClass; FString ParentClass;
@@ -69,7 +69,7 @@ public:
if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue; if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue;
FString ClassName = MCPUtils::FormatName(Class); FString ClassName = MCPUtils::FormatName(Class);
if (!Filter.IsEmpty() && !ClassName.Contains(Filter, ESearchCase::IgnoreCase)) continue; if (!Query.IsEmpty() && !ClassName.Contains(Query, ESearchCase::IgnoreCase)) continue;
TotalMatched++; TotalMatched++;
if (Matches.Num() < Limit) if (Matches.Num() < Limit)

View File

@@ -0,0 +1,99 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "UMCPHandler_SetProperties.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
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.
MCPFetcher F(Result);
UObject* Obj = F.Walk(Path).Cast<UObject>();
if (!Obj) return;
// Get the editable template (e.g. Blueprint → CDO).
UObject* Template = MCPUtils::GetEditableTemplate(Obj, Result);
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.
Template->PreEditChange(nullptr);
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++;
}
Template->PostEditChange();
// Save.
Template->MarkPackageDirty();
bool bSaved = MCPUtils::SaveGenericPackage(Obj);
Result.Appendf(TEXT("Set %d/%d properties.\n"), SuccessCount, Properties.Json->Values.Num());
if (!bSaved)
Result.Append(TEXT("Warning: Save failed\n"));
}
};

View File

@@ -11,6 +11,9 @@ class UMCPHandler_ShowCommands : public UObject, public IMCPHandler
GENERATED_BODY() GENERATED_BODY()
public: 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")) UPROPERTY(meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
bool Verbose = false; bool Verbose = false;
@@ -21,8 +24,14 @@ public:
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{ {
FString QueryLower = Query.ToLower();
for (UClass* Class : MCPUtils::CollectHandlerClasses()) for (UClass* Class : MCPUtils::CollectHandlerClasses())
{ {
FString ToolName = MCPUtils::GetToolName(Class);
if (!ToolName.ToLower().Contains(QueryLower))
continue;
if (Verbose) if (Verbose)
{ {
MCPUtils::FormatCommandHelp(Class, Result); MCPUtils::FormatCommandHelp(Class, Result);

View File

@@ -7,9 +7,9 @@
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h" #include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphNode.h"
#include "BlueprintNodeSpawner.h" #include "EdGraph/EdGraphSchema.h"
#include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_SpawnNodesInGraph.generated.h" #include "UMCPHandler_SpawnNodes.generated.h"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -33,7 +33,7 @@ struct FSpawnNodeEntry
UCLASS() UCLASS()
class UMCPHandler_SpawnNodesInGraph : public UObject, public IMCPHandler class UMCPHandler_SpawnNodes : public UObject, public IMCPHandler
{ {
GENERATED_BODY() GENERATED_BODY()
@@ -41,14 +41,14 @@ public:
UPROPERTY(meta=(Description="Path to a graph, e.g. /Game/Foo,graph:EventGraph")) UPROPERTY(meta=(Description="Path to a graph, e.g. /Game/Foo,graph:EventGraph"))
FString Graph; FString Graph;
UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use search_spawnable_node_types to find action names.")) UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use SearchSpawnableNodeTypes to find action names."))
FMCPJsonArray Nodes; FMCPJsonArray Nodes;
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
return TEXT("Create nodes in a Blueprint graph using the editor's action database. " 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, including custom K2 nodes. " "Can create ANY node type that appears in the editor's right-click menu. "
"Use search_spawnable_node_types first to find the exact action name."); "Use SearchSpawnableNodeTypes first to find the exact action name.");
} }
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
@@ -66,28 +66,27 @@ public:
if (!MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal, Result)) if (!MCPUtils::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal, Result))
continue; continue;
// Find the spawner by exact full name // Find the action by exact full name
TArray<UBlueprintNodeSpawner*> Matches = MCPUtils::SearchNodeSpawners(Entry.ActionName, 0, /*ExactMatch=*/true, TargetGraph); TArray<TSharedPtr<FEdGraphSchemaAction>> Matches = MCPUtils::SearchGraphActions(TargetGraph, Entry.ActionName, 0, /*ExactMatch=*/true);
if (Matches.Num() == 0) if (Matches.Num() == 0)
{ {
Result.Appendf(TEXT("ERROR: No action found matching '%s'. Use search_spawnable_node_types to find available actions.\n"), Result.Appendf(TEXT("ERROR: No action found matching '%s'. Use SearchSpawnableNodeTypes to find available actions.\n"),
*Entry.ActionName); *Entry.ActionName);
continue; continue;
} }
if (Matches.Num() > 1) if (Matches.Num() > 1)
{ {
Result.Appendf(TEXT("ERROR: Ambiguous: %d spawners match '%s'.\n"), Result.Appendf(TEXT("ERROR: Ambiguous: %d actions match '%s'.\n"),
Matches.Num(), *Entry.ActionName); Matches.Num(), *Entry.ActionName);
continue; continue;
} }
// Invoke the spawner // Perform the action
FVector2D Location(Entry.PosX, Entry.PosY); FVector2D Location(Entry.PosX, Entry.PosY);
IBlueprintNodeBinder::FBindingSet Bindings; UEdGraphNode* NewNode = Matches[0]->PerformAction(TargetGraph, nullptr, Location, /*bSelectNewNode=*/false);
UEdGraphNode* NewNode = Matches[0]->Invoke(TargetGraph, Bindings, Location);
if (!NewNode) if (!NewNode)
{ {
Result.Appendf(TEXT("ERROR: Spawner Invoke() returned null for '%s'.\n"), *Entry.ActionName); Result.Appendf(TEXT("ERROR: PerformAction returned null for '%s'.\n"), *Entry.ActionName);
continue; continue;
} }
@@ -99,10 +98,17 @@ public:
SuccessCount++; SuccessCount++;
} }
// Mark the owning asset as modified
UBlueprint* BP = Cast<UBlueprint>(TargetGraph->GetOuter()); UBlueprint* BP = Cast<UBlueprint>(TargetGraph->GetOuter());
if (BP) if (BP)
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
TargetGraph->NotifyGraphChanged();
UObject* Outer = TargetGraph->GetOuter();
if (Outer)
Outer->MarkPackageDirty();
Result.Appendf(TEXT("Spawned %d/%d nodes.\n"), SuccessCount, TotalCount); Result.Appendf(TEXT("Spawned %d/%d nodes.\n"), SuccessCount, TotalCount);
} }
}; };

View File

@@ -3,6 +3,7 @@
#include "Engine/World.h" #include "Engine/World.h"
#include "Engine/Level.h" #include "Engine/Level.h"
#include "Engine/LevelScriptBlueprint.h" #include "Engine/LevelScriptBlueprint.h"
#include "Materials/Material.h"
#include "MCPUtils.h" #include "MCPUtils.h"
#include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h" #include "AssetRegistry/IAssetRegistry.h"
@@ -101,12 +102,15 @@ bool MCPAssetsBase::Load()
for (const FAssetData &Asset : AssetsToLoad) for (const FAssetData &Asset : AssetsToLoad)
{ {
UObject *Obj = TryLoadAsset(Asset); UObject *Obj = TryLoadAsset(Asset);
if (Obj != nullptr) if (!Obj) continue;
{
// If this is a material open in the editor, use the editor's transient copy.
if (UMaterial* Mat = Cast<UMaterial>(Obj))
Obj = MCPUtils::ReplaceMaterialWithTransientCopy(Mat);
AssetResults.Add(Asset); AssetResults.Add(Asset);
UObjectResults.Add(Obj); UObjectResults.Add(Obj);
} }
}
if (bErrorIfNone && AssetResults.IsEmpty()) if (bErrorIfNone && AssetResults.IsEmpty())
{ {
SetError(FString::Printf(TEXT("%s '%s' exists but cannot be loaded."), *TargetClass->GetName(), SetError(FString::Printf(TEXT("%s '%s' exists but cannot be loaded."), *TargetClass->GetName(),

View File

@@ -8,6 +8,8 @@
#include "Engine/SimpleConstructionScript.h" #include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h" #include "Engine/SCS_Node.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "Materials/Material.h"
#include "MaterialGraph/MaterialGraph.h"
#include "Engine/LevelScriptBlueprint.h" #include "Engine/LevelScriptBlueprint.h"
MCPFetcher& MCPFetcher::SetError(const FString& Msg) MCPFetcher& MCPFetcher::SetError(const FString& Msg)
@@ -55,7 +57,8 @@ MCPFetcher& MCPFetcher::Walk(const FString& Path)
for (int32 i = Start; i < Segments.Num(); i++) for (int32 i = Start; i < Segments.Num(); i++)
{ {
FString Key, Value; FString Key, Value;
Segments[i].Split(TEXT(":"), &Key, &Value); if (!Segments[i].Split(TEXT(":"), &Key, &Value))
Key = Segments[i];
if (StrEq(Key, TEXT("graph"))) Graph(Value); if (StrEq(Key, TEXT("graph"))) Graph(Value);
else if (StrEq(Key, TEXT("node"))) Node(Value); else if (StrEq(Key, TEXT("node"))) Node(Value);
@@ -80,15 +83,31 @@ void MCPFetcher::LoadUAsset(const FString& PackagePath)
SetObj(LoadObject<UObject>(nullptr, *PackagePath)); SetObj(LoadObject<UObject>(nullptr, *PackagePath));
if (!Obj) if (!Obj)
SetError(FString::Printf(TEXT("Could not load asset '%s'"), *PackagePath)); SetError(FString::Printf(TEXT("Could not load asset '%s'"), *PackagePath));
// If this is a material open in the editor, use the editor's transient copy.
if (UMaterial* Mat = ::Cast<UMaterial>(Obj))
SetObj(MCPUtils::ReplaceMaterialWithTransientCopy(Mat));
} }
MCPFetcher& MCPFetcher::Graph(const FString& Value) MCPFetcher& MCPFetcher::Graph(const FString& Value)
{ {
if (bError) return *this; if (bError) return *this;
// Material with blank graph name → navigate to the material graph.
if (UMaterial* Mat = ::Cast<UMaterial>(Obj))
{
if (!Value.IsEmpty())
return SetError(FString::Printf(TEXT("Materials do not have named graphs (got '%s')"), *Value));
MCPUtils::EnsureMaterialGraph(Mat);
if (!Mat->MaterialGraph)
return SetError(FString::Printf(TEXT("Material '%s' has no material graph"), *Mat->GetName()));
SetObj(Mat->MaterialGraph);
return *this;
}
UBlueprint* BP = ::Cast<UBlueprint>(Obj); UBlueprint* BP = ::Cast<UBlueprint>(Obj);
if (!BP) if (!BP)
return TypeMismatch(TEXT("graph"), TEXT("Blueprint")); return TypeMismatch(TEXT("graph"), TEXT("Blueprint or Material"));
TArray<UEdGraph*> Matches = MCPUtils::AllGraphsNamed(BP, Value); TArray<UEdGraph*> Matches = MCPUtils::AllGraphsNamed(BP, Value);
if (Matches.Num() == 0) if (Matches.Num() == 0)

View File

@@ -34,9 +34,9 @@
#include "Handlers/UMCPHandler_DisconnectPins.h" #include "Handlers/UMCPHandler_DisconnectPins.h"
#include "Handlers/UMCPHandler_DisconnectMaterialExpressionPin.h" #include "Handlers/UMCPHandler_DisconnectMaterialExpressionPin.h"
#include "Handlers/UMCPHandler_DumpBlueprint.h" #include "Handlers/UMCPHandler_DumpBlueprint.h"
#include "Handlers/UMCPHandler_DumpProperties.h"
#include "Handlers/UMCPHandler_DumpGraphs.h" #include "Handlers/UMCPHandler_DumpGraphs.h"
#include "Handlers/UMCPHandler_DumpMaterial.h" #include "Handlers/UMCPHandler_DumpMaterial.h"
#include "Handlers/UMCPHandler_DumpMaterialExpressionGraph.h"
#include "Handlers/UMCPHandler_DumpMaterialFunction.h" #include "Handlers/UMCPHandler_DumpMaterialFunction.h"
#include "Handlers/UMCPHandler_DumpMaterialInstanceParameters.h" #include "Handlers/UMCPHandler_DumpMaterialInstanceParameters.h"
#include "Handlers/UMCPHandler_DuplicateNodesInGraph.h" #include "Handlers/UMCPHandler_DuplicateNodesInGraph.h"
@@ -47,10 +47,12 @@
#include "Handlers/UMCPHandler_ListAnimSlotNames.h" #include "Handlers/UMCPHandler_ListAnimSlotNames.h"
#include "Handlers/UMCPHandler_ListAnimSyncGroups.h" #include "Handlers/UMCPHandler_ListAnimSyncGroups.h"
#include "Handlers/UMCPHandler_ListBlueprintAssets.h" #include "Handlers/UMCPHandler_ListBlueprintAssets.h"
#include "Handlers/UMCPHandler_ListOpenAssetEditors.h"
#include "Handlers/UMCPHandler_ListBlueprintComponents.h" #include "Handlers/UMCPHandler_ListBlueprintComponents.h"
#include "Handlers/UMCPHandler_ListBlueprintInterfaces.h" #include "Handlers/UMCPHandler_ListBlueprintInterfaces.h"
#include "Handlers/UMCPHandler_ListClassProperties.h" #include "Handlers/UMCPHandler_ListClassProperties.h"
#include "Handlers/UMCPHandler_ListEventDispatchers.h" #include "Handlers/UMCPHandler_ListEventDispatchers.h"
#include "Handlers/UMCPHandler_OpenAssetEditor.h"
#include "Handlers/UMCPHandler_RefreshAllNodesInGraph.h" #include "Handlers/UMCPHandler_RefreshAllNodesInGraph.h"
#include "Handlers/UMCPHandler_RemoveAnimStateFromMachine.h" #include "Handlers/UMCPHandler_RemoveAnimStateFromMachine.h"
#include "Handlers/UMCPHandler_RemoveBlueprintComponent.h" #include "Handlers/UMCPHandler_RemoveBlueprintComponent.h"
@@ -81,7 +83,8 @@
#include "Handlers/UMCPHandler_SetMaterialInstanceParameter.h" #include "Handlers/UMCPHandler_SetMaterialInstanceParameter.h"
#include "Handlers/UMCPHandler_SetMaterialProperty.h" #include "Handlers/UMCPHandler_SetMaterialProperty.h"
#include "Handlers/UMCPHandler_SetNodeComment.h" #include "Handlers/UMCPHandler_SetNodeComment.h"
#include "Handlers/UMCPHandler_SetProperties.h"
#include "Handlers/UMCPHandler_SetNodePositions.h" #include "Handlers/UMCPHandler_SetNodePositions.h"
#include "Handlers/UMCPHandler_SetPinDefaultValues.h" #include "Handlers/UMCPHandler_SetPinDefaultValues.h"
#include "Handlers/UMCPHandler_ShowCommands.h" #include "Handlers/UMCPHandler_ShowCommands.h"
#include "Handlers/UMCPHandler_SpawnNodesInGraph.h" #include "Handlers/UMCPHandler_SpawnNodes.h"

View File

@@ -1,7 +1,5 @@
#include "MCPUtils.h" #include "MCPUtils.h"
#include "MCPHandler.h" #include "MCPHandler.h"
#include "BlueprintActionDatabase.h"
#include "BlueprintNodeSpawner.h"
#include "Dom/JsonValue.h" #include "Dom/JsonValue.h"
#include "Serialization/JsonReader.h" #include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h" #include "Serialization/JsonWriter.h"
@@ -73,6 +71,8 @@
#include "MaterialGraph/MaterialGraph.h" #include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h" #include "MaterialGraph/MaterialGraphNode.h"
#include "MaterialGraph/MaterialGraphSchema.h" #include "MaterialGraph/MaterialGraphSchema.h"
#include "IMaterialEditor.h"
#include "Subsystems/AssetEditorSubsystem.h"
// Mesh, animation, texture support // Mesh, animation, texture support
#include "Engine/StaticMesh.h" #include "Engine/StaticMesh.h"
@@ -102,7 +102,7 @@ MCPErrorCallback::MCPErrorCallback(FString& OutError)
{} {}
MCPErrorCallback::MCPErrorCallback(FStringBuilderBase& OutResult) MCPErrorCallback::MCPErrorCallback(FStringBuilderBase& OutResult)
: Func([&OutResult](const FString& Msg) { OutResult.Reset(); OutResult.Appendf(TEXT("ERROR: %s\n"), *Msg); }) : Func([&OutResult](const FString& Msg) { OutResult.Appendf(TEXT("ERROR: %s\n"), *Msg); })
{} {}
// ============================================================ // ============================================================
@@ -186,9 +186,9 @@ FString MCPUtils::FormatName(const FBPVariableDescription &Var)
return Name; return Name;
} }
FString MCPUtils::FormatName(const UClass *Class) FString MCPUtils::FormatName(const UStruct *Struct)
{ {
FString Name = Class->GetName(); FString Name = Struct->GetName();
SanitizeNameInPlace(Name); SanitizeNameInPlace(Name);
return Name; return Name;
} }
@@ -254,9 +254,19 @@ FString MCPUtils::FormatName(const UEnum *Enum)
return Name; return Name;
} }
bool MCPUtils::Identifies(const FString &Name, const UClass *Class) FString MCPUtils::FormatName(const FProperty *Prop)
{ {
return FormatName(Class).Equals(Name, ESearchCase::IgnoreCase); return Prop->GetName();
}
bool MCPUtils::Identifies(const FString &Name, const FBPVariableDescription &Var)
{
return FormatName(Var).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, const UStruct *Struct)
{
return FormatName(Struct).Equals(Name, ESearchCase::IgnoreCase);
} }
bool MCPUtils::Identifies(const FString &Name, const UMaterial *Material) bool MCPUtils::Identifies(const FString &Name, const UMaterial *Material)
@@ -314,6 +324,11 @@ bool MCPUtils::Identifies(const FString &Name, const UEnum *Enum)
return FormatName(Enum).Equals(Name, ESearchCase::IgnoreCase); return FormatName(Enum).Equals(Name, ESearchCase::IgnoreCase);
} }
bool MCPUtils::Identifies(const FString &Name, const FProperty *Prop)
{
return FormatName(Prop).Equals(Name, ESearchCase::IgnoreCase);
}
// ============================================================ // ============================================================
// Identifies // Identifies
// ============================================================ // ============================================================
@@ -1167,6 +1182,34 @@ void MCPUtils::EnsureMaterialGraph(UMaterial* Material)
} }
} }
UMaterial* MCPUtils::ReplaceMaterialWithTransientCopy(UMaterial* Material)
{
if (!Material) return nullptr;
// Already a preview material — nothing to do.
if (Material->GetOutermost() == GetTransientPackage())
return Material;
// If the material editor has a transient preview copy open, get it
// via the editor API. This follows the same pattern as Epic's
// MaterialEditingLibrary (FindMaterialEditorForAsset).
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
IAssetEditorInstance* EditorInstance = Sub ? Sub->FindEditorForAsset(Material, false) : nullptr;
if (EditorInstance)
{
// This is a weird hack. We know that the IAssetEditorInstance for a material
// is always going to be an FMaterialEditor, which conforms to IMaterialEditor.
// If that weren't the case, this unsafe code would crash hard. However,
// lots of places in unreal use this same unsafe pattern.
IMaterialEditor* MatEditor = static_cast<IMaterialEditor*>(EditorInstance);
UMaterialInterface* Edited = MatEditor->GetMaterialInterface();
if (UMaterial* EditedMat = Cast<UMaterial>(Edited))
return EditedMat;
}
return Material;
}
bool MCPUtils::SaveMaterialPackage(UMaterial* Material) bool MCPUtils::SaveMaterialPackage(UMaterial* Material)
{ {
if (!Material) return false; if (!Material) return false;
@@ -1392,70 +1435,53 @@ UAnimStateTransitionNode* MCPUtils::FindTransition(UAnimationStateMachineGraph*
} }
// ============================================================ // ============================================================
// Node spawners // Graph actions (node spawning)
// ============================================================ // ============================================================
FString MCPUtils::NodeSpawnerFullName(UBlueprintNodeSpawner* Spawner) #include "EdGraph/EdGraphSchema.h"
FString MCPUtils::ActionFullName(const TSharedPtr<FEdGraphSchemaAction>& Action)
{ {
const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec(); FString Category = Action->GetCategory().ToString();
FString Category = UiSpec.Category.ToString(); FString MenuName = Action->GetMenuDescription().ToString();
FString MenuName = UiSpec.MenuName.ToString();
if (Category.IsEmpty()) if (Category.IsEmpty())
{
return MenuName; return MenuName;
}
return Category + TEXT("|") + MenuName; return Category + TEXT("|") + MenuName;
} }
TArray<UBlueprintNodeSpawner*> MCPUtils::SearchNodeSpawners(const FString& Query, int32 MaxResults, bool ExactMatch, UEdGraph* GraphFilter) TArray<TSharedPtr<FEdGraphSchemaAction>> MCPUtils::SearchGraphActions(UEdGraph* Graph, const FString& Query, int32 MaxResults, bool ExactMatch)
{ {
FString QueryLower = Query.ToLower(); FString QueryLower = Query.ToLower();
TArray<UBlueprintNodeSpawner*> Result; TArray<TSharedPtr<FEdGraphSchemaAction>> Result;
for (const auto& Pair : FBlueprintActionDatabase::Get().GetAllActions()) FGraphContextMenuBuilder ContextMenuBuilder(Graph);
{ Graph->GetSchema()->GetGraphContextActions(ContextMenuBuilder);
for (UBlueprintNodeSpawner* Spawner : Pair.Value)
{
if (!Spawner) continue;
if (Spawner->PrimeDefaultUiSpec().MenuName.IsEmpty()) continue;
// Filter by graph compatibility if a graph was provided for (int32 i = 0; i < ContextMenuBuilder.GetNumActions(); i++)
if (GraphFilter && Spawner->NodeClass)
{ {
UEdGraphNode* NodeCDO = CastChecked<UEdGraphNode>(Spawner->NodeClass->ClassDefaultObject); TSharedPtr<FEdGraphSchemaAction> Action = ContextMenuBuilder.GetSchemaAction(i);
if (!NodeCDO->IsCompatibleWithGraph(GraphFilter)) if (!Action.IsValid()) continue;
{
continue;
}
}
FString FullName = NodeSpawnerFullName(Spawner); FString FullName = ActionFullName(Action);
if (FullName.IsEmpty()) continue;
if (ExactMatch) if (ExactMatch)
{ {
if (FullName.ToLower() != QueryLower) if (FullName.ToLower() != QueryLower)
{
continue; continue;
} }
}
else else
{ {
FString Keywords = Spawner->PrimeDefaultUiSpec().Keywords.ToString(); FString Keywords = Action->GetKeywords().ToString();
if (!FullName.ToLower().Contains(QueryLower) if (!FullName.ToLower().Contains(QueryLower) && !Keywords.ToLower().Contains(QueryLower))
&& !Keywords.ToLower().Contains(QueryLower))
{
continue; continue;
} }
}
Result.Add(Spawner);
Result.Add(Action);
if ((MaxResults > 0) && (Result.Num() >= MaxResults)) if ((MaxResults > 0) && (Result.Num() >= MaxResults))
{
break; break;
} }
}
}
return Result; return Result;
} }
@@ -1734,6 +1760,121 @@ FString MCPUtils::FormatPropertyType(FProperty* Prop)
return TEXT("string"); return TEXT("string");
} }
// ============================================================
// GetEditableTemplate
// ============================================================
UObject* MCPUtils::GetEditableTemplate(UObject* Obj, MCPErrorCallback Error)
{
if (!Obj)
{
Error.SetError(TEXT("Object is null"));
return nullptr;
}
// Blueprint → operate on the Class Default Object.
if (UBlueprint* BP = Cast<UBlueprint>(Obj))
{
if (!BP->GeneratedClass)
{
Error.SetError(FString::Printf(TEXT("Blueprint '%s' has no GeneratedClass"), *Obj->GetName()));
return nullptr;
}
return BP->GeneratedClass->GetDefaultObject();
}
// These asset types are safe to edit directly.
if (Cast<UMaterial>(Obj)) return Obj;
if (Cast<UMaterialInstance>(Obj)) return Obj;
if (Cast<UStaticMesh>(Obj)) return Obj;
if (Cast<USkeletalMesh>(Obj)) return Obj;
if (Cast<UTexture>(Obj)) return Obj;
if (Cast<UActorComponent>(Obj)) return Obj;
// Unknown type — refuse for safety.
Error.SetError(FString::Printf(TEXT("Object type '%s' is not supported for generic property editing"), *Obj->GetClass()->GetName()));
return nullptr;
}
// ============================================================
// FindPropertyByName
// ============================================================
FProperty* MCPUtils::FindPropertyByName(UObject* Obj, const FString& Name, MCPErrorCallback Error)
{
if (!Obj)
{
Error.SetError(TEXT("Object is null"));
return nullptr;
}
FProperty* Found = nullptr;
for (TFieldIterator<FProperty> PropIt(Obj->GetClass()); PropIt; ++PropIt)
{
if (!Identifies(Name, *PropIt)) continue;
if (Found)
{
Error.SetError(FString::Printf(TEXT("Ambiguous property '%s' on %s"), *Name, *FormatName(Obj->GetClass())));
return nullptr;
}
Found = *PropIt;
}
if (!Found)
Error.SetError(FString::Printf(TEXT("Property '%s' not found on %s"), *Name, *FormatName(Obj->GetClass())));
return Found;
}
// ============================================================
// GetPropertyValueText
// ============================================================
FString MCPUtils::GetPropertyValueText(UObject* Container, FProperty* Prop)
{
FString Result;
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
Prop->ExportTextItem_Direct(Result, ValuePtr, nullptr, Container, PPF_None);
return Result;
}
// ============================================================
// SetPropertyValueText
// ============================================================
bool MCPUtils::SetPropertyValueText(UObject* Container, FProperty* Prop, const FString& Value, MCPErrorCallback Error)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, Container, PPF_None);
if (!ImportResult)
{
Error.SetError(FString::Printf(TEXT("Failed to parse '%s' for property '%s' (type: %s)"),
*Value, *FormatName(Prop), *Prop->GetCPPType()));
return false;
}
return true;
}
// ============================================================
// BlueprintVisibleProperties
// ============================================================
TArray<FProperty*> MCPUtils::BlueprintVisibleProperties(UObject* Obj, const FString& Query)
{
TArray<FProperty*> Result;
if (!Obj) return Result;
for (TFieldIterator<FProperty> PropIt(Obj->GetClass()); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
if (!Prop) continue;
if (!Prop->HasAnyPropertyFlags(CPF_BlueprintVisible)) continue;
if (!Query.IsEmpty() && !FormatName(Prop).Contains(Query, ESearchCase::IgnoreCase))
continue;
Result.Add(Prop);
}
return Result;
}
// ============================================================ // ============================================================
// FormatCommandHelp — verbose description of one handler command // FormatCommandHelp — verbose description of one handler command
// ============================================================ // ============================================================

View File

@@ -12,7 +12,7 @@ class UMaterial;
class UMaterialInstance; class UMaterialInstance;
class UMaterialFunction; class UMaterialFunction;
class UMaterialExpression; class UMaterialExpression;
class UBlueprintNodeSpawner; struct FEdGraphSchemaAction;
class UAnimationStateMachineGraph; class UAnimationStateMachineGraph;
class UAnimStateNode; class UAnimStateNode;
class UAnimStateTransitionNode; class UAnimStateTransitionNode;
@@ -122,7 +122,7 @@ public:
static FString FormatName(const UEdGraphPin *Pin); static FString FormatName(const UEdGraphPin *Pin);
static FString FormatName(const FMemberReference &Ref); static FString FormatName(const FMemberReference &Ref);
static FString FormatName(const FBPVariableDescription &Var); static FString FormatName(const FBPVariableDescription &Var);
static FString FormatName(const UClass *Class); static FString FormatName(const UStruct *Struct);
static FString FormatName(const UMaterial *Material); static FString FormatName(const UMaterial *Material);
static FString FormatName(const UMaterialInstance *MaterialInstance); static FString FormatName(const UMaterialInstance *MaterialInstance);
static FString FormatName(const UMaterialFunction *MaterialFunction); static FString FormatName(const UMaterialFunction *MaterialFunction);
@@ -134,6 +134,7 @@ public:
static FString FormatName(const UTexture *Texture); static FString FormatName(const UTexture *Texture);
static FString FormatName(const UScriptStruct *Struct); static FString FormatName(const UScriptStruct *Struct);
static FString FormatName(const UEnum *Enum); static FString FormatName(const UEnum *Enum);
static FString FormatName(const FProperty *Prop);
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
// //
@@ -154,7 +155,8 @@ public:
static bool Identifies(const FString &Name, const UEdGraphNode* Node); static bool Identifies(const FString &Name, const UEdGraphNode* Node);
static bool Identifies(const FString &Name, const UEdGraphPin *Pin); static bool Identifies(const FString &Name, const UEdGraphPin *Pin);
static bool Identifies(const FString &Name, const FMemberReference &Ref); static bool Identifies(const FString &Name, const FMemberReference &Ref);
static bool Identifies(const FString &Name, const UClass *Class); static bool Identifies(const FString &Name, const FBPVariableDescription &Var);
static bool Identifies(const FString &Name, const UStruct *Struct);
static bool Identifies(const FString &Name, const UMaterial *Material); static bool Identifies(const FString &Name, const UMaterial *Material);
static bool Identifies(const FString &Name, const UMaterialInstance *MaterialInstance); static bool Identifies(const FString &Name, const UMaterialInstance *MaterialInstance);
static bool Identifies(const FString &Name, const UMaterialFunction *MaterialFunction); static bool Identifies(const FString &Name, const UMaterialFunction *MaterialFunction);
@@ -166,6 +168,7 @@ public:
static bool Identifies(const FString &Name, const UTexture *Texture); static bool Identifies(const FString &Name, const UTexture *Texture);
static bool Identifies(const FString &Name, const UScriptStruct *Struct); static bool Identifies(const FString &Name, const UScriptStruct *Struct);
static bool Identifies(const FString &Name, const UEnum *Enum); static bool Identifies(const FString &Name, const UEnum *Enum);
static bool Identifies(const FString &Name, const FProperty *Prop);
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
@@ -265,14 +268,30 @@ public:
static bool SaveMaterialPackage(UMaterial* Material); static bool SaveMaterialPackage(UMaterial* Material);
static bool SaveGenericPackage(UObject* Asset); static bool SaveGenericPackage(UObject* Asset);
// If the material editor has a transient preview copy of this material,
// return that copy (which is what the editor is actually working on).
// Otherwise return the original.
static UMaterial* ReplaceMaterialWithTransientCopy(UMaterial* Material);
// ----- Anim blueprint helpers ----- // ----- Anim blueprint helpers -----
static UAnimationStateMachineGraph* FindStateMachineGraph(UBlueprint* BP, const FString& GraphName); static UAnimationStateMachineGraph* FindStateMachineGraph(UBlueprint* BP, const FString& GraphName);
static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName, MCPErrorCallback Error); static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName, MCPErrorCallback Error);
static UAnimStateTransitionNode* FindTransition(UAnimationStateMachineGraph* SMGraph, const FString& FromStateName, const FString& ToStateName); static UAnimStateTransitionNode* FindTransition(UAnimationStateMachineGraph* SMGraph, const FString& FromStateName, const FString& ToStateName);
// ----- Node spawners ----- // ----- Graph actions (node spawning) -----
static FString NodeSpawnerFullName(UBlueprintNodeSpawner* Spawner); static FString ActionFullName(const TSharedPtr<FEdGraphSchemaAction>& Action);
static TArray<UBlueprintNodeSpawner*> SearchNodeSpawners(const FString& Query, int32 MaxResults = 0, bool ExactMatch = false, UEdGraph* GraphFilter = nullptr); static TArray<TSharedPtr<FEdGraphSchemaAction>> SearchGraphActions(UEdGraph* Graph, const FString& Query, int32 MaxResults = 0, bool ExactMatch = false);
// ----- Editable template -----
// Given an object, returns the appropriate template object for generic
// property editing, or nullptr if the type isn't whitelisted.
// UBlueprint → CDO; UMaterial, UActorComponent, etc. → as-is.
static UObject* GetEditableTemplate(UObject* Obj, MCPErrorCallback Error);
static TArray<FProperty*> BlueprintVisibleProperties(UObject* Obj, const FString& Query = FString());
static FProperty* FindPropertyByName(UObject* Obj, const FString& Name, MCPErrorCallback Error = nullptr);
static FString GetPropertyValueText(UObject* Container, FProperty* Prop);
static bool SetPropertyValueText(UObject* Container, FProperty* Prop, const FString& Value, MCPErrorCallback Error = nullptr);
// ----- Property population ----- // ----- Property population -----
static FString PropertyNameToJsonKey(const FString& PropName); static FString PropertyNameToJsonKey(const FString& PropName);

View File

@@ -1,92 +0,0 @@
#!/usr/bin/env python3
"""Split a handler source file into one file per UMCPHandler class.
Usage: python3 tools/split-handlers.py <source_file>
Reads the #include block from the top of the file, then splits at the
closing }; (column 0) of each UMCPHandler class. Each output file is
named after the handler class and placed in the same directory. The
shared #include block is prepended to every output file.
"""
import sys
import re
import os
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <source_file>")
sys.exit(1)
filepath = sys.argv[1]
with open(filepath) as f:
lines = f.readlines()
# Extract the #include block from the top of the file.
# Collect #pragma, #include, blank lines, and // comment lines
# until we hit something else.
include_block = []
body_start = 0
for i, line in enumerate(lines):
stripped = line.strip()
if (stripped == '' or stripped.startswith('#pragma') or
stripped.startswith('#include') or stripped.startswith('//')):
include_block.append(line)
else:
body_start = i
break
# Strip trailing blank/comment lines from include block
while include_block and include_block[-1].strip() in ('', ) or (
include_block and include_block[-1].strip().startswith('//')):
body_start -= 1
include_block.pop()
if not include_block:
break
# Remove the .generated.h include from the shared block (each output
# file gets its own).
include_block = [l for l in include_block if '.generated.h' not in l]
body_lines = lines[body_start:]
# Find split points in the body: after each }; that closes a UMCPHandler.
handler_name = None
splits = [] # list of (end_index_exclusive_in_body, class_name)
for i, line in enumerate(body_lines):
m = re.match(r'class (UMCPHandler_\w+)', line)
if m:
handler_name = m.group(1)
if line.startswith('};') and handler_name:
splits.append((i + 1, handler_name))
handler_name = None
if not splits:
print("No UMCPHandler classes found.")
sys.exit(1)
dirname = os.path.dirname(filepath)
start = 0
for end, name in splits:
outpath = os.path.join(dirname, name + '.h')
with open(outpath, 'w') as f:
f.writelines(include_block)
f.write(f'#include "{name}.generated.h"\n')
f.write('\n')
f.writelines(body_lines[start:end])
print(f" {name}.h ({end - start} lines)")
start = end
# Warn about trailing content
if start < len(body_lines):
remaining = ''.join(body_lines[start:]).strip()
if remaining:
print(f"Warning: {len(body_lines) - start} trailing lines not included in any output file:")
for line in body_lines[start:]:
print(f" | {line}", end='')
if __name__ == '__main__':
main()