#include "DruidsMCPBridgeServer.h"

#include "AssetRegistry/AssetRegistryModule.h"
#include "Components/ActorComponent.h"
#include "Components/MeshComponent.h"
#include "Components/PrimitiveComponent.h"
#include "Components/SceneComponent.h"
#include "Components/SplineComponent.h"
#include "Components/StaticMeshComponent.h"
#include "DruidsMCPBridge.h"
#include "Engine/Blueprint.h"
#include "Engine/DataAsset.h"
#include "Engine/StaticMesh.h"
#include "Engine/World.h"
#include "Editor.h"
#include "EditorViewportClient.h"
#include "EdGraphNode_Comment.h"
#include "Engine/Texture.h"
#include "GameFramework/Actor.h"
#include "Engine/SCS_Node.h"
#include "Engine/SimpleConstructionScript.h"
#include "Extensions/DruidsMCPBridgeSageExtensionRegistry.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "HttpPath.h"
#include "HttpServerModule.h"
#include "HttpServerResponse.h"
#include "IHttpRouter.h"
#include "IAssetViewport.h"
#include "ImageUtils.h"
#include "Interfaces/IPluginManager.h"
#include "InputCoreTypes.h"
#include "Interfaces/IPv4/IPv4Endpoint.h"
#include "K2Node_CallFunction.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_ExecutionSequence.h"
#include "K2Node_Event.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_FunctionResult.h"
#include "K2Node_IfThenElse.h"
#include "K2Node_InputAxisKeyEvent.h"
#include "K2Node_InputKey.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_Variable.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "Kismet/KismetMathLibrary.h"
#include "Kismet/KismetStringLibrary.h"
#include "Kismet2/CompilerResultsLog.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Layout/SlateRect.h"
#include "LevelEditor.h"
#include "MaterialDomain.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionComment.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialInstance.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialInterface.h"
#include "Misc/PackageName.h"
#include "Misc/EngineVersion.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "UObject/NoExportTypes.h"
#include "UObject/SavePackage.h"
#include "UObject/UnrealType.h"
#include "UObject/UObjectIterator.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#include "SocketSubsystem.h"
#include "Sockets.h"
#include "Subsystems/EditorActorSubsystem.h"

namespace DruidsMCPBridge
{
	static constexpr uint16 DefaultPort = 8765;
	static constexpr uint16 MaxPortAttempts = 25;
	static const TCHAR* EndpointPath = TEXT("/mcp");
	static const TCHAR* JsonRpcVersion = TEXT("2.0");
	static const TCHAR* StandardMacrosBlueprintPath = TEXT("/Engine/EditorBlueprintResources/StandardMacros.StandardMacros");
	static const TCHAR* StandardMacrosSpawnerPath = TEXT("/Engine/EditorBlueprintResources/StandardMacros");

	static bool IsSupportedStandardFlowMacro(const FString& MacroName)
	{
		return MacroName.Equals(TEXT("DoOnce"), ESearchCase::IgnoreCase) ||
			MacroName.Equals(TEXT("Gate"), ESearchCase::IgnoreCase) ||
			MacroName.Equals(TEXT("FlipFlop"), ESearchCase::IgnoreCase);
	}

	static FString GetCanonicalStandardMacroName(const FString& MacroName)
	{
		if (MacroName.Equals(TEXT("DoOnce"), ESearchCase::IgnoreCase))
		{
			return TEXT("DoOnce");
		}
		if (MacroName.Equals(TEXT("Gate"), ESearchCase::IgnoreCase))
		{
			return TEXT("Gate");
		}
		if (MacroName.Equals(TEXT("FlipFlop"), ESearchCase::IgnoreCase))
		{
			return TEXT("FlipFlop");
		}
		return MacroName;
	}

	static FString GetStandardMacroSpawnerId(const FString& MacroName)
	{
		return FString::Printf(TEXT("macro::%s:%s"), StandardMacrosSpawnerPath, *GetCanonicalStandardMacroName(MacroName));
	}

	static FString NormalizeInputKeyQueryText(FString Text)
	{
		Text.TrimStartAndEndInline();
		Text.ToLowerInline();
		Text.ReplaceInline(TEXT(" "), TEXT(""));
		Text.ReplaceInline(TEXT("_"), TEXT(""));
		Text.ReplaceInline(TEXT("-"), TEXT(""));
		Text.ReplaceInline(TEXT("("), TEXT(""));
		Text.ReplaceInline(TEXT(")"), TEXT(""));
		return Text;
	}

	static bool TryResolveInputKeyNameFromQuery(const FString& Query, FString& OutKeyName)
	{
		if (Query.StartsWith(TEXT("input_key::"), ESearchCase::IgnoreCase))
		{
			OutKeyName = Query.RightChop(11).TrimStartAndEnd();
			return !OutKeyName.IsEmpty();
		}

		FString Normalized = NormalizeInputKeyQueryText(Query);
		Normalized.ReplaceInline(TEXT("keyboard"), TEXT(""));
		Normalized.ReplaceInline(TEXT("input"), TEXT(""));
		Normalized.ReplaceInline(TEXT("event"), TEXT(""));
		Normalized.ReplaceInline(TEXT("pressed"), TEXT(""));
		Normalized.ReplaceInline(TEXT("press"), TEXT(""));
		Normalized.ReplaceInline(TEXT("key"), TEXT(""));

		if (Normalized.Len() == 1)
		{
			const TCHAR C = Normalized[0];
			if ((C >= TEXT('a') && C <= TEXT('z')) || (C >= TEXT('0') && C <= TEXT('9')))
			{
				OutKeyName = Normalized.ToUpper();
				return true;
			}
		}
		if (Normalized == TEXT("space") || Normalized == TEXT("spacebar"))
		{
			OutKeyName = TEXT("SpaceBar");
			return true;
		}
		if (Normalized == TEXT("escape") || Normalized == TEXT("esc"))
		{
			OutKeyName = TEXT("Escape");
			return true;
		}
		if (Normalized == TEXT("enter") || Normalized == TEXT("return"))
		{
			OutKeyName = TEXT("Enter");
			return true;
		}
		return false;
	}

	static FString MakeBlueprintObjectPath(const FString& PackagePath)
	{
		const FString PackageName = FPackageName::ObjectPathToPackageName(PackagePath);
		return FString::Printf(TEXT("%s.%s"), *PackageName, *FPackageName::GetShortName(PackageName));
	}

	static FString MakeObjectPathIfPackagePath(const FString& RawPath)
	{
		const FString Path = RawPath.TrimStartAndEnd();
		if (Path.IsEmpty() || Path.Contains(TEXT(".")))
		{
			return Path;
		}
		return FString::Printf(TEXT("%s.%s"), *Path, *FPackageName::GetShortName(Path));
	}

	static FString ResolvePrimitiveStaticMeshPath(const FString& PrimitiveShape)
	{
		const FString Shape = PrimitiveShape.ToLower().TrimStartAndEnd();
		if (Shape == TEXT("cube")) { return TEXT("/Engine/BasicShapes/Cube"); }
		if (Shape == TEXT("sphere")) { return TEXT("/Engine/BasicShapes/Sphere"); }
		if (Shape == TEXT("cylinder")) { return TEXT("/Engine/BasicShapes/Cylinder"); }
		if (Shape == TEXT("cone")) { return TEXT("/Engine/BasicShapes/Cone"); }
		if (Shape == TEXT("plane")) { return TEXT("/Engine/BasicShapes/Plane"); }
		return TEXT("");
	}

	template <typename AssetType>
	static AssetType* LoadAssetByPackageOrObjectPath(const FString& RawPath)
	{
		const FString Path = RawPath.TrimStartAndEnd();
		if (Path.IsEmpty())
		{
			return nullptr;
		}
		if (AssetType* Asset = LoadObject<AssetType>(nullptr, *Path))
		{
			return Asset;
		}
		return LoadObject<AssetType>(nullptr, *MakeObjectPathIfPackagePath(Path));
	}

	static USCS_Node* FindSCSNodeByName(UBlueprint* Blueprint, const FString& ComponentName)
	{
		if (!Blueprint || !Blueprint->SimpleConstructionScript)
		{
			return nullptr;
		}
		for (USCS_Node* Node : Blueprint->SimpleConstructionScript->GetAllNodes())
		{
			if (Node && Node->GetVariableName().ToString().Equals(ComponentName, ESearchCase::IgnoreCase))
			{
				return Node;
			}
		}
		return nullptr;
	}

	static bool HasBlueprintVariableOrComponent(UBlueprint* Blueprint, const FString& Name)
	{
		if (!Blueprint || Name.IsEmpty())
		{
			return false;
		}
		if (FindFProperty<FProperty>(Blueprint->SkeletonGeneratedClass, FName(*Name)) ||
			FindFProperty<FProperty>(Blueprint->GeneratedClass, FName(*Name)))
		{
			return true;
		}
		return FindSCSNodeByName(Blueprint, Name) != nullptr;
	}

	static TSharedPtr<FJsonObject> MakeVectorObject(const FVector& Value)
	{
		TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
		Object->SetNumberField(TEXT("x"), Value.X);
		Object->SetNumberField(TEXT("y"), Value.Y);
		Object->SetNumberField(TEXT("z"), Value.Z);
		return Object;
	}

	static TSharedPtr<FJsonObject> MakeBoxObject(const FBox& Box)
	{
		TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
		Object->SetObjectField(TEXT("min"), MakeVectorObject(Box.Min));
		Object->SetObjectField(TEXT("max"), MakeVectorObject(Box.Max));
		Object->SetObjectField(TEXT("center"), MakeVectorObject(Box.GetCenter()));
		Object->SetObjectField(TEXT("extent"), MakeVectorObject(Box.GetExtent()));
		return Object;
	}

	static TSharedPtr<FJsonObject> MakeRotatorObject(const FRotator& Value)
	{
		TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
		Object->SetNumberField(TEXT("pitch"), Value.Pitch);
		Object->SetNumberField(TEXT("yaw"), Value.Yaw);
		Object->SetNumberField(TEXT("roll"), Value.Roll);
		return Object;
	}

	static TSharedPtr<FJsonObject> MakeTransformObject(const FTransform& Transform)
	{
		TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
		Object->SetObjectField(TEXT("location"), MakeVectorObject(Transform.GetLocation()));
		Object->SetObjectField(TEXT("rotation"), MakeRotatorObject(Transform.Rotator()));
		Object->SetObjectField(TEXT("scale"), MakeVectorObject(Transform.GetScale3D()));
		return Object;
	}

	static double GetOptionalNumberField(const TSharedPtr<FJsonObject>& Object, const FString& FieldName, double DefaultValue)
	{
		if (!Object.IsValid())
		{
			return DefaultValue;
		}
		double Value = DefaultValue;
		if (Object->TryGetNumberField(FieldName, Value))
		{
			return Value;
		}
		FString StringValue;
		if (Object->TryGetStringField(FieldName, StringValue) && !StringValue.TrimStartAndEnd().IsEmpty())
		{
			return FCString::Atod(*StringValue);
		}
		return DefaultValue;
	}

	static bool GetOptionalBoolField(const TSharedPtr<FJsonObject>& Object, const FString& FieldName, bool DefaultValue)
	{
		if (!Object.IsValid())
		{
			return DefaultValue;
		}
		bool Value = DefaultValue;
		if (Object->TryGetBoolField(FieldName, Value))
		{
			return Value;
		}
		FString StringValue;
		if (Object->TryGetStringField(FieldName, StringValue))
		{
			StringValue.TrimStartAndEndInline();
			if (StringValue.Equals(TEXT("true"), ESearchCase::IgnoreCase) || StringValue == TEXT("1") || StringValue.Equals(TEXT("yes"), ESearchCase::IgnoreCase))
			{
				return true;
			}
			if (StringValue.Equals(TEXT("false"), ESearchCase::IgnoreCase) || StringValue == TEXT("0") || StringValue.Equals(TEXT("no"), ESearchCase::IgnoreCase))
			{
				return false;
			}
		}
		return DefaultValue;
	}

	static FVector GetVectorFromArgs(const TSharedPtr<FJsonObject>& Arguments, const TCHAR* Prefix, const FVector& DefaultValue)
	{
		return FVector(
			GetOptionalNumberField(Arguments, FString::Printf(TEXT("%sX"), Prefix), DefaultValue.X),
			GetOptionalNumberField(Arguments, FString::Printf(TEXT("%sY"), Prefix), DefaultValue.Y),
			GetOptionalNumberField(Arguments, FString::Printf(TEXT("%sZ"), Prefix), DefaultValue.Z));
	}

	static FRotator GetRotatorFromArgs(const TSharedPtr<FJsonObject>& Arguments, const FRotator& DefaultValue)
	{
		return FRotator(
			GetOptionalNumberField(Arguments, TEXT("RotationPitch"), DefaultValue.Pitch),
			GetOptionalNumberField(Arguments, TEXT("RotationYaw"), DefaultValue.Yaw),
			GetOptionalNumberField(Arguments, TEXT("RotationRoll"), DefaultValue.Roll));
	}

	static FString GetLevelViewportTypeName(ELevelViewportType ViewportType)
	{
		switch (ViewportType)
		{
		case LVT_OrthoXY:
			return TEXT("OrthoXY");
		case LVT_OrthoXZ:
			return TEXT("OrthoXZ");
		case LVT_OrthoYZ:
			return TEXT("OrthoYZ");
		case LVT_Perspective:
			return TEXT("Perspective");
		case LVT_OrthoFreelook:
			return TEXT("OrthoFreelook");
		case LVT_OrthoNegativeXY:
			return TEXT("OrthoNegativeXY");
		case LVT_OrthoNegativeXZ:
			return TEXT("OrthoNegativeXZ");
		case LVT_OrthoNegativeYZ:
			return TEXT("OrthoNegativeYZ");
		default:
			return FString::Printf(TEXT("Unknown_%d"), static_cast<int32>(ViewportType));
		}
	}

	static TSharedPtr<FJsonObject> GetPCGAvailabilitySummary()
	{
		const TSharedPtr<IPlugin> PCGPlugin = IPluginManager::Get().FindPlugin(TEXT("PCG"));
		const bool bInstalled = PCGPlugin.IsValid();
		const bool bEnabled = bInstalled && PCGPlugin->IsEnabled();
		const bool bModuleExists = FModuleManager::Get().ModuleExists(TEXT("PCG"));
		const bool bModuleLoaded = FModuleManager::Get().IsModuleLoaded(TEXT("PCG"));
		TSharedPtr<FJsonObject> Status = MakeShared<FJsonObject>();
		Status->SetBoolField(TEXT("pcg_installed"), bInstalled);
		Status->SetBoolField(TEXT("pcg_enabled"), bEnabled);
		Status->SetBoolField(TEXT("pcg_module_exists"), bModuleExists);
		Status->SetBoolField(TEXT("pcg_module_loaded"), bModuleLoaded);
		Status->SetBoolField(TEXT("pcg_available"), bInstalled && bEnabled && bModuleExists);
		Status->SetStringField(TEXT("plugin_name"), PCGPlugin.IsValid() ? PCGPlugin->GetName() : TEXT("PCG"));
		return Status;
	}

	static TArray<TSharedPtr<FJsonValue>> MakeLocalStringArray(const TArray<FString>& Strings)
	{
		TArray<TSharedPtr<FJsonValue>> Values;
		for (const FString& String : Strings)
		{
			Values.Add(MakeShared<FJsonValueString>(String));
		}
		return Values;
	}

	static TSharedPtr<FJsonObject> MakePCGRoadCapabilitiesSummary()
	{
		const TSharedPtr<FJsonObject> Availability = GetPCGAvailabilitySummary();
		const bool bPCGAvailable = Availability.IsValid() && Availability->GetBoolField(TEXT("pcg_available"));
		const bool bSpawnActorNodeClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGSpawnActorNode")) != nullptr
			|| LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGNode")) != nullptr;
		const bool bSpawnActorSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGSpawnActorSettings")) != nullptr;
		const bool bCreatePointsGridSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGCreatePointsGridSettings")) != nullptr;
		const bool bTransformPointsSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGTransformPointsSettings")) != nullptr;
		const bool bSplineSamplerSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGSplineSamplerSettings")) != nullptr
			|| LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGGetSplineDataSettings")) != nullptr;
		const bool bGridRoadClassesAvailable = bPCGAvailable && bCreatePointsGridSettingsClass && bSpawnActorNodeClass && bSpawnActorSettingsClass;

		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("tool"), TEXT("pcg_get_road_capabilities"));
		Result->SetObjectField(TEXT("pcg_status"), Availability);
		Result->SetStringField(TEXT("support_level"), TEXT("slice_85_city_update_visual_qa"));
		Result->SetStringField(TEXT("repro_package_path"), TEXT("/Game/DruidsRepro/CityPCG"));
		Result->SetBoolField(TEXT("pcg_available"), bPCGAvailable);
		Result->SetBoolField(TEXT("road_recipe_available"), bGridRoadClassesAvailable);
		Result->SetBoolField(TEXT("city_layout_recipe_available"), bGridRoadClassesAvailable);
		Result->SetBoolField(TEXT("city_update_recipe_available"), bGridRoadClassesAvailable);
		Result->SetBoolField(TEXT("spline_road_proof_available"), bGridRoadClassesAvailable);
		Result->SetBoolField(TEXT("true_spline_road_sampling_supported"), false);
		Result->SetBoolField(TEXT("arbitrary_road_network_editing"), false);
		Result->SetBoolField(TEXT("landscape_projection"), false);
		Result->SetBoolField(TEXT("pie_execution"), false);

		TSharedPtr<FJsonObject> NodeClasses = MakeShared<FJsonObject>();
		NodeClasses->SetBoolField(TEXT("create_points_grid_settings"), bCreatePointsGridSettingsClass);
		NodeClasses->SetBoolField(TEXT("transform_points_settings"), bTransformPointsSettingsClass);
		NodeClasses->SetBoolField(TEXT("spawn_actor_node_or_base_node"), bSpawnActorNodeClass);
		NodeClasses->SetBoolField(TEXT("spawn_actor_settings"), bSpawnActorSettingsClass);
		NodeClasses->SetBoolField(TEXT("spline_sampler_or_get_spline_data_settings"), bSplineSamplerSettingsClass);
		Result->SetObjectField(TEXT("detected_pcg_classes"), NodeClasses);

		TSharedPtr<FJsonObject> RoadSupport = MakeShared<FJsonObject>();
		RoadSupport->SetStringField(TEXT("road_segments_from_grid"), bGridRoadClassesAvailable ? TEXT("supported") : TEXT("unavailable"));
		RoadSupport->SetStringField(TEXT("road_intersections_from_grid"), bGridRoadClassesAvailable ? TEXT("supported_single_crossing") : TEXT("unavailable"));
		RoadSupport->SetStringField(TEXT("building_lot_exclusion"), bGridRoadClassesAvailable ? TEXT("supported_quadrant_lots") : TEXT("unavailable"));
		RoadSupport->SetStringField(TEXT("spline_road_sampling"), bSplineSamplerSettingsClass ? TEXT("unverified") : TEXT("unavailable"));
		RoadSupport->SetStringField(TEXT("spline_bounds_road_proof"), bGridRoadClassesAvailable ? TEXT("supported_bounds_from_spline_rectangle") : TEXT("unavailable"));
		RoadSupport->SetStringField(TEXT("spline_road_authoring"), TEXT("proof_gate_later_slice"));
		RoadSupport->SetStringField(TEXT("continuous_spline_mesh_roads"), TEXT("unsupported"));
		RoadSupport->SetStringField(TEXT("arbitrary_freeform_roads"), TEXT("unsupported"));
		RoadSupport->SetStringField(TEXT("first_mutating_slice"), TEXT("pcg_run_road_recipe"));
		RoadSupport->SetStringField(TEXT("city_layout_recipe"), bGridRoadClassesAvailable ? TEXT("supported_crossroads_with_quadrant_building_lots") : TEXT("unavailable"));
		Result->SetObjectField(TEXT("road_support"), RoadSupport);

		TArray<TSharedPtr<FJsonValue>> Boundaries;
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Slice 80 supports one straight road recipe that creates PCG-owned spawned road segment actors.")));
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Slice 81 supports one city layout recipe that composes a straight PCG road strip with a PCG skyscraper grid.")));
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Slice 82 adds one PCG-owned intersection actor and road-network metadata for horizontal/vertical road strips.")));
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Slice 83 adds deterministic quadrant building lots offset from road corridors, with per-lot clearance metadata and PCG-owned skyscraper output.")));
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Slice 84 adds a spline road proof gate: a visible spline rectangle can drive straight road strip generation through bounds_from_spline_rectangle, but true curved PCG spline sampling remains unsupported/unverified.")));
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Slice 85 adds an update recipe for existing Druids repro cities: it recreates the proven city layout with new area/density/seed settings and returns before/after counts plus a fresh capture.")));
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("The first shippable road path will use PCG Create Points Grid plus native Spawn Actor to spawn road segment actors owned by PCG managed output.")));
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Spline roads remain a planned follow-up slice.")));
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Spline sampling is reported as unverified unless PCG spline-related settings classes are discoverable; even then it remains a later proof gate.")));
		Boundaries.Add(MakeShared<FJsonValueString>(TEXT("No arbitrary PCG graph editing, landscape projection, continuous spline mesh roads, or PIE execution.")));
		Result->SetArrayField(TEXT("boundaries"), Boundaries);

		Result->SetArrayField(TEXT("recommended_next_tools"), MakeLocalStringArray({
			TEXT("pcg_get_status"),
			TEXT("blueprint_create_road_actor"),
			TEXT("blueprint_create_skyscraper_actor"),
			TEXT("pcg_run_road_recipe"),
			TEXT("pcg_run_spline_road_proof"),
			TEXT("pcg_run_city_layout_recipe"),
			TEXT("pcg_update_city_layout_recipe")
		}));
		Result->SetArrayField(TEXT("planned_next_tools"), MakeLocalStringArray({
			TEXT("pcg_true_spline_road_sampling")
		}));
		Result->SetStringField(TEXT("next_step"), bGridRoadClassesAvailable
			? TEXT("Call pcg_run_city_layout_recipe for a one-call PCG road plus skyscraper layout, pcg_run_road_recipe for road-only validation, or pcg_run_spline_road_proof to test spline-rectangle driven road bounds.")
			: TEXT("PCG road segment generation is not available until PCG is enabled and grid/spawn-actor classes are discoverable."));
		return Result;
	}

	static FString GetOptionalStringField(const TSharedPtr<FJsonObject>& Object, const FString& FieldName, const FString& DefaultValue = TEXT(""))
	{
		if (!Object.IsValid())
		{
			return DefaultValue;
		}
		FString Value;
		return Object->TryGetStringField(FieldName, Value) ? Value : DefaultValue;
	}

	static int32 GetOptionalIntField(const TSharedPtr<FJsonObject>& Object, const FString& FieldName, int32 DefaultValue)
	{
		if (!Object.IsValid())
		{
			return DefaultValue;
		}
		double NumberValue = 0.0;
		if (Object->TryGetNumberField(FieldName, NumberValue))
		{
			return static_cast<int32>(NumberValue);
		}
		FString StringValue;
		if (Object->TryGetStringField(FieldName, StringValue) && !StringValue.TrimStartAndEnd().IsEmpty())
		{
			return FCString::Atoi(*StringValue);
		}
		return DefaultValue;
	}

	static TSharedPtr<FJsonObject> MakePCGForestRequestSummary(const TSharedPtr<FJsonObject>& Arguments)
	{
		TSharedPtr<FJsonObject> Request = MakeShared<FJsonObject>();
		const double AreaX = GetOptionalNumberField(Arguments, TEXT("AreaX"), 1600.0);
		const double AreaY = GetOptionalNumberField(Arguments, TEXT("AreaY"), 1600.0);
		const int32 TargetCount = FMath::Max(1, GetOptionalIntField(Arguments, TEXT("TargetCount"), 24));
		const FString SpacingMode = GetOptionalStringField(Arguments, TEXT("SpacingMode"), TEXT("Explicit")).TrimStartAndEnd();
		const bool bUseTargetCountSpacing = SpacingMode.Equals(TEXT("TargetCount"), ESearchCase::IgnoreCase)
			|| SpacingMode.Equals(TEXT("AutoTargetCount"), ESearchCase::IgnoreCase)
			|| SpacingMode.Equals(TEXT("FromTargetCount"), ESearchCase::IgnoreCase);
		const double ExplicitTreeSpacing = FMath::Max(1.0, GetOptionalNumberField(Arguments, TEXT("TreeSpacing"), 200.0));
		const double DerivedTreeSpacing = FMath::Max(1.0, FMath::Sqrt(FMath::Max(1.0, AreaX * AreaY) / static_cast<double>(TargetCount)));
		const double TreeSpacing = bUseTargetCountSpacing ? DerivedTreeSpacing : ExplicitTreeSpacing;
		Request->SetStringField(TEXT("tree_blueprint_path"), GetOptionalStringField(Arguments, TEXT("TreeBlueprintPath")));
		Request->SetStringField(TEXT("package_path"), GetOptionalStringField(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/VisualPCG")));
		Request->SetStringField(TEXT("forest_name"), GetOptionalStringField(Arguments, TEXT("ForestName"), TEXT("Druids_PCG_Forest")));
		Request->SetNumberField(TEXT("seed"), GetOptionalIntField(Arguments, TEXT("Seed"), 1337));
		Request->SetNumberField(TEXT("target_count"), TargetCount);
		Request->SetNumberField(TEXT("area_x"), AreaX);
		Request->SetNumberField(TEXT("area_y"), AreaY);
		FString BoundsShape = GetOptionalStringField(Arguments, TEXT("BoundsShape"), TEXT("Rectangle")).TrimStartAndEnd();
		if (BoundsShape.IsEmpty())
		{
			BoundsShape = TEXT("Rectangle");
		}
		const bool bRectangleBounds = BoundsShape.Equals(TEXT("Rectangle"), ESearchCase::IgnoreCase)
			|| BoundsShape.Equals(TEXT("Rect"), ESearchCase::IgnoreCase)
			|| BoundsShape.Equals(TEXT("Box"), ESearchCase::IgnoreCase);
		Request->SetStringField(TEXT("bounds_shape_requested"), BoundsShape);
		Request->SetStringField(TEXT("bounds_shape"), bRectangleBounds ? TEXT("rectangle") : TEXT("unsupported"));
		Request->SetBoolField(TEXT("bounds_shape_supported"), bRectangleBounds);
		Request->SetNumberField(TEXT("bounds_height"), FMath::Max(1.0, GetOptionalNumberField(Arguments, TEXT("BoundsHeight"), 400.0)));
		Request->SetStringField(TEXT("spacing_mode"), bUseTargetCountSpacing ? TEXT("target_count") : TEXT("explicit"));
		Request->SetStringField(TEXT("tree_spacing_source"), bUseTargetCountSpacing ? TEXT("derived_from_target_count") : TEXT("explicit_or_default"));
		Request->SetNumberField(TEXT("tree_spacing"), TreeSpacing);
		Request->SetNumberField(TEXT("explicit_tree_spacing"), ExplicitTreeSpacing);
		Request->SetNumberField(TEXT("derived_tree_spacing"), DerivedTreeSpacing);
		const FString VariationMode = GetOptionalStringField(Arguments, TEXT("VariationMode"), TEXT("None")).TrimStartAndEnd();
		const bool bUseTransformVariation = VariationMode.Equals(TEXT("TransformPoints"), ESearchCase::IgnoreCase)
			|| VariationMode.Equals(TEXT("Natural"), ESearchCase::IgnoreCase)
			|| VariationMode.Equals(TEXT("JitterScale"), ESearchCase::IgnoreCase);
		Request->SetStringField(TEXT("variation_mode"), bUseTransformVariation ? TEXT("transform_points") : TEXT("none"));
		Request->SetNumberField(TEXT("position_jitter"), FMath::Max(0.0, GetOptionalNumberField(Arguments, TEXT("PositionJitter"), 0.0)));
		Request->SetNumberField(TEXT("offset_x"), GetOptionalNumberField(Arguments, TEXT("OffsetX"), 0.0));
		Request->SetNumberField(TEXT("offset_y"), GetOptionalNumberField(Arguments, TEXT("OffsetY"), 0.0));
		Request->SetNumberField(TEXT("offset_z"), GetOptionalNumberField(Arguments, TEXT("OffsetZ"), 0.0));
		const double MinScale = FMath::Max(0.01, GetOptionalNumberField(Arguments, TEXT("MinScale"), 1.0));
		const double MaxScale = FMath::Max(MinScale, GetOptionalNumberField(Arguments, TEXT("MaxScale"), MinScale));
		Request->SetNumberField(TEXT("min_scale"), MinScale);
		Request->SetNumberField(TEXT("max_scale"), MaxScale);
		const double YawJitter = FMath::Max(0.0, GetOptionalNumberField(Arguments, TEXT("YawJitter"), 0.0));
		Request->SetNumberField(TEXT("yaw_min"), GetOptionalNumberField(Arguments, TEXT("YawMin"), -YawJitter));
		Request->SetNumberField(TEXT("yaw_max"), GetOptionalNumberField(Arguments, TEXT("YawMax"), YawJitter));
		return Request;
	}

	static FString GetPCGTreeStyle(const TSharedPtr<FJsonObject>& Arguments)
	{
		FString TreeStyle = GetOptionalStringField(Arguments, TEXT("TreeStyle"), TEXT("RoundCanopy")).TrimStartAndEnd();
		if (TreeStyle.IsEmpty())
		{
			TreeStyle = TEXT("RoundCanopy");
		}
		if (TreeStyle.Equals(TEXT("Pine"), ESearchCase::IgnoreCase))
		{
			TreeStyle = TEXT("TallPine");
		}
		return TreeStyle;
	}

	static void ApplyPCGTreeStyleDefaults(const FString& TreeStyle, TSharedPtr<FJsonObject> TreeArguments)
	{
		if (!TreeArguments.IsValid())
		{
			return;
		}
		if (TreeStyle.Equals(TEXT("TallPine"), ESearchCase::IgnoreCase))
		{
			TreeArguments->SetStringField(TEXT("TrunkHeight"), TEXT("280"));
			TreeArguments->SetStringField(TEXT("TrunkRadius"), TEXT("18"));
			TreeArguments->SetStringField(TEXT("CanopyRadius"), TEXT("70"));
			TreeArguments->SetStringField(TEXT("CanopyCenterZ"), TEXT("330"));
		}
		else if (TreeStyle.Equals(TEXT("WideCanopy"), ESearchCase::IgnoreCase))
		{
			TreeArguments->SetStringField(TEXT("TrunkHeight"), TEXT("190"));
			TreeArguments->SetStringField(TEXT("TrunkRadius"), TEXT("28"));
			TreeArguments->SetStringField(TEXT("CanopyRadius"), TEXT("115"));
			TreeArguments->SetStringField(TEXT("CanopyCenterZ"), TEXT("250"));
		}
		else
		{
			TreeArguments->SetStringField(TEXT("TrunkHeight"), TEXT("220"));
			TreeArguments->SetStringField(TEXT("TrunkRadius"), TEXT("24"));
			TreeArguments->SetStringField(TEXT("CanopyRadius"), TEXT("85"));
			TreeArguments->SetStringField(TEXT("CanopyCenterZ"), TEXT("275"));
		}
	}

	static TSharedPtr<FJsonObject> MakePCGRecipeForestArguments(const TSharedPtr<FJsonObject>& Arguments, const FString& TreeBlueprintPath, const FString& ForestName, const FString& GraphForestName)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("TreeBlueprintPath"), TreeBlueprintPath);
		Result->SetStringField(TEXT("PackagePath"), GetOptionalStringField(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/VisualPCG")));
		Result->SetStringField(TEXT("ForestName"), GraphForestName.IsEmpty() ? ForestName : GraphForestName);
		Result->SetStringField(TEXT("GraphMode"), TEXT("BoundsSpawnActor"));
		Result->SetStringField(TEXT("BoundsShape"), TEXT("Rectangle"));
		Result->SetStringField(TEXT("SpacingMode"), TEXT("TargetCount"));
		Result->SetStringField(TEXT("VariationMode"), TEXT("TransformPoints"));
		Result->SetStringField(TEXT("Seed"), FString::FromInt(GetOptionalIntField(Arguments, TEXT("Seed"), 1337)));
		Result->SetStringField(TEXT("TargetCount"), FString::FromInt(FMath::Max(1, GetOptionalIntField(Arguments, TEXT("TargetCount"), 24))));
		Result->SetStringField(TEXT("AreaX"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("AreaX"), 1600.0)));
		Result->SetStringField(TEXT("AreaY"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("AreaY"), 1600.0)));
		Result->SetStringField(TEXT("BoundsHeight"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("BoundsHeight"), 400.0)));
		Result->SetStringField(TEXT("TreeSpacing"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("TreeSpacing"), 200.0)));
		Result->SetStringField(TEXT("PositionJitter"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("PositionJitter"), 45.0)));
		Result->SetStringField(TEXT("MinScale"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("MinScale"), 0.85)));
		Result->SetStringField(TEXT("MaxScale"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("MaxScale"), 1.25)));
		Result->SetStringField(TEXT("YawJitter"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("YawJitter"), GetOptionalNumberField(Arguments, TEXT("YawRandomization"), 20.0))));
		return Result;
	}

	static TSharedPtr<FJsonObject> MakePCGRecipePlaceArguments(const TSharedPtr<FJsonObject>& Arguments, const FString& TreeBlueprintPath, const FString& ForestName, const FString& GraphPath)
	{
		TSharedPtr<FJsonObject> Result = MakePCGRecipeForestArguments(Arguments, TreeBlueprintPath, ForestName, ForestName);
		Result->SetStringField(TEXT("ForestName"), ForestName);
		Result->SetStringField(TEXT("GraphPath"), GraphPath);
		Result->SetStringField(TEXT("AllowCurrentLevel"), TEXT("true"));
		Result->SetStringField(TEXT("LocationX"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("LocationX"), 0.0)));
		Result->SetStringField(TEXT("LocationY"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("LocationY"), 0.0)));
		Result->SetStringField(TEXT("LocationZ"), FString::SanitizeFloat(GetOptionalNumberField(Arguments, TEXT("LocationZ"), 0.0)));
		return Result;
	}

	static TSharedPtr<FJsonObject> MakePCGUnsupportedBoundsShapeResult(const FString& ToolName, const TSharedPtr<FJsonObject>& Request)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		const FString RequestedShape = Request.IsValid() ? Request->GetStringField(TEXT("bounds_shape_requested")) : TEXT("");
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("created"), false);
		Result->SetBoolField(TEXT("placed"), false);
		Result->SetBoolField(TEXT("generated"), false);
		Result->SetStringField(TEXT("tool"), ToolName);
		Result->SetStringField(TEXT("status"), TEXT("pcg_bounds_shape_unsupported"));
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("BoundsShape '%s' is not supported by this low-level PCG tool. Use BoundsShape=Rectangle here, or use BoundsShape=SplineRectangle with pcg_run_forest_recipe/pcg_update_forest_recipe for rectangular spline bounds. Arbitrary/freeform spline area sampling remains unsupported."), *RequestedShape));
		Result->SetObjectField(TEXT("request"), Request);
		TSharedPtr<FJsonObject> ShapeSupport = MakeShared<FJsonObject>();
		ShapeSupport->SetBoolField(TEXT("rectangle"), true);
		ShapeSupport->SetBoolField(TEXT("spline"), false);
		ShapeSupport->SetBoolField(TEXT("arbitrary_volume"), false);
		ShapeSupport->SetStringField(TEXT("next_shape_slice"), TEXT("spline_bounds_controls"));
		Result->SetObjectField(TEXT("shape_support"), ShapeSupport);
		return Result;
	}

	static FVector MakePCGRectangleActorScale(const TSharedPtr<FJsonObject>& Request)
	{
		const double AreaX = FMath::Max(1.0, Request.IsValid() ? Request->GetNumberField(TEXT("area_x")) : 1600.0);
		const double AreaY = FMath::Max(1.0, Request.IsValid() ? Request->GetNumberField(TEXT("area_y")) : 1600.0);
		const double BoundsHeight = FMath::Max(1.0, Request.IsValid() ? Request->GetNumberField(TEXT("bounds_height")) : 400.0);
		return FVector(
			FMath::Max(0.01, AreaX / 200.0),
			FMath::Max(0.01, AreaY / 200.0),
			FMath::Max(0.01, BoundsHeight / 200.0));
	}

	static void ApplyPCGRectangleBoundsToActor(AActor* Actor, const TSharedPtr<FJsonObject>& Request)
	{
		if (!Actor)
		{
			return;
		}
		Actor->SetActorScale3D(MakePCGRectangleActorScale(Request));
		Actor->Tags.AddUnique(TEXT("DruidsPCGRectangleBounds"));
	}

	static TSharedPtr<FJsonObject> MakePCGRectangleBoundsSummary(AActor* Actor, const TSharedPtr<FJsonObject>& Request)
	{
		TSharedPtr<FJsonObject> Bounds = MakeShared<FJsonObject>();
		Bounds->SetStringField(TEXT("shape"), TEXT("rectangle"));
		Bounds->SetStringField(TEXT("resize_tool"), TEXT("pcg_set_forest_bounds"));
		Bounds->SetBoolField(TEXT("editable_in_level"), true);
		Bounds->SetBoolField(TEXT("grid_generation_remains_single_layer"), true);
		Bounds->SetNumberField(TEXT("area_x"), Request.IsValid() ? Request->GetNumberField(TEXT("area_x")) : 1600.0);
		Bounds->SetNumberField(TEXT("area_y"), Request.IsValid() ? Request->GetNumberField(TEXT("area_y")) : 1600.0);
		Bounds->SetNumberField(TEXT("bounds_height"), Request.IsValid() ? Request->GetNumberField(TEXT("bounds_height")) : 400.0);
		Bounds->SetObjectField(TEXT("actor_scale"), MakeVectorObject(Actor ? Actor->GetActorScale3D() : MakePCGRectangleActorScale(Request)));
		if (Actor)
		{
			const FBox ActorBounds = Actor->GetComponentsBoundingBox(false);
			if (ActorBounds.IsValid)
			{
				Bounds->SetObjectField(TEXT("actor_bounds"), MakeBoxObject(ActorBounds));
			}
		}
		return Bounds;
	}

	static TSharedPtr<FJsonObject> MakeSplineBoundsSummary(USplineComponent* Spline)
	{
		TSharedPtr<FJsonObject> Summary = MakeShared<FJsonObject>();
		Summary->SetBoolField(TEXT("found"), Spline != nullptr);
		if (!Spline)
		{
			return Summary;
		}

		const int32 PointCount = Spline->GetNumberOfSplinePoints();
		Summary->SetStringField(TEXT("component_name"), Spline->GetName());
		Summary->SetBoolField(TEXT("closed_loop"), Spline->IsClosedLoop());
		Summary->SetNumberField(TEXT("point_count"), PointCount);
		Summary->SetNumberField(TEXT("length"), Spline->GetSplineLength());

		TArray<TSharedPtr<FJsonValue>> Points;
		FBox LocalBounds(EForceInit::ForceInit);
		FBox WorldBounds(EForceInit::ForceInit);
		for (int32 PointIndex = 0; PointIndex < PointCount; ++PointIndex)
		{
			const FVector Local = Spline->GetLocationAtSplinePoint(PointIndex, ESplineCoordinateSpace::Local);
			const FVector World = Spline->GetLocationAtSplinePoint(PointIndex, ESplineCoordinateSpace::World);
			LocalBounds += Local;
			WorldBounds += World;

			TSharedPtr<FJsonObject> Point = MakeShared<FJsonObject>();
			Point->SetNumberField(TEXT("index"), PointIndex);
			Point->SetObjectField(TEXT("local"), MakeVectorObject(Local));
			Point->SetObjectField(TEXT("world"), MakeVectorObject(World));
			Points.Add(MakeShared<FJsonValueObject>(Point));
		}
		Summary->SetArrayField(TEXT("points"), Points);
		Summary->SetObjectField(TEXT("local_bounds"), LocalBounds.IsValid ? MakeBoxObject(LocalBounds) : MakeShared<FJsonObject>());
		Summary->SetObjectField(TEXT("world_bounds"), WorldBounds.IsValid ? MakeBoxObject(WorldBounds) : MakeShared<FJsonObject>());
		return Summary;
	}

	static USplineComponent* FindSplineBoundsComponent(AActor* Actor)
	{
		if (!Actor)
		{
			return nullptr;
		}

		TArray<USplineComponent*> SplineComponents;
		Actor->GetComponents<USplineComponent>(SplineComponents);
		for (USplineComponent* Spline : SplineComponents)
		{
			if (Spline && Spline->GetName().Equals(TEXT("DruidsSplineBounds"), ESearchCase::IgnoreCase))
			{
				return Spline;
			}
		}
		return SplineComponents.Num() > 0 ? SplineComponents[0] : nullptr;
	}

	static FVector2D EstimateRectangleSplineSize(USplineComponent* Spline, const FVector2D& DefaultSize)
	{
		if (!Spline)
		{
			return DefaultSize;
		}

		FBox LocalBounds(EForceInit::ForceInit);
		const int32 PointCount = Spline->GetNumberOfSplinePoints();
		for (int32 PointIndex = 0; PointIndex < PointCount; ++PointIndex)
		{
			LocalBounds += Spline->GetLocationAtSplinePoint(PointIndex, ESplineCoordinateSpace::Local);
		}
		if (!LocalBounds.IsValid)
		{
			return DefaultSize;
		}
		const FVector Extent = LocalBounds.GetExtent();
		return FVector2D(FMath::Max(1.0, Extent.X * 2.0), FMath::Max(1.0, Extent.Y * 2.0));
	}

	static void ConfigureRectangleSplineBounds(USplineComponent* Spline, double Width, double Depth, bool bClosedLoop)
	{
		if (!Spline)
		{
			return;
		}

		Spline->ClearSplinePoints(false);
		const double HalfWidth = FMath::Max(1.0, Width) * 0.5;
		const double HalfDepth = FMath::Max(1.0, Depth) * 0.5;
		const TArray<FVector> Points = {
			FVector(-HalfWidth, -HalfDepth, 0.0),
			FVector(HalfWidth, -HalfDepth, 0.0),
			FVector(HalfWidth, HalfDepth, 0.0),
			FVector(-HalfWidth, HalfDepth, 0.0)
		};
		for (const FVector& Point : Points)
		{
			Spline->AddSplinePoint(Point, ESplineCoordinateSpace::Local, false);
		}
		for (int32 PointIndex = 0; PointIndex < Points.Num(); ++PointIndex)
		{
			Spline->SetSplinePointType(PointIndex, ESplinePointType::Linear, false);
		}
		Spline->SetClosedLoop(bClosedLoop, false);
		Spline->UpdateSpline();
	}

	static TSharedPtr<FJsonObject> MakePCGUnavailableResult(const FString& ToolName, const TSharedPtr<FJsonObject>& Arguments, const FString& Operation)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("created"), false);
		Result->SetBoolField(TEXT("placed"), false);
		Result->SetBoolField(TEXT("generated"), false);
		Result->SetBoolField(TEXT("available"), false);
		Result->SetStringField(TEXT("tool"), ToolName);
		Result->SetStringField(TEXT("operation"), Operation);
		Result->SetStringField(TEXT("status"), TEXT("pcg_unavailable"));
		Result->SetStringField(TEXT("error"), TEXT("PCG is not enabled and loadable in this project. Enable the PCG plugin and restart/rebuild before running PCG forest generation."));
		Result->SetObjectField(TEXT("pcg_status"), GetPCGAvailabilitySummary());
		Result->SetObjectField(TEXT("request"), MakePCGForestRequestSummary(Arguments));
		TArray<TSharedPtr<FJsonValue>> NextSteps;
		NextSteps.Add(MakeShared<FJsonValueString>(TEXT("Enable the PCG plugin for the project if PCG generation is desired.")));
		NextSteps.Add(MakeShared<FJsonValueString>(TEXT("Restart Unreal so PCG classes/modules are available.")));
		NextSteps.Add(MakeShared<FJsonValueString>(TEXT("Run pcg_get_status again before mutating the editor world.")));
		Result->SetArrayField(TEXT("next_steps"), NextSteps);
		return Result;
	}

	static UEditorActorSubsystem* GetEditorActorSubsystem()
	{
		return GEditor ? GEditor->GetEditorSubsystem<UEditorActorSubsystem>() : nullptr;
	}

	static AActor* FindEditorActorByNameLabelOrPath(const FString& ActorNameOrLabel)
	{
		UEditorActorSubsystem* ActorSubsystem = GetEditorActorSubsystem();
		if (!ActorSubsystem)
		{
			return nullptr;
		}

		const FString Query = ActorNameOrLabel.TrimStartAndEnd();
		for (AActor* Actor : ActorSubsystem->GetAllLevelActors())
		{
			if (!Actor)
			{
				continue;
			}
			if (Actor->GetName().Equals(Query, ESearchCase::IgnoreCase) ||
				Actor->GetActorLabel().Equals(Query, ESearchCase::IgnoreCase) ||
				Actor->GetPathName().Equals(Query, ESearchCase::IgnoreCase))
			{
				return Actor;
			}
		}
		return nullptr;
	}

	static TSharedPtr<FJsonObject> MakeEditorActorSummary(AActor* Actor, bool bSelected = false)
	{
		TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
		if (!Actor)
		{
			return Object;
		}

		FVector Origin = FVector::ZeroVector;
		FVector Extent = FVector::ZeroVector;
		Actor->GetActorBounds(false, Origin, Extent);
		TArray<TSharedPtr<FJsonValue>> Tags;
		for (const FName& Tag : Actor->Tags)
		{
			Tags.Add(MakeShared<FJsonValueString>(Tag.ToString()));
		}
		TArray<TSharedPtr<FJsonValue>> Components;
		TArray<USceneComponent*> SceneComponents;
		Actor->GetComponents<USceneComponent>(SceneComponents);
		for (USceneComponent* Component : SceneComponents)
		{
			if (!Component)
			{
				continue;
			}
			TSharedPtr<FJsonObject> ComponentObject = MakeShared<FJsonObject>();
			ComponentObject->SetStringField(TEXT("name"), Component->GetName());
			ComponentObject->SetStringField(TEXT("class_path"), Component->GetClass() ? Component->GetClass()->GetPathName() : TEXT(""));
			ComponentObject->SetObjectField(TEXT("world_transform"), MakeTransformObject(Component->GetComponentTransform()));
			ComponentObject->SetObjectField(TEXT("relative_transform"), MakeTransformObject(Component->GetRelativeTransform()));
			if (const UPrimitiveComponent* Primitive = Cast<UPrimitiveComponent>(Component))
			{
				ComponentObject->SetObjectField(TEXT("world_bounds"), MakeBoxObject(Primitive->Bounds.GetBox()));
			}
			if (const UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(Component))
			{
				ComponentObject->SetStringField(TEXT("static_mesh"), StaticMeshComponent->GetStaticMesh() ? StaticMeshComponent->GetStaticMesh()->GetPathName() : TEXT(""));
			}
			Components.Add(MakeShared<FJsonValueObject>(ComponentObject));
		}

		Object->SetStringField(TEXT("name"), Actor->GetName());
		Object->SetStringField(TEXT("label"), Actor->GetActorLabel());
		Object->SetStringField(TEXT("path"), Actor->GetPathName());
		Object->SetStringField(TEXT("class_path"), Actor->GetClass() ? Actor->GetClass()->GetPathName() : TEXT(""));
		Object->SetObjectField(TEXT("transform"), MakeTransformObject(Actor->GetActorTransform()));
		Object->SetObjectField(TEXT("bounds"), MakeBoxObject(FBox::BuildAABB(Origin, Extent)));
		Object->SetArrayField(TEXT("tags"), Tags);
		Object->SetArrayField(TEXT("components"), Components);
		Object->SetBoolField(TEXT("selected"), bSelected);
		Object->SetBoolField(TEXT("druids_repro_actor"), Actor->Tags.Contains(FName(TEXT("DruidsRepro"))) || Actor->Tags.Contains(FName(TEXT("DruidsMCPBridge"))));
		return Object;
	}

	static FBox EstimateStaticMeshComponentLocalBounds(const UStaticMeshComponent* Component)
	{
		if (!Component || !Component->GetStaticMesh())
		{
			return FBox(ForceInit);
		}

		const FBoxSphereBounds MeshBounds = Component->GetStaticMesh()->GetBounds();
		const FVector RelativeLocation = Component->GetRelativeLocation();
		const FVector RelativeScale = Component->GetRelativeScale3D();
		const FVector ScaledOrigin = FVector(
			MeshBounds.Origin.X * RelativeScale.X,
			MeshBounds.Origin.Y * RelativeScale.Y,
			MeshBounds.Origin.Z * RelativeScale.Z);
		const FVector ScaledExtent = FVector(
			FMath::Abs(MeshBounds.BoxExtent.X * RelativeScale.X),
			FMath::Abs(MeshBounds.BoxExtent.Y * RelativeScale.Y),
			FMath::Abs(MeshBounds.BoxExtent.Z * RelativeScale.Z));
		return FBox::BuildAABB(RelativeLocation + ScaledOrigin, ScaledExtent);
	}

	static TSharedPtr<FJsonObject> MakePrimitiveComponentSummary(const FString& Name, const FString& PrimitiveShape, const UStaticMeshComponent* Component)
	{
		TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
		Object->SetStringField(TEXT("name"), Name);
		Object->SetStringField(TEXT("primitive_shape"), PrimitiveShape);
		Object->SetStringField(TEXT("component_class"), Component ? Component->GetClass()->GetPathName() : TEXT(""));
		Object->SetStringField(TEXT("static_mesh"), Component && Component->GetStaticMesh() ? Component->GetStaticMesh()->GetPathName() : TEXT(""));
		Object->SetObjectField(TEXT("relative_location"), MakeVectorObject(Component ? Component->GetRelativeLocation() : FVector::ZeroVector));
		Object->SetObjectField(TEXT("relative_scale"), MakeVectorObject(Component ? Component->GetRelativeScale3D() : FVector::OneVector));
		if (Component)
		{
			Object->SetObjectField(TEXT("estimated_local_bounds"), MakeBoxObject(EstimateStaticMeshComponentLocalBounds(Component)));
			UMaterialInterface* Material = Component->GetMaterial(0);
			Object->SetStringField(TEXT("material"), Material ? Material->GetPathName() : TEXT(""));
		}
		return Object;
	}

	struct FAllowlistedComponentFunction
	{
		const TCHAR* DisplayName;
		const TCHAR* SpawnerId;
		const TCHAR* FunctionName;
		const TCHAR* Category;
		const TCHAR* TargetPin;
		const TCHAR* VectorPin;
		const TCHAR* ExecInput;
		const TCHAR* ExecOutput;
	};

	static const TArray<FAllowlistedComponentFunction>& GetAllowlistedComponentFunctions()
	{
		static const TArray<FAllowlistedComponentFunction> Functions = {
			{ TEXT("Set Relative Location"), TEXT("func::/Script/Engine.SceneComponent:K2_SetRelativeLocation"), TEXT("K2_SetRelativeLocation"), TEXT("Components|Scene"), TEXT("self"), TEXT("NewLocation"), TEXT("execute"), TEXT("then") },
			{ TEXT("Add Local Offset"), TEXT("func::/Script/Engine.SceneComponent:K2_AddLocalOffset"), TEXT("K2_AddLocalOffset"), TEXT("Components|Scene"), TEXT("self"), TEXT("DeltaLocation"), TEXT("execute"), TEXT("then") },
			{ TEXT("Add Relative Location"), TEXT("func::/Script/Engine.SceneComponent:K2_AddRelativeLocation"), TEXT("K2_AddRelativeLocation"), TEXT("Components|Scene"), TEXT("self"), TEXT("DeltaLocation"), TEXT("execute"), TEXT("then") },
			{ TEXT("Set Static Mesh"), TEXT("func::/Script/Engine.StaticMeshComponent:SetStaticMesh"), TEXT("SetStaticMesh"), TEXT("Components|StaticMesh"), TEXT("self"), TEXT("NewMesh"), TEXT("execute"), TEXT("then") },
			{ TEXT("Set Material"), TEXT("func::/Script/Engine.PrimitiveComponent:SetMaterial"), TEXT("SetMaterial"), TEXT("Components|Primitive"), TEXT("self"), TEXT("Material"), TEXT("execute"), TEXT("then") },
		};
		return Functions;
	}

	static UClass* ResolveDynamicCastTargetClass(const FString& RawClassOrAssetPath, FString& OutCanonicalClassPath, FString& OutError)
	{
		FString ClassOrAssetPath = RawClassOrAssetPath.TrimStartAndEnd();
		if (ClassOrAssetPath.IsEmpty())
		{
			OutError = TEXT("Dynamic cast spawner requires a target class path.");
			return nullptr;
		}

		if (UClass* TargetClass = LoadObject<UClass>(nullptr, *ClassOrAssetPath))
		{
			OutCanonicalClassPath = TargetClass->GetPathName();
			return TargetClass;
		}

		UBlueprint* TargetBlueprint = LoadObject<UBlueprint>(nullptr, *ClassOrAssetPath);
		if (!TargetBlueprint && ClassOrAssetPath.StartsWith(TEXT("/Game/")))
		{
			TargetBlueprint = LoadObject<UBlueprint>(nullptr, *MakeBlueprintObjectPath(ClassOrAssetPath));
		}

		if (TargetBlueprint && TargetBlueprint->GeneratedClass)
		{
			OutCanonicalClassPath = TargetBlueprint->GeneratedClass->GetPathName();
			return TargetBlueprint->GeneratedClass;
		}

		OutError = FString::Printf(TEXT("Could not resolve dynamic cast target class or Blueprint asset: %s"), *RawClassOrAssetPath);
		return nullptr;
	}

	static bool ParseExternalVariableSpawnerSpec(const FString& SpawnerId, const FString& Prefix, FString& OutClassPath, FString& OutVariableName)
	{
		if (!SpawnerId.StartsWith(Prefix))
		{
			return false;
		}

		const FString Spec = SpawnerId.RightChop(Prefix.Len()).TrimStartAndEnd();
		if (Spec.IsEmpty())
		{
			return false;
		}

		FString ClassPath;
		FString VariableName;
		if (!Spec.Split(TEXT(":"), &ClassPath, &VariableName, ESearchCase::CaseSensitive, ESearchDir::FromEnd))
		{
			return false;
		}

		ClassPath.TrimStartAndEndInline();
		VariableName.TrimStartAndEndInline();
		if (ClassPath.IsEmpty() || VariableName.IsEmpty())
		{
			return false;
		}

		OutClassPath = ClassPath;
		OutVariableName = VariableName;
		return true;
	}

	static bool ResolveExternalVariableReference(const FString& RawClassOrAssetPath, const FString& VariableName, UClass*& OutOwnerClass, FString& OutCanonicalClassPath, FString& OutError)
	{
		OutOwnerClass = ResolveDynamicCastTargetClass(RawClassOrAssetPath, OutCanonicalClassPath, OutError);
		if (!OutOwnerClass)
		{
			return false;
		}

		if (!FindFProperty<FProperty>(OutOwnerClass, FName(*VariableName)))
		{
			OutError = FString::Printf(TEXT("Variable '%s' does not exist on external class '%s'."), *VariableName, *OutCanonicalClassPath);
			return false;
		}

		return true;
	}

	static FString GetPinContainerTypeName(const FEdGraphPinType& PinType)
	{
		switch (PinType.ContainerType)
		{
		case EPinContainerType::None:
			return TEXT("none");
		case EPinContainerType::Array:
			return TEXT("array");
		case EPinContainerType::Set:
			return TEXT("set");
		case EPinContainerType::Map:
			return TEXT("map");
		default:
			return TEXT("unknown");
		}
	}

	static FString GetPinSubCategoryObjectPath(const TWeakObjectPtr<UObject>& Object)
	{
		UObject* ResolvedObject = Object.Get();
		return ResolvedObject ? ResolvedObject->GetPathName() : TEXT("");
	}

	static FString GetTerminalTypeDisplayName(const FName& Category, const FName& SubCategory, UObject* SubCategoryObject)
	{
		if (SubCategoryObject)
		{
			return SubCategoryObject->GetName();
		}
		if (Category == UEdGraphSchema_K2::PC_Real && SubCategory != NAME_None)
		{
			return SubCategory.ToString();
		}
		return Category.ToString();
	}

	static TSharedPtr<FJsonObject> MakeTerminalPinTypeObject(const FName& Category, const FName& SubCategory, const TWeakObjectPtr<UObject>& SubCategoryObject)
	{
		UObject* ResolvedObject = SubCategoryObject.Get();
		TSharedPtr<FJsonObject> TypeObject = MakeShared<FJsonObject>();
		TypeObject->SetStringField(TEXT("pin_category"), Category.ToString());
		TypeObject->SetStringField(TEXT("pin_subcategory"), SubCategory.ToString());
		TypeObject->SetStringField(TEXT("pin_subcategory_object"), ResolvedObject ? ResolvedObject->GetPathName() : TEXT(""));
		TypeObject->SetStringField(TEXT("display_name"), GetTerminalTypeDisplayName(Category, SubCategory, ResolvedObject));
		return TypeObject;
	}

	static FString MakeTerminalPinTypeString(const FName& Category, const FName& SubCategory, UObject* SubCategoryObject)
	{
		if (SubCategoryObject)
		{
			return SubCategoryObject->GetName();
		}
		if (Category == UEdGraphSchema_K2::PC_Real && SubCategory != NAME_None)
		{
			return SubCategory.ToString();
		}
		return Category.ToString();
	}

	static FString MakePinTypeString(const FEdGraphPinType& PinType)
	{
		const FString KeyType = MakeTerminalPinTypeString(PinType.PinCategory, PinType.PinSubCategory, PinType.PinSubCategoryObject.Get());
		switch (PinType.ContainerType)
		{
		case EPinContainerType::Array:
			return FString::Printf(TEXT("array<%s>"), *KeyType);
		case EPinContainerType::Set:
			return FString::Printf(TEXT("set<%s>"), *KeyType);
		case EPinContainerType::Map:
			return FString::Printf(TEXT("map<%s,%s>"),
				*KeyType,
				*MakeTerminalPinTypeString(PinType.PinValueType.TerminalCategory, PinType.PinValueType.TerminalSubCategory, PinType.PinValueType.TerminalSubCategoryObject.Get()));
		default:
			return KeyType;
		}
	}

	static TSharedPtr<FJsonObject> MakePinTypeObject(const FEdGraphPinType& PinType)
	{
		TSharedPtr<FJsonObject> TypeObject = MakeTerminalPinTypeObject(PinType.PinCategory, PinType.PinSubCategory, PinType.PinSubCategoryObject);
		TypeObject->SetStringField(TEXT("container_type"), GetPinContainerTypeName(PinType));
		TypeObject->SetStringField(TEXT("type_string"), MakePinTypeString(PinType));
		TypeObject->SetBoolField(TEXT("is_array"), PinType.ContainerType == EPinContainerType::Array);
		TypeObject->SetBoolField(TEXT("is_set"), PinType.ContainerType == EPinContainerType::Set);
		TypeObject->SetBoolField(TEXT("is_map"), PinType.ContainerType == EPinContainerType::Map);
		if (PinType.ContainerType == EPinContainerType::Map)
		{
			TypeObject->SetObjectField(TEXT("key_type"), MakeTerminalPinTypeObject(PinType.PinCategory, PinType.PinSubCategory, PinType.PinSubCategoryObject));
			TypeObject->SetObjectField(TEXT("value_type"), MakeTerminalPinTypeObject(PinType.PinValueType.TerminalCategory, PinType.PinValueType.TerminalSubCategory, PinType.PinValueType.TerminalSubCategoryObject));
		}
		else if (PinType.ContainerType == EPinContainerType::Array || PinType.ContainerType == EPinContainerType::Set)
		{
			TypeObject->SetObjectField(TEXT("element_type"), MakeTerminalPinTypeObject(PinType.PinCategory, PinType.PinSubCategory, PinType.PinSubCategoryObject));
		}
		return TypeObject;
	}

	static bool SplitTemplateType(const FString& RawType, const FString& Prefix, FString& OutInner)
	{
		FString Type = RawType.TrimStartAndEnd();
		if (!Type.ToLower().StartsWith(Prefix.ToLower() + TEXT("<")) || !Type.EndsWith(TEXT(">")))
		{
			return false;
		}
		OutInner = Type.Mid(Prefix.Len() + 1, Type.Len() - Prefix.Len() - 2).TrimStartAndEnd();
		return !OutInner.IsEmpty();
	}

	static bool SplitTopLevelComma(const FString& Text, FString& OutLeft, FString& OutRight)
	{
		int32 Depth = 0;
		for (int32 Index = 0; Index < Text.Len(); ++Index)
		{
			const TCHAR Character = Text[Index];
			if (Character == TCHAR('<'))
			{
				++Depth;
			}
			else if (Character == TCHAR('>'))
			{
				--Depth;
			}
			else if (Character == TCHAR(',') && Depth == 0)
			{
				OutLeft = Text.Left(Index).TrimStartAndEnd();
				OutRight = Text.Mid(Index + 1).TrimStartAndEnd();
				return !OutLeft.IsEmpty() && !OutRight.IsEmpty();
			}
		}
		return false;
	}

	static bool TryResolveVariablePinType(const FString& RawType, const FString& ObjectClassPath, FEdGraphPinType& OutPinType, FString& OutCanonicalType, FString& OutError)
	{
		FString Type = RawType.TrimStartAndEnd();
		FString LowerType = Type.ToLower();
		bool bArray = false;
		bool bSet = false;
		FString MapInner;
		if (SplitTemplateType(Type, TEXT("map"), MapInner))
		{
			FString KeyTypeText;
			FString ValueTypeText;
			if (!SplitTopLevelComma(MapInner, KeyTypeText, ValueTypeText))
			{
				OutError = FString::Printf(TEXT("Unsupported Blueprint map variable type '%s'. Use map<key,value>, for example map<name,string>."), *RawType);
				return false;
			}

			FEdGraphPinType KeyPinType;
			FString KeyCanonicalType;
			if (!TryResolveVariablePinType(KeyTypeText, ObjectClassPath, KeyPinType, KeyCanonicalType, OutError))
			{
				return false;
			}
			FEdGraphPinType ValuePinType;
			FString ValueCanonicalType;
			if (!TryResolveVariablePinType(ValueTypeText, ObjectClassPath, ValuePinType, ValueCanonicalType, OutError))
			{
				return false;
			}
			if (KeyPinType.ContainerType != EPinContainerType::None || ValuePinType.ContainerType != EPinContainerType::None)
			{
				OutError = FString::Printf(TEXT("Nested Blueprint containers are not supported for variable type '%s'."), *RawType);
				return false;
			}

			OutPinType = KeyPinType;
			OutPinType.ContainerType = EPinContainerType::Map;
			OutPinType.PinValueType.TerminalCategory = ValuePinType.PinCategory;
			OutPinType.PinValueType.TerminalSubCategory = ValuePinType.PinSubCategory;
			OutPinType.PinValueType.TerminalSubCategoryObject = ValuePinType.PinSubCategoryObject;
			OutCanonicalType = FString::Printf(TEXT("map<%s,%s>"), *KeyCanonicalType, *ValueCanonicalType);
			return true;
		}

		if (LowerType.StartsWith(TEXT("array<")) && LowerType.EndsWith(TEXT(">")))
		{
			bArray = true;
			Type = Type.Mid(6, Type.Len() - 7).TrimStartAndEnd();
			LowerType = Type.ToLower();
		}
		else if (LowerType.EndsWith(TEXT("[]")))
		{
			bArray = true;
			Type.LeftChopInline(2);
			Type = Type.TrimStartAndEnd();
			LowerType = Type.ToLower();
		}
		else
		{
			FString SetInner;
			if (SplitTemplateType(Type, TEXT("set"), SetInner))
			{
				bSet = true;
				Type = SetInner;
				LowerType = Type.ToLower();
			}
		}

		FString NormalizedType = LowerType;
		NormalizedType.ReplaceInline(TEXT(" "), TEXT(""));
		NormalizedType.ReplaceInline(TEXT("_"), TEXT(""));
		NormalizedType.ReplaceInline(TEXT("-"), TEXT(""));
		NormalizedType.ReplaceInline(TEXT("("), TEXT(""));
		NormalizedType.ReplaceInline(TEXT(")"), TEXT(""));

		OutPinType = FEdGraphPinType();
		if (NormalizedType == TEXT("bool") || NormalizedType == TEXT("boolean"))
		{
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Boolean;
			OutCanonicalType = TEXT("bool");
		}
		else if (NormalizedType == TEXT("int") || NormalizedType == TEXT("integer"))
		{
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Int;
			OutCanonicalType = TEXT("int");
		}
		else if (NormalizedType == TEXT("float") || NormalizedType == TEXT("real"))
		{
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Real;
			OutPinType.PinSubCategory = UEdGraphSchema_K2::PC_Float;
			OutCanonicalType = TEXT("float");
		}
		else if (NormalizedType == TEXT("string"))
		{
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_String;
			OutCanonicalType = TEXT("string");
		}
		else if (NormalizedType == TEXT("name"))
		{
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Name;
			OutCanonicalType = TEXT("name");
		}
		else if (NormalizedType == TEXT("text"))
		{
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Text;
			OutCanonicalType = TEXT("text");
		}
		else if (NormalizedType == TEXT("vector"))
		{
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
			OutPinType.PinSubCategoryObject = TBaseStructure<FVector>::Get();
			OutCanonicalType = TEXT("vector");
		}
		else if (NormalizedType == TEXT("rotator"))
		{
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
			OutPinType.PinSubCategoryObject = TBaseStructure<FRotator>::Get();
			OutCanonicalType = TEXT("rotator");
		}
		else if (NormalizedType == TEXT("transform"))
		{
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
			OutPinType.PinSubCategoryObject = TBaseStructure<FTransform>::Get();
			OutCanonicalType = TEXT("transform");
		}
		else if (NormalizedType == TEXT("object") || NormalizedType == TEXT("objectreference"))
		{
			const FString ClassPath = ObjectClassPath.IsEmpty() ? FString(TEXT("/Script/Engine.Actor")) : ObjectClassPath;
			UClass* ObjectClass = LoadObject<UClass>(nullptr, *ClassPath);
			if (!ObjectClass || !UEdGraphSchema_K2::IsAllowableBlueprintVariableType(ObjectClass))
			{
				OutError = FString::Printf(TEXT("Could not load allowable object variable class: %s"), *ClassPath);
				return false;
			}
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object;
			OutPinType.PinSubCategoryObject = ObjectClass;
			OutCanonicalType = TEXT("object");
		}
		else if (NormalizedType == TEXT("class") || NormalizedType == TEXT("classreference"))
		{
			const FString ClassPath = ObjectClassPath.IsEmpty() ? FString(TEXT("/Script/Engine.Actor")) : ObjectClassPath;
			UClass* ClassType = LoadObject<UClass>(nullptr, *ClassPath);
			if (!ClassType || !UEdGraphSchema_K2::IsAllowableBlueprintVariableType(ClassType))
			{
				OutError = FString::Printf(TEXT("Could not load allowable class variable class: %s"), *ClassPath);
				return false;
			}
			OutPinType.PinCategory = UEdGraphSchema_K2::PC_Class;
			OutPinType.PinSubCategoryObject = ClassType;
			OutCanonicalType = TEXT("class");
		}
		else
		{
			OutError = FString::Printf(TEXT("Unsupported Blueprint variable type '%s'. Supported types: bool, int, float, string, name, text, vector, rotator, transform, object, class, arrays like float[] or array<float>, sets like set<name>, and maps like map<name,string>."), *RawType);
			return false;
		}

		if (bArray)
		{
			OutPinType.ContainerType = EPinContainerType::Array;
			OutCanonicalType += TEXT("[]");
		}
		else if (bSet)
		{
			OutPinType.ContainerType = EPinContainerType::Set;
			OutCanonicalType = FString::Printf(TEXT("set<%s>"), *OutCanonicalType);
		}

		return true;
	}

	static FString ExportBlueprintVariableDefaultValue(UBlueprint* Blueprint, const FBPVariableDescription& Variable)
	{
		if (!Blueprint || !Blueprint->GeneratedClass)
		{
			return Variable.DefaultValue;
		}

		UObject* DefaultObject = Blueprint->GeneratedClass->GetDefaultObject();
		if (!DefaultObject)
		{
			return Variable.DefaultValue;
		}

		FProperty* Property = FindFProperty<FProperty>(Blueprint->GeneratedClass, Variable.VarName);
		if (!Property)
		{
			return Variable.DefaultValue;
		}

		FString ExportedValue;
		if (Property->ExportText_InContainer(0, ExportedValue, DefaultObject, nullptr, DefaultObject, PPF_None))
		{
			return ExportedValue;
		}
		return Variable.DefaultValue;
	}

	static FString JsonValueToPropertyImportText(const TSharedPtr<FJsonValue>& Value)
	{
		if (!Value.IsValid())
		{
			return TEXT("");
		}

		FString StringValue;
		if (Value->TryGetString(StringValue))
		{
			return StringValue;
		}

		double NumberValue = 0.0;
		if (Value->TryGetNumber(NumberValue))
		{
			return FString::SanitizeFloat(NumberValue);
		}

		bool BoolValue = false;
		if (Value->TryGetBool(BoolValue))
		{
			return BoolValue ? TEXT("true") : TEXT("false");
		}

		FString Output;
		const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
		FJsonSerializer::Serialize(Value, TEXT(""), Writer);
		return Output;
	}

	static bool TryImportPropertyText(UObject* Object, FProperty* Property, const FString& Value, FString& OutError)
	{
		if (!Object || !Property)
		{
			OutError = TEXT("Property import target is invalid.");
			return false;
		}

		const TCHAR* Result = Property->ImportText_Direct(*Value, Property->ContainerPtrToValuePtr<void>(Object), Object, PPF_None);
		if (!Result)
		{
			OutError = FString::Printf(TEXT("Could not import value '%s' for property '%s'."), *Value, *Property->GetName());
			return false;
		}

		return true;
	}

	static TSharedPtr<FJsonObject> ExportEditablePropertyValues(UObject* Object)
	{
		TSharedPtr<FJsonObject> Values = MakeShared<FJsonObject>();
		if (!Object)
		{
			return Values;
		}

		UObject* DefaultObject = Object->GetClass() ? Object->GetClass()->GetDefaultObject() : nullptr;
		for (TFieldIterator<FProperty> It(Object->GetClass(), EFieldIteratorFlags::IncludeSuper); It; ++It)
		{
			FProperty* Property = *It;
			if (!Property ||
				!Property->HasAnyPropertyFlags(CPF_Edit) ||
				Property->HasAnyPropertyFlags(CPF_Transient | CPF_EditConst | CPF_Deprecated))
			{
				continue;
			}

			FString ExportedValue;
			if (Property->ExportText_InContainer(0, ExportedValue, Object, DefaultObject, Object, PPF_None))
			{
				Values->SetStringField(Property->GetName(), ExportedValue);
			}
		}

		return Values;
	}

	static TSharedPtr<FJsonObject> MakeObjectReferenceObject(const UObject* ReferencedObject)
	{
		TSharedPtr<FJsonObject> Reference = MakeShared<FJsonObject>();
		if (!ReferencedObject)
		{
			return Reference;
		}

		Reference->SetStringField(TEXT("object_path"), ReferencedObject->GetPathName());
		Reference->SetStringField(TEXT("package_path"), ReferencedObject->GetOutermost() ? ReferencedObject->GetOutermost()->GetName() : TEXT(""));
		Reference->SetStringField(TEXT("name"), ReferencedObject->GetName());
		Reference->SetStringField(TEXT("class_path"), ReferencedObject->GetClass() ? ReferencedObject->GetClass()->GetPathName() : TEXT(""));
		Reference->SetStringField(TEXT("class_name"), ReferencedObject->GetClass() ? ReferencedObject->GetClass()->GetName() : TEXT(""));
		return Reference;
	}

	static TSharedPtr<FJsonObject> ExportInspectableProperties(
		UObject* Object,
		int32 PropertyLimit,
		TArray<TSharedPtr<FJsonValue>>& OutReferences,
		TArray<TSharedPtr<FJsonValue>>& OutPartialFailures)
	{
		TSharedPtr<FJsonObject> Values = MakeShared<FJsonObject>();
		if (!Object)
		{
			return Values;
		}

		int32 ExportedCount = 0;
		for (TFieldIterator<FProperty> It(Object->GetClass(), EFieldIteratorFlags::IncludeSuper); It; ++It)
		{
			FProperty* Property = *It;
			if (!Property ||
				Property->HasAnyPropertyFlags(CPF_Transient | CPF_Deprecated) ||
				Property->HasAnyPropertyFlags(CPF_DisableEditOnInstance))
			{
				continue;
			}

			const bool bInspectable =
				Property->HasAnyPropertyFlags(CPF_Edit | CPF_BlueprintVisible | CPF_BlueprintReadOnly) ||
				Property->HasAnyPropertyFlags(CPF_AssetRegistrySearchable);
			if (!bInspectable)
			{
				continue;
			}

			if (PropertyLimit > 0 && ExportedCount >= PropertyLimit)
			{
				TSharedPtr<FJsonObject> Failure = MakeShared<FJsonObject>();
				Failure->SetStringField(TEXT("stage"), TEXT("properties"));
				Failure->SetStringField(TEXT("reason"), FString::Printf(TEXT("Property limit %d reached."), PropertyLimit));
				OutPartialFailures.Add(MakeShared<FJsonValueObject>(Failure));
				break;
			}

			TSharedPtr<FJsonObject> PropertyObject = MakeShared<FJsonObject>();
			PropertyObject->SetStringField(TEXT("name"), Property->GetName());
			PropertyObject->SetStringField(TEXT("cpp_type"), Property->GetCPPType());
			PropertyObject->SetBoolField(TEXT("editable"), Property->HasAnyPropertyFlags(CPF_Edit));
			PropertyObject->SetBoolField(TEXT("blueprint_visible"), Property->HasAnyPropertyFlags(CPF_BlueprintVisible));
			PropertyObject->SetBoolField(TEXT("blueprint_read_only"), Property->HasAnyPropertyFlags(CPF_BlueprintReadOnly));
			PropertyObject->SetBoolField(TEXT("asset_registry_searchable"), Property->HasAnyPropertyFlags(CPF_AssetRegistrySearchable));

			FString ExportedValue;
			if (Property->ExportText_InContainer(0, ExportedValue, Object, nullptr, Object, PPF_None))
			{
				PropertyObject->SetStringField(TEXT("value"), ExportedValue);
			}
			else
			{
				TSharedPtr<FJsonObject> Failure = MakeShared<FJsonObject>();
				Failure->SetStringField(TEXT("stage"), TEXT("property_export"));
				Failure->SetStringField(TEXT("property"), Property->GetName());
				Failure->SetStringField(TEXT("reason"), TEXT("ExportText_InContainer returned false."));
				OutPartialFailures.Add(MakeShared<FJsonValueObject>(Failure));
			}

			if (const FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property))
			{
				UObject* ReferencedObject = ObjectProperty->GetObjectPropertyValue_InContainer(Object);
				if (ReferencedObject)
				{
					TSharedPtr<FJsonObject> Reference = MakeObjectReferenceObject(ReferencedObject);
					Reference->SetStringField(TEXT("property"), Property->GetName());
					OutReferences.Add(MakeShared<FJsonValueObject>(Reference));
					PropertyObject->SetObjectField(TEXT("reference"), Reference);
				}
			}

			Values->SetObjectField(Property->GetName(), PropertyObject);
			++ExportedCount;
		}

		return Values;
	}

	static bool IsGameplayCameraObject(const UObject* Object)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		const FString ClassPath = Object->GetClass()->GetPathName();
		return ClassPath.Contains(TEXT("/Script/GameplayCameras."));
	}

	static bool IsGameplayCameraAsset(const UObject* Object)
	{
		if (!IsGameplayCameraObject(Object))
		{
			return false;
		}

		const FString ClassName = Object->GetClass()->GetName();
		return ClassName.Equals(TEXT("CameraAsset")) ||
			ClassName.Equals(TEXT("CameraRigAsset")) ||
			ClassName.Equals(TEXT("CameraRigProxyAsset")) ||
			ClassName.Equals(TEXT("CameraVariableCollection"));
	}

	static FString GetGameplayCameraAssetKind(const UObject* Object)
	{
		if (!Object || !Object->GetClass())
		{
			return TEXT("unknown");
		}

		const FString ClassName = Object->GetClass()->GetName();
		if (ClassName.Equals(TEXT("CameraAsset")))
		{
			return TEXT("camera_asset");
		}
		if (ClassName.Equals(TEXT("CameraRigAsset")))
		{
			return TEXT("camera_rig_asset");
		}
		if (ClassName.Equals(TEXT("CameraRigProxyAsset")))
		{
			return TEXT("camera_rig_proxy_asset");
		}
		if (ClassName.Equals(TEXT("CameraVariableCollection")))
		{
			return TEXT("camera_variable_collection");
		}
		return TEXT("gameplay_camera_object");
	}

	static TSharedPtr<FJsonObject> MakeGameplayCameraObjectSummary(const UObject* Object, const FString& Role = TEXT(""))
	{
		TSharedPtr<FJsonObject> Summary = MakeObjectReferenceObject(Object);
		if (!Object)
		{
			return Summary;
		}

		if (!Role.IsEmpty())
		{
			Summary->SetStringField(TEXT("role"), Role);
		}
		Summary->SetBoolField(TEXT("is_gameplay_camera_object"), IsGameplayCameraObject(Object));
		Summary->SetBoolField(TEXT("is_asset"), Object->IsAsset());
		return Summary;
	}

	static UObject* GetObjectPropertyValueByName(UObject* Object, const TCHAR* PropertyName)
	{
		if (!Object || !Object->GetClass())
		{
			return nullptr;
		}

		FProperty* Property = Object->GetClass()->FindPropertyByName(PropertyName);
		const FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property);
		if (!ObjectProperty)
		{
			return nullptr;
		}

		return ObjectProperty->GetObjectPropertyValue_InContainer(Object);
	}

	static void GetObjectArrayPropertyValuesByName(UObject* Object, const TCHAR* PropertyName, TArray<UObject*>& OutObjects)
	{
		if (!Object || !Object->GetClass())
		{
			return;
		}

		FProperty* Property = Object->GetClass()->FindPropertyByName(PropertyName);
		const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property);
		if (!ArrayProperty)
		{
			return;
		}

		const FObjectPropertyBase* InnerObjectProperty = CastField<FObjectPropertyBase>(ArrayProperty->Inner);
		if (!InnerObjectProperty)
		{
			return;
		}

		void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(Object);
		FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
		for (int32 Index = 0; Index < Helper.Num(); ++Index)
		{
			UObject* ReferencedObject = InnerObjectProperty->GetObjectPropertyValue(Helper.GetRawPtr(Index));
			if (ReferencedObject)
			{
				OutObjects.Add(ReferencedObject);
			}
		}
	}

	static bool SetObjectPropertyValueByName(UObject* Object, const TCHAR* PropertyName, UObject* Value)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FProperty* Property = Object->GetClass()->FindPropertyByName(PropertyName);
		FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property);
		if (!ObjectProperty)
		{
			return false;
		}

		ObjectProperty->SetObjectPropertyValue_InContainer(Object, Value);
		return true;
	}

	static bool SetNamePropertyValueByName(UObject* Object, const TCHAR* PropertyName, const FName& Value)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FNameProperty* NameProperty = CastField<FNameProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		if (!NameProperty)
		{
			return false;
		}

		NameProperty->SetPropertyValue_InContainer(Object, Value);
		return true;
	}

	static bool SetStringPropertyValueByName(UObject* Object, const TCHAR* PropertyName, const FString& Value)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FStrProperty* StringProperty = CastField<FStrProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		if (!StringProperty)
		{
			return false;
		}

		StringProperty->SetPropertyValue_InContainer(Object, Value);
		return true;
	}

	static bool SetIntPropertyValueByName(UObject* Object, const TCHAR* PropertyName, int32 Value)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FIntProperty* IntProperty = CastField<FIntProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		if (!IntProperty)
		{
			return false;
		}

		IntProperty->SetPropertyValue_InContainer(Object, Value);
		return true;
	}

	static bool SetBoolPropertyValueByName(UObject* Object, const TCHAR* PropertyName, bool Value)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FBoolProperty* BoolProperty = CastField<FBoolProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		if (!BoolProperty)
		{
			return false;
		}

		BoolProperty->SetPropertyValue_InContainer(Object, Value);
		return true;
	}

	static bool SetClassPropertyValueByName(UObject* Object, const TCHAR* PropertyName, UClass* Value)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FClassProperty* ClassProperty = CastField<FClassProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		if (!ClassProperty)
		{
			return false;
		}

		ClassProperty->SetObjectPropertyValue_InContainer(Object, Value);
		return true;
	}

	static bool AddNameToArrayPropertyByName(UObject* Object, const TCHAR* PropertyName, const FName& Value)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		FNameProperty* InnerNameProperty = ArrayProperty ? CastField<FNameProperty>(ArrayProperty->Inner) : nullptr;
		if (!ArrayProperty || !InnerNameProperty)
		{
			return false;
		}

		void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(Object);
		FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
		const int32 NewIndex = Helper.AddValue();
		InnerNameProperty->SetPropertyValue(Helper.GetRawPtr(NewIndex), Value);
		return true;
	}

	static bool SetVectorPropertyValueByName(UObject* Object, const TCHAR* PropertyName, const FVector& Value)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FStructProperty* StructProperty = CastField<FStructProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		if (!StructProperty || StructProperty->Struct != TBaseStructure<FVector>::Get())
		{
			return false;
		}

		*StructProperty->ContainerPtrToValuePtr<FVector>(Object) = Value;
		return true;
	}

	static bool SetRotatorPropertyValueByName(UObject* Object, const TCHAR* PropertyName, const FRotator& Value)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FStructProperty* StructProperty = CastField<FStructProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		if (!StructProperty || StructProperty->Struct != TBaseStructure<FRotator>::Get())
		{
			return false;
		}

		*StructProperty->ContainerPtrToValuePtr<FRotator>(Object) = Value;
		return true;
	}

	static bool AddObjectToArrayPropertyByName(UObject* Object, const TCHAR* PropertyName, UObject* Value)
	{
		if (!Object || !Value || !Object->GetClass())
		{
			return false;
		}

		FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		if (!ArrayProperty)
		{
			return false;
		}

		FObjectPropertyBase* InnerObjectProperty = CastField<FObjectPropertyBase>(ArrayProperty->Inner);
		if (!InnerObjectProperty)
		{
			return false;
		}

		void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(Object);
		FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
		const int32 NewIndex = Helper.AddValue();
		InnerObjectProperty->SetObjectPropertyValue(Helper.GetRawPtr(NewIndex), Value);
		return true;
	}

	static bool AddObjectToStructArrayPropertyByName(UObject* Object, const TCHAR* StructPropertyName, const TCHAR* ArrayPropertyName, UObject* Value)
	{
		if (!Object || !Value || !Object->GetClass())
		{
			return false;
		}

		FStructProperty* StructProperty = CastField<FStructProperty>(Object->GetClass()->FindPropertyByName(StructPropertyName));
		if (!StructProperty)
		{
			return false;
		}

		FArrayProperty* ArrayProperty = CastField<FArrayProperty>(StructProperty->Struct->FindPropertyByName(ArrayPropertyName));
		if (!ArrayProperty)
		{
			return false;
		}

		FObjectPropertyBase* InnerObjectProperty = CastField<FObjectPropertyBase>(ArrayProperty->Inner);
		if (!InnerObjectProperty)
		{
			return false;
		}

		void* StructValue = StructProperty->ContainerPtrToValuePtr<void>(Object);
		void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(StructValue);
		FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
		const int32 NewIndex = Helper.AddValue();
		InnerObjectProperty->SetObjectPropertyValue(Helper.GetRawPtr(NewIndex), Value);
		return true;
	}

	static UClass* LoadReproClass(const TCHAR* ClassPath)
	{
		return LoadObject<UClass>(nullptr, ClassPath);
	}

	static UObject* CreateReproObject(UObject* Outer, UClass* Class, const FString& Name, EObjectFlags Flags = RF_Public | RF_Standalone | RF_Transactional)
	{
		if (!Outer || !Class || Name.IsEmpty())
		{
			return nullptr;
		}

		return NewObject<UObject>(Outer, Class, FName(*Name), Flags);
	}

	static bool ImportPropertyValueByName(UObject* Object, const TCHAR* PropertyName, const FString& Value, FString& OutError)
	{
		if (!Object || !Object->GetClass())
		{
			OutError = TEXT("Property import target is invalid.");
			return false;
		}

		FProperty* Property = Object->GetClass()->FindPropertyByName(PropertyName);
		if (!Property)
		{
			OutError = FString::Printf(TEXT("Property '%s' was not found on %s."), PropertyName, *Object->GetClass()->GetName());
			return false;
		}

		const TCHAR* Result = Property->ImportText_Direct(*Value, Property->ContainerPtrToValuePtr<void>(Object), Object, PPF_None);
		if (!Result)
		{
			OutError = FString::Printf(TEXT("Could not import value '%s' for property '%s'."), *Value, PropertyName);
			return false;
		}

		return true;
	}

	static bool IsMaterialObject(const UObject* Object)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		return Object->IsA<UMaterialInterface>() || Object->IsA<UMaterialFunctionInterface>();
	}

	static FString GetMaterialAssetKind(const UObject* Object)
	{
		if (!Object)
		{
			return TEXT("unknown");
		}
		if (Object->IsA<UMaterial>())
		{
			return TEXT("material");
		}
		if (Object->IsA<UMaterialInstanceConstant>())
		{
			return TEXT("material_instance_constant");
		}
		if (Object->IsA<UMaterialInstance>())
		{
			return TEXT("material_instance");
		}
		if (Object->IsA<UMaterialFunction>())
		{
			return TEXT("material_function");
		}
		if (Object->IsA<UMaterialFunctionInterface>())
		{
			return TEXT("material_function_interface");
		}
		return TEXT("material_object");
	}

	static FString GetMaterialExpressionId(const UMaterialExpression* Expression)
	{
		if (!Expression)
		{
			return TEXT("");
		}

		if (Expression->MaterialExpressionGuid.IsValid())
		{
			return Expression->MaterialExpressionGuid.ToString(EGuidFormats::DigitsWithHyphens);
		}
		return Expression->GetPathName();
	}

	static TSharedPtr<FJsonObject> ExportMaterialExpressionInput(const FExpressionInput* Input, const FString& InputName)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("input_name"), InputName);
		if (!Input)
		{
			Result->SetBoolField(TEXT("connected"), false);
			return Result;
		}

		Result->SetBoolField(TEXT("connected"), Input->Expression != nullptr);
		Result->SetNumberField(TEXT("output_index"), Input->OutputIndex);
		Result->SetStringField(TEXT("raw_input_name"), Input->InputName.ToString());
		Result->SetNumberField(TEXT("mask"), Input->Mask);
		Result->SetNumberField(TEXT("mask_r"), Input->MaskR);
		Result->SetNumberField(TEXT("mask_g"), Input->MaskG);
		Result->SetNumberField(TEXT("mask_b"), Input->MaskB);
		Result->SetNumberField(TEXT("mask_a"), Input->MaskA);
		if (Input->Expression)
		{
			Result->SetStringField(TEXT("source_node_guid"), GetMaterialExpressionId(Input->Expression));
			Result->SetStringField(TEXT("source_node_title"), Input->Expression->GetName());
			Result->SetStringField(TEXT("source_node_class"), Input->Expression->GetClass() ? Input->Expression->GetClass()->GetName() : TEXT(""));
		}
		return Result;
	}

	static TSharedPtr<FJsonObject> ExportMaterialExpression(UMaterialExpression* Expression)
	{
		TSharedPtr<FJsonObject> Result = MakeObjectReferenceObject(Expression);
		if (!Expression)
		{
			return Result;
		}

		Result->SetStringField(TEXT("node_guid"), GetMaterialExpressionId(Expression));
		Result->SetStringField(TEXT("node_title"), Expression->GetName());
		Result->SetStringField(TEXT("node_kind"), TEXT("material_expression"));
		Result->SetStringField(TEXT("expression_class"), Expression->GetClass() ? Expression->GetClass()->GetName() : TEXT(""));
		Result->SetStringField(TEXT("description"), Expression->Desc);
		Result->SetNumberField(TEXT("node_pos_x"), Expression->MaterialExpressionEditorX);
		Result->SetNumberField(TEXT("node_pos_y"), Expression->MaterialExpressionEditorY);
		Result->SetBoolField(TEXT("is_parameter"), Expression->bIsParameterExpression != 0);
		if (Expression->HasAParameterName())
		{
			Result->SetStringField(TEXT("parameter_name"), Expression->GetParameterName().ToString());
		}

		TArray<TSharedPtr<FJsonValue>> References;
		TArray<TSharedPtr<FJsonValue>> PartialFailures;
		Result->SetObjectField(TEXT("properties"), ExportInspectableProperties(Expression, 40, References, PartialFailures));
		Result->SetArrayField(TEXT("references"), References);

		TArray<TSharedPtr<FJsonValue>> Outputs;
		for (int32 OutputIndex = 0; OutputIndex < Expression->Outputs.Num(); ++OutputIndex)
		{
			const FExpressionOutput& Output = Expression->Outputs[OutputIndex];
			TSharedPtr<FJsonObject> OutputObject = MakeShared<FJsonObject>();
			OutputObject->SetNumberField(TEXT("output_index"), OutputIndex);
			OutputObject->SetStringField(TEXT("output_name"), Output.OutputName.ToString());
			OutputObject->SetNumberField(TEXT("mask"), Output.Mask);
			OutputObject->SetNumberField(TEXT("mask_r"), Output.MaskR);
			OutputObject->SetNumberField(TEXT("mask_g"), Output.MaskG);
			OutputObject->SetNumberField(TEXT("mask_b"), Output.MaskB);
			OutputObject->SetNumberField(TEXT("mask_a"), Output.MaskA);
			Outputs.Add(MakeShared<FJsonValueObject>(OutputObject));
		}
		Result->SetArrayField(TEXT("outputs"), Outputs);
		Result->SetNumberField(TEXT("output_count"), Outputs.Num());

		TArray<TSharedPtr<FJsonValue>> Inputs;
		int32 InputIndex = 0;
		for (FExpressionInputIterator It(Expression); It; ++It, ++InputIndex)
		{
			const FString InputName = Expression->GetInputName(InputIndex).ToString();
			TSharedPtr<FJsonObject> InputObject = ExportMaterialExpressionInput(It.Input, InputName);
			InputObject->SetNumberField(TEXT("input_index"), InputIndex);
			Inputs.Add(MakeShared<FJsonValueObject>(InputObject));
		}
		Result->SetArrayField(TEXT("inputs"), Inputs);
		Result->SetNumberField(TEXT("input_count"), Inputs.Num());

		TArray<TSharedPtr<FJsonValue>> ReferencedTextures;
		for (UObject* Texture : Expression->GetReferencedTextures())
		{
			if (Texture)
			{
				ReferencedTextures.Add(MakeShared<FJsonValueObject>(MakeObjectReferenceObject(Texture)));
			}
		}
		Result->SetArrayField(TEXT("referenced_textures"), ReferencedTextures);
		Result->SetNumberField(TEXT("referenced_texture_count"), ReferencedTextures.Num());
		Result->SetArrayField(TEXT("partial_failures"), PartialFailures);
		return Result;
	}

	static TSharedPtr<FJsonObject> ExportMaterialRootInput(const FString& Name, FExpressionInput* Input)
	{
		TSharedPtr<FJsonObject> Result = ExportMaterialExpressionInput(Input, Name);
		Result->SetStringField(TEXT("root_property"), Name);
		Result->SetStringField(TEXT("target"), TEXT("material_root"));
		return Result;
	}

	static void AddMaterialConnection(TArray<TSharedPtr<FJsonValue>>& Connections, const FString& TargetNodeGuid, const FString& TargetPinName, const FExpressionInput* Input)
	{
		if (!Input || !Input->Expression)
		{
			return;
		}

		TSharedPtr<FJsonObject> Connection = MakeShared<FJsonObject>();
		Connection->SetStringField(TEXT("source_node_guid"), GetMaterialExpressionId(Input->Expression));
		Connection->SetStringField(TEXT("source_node_title"), Input->Expression->GetName());
		Connection->SetNumberField(TEXT("source_output_index"), Input->OutputIndex);
		Connection->SetStringField(TEXT("target_node_guid"), TargetNodeGuid);
		Connection->SetStringField(TEXT("target_pin_name"), TargetPinName);
		Connections.Add(MakeShared<FJsonValueObject>(Connection));
	}

	static void AddMaterialRootInput(
		UMaterial* Material,
		EMaterialProperty Property,
		const FString& Name,
		TArray<TSharedPtr<FJsonValue>>& RootInputs,
		TArray<TSharedPtr<FJsonValue>>& Connections)
	{
		if (!Material)
		{
			return;
		}

		FExpressionInput* Input = Material->GetExpressionInputForProperty(Property);
		if (!Input)
		{
			return;
		}

		RootInputs.Add(MakeShared<FJsonValueObject>(ExportMaterialRootInput(Name, Input)));
		AddMaterialConnection(Connections, TEXT("material_root"), Name, Input);
	}

	static void ExportMaterialExpressionArray(
		TSharedPtr<FJsonObject> Result,
		TConstArrayView<TObjectPtr<UMaterialExpression>> Expressions,
		TArray<TSharedPtr<FJsonValue>>& Connections)
	{
		TArray<TSharedPtr<FJsonValue>> ExpressionValues;
		for (UMaterialExpression* Expression : Expressions)
		{
			if (!Expression)
			{
				continue;
			}

			TSharedPtr<FJsonObject> ExpressionObject = ExportMaterialExpression(Expression);
			ExpressionValues.Add(MakeShared<FJsonValueObject>(ExpressionObject));

			const FString TargetNodeGuid = GetMaterialExpressionId(Expression);
			int32 InputIndex = 0;
			for (FExpressionInputIterator It(Expression); It; ++It, ++InputIndex)
			{
				AddMaterialConnection(Connections, TargetNodeGuid, Expression->GetInputName(InputIndex).ToString(), It.Input);
			}
		}

		Result->SetArrayField(TEXT("expressions"), ExpressionValues);
		Result->SetNumberField(TEXT("expression_count"), ExpressionValues.Num());
	}

	static void ExportMaterialCommentArray(TSharedPtr<FJsonObject> Result, TConstArrayView<TObjectPtr<UMaterialExpressionComment>> Comments)
	{
		TArray<TSharedPtr<FJsonValue>> CommentValues;
		for (UMaterialExpressionComment* Comment : Comments)
		{
			if (!Comment)
			{
				continue;
			}

			TSharedPtr<FJsonObject> CommentObject = MakeObjectReferenceObject(Comment);
			CommentObject->SetStringField(TEXT("node_guid"), GetMaterialExpressionId(Comment));
			CommentObject->SetStringField(TEXT("node_kind"), TEXT("comment"));
			CommentObject->SetStringField(TEXT("comment"), Comment->Text);
			CommentObject->SetStringField(TEXT("node_title"), Comment->Text);
			CommentObject->SetNumberField(TEXT("node_pos_x"), Comment->MaterialExpressionEditorX);
			CommentObject->SetNumberField(TEXT("node_pos_y"), Comment->MaterialExpressionEditorY);
			CommentObject->SetNumberField(TEXT("width"), Comment->SizeX);
			CommentObject->SetNumberField(TEXT("height"), Comment->SizeY);
			CommentValues.Add(MakeShared<FJsonValueObject>(CommentObject));
		}
		Result->SetArrayField(TEXT("comments"), CommentValues);
		Result->SetNumberField(TEXT("comment_count"), CommentValues.Num());
	}

	static TSharedPtr<FJsonObject> ExportPropertyValueSummary(
		const FProperty* Property,
		const void* ValuePtr,
		int32 Depth,
		int32 MaxDepth);

	static void ExportMaterialInstanceParameterArray(UObject* Instance, const TCHAR* PropertyName, const FString& OutputName, TSharedPtr<FJsonObject> Result)
	{
		TArray<TSharedPtr<FJsonValue>> Values;
		if (Instance && Instance->GetClass())
		{
			if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Instance->GetClass()->FindPropertyByName(PropertyName)))
			{
				void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(Instance);
				FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
				for (int32 Index = 0; Index < Helper.Num(); ++Index)
				{
					TSharedPtr<FJsonObject> Parameter = ExportPropertyValueSummary(ArrayProperty->Inner, Helper.GetRawPtr(Index), 0, 4);
					Parameter->SetNumberField(TEXT("index"), Index);
					Parameter->SetStringField(TEXT("parameter_kind"), OutputName);
					Parameter->SetBoolField(TEXT("overridden"), true);
					Values.Add(MakeShared<FJsonValueObject>(Parameter));
				}
			}
		}
		Result->SetArrayField(OutputName, Values);
		Result->SetNumberField(FString::Printf(TEXT("%s_count"), *OutputName), Values.Num());
	}

	static TSharedPtr<FJsonObject> ExportMaterialInspection(UObject* AssetObject)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("recognized"), IsMaterialObject(AssetObject));
		Result->SetStringField(TEXT("asset_kind"), GetMaterialAssetKind(AssetObject));
		if (!IsMaterialObject(AssetObject))
		{
			return Result;
		}

		Result->SetObjectField(TEXT("asset"), MakeObjectReferenceObject(AssetObject));
		if (UMaterial* Material = Cast<UMaterial>(AssetObject))
		{
			TArray<TSharedPtr<FJsonValue>> Connections;
			Result->SetStringField(TEXT("material_domain"), StaticEnum<EMaterialDomain>() ? StaticEnum<EMaterialDomain>()->GetNameStringByValue(Material->MaterialDomain) : FString::FromInt(Material->MaterialDomain));
			Result->SetStringField(TEXT("blend_mode"), StaticEnum<EBlendMode>() ? StaticEnum<EBlendMode>()->GetNameStringByValue(Material->BlendMode) : FString::FromInt(Material->BlendMode));
			Result->SetBoolField(TEXT("two_sided"), Material->TwoSided != 0);
			Result->SetBoolField(TEXT("use_material_attributes"), Material->bUseMaterialAttributes != 0);

			TArray<TSharedPtr<FJsonValue>> RootInputs;
			AddMaterialRootInput(Material, MP_BaseColor, TEXT("BaseColor"), RootInputs, Connections);
			AddMaterialRootInput(Material, MP_Metallic, TEXT("Metallic"), RootInputs, Connections);
			AddMaterialRootInput(Material, MP_Specular, TEXT("Specular"), RootInputs, Connections);
			AddMaterialRootInput(Material, MP_Roughness, TEXT("Roughness"), RootInputs, Connections);
			AddMaterialRootInput(Material, MP_EmissiveColor, TEXT("EmissiveColor"), RootInputs, Connections);
			AddMaterialRootInput(Material, MP_Opacity, TEXT("Opacity"), RootInputs, Connections);
			AddMaterialRootInput(Material, MP_Normal, TEXT("Normal"), RootInputs, Connections);
			AddMaterialRootInput(Material, MP_MaterialAttributes, TEXT("MaterialAttributes"), RootInputs, Connections);
			Result->SetArrayField(TEXT("root_inputs"), RootInputs);
			Result->SetNumberField(TEXT("root_input_count"), RootInputs.Num());

#if WITH_EDITORONLY_DATA
			ExportMaterialExpressionArray(Result, Material->GetExpressions(), Connections);
#endif
#if WITH_EDITOR
			ExportMaterialCommentArray(Result, Material->GetEditorComments());
#endif
			Result->SetArrayField(TEXT("connections"), Connections);
			Result->SetNumberField(TEXT("connection_count"), Connections.Num());
		}
		else if (UMaterialInstance* Instance = Cast<UMaterialInstance>(AssetObject))
		{
			Result->SetObjectField(TEXT("parent"), MakeObjectReferenceObject(Instance->Parent));
			ExportMaterialInstanceParameterArray(Instance, TEXT("ScalarParameterValues"), TEXT("scalar_parameters"), Result);
			ExportMaterialInstanceParameterArray(Instance, TEXT("VectorParameterValues"), TEXT("vector_parameters"), Result);
			ExportMaterialInstanceParameterArray(Instance, TEXT("TextureParameterValues"), TEXT("texture_parameters"), Result);
			ExportMaterialInstanceParameterArray(Instance, TEXT("RuntimeVirtualTextureParameterValues"), TEXT("runtime_virtual_texture_parameters"), Result);
			ExportMaterialInstanceParameterArray(Instance, TEXT("SparseVolumeTextureParameterValues"), TEXT("sparse_volume_texture_parameters"), Result);
			ExportMaterialInstanceParameterArray(Instance, TEXT("FontParameterValues"), TEXT("font_parameters"), Result);
		}
		else if (UMaterialFunction* Function = Cast<UMaterialFunction>(AssetObject))
		{
			TArray<TSharedPtr<FJsonValue>> Connections;
			Result->SetStringField(TEXT("description"), Function->Description);
			ExportMaterialExpressionArray(Result, Function->GetExpressions(), Connections);
			ExportMaterialCommentArray(Result, Function->GetExpressionCollection().EditorComments);
			Result->SetArrayField(TEXT("connections"), Connections);
			Result->SetNumberField(TEXT("connection_count"), Connections.Num());
		}

		return Result;
	}

	static void GetGameplayCameraInterfaceParameters(UObject* CameraRig, TArray<UObject*>& OutObjects)
	{
		if (!CameraRig || !CameraRig->GetClass())
		{
			return;
		}

		const FStructProperty* InterfaceProperty = CastField<FStructProperty>(CameraRig->GetClass()->FindPropertyByName(TEXT("Interface")));
		if (!InterfaceProperty)
		{
			return;
		}

		void* InterfaceValue = InterfaceProperty->ContainerPtrToValuePtr<void>(CameraRig);
		const FArrayProperty* ParametersProperty = CastField<FArrayProperty>(InterfaceProperty->Struct->FindPropertyByName(TEXT("InterfaceParameters")));
		if (!ParametersProperty)
		{
			return;
		}

		const FObjectPropertyBase* InnerObjectProperty = CastField<FObjectPropertyBase>(ParametersProperty->Inner);
		if (!InnerObjectProperty)
		{
			return;
		}

		void* ArrayValue = ParametersProperty->ContainerPtrToValuePtr<void>(InterfaceValue);
		FScriptArrayHelper Helper(ParametersProperty, ArrayValue);
		for (int32 Index = 0; Index < Helper.Num(); ++Index)
		{
			UObject* ParameterObject = InnerObjectProperty->GetObjectPropertyValue(Helper.GetRawPtr(Index));
			if (ParameterObject)
			{
				OutObjects.Add(ParameterObject);
			}
		}
	}

	static void AddGameplayCameraObjectArray(
		TSharedPtr<FJsonObject> Result,
		const TCHAR* FieldName,
		const FString& Role,
		const TArray<UObject*>& Objects)
	{
		TArray<TSharedPtr<FJsonValue>> Values;
		for (UObject* Object : Objects)
		{
			Values.Add(MakeShared<FJsonValueObject>(MakeGameplayCameraObjectSummary(Object, Role)));
		}
		Result->SetArrayField(FieldName, Values);
		Result->SetNumberField(FString::Printf(TEXT("%s_count"), FieldName), Values.Num());
	}

	static TSharedPtr<FJsonObject> ExportGameplayCameraObjectGraph(
		UObject* Object,
		const FString& Role,
		int32 Depth,
		TSet<const UObject*>& Visited)
	{
		TSharedPtr<FJsonObject> Node = MakeGameplayCameraObjectSummary(Object, Role);
		if (!Object)
		{
			return Node;
		}

		if (Visited.Contains(Object))
		{
			Node->SetBoolField(TEXT("cycle"), true);
			return Node;
		}

		Visited.Add(Object);
		Node->SetNumberField(TEXT("depth"), Depth);

		TArray<TSharedPtr<FJsonValue>> ChildValues;
		if (Depth < 6 && Object->GetClass())
		{
			for (TFieldIterator<FProperty> It(Object->GetClass(), EFieldIteratorFlags::IncludeSuper); It; ++It)
			{
				FProperty* Property = *It;
				if (!Property || Property->HasAnyPropertyFlags(CPF_Transient | CPF_Deprecated))
				{
					continue;
				}

				if (const FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property))
				{
					UObject* ReferencedObject = ObjectProperty->GetObjectPropertyValue_InContainer(Object);
					if (IsGameplayCameraObject(ReferencedObject))
					{
						ChildValues.Add(MakeShared<FJsonValueObject>(ExportGameplayCameraObjectGraph(ReferencedObject, Property->GetName(), Depth + 1, Visited)));
					}
				}
				else if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property))
				{
					const FObjectPropertyBase* InnerObjectProperty = CastField<FObjectPropertyBase>(ArrayProperty->Inner);
					if (!InnerObjectProperty)
					{
						continue;
					}

					void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(Object);
					FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
					for (int32 Index = 0; Index < Helper.Num(); ++Index)
					{
						UObject* ReferencedObject = InnerObjectProperty->GetObjectPropertyValue(Helper.GetRawPtr(Index));
						if (IsGameplayCameraObject(ReferencedObject))
						{
							ChildValues.Add(MakeShared<FJsonValueObject>(ExportGameplayCameraObjectGraph(
								ReferencedObject,
								FString::Printf(TEXT("%s[%d]"), *Property->GetName(), Index),
								Depth + 1,
								Visited)));
						}
					}
				}
			}
		}

		Node->SetArrayField(TEXT("children"), ChildValues);
		Node->SetNumberField(TEXT("child_count"), ChildValues.Num());

		TArray<TSharedPtr<FJsonValue>> PropertyReferences;
		TArray<TSharedPtr<FJsonValue>> PropertyFailures;
		Node->SetObjectField(TEXT("properties"), ExportInspectableProperties(Object, 40, PropertyReferences, PropertyFailures));
		return Node;
	}

	static TSharedPtr<FJsonObject> ExportGameplayCameraInspection(UObject* AssetObject)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("recognized"), IsGameplayCameraObject(AssetObject));
		Result->SetStringField(TEXT("asset_kind"), GetGameplayCameraAssetKind(AssetObject));

		if (!IsGameplayCameraObject(AssetObject))
		{
			return Result;
		}

		TSet<const UObject*> Visited;
		Result->SetObjectField(TEXT("object_graph"), ExportGameplayCameraObjectGraph(AssetObject, TEXT("asset"), 0, Visited));

		const FString ClassName = AssetObject && AssetObject->GetClass() ? AssetObject->GetClass()->GetName() : TEXT("");
		if (ClassName.Equals(TEXT("CameraAsset")))
		{
			Result->SetObjectField(TEXT("camera_director"), MakeGameplayCameraObjectSummary(GetObjectPropertyValueByName(AssetObject, TEXT("CameraDirector")), TEXT("camera_director")));

			TArray<UObject*> CameraRigs;
			TArray<UObject*> EnterTransitions;
			TArray<UObject*> ExitTransitions;
			TArray<UObject*> SharedTransitionObjects;
			GetObjectArrayPropertyValuesByName(AssetObject, TEXT("CameraRigs"), CameraRigs);
			GetObjectArrayPropertyValuesByName(AssetObject, TEXT("EnterTransitions"), EnterTransitions);
			GetObjectArrayPropertyValuesByName(AssetObject, TEXT("ExitTransitions"), ExitTransitions);
			GetObjectArrayPropertyValuesByName(AssetObject, TEXT("AllSharedTransitionsObjects"), SharedTransitionObjects);
			AddGameplayCameraObjectArray(Result, TEXT("camera_rigs"), TEXT("camera_rig"), CameraRigs);
			AddGameplayCameraObjectArray(Result, TEXT("enter_transitions"), TEXT("enter_transition"), EnterTransitions);
			AddGameplayCameraObjectArray(Result, TEXT("exit_transitions"), TEXT("exit_transition"), ExitTransitions);
			AddGameplayCameraObjectArray(Result, TEXT("shared_transition_objects"), TEXT("shared_transition_object"), SharedTransitionObjects);
		}
		else if (ClassName.Equals(TEXT("CameraRigAsset")))
		{
			Result->SetObjectField(TEXT("root_node"), MakeGameplayCameraObjectSummary(GetObjectPropertyValueByName(AssetObject, TEXT("RootNode")), TEXT("root_node")));

			TArray<UObject*> InterfaceParameters;
			TArray<UObject*> EnterTransitions;
			TArray<UObject*> ExitTransitions;
			TArray<UObject*> NodeTreeObjects;
			TArray<UObject*> TransitionObjects;
			GetGameplayCameraInterfaceParameters(AssetObject, InterfaceParameters);
			GetObjectArrayPropertyValuesByName(AssetObject, TEXT("EnterTransitions"), EnterTransitions);
			GetObjectArrayPropertyValuesByName(AssetObject, TEXT("ExitTransitions"), ExitTransitions);
			GetObjectArrayPropertyValuesByName(AssetObject, TEXT("AllNodeTreeObjects"), NodeTreeObjects);
			GetObjectArrayPropertyValuesByName(AssetObject, TEXT("AllTransitionsObjects"), TransitionObjects);
			AddGameplayCameraObjectArray(Result, TEXT("interface_parameters"), TEXT("interface_parameter"), InterfaceParameters);
			AddGameplayCameraObjectArray(Result, TEXT("enter_transitions"), TEXT("enter_transition"), EnterTransitions);
			AddGameplayCameraObjectArray(Result, TEXT("exit_transitions"), TEXT("exit_transition"), ExitTransitions);
			AddGameplayCameraObjectArray(Result, TEXT("node_tree_objects"), TEXT("node_tree_object"), NodeTreeObjects);
			AddGameplayCameraObjectArray(Result, TEXT("transition_objects"), TEXT("transition_object"), TransitionObjects);
		}

		return Result;
	}

	static bool IsStateTreeObject(const UObject* Object)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		const FString ClassPath = Object->GetClass()->GetPathName();
		return ClassPath.Contains(TEXT("/Script/StateTreeModule.")) ||
			ClassPath.Contains(TEXT("/Script/StateTreeEditorModule."));
	}

	static bool IsStateTreeAsset(const UObject* Object)
	{
		return IsStateTreeObject(Object) && Object->GetClass() && Object->GetClass()->GetName().Equals(TEXT("StateTree"));
	}

	static TSharedPtr<FJsonObject> ExportStructValueSummary(
		const UStruct* Struct,
		const void* StructValue,
		int32 Depth,
		int32 MaxDepth);

	static FString ExportPropertyTextValue(const FProperty* Property, const void* ValuePtr, const void* DefaultValuePtr = nullptr)
	{
		if (!Property || !ValuePtr)
		{
			return TEXT("");
		}

		FString ExportedValue;
		Property->ExportText_Direct(ExportedValue, ValuePtr, DefaultValuePtr, nullptr, PPF_None);
		return ExportedValue;
	}

	static TSharedPtr<FJsonObject> ExportPropertyValueSummary(
		const FProperty* Property,
		const void* ValuePtr,
		int32 Depth,
		int32 MaxDepth)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		if (!Property || !ValuePtr)
		{
			return Result;
		}

		Result->SetStringField(TEXT("name"), Property->GetName());
		Result->SetStringField(TEXT("cpp_type"), Property->GetCPPType());

		if (const FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property))
		{
			UObject* ReferencedObject = ObjectProperty->GetObjectPropertyValue(ValuePtr);
			Result->SetStringField(TEXT("value"), ReferencedObject ? ReferencedObject->GetPathName() : TEXT("None"));
			if (ReferencedObject)
			{
				Result->SetObjectField(TEXT("reference"), MakeObjectReferenceObject(ReferencedObject));
			}
		}
		else if (const FStructProperty* StructProperty = CastField<FStructProperty>(Property))
		{
			Result->SetStringField(TEXT("value"), ExportPropertyTextValue(Property, ValuePtr));
			if (Depth < MaxDepth)
			{
				Result->SetObjectField(TEXT("struct"), ExportStructValueSummary(StructProperty->Struct, ValuePtr, Depth + 1, MaxDepth));
			}
		}
		else if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Property))
		{
			Result->SetStringField(TEXT("value"), ExportPropertyTextValue(Property, ValuePtr));
			Result->SetStringField(TEXT("element_cpp_type"), ArrayProperty->Inner ? ArrayProperty->Inner->GetCPPType() : TEXT(""));

			TArray<TSharedPtr<FJsonValue>> Items;
			FScriptArrayHelper Helper(ArrayProperty, ValuePtr);
			const int32 ItemLimit = FMath::Min(Helper.Num(), 50);
			for (int32 Index = 0; Index < ItemLimit; ++Index)
			{
				void* ItemPtr = Helper.GetRawPtr(Index);
				TSharedPtr<FJsonObject> Item = ExportPropertyValueSummary(ArrayProperty->Inner, ItemPtr, Depth + 1, MaxDepth);
				Item->SetNumberField(TEXT("index"), Index);
				Items.Add(MakeShared<FJsonValueObject>(Item));
			}
			Result->SetArrayField(TEXT("items"), Items);
			Result->SetNumberField(TEXT("count"), Helper.Num());
			if (Helper.Num() > ItemLimit)
			{
				Result->SetNumberField(TEXT("truncated_after"), ItemLimit);
			}
		}
		else
		{
			Result->SetStringField(TEXT("value"), ExportPropertyTextValue(Property, ValuePtr));
		}

		return Result;
	}

	static TSharedPtr<FJsonObject> ExportStructValueSummary(
		const UStruct* Struct,
		const void* StructValue,
		int32 Depth,
		int32 MaxDepth)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		if (!Struct || !StructValue)
		{
			return Result;
		}

		Result->SetStringField(TEXT("struct_name"), Struct->GetName());
		Result->SetStringField(TEXT("struct_path"), Struct->GetPathName());

		TArray<TSharedPtr<FJsonValue>> Fields;
		int32 FieldCount = 0;
		for (TFieldIterator<FProperty> It(Struct, EFieldIteratorFlags::IncludeSuper); It; ++It)
		{
			const FProperty* Property = *It;
			if (!Property || Property->HasAnyPropertyFlags(CPF_Transient | CPF_Deprecated))
			{
				continue;
			}

			const void* PropertyValue = Property->ContainerPtrToValuePtr<void>(StructValue);
			Fields.Add(MakeShared<FJsonValueObject>(ExportPropertyValueSummary(Property, PropertyValue, Depth + 1, MaxDepth)));
			++FieldCount;
			if (FieldCount >= 60)
			{
				Result->SetBoolField(TEXT("truncated"), true);
				break;
			}
		}

		Result->SetArrayField(TEXT("fields"), Fields);
		Result->SetNumberField(TEXT("field_count"), Fields.Num());
		return Result;
	}

	static TSharedPtr<FJsonObject> ExportStateTreeEditorNode(const FStructProperty* NodeProperty, const void* NodePtr)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		if (!NodeProperty || !NodePtr)
		{
			return Result;
		}

		Result->SetObjectField(TEXT("raw"), ExportStructValueSummary(NodeProperty->Struct, NodePtr, 0, 4));

		const FGuid* IdPtr = nullptr;
		if (const FStructProperty* IdProperty = CastField<FStructProperty>(NodeProperty->Struct->FindPropertyByName(TEXT("ID"))))
		{
			if (IdProperty->Struct && IdProperty->Struct->GetName().Equals(TEXT("Guid"), ESearchCase::IgnoreCase))
			{
				IdPtr = IdProperty->ContainerPtrToValuePtr<FGuid>(NodePtr);
			}
		}
		if (IdPtr)
		{
			Result->SetStringField(TEXT("id"), IdPtr->ToString(EGuidFormats::DigitsWithHyphens));
		}

		return Result;
	}

	static void ExportStateTreeEditorNodeArray(
		TSharedPtr<FJsonObject> Result,
		UObject* Owner,
		const TCHAR* PropertyName,
		const TCHAR* OutputName)
	{
		TArray<TSharedPtr<FJsonValue>> Items;
		if (Owner && Owner->GetClass())
		{
			if (const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Owner->GetClass()->FindPropertyByName(PropertyName)))
			{
				if (const FStructProperty* NodeProperty = CastField<FStructProperty>(ArrayProperty->Inner))
				{
					void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(Owner);
					FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
					for (int32 Index = 0; Index < Helper.Num(); ++Index)
					{
						TSharedPtr<FJsonObject> Node = ExportStateTreeEditorNode(NodeProperty, Helper.GetRawPtr(Index));
						Node->SetNumberField(TEXT("index"), Index);
						Items.Add(MakeShared<FJsonValueObject>(Node));
					}
				}
			}
		}

		Result->SetArrayField(OutputName, Items);
		Result->SetNumberField(FString::Printf(TEXT("%s_count"), OutputName), Items.Num());
	}

	static TSharedPtr<FJsonObject> ExportStateTreeTransition(const FStructProperty* TransitionProperty, const void* TransitionPtr)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		if (!TransitionProperty || !TransitionPtr)
		{
			return Result;
		}

		Result->SetObjectField(TEXT("raw"), ExportStructValueSummary(TransitionProperty->Struct, TransitionPtr, 0, 4));
		if (const FProperty* TriggerProperty = TransitionProperty->Struct->FindPropertyByName(TEXT("Trigger")))
		{
			Result->SetStringField(TEXT("trigger"), ExportPropertyTextValue(TriggerProperty, TriggerProperty->ContainerPtrToValuePtr<void>(TransitionPtr)));
		}
		if (const FProperty* StateProperty = TransitionProperty->Struct->FindPropertyByName(TEXT("State")))
		{
			Result->SetStringField(TEXT("target"), ExportPropertyTextValue(StateProperty, StateProperty->ContainerPtrToValuePtr<void>(TransitionPtr)));
		}
		if (const FArrayProperty* ConditionsProperty = CastField<FArrayProperty>(TransitionProperty->Struct->FindPropertyByName(TEXT("Conditions"))))
		{
			const void* ConditionsValue = ConditionsProperty->ContainerPtrToValuePtr<void>(TransitionPtr);
			FScriptArrayHelper Helper(ConditionsProperty, ConditionsValue);
			Result->SetNumberField(TEXT("condition_count"), Helper.Num());
		}
		return Result;
	}

	static TSharedPtr<FJsonObject> ExportStateTreeStateObject(UObject* StateObject, int32 Depth, TSet<const UObject*>& Visited)
	{
		TSharedPtr<FJsonObject> Result = MakeObjectReferenceObject(StateObject);
		if (!StateObject || !StateObject->GetClass())
		{
			return Result;
		}

		if (Visited.Contains(StateObject))
		{
			Result->SetBoolField(TEXT("cycle"), true);
			return Result;
		}
		Visited.Add(StateObject);

		Result->SetNumberField(TEXT("depth"), Depth);
		if (const FProperty* NameProperty = StateObject->GetClass()->FindPropertyByName(TEXT("Name")))
		{
			Result->SetStringField(TEXT("state_name"), ExportPropertyTextValue(NameProperty, NameProperty->ContainerPtrToValuePtr<void>(StateObject)));
		}
		if (const FProperty* TypeProperty = StateObject->GetClass()->FindPropertyByName(TEXT("Type")))
		{
			Result->SetStringField(TEXT("state_type"), ExportPropertyTextValue(TypeProperty, TypeProperty->ContainerPtrToValuePtr<void>(StateObject)));
		}
		if (const FProperty* EnabledProperty = StateObject->GetClass()->FindPropertyByName(TEXT("bEnabled")))
		{
			Result->SetStringField(TEXT("enabled"), ExportPropertyTextValue(EnabledProperty, EnabledProperty->ContainerPtrToValuePtr<void>(StateObject)));
		}

		ExportStateTreeEditorNodeArray(Result, StateObject, TEXT("EnterConditions"), TEXT("enter_conditions"));
		ExportStateTreeEditorNodeArray(Result, StateObject, TEXT("Tasks"), TEXT("tasks"));
		ExportStateTreeEditorNodeArray(Result, StateObject, TEXT("Considerations"), TEXT("considerations"));

		TArray<TSharedPtr<FJsonValue>> Transitions;
		if (const FArrayProperty* TransitionsProperty = CastField<FArrayProperty>(StateObject->GetClass()->FindPropertyByName(TEXT("Transitions"))))
		{
			if (const FStructProperty* TransitionProperty = CastField<FStructProperty>(TransitionsProperty->Inner))
			{
				void* TransitionsValue = TransitionsProperty->ContainerPtrToValuePtr<void>(StateObject);
				FScriptArrayHelper Helper(TransitionsProperty, TransitionsValue);
				for (int32 Index = 0; Index < Helper.Num(); ++Index)
				{
					TSharedPtr<FJsonObject> Transition = ExportStateTreeTransition(TransitionProperty, Helper.GetRawPtr(Index));
					Transition->SetNumberField(TEXT("index"), Index);
					Transitions.Add(MakeShared<FJsonValueObject>(Transition));
				}
			}
		}
		Result->SetArrayField(TEXT("transitions"), Transitions);
		Result->SetNumberField(TEXT("transition_count"), Transitions.Num());

		TArray<TSharedPtr<FJsonValue>> Children;
		if (Depth < 10)
		{
			TArray<UObject*> ChildObjects;
			GetObjectArrayPropertyValuesByName(StateObject, TEXT("Children"), ChildObjects);
			for (UObject* ChildObject : ChildObjects)
			{
				Children.Add(MakeShared<FJsonValueObject>(ExportStateTreeStateObject(ChildObject, Depth + 1, Visited)));
			}
		}
		Result->SetArrayField(TEXT("children"), Children);
		Result->SetNumberField(TEXT("child_count"), Children.Num());

		return Result;
	}

	static TSharedPtr<FJsonObject> ExportStateTreeInspection(UObject* AssetObject)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("recognized"), IsStateTreeObject(AssetObject));
		Result->SetBoolField(TEXT("is_state_tree_asset"), IsStateTreeAsset(AssetObject));
		if (!IsStateTreeObject(AssetObject))
		{
			return Result;
		}

		Result->SetObjectField(TEXT("asset"), MakeObjectReferenceObject(AssetObject));

		UObject* EditorData = GetObjectPropertyValueByName(AssetObject, TEXT("EditorData"));
		if (EditorData)
		{
			Result->SetObjectField(TEXT("editor_data"), MakeObjectReferenceObject(EditorData));
			ExportStateTreeEditorNodeArray(Result, EditorData, TEXT("Evaluators"), TEXT("evaluators"));
			ExportStateTreeEditorNodeArray(Result, EditorData, TEXT("GlobalTasks"), TEXT("global_tasks"));

			TArray<TSharedPtr<FJsonValue>> States;
			TArray<UObject*> SubTrees;
			GetObjectArrayPropertyValuesByName(EditorData, TEXT("SubTrees"), SubTrees);
			TSet<const UObject*> Visited;
			for (UObject* SubTree : SubTrees)
			{
				States.Add(MakeShared<FJsonValueObject>(ExportStateTreeStateObject(SubTree, 0, Visited)));
			}
			Result->SetArrayField(TEXT("states"), States);
			Result->SetNumberField(TEXT("state_count"), States.Num());
		}
		else
		{
			Result->SetStringField(TEXT("editor_data_status"), TEXT("not_available"));
		}

		TSharedPtr<FJsonObject> Runtime = MakeShared<FJsonObject>();
		if (AssetObject && AssetObject->GetClass())
		{
			if (const FArrayProperty* StatesProperty = CastField<FArrayProperty>(AssetObject->GetClass()->FindPropertyByName(TEXT("States"))))
			{
				void* StatesValue = StatesProperty->ContainerPtrToValuePtr<void>(AssetObject);
				FScriptArrayHelper Helper(StatesProperty, StatesValue);
				Runtime->SetNumberField(TEXT("compiled_state_count"), Helper.Num());
			}
			if (const FArrayProperty* TransitionsProperty = CastField<FArrayProperty>(AssetObject->GetClass()->FindPropertyByName(TEXT("Transitions"))))
			{
				void* TransitionsValue = TransitionsProperty->ContainerPtrToValuePtr<void>(AssetObject);
				FScriptArrayHelper Helper(TransitionsProperty, TransitionsValue);
				Runtime->SetNumberField(TEXT("compiled_transition_count"), Helper.Num());
			}
			if (const FProperty* EvaluatorsNumProperty = AssetObject->GetClass()->FindPropertyByName(TEXT("EvaluatorsNum")))
			{
				Runtime->SetStringField(TEXT("compiled_evaluator_count"), ExportPropertyTextValue(EvaluatorsNumProperty, EvaluatorsNumProperty->ContainerPtrToValuePtr<void>(AssetObject)));
			}
			if (const FProperty* GlobalTasksNumProperty = AssetObject->GetClass()->FindPropertyByName(TEXT("GlobalTasksNum")))
			{
				Runtime->SetStringField(TEXT("compiled_global_task_count"), ExportPropertyTextValue(GlobalTasksNumProperty, GlobalTasksNumProperty->ContainerPtrToValuePtr<void>(AssetObject)));
			}
		}
		Result->SetObjectField(TEXT("runtime"), Runtime);

		return Result;
	}

	static UEdGraph* FindStandardFlowMacroGraph(const FString& MacroName, FString& OutError)
	{
		const FString CanonicalMacroName = GetCanonicalStandardMacroName(MacroName);
		if (!IsSupportedStandardFlowMacro(CanonicalMacroName))
		{
			OutError = FString::Printf(TEXT("Unsupported StandardMacros flow-control macro: %s"), *MacroName);
			return nullptr;
		}

		UBlueprint* MacroLibrary = LoadObject<UBlueprint>(nullptr, StandardMacrosBlueprintPath);
		if (!MacroLibrary)
		{
			OutError = FString::Printf(TEXT("Could not load StandardMacros Blueprint: %s"), StandardMacrosBlueprintPath);
			return nullptr;
		}

		for (UEdGraph* MacroGraph : MacroLibrary->MacroGraphs)
		{
			if (MacroGraph && MacroGraph->GetName().Equals(CanonicalMacroName, ESearchCase::IgnoreCase))
			{
				return MacroGraph;
			}
		}

		TArray<FString> AvailableMacros;
		for (UEdGraph* MacroGraph : MacroLibrary->MacroGraphs)
		{
			if (MacroGraph)
			{
				AvailableMacros.Add(MacroGraph->GetName());
			}
		}
		OutError = FString::Printf(TEXT("StandardMacros macro graph '%s' was not found. Available macros: %s"), *CanonicalMacroName, *FString::Join(AvailableMacros, TEXT(", ")));
		return nullptr;
	}

	static FString NormalizeSearchText(const FString& Text)
	{
		FString Normalized = Text.ToLower();
		for (const TCHAR Character : { TCHAR(' '), TCHAR('\t'), TCHAR('\r'), TCHAR('\n'), TCHAR('_'), TCHAR('-'), TCHAR('|'), TCHAR('('), TCHAR(')'), TCHAR('['), TCHAR(']') })
		{
			FString CharacterString;
			CharacterString.AppendChar(Character);
			Normalized.ReplaceInline(*CharacterString, TEXT(""));
		}
		return Normalized;
	}

	static TArray<FString> ReadStringArrayField(const TSharedPtr<FJsonObject>& Object, const FString& FieldName, const TArray<FString>& DefaultValues)
	{
		const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
		if (!Object.IsValid() || !Object->TryGetArrayField(FieldName, Values) || !Values)
		{
			return DefaultValues;
		}

		TArray<FString> Strings;
		for (const TSharedPtr<FJsonValue>& Value : *Values)
		{
			if (!Value.IsValid())
			{
				continue;
			}
			FString StringValue;
			if (Value->TryGetString(StringValue) && !StringValue.TrimStartAndEnd().IsEmpty())
			{
				Strings.Add(StringValue.TrimStartAndEnd());
			}
		}
		return Strings;
	}

	static FString TryGetJsonStringField(const TSharedPtr<FJsonObject>& Object, const FString& FieldName)
	{
		FString Value;
		if (Object.IsValid())
		{
			Object->TryGetStringField(FieldName, Value);
		}
		return Value;
	}

	static bool TryGetJsonBoolField(const TSharedPtr<FJsonObject>& Object, const FString& FieldName, bool bDefaultValue = false)
	{
		bool bValue = bDefaultValue;
		if (Object.IsValid())
		{
			Object->TryGetBoolField(FieldName, bValue);
		}
		return bValue;
	}

	static FString GetSearchAssetDomainFromClassPath(const FString& ClassPath)
	{
		if (ClassPath.Contains(TEXT("Engine.Material")) || ClassPath.Contains(TEXT("/Script/Engine.Material")))
		{
			return TEXT("material");
		}
		if (ClassPath.Contains(TEXT("GameplayCameras.")))
		{
			return TEXT("gameplay_camera");
		}
		if (ClassPath.Contains(TEXT("StateTreeModule.StateTree")) || ClassPath.Contains(TEXT("StateTreeEditorModule.")))
		{
			return TEXT("state_tree");
		}
		if (ClassPath.Contains(TEXT("Engine.Blueprint")) || ClassPath.Contains(TEXT("/Script/Engine.Blueprint")))
		{
			return TEXT("blueprint");
		}
		return TEXT("generic_uobject");
	}

	static bool IsSpecializedSearchAssetDomain(const FString& AssetDomain)
	{
		return AssetDomain == TEXT("state_tree") || AssetDomain == TEXT("gameplay_camera") || AssetDomain == TEXT("material");
	}

	static TSharedPtr<FJsonObject> MakeInspectAssetArguments(const FString& AssetPath)
	{
		TSharedPtr<FJsonObject> Arguments = MakeShared<FJsonObject>();
		Arguments->SetStringField(TEXT("AssetNameOrPath"), AssetPath);
		Arguments->SetStringField(TEXT("Type"), TEXT("Any"));
		return Arguments;
	}

	static void AddRecommendedNextTool(TSharedPtr<FJsonObject> Result, const FString& ToolName)
	{
		if (!Result.IsValid() || ToolName.IsEmpty())
		{
			return;
		}

		TArray<FString> Tools;
		const TArray<TSharedPtr<FJsonValue>>* ExistingValues = nullptr;
		if (Result->TryGetArrayField(TEXT("recommended_next_tools"), ExistingValues) && ExistingValues)
		{
			for (const TSharedPtr<FJsonValue>& Value : *ExistingValues)
			{
				FString Existing;
				if (Value.IsValid() && Value->TryGetString(Existing) && !Existing.IsEmpty())
				{
					Tools.AddUnique(Existing);
				}
			}
		}
		Tools.AddUnique(ToolName);

		TArray<TSharedPtr<FJsonValue>> ToolValues;
		for (const FString& Tool : Tools)
		{
			ToolValues.Add(MakeShared<FJsonValueString>(Tool));
		}
		Result->SetArrayField(TEXT("recommended_next_tools"), ToolValues);
	}

	static FString MakeProjectSearchInspectedText(const FString& Inspectability, const FString& MatchTarget)
	{
		if (Inspectability == TEXT("deep_blueprint"))
		{
			return FString::Printf(TEXT("Blueprint %s were indexed from Unreal graph/readback data."), MatchTarget.IsEmpty() ? TEXT("data") : *MatchTarget);
		}
		if (Inspectability == TEXT("deep_specialized_asset"))
		{
			return TEXT("Specialized asset data was indexed through the bridge's read-only asset inspector.");
		}
		if (Inspectability == TEXT("shallow_asset_metadata"))
		{
			return TEXT("Only Asset Registry identity, class, and tag metadata were indexed for this asset.");
		}
		return TEXT("Searchable Unreal metadata was indexed.");
	}

	static FString MakeProjectSearchNotInspectedText(const FString& Inspectability)
	{
		if (Inspectability == TEXT("shallow_asset_metadata"))
		{
			return TEXT("Object properties and internal asset graph data were not loaded during project search; call unreal_inspect_asset for details.");
		}
		if (Inspectability == TEXT("deep_blueprint"))
		{
			return TEXT("Non-Blueprint asset internals are not represented by this Blueprint result.");
		}
		return TEXT("Some native/private fields may be unavailable if Unreal does not expose them through reflection or graph readback.");
	}

	static FString MakeSpecializedSearchMatchedText(const FString& Query, const TSharedPtr<FJsonObject>& AssetSummary)
	{
		const FString AssetDomain = TryGetJsonStringField(AssetSummary, TEXT("asset_domain"));
		const FString AssetName = TryGetJsonStringField(AssetSummary, TEXT("asset_name"));
		const FString SpecializedReader = TryGetJsonStringField(AssetSummary, TEXT("specialized_reader"));
		if (!Query.IsEmpty())
		{
			return FString::Printf(TEXT("%s matched inside %s specialized asset data for %s"), *Query, SpecializedReader.IsEmpty() ? *AssetDomain : *SpecializedReader, AssetName.IsEmpty() ? TEXT("asset") : *AssetName);
		}
		return FString::Printf(TEXT("%s specialized asset data for %s"), SpecializedReader.IsEmpty() ? *AssetDomain : *SpecializedReader, AssetName.IsEmpty() ? TEXT("asset") : *AssetName);
	}

	static bool TextMatchesSearchQuery(const FString& Text, const FString& Query, const FString& NormalizedQuery)
	{
		if (Text.IsEmpty() || Query.IsEmpty())
		{
			return false;
		}
		return Text.Contains(Query, ESearchCase::IgnoreCase) || NormalizeSearchText(Text).Contains(NormalizedQuery);
	}

	static int32 ScoreSearchText(const FString& Text, const FString& Query, const FString& NormalizedQuery, int32 ExactScore, int32 SubstringScore, FString& OutReason)
	{
		if (Text.IsEmpty())
		{
			return 0;
		}
		if (Text.Equals(Query, ESearchCase::IgnoreCase) || NormalizeSearchText(Text).Equals(NormalizedQuery, ESearchCase::IgnoreCase))
		{
			OutReason = TEXT("exact_match");
			return ExactScore;
		}
		if (TextMatchesSearchQuery(Text, Query, NormalizedQuery))
		{
			OutReason = TEXT("substring_match");
			return SubstringScore;
		}
		return 0;
	}

	static FString MakeBlueprintSearchSummary(const TSharedPtr<FJsonObject>& Result)
	{
		if (!Result.IsValid())
		{
			return TEXT("");
		}

		const FString MatchTarget = TryGetJsonStringField(Result, TEXT("match_target"));
		const FString AssetName = TryGetJsonStringField(Result, TEXT("asset_name"));
		const FString GraphName = TryGetJsonStringField(Result, TEXT("graph_name"));
		const FString NodeTitle = TryGetJsonStringField(Result, TEXT("node_title"));
		const FString NodeKind = TryGetJsonStringField(Result, TEXT("node_kind"));
		const FString SpawnerId = TryGetJsonStringField(Result, TEXT("spawner_id"));
		if (MatchTarget == TEXT("comments"))
		{
			const TSharedPtr<FJsonObject>* Comment = nullptr;
			if (Result->TryGetObjectField(TEXT("comment"), Comment) && Comment && Comment->IsValid())
			{
				TArray<FString> ContainedNodeTitles;
				const TArray<TSharedPtr<FJsonValue>>* ContainedNodes = nullptr;
				if ((*Comment)->TryGetArrayField(TEXT("contained_nodes"), ContainedNodes) && ContainedNodes)
				{
					for (const TSharedPtr<FJsonValue>& ContainedNodeValue : *ContainedNodes)
					{
						TSharedPtr<FJsonObject> ContainedNode = ContainedNodeValue.IsValid() ? ContainedNodeValue->AsObject() : nullptr;
						FString ContainedNodeTitle = TryGetJsonStringField(ContainedNode, TEXT("node_title"));
						if (ContainedNodeTitle.IsEmpty())
						{
							ContainedNodeTitle = TryGetJsonStringField(ContainedNode, TEXT("node_kind"));
						}
						if (!ContainedNodeTitle.IsEmpty())
						{
							ContainedNodeTitles.AddUnique(ContainedNodeTitle);
						}
					}
				}
				if (ContainedNodeTitles.Num() > 0)
				{
					return FString::Printf(TEXT("Comment in %s containing %s"), GraphName.IsEmpty() ? TEXT("graph") : *GraphName, *FString::Join(ContainedNodeTitles, TEXT(", ")));
				}
				return FString::Printf(TEXT("Comment in %s"), GraphName.IsEmpty() ? TEXT("graph") : *GraphName);
			}
		}
		if (MatchTarget == TEXT("variables"))
		{
			const TSharedPtr<FJsonObject>* VariableReference = nullptr;
			if (Result->TryGetObjectField(TEXT("variable_reference"), VariableReference) && VariableReference && VariableReference->IsValid())
			{
				const FString Name = TryGetJsonStringField(*VariableReference, TEXT("name"));
				const FString PinCategory = TryGetJsonStringField(*VariableReference, TEXT("pin_category"));
				const FString PinSubCategory = TryGetJsonStringField(*VariableReference, TEXT("pin_subcategory"));
				const FString TypeText = PinSubCategory.IsEmpty() ? PinCategory : FString::Printf(TEXT("%s/%s"), *PinCategory, *PinSubCategory);
				return FString::Printf(TEXT("Blueprint variable %s: %s"), Name.IsEmpty() ? TEXT("(unnamed)") : *Name, TypeText.IsEmpty() ? TEXT("unknown type") : *TypeText);
			}
		}
		if (MatchTarget == TEXT("functions"))
		{
			const TSharedPtr<FJsonObject>* FunctionReference = nullptr;
			if (Result->TryGetObjectField(TEXT("function_reference"), FunctionReference) && FunctionReference && FunctionReference->IsValid())
			{
				FString Name = TryGetJsonStringField(*FunctionReference, TEXT("name"));
				if (Name.IsEmpty())
				{
					Name = TryGetJsonStringField(*FunctionReference, TEXT("function_name"));
				}
				const FString Graph = TryGetJsonStringField(*FunctionReference, TEXT("graph"));
				const FString InterfaceClassName = TryGetJsonStringField(*FunctionReference, TEXT("interface_class_name"));
				if (!InterfaceClassName.IsEmpty())
				{
					return FString::Printf(TEXT("Blueprint interface implementation %s.%s in %s"), *InterfaceClassName, Name.IsEmpty() ? TEXT("(unnamed)") : *Name, Graph.IsEmpty() ? TEXT("graph") : *Graph);
				}
				return FString::Printf(TEXT("Blueprint function %s in %s"), Name.IsEmpty() ? TEXT("(unnamed)") : *Name, Graph.IsEmpty() ? TEXT("graph") : *Graph);
			}
		}
		if (MatchTarget == TEXT("components"))
		{
			const TSharedPtr<FJsonObject>* Component = nullptr;
			if (Result->TryGetObjectField(TEXT("component"), Component) && Component && Component->IsValid())
			{
				const FString Name = TryGetJsonStringField(*Component, TEXT("name"));
				const FString ComponentClass = TryGetJsonStringField(*Component, TEXT("component_class"));
				return FString::Printf(TEXT("Component %s: %s"), Name.IsEmpty() ? TEXT("(unnamed)") : *Name, ComponentClass.IsEmpty() ? TEXT("unknown class") : *ComponentClass);
			}
		}
		if (MatchTarget == TEXT("assets"))
		{
			const FString AssetDomain = TryGetJsonStringField(Result, TEXT("asset_domain"));
			if (AssetDomain == TEXT("blueprint") || AssetDomain.IsEmpty())
			{
				return FString::Printf(TEXT("Blueprint asset %s"), AssetName.IsEmpty() ? TEXT("(unnamed)") : *AssetName);
			}
			if (AssetDomain == TEXT("state_tree"))
			{
				return FString::Printf(TEXT("StateTree asset %s"), AssetName.IsEmpty() ? TEXT("(unnamed)") : *AssetName);
			}
			if (AssetDomain == TEXT("gameplay_camera"))
			{
				return FString::Printf(TEXT("Gameplay Camera asset %s"), AssetName.IsEmpty() ? TEXT("(unnamed)") : *AssetName);
			}
			if (AssetDomain == TEXT("material"))
			{
				return FString::Printf(TEXT("Material asset %s"), AssetName.IsEmpty() ? TEXT("(unnamed)") : *AssetName);
			}
			return FString::Printf(TEXT("Unreal asset %s"), AssetName.IsEmpty() ? TEXT("(unnamed)") : *AssetName);
		}
		if (!NodeTitle.IsEmpty())
		{
			if (!SpawnerId.IsEmpty())
			{
				return FString::Printf(TEXT("%s node %s in %s (%s)"), NodeKind.IsEmpty() ? TEXT("Blueprint") : *NodeKind, *NodeTitle, GraphName.IsEmpty() ? TEXT("graph") : *GraphName, *SpawnerId);
			}
			return FString::Printf(TEXT("%s node %s in %s"), NodeKind.IsEmpty() ? TEXT("Blueprint") : *NodeKind, *NodeTitle, GraphName.IsEmpty() ? TEXT("graph") : *GraphName);
		}
		if (!SpawnerId.IsEmpty())
		{
			return FString::Printf(TEXT("Blueprint node in %s (%s)"), GraphName.IsEmpty() ? TEXT("graph") : *GraphName, *SpawnerId);
		}
		return FString::Printf(TEXT("%s result in %s"), MatchTarget.IsEmpty() ? TEXT("Blueprint search") : *MatchTarget, AssetName.IsEmpty() ? TEXT("asset") : *AssetName);
	}

	static void AddBlueprintSearchResult(
		TArray<TSharedPtr<FJsonValue>>& Results,
		const TSharedPtr<FJsonObject>& AssetSummary,
		const FString& MatchTarget,
		const FString& MatchedText,
		const FString& MatchReason,
		int32 Score,
		const FString& GraphName = TEXT(""),
		const TSharedPtr<FJsonObject>& NodeObject = nullptr,
		const TSharedPtr<FJsonObject>& MetadataObject = nullptr)
	{
		if (!AssetSummary.IsValid() || Score <= 0)
		{
			return;
		}

		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("asset_path"), TryGetJsonStringField(AssetSummary, TEXT("asset_path")));
		Result->SetStringField(TEXT("asset_name"), TryGetJsonStringField(AssetSummary, TEXT("asset_name")));
		Result->SetStringField(TEXT("match_target"), MatchTarget);
		Result->SetStringField(TEXT("matched_text"), MatchedText);
		Result->SetStringField(TEXT("match_reason"), MatchReason);
		Result->SetNumberField(TEXT("score"), Score);
		const FString AssetDomain = TryGetJsonStringField(AssetSummary, TEXT("asset_domain"));
		const FString Inspectability = TryGetJsonStringField(AssetSummary, TEXT("inspectability"));
		const FString SpecializedReader = TryGetJsonStringField(AssetSummary, TEXT("specialized_reader"));
		Result->SetStringField(TEXT("asset_domain"), AssetDomain.IsEmpty() ? TEXT("blueprint") : AssetDomain);
		Result->SetStringField(TEXT("inspectability"), Inspectability.IsEmpty() ? TEXT("deep_blueprint") : Inspectability);
		if (!SpecializedReader.IsEmpty())
		{
			Result->SetStringField(TEXT("specialized_reader"), SpecializedReader);
		}
		Result->SetStringField(TEXT("why_this_matched"), FString::Printf(TEXT("Matched %s via %s."), MatchedText.IsEmpty() ? TEXT("searchable Unreal metadata") : *MatchedText, MatchReason.IsEmpty() ? TEXT("unknown_reason") : *MatchReason));
		Result->SetStringField(TEXT("what_was_inspected"), MakeProjectSearchInspectedText(Result->GetStringField(TEXT("inspectability")), MatchTarget));
		Result->SetStringField(TEXT("what_was_not_inspected"), MakeProjectSearchNotInspectedText(Result->GetStringField(TEXT("inspectability"))));
		if (!GraphName.IsEmpty())
		{
			Result->SetStringField(TEXT("graph_name"), GraphName);
			TSharedPtr<FJsonObject> ReadGraphArguments = MakeShared<FJsonObject>();
			ReadGraphArguments->SetStringField(TEXT("BlueprintNameOrPath"), TryGetJsonStringField(AssetSummary, TEXT("asset_path")));
			ReadGraphArguments->SetStringField(TEXT("GraphName"), GraphName);
			Result->SetObjectField(TEXT("read_graph_arguments"), ReadGraphArguments);
			Result->SetStringField(TEXT("next_step"), TEXT("Call blueprint_read_graph with read_graph_arguments to inspect the matching graph."));
			AddRecommendedNextTool(Result, TEXT("blueprint_read_graph"));
		}

		const bool bIsBlueprintDomain = Result->GetStringField(TEXT("asset_domain")) == TEXT("blueprint");
		if (!bIsBlueprintDomain)
		{
			const FString AssetPath = TryGetJsonStringField(AssetSummary, TEXT("asset_path"));
			Result->SetObjectField(TEXT("inspect_asset_arguments"), MakeInspectAssetArguments(AssetPath));
			Result->SetStringField(TEXT("next_step"), TEXT("Call unreal_inspect_asset with inspect_asset_arguments to inspect this asset."));
			AddRecommendedNextTool(Result, TEXT("unreal_inspect_asset"));
		}

		if (NodeObject.IsValid())
		{
			const FString NodeGuid = TryGetJsonStringField(NodeObject, TEXT("node_guid"));
			const FString NodeTitle = TryGetJsonStringField(NodeObject, TEXT("title"));
			const FString NodeKind = TryGetJsonStringField(NodeObject, TEXT("node_kind"));
			const FString SpawnerId = TryGetJsonStringField(NodeObject, TEXT("spawner_id"));
			if (!NodeGuid.IsEmpty())
			{
				Result->SetStringField(TEXT("node_guid"), NodeGuid);
			}
			if (!NodeTitle.IsEmpty())
			{
				Result->SetStringField(TEXT("node_title"), NodeTitle);
			}
			if (!NodeKind.IsEmpty())
			{
				Result->SetStringField(TEXT("node_kind"), NodeKind);
			}
			if (!SpawnerId.IsEmpty())
			{
				Result->SetStringField(TEXT("spawner_id"), SpawnerId);
			}
			for (const FString& MetadataField : TArray<FString>{ TEXT("function_reference"), TEXT("variable_reference"), TEXT("cast_reference"), TEXT("macro_reference"), TEXT("input_event"), TEXT("custom_event"), TEXT("event_reference"), TEXT("interface_implementation"), TEXT("comment") })
			{
				const TSharedPtr<FJsonObject>* Metadata = nullptr;
				if (NodeObject->TryGetObjectField(MetadataField, Metadata) && Metadata && Metadata->IsValid())
				{
					Result->SetObjectField(MetadataField, *Metadata);
				}
			}
		}

		if (MetadataObject.IsValid())
		{
			if (MatchTarget == TEXT("graphs"))
			{
				Result->SetObjectField(TEXT("graph"), MetadataObject);
				const TSharedPtr<FJsonObject>* ReadGraphArguments = nullptr;
				if (MetadataObject->TryGetObjectField(TEXT("read_graph_arguments"), ReadGraphArguments) && ReadGraphArguments && ReadGraphArguments->IsValid())
				{
					Result->SetObjectField(TEXT("read_graph_arguments"), *ReadGraphArguments);
				}
			}
			else if (MatchTarget == TEXT("comments"))
			{
				Result->SetStringField(TEXT("node_kind"), TEXT("comment"));
				Result->SetObjectField(TEXT("comment"), MetadataObject);
				const FString NodeGuid = TryGetJsonStringField(MetadataObject, TEXT("node_guid"));
				if (!NodeGuid.IsEmpty())
				{
					Result->SetStringField(TEXT("node_guid"), NodeGuid);
				}
			}
			else if (MatchTarget == TEXT("variables"))
			{
				Result->SetObjectField(TEXT("variable_reference"), MetadataObject);
			}
			else if (MatchTarget == TEXT("functions"))
			{
				Result->SetObjectField(TEXT("function_reference"), MetadataObject);
				if (MetadataObject->HasField(TEXT("interface_class_path")))
				{
					Result->SetObjectField(TEXT("interface_implementation"), MetadataObject);
					Result->SetStringField(TEXT("node_kind"), TEXT("interface_event"));
				}
				const FString Graph = TryGetJsonStringField(MetadataObject, TEXT("graph"));
				const FString SpawnerId = TryGetJsonStringField(MetadataObject, TEXT("spawner_id"));
				if (!Graph.IsEmpty() && !Result->HasField(TEXT("graph_name")))
				{
					Result->SetStringField(TEXT("graph_name"), Graph);
				}
				if (!SpawnerId.IsEmpty())
				{
					Result->SetStringField(TEXT("spawner_id"), SpawnerId);
				}
			}
			else if (MatchTarget == TEXT("interfaces"))
			{
				Result->SetObjectField(TEXT("interface_implementation"), MetadataObject);
			}
			else if (MatchTarget == TEXT("components"))
			{
				Result->SetStringField(TEXT("component_class"), TryGetJsonStringField(MetadataObject, TEXT("component_class")));
				Result->SetObjectField(TEXT("component"), MetadataObject);
			}
			else if (MatchTarget == TEXT("metadata"))
			{
				Result->SetObjectField(TEXT("metadata"), MetadataObject);
			}
		}

		Result->SetBoolField(TEXT("has_followup_tool"), Result->HasTypedField<EJson::Array>(TEXT("recommended_next_tools")));
		Result->SetStringField(TEXT("summary"), MakeBlueprintSearchSummary(Result));
		Results.Add(MakeShared<FJsonValueObject>(Result));
	}

	static UObject* LoadPCGGraphObject(const FString& GraphPath)
	{
		if (GraphPath.TrimStartAndEnd().IsEmpty())
		{
			return nullptr;
		}

		const FString ObjectPath = MakeObjectPathIfPackagePath(GraphPath);
		return LoadObject<UObject>(nullptr, *ObjectPath);
	}

	static TArray<TSharedPtr<FJsonValue>> MakePCGPinArray(UObject* Node, const TCHAR* PropertyName);

	static TArray<TSharedPtr<FJsonValue>> MakePCGNodeArray(UObject* Graph, const TCHAR* PropertyName)
	{
		TArray<TSharedPtr<FJsonValue>> Result;
		if (!Graph || !Graph->GetClass())
		{
			return Result;
		}

		FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Graph->GetClass()->FindPropertyByName(PropertyName));
		FObjectPropertyBase* InnerObjectProperty = ArrayProperty ? CastField<FObjectPropertyBase>(ArrayProperty->Inner) : nullptr;
		if (!ArrayProperty || !InnerObjectProperty)
		{
			return Result;
		}

		void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(Graph);
		FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
		for (int32 Index = 0; Index < Helper.Num(); ++Index)
		{
			UObject* Node = InnerObjectProperty->GetObjectPropertyValue(Helper.GetRawPtr(Index));
			if (!Node)
			{
				continue;
			}

			TSharedPtr<FJsonObject> NodeObject = MakeObjectReferenceObject(Node);
			if (FNameProperty* TitleProperty = CastField<FNameProperty>(Node->GetClass()->FindPropertyByName(TEXT("NodeTitle"))))
			{
				NodeObject->SetStringField(TEXT("node_title"), TitleProperty->GetPropertyValue_InContainer(Node).ToString());
			}
			if (FIntProperty* PositionXProperty = CastField<FIntProperty>(Node->GetClass()->FindPropertyByName(TEXT("PositionX"))))
			{
				NodeObject->SetNumberField(TEXT("position_x"), PositionXProperty->GetPropertyValue_InContainer(Node));
			}
			if (FIntProperty* PositionYProperty = CastField<FIntProperty>(Node->GetClass()->FindPropertyByName(TEXT("PositionY"))))
			{
				NodeObject->SetNumberField(TEXT("position_y"), PositionYProperty->GetPropertyValue_InContainer(Node));
			}
			if (FStrProperty* CommentProperty = CastField<FStrProperty>(Node->GetClass()->FindPropertyByName(TEXT("NodeComment"))))
			{
				const FString Comment = CommentProperty->GetPropertyValue_InContainer(Node);
				if (!Comment.IsEmpty())
				{
					NodeObject->SetStringField(TEXT("comment"), Comment);
				}
			}
			if (UObject* SettingsInterface = GetObjectPropertyValueByName(Node, TEXT("SettingsInterface")))
			{
				NodeObject->SetObjectField(TEXT("settings_interface"), MakeObjectReferenceObject(SettingsInterface));
				TArray<TSharedPtr<FJsonValue>> SettingsReferences;
				TArray<TSharedPtr<FJsonValue>> SettingsFailures;
				NodeObject->SetObjectField(TEXT("settings_properties"), ExportInspectableProperties(SettingsInterface, 24, SettingsReferences, SettingsFailures));
			}
			NodeObject->SetArrayField(TEXT("input_pins"), MakePCGPinArray(Node, TEXT("InputPins")));
			NodeObject->SetArrayField(TEXT("output_pins"), MakePCGPinArray(Node, TEXT("OutputPins")));
			Result.Add(MakeShared<FJsonValueObject>(NodeObject));
		}
		return Result;
	}

	static FString GetPCGPinLabel(UObject* Pin)
	{
		if (!Pin || !Pin->GetClass())
		{
			return TEXT("");
		}

		FStructProperty* PropertiesProperty = CastField<FStructProperty>(Pin->GetClass()->FindPropertyByName(TEXT("Properties")));
		if (!PropertiesProperty || !PropertiesProperty->Struct)
		{
			return TEXT("");
		}

		FNameProperty* LabelProperty = CastField<FNameProperty>(PropertiesProperty->Struct->FindPropertyByName(TEXT("Label")));
		if (!LabelProperty)
		{
			return TEXT("");
		}

		void* PropertiesValue = PropertiesProperty->ContainerPtrToValuePtr<void>(Pin);
		return LabelProperty->GetPropertyValue_InContainer(PropertiesValue).ToString();
	}

	static bool SetPCGPinLabel(UObject* Pin, const FName& Label)
	{
		if (!Pin || !Pin->GetClass())
		{
			return false;
		}

		FStructProperty* PropertiesProperty = CastField<FStructProperty>(Pin->GetClass()->FindPropertyByName(TEXT("Properties")));
		if (!PropertiesProperty || !PropertiesProperty->Struct)
		{
			return false;
		}

		FNameProperty* LabelProperty = CastField<FNameProperty>(PropertiesProperty->Struct->FindPropertyByName(TEXT("Label")));
		if (!LabelProperty)
		{
			return false;
		}

		void* PropertiesValue = PropertiesProperty->ContainerPtrToValuePtr<void>(Pin);
		LabelProperty->SetPropertyValue_InContainer(PropertiesValue, Label);
		return true;
	}

	static UObject* FindPCGNodeByName(UObject* Graph, const FString& NodeName)
	{
		if (!Graph || !Graph->GetClass() || NodeName.IsEmpty())
		{
			return nullptr;
		}

		FArrayProperty* NodesProperty = CastField<FArrayProperty>(Graph->GetClass()->FindPropertyByName(TEXT("Nodes")));
		FObjectPropertyBase* NodeObjectProperty = NodesProperty ? CastField<FObjectPropertyBase>(NodesProperty->Inner) : nullptr;
		if (!NodesProperty || !NodeObjectProperty)
		{
			return nullptr;
		}

		void* NodesValue = NodesProperty->ContainerPtrToValuePtr<void>(Graph);
		FScriptArrayHelper NodesHelper(NodesProperty, NodesValue);
		for (int32 Index = 0; Index < NodesHelper.Num(); ++Index)
		{
			UObject* Node = NodeObjectProperty->GetObjectPropertyValue(NodesHelper.GetRawPtr(Index));
			if (Node && Node->GetName().Equals(NodeName, ESearchCase::IgnoreCase))
			{
				return Node;
			}
		}
		return nullptr;
	}

	static bool ConfigurePCGCreatePointsGridNode(UObject* GridNode, const FVector& GridExtents, const FVector& CellSize, FString& OutError)
	{
		if (!GridNode)
		{
			OutError = TEXT("PCG create-points-grid node was invalid.");
			return false;
		}

		UObject* Settings = GetObjectPropertyValueByName(GridNode, TEXT("SettingsInterface"));
		if (!Settings)
		{
			OutError = TEXT("PCG create-points-grid node had no settings interface.");
			return false;
		}

		const bool bSetExtents = SetVectorPropertyValueByName(Settings, TEXT("GridExtents"), GridExtents);
		const bool bSetCellSize = SetVectorPropertyValueByName(Settings, TEXT("CellSize"), CellSize);
		if (!bSetExtents || !bSetCellSize)
		{
			OutError = TEXT("Could not update PCG create-points-grid GridExtents or CellSize.");
			return false;
		}

		GridNode->Modify();
		Settings->Modify();
		GridNode->MarkPackageDirty();
		Settings->MarkPackageDirty();
		return true;
	}

	static UObject* CreatePCGPin(UObject* Node, UClass* PCGPinClass, const FString& Name, const FName& Label, const TCHAR* NodePinArrayName)
	{
		if (!Node || !PCGPinClass || Name.IsEmpty())
		{
			return nullptr;
		}

		UObject* Pin = NewObject<UObject>(Node, PCGPinClass, FName(*Name), RF_Transactional);
		if (!Pin)
		{
			return nullptr;
		}

		SetObjectPropertyValueByName(Pin, TEXT("Node"), Node);
		SetPCGPinLabel(Pin, Label);
		AddObjectToArrayPropertyByName(Node, NodePinArrayName, Pin);
		return Pin;
	}

	static UObject* CreatePCGRerouteNode(UObject* Graph, FString& OutError)
	{
		static const FString RerouteNodeName = TEXT("DruidsPassThroughReroute");
		if (UObject* ExistingNode = FindPCGNodeByName(Graph, RerouteNodeName))
		{
			return ExistingNode;
		}

		UClass* PCGNodeClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGNode"));
		UClass* PCGPinClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGPin"));
		UClass* RerouteSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGRerouteSettings"));
		if (!Graph || !PCGNodeClass || !PCGPinClass || !RerouteSettingsClass)
		{
			OutError = TEXT("Could not load PCGNode, PCGPin, or PCGRerouteSettings class.");
			return nullptr;
		}

		UObject* Node = NewObject<UObject>(Graph, PCGNodeClass, FName(*RerouteNodeName), RF_Transactional);
		UObject* Settings = NewObject<UObject>(Node, RerouteSettingsClass, TEXT("DruidsRerouteSettings"), RF_Transactional);
		if (!Node || !Settings)
		{
			OutError = TEXT("Could not create PCG reroute node or settings.");
			return nullptr;
		}

		SetObjectPropertyValueByName(Node, TEXT("SettingsInterface"), Settings);
		SetNamePropertyValueByName(Node, TEXT("NodeTitle"), FName(TEXT("DruidsPassThroughReroute")));
		SetIntPropertyValueByName(Node, TEXT("PositionX"), 100);
		SetIntPropertyValueByName(Node, TEXT("PositionY"), 0);
		CreatePCGPin(Node, PCGPinClass, TEXT("DruidsRerouteInputPin"), FName(TEXT("In")), TEXT("InputPins"));
		CreatePCGPin(Node, PCGPinClass, TEXT("DruidsRerouteOutputPin"), FName(TEXT("Out")), TEXT("OutputPins"));
		if (!AddObjectToArrayPropertyByName(Graph, TEXT("Nodes"), Node))
		{
			OutError = TEXT("Could not add reroute node to PCG graph Nodes array.");
			return nullptr;
		}

		return Node;
	}

	static UObject* CreatePCGCreatePointsGridNode(UObject* Graph, const FVector& GridExtents, const FVector& CellSize, FString& OutError)
	{
		static const FString GridNodeName = TEXT("DruidsCreatePointsGrid");
		if (UObject* ExistingNode = FindPCGNodeByName(Graph, GridNodeName))
		{
			ConfigurePCGCreatePointsGridNode(ExistingNode, GridExtents, CellSize, OutError);
			return ExistingNode;
		}

		UClass* PCGNodeClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGNode"));
		UClass* PCGPinClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGPin"));
		UClass* GridSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGCreatePointsGridSettings"));
		if (!Graph || !PCGNodeClass || !PCGPinClass || !GridSettingsClass)
		{
			OutError = TEXT("Could not load PCGNode, PCGPin, or PCGCreatePointsGridSettings class.");
			return nullptr;
		}

		UObject* Node = NewObject<UObject>(Graph, PCGNodeClass, FName(*GridNodeName), RF_Transactional);
		UObject* Settings = NewObject<UObject>(Node, GridSettingsClass, TEXT("DruidsCreatePointsGridSettings"), RF_Transactional);
		if (!Node || !Settings)
		{
			OutError = TEXT("Could not create PCG create-points-grid node or settings.");
			return nullptr;
		}

		SetVectorPropertyValueByName(Settings, TEXT("GridExtents"), GridExtents);
		SetVectorPropertyValueByName(Settings, TEXT("CellSize"), CellSize);
		SetObjectPropertyValueByName(Node, TEXT("SettingsInterface"), Settings);
		SetNamePropertyValueByName(Node, TEXT("NodeTitle"), FName(TEXT("DruidsCreatePointsGrid")));
		SetIntPropertyValueByName(Node, TEXT("PositionX"), 100);
		SetIntPropertyValueByName(Node, TEXT("PositionY"), 160);
		CreatePCGPin(Node, PCGPinClass, TEXT("DruidsGridOutputPin"), FName(TEXT("Out")), TEXT("OutputPins"));
		if (!AddObjectToArrayPropertyByName(Graph, TEXT("Nodes"), Node))
		{
			OutError = TEXT("Could not add create-points-grid node to PCG graph Nodes array.");
			return nullptr;
		}

		return Node;
	}

	static FVector MakePCGGridExtentsFromRequest(const TSharedPtr<FJsonObject>& Request)
	{
		const double AreaX = FMath::Max(1.0, Request.IsValid() ? Request->GetNumberField(TEXT("area_x")) : 1600.0);
		const double AreaY = FMath::Max(1.0, Request.IsValid() ? Request->GetNumberField(TEXT("area_y")) : 1600.0);
		return FVector(AreaX * 0.5, AreaY * 0.5, 50.0);
	}

	static FVector MakePCGCellSizeFromRequest(const TSharedPtr<FJsonObject>& Request)
	{
		const double TreeSpacing = FMath::Max(1.0, Request.IsValid() ? Request->GetNumberField(TEXT("tree_spacing")) : 200.0);
		return FVector(TreeSpacing, TreeSpacing, 100.0);
	}

	static int32 EstimatePCGGridPointCount(const FVector& GridExtents, const FVector& CellSize);

	static void AddPCGDensityFields(const TSharedPtr<FJsonObject>& Object, const TSharedPtr<FJsonObject>& Request, const FVector& GridExtents, const FVector& CellSize)
	{
		if (!Object.IsValid() || !Request.IsValid())
		{
			return;
		}

		const int32 ExpectedPointCount = EstimatePCGGridPointCount(GridExtents, CellSize);
		const int32 TargetCount = static_cast<int32>(Request->GetNumberField(TEXT("target_count")));
		Object->SetStringField(TEXT("spacing_mode"), Request->GetStringField(TEXT("spacing_mode")));
		Object->SetStringField(TEXT("tree_spacing_source"), Request->GetStringField(TEXT("tree_spacing_source")));
		Object->SetNumberField(TEXT("tree_spacing"), Request->GetNumberField(TEXT("tree_spacing")));
		Object->SetNumberField(TEXT("target_count"), TargetCount);
		Object->SetNumberField(TEXT("expected_point_count"), ExpectedPointCount);
		Object->SetNumberField(TEXT("count_delta"), ExpectedPointCount - TargetCount);
		Object->SetNumberField(TEXT("absolute_count_delta"), FMath::Abs(ExpectedPointCount - TargetCount));
	}

	static UObject* CreatePCGSpawnActorNode(UObject* Graph, UClass* TemplateActorClass, const FString& ForestName, FString& OutError)
	{
		static const FString SpawnNodeName = TEXT("DruidsSpawnTreeActor");
		if (UObject* ExistingNode = FindPCGNodeByName(Graph, SpawnNodeName))
		{
			return ExistingNode;
		}

		UClass* PCGNodeClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGSpawnActorNode"));
		if (!PCGNodeClass)
		{
			PCGNodeClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGNode"));
		}
		UClass* PCGPinClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGPin"));
		UClass* SpawnActorSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGSpawnActorSettings"));
		if (!Graph || !PCGNodeClass || !PCGPinClass || !SpawnActorSettingsClass || !TemplateActorClass)
		{
			OutError = TEXT("Could not load PCGSpawnActorNode, PCGPin, PCGSpawnActorSettings, or template actor class.");
			return nullptr;
		}

		UObject* Node = NewObject<UObject>(Graph, PCGNodeClass, FName(*SpawnNodeName), RF_Transactional);
		UObject* Settings = NewObject<UObject>(Node, SpawnActorSettingsClass, TEXT("DruidsSpawnTreeActorSettings"), RF_Transactional);
		if (!Node || !Settings)
		{
			OutError = TEXT("Could not create PCG spawn-actor node or settings.");
			return nullptr;
		}

		FString ImportError;
		if (!ImportPropertyValueByName(Settings, TEXT("Option"), TEXT("NoMerging"), ImportError))
		{
			ImportPropertyValueByName(Settings, TEXT("Option"), TEXT("EPCGSpawnActorOption::NoMerging"), ImportError);
		}
		ImportPropertyValueByName(Settings, TEXT("GenerationTrigger"), TEXT("DoNotGenerate"), ImportError);
		SetBoolPropertyValueByName(Settings, TEXT("bForceDisableActorParsing"), true);
		SetBoolPropertyValueByName(Settings, TEXT("bInheritActorTags"), false);
		SetBoolPropertyValueByName(Settings, TEXT("bWarnOnIdenticalSpawn"), false);
		SetClassPropertyValueByName(Settings, TEXT("TemplateActorClass"), TemplateActorClass);
		AddNameToArrayPropertyByName(Settings, TEXT("TagsToAddOnActors"), TEXT("DruidsMCPBridge"));
		AddNameToArrayPropertyByName(Settings, TEXT("TagsToAddOnActors"), TEXT("DruidsRepro"));
		AddNameToArrayPropertyByName(Settings, TEXT("TagsToAddOnActors"), TEXT("DruidsVisualPCG"));
		AddNameToArrayPropertyByName(Settings, TEXT("TagsToAddOnActors"), TEXT("DruidsNativePCGSpawnActor"));
		AddNameToArrayPropertyByName(Settings, TEXT("TagsToAddOnActors"), FName(*FString::Printf(TEXT("DruidsPCGOutput_%s"), *ForestName)));
		AddNameToArrayPropertyByName(Settings, TEXT("TagsToAddOnActors"), FName(*FString::Printf(TEXT("DruidsPCGForestOutput_%s"), *ForestName)));

		SetObjectPropertyValueByName(Node, TEXT("SettingsInterface"), Settings);
		SetNamePropertyValueByName(Node, TEXT("NodeTitle"), FName(TEXT("DruidsSpawnTreeActor")));
		SetIntPropertyValueByName(Node, TEXT("PositionX"), 420);
		SetIntPropertyValueByName(Node, TEXT("PositionY"), 160);
		CreatePCGPin(Node, PCGPinClass, TEXT("DruidsSpawnActorInputPin"), FName(TEXT("In")), TEXT("InputPins"));
		CreatePCGPin(Node, PCGPinClass, TEXT("DruidsSpawnActorOutputPin"), FName(TEXT("Out")), TEXT("OutputPins"));
		if (!AddObjectToArrayPropertyByName(Graph, TEXT("Nodes"), Node))
		{
			OutError = TEXT("Could not add spawn-actor node to PCG graph Nodes array.");
			return nullptr;
		}

		return Node;
	}

	static UObject* CreatePCGTransformPointsNode(UObject* Graph, const TSharedPtr<FJsonObject>& Request, FString& OutError)
	{
		static const FString TransformNodeName = TEXT("DruidsTransformPoints");
		if (UObject* ExistingNode = FindPCGNodeByName(Graph, TransformNodeName))
		{
			return ExistingNode;
		}

		UClass* PCGNodeClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGNode"));
		UClass* PCGPinClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGPin"));
		UClass* TransformSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGTransformPointsSettings"));
		if (!Graph || !PCGNodeClass || !PCGPinClass || !TransformSettingsClass || !Request.IsValid())
		{
			OutError = TEXT("Could not load PCGNode, PCGPin, PCGTransformPointsSettings, or variation request.");
			return nullptr;
		}

		UObject* Node = NewObject<UObject>(Graph, PCGNodeClass, FName(*TransformNodeName), RF_Transactional);
		UObject* Settings = NewObject<UObject>(Node, TransformSettingsClass, TEXT("DruidsTransformPointsSettings"), RF_Transactional);
		if (!Node || !Settings)
		{
			OutError = TEXT("Could not create PCG transform-points node or settings.");
			return nullptr;
		}

		const double PositionJitter = FMath::Max(0.0, Request->GetNumberField(TEXT("position_jitter")));
		const double OffsetX = Request->GetNumberField(TEXT("offset_x"));
		const double OffsetY = Request->GetNumberField(TEXT("offset_y"));
		const double OffsetZ = Request->GetNumberField(TEXT("offset_z"));
		const double MinScale = FMath::Max(0.01, Request->GetNumberField(TEXT("min_scale")));
		const double MaxScale = FMath::Max(MinScale, Request->GetNumberField(TEXT("max_scale")));
		const double YawMin = Request->GetNumberField(TEXT("yaw_min"));
		const double YawMax = Request->GetNumberField(TEXT("yaw_max"));

		SetVectorPropertyValueByName(Settings, TEXT("OffsetMin"), FVector(OffsetX - PositionJitter, OffsetY - PositionJitter, OffsetZ));
		SetVectorPropertyValueByName(Settings, TEXT("OffsetMax"), FVector(OffsetX + PositionJitter, OffsetY + PositionJitter, OffsetZ));
		SetBoolPropertyValueByName(Settings, TEXT("bAbsoluteOffset"), true);
		SetRotatorPropertyValueByName(Settings, TEXT("RotationMin"), FRotator(0.0, YawMin, 0.0));
		SetRotatorPropertyValueByName(Settings, TEXT("RotationMax"), FRotator(0.0, YawMax, 0.0));
		SetBoolPropertyValueByName(Settings, TEXT("bAbsoluteRotation"), false);
		SetVectorPropertyValueByName(Settings, TEXT("ScaleMin"), FVector(MinScale));
		SetVectorPropertyValueByName(Settings, TEXT("ScaleMax"), FVector(MaxScale));
		SetBoolPropertyValueByName(Settings, TEXT("bAbsoluteScale"), false);
		SetBoolPropertyValueByName(Settings, TEXT("bUniformScale"), true);
		SetBoolPropertyValueByName(Settings, TEXT("bRecomputeSeed"), true);

		SetObjectPropertyValueByName(Node, TEXT("SettingsInterface"), Settings);
		SetNamePropertyValueByName(Node, TEXT("NodeTitle"), FName(TEXT("DruidsTransformPoints")));
		SetIntPropertyValueByName(Node, TEXT("PositionX"), 300);
		SetIntPropertyValueByName(Node, TEXT("PositionY"), 160);
		CreatePCGPin(Node, PCGPinClass, TEXT("DruidsTransformPointsInputPin"), FName(TEXT("In")), TEXT("InputPins"));
		CreatePCGPin(Node, PCGPinClass, TEXT("DruidsTransformPointsOutputPin"), FName(TEXT("Out")), TEXT("OutputPins"));
		if (!AddObjectToArrayPropertyByName(Graph, TEXT("Nodes"), Node))
		{
			OutError = TEXT("Could not add transform-points node to PCG graph Nodes array.");
			return nullptr;
		}

		return Node;
	}

	static int32 EstimatePCGGridPointCount(const FVector& GridExtents, const FVector& CellSize)
	{
		if (CellSize.X <= 0.0 || CellSize.Y <= 0.0 || CellSize.Z <= 0.0)
		{
			return 0;
		}

		const int64 CountX = FMath::Max<int64>(1, FMath::TruncToInt64((2.0 * GridExtents.X) / CellSize.X));
		const int64 CountY = FMath::Max<int64>(1, FMath::TruncToInt64((2.0 * GridExtents.Y) / CellSize.Y));
		const int64 CountZ = FMath::Max<int64>(1, FMath::TruncToInt64((2.0 * GridExtents.Z) / CellSize.Z));
		const int64 Total = CountX * CountY * CountZ;
		return Total > MAX_int32 ? MAX_int32 : static_cast<int32>(Total);
	}

	static TArray<TSharedPtr<FJsonValue>> MakePCGPinArray(UObject* Node, const TCHAR* PropertyName)
	{
		TArray<TSharedPtr<FJsonValue>> Result;
		if (!Node || !Node->GetClass())
		{
			return Result;
		}

		FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Node->GetClass()->FindPropertyByName(PropertyName));
		FObjectPropertyBase* InnerObjectProperty = ArrayProperty ? CastField<FObjectPropertyBase>(ArrayProperty->Inner) : nullptr;
		if (!ArrayProperty || !InnerObjectProperty)
		{
			return Result;
		}

		void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(Node);
		FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
		for (int32 Index = 0; Index < Helper.Num(); ++Index)
		{
			UObject* Pin = InnerObjectProperty->GetObjectPropertyValue(Helper.GetRawPtr(Index));
			if (!Pin)
			{
				continue;
			}

			TSharedPtr<FJsonObject> PinObject = MakeObjectReferenceObject(Pin);
			PinObject->SetStringField(TEXT("label"), GetPCGPinLabel(Pin));
			int32 EdgeCount = 0;
			if (FArrayProperty* EdgesProperty = CastField<FArrayProperty>(Pin->GetClass()->FindPropertyByName(TEXT("Edges"))))
			{
				void* EdgesValue = EdgesProperty->ContainerPtrToValuePtr<void>(Pin);
				FScriptArrayHelper EdgesHelper(EdgesProperty, EdgesValue);
				EdgeCount = EdgesHelper.Num();
			}
			PinObject->SetNumberField(TEXT("edge_count"), EdgeCount);
			PinObject->SetBoolField(TEXT("connected"), EdgeCount > 0);
			Result.Add(MakeShared<FJsonValueObject>(PinObject));
		}
		return Result;
	}

	static int32 AppendPCGConnectionsForPins(UObject* Node, const TCHAR* PropertyName, TSet<FString>& SeenEdges, TArray<TSharedPtr<FJsonValue>>& OutConnections)
	{
		if (!Node || !Node->GetClass())
		{
			return 0;
		}

		FArrayProperty* PinsProperty = CastField<FArrayProperty>(Node->GetClass()->FindPropertyByName(PropertyName));
		FObjectPropertyBase* PinObjectProperty = PinsProperty ? CastField<FObjectPropertyBase>(PinsProperty->Inner) : nullptr;
		if (!PinsProperty || !PinObjectProperty)
		{
			return 0;
		}

		int32 AddedCount = 0;
		void* PinsValue = PinsProperty->ContainerPtrToValuePtr<void>(Node);
		FScriptArrayHelper PinsHelper(PinsProperty, PinsValue);
		for (int32 PinIndex = 0; PinIndex < PinsHelper.Num(); ++PinIndex)
		{
			UObject* Pin = PinObjectProperty->GetObjectPropertyValue(PinsHelper.GetRawPtr(PinIndex));
			if (!Pin || !Pin->GetClass())
			{
				continue;
			}

			FArrayProperty* EdgesProperty = CastField<FArrayProperty>(Pin->GetClass()->FindPropertyByName(TEXT("Edges")));
			FObjectPropertyBase* EdgeObjectProperty = EdgesProperty ? CastField<FObjectPropertyBase>(EdgesProperty->Inner) : nullptr;
			if (!EdgesProperty || !EdgeObjectProperty)
			{
				continue;
			}

			void* EdgesValue = EdgesProperty->ContainerPtrToValuePtr<void>(Pin);
			FScriptArrayHelper EdgesHelper(EdgesProperty, EdgesValue);
			for (int32 EdgeIndex = 0; EdgeIndex < EdgesHelper.Num(); ++EdgeIndex)
			{
				UObject* Edge = EdgeObjectProperty->GetObjectPropertyValue(EdgesHelper.GetRawPtr(EdgeIndex));
				if (!Edge)
				{
					continue;
				}

				const FString EdgePath = Edge->GetPathName();
				if (SeenEdges.Contains(EdgePath))
				{
					continue;
				}
				SeenEdges.Add(EdgePath);

				UObject* UpstreamPin = GetObjectPropertyValueByName(Edge, TEXT("InputPin"));
				UObject* DownstreamPin = GetObjectPropertyValueByName(Edge, TEXT("OutputPin"));
				UObject* UpstreamNode = GetObjectPropertyValueByName(UpstreamPin, TEXT("Node"));
				UObject* DownstreamNode = GetObjectPropertyValueByName(DownstreamPin, TEXT("Node"));

				TSharedPtr<FJsonObject> Connection = MakeObjectReferenceObject(Edge);
				Connection->SetObjectField(TEXT("from_node"), MakeObjectReferenceObject(UpstreamNode));
				Connection->SetStringField(TEXT("from_pin"), GetPCGPinLabel(UpstreamPin));
				Connection->SetObjectField(TEXT("to_node"), MakeObjectReferenceObject(DownstreamNode));
				Connection->SetStringField(TEXT("to_pin"), GetPCGPinLabel(DownstreamPin));
				OutConnections.Add(MakeShared<FJsonValueObject>(Connection));
				++AddedCount;
			}
		}

		return AddedCount;
	}

	static TSharedPtr<FJsonObject> MakePCGGraphSummary(UObject* Graph)
	{
		TSharedPtr<FJsonObject> Summary = MakeObjectReferenceObject(Graph);
		if (!Graph)
		{
			Summary->SetBoolField(TEXT("recognized"), false);
			return Summary;
		}

		Summary->SetBoolField(TEXT("recognized"), Graph->GetClass() && Graph->GetClass()->GetPathName().Contains(TEXT("/Script/PCG.PCGGraph")));
		Summary->SetStringField(TEXT("asset_domain"), TEXT("pcg"));
		Summary->SetStringField(TEXT("inspectability"), TEXT("pcg_graph_asset_summary"));
		Summary->SetBoolField(TEXT("graph_authoring_supported"), false);
		Summary->SetBoolField(TEXT("generation_supported"), false);
		Summary->SetStringField(TEXT("next_step"), TEXT("Attach this minimal graph to a PCG Volume, then implement deterministic graph nodes in the next slice."));

		UObject* InputNode = GetObjectPropertyValueByName(Graph, TEXT("InputNode"));
		UObject* OutputNode = GetObjectPropertyValueByName(Graph, TEXT("OutputNode"));
		if (InputNode)
		{
			TSharedPtr<FJsonObject> InputNodeObject = MakeObjectReferenceObject(InputNode);
			InputNodeObject->SetArrayField(TEXT("input_pins"), MakePCGPinArray(InputNode, TEXT("InputPins")));
			InputNodeObject->SetArrayField(TEXT("output_pins"), MakePCGPinArray(InputNode, TEXT("OutputPins")));
			Summary->SetObjectField(TEXT("input_node"), InputNodeObject);
		}
		if (OutputNode)
		{
			TSharedPtr<FJsonObject> OutputNodeObject = MakeObjectReferenceObject(OutputNode);
			OutputNodeObject->SetArrayField(TEXT("input_pins"), MakePCGPinArray(OutputNode, TEXT("InputPins")));
			OutputNodeObject->SetArrayField(TEXT("output_pins"), MakePCGPinArray(OutputNode, TEXT("OutputPins")));
			Summary->SetObjectField(TEXT("output_node"), OutputNodeObject);
		}

		TArray<TSharedPtr<FJsonValue>> Nodes = MakePCGNodeArray(Graph, TEXT("Nodes"));
		TArray<TSharedPtr<FJsonValue>> Connections;
		TSet<FString> SeenEdges;
		AppendPCGConnectionsForPins(InputNode, TEXT("OutputPins"), SeenEdges, Connections);
		AppendPCGConnectionsForPins(OutputNode, TEXT("InputPins"), SeenEdges, Connections);
		if (FArrayProperty* NodesProperty = CastField<FArrayProperty>(Graph->GetClass()->FindPropertyByName(TEXT("Nodes"))))
		{
			FObjectPropertyBase* NodeObjectProperty = CastField<FObjectPropertyBase>(NodesProperty->Inner);
			void* NodesValue = NodesProperty->ContainerPtrToValuePtr<void>(Graph);
			FScriptArrayHelper NodesHelper(NodesProperty, NodesValue);
			for (int32 NodeIndex = 0; NodeIndex < NodesHelper.Num(); ++NodeIndex)
			{
				UObject* Node = NodeObjectProperty ? NodeObjectProperty->GetObjectPropertyValue(NodesHelper.GetRawPtr(NodeIndex)) : nullptr;
				AppendPCGConnectionsForPins(Node, TEXT("InputPins"), SeenEdges, Connections);
				AppendPCGConnectionsForPins(Node, TEXT("OutputPins"), SeenEdges, Connections);
			}
		}
		Summary->SetArrayField(TEXT("nodes"), Nodes);
		Summary->SetArrayField(TEXT("connections"), Connections);
		Summary->SetNumberField(TEXT("node_count"), Nodes.Num());
		Summary->SetBoolField(TEXT("has_input_node"), InputNode != nullptr);
		Summary->SetBoolField(TEXT("has_output_node"), OutputNode != nullptr);
		Summary->SetNumberField(TEXT("connection_count"), Connections.Num());
		return Summary;
	}

	static bool InvokePCGAddEdgeTo(UObject* FromNode, const FName& FromPinLabel, UObject* ToNode, const FName& ToPinLabel, FString& OutError)
	{
		if (!FromNode || !ToNode)
		{
			OutError = TEXT("PCG input or output node was invalid.");
			return false;
		}

		UFunction* AddEdgeFunction = FromNode->FindFunction(TEXT("AddEdgeTo"));
		if (!AddEdgeFunction)
		{
			OutError = TEXT("PCG node did not expose AddEdgeTo.");
			return false;
		}

		uint8* Params = static_cast<uint8*>(FMemory_Alloca(AddEdgeFunction->ParmsSize));
		FMemory::Memzero(Params, AddEdgeFunction->ParmsSize);
		AddEdgeFunction->InitializeStruct(Params);
		for (TFieldIterator<FProperty> It(AddEdgeFunction); It; ++It)
		{
			FProperty* Property = *It;
			if (!Property || !Property->HasAnyPropertyFlags(CPF_Parm) || Property->HasAnyPropertyFlags(CPF_ReturnParm))
			{
				continue;
			}

			if (Property->GetFName() == FName(TEXT("FromPinLabel")))
			{
				if (FNameProperty* NameProperty = CastField<FNameProperty>(Property))
				{
					NameProperty->SetPropertyValue_InContainer(Params, FromPinLabel);
				}
			}
			else if (Property->GetFName() == FName(TEXT("To")))
			{
				if (FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property))
				{
					ObjectProperty->SetObjectPropertyValue_InContainer(Params, ToNode);
				}
			}
			else if (Property->GetFName() == FName(TEXT("ToPinLabel")))
			{
				if (FNameProperty* NameProperty = CastField<FNameProperty>(Property))
				{
					NameProperty->SetPropertyValue_InContainer(Params, ToPinLabel);
				}
			}
		}

		FromNode->ProcessEvent(AddEdgeFunction, Params);
		AddEdgeFunction->DestroyStruct(Params);
		return true;
	}

	static UObject* FindPCGComponent(AActor* Actor)
	{
		if (!Actor)
		{
			return nullptr;
		}

		UClass* PCGComponentClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGComponent"));
		if (!PCGComponentClass)
		{
			return nullptr;
		}

		TArray<UActorComponent*> Components;
		Actor->GetComponents(Components);
		for (UActorComponent* Component : Components)
		{
			if (Component && Component->IsA(PCGComponentClass))
			{
				return Component;
			}
		}
		return nullptr;
	}

	static bool InvokePCGSetGraph(UObject* PCGComponent, UObject* Graph, FString& OutError)
	{
		if (!PCGComponent)
		{
			OutError = TEXT("PCG component was invalid.");
			return false;
		}

		UFunction* SetGraphFunction = PCGComponent->FindFunction(TEXT("SetGraph"));
		if (!SetGraphFunction)
		{
			OutError = TEXT("PCG component did not expose SetGraph.");
			return false;
		}

		uint8* Params = static_cast<uint8*>(FMemory_Alloca(SetGraphFunction->ParmsSize));
		FMemory::Memzero(Params, SetGraphFunction->ParmsSize);
		SetGraphFunction->InitializeStruct(Params);
		bool bSetObjectParam = false;
		for (TFieldIterator<FProperty> It(SetGraphFunction); It; ++It)
		{
			FProperty* Property = *It;
			if (!Property || !Property->HasAnyPropertyFlags(CPF_Parm) || Property->HasAnyPropertyFlags(CPF_ReturnParm))
			{
				continue;
			}

			if (FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property))
			{
				ObjectProperty->SetObjectPropertyValue_InContainer(Params, Graph);
				bSetObjectParam = true;
				break;
			}
		}

		if (!bSetObjectParam)
		{
			SetGraphFunction->DestroyStruct(Params);
			OutError = TEXT("PCG SetGraph parameters did not contain an object graph argument.");
			return false;
		}

		PCGComponent->ProcessEvent(SetGraphFunction, Params);
		SetGraphFunction->DestroyStruct(Params);
		return true;
	}

	static bool InvokePCGCleanupLocalImmediate(UObject* PCGComponent, bool bRemoveComponents, bool bCleanupLocalComponents, FString& OutError)
	{
		if (!PCGComponent)
		{
			OutError = TEXT("PCG component was invalid.");
			return false;
		}

		UFunction* CleanupFunction = PCGComponent->FindFunction(TEXT("CleanupLocalImmediate"));
		if (!CleanupFunction)
		{
			OutError = TEXT("PCG component did not expose CleanupLocalImmediate.");
			return false;
		}

		uint8* Params = static_cast<uint8*>(FMemory_Alloca(CleanupFunction->ParmsSize));
		FMemory::Memzero(Params, CleanupFunction->ParmsSize);
		CleanupFunction->InitializeStruct(Params);
		for (TFieldIterator<FProperty> It(CleanupFunction); It; ++It)
		{
			FProperty* Property = *It;
			if (!Property || !Property->HasAnyPropertyFlags(CPF_Parm) || Property->HasAnyPropertyFlags(CPF_ReturnParm))
			{
				continue;
			}

			if (FBoolProperty* BoolProperty = CastField<FBoolProperty>(Property))
			{
				if (Property->GetFName() == FName(TEXT("bRemoveComponents")))
				{
					BoolProperty->SetPropertyValue_InContainer(Params, bRemoveComponents);
				}
				else if (Property->GetFName() == FName(TEXT("bCleanupLocalComponents")))
				{
					BoolProperty->SetPropertyValue_InContainer(Params, bCleanupLocalComponents);
				}
			}
		}

		PCGComponent->ProcessEvent(CleanupFunction, Params);
		CleanupFunction->DestroyStruct(Params);
		return true;
	}

	static bool InvokePCGCleanupLocal(UObject* PCGComponent, bool bRemoveComponents, bool bSave, FString& OutError)
	{
		if (!PCGComponent)
		{
			OutError = TEXT("PCG component was invalid.");
			return false;
		}

		UFunction* CleanupFunction = PCGComponent->FindFunction(TEXT("CleanupLocal"));
		if (!CleanupFunction)
		{
			OutError = TEXT("PCG component did not expose CleanupLocal.");
			return false;
		}

		uint8* Params = static_cast<uint8*>(FMemory_Alloca(CleanupFunction->ParmsSize));
		FMemory::Memzero(Params, CleanupFunction->ParmsSize);
		CleanupFunction->InitializeStruct(Params);
		for (TFieldIterator<FProperty> It(CleanupFunction); It; ++It)
		{
			FProperty* Property = *It;
			if (!Property || !Property->HasAnyPropertyFlags(CPF_Parm) || Property->HasAnyPropertyFlags(CPF_ReturnParm))
			{
				continue;
			}

			if (FBoolProperty* BoolProperty = CastField<FBoolProperty>(Property))
			{
				if (Property->GetFName() == FName(TEXT("bRemoveComponents")))
				{
					BoolProperty->SetPropertyValue_InContainer(Params, bRemoveComponents);
				}
				else if (Property->GetFName() == FName(TEXT("bSave")))
				{
					BoolProperty->SetPropertyValue_InContainer(Params, bSave);
				}
			}
		}

		PCGComponent->ProcessEvent(CleanupFunction, Params);
		CleanupFunction->DestroyStruct(Params);
		return true;
	}

	static UObject* ResolvePCGGraphFromComponent(UObject* PCGComponent)
	{
		if (!PCGComponent)
		{
			return nullptr;
		}

		if (UObject* Graph = GetObjectPropertyValueByName(PCGComponent, TEXT("Graph")))
		{
			return Graph;
		}
		if (UObject* GraphInstance = GetObjectPropertyValueByName(PCGComponent, TEXT("GraphInstance")))
		{
			if (UObject* Graph = GetObjectPropertyValueByName(GraphInstance, TEXT("Graph")))
			{
				return Graph;
			}
		}
		if (UObject* OriginalGraph = GetObjectPropertyValueByName(PCGComponent, TEXT("OriginalGraph")))
		{
			return OriginalGraph;
		}
		return nullptr;
	}

	static bool InvokePCGGenerateLocal(UObject* PCGComponent, bool bForce, FString& OutError)
	{
		if (!PCGComponent)
		{
			OutError = TEXT("PCG component was invalid.");
			return false;
		}

		UFunction* GenerateFunction = PCGComponent->FindFunction(TEXT("GenerateLocal"));
		if (!GenerateFunction)
		{
			OutError = TEXT("PCG component did not expose GenerateLocal.");
			return false;
		}

		uint8* Params = static_cast<uint8*>(FMemory_Alloca(GenerateFunction->ParmsSize));
		FMemory::Memzero(Params, GenerateFunction->ParmsSize);
		GenerateFunction->InitializeStruct(Params);
		for (TFieldIterator<FProperty> It(GenerateFunction); It; ++It)
		{
			FProperty* Property = *It;
			if (!Property || !Property->HasAnyPropertyFlags(CPF_Parm) || Property->HasAnyPropertyFlags(CPF_ReturnParm))
			{
				continue;
			}

			if (FBoolProperty* BoolProperty = CastField<FBoolProperty>(Property))
			{
				BoolProperty->SetPropertyValue_InContainer(Params, bForce);
				break;
			}
		}

		PCGComponent->ProcessEvent(GenerateFunction, Params);
		GenerateFunction->DestroyStruct(Params);
		return true;
	}

	static bool GetBoolPropertyValueByName(UObject* Object, const TCHAR* PropertyName)
	{
		if (!Object || !Object->GetClass())
		{
			return false;
		}

		FBoolProperty* BoolProperty = CastField<FBoolProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		return BoolProperty ? BoolProperty->GetPropertyValue_InContainer(Object) : false;
	}

	static int32 GetArrayPropertyCountByName(UObject* Object, const TCHAR* PropertyName)
	{
		if (!Object || !Object->GetClass())
		{
			return 0;
		}

		FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Object->GetClass()->FindPropertyByName(PropertyName));
		if (!ArrayProperty)
		{
			return 0;
		}

		void* ArrayValue = ArrayProperty->ContainerPtrToValuePtr<void>(Object);
		FScriptArrayHelper Helper(ArrayProperty, ArrayValue);
		return Helper.Num();
	}

	static int32 GetPCGPointDataCount(UObject* Data)
	{
		if (!Data || !Data->GetClass())
		{
			return 0;
		}

		UFunction* GetNumPointsFunction = Data->FindFunction(TEXT("GetNumPoints"));
		if (GetNumPointsFunction)
		{
			uint8* Params = static_cast<uint8*>(FMemory_Alloca(GetNumPointsFunction->ParmsSize));
			FMemory::Memzero(Params, GetNumPointsFunction->ParmsSize);
			GetNumPointsFunction->InitializeStruct(Params);
			Data->ProcessEvent(GetNumPointsFunction, Params);
			int32 Count = 0;
			for (TFieldIterator<FProperty> It(GetNumPointsFunction); It; ++It)
			{
				FProperty* Property = *It;
				if (Property && Property->HasAnyPropertyFlags(CPF_ReturnParm))
				{
					if (FIntProperty* IntProperty = CastField<FIntProperty>(Property))
					{
						Count = IntProperty->GetPropertyValue_InContainer(Params);
					}
					break;
				}
			}
			GetNumPointsFunction->DestroyStruct(Params);
			return Count;
		}

		return GetArrayPropertyCountByName(Data, TEXT("Points"));
	}

	static TSharedPtr<FJsonObject> MakePCGGeneratedOutputSummary(UObject* PCGComponent)
	{
		TSharedPtr<FJsonObject> Summary = MakeShared<FJsonObject>();
		Summary->SetNumberField(TEXT("tagged_data_count"), 0);
		Summary->SetNumberField(TEXT("point_data_count"), 0);
		Summary->SetNumberField(TEXT("total_point_count"), 0);
		if (!PCGComponent || !PCGComponent->GetClass())
		{
			return Summary;
		}

		FStructProperty* CollectionProperty = CastField<FStructProperty>(PCGComponent->GetClass()->FindPropertyByName(TEXT("GeneratedGraphOutput")));
		FArrayProperty* TaggedDataProperty = CollectionProperty && CollectionProperty->Struct ? CastField<FArrayProperty>(CollectionProperty->Struct->FindPropertyByName(TEXT("TaggedData"))) : nullptr;
		FStructProperty* TaggedDataStructProperty = TaggedDataProperty ? CastField<FStructProperty>(TaggedDataProperty->Inner) : nullptr;
		if (!CollectionProperty || !TaggedDataProperty || !TaggedDataStructProperty || !TaggedDataStructProperty->Struct)
		{
			Summary->SetStringField(TEXT("status"), TEXT("generated_graph_output_unavailable"));
			return Summary;
		}

		void* CollectionValue = CollectionProperty->ContainerPtrToValuePtr<void>(PCGComponent);
		void* TaggedDataValue = TaggedDataProperty->ContainerPtrToValuePtr<void>(CollectionValue);
		FScriptArrayHelper TaggedDataHelper(TaggedDataProperty, TaggedDataValue);
		TArray<TSharedPtr<FJsonValue>> Items;
		int32 PointDataCount = 0;
		int32 TotalPointCount = 0;
		for (int32 Index = 0; Index < TaggedDataHelper.Num(); ++Index)
		{
			void* ItemValue = TaggedDataHelper.GetRawPtr(Index);
			FObjectPropertyBase* DataProperty = CastField<FObjectPropertyBase>(TaggedDataStructProperty->Struct->FindPropertyByName(TEXT("Data")));
			FNameProperty* PinProperty = CastField<FNameProperty>(TaggedDataStructProperty->Struct->FindPropertyByName(TEXT("Pin")));
			UObject* Data = DataProperty ? DataProperty->GetObjectPropertyValue(ItemValue) : nullptr;

			TSharedPtr<FJsonObject> Item = MakeObjectReferenceObject(Data);
			Item->SetStringField(TEXT("pin"), PinProperty ? PinProperty->GetPropertyValue_InContainer(ItemValue).ToString() : TEXT(""));
			const int32 PointCount = GetPCGPointDataCount(Data);
			Item->SetNumberField(TEXT("point_count"), PointCount);
			if (PointCount > 0)
			{
				++PointDataCount;
				TotalPointCount += PointCount;
			}
			Items.Add(MakeShared<FJsonValueObject>(Item));
		}

		Summary->SetStringField(TEXT("status"), TEXT("generated_graph_output_inspected"));
		Summary->SetNumberField(TEXT("tagged_data_count"), TaggedDataHelper.Num());
		Summary->SetNumberField(TEXT("point_data_count"), PointDataCount);
		Summary->SetNumberField(TEXT("total_point_count"), TotalPointCount);
		Summary->SetArrayField(TEXT("items"), Items);
		return Summary;
	}

	static TSharedPtr<FJsonObject> MakePCGComponentGenerationSummary(UObject* PCGComponent)
	{
		TSharedPtr<FJsonObject> Summary = MakeObjectReferenceObject(PCGComponent);
		if (!PCGComponent)
		{
			return Summary;
		}

		const bool bGenerated = GetBoolPropertyValueByName(PCGComponent, TEXT("bGenerated"));
		Summary->SetBoolField(TEXT("generated"), bGenerated);
		Summary->SetBoolField(TEXT("dirty_generated"), GetBoolPropertyValueByName(PCGComponent, TEXT("bDirtyGenerated")));
		Summary->SetBoolField(TEXT("was_generated_this_session"), GetBoolPropertyValueByName(PCGComponent, TEXT("bWasGeneratedThisSession")));
		Summary->SetNumberField(TEXT("generated_resource_count"), GetArrayPropertyCountByName(PCGComponent, TEXT("GeneratedResources")));
		if (FStructProperty* BoundsProperty = CastField<FStructProperty>(PCGComponent->GetClass()->FindPropertyByName(TEXT("LastGeneratedBounds"))))
		{
			if (BoundsProperty->Struct && BoundsProperty->Struct->GetFName() == NAME_Box)
			{
				const FBox& Bounds = *BoundsProperty->ContainerPtrToValuePtr<FBox>(PCGComponent);
				Summary->SetObjectField(TEXT("last_generated_bounds"), Bounds.IsValid ? MakeBoxObject(Bounds) : MakeShared<FJsonObject>());
			}
		}
		Summary->SetObjectField(TEXT("generated_graph_output"), MakePCGGeneratedOutputSummary(PCGComponent));

		TArray<TSharedPtr<FJsonValue>> ResourceSummaries;
		TArray<TSharedPtr<FJsonValue>> NativeSpawnedActors;
		int32 NativeSpawnedActorCount = 0;
		if (FArrayProperty* ResourcesProperty = CastField<FArrayProperty>(PCGComponent->GetClass()->FindPropertyByName(TEXT("GeneratedResources"))))
		{
			FObjectPropertyBase* ResourceObjectProperty = CastField<FObjectPropertyBase>(ResourcesProperty->Inner);
			void* ResourcesValue = ResourcesProperty->ContainerPtrToValuePtr<void>(PCGComponent);
			FScriptArrayHelper ResourcesHelper(ResourcesProperty, ResourcesValue);
			for (int32 ResourceIndex = 0; ResourceIndex < ResourcesHelper.Num(); ++ResourceIndex)
			{
				UObject* Resource = ResourceObjectProperty ? ResourceObjectProperty->GetObjectPropertyValue(ResourcesHelper.GetRawPtr(ResourceIndex)) : nullptr;
				if (!Resource)
				{
					continue;
				}

				TSharedPtr<FJsonObject> ResourceObject = MakeObjectReferenceObject(Resource);
				ResourceObject->SetNumberField(TEXT("index"), ResourceIndex);
				ResourceObject->SetBoolField(TEXT("is_managed_actors"), Resource->GetClass() && Resource->GetClass()->GetName().Contains(TEXT("ManagedActors")));

				int32 ResourceActorCount = 0;
				TArray<TSharedPtr<FJsonValue>> ResourceActors;
				if (FSetProperty* GeneratedActorsProperty = CastField<FSetProperty>(Resource->GetClass()->FindPropertyByName(TEXT("GeneratedActors"))))
				{
					FSoftObjectProperty* SoftObjectProperty = CastField<FSoftObjectProperty>(GeneratedActorsProperty->ElementProp);
					void* SetValue = GeneratedActorsProperty->ContainerPtrToValuePtr<void>(Resource);
					FScriptSetHelper SetHelper(GeneratedActorsProperty, SetValue);
					for (int32 SetIndex = 0; SetIndex < SetHelper.GetMaxIndex(); ++SetIndex)
					{
						if (!SetHelper.IsValidIndex(SetIndex))
						{
							continue;
						}

						FSoftObjectPtr SoftActor = SoftObjectProperty ? SoftObjectProperty->GetPropertyValue(SetHelper.GetElementPtr(SetIndex)) : FSoftObjectPtr();
						AActor* Actor = Cast<AActor>(SoftActor.LoadSynchronous());
						if (!Actor)
						{
							continue;
						}

						++ResourceActorCount;
						++NativeSpawnedActorCount;
						TSharedPtr<FJsonObject> ActorObject = MakeEditorActorSummary(Actor, false);
						ResourceActors.Add(MakeShared<FJsonValueObject>(ActorObject));
						NativeSpawnedActors.Add(MakeShared<FJsonValueObject>(ActorObject));
					}
				}
				ResourceObject->SetNumberField(TEXT("generated_actor_count"), ResourceActorCount);
				ResourceObject->SetArrayField(TEXT("generated_actors"), ResourceActors);
				ResourceSummaries.Add(MakeShared<FJsonValueObject>(ResourceObject));
			}
		}
		Summary->SetNumberField(TEXT("native_spawned_actor_count"), NativeSpawnedActorCount);
		Summary->SetArrayField(TEXT("native_spawned_actors"), NativeSpawnedActors);
		Summary->SetArrayField(TEXT("generated_resources"), ResourceSummaries);
		return Summary;
	}

	static bool HasBlueprintSearchTarget(const TSet<FString>& Targets, const FString& Target)
	{
		return Targets.Contains(TEXT("all")) || Targets.Contains(Target);
	}
}

bool FDruidsMCPBridgeServer::Start(uint16 PreferredPort)
{
	if (bRunning)
	{
		UE_LOG(LogDruidsMCPBridge, Display, TEXT("Druids MCP Bridge already running at %s"), *GetEndpointUrl());
		return true;
	}

	FHttpServerModule& HttpServerModule = FHttpServerModule::Get();
	UE_LOG(LogDruidsMCPBridge, Display, TEXT("Starting Druids MCP Bridge. HTTPServer available: %s"), FHttpServerModule::IsAvailable() ? TEXT("true") : TEXT("false"));
	UE_LOG(LogDruidsMCPBridge, Display, TEXT("Enabling HTTPServer listeners before binding routes"));
	HttpServerModule.StartAllListeners();

	const uint16 StartPort = PreferredPort == 0 ? DruidsMCPBridge::DefaultPort : PreferredPort;
	for (uint16 Attempt = 0; Attempt < DruidsMCPBridge::MaxPortAttempts; ++Attempt)
	{
		const uint16 CandidatePort = StartPort + Attempt;
		UE_LOG(LogDruidsMCPBridge, Display, TEXT("Trying MCP endpoint port %d"), CandidatePort);
		if (BindOnPort(CandidatePort))
		{
			ActivePort = CandidatePort;
			bRunning = true;
			UE_LOG(LogDruidsMCPBridge, Display, TEXT("Route bound. Starting HTTPServer listeners after route registration"));
			HttpServerModule.StartAllListeners();
			const bool bVerifiedListening = VerifyLocalPortListening(ActivePort);
			UE_LOG(LogDruidsMCPBridge, Display, TEXT("Druids MCP Bridge listening at %s"), *GetEndpointUrl());
			UE_LOG(LogDruidsMCPBridge, Display, TEXT("Druids MCP Bridge local TCP verification for 127.0.0.1:%d: %s"), ActivePort, bVerifiedListening ? TEXT("success") : TEXT("failed"));
			return true;
		}
	}

	UE_LOG(LogDruidsMCPBridge, Error, TEXT("Druids MCP Bridge failed to bind a port starting at %d"), StartPort);
	return false;
}

void FDruidsMCPBridgeServer::Stop()
{
	if (Router.IsValid() && RouteHandle.IsValid())
	{
		Router->UnbindRoute(RouteHandle);
		RouteHandle.Reset();
	}

	if (bRunning)
	{
		UE_LOG(LogDruidsMCPBridge, Display, TEXT("Druids MCP Bridge stopped"));
	}

	Router.Reset();
	ActivePort = 0;
	bRunning = false;

	if (FHttpServerModule::IsAvailable())
	{
		FHttpServerModule::Get().StopAllListeners();
	}
}

FString FDruidsMCPBridgeServer::GetEndpointUrl() const
{
	if (!bRunning || ActivePort == 0)
	{
		return TEXT("stopped");
	}

	return FString::Printf(TEXT("http://127.0.0.1:%d%s"), ActivePort, DruidsMCPBridge::EndpointPath);
}

bool FDruidsMCPBridgeServer::BindOnPort(uint16 Port)
{
	FHttpServerModule& HttpServerModule = FHttpServerModule::Get();
	UE_LOG(LogDruidsMCPBridge, Display, TEXT("Requesting HTTP router for port %d"), Port);
	TSharedPtr<IHttpRouter> CandidateRouter = HttpServerModule.GetHttpRouter(Port, true);
	if (!CandidateRouter.IsValid())
	{
		UE_LOG(LogDruidsMCPBridge, Warning, TEXT("HTTPServer failed to create/listen with router for port %d"), Port);
		return false;
	}
	UE_LOG(LogDruidsMCPBridge, Display, TEXT("HTTP router is valid for port %d. Binding POST %s"), Port, DruidsMCPBridge::EndpointPath);

	FHttpRouteHandle CandidateRoute = CandidateRouter->BindRoute(
		FHttpPath(DruidsMCPBridge::EndpointPath),
		EHttpServerRequestVerbs::VERB_POST,
		FHttpRequestHandler::CreateRaw(this, &FDruidsMCPBridgeServer::HandleMCPRequest));

	if (!CandidateRoute.IsValid())
	{
		UE_LOG(LogDruidsMCPBridge, Warning, TEXT("HTTPServer failed to bind POST %s on port %d"), DruidsMCPBridge::EndpointPath, Port);
		return false;
	}
	UE_LOG(LogDruidsMCPBridge, Display, TEXT("HTTP route POST %s bound on port %d"), DruidsMCPBridge::EndpointPath, Port);

	Router = CandidateRouter;
	RouteHandle = CandidateRoute;
	return true;
}

bool FDruidsMCPBridgeServer::VerifyLocalPortListening(uint16 Port) const
{
	ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
	if (!SocketSubsystem)
	{
		UE_LOG(LogDruidsMCPBridge, Warning, TEXT("Socket subsystem unavailable; cannot verify local TCP port"));
		return false;
	}

	bool bIsValidAddress = false;
	TSharedRef<FInternetAddr> Address = SocketSubsystem->CreateInternetAddr();
	Address->SetIp(TEXT("127.0.0.1"), bIsValidAddress);
	Address->SetPort(Port);

	if (!bIsValidAddress)
	{
		UE_LOG(LogDruidsMCPBridge, Warning, TEXT("Could not build loopback verification address for 127.0.0.1:%d"), Port);
		return false;
	}

	FSocket* Socket = SocketSubsystem->CreateSocket(NAME_Stream, TEXT("DruidsMCPBridgePortVerification"), false);
	if (!Socket)
	{
		UE_LOG(LogDruidsMCPBridge, Warning, TEXT("Could not create socket for local TCP verification"));
		return false;
	}

	const bool bConnected = Socket->Connect(*Address);
	Socket->Close();
	SocketSubsystem->DestroySocket(Socket);
	return bConnected;
}

bool FDruidsMCPBridgeServer::HandleMCPRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
	const FUTF8ToTCHAR BodyText(reinterpret_cast<const ANSICHAR*>(Request.Body.GetData()), Request.Body.Num());
	const FString RequestBody(BodyText.Length(), BodyText.Get());

	if (IsInGameThread())
	{
		HandleMCPRequestOnGameThread(RequestBody, OnComplete);
	}
	else
	{
		AsyncTask(ENamedThreads::GameThread, [this, RequestBody, OnComplete]()
		{
			HandleMCPRequestOnGameThread(RequestBody, OnComplete);
		});
	}

	return true;
}

void FDruidsMCPBridgeServer::HandleMCPRequestOnGameThread(FString RequestBody, FHttpResultCallback OnComplete)
{
	TSharedPtr<FJsonObject> RequestObject;
	const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(RequestBody);

	if (!FJsonSerializer::Deserialize(Reader, RequestObject) || !RequestObject.IsValid())
	{
		OnComplete(MakeHttpJsonResponse(JsonObjectToString(MakeJsonRpcErrorWithId(nullptr, -32700, TEXT("Parse error")))));
		return;
	}

	TSharedPtr<FJsonObject> ResponseObject = ProcessJsonRpcRequest(RequestObject);
	if (!ResponseObject.IsValid())
	{
		OnComplete(FHttpServerResponse::Ok());
		return;
	}

	OnComplete(MakeHttpJsonResponse(JsonObjectToString(ResponseObject)));
}

TUniquePtr<FHttpServerResponse> FDruidsMCPBridgeServer::MakeHttpJsonResponse(const FString& JsonText) const
{
	TUniquePtr<FHttpServerResponse> Response = FHttpServerResponse::Create(JsonText, TEXT("application/json; charset=utf-8"));
	Response->Code = EHttpServerResponseCodes::Ok;
	return Response;
}

TUniquePtr<FHttpServerResponse> FDruidsMCPBridgeServer::MakeHttpErrorResponse(const FString& Message) const
{
	TUniquePtr<FHttpServerResponse> Response = FHttpServerResponse::Error(EHttpServerResponseCodes::BadRequest, TEXT("bad_request"), Message);
	return Response;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ProcessJsonRpcRequest(const TSharedPtr<FJsonObject>& RequestObject)
{
	FString Method;
	if (!RequestObject->TryGetStringField(TEXT("method"), Method))
	{
		return MakeJsonRpcError(RequestObject, -32600, TEXT("Invalid Request: missing method"));
	}

	if (Method == TEXT("initialize"))
	{
		return HandleInitialize(RequestObject);
	}
	if (Method == TEXT("notifications/initialized"))
	{
		return HandleNotification(RequestObject);
	}
	if (Method == TEXT("ping"))
	{
		return HandlePing(RequestObject);
	}
	if (Method == TEXT("tools/list"))
	{
		return HandleToolsList(RequestObject);
	}
	if (Method == TEXT("tools/call"))
	{
		return HandleToolsCall(RequestObject);
	}

	return MakeJsonRpcError(RequestObject, -32601, FString::Printf(TEXT("Method not found: %s"), *Method));
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::HandleInitialize(const TSharedPtr<FJsonObject>& RequestObject) const
{
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("protocolVersion"), TEXT("2025-06-18"));

	TSharedPtr<FJsonObject> Capabilities = MakeShared<FJsonObject>();
	Capabilities->SetObjectField(TEXT("tools"), MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("capabilities"), Capabilities);

	TSharedPtr<FJsonObject> ServerInfo = MakeShared<FJsonObject>();
	ServerInfo->SetStringField(TEXT("name"), TEXT("druids-unreal-mcp-bridge"));
	ServerInfo->SetStringField(TEXT("version"), TEXT("1.0.0"));
	Result->SetObjectField(TEXT("serverInfo"), ServerInfo);

	return MakeJsonRpcResult(RequestObject, Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::HandleNotification(const TSharedPtr<FJsonObject>& RequestObject) const
{
	const TSharedPtr<FJsonValue> IdField = RequestObject->TryGetField(TEXT("id"));
	if (!IdField.IsValid())
	{
		return nullptr;
	}

	return MakeJsonRpcResult(RequestObject, MakeShared<FJsonObject>());
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::HandlePing(const TSharedPtr<FJsonObject>& RequestObject) const
{
	return MakeJsonRpcResult(RequestObject, MakeShared<FJsonObject>());
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::HandleToolsList(const TSharedPtr<FJsonObject>& RequestObject) const
{
	TArray<TSharedPtr<FJsonValue>> Tools;

	TSharedPtr<FJsonObject> EmptySchema = MakeObjectSchema();
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("unreal_get_status"),
		TEXT("Return Druids MCP Bridge and Unreal Editor status."),
		EmptySchema)));
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("unreal_get_capabilities"),
		TEXT("Return the authoritative Druids MCP Bridge capability matrix and recommended discovery workflow."),
		EmptySchema)));

	TSharedPtr<FJsonObject> RecommendToolsSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> RecommendToolsProps = MakeShared<FJsonObject>();
	RecommendToolsProps->SetObjectField(TEXT("Task"), MakeStringProperty(TEXT("Natural-language task or user request to route, for example create a Blueprint actor that changes a material parameter.")));
	RecommendToolsProps->SetObjectField(TEXT("Domain"), MakeStringProperty(TEXT("Optional domain hint: blueprint, material, asset_inspection, project_search, onboarding, sage, or broad.")));
	RecommendToolsProps->SetObjectField(TEXT("Intent"), MakeStringProperty(TEXT("Optional intent hint: search, inspect, author, connect, compile, onboard, or unsupported_check.")));
	RecommendToolsProps->SetObjectField(TEXT("AssetType"), MakeStringProperty(TEXT("Optional Unreal asset type hint, for example Blueprint, Material, StateTree, CameraRigAsset.")));
	RecommendToolsSchema->SetObjectField(TEXT("properties"), RecommendToolsProps);
	RecommendToolsSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("Task")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("unreal_recommend_tools"),
		TEXT("Route a task to the most relevant Druids MCP tools and known unsupported boundaries without hiding the full tool surface."),
		RecommendToolsSchema)));

	TSharedPtr<FJsonObject> SageListSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> SageListProps = MakeShared<FJsonObject>();
	SageListProps->SetObjectField(TEXT("limit"), MakeStringProperty(TEXT("Maximum extensions to return. Defaults to 100.")));
	SageListSchema->SetObjectField(TEXT("properties"), SageListProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("sage_list_extensions"),
		TEXT("List standalone Sage extensions discovered by the Druids MCP Bridge."),
		SageListSchema)));

	TSharedPtr<FJsonObject> SageSearchSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> SageSearchProps = MakeShared<FJsonObject>();
	SageSearchProps->SetObjectField(TEXT("search"), MakeStringProperty(TEXT("Search text matched against extension names, descriptions, actions, and queries.")));
	SageSearchProps->SetObjectField(TEXT("limit"), MakeStringProperty(TEXT("Maximum matches to return. Defaults to 25.")));
	SageSearchSchema->SetObjectField(TEXT("properties"), SageSearchProps);
	SageSearchSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("search")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("sage_search_extensions"),
		TEXT("Search standalone Sage extensions and capabilities."),
		SageSearchSchema)));

	TSharedPtr<FJsonObject> SageDetailsSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> SageDetailsProps = MakeShared<FJsonObject>();
	SageDetailsProps->SetObjectField(TEXT("extension_id"), MakeStringProperty(TEXT("Extension id from sage_list_extensions or sage_search_extensions.")));
	SageDetailsSchema->SetObjectField(TEXT("properties"), SageDetailsProps);
	SageDetailsSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("extension_id")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("sage_get_extension_details"),
		TEXT("Return full metadata, actions, and queries for a standalone Sage extension."),
		SageDetailsSchema)));

	TSharedPtr<FJsonObject> SageQuerySchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> SageQueryProps = MakeShared<FJsonObject>();
	SageQueryProps->SetObjectField(TEXT("extension_id"), MakeStringProperty(TEXT("Extension id from Sage discovery.")));
	SageQueryProps->SetObjectField(TEXT("query_name"), MakeStringProperty(TEXT("Query function name to execute.")));
	SageQueryProps->SetObjectField(TEXT("parameters"), MakeObjectProperty(TEXT("Query parameters keyed by parameter name.")));
	SageQuerySchema->SetObjectField(TEXT("properties"), SageQueryProps);
	SageQuerySchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("extension_id")),
		MakeShared<FJsonValueString>(TEXT("query_name"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("sage_call_query"),
		TEXT("Execute a query on a standalone Sage extension and return JSON results."),
		SageQuerySchema)));

	TSharedPtr<FJsonObject> SageActionSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> SageActionProps = MakeShared<FJsonObject>();
	SageActionProps->SetObjectField(TEXT("extension_id"), MakeStringProperty(TEXT("Extension id from Sage discovery.")));
	SageActionProps->SetObjectField(TEXT("action_name"), MakeStringProperty(TEXT("Action function name to execute.")));
	SageActionProps->SetObjectField(TEXT("parameters"), MakeObjectProperty(TEXT("Action parameters keyed by parameter name.")));
	SageActionSchema->SetObjectField(TEXT("properties"), SageActionProps);
	SageActionSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("extension_id")),
		MakeShared<FJsonValueString>(TEXT("action_name"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("sage_call_action"),
		TEXT("Execute an action on a standalone Sage extension and return action status JSON."),
		SageActionSchema)));

	TSharedPtr<FJsonObject> ResolveAssetSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> ResolveAssetProps = MakeShared<FJsonObject>();
	ResolveAssetProps->SetObjectField(TEXT("name_or_path"), MakeStringProperty(TEXT("Asset name, keyword, or full Unreal asset path.")));
	ResolveAssetProps->SetObjectField(TEXT("type"), MakeStringProperty(TEXT("Asset type filter such as Blueprint, Material, Any. Defaults to Blueprint.")));
	ResolveAssetProps->SetObjectField(TEXT("limit"), MakeStringProperty(TEXT("Maximum candidates to return. Defaults to 10.")));
	ResolveAssetSchema->SetObjectField(TEXT("properties"), ResolveAssetProps);
	ResolveAssetSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("name_or_path")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("unreal_resolve_asset"),
		TEXT("Resolve an asset name or partial path to candidate Unreal asset paths."),
		ResolveAssetSchema)));

	TSharedPtr<FJsonObject> InspectAssetSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> InspectAssetProps = MakeShared<FJsonObject>();
	InspectAssetProps->SetObjectField(TEXT("AssetNameOrPath"), MakeStringProperty(TEXT("Asset name, keyword, full package path, or full object path.")));
	InspectAssetProps->SetObjectField(TEXT("Type"), MakeStringProperty(TEXT("Optional asset type filter for name searches. Defaults to Any.")));
	InspectAssetProps->SetObjectField(TEXT("PropertyLimit"), MakeStringProperty(TEXT("Maximum inspectable properties to return. Defaults to 200. Use 0 for no explicit limit.")));
	InspectAssetSchema->SetObjectField(TEXT("properties"), InspectAssetProps);
	InspectAssetSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("AssetNameOrPath")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("unreal_inspect_asset"),
		TEXT("Read-only inspection for any UObject asset: identity, class, package, editable/public properties, object references, and partial failures."),
		InspectAssetSchema)));

	TSharedPtr<FJsonObject> DataAssetFindClassesSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> DataAssetFindClassesProps = MakeShared<FJsonObject>();
	DataAssetFindClassesProps->SetObjectField(TEXT("Query"), MakeStringProperty(TEXT("Optional class/name substring to match, for example PDA_PlayerCharacter or PrimaryDataAsset.")));
	DataAssetFindClassesProps->SetObjectField(TEXT("PackagePaths"), MakeObjectProperty(TEXT("Optional array of package paths to search for Blueprint-generated Data Asset classes. Defaults to /Game.")));
	DataAssetFindClassesProps->SetObjectField(TEXT("IncludeNative"), MakeStringProperty(TEXT("Optional bool. Include native UDataAsset/UPrimaryDataAsset classes. Defaults to true.")));
	DataAssetFindClassesProps->SetObjectField(TEXT("IncludeBlueprint"), MakeStringProperty(TEXT("Optional bool. Include Blueprint-generated Data Asset classes. Defaults to true.")));
	DataAssetFindClassesProps->SetObjectField(TEXT("OnlyInstantiable"), MakeStringProperty(TEXT("Optional bool. Exclude abstract/deprecated classes. Defaults to true.")));
	DataAssetFindClassesProps->SetObjectField(TEXT("Limit"), MakeStringProperty(TEXT("Maximum class results to return. Defaults to 100.")));
	DataAssetFindClassesSchema->SetObjectField(TEXT("properties"), DataAssetFindClassesProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("data_asset_find_classes"),
		TEXT("Discover native and Blueprint-generated Data Asset / Primary Data Asset classes for real Data Asset instance authoring. Read-only; does not create Blueprint child classes."),
		DataAssetFindClassesSchema)));

	TSharedPtr<FJsonObject> DataAssetCreateSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> DataAssetCreateProps = MakeShared<FJsonObject>();
	DataAssetCreateProps->SetObjectField(TEXT("ClassPath"), MakeStringProperty(TEXT("Data Asset class path from data_asset_find_classes, such as /Script/Engine.PrimaryDataAsset or /Game/.../PDA_Ability.PDA_Ability_C.")));
	DataAssetCreateProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Destination package folder, for example /Game/Halcyon/DataAssets.")));
	DataAssetCreateProps->SetObjectField(TEXT("AssetName"), MakeStringProperty(TEXT("New Data Asset asset name, for example DA_Absquatulate.")));
	DataAssetCreateProps->SetObjectField(TEXT("Save"), MakeStringProperty(TEXT("Optional bool. Save the created asset package to disk. Defaults to true.")));
	DataAssetCreateSchema->SetObjectField(TEXT("properties"), DataAssetCreateProps);
	DataAssetCreateSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("ClassPath")),
		MakeShared<FJsonValueString>(TEXT("PackagePath")),
		MakeShared<FJsonValueString>(TEXT("AssetName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("data_asset_create"),
		TEXT("Create a real Data Asset UObject instance from an instantiable Data Asset class. This does not create a Blueprint Class child and does not set custom properties yet."),
		DataAssetCreateSchema)));

	TSharedPtr<FJsonObject> DataAssetSetPropertySchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> DataAssetSetPropertyProps = MakeShared<FJsonObject>();
	DataAssetSetPropertyProps->SetObjectField(TEXT("AssetNameOrPath"), MakeStringProperty(TEXT("Data Asset asset name, package path, or object path to edit.")));
	DataAssetSetPropertyProps->SetObjectField(TEXT("PropertyName"), MakeStringProperty(TEXT("Editable property name to set. Supports top-level names and nested struct paths such as ItemSummary.Name.")));
	DataAssetSetPropertyProps->SetObjectField(TEXT("Value"), MakeObjectProperty(TEXT("JSON value to import into the reflected property. Array/map indexing is not supported yet.")));
	DataAssetSetPropertyProps->SetObjectField(TEXT("Save"), MakeStringProperty(TEXT("Optional bool. Save the edited asset package to disk. Defaults to true.")));
	DataAssetSetPropertySchema->SetObjectField(TEXT("properties"), DataAssetSetPropertyProps);
	DataAssetSetPropertySchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("AssetNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("PropertyName")),
		MakeShared<FJsonValueString>(TEXT("Value"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("data_asset_set_property"),
		TEXT("Set one editable reflected property on a real Data Asset instance and save it. Supports top-level fields and nested struct paths; array/map indexing is not supported yet."),
		DataAssetSetPropertySchema)));

	TSharedPtr<FJsonObject> DataAssetCreateBatchSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> DataAssetCreateBatchProps = MakeShared<FJsonObject>();
	DataAssetCreateBatchProps->SetObjectField(TEXT("DefaultClassPath"), MakeStringProperty(TEXT("Optional default Data Asset class path for items that omit ClassPath.")));
	DataAssetCreateBatchProps->SetObjectField(TEXT("DefaultPackagePath"), MakeStringProperty(TEXT("Optional default destination package folder for items that omit PackagePath.")));
	DataAssetCreateBatchProps->SetObjectField(TEXT("Items"), MakeObjectProperty(TEXT("Array of item objects. Each item requires AssetName and may include ClassPath, PackagePath, and Properties object keyed by property path.")));
	DataAssetCreateBatchProps->SetObjectField(TEXT("Save"), MakeStringProperty(TEXT("Optional bool. Save created/edited assets. Defaults to true.")));
	DataAssetCreateBatchProps->SetObjectField(TEXT("ContinueOnError"), MakeStringProperty(TEXT("Optional bool. Continue creating later items after a failure. Defaults to false.")));
	DataAssetCreateBatchSchema->SetObjectField(TEXT("properties"), DataAssetCreateBatchProps);
	DataAssetCreateBatchSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("Items")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("data_asset_create_batch"),
		TEXT("Create multiple real Data Asset instances and optionally populate supported top-level or nested struct properties. Stops on first failure unless ContinueOnError is true."),
		DataAssetCreateBatchSchema)));

	TSharedPtr<FJsonObject> EditorWorldStatusSchema = MakeObjectSchema();
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("editor_get_world_status"),
		TEXT("Return current Unreal Editor world status, actor counts, selected actors, and editor placement guardrail state."),
		EditorWorldStatusSchema)));

	TSharedPtr<FJsonObject> EditorPlaceActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> EditorPlaceActorProps = MakeShared<FJsonObject>();
	EditorPlaceActorProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset path/name to place. Use this for Blueprint actor assets.")));
	EditorPlaceActorProps->SetObjectField(TEXT("ActorClassPath"), MakeStringProperty(TEXT("Native actor class path to place when BlueprintNameOrPath is empty.")));
	EditorPlaceActorProps->SetObjectField(TEXT("ActorLabel"), MakeStringProperty(TEXT("Optional editor actor label. Defaults to asset/class name.")));
	EditorPlaceActorProps->SetObjectField(TEXT("AllowCurrentLevel"), MakeStringProperty(TEXT("Required true for Slice 55 because placement mutates the current editor world.")));
	EditorPlaceActorProps->SetObjectField(TEXT("LocationX"), MakeStringProperty(TEXT("Optional actor X location. Defaults to 0.")));
	EditorPlaceActorProps->SetObjectField(TEXT("LocationY"), MakeStringProperty(TEXT("Optional actor Y location. Defaults to 0.")));
	EditorPlaceActorProps->SetObjectField(TEXT("LocationZ"), MakeStringProperty(TEXT("Optional actor Z location. Defaults to 0.")));
	EditorPlaceActorProps->SetObjectField(TEXT("RotationPitch"), MakeStringProperty(TEXT("Optional actor pitch rotation. Defaults to 0.")));
	EditorPlaceActorProps->SetObjectField(TEXT("RotationYaw"), MakeStringProperty(TEXT("Optional actor yaw rotation. Defaults to 0.")));
	EditorPlaceActorProps->SetObjectField(TEXT("RotationRoll"), MakeStringProperty(TEXT("Optional actor roll rotation. Defaults to 0.")));
	EditorPlaceActorProps->SetObjectField(TEXT("ScaleX"), MakeStringProperty(TEXT("Optional actor X scale. Defaults to 1.")));
	EditorPlaceActorProps->SetObjectField(TEXT("ScaleY"), MakeStringProperty(TEXT("Optional actor Y scale. Defaults to 1.")));
	EditorPlaceActorProps->SetObjectField(TEXT("ScaleZ"), MakeStringProperty(TEXT("Optional actor Z scale. Defaults to 1.")));
	EditorPlaceActorSchema->SetObjectField(TEXT("properties"), EditorPlaceActorProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("editor_place_actor"),
		TEXT("Place a Blueprint or native actor into the current editor world with explicit opt-in and Druids repro tags."),
		EditorPlaceActorSchema)));

	TSharedPtr<FJsonObject> EditorSetActorTransformSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> EditorSetActorTransformProps = MakeShared<FJsonObject>();
	EditorSetActorTransformProps->SetObjectField(TEXT("ActorNameOrLabel"), MakeStringProperty(TEXT("Actor name, label, or path from editor_read_level_actors/editor_place_actor.")));
	EditorSetActorTransformProps->SetObjectField(TEXT("LocationX"), MakeStringProperty(TEXT("Optional actor X location.")));
	EditorSetActorTransformProps->SetObjectField(TEXT("LocationY"), MakeStringProperty(TEXT("Optional actor Y location.")));
	EditorSetActorTransformProps->SetObjectField(TEXT("LocationZ"), MakeStringProperty(TEXT("Optional actor Z location.")));
	EditorSetActorTransformProps->SetObjectField(TEXT("RotationPitch"), MakeStringProperty(TEXT("Optional actor pitch rotation.")));
	EditorSetActorTransformProps->SetObjectField(TEXT("RotationYaw"), MakeStringProperty(TEXT("Optional actor yaw rotation.")));
	EditorSetActorTransformProps->SetObjectField(TEXT("RotationRoll"), MakeStringProperty(TEXT("Optional actor roll rotation.")));
	EditorSetActorTransformProps->SetObjectField(TEXT("ScaleX"), MakeStringProperty(TEXT("Optional actor X scale.")));
	EditorSetActorTransformProps->SetObjectField(TEXT("ScaleY"), MakeStringProperty(TEXT("Optional actor Y scale.")));
	EditorSetActorTransformProps->SetObjectField(TEXT("ScaleZ"), MakeStringProperty(TEXT("Optional actor Z scale.")));
	EditorSetActorTransformSchema->SetObjectField(TEXT("properties"), EditorSetActorTransformProps);
	EditorSetActorTransformSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("ActorNameOrLabel")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("editor_set_actor_transform"),
		TEXT("Set transform fields on an existing editor actor by name, label, or path and return updated bounds/readback."),
		EditorSetActorTransformSchema)));

	TSharedPtr<FJsonObject> EditorSetComponentTransformSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> EditorSetComponentTransformProps = MakeShared<FJsonObject>();
	EditorSetComponentTransformProps->SetObjectField(TEXT("ActorNameOrLabel"), MakeStringProperty(TEXT("Actor name, label, or path from editor_read_level_actors/editor_place_actor.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("ComponentName"), MakeStringProperty(TEXT("Actor component name to update, for example Canopy.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("LocationX"), MakeStringProperty(TEXT("Optional relative component X location.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("LocationY"), MakeStringProperty(TEXT("Optional relative component Y location.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("LocationZ"), MakeStringProperty(TEXT("Optional relative component Z location.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("RotationPitch"), MakeStringProperty(TEXT("Optional relative component pitch rotation.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("RotationYaw"), MakeStringProperty(TEXT("Optional relative component yaw rotation.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("RotationRoll"), MakeStringProperty(TEXT("Optional relative component roll rotation.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("ScaleX"), MakeStringProperty(TEXT("Optional relative component X scale.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("ScaleY"), MakeStringProperty(TEXT("Optional relative component Y scale.")));
	EditorSetComponentTransformProps->SetObjectField(TEXT("ScaleZ"), MakeStringProperty(TEXT("Optional relative component Z scale.")));
	EditorSetComponentTransformSchema->SetObjectField(TEXT("properties"), EditorSetComponentTransformProps);
	EditorSetComponentTransformSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("ActorNameOrLabel")),
		MakeShared<FJsonValueString>(TEXT("ComponentName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("editor_set_component_transform"),
		TEXT("Set relative transform fields on an editor actor component and return updated actor/component bounds for visual adjustment loops."),
		EditorSetComponentTransformSchema)));

	TSharedPtr<FJsonObject> EditorReadLevelActorsSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> EditorReadLevelActorsProps = MakeShared<FJsonObject>();
	EditorReadLevelActorsProps->SetObjectField(TEXT("Filter"), MakeStringProperty(TEXT("Optional substring filter matched against actor name, label, path, class, or tags.")));
	EditorReadLevelActorsProps->SetObjectField(TEXT("ReproOnly"), MakeStringProperty(TEXT("Optional bool. When true, only actors tagged DruidsRepro/DruidsMCPBridge are returned.")));
	EditorReadLevelActorsProps->SetObjectField(TEXT("Limit"), MakeStringProperty(TEXT("Maximum actors to return. Defaults to 100.")));
	EditorReadLevelActorsSchema->SetObjectField(TEXT("properties"), EditorReadLevelActorsProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("editor_read_level_actors"),
		TEXT("Read editor-world actors with labels, paths, classes, transforms, bounds, tags, and selection state."),
		EditorReadLevelActorsSchema)));

	TSharedPtr<FJsonObject> EditorSelectActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> EditorSelectActorProps = MakeShared<FJsonObject>();
	EditorSelectActorProps->SetObjectField(TEXT("ActorNameOrLabel"), MakeStringProperty(TEXT("Actor name, label, or path to select.")));
	EditorSelectActorProps->SetObjectField(TEXT("ClearSelection"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	EditorSelectActorSchema->SetObjectField(TEXT("properties"), EditorSelectActorProps);
	EditorSelectActorSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("ActorNameOrLabel")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("editor_select_actor"),
		TEXT("Select an editor actor by name, label, or path and return selected actor readback."),
		EditorSelectActorSchema)));

	TSharedPtr<FJsonObject> EditorCleanupReproActorsSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> EditorCleanupReproActorsProps = MakeShared<FJsonObject>();
	EditorCleanupReproActorsProps->SetObjectField(TEXT("Filter"), MakeStringProperty(TEXT("Optional substring filter. Only repro-tagged actors matching this filter are deleted.")));
	EditorCleanupReproActorsProps->SetObjectField(TEXT("DryRun"), MakeStringProperty(TEXT("Optional bool. When true, report matching repro actors without deleting. Defaults to false.")));
	EditorCleanupReproActorsSchema->SetObjectField(TEXT("properties"), EditorCleanupReproActorsProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("editor_cleanup_repro_actors"),
		TEXT("Delete only Druids repro-tagged editor actors, optionally filtered, for deterministic cleanup after placement tests."),
		EditorCleanupReproActorsSchema)));

	TSharedPtr<FJsonObject> EditorCaptureViewportSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> EditorCaptureViewportProps = MakeShared<FJsonObject>();
	EditorCaptureViewportProps->SetObjectField(TEXT("ActorNameOrLabel"), MakeStringProperty(TEXT("Optional actor name, label, or path to focus/capture. Defaults to selected actors.")));
	EditorCaptureViewportProps->SetObjectField(TEXT("Focus"), MakeStringProperty(TEXT("Optional bool. When true, focuses the active editor viewport on target actors before capture. Defaults to true.")));
	EditorCaptureViewportProps->SetObjectField(TEXT("FilenamePrefix"), MakeStringProperty(TEXT("Optional PNG filename prefix. Defaults to DruidsViewport.")));
	EditorCaptureViewportSchema->SetObjectField(TEXT("properties"), EditorCaptureViewportProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("editor_capture_viewport"),
		TEXT("Capture the active editor viewport to PNG and return screenshot path plus target actor/component bounds for visual QA."),
		EditorCaptureViewportSchema)));

	TSharedPtr<FJsonObject> BlueprintCreateRoadActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> BlueprintCreateRoadActorProps = MakeShared<FJsonObject>();
	BlueprintCreateRoadActorProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Content folder path. Defaults to /Game/DruidsRepro/CityPCG.")));
	BlueprintCreateRoadActorProps->SetObjectField(TEXT("AssetName"), MakeStringProperty(TEXT("Road Blueprint asset name. Defaults to BP_Druids_RoadSegment.")));
	BlueprintCreateRoadActorProps->SetObjectField(TEXT("Length"), MakeStringProperty(TEXT("Road segment length in Unreal units. Defaults to 400.")));
	BlueprintCreateRoadActorProps->SetObjectField(TEXT("Width"), MakeStringProperty(TEXT("Road segment width in Unreal units. Defaults to 180.")));
	BlueprintCreateRoadActorProps->SetObjectField(TEXT("Height"), MakeStringProperty(TEXT("Road segment height in Unreal units. Defaults to 20.")));
	BlueprintCreateRoadActorProps->SetObjectField(TEXT("MaterialPath"), MakeStringProperty(TEXT("Optional material asset path for the road slab.")));
	BlueprintCreateRoadActorSchema->SetObjectField(TEXT("properties"), BlueprintCreateRoadActorProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_create_road_actor"),
		TEXT("Create a deterministic primitive road segment Actor Blueprint from a cube slab for PCG road generation tests."),
		BlueprintCreateRoadActorSchema)));

	TSharedPtr<FJsonObject> BlueprintCreateIntersectionActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> BlueprintCreateIntersectionActorProps = MakeShared<FJsonObject>();
	BlueprintCreateIntersectionActorProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Content folder path. Defaults to /Game/DruidsRepro/CityPCG.")));
	BlueprintCreateIntersectionActorProps->SetObjectField(TEXT("AssetName"), MakeStringProperty(TEXT("Intersection Blueprint asset name. Defaults to BP_Druids_Intersection.")));
	BlueprintCreateIntersectionActorProps->SetObjectField(TEXT("Size"), MakeStringProperty(TEXT("Intersection square size in Unreal units. Defaults to 220.")));
	BlueprintCreateIntersectionActorProps->SetObjectField(TEXT("Height"), MakeStringProperty(TEXT("Intersection slab height in Unreal units. Defaults to 22.")));
	BlueprintCreateIntersectionActorProps->SetObjectField(TEXT("MaterialPath"), MakeStringProperty(TEXT("Optional material asset path for the intersection slab.")));
	BlueprintCreateIntersectionActorSchema->SetObjectField(TEXT("properties"), BlueprintCreateIntersectionActorProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_create_intersection_actor"),
		TEXT("Create a deterministic primitive road intersection Actor Blueprint from a square cube slab for PCG city network tests."),
		BlueprintCreateIntersectionActorSchema)));

	TSharedPtr<FJsonObject> BlueprintCreateSkyscraperActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> BlueprintCreateSkyscraperActorProps = MakeShared<FJsonObject>();
	BlueprintCreateSkyscraperActorProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Content folder path. Defaults to /Game/DruidsRepro/CityPCG.")));
	BlueprintCreateSkyscraperActorProps->SetObjectField(TEXT("AssetName"), MakeStringProperty(TEXT("Skyscraper Blueprint asset name. Defaults to BP_Druids_Skyscraper.")));
	BlueprintCreateSkyscraperActorProps->SetObjectField(TEXT("Width"), MakeStringProperty(TEXT("Building footprint X size in Unreal units. Defaults to 160.")));
	BlueprintCreateSkyscraperActorProps->SetObjectField(TEXT("Depth"), MakeStringProperty(TEXT("Building footprint Y size in Unreal units. Defaults to 160.")));
	BlueprintCreateSkyscraperActorProps->SetObjectField(TEXT("Height"), MakeStringProperty(TEXT("Building height in Unreal units. Defaults to 700.")));
	BlueprintCreateSkyscraperActorProps->SetObjectField(TEXT("MaterialPath"), MakeStringProperty(TEXT("Optional material asset path for the building mass.")));
	BlueprintCreateSkyscraperActorSchema->SetObjectField(TEXT("properties"), BlueprintCreateSkyscraperActorProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_create_skyscraper_actor"),
		TEXT("Create a deterministic primitive skyscraper Actor Blueprint from a cube mass for PCG city layout tests."),
		BlueprintCreateSkyscraperActorSchema)));

	TSharedPtr<FJsonObject> PCGStatusSchema = MakeObjectSchema();
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_get_status"),
		TEXT("Read-only PCG availability check: installed/enabled plugin state, module presence, supported engine version, and current bridge PCG mutation boundaries."),
		PCGStatusSchema)));

	TSharedPtr<FJsonObject> PCGRoadCapabilitiesSchema = MakeObjectSchema();
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_get_road_capabilities"),
		TEXT("Read-only road/city PCG capability manifest: reports whether grid-spawned road segments, intersections, lot exclusion, and spline road sampling are supported, planned, unverified, or unsupported."),
		PCGRoadCapabilitiesSchema)));

	TSharedPtr<FJsonObject> PCGCreateSplineBoundsActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGCreateSplineBoundsActorProps = MakeShared<FJsonObject>();
	PCGCreateSplineBoundsActorProps->SetObjectField(TEXT("ActorName"), MakeStringProperty(TEXT("Optional spline bounds actor label/name. Defaults to Druids_PCG_Spline_Bounds.")));
	PCGCreateSplineBoundsActorProps->SetObjectField(TEXT("AllowCurrentLevel"), MakeStringProperty(TEXT("Required true because this creates an editor-world spline actor.")));
	PCGCreateSplineBoundsActorProps->SetObjectField(TEXT("LocationX"), MakeStringProperty(TEXT("Optional actor X location. Defaults to 0.")));
	PCGCreateSplineBoundsActorProps->SetObjectField(TEXT("LocationY"), MakeStringProperty(TEXT("Optional actor Y location. Defaults to 0.")));
	PCGCreateSplineBoundsActorProps->SetObjectField(TEXT("LocationZ"), MakeStringProperty(TEXT("Optional actor Z location. Defaults to 0.")));
	PCGCreateSplineBoundsActorProps->SetObjectField(TEXT("Width"), MakeStringProperty(TEXT("Spline rectangle width in Unreal units. Defaults to 1200.")));
	PCGCreateSplineBoundsActorProps->SetObjectField(TEXT("Depth"), MakeStringProperty(TEXT("Spline rectangle depth in Unreal units. Defaults to 800.")));
	PCGCreateSplineBoundsActorProps->SetObjectField(TEXT("ClosedLoop"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	PCGCreateSplineBoundsActorSchema->SetObjectField(TEXT("properties"), PCGCreateSplineBoundsActorProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_create_spline_bounds_actor"),
		TEXT("Create a visible editor-world spline bounds actor for future PCG shape workflows. This does not enable spline-shaped PCG generation yet."),
		PCGCreateSplineBoundsActorSchema)));

	TSharedPtr<FJsonObject> PCGInspectSplineBoundsActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGInspectSplineBoundsActorProps = MakeShared<FJsonObject>();
	PCGInspectSplineBoundsActorProps->SetObjectField(TEXT("ActorNameOrLabel"), MakeStringProperty(TEXT("Spline bounds actor name, label, or path.")));
	PCGInspectSplineBoundsActorSchema->SetObjectField(TEXT("properties"), PCGInspectSplineBoundsActorProps);
	PCGInspectSplineBoundsActorSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("ActorNameOrLabel")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_inspect_spline_bounds_actor"),
		TEXT("Inspect a Druids spline bounds scaffold actor and return spline points, length, and bounds. This is read-only and does not imply spline PCG generation support."),
		PCGInspectSplineBoundsActorSchema)));

	TSharedPtr<FJsonObject> PCGSetSplineBoundsActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGSetSplineBoundsActorProps = MakeShared<FJsonObject>();
	PCGSetSplineBoundsActorProps->SetObjectField(TEXT("ActorNameOrLabel"), MakeStringProperty(TEXT("Spline bounds actor name, label, or path.")));
	PCGSetSplineBoundsActorProps->SetObjectField(TEXT("Width"), MakeStringProperty(TEXT("New spline rectangle width in Unreal units.")));
	PCGSetSplineBoundsActorProps->SetObjectField(TEXT("Depth"), MakeStringProperty(TEXT("New spline rectangle depth in Unreal units.")));
	PCGSetSplineBoundsActorProps->SetObjectField(TEXT("ClosedLoop"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	PCGSetSplineBoundsActorProps->SetObjectField(TEXT("LocationX"), MakeStringProperty(TEXT("Optional actor X location. Defaults to existing location.")));
	PCGSetSplineBoundsActorProps->SetObjectField(TEXT("LocationY"), MakeStringProperty(TEXT("Optional actor Y location. Defaults to existing location.")));
	PCGSetSplineBoundsActorProps->SetObjectField(TEXT("LocationZ"), MakeStringProperty(TEXT("Optional actor Z location. Defaults to existing location.")));
	PCGSetSplineBoundsActorSchema->SetObjectField(TEXT("properties"), PCGSetSplineBoundsActorProps);
	PCGSetSplineBoundsActorSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("ActorNameOrLabel")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_set_spline_bounds_actor"),
		TEXT("Resize or move an existing Druids spline bounds scaffold actor. This updates editor-visible spline geometry only; PCG spline generation remains unsupported."),
		PCGSetSplineBoundsActorSchema)));

	TSharedPtr<FJsonObject> PCGCreateForestReproSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGCreateForestReproProps = MakeShared<FJsonObject>();
	PCGCreateForestReproProps->SetObjectField(TEXT("TreeBlueprintPath"), MakeStringProperty(TEXT("Tree Blueprint path to scatter, for example /Game/DruidsRepro/VisualPCG/BP_Druids_Tree.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Optional repro content path. Defaults to /Game/DruidsRepro/VisualPCG.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("ForestName"), MakeStringProperty(TEXT("Optional forest repro name. Defaults to BP_Druids_PCG_Forest.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("GraphMode"), MakeStringProperty(TEXT("Optional graph mode. Defaults to Passthrough. Reroute creates one middle reroute node. Grid creates one deterministic Create Points Grid source node wired to output. SpawnActor wires grid points into a native PCG Spawn Actor node targeting TreeBlueprintPath.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("Seed"), MakeStringProperty(TEXT("Deterministic seed. Defaults to 1337.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("TargetCount"), MakeStringProperty(TEXT("Target tree count. Defaults to 24.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("AreaX"), MakeStringProperty(TEXT("Forest area X size. Defaults to 1600.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("AreaY"), MakeStringProperty(TEXT("Forest area Y size. Defaults to 1600.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("BoundsShape"), MakeStringProperty(TEXT("Optional bounds shape. Rectangle/Rect/Box are supported. Spline and arbitrary volume shapes return structured unsupported results in this slice.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("BoundsHeight"), MakeStringProperty(TEXT("Visible rectangular PCG volume height. Defaults to 400; grid generation remains a single layer.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("TreeSpacing"), MakeStringProperty(TEXT("Tree spacing for BoundsSpawnActor graph mode. Defaults to 200.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("SpacingMode"), MakeStringProperty(TEXT("Optional spacing mode. Use TargetCount to derive TreeSpacing from AreaX, AreaY, and TargetCount; otherwise explicit/default TreeSpacing is used.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("VariationMode"), MakeStringProperty(TEXT("Optional variation mode. Use TransformPoints or Natural to insert a PCG Transform Points node before Spawn Actor.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("PositionJitter"), MakeStringProperty(TEXT("Optional XY random position jitter in Unreal units for VariationMode=TransformPoints.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("MinScale"), MakeStringProperty(TEXT("Optional minimum uniform scale for VariationMode=TransformPoints. Defaults to 1.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("MaxScale"), MakeStringProperty(TEXT("Optional maximum uniform scale for VariationMode=TransformPoints. Defaults to MinScale.")));
	PCGCreateForestReproProps->SetObjectField(TEXT("YawJitter"), MakeStringProperty(TEXT("Optional symmetric yaw jitter in degrees for VariationMode=TransformPoints.")));
	PCGCreateForestReproSchema->SetObjectField(TEXT("properties"), PCGCreateForestReproProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_create_forest_repro"),
		TEXT("Create or validate the deterministic PCG forest repro request. Returns structured unavailable when PCG is disabled; does not fake generation."),
		PCGCreateForestReproSchema)));

	TSharedPtr<FJsonObject> PCGPlaceForestActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGPlaceForestActorProps = MakeShared<FJsonObject>();
	PCGPlaceForestActorProps->SetObjectField(TEXT("ForestName"), MakeStringProperty(TEXT("Forest actor label/name. Defaults to Druids_PCG_Forest.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("TreeBlueprintPath"), MakeStringProperty(TEXT("Tree Blueprint path to scatter.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("GraphPath"), MakeStringProperty(TEXT("Optional PCG graph package or object path created by pcg_create_forest_repro.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("AllowCurrentLevel"), MakeStringProperty(TEXT("Required true when placement is supported because this mutates the current editor world.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("LocationX"), MakeStringProperty(TEXT("Optional actor X location. Defaults to 0.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("LocationY"), MakeStringProperty(TEXT("Optional actor Y location. Defaults to 0.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("LocationZ"), MakeStringProperty(TEXT("Optional actor Z location. Defaults to 0.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("Seed"), MakeStringProperty(TEXT("Deterministic seed. Defaults to 1337.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("TargetCount"), MakeStringProperty(TEXT("Target tree count. Defaults to 24.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("AreaX"), MakeStringProperty(TEXT("Forest area X size. Defaults to 1600.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("AreaY"), MakeStringProperty(TEXT("Forest area Y size. Defaults to 1600.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("BoundsShape"), MakeStringProperty(TEXT("Optional bounds shape. Rectangle/Rect/Box are supported. Spline and arbitrary volume shapes return structured unsupported results in this slice.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("BoundsHeight"), MakeStringProperty(TEXT("Visible rectangular PCG volume height. Defaults to 400; grid generation remains a single layer.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("TreeSpacing"), MakeStringProperty(TEXT("Tree spacing for bounds-driven forest graphs. Defaults to 200.")));
	PCGPlaceForestActorProps->SetObjectField(TEXT("SpacingMode"), MakeStringProperty(TEXT("Optional spacing mode. Use TargetCount to derive TreeSpacing from AreaX, AreaY, and TargetCount; otherwise explicit/default TreeSpacing is used.")));
	PCGPlaceForestActorSchema->SetObjectField(TEXT("properties"), PCGPlaceForestActorProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_place_forest_actor"),
		TEXT("Place a deterministic PCG forest repro actor when PCG is enabled; returns structured unavailable when disabled."),
		PCGPlaceForestActorSchema)));

	TSharedPtr<FJsonObject> PCGInspectGraphSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGInspectGraphProps = MakeShared<FJsonObject>();
	PCGInspectGraphProps->SetObjectField(TEXT("GraphPath"), MakeStringProperty(TEXT("PCG graph package or object path to inspect.")));
	PCGInspectGraphSchema->SetObjectField(TEXT("properties"), PCGInspectGraphProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_inspect_graph"),
		TEXT("Inspect a minimal PCG graph asset without requiring compile-time PCG module dependency."),
		PCGInspectGraphSchema)));

	TSharedPtr<FJsonObject> PCGSetForestBoundsSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGSetForestBoundsProps = MakeShared<FJsonObject>();
	PCGSetForestBoundsProps->SetObjectField(TEXT("ForestActorNameOrLabel"), MakeStringProperty(TEXT("Forest actor name, label, or path with an attached Druids PCG graph.")));
	PCGSetForestBoundsProps->SetObjectField(TEXT("GraphPath"), MakeStringProperty(TEXT("Optional PCG graph package or object path. Defaults to the actor's attached PCG graph when available.")));
	PCGSetForestBoundsProps->SetObjectField(TEXT("AreaX"), MakeStringProperty(TEXT("New forest area X size.")));
	PCGSetForestBoundsProps->SetObjectField(TEXT("AreaY"), MakeStringProperty(TEXT("New forest area Y size.")));
	PCGSetForestBoundsProps->SetObjectField(TEXT("BoundsShape"), MakeStringProperty(TEXT("Optional bounds shape. Rectangle/Rect/Box are supported. Spline and arbitrary volume shapes return structured unsupported results in this slice.")));
	PCGSetForestBoundsProps->SetObjectField(TEXT("BoundsHeight"), MakeStringProperty(TEXT("Visible rectangular PCG volume height. Defaults to 400; grid generation remains a single layer.")));
	PCGSetForestBoundsProps->SetObjectField(TEXT("TreeSpacing"), MakeStringProperty(TEXT("New tree spacing. Defaults to 200.")));
	PCGSetForestBoundsProps->SetObjectField(TEXT("SpacingMode"), MakeStringProperty(TEXT("Optional spacing mode. Use TargetCount to derive TreeSpacing from AreaX, AreaY, and TargetCount; otherwise explicit/default TreeSpacing is used.")));
	PCGSetForestBoundsProps->SetObjectField(TEXT("TargetCount"), MakeStringProperty(TEXT("Target tree count used when SpacingMode=TargetCount. Defaults to 24.")));
	PCGSetForestBoundsSchema->SetObjectField(TEXT("properties"), PCGSetForestBoundsProps);
	PCGSetForestBoundsSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("ForestActorNameOrLabel")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_set_forest_bounds"),
		TEXT("Update a bounds-driven Druids PCG forest graph by changing area and tree spacing, then return expected generated count before regeneration."),
		PCGSetForestBoundsSchema)));

	TSharedPtr<FJsonObject> PCGGenerateSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGGenerateProps = MakeShared<FJsonObject>();
	PCGGenerateProps->SetObjectField(TEXT("ForestActorNameOrLabel"), MakeStringProperty(TEXT("Forest actor name, label, or path.")));
	PCGGenerateSchema->SetObjectField(TEXT("properties"), PCGGenerateProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_generate"),
		TEXT("Generate a supported PCG forest repro actor. Slice 59 returns unavailable until PCG is enabled and a graph-backed repro exists."),
		PCGGenerateSchema)));

	TSharedPtr<FJsonObject> PCGCreateVisualOutputSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGCreateVisualOutputProps = MakeShared<FJsonObject>();
	PCGCreateVisualOutputProps->SetObjectField(TEXT("ForestActorNameOrLabel"), MakeStringProperty(TEXT("Forest actor name, label, or path. Used as the visual output origin.")));
	PCGCreateVisualOutputProps->SetObjectField(TEXT("TreeBlueprintPath"), MakeStringProperty(TEXT("Actor Blueprint path to spawn at deterministic grid point positions.")));
	PCGCreateVisualOutputProps->SetObjectField(TEXT("AllowCurrentLevel"), MakeStringProperty(TEXT("Required true because this creates visible repro actors in the current editor world.")));
	PCGCreateVisualOutputProps->SetObjectField(TEXT("CleanupExisting"), MakeStringProperty(TEXT("Optional bool. Defaults true; removes previous Druids visual output actors for this forest before spawning.")));
	PCGCreateVisualOutputProps->SetObjectField(TEXT("Spacing"), MakeStringProperty(TEXT("Optional spacing override. Defaults to 200 to match the Slice 62 grid cell size.")));
	PCGCreateVisualOutputSchema->SetObjectField(TEXT("properties"), PCGCreateVisualOutputProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_create_visual_repro_output"),
		TEXT("Create visible tree actors from the deterministic PCG grid repro contract. This is bridge-created visual repro output, not native PCG SpawnActor graph generation."),
		PCGCreateVisualOutputSchema)));

	TSharedPtr<FJsonObject> PCGInspectGeneratedSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGInspectGeneratedProps = MakeShared<FJsonObject>();
	PCGInspectGeneratedProps->SetObjectField(TEXT("ForestActorNameOrLabel"), MakeStringProperty(TEXT("Forest actor name, label, or path.")));
	PCGInspectGeneratedSchema->SetObjectField(TEXT("properties"), PCGInspectGeneratedProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_inspect_generated"),
		TEXT("Inspect deterministic PCG forest repro metadata and generated counts when available; returns structured unavailable when PCG is disabled."),
		PCGInspectGeneratedSchema)));

	TSharedPtr<FJsonObject> PCGRunForestRecipeSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGRunForestRecipeProps = MakeShared<FJsonObject>();
	PCGRunForestRecipeProps->SetObjectField(TEXT("TreeName"), MakeStringProperty(TEXT("Optional tree Blueprint asset name. Defaults to BP_Druids_Tree.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("TreeBlueprintPath"), MakeStringProperty(TEXT("Optional existing tree Blueprint path. When omitted, the recipe creates/reuses TreeName under PackagePath.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("ForestName"), MakeStringProperty(TEXT("Optional forest actor/graph name. Defaults to Druids_Forest.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Optional repro content path. Defaults to /Game/DruidsRepro/VisualPCG.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("BoundsShape"), MakeStringProperty(TEXT("Rectangle or SplineRectangle. SplineRectangle uses a visible rectangular spline scaffold as the bounds source.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("SplineBoundsActor"), MakeStringProperty(TEXT("Optional spline bounds actor name/label for BoundsShape=SplineRectangle.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("TreeStyle"), MakeStringProperty(TEXT("RoundCanopy, TallPine, or WideCanopy. Defaults to RoundCanopy.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("AreaX"), MakeStringProperty(TEXT("Forest area X size. Defaults to 1600.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("AreaY"), MakeStringProperty(TEXT("Forest area Y size. Defaults to 1600.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("TargetCount"), MakeStringProperty(TEXT("Target tree count. Defaults to 24.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("Seed"), MakeStringProperty(TEXT("Deterministic seed. Defaults to 1337.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("PositionJitter"), MakeStringProperty(TEXT("XY jitter in Unreal units. Defaults to 45.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("MinScale"), MakeStringProperty(TEXT("Minimum uniform instance scale. Defaults to 0.85.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("MaxScale"), MakeStringProperty(TEXT("Maximum uniform instance scale. Defaults to 1.25.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("YawRandomization"), MakeStringProperty(TEXT("Symmetric yaw randomization in degrees. Defaults to 20.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("CaptureViewport"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	PCGRunForestRecipeProps->SetObjectField(TEXT("CleanupExisting"), MakeStringProperty(TEXT("Optional bool. Defaults to true; removes repro actors matching ForestName before creating.")));
	PCGRunForestRecipeSchema->SetObjectField(TEXT("properties"), PCGRunForestRecipeProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_run_forest_recipe"),
		TEXT("One-call PCG forest workflow: create/reuse a primitive tree, create a deterministic bounds-driven PCG graph, place/generate/inspect the forest, and optionally capture the viewport."),
		PCGRunForestRecipeSchema)));

	TSharedPtr<FJsonObject> PCGRunRoadRecipeSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGRunRoadRecipeProps = MakeShared<FJsonObject>();
	PCGRunRoadRecipeProps->SetObjectField(TEXT("RoadName"), MakeStringProperty(TEXT("Optional road segment Blueprint asset name. Defaults to BP_Druids_RoadSegment.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("RoadBlueprintPath"), MakeStringProperty(TEXT("Optional existing road segment Blueprint path. When omitted, the recipe creates/reuses RoadName under PackagePath.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("RoadNetworkName"), MakeStringProperty(TEXT("Optional PCG road actor/graph name. Defaults to Druids_Roads.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Optional repro content path. Defaults to /Game/DruidsRepro/CityPCG.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("RoadLength"), MakeStringProperty(TEXT("Total straight road length in Unreal units. Defaults to 1600.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("RoadWidth"), MakeStringProperty(TEXT("Road segment width in Unreal units. Defaults to 180.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("SegmentLength"), MakeStringProperty(TEXT("Road segment length and grid spacing. Defaults to 400.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("SegmentCount"), MakeStringProperty(TEXT("Optional road segment count. Defaults to 5.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("Axis"), MakeStringProperty(TEXT("Road strip axis: X or Y. Defaults to X.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("Seed"), MakeStringProperty(TEXT("Deterministic seed. Defaults to 2024.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("CaptureViewport"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	PCGRunRoadRecipeProps->SetObjectField(TEXT("CleanupExisting"), MakeStringProperty(TEXT("Optional bool. Defaults to true; removes repro actors matching RoadNetworkName before creating.")));
	PCGRunRoadRecipeSchema->SetObjectField(TEXT("properties"), PCGRunRoadRecipeProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_run_road_recipe"),
		TEXT("One-call PCG road workflow: create/reuse a road segment Blueprint, create a straight-line native Spawn Actor PCG graph, place/generate/inspect road segments, and optionally capture the viewport."),
		PCGRunRoadRecipeSchema)));

	TSharedPtr<FJsonObject> PCGRunSplineRoadProofSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGRunSplineRoadProofProps = MakeShared<FJsonObject>();
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("ProofName"), MakeStringProperty(TEXT("Optional proof name. Defaults to Druids_SplineRoadProof.")));
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Optional repro content path. Defaults to /Game/DruidsRepro/CityPCG.")));
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("Width"), MakeStringProperty(TEXT("Initial spline rectangle width. Defaults to 1600.")));
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("Depth"), MakeStringProperty(TEXT("Spline rectangle depth. Defaults to 240.")));
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("ResizedWidth"), MakeStringProperty(TEXT("Second spline rectangle width used to prove adjustable output. Defaults to 2400.")));
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("RoadWidth"), MakeStringProperty(TEXT("Road segment width. Defaults to 180.")));
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("SegmentLength"), MakeStringProperty(TEXT("Road segment length/spacing. Defaults to 400.")));
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("Seed"), MakeStringProperty(TEXT("Deterministic seed. Defaults to 4096.")));
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("CaptureViewport"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	PCGRunSplineRoadProofProps->SetObjectField(TEXT("CleanupExisting"), MakeStringProperty(TEXT("Optional bool. Defaults to true; removes repro actors matching ProofName before creating.")));
	PCGRunSplineRoadProofSchema->SetObjectField(TEXT("properties"), PCGRunSplineRoadProofProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_run_spline_road_proof"),
		TEXT("Proof-gate for spline-adjusted roads. Creates a visible spline rectangle, derives straight PCG road strips from its bounds before/after resize, and clearly reports that true curved spline sampling is still unsupported."),
		PCGRunSplineRoadProofSchema)));

	TSharedPtr<FJsonObject> PCGRunCityLayoutRecipeSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGRunCityLayoutRecipeProps = MakeShared<FJsonObject>();
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("CityName"), MakeStringProperty(TEXT("Optional city layout name. Defaults to Druids_City.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Optional repro content path. Defaults to /Game/DruidsRepro/CityPCG.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("AreaX"), MakeStringProperty(TEXT("City area X size in Unreal units. Defaults to 2200.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("AreaY"), MakeStringProperty(TEXT("City area Y size in Unreal units. Defaults to 1600.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("RoadWidth"), MakeStringProperty(TEXT("Main road width in Unreal units. Defaults to 180.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("RoadSegmentLength"), MakeStringProperty(TEXT("Main road segment length. Defaults to 400.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("RoadSegmentCount"), MakeStringProperty(TEXT("Main road segment count. Defaults to 6.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("BuildingTargetCount"), MakeStringProperty(TEXT("Target skyscraper count. Defaults to 18.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("Seed"), MakeStringProperty(TEXT("Deterministic seed. Defaults to 3031.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("CaptureViewport"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	PCGRunCityLayoutRecipeProps->SetObjectField(TEXT("CleanupExisting"), MakeStringProperty(TEXT("Optional bool. Defaults to true; removes repro actors matching CityName before creating.")));
	PCGRunCityLayoutRecipeSchema->SetObjectField(TEXT("properties"), PCGRunCityLayoutRecipeProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_run_city_layout_recipe"),
		TEXT("One-call PCG city workflow: create a straight PCG road strip and a native PCG-spawned skyscraper grid, then inspect and optionally capture the layout."),
		PCGRunCityLayoutRecipeSchema)));

	TSharedPtr<FJsonObject> PCGUpdateCityLayoutRecipeSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGUpdateCityLayoutRecipeProps = MakeShared<FJsonObject>();
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("CityName"), MakeStringProperty(TEXT("Existing Druids repro city name to update. Defaults to Druids_City.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Optional repro content path. Defaults to /Game/DruidsRepro/CityPCG.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("AreaX"), MakeStringProperty(TEXT("Updated city area X size.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("AreaY"), MakeStringProperty(TEXT("Updated city area Y size.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("RoadWidth"), MakeStringProperty(TEXT("Updated main road width.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("RoadSegmentLength"), MakeStringProperty(TEXT("Updated road segment length.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("RoadSegmentCount"), MakeStringProperty(TEXT("Updated horizontal road segment count.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("VerticalRoadSegmentCount"), MakeStringProperty(TEXT("Updated vertical road segment count.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("BuildingTargetCount"), MakeStringProperty(TEXT("Updated target skyscraper count.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("RoadSetback"), MakeStringProperty(TEXT("Updated building lot setback from road corridors.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("Seed"), MakeStringProperty(TEXT("Updated deterministic seed.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("CaptureViewport"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	PCGUpdateCityLayoutRecipeProps->SetObjectField(TEXT("AllowCreateIfMissing"), MakeStringProperty(TEXT("Optional bool. Defaults false; when false, the update requires existing repro actors matching CityName.")));
	PCGUpdateCityLayoutRecipeSchema->SetObjectField(TEXT("properties"), PCGUpdateCityLayoutRecipeProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_update_city_layout_recipe"),
		TEXT("Update an existing Druids repro PCG city by recreating the proven city recipe with new area, road, density, and seed settings, returning before/after actor counts and a fresh capture."),
		PCGUpdateCityLayoutRecipeSchema)));

	TSharedPtr<FJsonObject> PCGUpdateForestRecipeSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGUpdateForestRecipeProps = MakeShared<FJsonObject>();
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("ForestActor"), MakeStringProperty(TEXT("Forest actor name, label, or path. ForestName is used when omitted.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("ForestName"), MakeStringProperty(TEXT("Forest actor label/name. Defaults to Druids_Forest.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("TreeBlueprintPath"), MakeStringProperty(TEXT("Tree Blueprint path to keep attached to the refreshed graph.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Optional repro content path. Defaults to /Game/DruidsRepro/VisualPCG.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("BoundsShape"), MakeStringProperty(TEXT("Rectangle or SplineRectangle. SplineRectangle derives area from a rectangular spline bounds actor.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("SplineBoundsActor"), MakeStringProperty(TEXT("Optional spline bounds actor name/label for BoundsShape=SplineRectangle.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("AreaX"), MakeStringProperty(TEXT("New forest area X size.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("AreaY"), MakeStringProperty(TEXT("New forest area Y size.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("TargetCount"), MakeStringProperty(TEXT("New target tree count.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("TreeSpacing"), MakeStringProperty(TEXT("Optional explicit tree spacing.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("Seed"), MakeStringProperty(TEXT("Deterministic seed. Defaults to 1337.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("PositionJitter"), MakeStringProperty(TEXT("XY jitter in Unreal units. Defaults to 45.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("MinScale"), MakeStringProperty(TEXT("Minimum uniform instance scale. Defaults to 0.85.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("MaxScale"), MakeStringProperty(TEXT("Maximum uniform instance scale. Defaults to 1.25.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("YawRandomization"), MakeStringProperty(TEXT("Symmetric yaw randomization in degrees. Defaults to 20.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("Regenerate"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("CaptureViewport"), MakeStringProperty(TEXT("Optional bool. Defaults to true.")));
	PCGUpdateForestRecipeProps->SetObjectField(TEXT("AllowCurrentLevelMutation"), MakeStringProperty(TEXT("Optional bool for non-repro forest actors. Defaults to false.")));
	PCGUpdateForestRecipeSchema->SetObjectField(TEXT("properties"), PCGUpdateForestRecipeProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_update_forest_recipe"),
		TEXT("Update an existing Druids PCG forest by refreshing its graph/bounds/variation, regenerating, inspecting, and optionally capturing the viewport."),
		PCGUpdateForestRecipeSchema)));

	TSharedPtr<FJsonObject> PCGMultiClassProofSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> PCGMultiClassProofProps = MakeShared<FJsonObject>();
	PCGMultiClassProofProps->SetObjectField(TEXT("ForestName"), MakeStringProperty(TEXT("Optional forest name for the proof result.")));
	PCGMultiClassProofProps->SetObjectField(TEXT("TreeBlueprintPaths"), MakeObjectProperty(TEXT("Optional array of tree Blueprint paths. Current slice reports structured unsupported unless true multi-class PCG spawning is proven.")));
	PCGMultiClassProofSchema->SetObjectField(TEXT("properties"), PCGMultiClassProofProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("pcg_multiclass_spawn_proof"),
		TEXT("Proof-gate for true multi-class PCG spawning. Returns structured unsupported until the narrow multi-class graph path is proven stable."),
		PCGMultiClassProofSchema)));

	TSharedPtr<FJsonObject> CreateLongearsReproSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> CreateLongearsReproProps = MakeShared<FJsonObject>();
	CreateLongearsReproProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Content folder path for generated repro assets. Defaults to /Game/DruidsRepro/LongearsHG.")));
	CreateLongearsReproProps->SetObjectField(TEXT("AssetPrefix"), MakeStringProperty(TEXT("Prefix for generated asset names. Defaults to DruidsHG.")));
	CreateLongearsReproSchema->SetObjectField(TEXT("properties"), CreateLongearsReproProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("unreal_create_longears_repro_assets"),
		TEXT("Create minimal dummy CameraAsset, CameraRigAsset, and StateTree assets for validating Longears H/G inspection readers."),
		CreateLongearsReproSchema)));

	TSharedPtr<FJsonObject> CreateMaterialReproSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> CreateMaterialReproProps = MakeShared<FJsonObject>();
	CreateMaterialReproProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Content folder path for generated material repro assets. Defaults to /Game/DruidsRepro/Materials.")));
	CreateMaterialReproProps->SetObjectField(TEXT("AssetPrefix"), MakeStringProperty(TEXT("Prefix for generated asset names. Defaults to DruidsMaterial.")));
	CreateMaterialReproProps->SetObjectField(TEXT("Stress"), MakeStringProperty(TEXT("Optional bool. When true, creates a larger deterministic stress material graph for inspection/search validation.")));
	CreateMaterialReproSchema->SetObjectField(TEXT("properties"), CreateMaterialReproProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("unreal_create_material_repro_assets"),
		TEXT("Create deterministic Material, Material Instance, and Material Function repro assets for validating material inspection/search readers."),
		CreateMaterialReproSchema)));

	TSharedPtr<FJsonObject> MaterialCreateSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> MaterialCreateProps = MakeShared<FJsonObject>();
	MaterialCreateProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Content folder path, for example /Game/DruidsRepro/MaterialAuthoring.")));
	MaterialCreateProps->SetObjectField(TEXT("AssetName"), MakeStringProperty(TEXT("New Material asset name.")));
	MaterialCreateSchema->SetObjectField(TEXT("properties"), MaterialCreateProps);
	MaterialCreateSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("PackagePath")),
		MakeShared<FJsonValueString>(TEXT("AssetName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("material_create"),
		TEXT("Create a blank Material asset for minimal allowlisted material graph authoring."),
		MaterialCreateSchema)));

	TSharedPtr<FJsonObject> MaterialFindExpressionClassesSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> MaterialFindExpressionClassesProps = MakeShared<FJsonObject>();
	MaterialFindExpressionClassesProps->SetObjectField(TEXT("Query"), MakeStringProperty(TEXT("Optional query matched against canonical class names, display names, and aliases.")));
	MaterialFindExpressionClassesProps->SetObjectField(TEXT("SupportedOnly"), MakeStringProperty(TEXT("Optional bool. When true, return only classes supported by material_add_expression. Defaults to false.")));
	MaterialFindExpressionClassesProps->SetObjectField(TEXT("Limit"), MakeStringProperty(TEXT("Maximum classes to return. Defaults to 50.")));
	MaterialFindExpressionClassesSchema->SetObjectField(TEXT("properties"), MaterialFindExpressionClassesProps);
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("material_find_expression_classes"),
		TEXT("Find canonical Material expression classes and report whether each is supported for current material authoring."),
		MaterialFindExpressionClassesSchema)));

	TSharedPtr<FJsonObject> MaterialResolveExpressionClassSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> MaterialResolveExpressionClassProps = MakeShared<FJsonObject>();
	MaterialResolveExpressionClassProps->SetObjectField(TEXT("Query"), MakeStringProperty(TEXT("Human expression query or canonical class name, for example lerp, panner, or MaterialExpressionLinearInterpolate.")));
	MaterialResolveExpressionClassProps->SetObjectField(TEXT("RequireSupported"), MakeStringProperty(TEXT("Optional bool. When true, unsupported classes cannot be returned as best supported candidates. Defaults to false.")));
	MaterialResolveExpressionClassSchema->SetObjectField(TEXT("properties"), MaterialResolveExpressionClassProps);
	MaterialResolveExpressionClassSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("Query")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("material_resolve_expression_class"),
		TEXT("Resolve a human material expression request to a canonical class with confidence and match reasons. Does not mutate materials."),
		MaterialResolveExpressionClassSchema)));

	TSharedPtr<FJsonObject> MaterialAddExpressionSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> MaterialAddExpressionProps = MakeShared<FJsonObject>();
	MaterialAddExpressionProps->SetObjectField(TEXT("MaterialNameOrPath"), MakeStringProperty(TEXT("Material asset name, keyword, full package path, or full object path.")));
	MaterialAddExpressionProps->SetObjectField(TEXT("ExpressionClass"), MakeStringProperty(TEXT("Canonical allowlisted class name, for example MaterialExpressionScalarParameter.")));
	MaterialAddExpressionProps->SetObjectField(TEXT("NodeName"), MakeStringProperty(TEXT("Optional stable object/node name for the expression.")));
	MaterialAddExpressionProps->SetObjectField(TEXT("ParameterName"), MakeStringProperty(TEXT("Optional parameter name for parameter expressions.")));
	MaterialAddExpressionProps->SetObjectField(TEXT("DefaultValue"), MakeStringProperty(TEXT("Optional default value import text for supported parameter defaults.")));
	MaterialAddExpressionProps->SetObjectField(TEXT("TexturePath"), MakeStringProperty(TEXT("Optional texture object path for texture parameter expressions.")));
	MaterialAddExpressionProps->SetObjectField(TEXT("PropertyValues"), MakeObjectProperty(TEXT("Optional object of exact reflected property names to import-text values, for example {\"SpeedX\":\"0.1\"}.")));
	MaterialAddExpressionProps->SetObjectField(TEXT("NodePosX"), MakeStringProperty(TEXT("Expression editor X position. Defaults to 0.")));
	MaterialAddExpressionProps->SetObjectField(TEXT("NodePosY"), MakeStringProperty(TEXT("Expression editor Y position. Defaults to 0.")));
	MaterialAddExpressionProps->SetObjectField(TEXT("Description"), MakeStringProperty(TEXT("Optional expression description/comment bubble text.")));
	MaterialAddExpressionSchema->SetObjectField(TEXT("properties"), MaterialAddExpressionProps);
	MaterialAddExpressionSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("MaterialNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("ExpressionClass"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("material_add_expression"),
		TEXT("Add an allowlisted canonical material expression to a Material asset."),
		MaterialAddExpressionSchema)));

	TSharedPtr<FJsonObject> MaterialSetExpressionPropertySchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> MaterialSetExpressionPropertyProps = MakeShared<FJsonObject>();
	MaterialSetExpressionPropertyProps->SetObjectField(TEXT("MaterialNameOrPath"), MakeStringProperty(TEXT("Material asset name, keyword, full package path, or full object path.")));
	MaterialSetExpressionPropertyProps->SetObjectField(TEXT("NodeGuid"), MakeStringProperty(TEXT("Expression node GUID from unreal_inspect_asset, material_add_expression, or material_update.")));
	MaterialSetExpressionPropertyProps->SetObjectField(TEXT("PropertyName"), MakeStringProperty(TEXT("Exact reflected property name, or supported alias such as Exponent for MaterialExpressionPower.")));
	MaterialSetExpressionPropertyProps->SetObjectField(TEXT("Value"), MakeStringProperty(TEXT("Import-text value to apply. JSON string, number, or bool values are accepted.")));
	MaterialSetExpressionPropertySchema->SetObjectField(TEXT("properties"), MaterialSetExpressionPropertyProps);
	MaterialSetExpressionPropertySchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("MaterialNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("NodeGuid")),
		MakeShared<FJsonValueString>(TEXT("PropertyName")),
		MakeShared<FJsonValueString>(TEXT("Value"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("material_set_expression_property"),
		TEXT("Set a reflected property on an existing material expression by stable node GUID. Supports small safe aliases such as Power Exponent to ConstExponent."),
		MaterialSetExpressionPropertySchema)));

	TSharedPtr<FJsonObject> MaterialConnectExpressionsSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> MaterialConnectExpressionsProps = MakeShared<FJsonObject>();
	MaterialConnectExpressionsProps->SetObjectField(TEXT("MaterialNameOrPath"), MakeStringProperty(TEXT("Material asset name, keyword, full package path, or full object path.")));
	MaterialConnectExpressionsProps->SetObjectField(TEXT("SourceNodeGuid"), MakeStringProperty(TEXT("Source expression node GUID from unreal_inspect_asset or material_add_expression.")));
	MaterialConnectExpressionsProps->SetObjectField(TEXT("TargetNodeGuid"), MakeStringProperty(TEXT("Target expression node GUID from unreal_inspect_asset or material_add_expression.")));
	MaterialConnectExpressionsProps->SetObjectField(TEXT("TargetInputName"), MakeStringProperty(TEXT("Exact target input name, for example A, B, Alpha, Input.")));
	MaterialConnectExpressionsProps->SetObjectField(TEXT("SourceOutputIndex"), MakeStringProperty(TEXT("Optional source output index. Defaults to 0.")));
	MaterialConnectExpressionsSchema->SetObjectField(TEXT("properties"), MaterialConnectExpressionsProps);
	MaterialConnectExpressionsSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("MaterialNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("SourceNodeGuid")),
		MakeShared<FJsonValueString>(TEXT("TargetNodeGuid")),
		MakeShared<FJsonValueString>(TEXT("TargetInputName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("material_connect_expressions"),
		TEXT("Connect one material expression output to another expression input by stable GUID and exact input name."),
		MaterialConnectExpressionsSchema)));

	TSharedPtr<FJsonObject> MaterialConnectRootSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> MaterialConnectRootProps = MakeShared<FJsonObject>();
	MaterialConnectRootProps->SetObjectField(TEXT("MaterialNameOrPath"), MakeStringProperty(TEXT("Material asset name, keyword, full package path, or full object path.")));
	MaterialConnectRootProps->SetObjectField(TEXT("SourceNodeGuid"), MakeStringProperty(TEXT("Source expression node GUID from unreal_inspect_asset or material_add_expression.")));
	MaterialConnectRootProps->SetObjectField(TEXT("RootInputName"), MakeStringProperty(TEXT("Root input name: BaseColor, Roughness, EmissiveColor, Normal, Metallic, Specular, Opacity.")));
	MaterialConnectRootProps->SetObjectField(TEXT("SourceOutputIndex"), MakeStringProperty(TEXT("Optional source output index. Defaults to 0.")));
	MaterialConnectRootSchema->SetObjectField(TEXT("properties"), MaterialConnectRootProps);
	MaterialConnectRootSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("MaterialNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("SourceNodeGuid")),
		MakeShared<FJsonValueString>(TEXT("RootInputName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("material_connect_root"),
		TEXT("Connect an expression output to an allowlisted Material root input."),
		MaterialConnectRootSchema)));

	TSharedPtr<FJsonObject> MaterialUpdateSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> MaterialUpdateProps = MakeShared<FJsonObject>();
	MaterialUpdateProps->SetObjectField(TEXT("MaterialNameOrPath"), MakeStringProperty(TEXT("Material asset name, keyword, full package path, or full object path.")));
	MaterialUpdateSchema->SetObjectField(TEXT("properties"), MaterialUpdateProps);
	MaterialUpdateSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("MaterialNameOrPath")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("material_update"),
		TEXT("Apply editor update/dirty behavior to a Material and return specialized material readback."),
		MaterialUpdateSchema)));

	TSharedPtr<FJsonObject> CreateBlueprintSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> CreateBlueprintProps = MakeShared<FJsonObject>();
	CreateBlueprintProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Content folder path, for example /Game/MyFolder.")));
	CreateBlueprintProps->SetObjectField(TEXT("AssetName"), MakeStringProperty(TEXT("New Blueprint asset name.")));
	CreateBlueprintProps->SetObjectField(TEXT("ParentClassPath"), MakeStringProperty(TEXT("Parent class path, for example /Script/Engine.Actor or /Script/Engine.Character.")));
	CreateBlueprintSchema->SetObjectField(TEXT("properties"), CreateBlueprintProps);
	CreateBlueprintSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("PackagePath")),
		MakeShared<FJsonValueString>(TEXT("AssetName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_create"),
		TEXT("Create a Blueprint asset from a native Unreal parent class."),
		CreateBlueprintSchema)));

	TSharedPtr<FJsonObject> CreatePrimitiveActorSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> CreatePrimitiveActorProps = MakeShared<FJsonObject>();
	CreatePrimitiveActorProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Content folder path, for example /Game/DruidsRepro/VisualPCG.")));
	CreatePrimitiveActorProps->SetObjectField(TEXT("AssetName"), MakeStringProperty(TEXT("New Blueprint actor asset name.")));
	CreatePrimitiveActorProps->SetObjectField(TEXT("Preset"), MakeStringProperty(TEXT("Primitive visual preset. Slice 54 supports Tree. Defaults to Tree.")));
	CreatePrimitiveActorProps->SetObjectField(TEXT("TrunkHeight"), MakeStringProperty(TEXT("Optional tree trunk height in Unreal units. Defaults to 220.")));
	CreatePrimitiveActorProps->SetObjectField(TEXT("TrunkRadius"), MakeStringProperty(TEXT("Optional tree trunk radius in Unreal units. Defaults to 24.")));
	CreatePrimitiveActorProps->SetObjectField(TEXT("CanopyRadius"), MakeStringProperty(TEXT("Optional tree canopy radius in Unreal units. Defaults to 85.")));
	CreatePrimitiveActorProps->SetObjectField(TEXT("CanopyCenterZ"), MakeStringProperty(TEXT("Optional tree canopy center Z in Unreal units. Defaults to trunk height plus 55.")));
	CreatePrimitiveActorProps->SetObjectField(TEXT("MaterialPath"), MakeStringProperty(TEXT("Optional material package/object path assigned to trunk and canopy.")));
	CreatePrimitiveActorSchema->SetObjectField(TEXT("properties"), CreatePrimitiveActorProps);
	CreatePrimitiveActorSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("PackagePath")),
		MakeShared<FJsonValueString>(TEXT("AssetName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_create_primitive_actor"),
		TEXT("Create a focused primitive visual Actor Blueprint preset, currently a tree made from cylinder trunk and sphere canopy components."),
		CreatePrimitiveActorSchema)));

	TSharedPtr<FJsonObject> CreateInterfaceSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> CreateInterfaceProps = MakeShared<FJsonObject>();
	CreateInterfaceProps->SetObjectField(TEXT("PackagePath"), MakeStringProperty(TEXT("Content folder path, for example /Game/MyFolder.")));
	CreateInterfaceProps->SetObjectField(TEXT("AssetName"), MakeStringProperty(TEXT("New Blueprint Interface asset name.")));
	CreateInterfaceSchema->SetObjectField(TEXT("properties"), CreateInterfaceProps);
	CreateInterfaceSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("PackagePath")),
		MakeShared<FJsonValueString>(TEXT("AssetName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_create_interface"),
		TEXT("Create a Blueprint Interface asset for interface implementation tests and workflows."),
		CreateInterfaceSchema)));

	TSharedPtr<FJsonObject> AddVariableSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> AddVariableProps = MakeShared<FJsonObject>();
	AddVariableProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	AddVariableProps->SetObjectField(TEXT("VarName"), MakeStringProperty(TEXT("Variable name to add.")));
	AddVariableProps->SetObjectField(TEXT("Type"), MakeStringProperty(TEXT("Variable type: bool, int, float, string, name, text, vector, rotator, transform, object, class, or arrays like float[] / array<float>.")));
	AddVariableProps->SetObjectField(TEXT("ObjectClassPath"), MakeStringProperty(TEXT("Optional class path for object/class variables, for example /Script/Engine.Actor. Defaults to Actor.")));
	AddVariableProps->SetObjectField(TEXT("DefaultValue"), MakeStringProperty(TEXT("Optional default value string.")));
	AddVariableProps->SetObjectField(TEXT("Category"), MakeStringProperty(TEXT("Optional variable category.")));
	AddVariableSchema->SetObjectField(TEXT("properties"), AddVariableProps);
	AddVariableSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("VarName")),
		MakeShared<FJsonValueString>(TEXT("Type"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_add_variable"),
		TEXT("Add a member variable to a Blueprint using native Unreal editor APIs."),
		AddVariableSchema)));

	TSharedPtr<FJsonObject> AddComponentSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> AddComponentProps = MakeShared<FJsonObject>();
	AddComponentProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	AddComponentProps->SetObjectField(TEXT("ComponentName"), MakeStringProperty(TEXT("Component variable name.")));
	AddComponentProps->SetObjectField(TEXT("ComponentClassPath"), MakeStringProperty(TEXT("Component class name/path, for example SpringArmComponent, CameraComponent, /Script/Engine.CameraComponent.")));
	AddComponentProps->SetObjectField(TEXT("ParentComponentName"), MakeStringProperty(TEXT("Optional parent component name.")));
	AddComponentProps->SetObjectField(TEXT("Properties"), MakeObjectProperty(TEXT("Optional property values keyed by property name.")));
	AddComponentProps->SetObjectField(TEXT("PrimitiveShape"), MakeStringProperty(TEXT("Optional built-in primitive mesh shape for StaticMeshComponent: Cube, Sphere, Cylinder, Cone, or Plane.")));
	AddComponentProps->SetObjectField(TEXT("StaticMeshPath"), MakeStringProperty(TEXT("Optional StaticMesh asset package/object path to assign to StaticMeshComponent.")));
	AddComponentProps->SetObjectField(TEXT("MaterialPath"), MakeStringProperty(TEXT("Optional Material or MaterialInstance asset package/object path to assign to mesh slot 0.")));
	AddComponentSchema->SetObjectField(TEXT("properties"), AddComponentProps);
	AddComponentSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("ComponentName")),
		MakeShared<FJsonValueString>(TEXT("ComponentClassPath"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_add_component"),
		TEXT("Add a component to a Blueprint's SimpleConstructionScript."),
		AddComponentSchema)));

	TSharedPtr<FJsonObject> AddComponentMotionSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> AddComponentMotionProps = MakeShared<FJsonObject>();
	AddComponentMotionProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	AddComponentMotionProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to edit. Defaults to EventGraph.")));
	AddComponentMotionProps->SetObjectField(TEXT("SourceNodeTitleOrGuid"), MakeStringProperty(TEXT("Source exec node title text or GUID, for example UnlockDoor.")));
	AddComponentMotionProps->SetObjectField(TEXT("SourcePinName"), MakeStringProperty(TEXT("Source exec output pin. Defaults to then.")));
	AddComponentMotionProps->SetObjectField(TEXT("ComponentName"), MakeStringProperty(TEXT("Blueprint-owned component name, for example Door.")));
	AddComponentMotionProps->SetObjectField(TEXT("Mode"), MakeStringProperty(TEXT("Motion mode: set_relative_location, add_local_offset, or add_relative_location.")));
	AddComponentMotionProps->SetObjectField(TEXT("Vector"), MakeObjectProperty(TEXT("Optional vector defaults keyed by X/Y/Z.")));
	AddComponentMotionProps->SetObjectField(TEXT("VariableToAxis"), MakeObjectProperty(TEXT("Optional mapping from axis X/Y/Z to Blueprint variable name, for example {\"Z\":\"DoorOpenHeight\"}.")));
	AddComponentMotionProps->SetObjectField(TEXT("NodePosX"), MakeStringProperty(TEXT("Optional base node X position.")));
	AddComponentMotionProps->SetObjectField(TEXT("NodePosY"), MakeStringProperty(TEXT("Optional base node Y position.")));
	AddComponentMotionSchema->SetObjectField(TEXT("properties"), AddComponentMotionProps);
	AddComponentMotionSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("SourceNodeTitleOrGuid")),
		MakeShared<FJsonValueString>(TEXT("ComponentName")),
		MakeShared<FJsonValueString>(TEXT("Mode"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_add_component_motion"),
		TEXT("Add an allowlisted component motion node chain and wire it to an existing exec source."),
		AddComponentMotionSchema)));

	TSharedPtr<FJsonObject> AddDynamicMaterialParameterSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> AddDynamicMaterialParameterProps = MakeShared<FJsonObject>();
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to edit. Defaults to EventGraph.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("SourceNodeTitleOrGuid"), MakeStringProperty(TEXT("Source exec node title text or GUID, for example UnlockDoor.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("SourcePinName"), MakeStringProperty(TEXT("Source exec output pin. Defaults to then.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("ComponentName"), MakeStringProperty(TEXT("Blueprint-owned primitive/mesh component name.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("MaterialIndex"), MakeStringProperty(TEXT("Material slot index. Defaults to 0.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("ParameterName"), MakeStringProperty(TEXT("Material scalar parameter name to update.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("ValueVariableName"), MakeStringProperty(TEXT("Optional Blueprint float variable to connect to Value.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("Value"), MakeStringProperty(TEXT("Optional literal scalar value used when ValueVariableName is empty.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("NodePosX"), MakeStringProperty(TEXT("Optional base node X position.")));
	AddDynamicMaterialParameterProps->SetObjectField(TEXT("NodePosY"), MakeStringProperty(TEXT("Optional base node Y position.")));
	AddDynamicMaterialParameterSchema->SetObjectField(TEXT("properties"), AddDynamicMaterialParameterProps);
	AddDynamicMaterialParameterSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("SourceNodeTitleOrGuid")),
		MakeShared<FJsonValueString>(TEXT("ComponentName")),
		MakeShared<FJsonValueString>(TEXT("ParameterName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_add_dynamic_material_parameter_update"),
		TEXT("Add a focused Create Dynamic Material Instance + Set Scalar Parameter Value chain for a Blueprint-owned component."),
		AddDynamicMaterialParameterSchema)));

	TSharedPtr<FJsonObject> CreateFunctionSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> CreateFunctionProps = MakeShared<FJsonObject>();
	CreateFunctionProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	CreateFunctionProps->SetObjectField(TEXT("FunctionName"), MakeStringProperty(TEXT("Function name to create.")));
	CreateFunctionProps->SetObjectField(TEXT("Inputs"), MakeObjectProperty(TEXT("Optional array of input descriptors with Name, Type, and optional ObjectClassPath.")));
	CreateFunctionProps->SetObjectField(TEXT("Outputs"), MakeObjectProperty(TEXT("Optional array of output descriptors with Name, Type, and optional ObjectClassPath.")));
	CreateFunctionSchema->SetObjectField(TEXT("properties"), CreateFunctionProps);
	CreateFunctionSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("FunctionName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_create_function"),
		TEXT("Create a Blueprint function graph with optional typed inputs and outputs."),
		CreateFunctionSchema)));

	TSharedPtr<FJsonObject> CreateEventGraphSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> CreateEventGraphProps = MakeShared<FJsonObject>();
	CreateEventGraphProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	CreateEventGraphProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Additional event graph name to create.")));
	CreateEventGraphSchema->SetObjectField(TEXT("properties"), CreateEventGraphProps);
	CreateEventGraphSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("GraphName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_create_event_graph"),
		TEXT("Create an additional Blueprint event graph page with an exact graph name."),
		CreateEventGraphSchema)));

	TSharedPtr<FJsonObject> ImplementInterfaceSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> ImplementInterfaceProps = MakeShared<FJsonObject>();
	ImplementInterfaceProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	ImplementInterfaceProps->SetObjectField(TEXT("InterfaceNameOrPath"), MakeStringProperty(TEXT("Blueprint Interface asset path/name or generated interface class path.")));
	ImplementInterfaceSchema->SetObjectField(TEXT("properties"), ImplementInterfaceProps);
	ImplementInterfaceSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("InterfaceNameOrPath"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_implement_interface"),
		TEXT("Add a Blueprint Interface to a Blueprint using native Unreal editor APIs."),
		ImplementInterfaceSchema)));

	TSharedPtr<FJsonObject> AddInterfaceEventSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> AddInterfaceEventProps = MakeShared<FJsonObject>();
	AddInterfaceEventProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	AddInterfaceEventProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to edit. Defaults to EventGraph.")));
	AddInterfaceEventProps->SetObjectField(TEXT("InterfaceNameOrPath"), MakeStringProperty(TEXT("Blueprint Interface asset path/name or generated interface class path.")));
	AddInterfaceEventProps->SetObjectField(TEXT("FunctionName"), MakeStringProperty(TEXT("Interface function/event name to implement as an event node.")));
	AddInterfaceEventProps->SetObjectField(TEXT("NodePosX"), MakeStringProperty(TEXT("Node X position. Defaults to 0.")));
	AddInterfaceEventProps->SetObjectField(TEXT("NodePosY"), MakeStringProperty(TEXT("Node Y position. Defaults to 0.")));
	AddInterfaceEventSchema->SetObjectField(TEXT("properties"), AddInterfaceEventProps);
	AddInterfaceEventSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("InterfaceNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("FunctionName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_add_interface_event"),
		TEXT("Add a Blueprint Interface implementation event node to a graph."),
		AddInterfaceEventSchema)));

	TSharedPtr<FJsonObject> AddCustomEventSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> AddCustomEventProps = MakeShared<FJsonObject>();
	AddCustomEventProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	AddCustomEventProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to edit. Defaults to EventGraph.")));
	AddCustomEventProps->SetObjectField(TEXT("EventName"), MakeStringProperty(TEXT("Custom event name to create.")));
	AddCustomEventProps->SetObjectField(TEXT("Parameters"), MakeObjectProperty(TEXT("Optional array of output parameter descriptors with Name, Type, and optional ObjectClassPath.")));
	AddCustomEventSchema->SetObjectField(TEXT("properties"), AddCustomEventProps);
	AddCustomEventSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("EventName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_add_custom_event"),
		TEXT("Add a custom event node with optional typed parameters using native Unreal editor APIs."),
		AddCustomEventSchema)));

	TSharedPtr<FJsonObject> AddCommentSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> AddCommentProps = MakeShared<FJsonObject>();
	AddCommentProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	AddCommentProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to edit. Defaults to EventGraph.")));
	AddCommentProps->SetObjectField(TEXT("CommentText"), MakeStringProperty(TEXT("Comment text to display.")));
	AddCommentProps->SetObjectField(TEXT("NodePosX"), MakeStringProperty(TEXT("Comment box X position. Defaults to 0.")));
	AddCommentProps->SetObjectField(TEXT("NodePosY"), MakeStringProperty(TEXT("Comment box Y position. Defaults to 0.")));
	AddCommentProps->SetObjectField(TEXT("Width"), MakeStringProperty(TEXT("Comment box width. Defaults to 400.")));
	AddCommentProps->SetObjectField(TEXT("Height"), MakeStringProperty(TEXT("Comment box height. Defaults to 220.")));
	AddCommentProps->SetObjectField(TEXT("FontSize"), MakeStringProperty(TEXT("Optional comment font size. Defaults to Unreal's comment-node default.")));
	AddCommentSchema->SetObjectField(TEXT("properties"), AddCommentProps);
	AddCommentSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("CommentText"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_add_comment"),
		TEXT("Add a Blueprint graph comment box with explicit bounds."),
		AddCommentSchema)));

	TSharedPtr<FJsonObject> BlueprintReadSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> BlueprintReadProps = MakeShared<FJsonObject>();
	BlueprintReadProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	BlueprintReadProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to read, for example EventGraph.")));
	BlueprintReadProps->SetObjectField(TEXT("Limit"), MakeStringProperty(TEXT("Maximum lines to return. Defaults to 1000.")));
	BlueprintReadSchema->SetObjectField(TEXT("properties"), BlueprintReadProps);
	BlueprintReadSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_read_graph"),
		TEXT("Resolve a Blueprint and read graph data using native Unreal graph APIs."),
		BlueprintReadSchema)));

	TSharedPtr<FJsonObject> BlueprintSearchProjectSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> BlueprintSearchProjectProps = MakeShared<FJsonObject>();
	BlueprintSearchProjectProps->SetObjectField(TEXT("Query"), MakeStringProperty(TEXT("Search query matched against Blueprint assets, graphs, nodes, comments, variables, functions, components, and metadata.")));
	BlueprintSearchProjectProps->SetObjectField(TEXT("Targets"), MakeObjectProperty(TEXT("Optional array of target types: assets, graphs, nodes, comments, variables, functions, components, metadata. Defaults to all.")));
	BlueprintSearchProjectProps->SetObjectField(TEXT("Intent"), MakeStringProperty(TEXT("Optional ranking intent: broad, find_example, find_comment, find_definition, find_usage, find_asset_reference, or system_overview. Defaults to broad.")));
	BlueprintSearchProjectProps->SetObjectField(TEXT("PackagePaths"), MakeObjectProperty(TEXT("Optional array of package paths to search. Defaults to /Game.")));
	BlueprintSearchProjectProps->SetObjectField(TEXT("Limit"), MakeStringProperty(TEXT("Maximum results to return. Defaults to 25.")));
	BlueprintSearchProjectProps->SetObjectField(TEXT("Refresh"), MakeStringProperty(TEXT("Rebuild the Blueprint search cache before searching. Defaults to false.")));
	BlueprintSearchProjectSchema->SetObjectField(TEXT("properties"), BlueprintSearchProjectProps);
	BlueprintSearchProjectSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("Query")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_search_project"),
		TEXT("Search Blueprint assets project-wide using native Unreal asset and graph metadata."),
		BlueprintSearchProjectSchema)));

	TSharedPtr<FJsonObject> BlueprintCompileSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> BlueprintCompileProps = MakeShared<FJsonObject>();
	BlueprintCompileProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	BlueprintCompileSchema->SetObjectField(TEXT("properties"), BlueprintCompileProps);
	BlueprintCompileSchema->SetArrayField(TEXT("required"), { MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")) });
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_compile"),
		TEXT("Compile a Blueprint and return structured compiler warnings, errors, and status."),
		BlueprintCompileSchema)));

	TSharedPtr<FJsonObject> FindSpawnerSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> FindSpawnerProps = MakeShared<FJsonObject>();
	FindSpawnerProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	FindSpawnerProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to search. Defaults to EventGraph.")));
	FindSpawnerProps->SetObjectField(TEXT("SearchQuery"), MakeStringProperty(TEXT("Node search text, for example Print String.")));
	FindSpawnerProps->SetObjectField(TEXT("InputType"), MakeStringProperty(TEXT("Optional input pin type filter.")));
	FindSpawnerProps->SetObjectField(TEXT("OutputType"), MakeStringProperty(TEXT("Optional output pin type filter.")));
	FindSpawnerSchema->SetObjectField(TEXT("properties"), FindSpawnerProps);
	FindSpawnerSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("SearchQuery"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_find_node_spawners"),
		TEXT("Resolve a Blueprint and search addable Blueprint node spawners."),
		FindSpawnerSchema)));

	TSharedPtr<FJsonObject> ResolveSpawnerSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> ResolveSpawnerProps = MakeShared<FJsonObject>();
	ResolveSpawnerProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	ResolveSpawnerProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to search. Defaults to EventGraph.")));
	ResolveSpawnerProps->SetObjectField(TEXT("Query"), MakeStringProperty(TEXT("Human request or node name, for example integer add or Print String.")));
	ResolveSpawnerProps->SetObjectField(TEXT("Intent"), MakeStringProperty(TEXT("Optional semantic intent, for example math_binary_op, conversion, event, variable_get, variable_set.")));
	ResolveSpawnerProps->SetObjectField(TEXT("Operator"), MakeStringProperty(TEXT("Optional operator such as add, subtract, multiply, divide.")));
	ResolveSpawnerProps->SetObjectField(TEXT("ValueType"), MakeStringProperty(TEXT("Optional value type such as int, float, bool, string.")));
	ResolveSpawnerProps->SetObjectField(TEXT("NodeCategory"), MakeStringProperty(TEXT("Optional preferred node category such as Math|Integer.")));
	ResolveSpawnerProps->SetObjectField(TEXT("InputType"), MakeStringProperty(TEXT("Optional input pin type filter.")));
	ResolveSpawnerProps->SetObjectField(TEXT("OutputType"), MakeStringProperty(TEXT("Optional output pin type filter.")));
	ResolveSpawnerSchema->SetObjectField(TEXT("properties"), ResolveSpawnerProps);
	ResolveSpawnerSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_resolve_node_spawner"),
		TEXT("Resolve a human node request to a ranked Blueprint spawner id using aliases, pin/category ranking, and native Kismet fallbacks."),
		ResolveSpawnerSchema)));

	TSharedPtr<FJsonObject> AddNodeSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> AddNodeProps = MakeShared<FJsonObject>();
	AddNodeProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	AddNodeProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to edit. Defaults to EventGraph.")));
	AddNodeProps->SetObjectField(TEXT("SpawnerId"), MakeStringProperty(TEXT("Spawner id from blueprint_find_node_spawners.")));
	AddNodeProps->SetObjectField(TEXT("NodeName"), MakeStringProperty(TEXT("Optional alias/name for the new node.")));
	AddNodeProps->SetObjectField(TEXT("NodePosX"), MakeStringProperty(TEXT("Optional X position for the new node.")));
	AddNodeProps->SetObjectField(TEXT("NodePosY"), MakeStringProperty(TEXT("Optional Y position for the new node.")));
	AddNodeProps->SetObjectField(TEXT("PinDefaults"), MakeObjectProperty(TEXT("Optional pin default values keyed by pin name.")));
	AddNodeSchema->SetObjectField(TEXT("properties"), AddNodeProps);
	AddNodeSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("SpawnerId"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_add_node"),
		TEXT("Add a Blueprint node using an existing native spawner id."),
		AddNodeSchema)));

	TSharedPtr<FJsonObject> SetNodePositionSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> SetNodePositionProps = MakeShared<FJsonObject>();
	SetNodePositionProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	SetNodePositionProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to edit. Defaults to EventGraph.")));
	SetNodePositionProps->SetObjectField(TEXT("NodeGuid"), MakeStringProperty(TEXT("Node GUID to move.")));
	SetNodePositionProps->SetObjectField(TEXT("NodePosX"), MakeStringProperty(TEXT("New X position.")));
	SetNodePositionProps->SetObjectField(TEXT("NodePosY"), MakeStringProperty(TEXT("New Y position.")));
	SetNodePositionSchema->SetObjectField(TEXT("properties"), SetNodePositionProps);
	SetNodePositionSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("NodeGuid")),
		MakeShared<FJsonValueString>(TEXT("NodePosX")),
		MakeShared<FJsonValueString>(TEXT("NodePosY"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_set_node_position"),
		TEXT("Move an existing Blueprint graph node by GUID."),
		SetNodePositionSchema)));

	TSharedPtr<FJsonObject> ConnectSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> ConnectProps = MakeShared<FJsonObject>();
	ConnectProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	ConnectProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to edit. Defaults to EventGraph.")));
	ConnectProps->SetObjectField(TEXT("SourceNodeGuid"), MakeStringProperty(TEXT("Source node GUID.")));
	ConnectProps->SetObjectField(TEXT("SourcePinName"), MakeStringProperty(TEXT("Source output pin name.")));
	ConnectProps->SetObjectField(TEXT("TargetNodeGuid"), MakeStringProperty(TEXT("Target node GUID.")));
	ConnectProps->SetObjectField(TEXT("TargetPinName"), MakeStringProperty(TEXT("Target input pin name.")));
	ConnectSchema->SetObjectField(TEXT("properties"), ConnectProps);
	ConnectSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("SourceNodeGuid")),
		MakeShared<FJsonValueString>(TEXT("SourcePinName")),
		MakeShared<FJsonValueString>(TEXT("TargetNodeGuid")),
		MakeShared<FJsonValueString>(TEXT("TargetPinName"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_connect_nodes"),
		TEXT("Connect two Blueprint nodes by GUID and pin name."),
		ConnectSchema)));

	TSharedPtr<FJsonObject> AddBySearchSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> AddBySearchProps = MakeShared<FJsonObject>();
	AddBySearchProps->SetObjectField(TEXT("BlueprintNameOrPath"), MakeStringProperty(TEXT("Blueprint asset name, keyword, or full asset path.")));
	AddBySearchProps->SetObjectField(TEXT("GraphName"), MakeStringProperty(TEXT("Graph name to edit. Defaults to EventGraph.")));
	AddBySearchProps->SetObjectField(TEXT("SourceNodeTitleOrGuid"), MakeStringProperty(TEXT("Source node title text or GUID, for example Event BeginPlay.")));
	AddBySearchProps->SetObjectField(TEXT("SourcePinName"), MakeStringProperty(TEXT("Source output pin name. Defaults to then.")));
	AddBySearchProps->SetObjectField(TEXT("SearchQuery"), MakeStringProperty(TEXT("Node search text, for example Print String.")));
	AddBySearchProps->SetObjectField(TEXT("TargetPinName"), MakeStringProperty(TEXT("Target input pin name. Defaults to execute.")));
	AddBySearchProps->SetObjectField(TEXT("PinDefaults"), MakeObjectProperty(TEXT("Optional pin default values keyed by pin name.")));
	AddBySearchSchema->SetObjectField(TEXT("properties"), AddBySearchProps);
	AddBySearchSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("BlueprintNameOrPath")),
		MakeShared<FJsonValueString>(TEXT("SourceNodeTitleOrGuid")),
		MakeShared<FJsonValueString>(TEXT("SearchQuery"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_add_node_by_search_and_connect"),
		TEXT("Resolve a Blueprint, find a source node, search a spawner, add the node, and connect it in one call."),
		AddBySearchSchema)));

	TSharedPtr<FJsonObject> RunRecipeSchema = MakeObjectSchema();
	TSharedPtr<FJsonObject> RunRecipeProps = MakeShared<FJsonObject>();
	RunRecipeProps->SetObjectField(TEXT("RecipeName"), MakeStringProperty(TEXT("Optional recipe name for reporting.")));
	RunRecipeProps->SetObjectField(TEXT("Steps"), MakeObjectProperty(TEXT("Array of step objects with Id, ToolName, and Arguments. Later string arguments can reference prior structured results as $StepId.path.to.field.")));
	RunRecipeSchema->SetObjectField(TEXT("properties"), RunRecipeProps);
	RunRecipeSchema->SetArrayField(TEXT("required"), {
		MakeShared<FJsonValueString>(TEXT("Steps"))
	});
	Tools.Add(MakeShared<FJsonValueObject>(MakeToolDefinition(
		TEXT("blueprint_run_recipe"),
		TEXT("Run a deterministic sequence of supported Blueprint-writing tool calls with per-step results and reference substitution."),
		RunRecipeSchema)));

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetArrayField(TEXT("tools"), Tools);
	return MakeJsonRpcResult(RequestObject, Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::HandleToolsCall(const TSharedPtr<FJsonObject>& RequestObject)
{
	const TSharedPtr<FJsonObject>* ParamsObject = nullptr;
	if (!RequestObject->TryGetObjectField(TEXT("params"), ParamsObject) || !ParamsObject || !ParamsObject->IsValid())
	{
		return MakeJsonRpcError(RequestObject, -32602, TEXT("Invalid params: tools/call requires params object"));
	}

	FString ToolName;
	if (!(*ParamsObject)->TryGetStringField(TEXT("name"), ToolName))
	{
		return MakeJsonRpcError(RequestObject, -32602, TEXT("Invalid params: missing tool name"));
	}

	TSharedPtr<FJsonObject> Arguments = MakeShared<FJsonObject>();
	const TSharedPtr<FJsonObject>* ArgumentsObject = nullptr;
	if ((*ParamsObject)->TryGetObjectField(TEXT("arguments"), ArgumentsObject) && ArgumentsObject && ArgumentsObject->IsValid())
	{
		Arguments = *ArgumentsObject;
	}

	TSharedPtr<FJsonObject> ToolResult = ExecuteTool(ToolName, Arguments);
	if (!ToolResult.IsValid())
	{
		return MakeJsonRpcError(RequestObject, -32601, FString::Printf(TEXT("Unknown tool: %s"), *ToolName));
	}

	return MakeJsonRpcResult(RequestObject, ToolResult);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteTool(const FString& ToolName, const TSharedPtr<FJsonObject>& Arguments)
{
	if (ToolName == TEXT("unreal_get_status"))
	{
		return ExecuteGetStatus();
	}
	if (ToolName == TEXT("unreal_get_capabilities"))
	{
		return ExecuteGetCapabilities();
	}
	if (ToolName == TEXT("unreal_recommend_tools"))
	{
		return ExecuteRecommendTools(Arguments);
	}
	if (ToolName == TEXT("sage_get_status"))
	{
		return ExecuteGetStatus();
	}
	if (ToolName == TEXT("sage_list_extensions"))
	{
		return ExecuteSageListExtensions(Arguments);
	}
	if (ToolName == TEXT("sage_search_extensions"))
	{
		return ExecuteSageSearchExtensions(Arguments);
	}
	if (ToolName == TEXT("sage_get_extension_details"))
	{
		return ExecuteSageGetExtensionDetails(Arguments);
	}
	if (ToolName == TEXT("sage_call_query"))
	{
		return ExecuteSageCallQuery(Arguments);
	}
	if (ToolName == TEXT("sage_call_action"))
	{
		return ExecuteSageCallAction(Arguments);
	}
	if (ToolName == TEXT("unreal_resolve_asset"))
	{
		return ExecuteResolveAsset(Arguments);
	}
	if (ToolName == TEXT("unreal_inspect_asset"))
	{
		return ExecuteInspectAsset(Arguments);
	}
	if (ToolName == TEXT("data_asset_find_classes"))
	{
		return ExecuteDataAssetFindClasses(Arguments);
	}
	if (ToolName == TEXT("data_asset_create"))
	{
		return ExecuteDataAssetCreate(Arguments);
	}
	if (ToolName == TEXT("data_asset_set_property"))
	{
		return ExecuteDataAssetSetProperty(Arguments);
	}
	if (ToolName == TEXT("data_asset_create_batch"))
	{
		return ExecuteDataAssetCreateBatch(Arguments);
	}
	if (ToolName == TEXT("editor_get_world_status"))
	{
		return ExecuteEditorGetWorldStatus(Arguments);
	}
	if (ToolName == TEXT("editor_place_actor"))
	{
		return ExecuteEditorPlaceActor(Arguments);
	}
	if (ToolName == TEXT("editor_set_actor_transform"))
	{
		return ExecuteEditorSetActorTransform(Arguments);
	}
	if (ToolName == TEXT("editor_set_component_transform"))
	{
		return ExecuteEditorSetComponentTransform(Arguments);
	}
	if (ToolName == TEXT("editor_read_level_actors"))
	{
		return ExecuteEditorReadLevelActors(Arguments);
	}
	if (ToolName == TEXT("editor_select_actor"))
	{
		return ExecuteEditorSelectActor(Arguments);
	}
	if (ToolName == TEXT("editor_cleanup_repro_actors"))
	{
		return ExecuteEditorCleanupReproActors(Arguments);
	}
	if (ToolName == TEXT("editor_capture_viewport"))
	{
		return ExecuteEditorCaptureViewport(Arguments);
	}
	if (ToolName == TEXT("pcg_get_status"))
	{
		return ExecutePCGGetStatus(Arguments);
	}
	if (ToolName == TEXT("pcg_get_road_capabilities"))
	{
		return ExecutePCGGetRoadCapabilities(Arguments);
	}
	if (ToolName == TEXT("pcg_create_spline_bounds_actor"))
	{
		return ExecutePCGCreateSplineBoundsActor(Arguments);
	}
	if (ToolName == TEXT("pcg_inspect_spline_bounds_actor"))
	{
		return ExecutePCGInspectSplineBoundsActor(Arguments);
	}
	if (ToolName == TEXT("pcg_set_spline_bounds_actor"))
	{
		return ExecutePCGSetSplineBoundsActor(Arguments);
	}
	if (ToolName == TEXT("pcg_create_forest_repro"))
	{
		return ExecutePCGCreateForestRepro(Arguments);
	}
	if (ToolName == TEXT("pcg_place_forest_actor"))
	{
		return ExecutePCGPlaceForestActor(Arguments);
	}
	if (ToolName == TEXT("pcg_inspect_graph"))
	{
		return ExecutePCGInspectGraph(Arguments);
	}
	if (ToolName == TEXT("pcg_set_forest_bounds"))
	{
		return ExecutePCGSetForestBounds(Arguments);
	}
	if (ToolName == TEXT("pcg_generate"))
	{
		return ExecutePCGGenerate(Arguments);
	}
	if (ToolName == TEXT("pcg_create_visual_repro_output"))
	{
		return ExecutePCGCreateVisualReproOutput(Arguments);
	}
	if (ToolName == TEXT("pcg_inspect_generated"))
	{
		return ExecutePCGInspectGenerated(Arguments);
	}
	if (ToolName == TEXT("pcg_run_forest_recipe"))
	{
		return ExecutePCGRunForestRecipe(Arguments);
	}
	if (ToolName == TEXT("pcg_run_road_recipe"))
	{
		return ExecutePCGRunRoadRecipe(Arguments);
	}
	if (ToolName == TEXT("pcg_run_spline_road_proof"))
	{
		return ExecutePCGRunSplineRoadProof(Arguments);
	}
	if (ToolName == TEXT("pcg_run_city_layout_recipe"))
	{
		return ExecutePCGRunCityLayoutRecipe(Arguments);
	}
	if (ToolName == TEXT("pcg_update_city_layout_recipe"))
	{
		return ExecutePCGUpdateCityLayoutRecipe(Arguments);
	}
	if (ToolName == TEXT("pcg_update_forest_recipe"))
	{
		return ExecutePCGUpdateForestRecipe(Arguments);
	}
	if (ToolName == TEXT("pcg_multiclass_spawn_proof"))
	{
		return ExecutePCGMultiClassProof(Arguments);
	}
	if (ToolName == TEXT("material_create"))
	{
		return ExecuteMaterialCreate(Arguments);
	}
	if (ToolName == TEXT("material_find_expression_classes"))
	{
		return ExecuteMaterialFindExpressionClasses(Arguments);
	}
	if (ToolName == TEXT("material_resolve_expression_class"))
	{
		return ExecuteMaterialResolveExpressionClass(Arguments);
	}
	if (ToolName == TEXT("material_add_expression"))
	{
		return ExecuteMaterialAddExpression(Arguments);
	}
	if (ToolName == TEXT("material_set_expression_property"))
	{
		return ExecuteMaterialSetExpressionProperty(Arguments);
	}
	if (ToolName == TEXT("material_connect_expressions"))
	{
		return ExecuteMaterialConnectExpressions(Arguments);
	}
	if (ToolName == TEXT("material_connect_root"))
	{
		return ExecuteMaterialConnectRoot(Arguments);
	}
	if (ToolName == TEXT("material_update"))
	{
		return ExecuteMaterialUpdate(Arguments);
	}
	if (ToolName == TEXT("unreal_create_longears_repro_assets"))
	{
		return ExecuteCreateLongearsReproAssets(Arguments);
	}
	if (ToolName == TEXT("unreal_create_material_repro_assets"))
	{
		return ExecuteCreateMaterialReproAssets(Arguments);
	}
	if (ToolName == TEXT("blueprint_create"))
	{
		return ExecuteCreateBlueprint(Arguments);
	}
	if (ToolName == TEXT("blueprint_create_interface"))
	{
		return ExecuteCreateBlueprintInterface(Arguments);
	}
	if (ToolName == TEXT("blueprint_create_road_actor"))
	{
		return ExecuteBlueprintCreateRoadActor(Arguments);
	}
	if (ToolName == TEXT("blueprint_create_intersection_actor"))
	{
		return ExecuteBlueprintCreateIntersectionActor(Arguments);
	}
	if (ToolName == TEXT("blueprint_create_skyscraper_actor"))
	{
		return ExecuteBlueprintCreateSkyscraperActor(Arguments);
	}
	if (ToolName == TEXT("blueprint_create_primitive_actor"))
	{
		return ExecuteBlueprintCreatePrimitiveActor(Arguments);
	}
	if (ToolName == TEXT("blueprint_add_variable"))
	{
		return ExecuteBlueprintAddVariable(Arguments);
	}
	if (ToolName == TEXT("blueprint_add_component"))
	{
		return ExecuteBlueprintAddComponent(Arguments);
	}
	if (ToolName == TEXT("blueprint_add_component_motion"))
	{
		return ExecuteBlueprintAddComponentMotion(Arguments);
	}
	if (ToolName == TEXT("blueprint_add_dynamic_material_parameter_update"))
	{
		return ExecuteBlueprintAddDynamicMaterialParameterUpdate(Arguments);
	}
	if (ToolName == TEXT("blueprint_create_function"))
	{
		return ExecuteBlueprintCreateFunction(Arguments);
	}
	if (ToolName == TEXT("blueprint_create_event_graph"))
	{
		return ExecuteBlueprintCreateEventGraph(Arguments);
	}
	if (ToolName == TEXT("blueprint_implement_interface"))
	{
		return ExecuteBlueprintImplementInterface(Arguments);
	}
	if (ToolName == TEXT("blueprint_add_interface_event"))
	{
		return ExecuteBlueprintAddInterfaceEvent(Arguments);
	}
	if (ToolName == TEXT("blueprint_add_custom_event"))
	{
		return ExecuteBlueprintAddCustomEvent(Arguments);
	}
	if (ToolName == TEXT("blueprint_add_comment"))
	{
		return ExecuteBlueprintAddComment(Arguments);
	}
	if (ToolName == TEXT("blueprint_read_graph"))
	{
		return ExecuteBlueprintReadGraph(Arguments);
	}
	if (ToolName == TEXT("blueprint_compile"))
	{
		return ExecuteBlueprintCompile(Arguments);
	}
	if (ToolName == TEXT("blueprint_find_node_spawners"))
	{
		return ExecuteBlueprintFindNodeSpawners(Arguments);
	}
	if (ToolName == TEXT("blueprint_resolve_node_spawner"))
	{
		return ExecuteBlueprintResolveNodeSpawner(Arguments);
	}
	if (ToolName == TEXT("blueprint_add_node"))
	{
		return ExecuteBlueprintAddNode(Arguments);
	}
	if (ToolName == TEXT("blueprint_set_node_position"))
	{
		return ExecuteBlueprintSetNodePosition(Arguments);
	}
	if (ToolName == TEXT("blueprint_connect_nodes"))
	{
		return ExecuteBlueprintConnectNodes(Arguments);
	}
	if (ToolName == TEXT("blueprint_add_node_by_search_and_connect"))
	{
		return ExecuteBlueprintAddNodeBySearchAndConnect(Arguments);
	}
	if (ToolName == TEXT("blueprint_search_project"))
	{
		return ExecuteBlueprintSearchProject(Arguments);
	}
	if (ToolName == TEXT("blueprint_run_recipe"))
	{
		return ExecuteBlueprintRunRecipe(Arguments);
	}

	return nullptr;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteGetStatus() const
{
	TSharedPtr<FJsonObject> Status = MakeShared<FJsonObject>();
	Status->SetBoolField(TEXT("running"), bRunning);
	Status->SetNumberField(TEXT("port"), ActivePort);
	Status->SetStringField(TEXT("endpoint_url"), GetEndpointUrl());
	Status->SetStringField(TEXT("unreal_version"), FEngineVersion::Current().ToString());
	Status->SetBoolField(TEXT("standalone"), true);
	Status->SetStringField(TEXT("backend"), TEXT("native_unreal_editor"));
	Status->SetStringField(TEXT("capabilities_endpoint"), TEXT("unreal_get_capabilities"));
	Status->SetStringField(TEXT("discovery_note"), TEXT("Call unreal_get_capabilities before concluding a Druids Blueprint capability is unavailable. Some hosts expose MCP tools lazily."));
	Status->SetObjectField(TEXT("capabilities"), BuildCapabilitiesManifest());
	return MakeToolJsonObjectResult(Status);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteGetCapabilities() const
{
	return MakeToolJsonObjectResult(BuildCapabilitiesManifest());
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteRecommendTools(const TSharedPtr<FJsonObject>& Arguments) const
{
	const FString Task = GetOptionalStringParam(Arguments, TEXT("Task")).TrimStartAndEnd();
	if (Task.IsEmpty())
	{
		return MakeToolTextResult(TEXT("unreal_recommend_tools requires Task."), true);
	}

	const FString Domain = GetOptionalStringParam(Arguments, TEXT("Domain")).TrimStartAndEnd();
	const FString Intent = GetOptionalStringParam(Arguments, TEXT("Intent")).TrimStartAndEnd();
	const FString AssetType = GetOptionalStringParam(Arguments, TEXT("AssetType")).TrimStartAndEnd();
	const FString RouteText = NormalizeResolverText(FString::Printf(TEXT("%s %s %s %s"), *Task, *Domain, *Intent, *AssetType));

	auto HasAny = [&RouteText](const TArray<FString>& Needles)
	{
		for (const FString& Needle : Needles)
		{
			if (RouteText.Contains(NormalizeResolverText(Needle)))
			{
				return true;
			}
		}
		return false;
	};

	const bool bBlueprint = HasAny({
		TEXT("blueprint"), TEXT("bp"), TEXT("graph"), TEXT("node"), TEXT("pin"), TEXT("actor"),
		TEXT("component"), TEXT("variable"), TEXT("event"), TEXT("function"), TEXT("interface"),
		TEXT("input"), TEXT("overlap"), TEXT("compile"), TEXT("recipe"), TEXT("tree"),
		TEXT("forest"), TEXT("primitive actor"), TEXT("visual actor")
	});
	const bool bPrimitiveVisualActor = HasAny({ TEXT("tree"), TEXT("primitive actor"), TEXT("visual actor"), TEXT("dummy tree"), TEXT("cylinder"), TEXT("sphere") });
	const bool bEditorPlacement = HasAny({
		TEXT("place actor"), TEXT("place the actor"), TEXT("level placement"), TEXT("map editing"),
		TEXT("spawn in level"), TEXT("add to level"), TEXT("editor world"), TEXT("viewport"),
		TEXT("screenshot"), TEXT("capture"), TEXT("visual inspect"), TEXT("visual qa")
	});
	const bool bPCG = HasAny({
		TEXT("pcg"), TEXT("procedural content"), TEXT("forest"), TEXT("scatter"), TEXT("generate foliage"),
		TEXT("road"), TEXT("roads"), TEXT("street"), TEXT("streets"), TEXT("city"), TEXT("city layout"),
		TEXT("skyscraper"), TEXT("block"), TEXT("intersection")
	});
	const bool bMaterial = HasAny({
		TEXT("material"), TEXT("shader"), TEXT("texture"), TEXT("emissive"), TEXT("roughness"),
		TEXT("base color"), TEXT("basecolor"), TEXT("normal"), TEXT("scalar parameter"),
		TEXT("vector parameter"), TEXT("power"), TEXT("panner"), TEXT("lerp")
	});
	const bool bAuthoring = HasAny({
		TEXT("create"), TEXT("add"), TEXT("write"), TEXT("edit"), TEXT("wire"), TEXT("connect"),
		TEXT("set"), TEXT("change"), TEXT("build"), TEXT("compile"), TEXT("author")
	});
	const bool bProjectSearchIntent = HasAny({
		TEXT("search"), TEXT("find"), TEXT("example"), TEXT("examples"), TEXT("project context"),
		TEXT("usage"), TEXT("reference"), TEXT("pseudocode"), TEXT("overview")
	});
	const bool bProjectSearch = bProjectSearchIntent || (HasAny({ TEXT("comments"), TEXT("metadata") }) && !bAuthoring);
	const bool bAssetInspection = HasAny({
		TEXT("inspect"), TEXT("read"), TEXT("asset"), TEXT("statetree"), TEXT("state tree"),
		TEXT("camera asset"), TEXT("camera rig"), TEXT("data asset"), TEXT("uobject")
	});
	const bool bDataAsset = HasAny({
		TEXT("data asset"), TEXT("data assets"), TEXT("primary data asset"), TEXT("primarydataasset"),
		TEXT("uprimarydataasset"), TEXT("pda"), TEXT("itemsummary")
	});
	const bool bOnboarding = HasAny({
		TEXT("connect ai"), TEXT("onboarding"), TEXT("codex"), TEXT("gemini"), TEXT("antigravity"),
		TEXT("claude"), TEXT("cursor"), TEXT("rider"), TEXT("bob"), TEXT("ibm bob"), TEXT("jules"), TEXT("google jules"),
		TEXT("mcp setup"), TEXT("assistant")
	});
	const bool bBobOnboarding = HasAny({ TEXT("bob"), TEXT("ibm bob") });
	const bool bJulesOnboarding = HasAny({ TEXT("jules"), TEXT("google jules") });
	const bool bSage = HasAny({ TEXT("sage"), TEXT("extension"), TEXT("workflow skill") });
	const bool bInspectIntent = HasAny({ TEXT("inspect"), TEXT("read"), TEXT("explain"), TEXT("pseudocode"), TEXT("inventory"), TEXT("overview") });

	TArray<FString> PrimaryDomains;
	TArray<FString> RelevantTools;
	TArray<FString> PossibleTools;
	TArray<FString> FirstSteps;
	TArray<FString> UnsupportedBoundaries;
	TArray<FString> Reasoning;

	if (bProjectSearch)
	{
		AddUniqueString(PrimaryDomains, TEXT("project_search"));
		AddUniqueString(RelevantTools, TEXT("blueprint_search_project"));
		AddUniqueString(RelevantTools, TEXT("unreal_inspect_asset"));
		AddUniqueString(PossibleTools, TEXT("blueprint_read_graph"));
		AddUniqueString(FirstSteps, TEXT("Call blueprint_search_project with the user's query and package paths, then follow result recommended_next_tools."));
		AddUniqueString(Reasoning, TEXT("Task includes search/find/example/context language."));
	}

	if (bDataAsset)
	{
		AddUniqueString(PrimaryDomains, TEXT("data_asset"));
		AddUniqueString(RelevantTools, TEXT("data_asset_find_classes"));
		if (bAuthoring)
		{
			AddUniqueString(RelevantTools, TEXT("data_asset_create"));
			AddUniqueString(RelevantTools, TEXT("data_asset_set_property"));
			AddUniqueString(RelevantTools, TEXT("data_asset_create_batch"));
		}
		AddUniqueString(RelevantTools, TEXT("unreal_resolve_asset"));
		AddUniqueString(RelevantTools, TEXT("unreal_inspect_asset"));
		AddUniqueString(PossibleTools, TEXT("blueprint_search_project"));
		AddUniqueString(FirstSteps, TEXT("Call data_asset_find_classes to discover the exact Data Asset / Primary Data Asset classes, then use data_asset_create_batch for multi-asset requests or data_asset_create plus data_asset_set_property for single assets. Do not use blueprint_create as a Data Asset instance workaround."));
		if (bAuthoring)
		{
			AddUniqueString(UnsupportedBoundaries, TEXT("data_asset_array_map_property_editing"));
		}
		AddUniqueString(Reasoning, TEXT("Task includes Data Asset / Primary Data Asset / PDA language."));
	}

	if (bMaterial)
	{
		AddUniqueString(PrimaryDomains, TEXT("material"));
		if (bAuthoring)
		{
			AddUniqueString(RelevantTools, TEXT("material_find_expression_classes"));
			AddUniqueString(RelevantTools, TEXT("material_resolve_expression_class"));
			AddUniqueString(RelevantTools, TEXT("material_create"));
			AddUniqueString(RelevantTools, TEXT("material_add_expression"));
			AddUniqueString(RelevantTools, TEXT("material_set_expression_property"));
			AddUniqueString(RelevantTools, TEXT("material_connect_expressions"));
			AddUniqueString(RelevantTools, TEXT("material_connect_root"));
			AddUniqueString(RelevantTools, TEXT("material_update"));
			AddUniqueString(PossibleTools, TEXT("unreal_inspect_asset"));
			AddUniqueString(FirstSteps, TEXT("Resolve expression classes before adding material expressions; use material_set_expression_property for post-create reflected property fixes."));
		}
		else
		{
			AddUniqueString(RelevantTools, TEXT("unreal_inspect_asset"));
			AddUniqueString(RelevantTools, TEXT("blueprint_search_project"));
			AddUniqueString(PossibleTools, TEXT("material_find_expression_classes"));
			AddUniqueString(FirstSteps, TEXT("Use unreal_inspect_asset for direct material readback, or blueprint_search_project to find material examples project-wide."));
		}
		AddUniqueString(Reasoning, TEXT("Task includes material/shader/texture/expression language."));
	}

	if (bBlueprint)
	{
		AddUniqueString(PrimaryDomains, TEXT("blueprint"));
		if (bInspectIntent || bProjectSearch)
		{
			AddUniqueString(RelevantTools, TEXT("blueprint_search_project"));
			AddUniqueString(RelevantTools, TEXT("blueprint_read_graph"));
			AddUniqueString(RelevantTools, TEXT("unreal_inspect_asset"));
			AddUniqueString(FirstSteps, TEXT("Search or inspect first, then read the exact graph_name from available_graphs before making conclusions."));
		}
		if (bAuthoring)
		{
			AddUniqueString(RelevantTools, TEXT("blueprint_create"));
			if (bPrimitiveVisualActor)
			{
				AddUniqueString(RelevantTools, TEXT("blueprint_create_primitive_actor"));
			}
			AddUniqueString(RelevantTools, TEXT("blueprint_read_graph"));
			AddUniqueString(RelevantTools, TEXT("blueprint_resolve_node_spawner"));
			AddUniqueString(RelevantTools, TEXT("blueprint_add_node"));
			AddUniqueString(RelevantTools, TEXT("blueprint_connect_nodes"));
			AddUniqueString(RelevantTools, TEXT("blueprint_add_comment"));
			AddUniqueString(RelevantTools, TEXT("blueprint_compile"));
			AddUniqueString(PossibleTools, TEXT("blueprint_run_recipe"));
			AddUniqueString(PossibleTools, TEXT("blueprint_add_node_by_search_and_connect"));
			if (bMaterial || HasAny({ TEXT("dynamic material"), TEXT("material parameter") }))
			{
				AddUniqueString(RelevantTools, TEXT("blueprint_add_dynamic_material_parameter_update"));
			}
			AddUniqueString(FirstSteps, TEXT("Prefer canonical spawner IDs via blueprint_resolve_node_spawner, then add/connect/compile and verify with blueprint_read_graph."));
		}
		AddUniqueString(Reasoning, TEXT("Task includes Blueprint graph/node/actor/component/event language."));
	}

	if (bEditorPlacement)
	{
		AddUniqueString(PrimaryDomains, TEXT("editor_world"));
		AddUniqueString(RelevantTools, TEXT("editor_get_world_status"));
		AddUniqueString(RelevantTools, TEXT("editor_place_actor"));
		AddUniqueString(RelevantTools, TEXT("editor_read_level_actors"));
		AddUniqueString(RelevantTools, TEXT("editor_set_actor_transform"));
		AddUniqueString(RelevantTools, TEXT("editor_set_component_transform"));
		AddUniqueString(RelevantTools, TEXT("editor_select_actor"));
		AddUniqueString(RelevantTools, TEXT("editor_cleanup_repro_actors"));
		AddUniqueString(RelevantTools, TEXT("editor_capture_viewport"));
		AddUniqueString(FirstSteps, TEXT("Call editor_get_world_status, then editor_place_actor with AllowCurrentLevel=true for explicit current-level mutation."));
		AddUniqueString(Reasoning, TEXT("Task includes editor-world actor placement language."));
	}

	if (bPCG)
	{
		AddUniqueString(PrimaryDomains, TEXT("pcg"));
		AddUniqueString(RelevantTools, TEXT("pcg_get_status"));
		AddUniqueString(RelevantTools, TEXT("pcg_get_road_capabilities"));
		AddUniqueString(RelevantTools, TEXT("blueprint_create_road_actor"));
		AddUniqueString(RelevantTools, TEXT("blueprint_create_intersection_actor"));
		AddUniqueString(RelevantTools, TEXT("blueprint_create_skyscraper_actor"));
		AddUniqueString(RelevantTools, TEXT("pcg_create_forest_repro"));
		AddUniqueString(RelevantTools, TEXT("pcg_place_forest_actor"));
		AddUniqueString(RelevantTools, TEXT("pcg_inspect_graph"));
		AddUniqueString(RelevantTools, TEXT("pcg_set_forest_bounds"));
		AddUniqueString(RelevantTools, TEXT("pcg_generate"));
		AddUniqueString(RelevantTools, TEXT("pcg_inspect_generated"));
		AddUniqueString(RelevantTools, TEXT("pcg_run_forest_recipe"));
		AddUniqueString(RelevantTools, TEXT("pcg_run_road_recipe"));
		AddUniqueString(RelevantTools, TEXT("pcg_run_spline_road_proof"));
		AddUniqueString(RelevantTools, TEXT("pcg_run_city_layout_recipe"));
		AddUniqueString(RelevantTools, TEXT("pcg_update_city_layout_recipe"));
		AddUniqueString(RelevantTools, TEXT("pcg_update_forest_recipe"));
		AddUniqueString(RelevantTools, TEXT("pcg_multiclass_spawn_proof"));
		AddUniqueString(RelevantTools, TEXT("pcg_create_visual_repro_output"));
		AddUniqueString(RelevantTools, TEXT("pcg_create_spline_bounds_actor"));
		AddUniqueString(RelevantTools, TEXT("pcg_inspect_spline_bounds_actor"));
		AddUniqueString(RelevantTools, TEXT("pcg_set_spline_bounds_actor"));
		AddUniqueString(PossibleTools, TEXT("blueprint_create_primitive_actor"));
		AddUniqueString(PossibleTools, TEXT("editor_place_actor"));
		AddUniqueString(PossibleTools, TEXT("editor_capture_viewport"));
		if (HasAny({ TEXT("road"), TEXT("roads"), TEXT("street"), TEXT("streets"), TEXT("city"), TEXT("city layout"), TEXT("intersection"), TEXT("skyscraper"), TEXT("spline") }))
		{
			AddUniqueString(FirstSteps, TEXT("Call pcg_get_status and pcg_get_road_capabilities, then use pcg_run_city_layout_recipe for grid cities, pcg_update_city_layout_recipe for make-it-bigger/denser updates, pcg_run_road_recipe for a straight road strip, or pcg_run_spline_road_proof for spline-rectangle road bounds. True curved spline sampling remains unsupported."));
		}
		else
		{
			AddUniqueString(FirstSteps, TEXT("Call pcg_get_status first, then prefer pcg_run_forest_recipe for a one-call forest workflow or pcg_update_forest_recipe for resize/density/naturalness adjustments."));
		}
		AddUniqueString(Reasoning, TEXT("Task includes PCG/procedural forest/scatter language."));
	}

	if (bAssetInspection && !bBlueprint && !bMaterial && !bDataAsset)
	{
		AddUniqueString(PrimaryDomains, TEXT("asset_inspection"));
		AddUniqueString(RelevantTools, TEXT("unreal_resolve_asset"));
		AddUniqueString(RelevantTools, TEXT("unreal_inspect_asset"));
		AddUniqueString(PossibleTools, TEXT("blueprint_search_project"));
		AddUniqueString(FirstSteps, TEXT("Resolve the asset if needed, then call unreal_inspect_asset and honor partial_failures/inspectability fields."));
		AddUniqueString(Reasoning, TEXT("Task asks to inspect/read non-Blueprint or generic UObject assets."));
	}

	if (bOnboarding)
	{
		AddUniqueString(PrimaryDomains, TEXT("onboarding"));
		AddUniqueString(RelevantTools, TEXT("unreal_get_status"));
		AddUniqueString(RelevantTools, TEXT("unreal_get_capabilities"));
		AddUniqueString(RelevantTools, TEXT("unreal_recommend_tools"));
		if (bBobOnboarding)
		{
			AddUniqueString(FirstSteps, TEXT("For IBM Bob, use Tools > Druids MCP Bridge > Connect AI Assistant... and copy the IBM Bob manual setup snippet for the existing local MCP endpoint. Then ask Bob to call unreal_get_status, unreal_get_capabilities, and unreal_recommend_tools with the user's task before deciding what Druids can do. If Bob sees a partial tool list, search/list tools again before saying Druids cannot do something. Existing direct MCP clients remain unchanged."));
		}
		if (bJulesOnboarding)
		{
			AddUniqueString(FirstSteps, TEXT("For Google Jules, use Tools > Druids MCP Bridge > Connect AI Assistant... and copy the Jules task packet. Jules should make GitHub/repository changes only, then hand a local validation packet to an Unreal-capable agent that calls unreal_get_status, unreal_get_capabilities, and unreal_recommend_tools. Jules should not assume it can reach the user's 127.0.0.1 Unreal endpoint directly; no Druids-hosted relay is provided."));
			AddUniqueString(UnsupportedBoundaries, TEXT("jules_direct_localhost_access"));
			AddUniqueString(UnsupportedBoundaries, TEXT("druids_hosted_relay"));
		}
		if (!bBobOnboarding && !bJulesOnboarding)
		{
			AddUniqueString(FirstSteps, TEXT("Use Tools > Druids MCP Bridge > Connect AI Assistant... in the Unreal Editor for client setup; MCP can report endpoint/status but config writes are editor UI driven."));
		}
		AddUniqueString(Reasoning, TEXT("Task mentions AI client connection/setup."));
	}

	if (bSage)
	{
		AddUniqueString(PrimaryDomains, TEXT("sage_extensions"));
		AddUniqueString(RelevantTools, TEXT("sage_list_extensions"));
		AddUniqueString(RelevantTools, TEXT("sage_search_extensions"));
		AddUniqueString(RelevantTools, TEXT("sage_get_extension_details"));
		AddUniqueString(PossibleTools, TEXT("sage_call_query"));
		AddUniqueString(PossibleTools, TEXT("sage_call_action"));
		AddUniqueString(FirstSteps, TEXT("List or search Sage extensions, inspect extension details, then call explicit query/action names."));
		AddUniqueString(Reasoning, TEXT("Task mentions Sage extensions/workflows."));
	}

	if (HasAny({ TEXT("pie"), TEXT("play in editor"), TEXT("run game"), TEXT("simulate"), TEXT("runtime test") }))
	{
		AddUniqueString(UnsupportedBoundaries, TEXT("run_pie"));
	}
	if (HasAny({ TEXT("enhanced input asset"), TEXT("input mapping context"), TEXT("imc_"), TEXT("input action asset") }))
	{
		AddUniqueString(UnsupportedBoundaries, TEXT("enhanced_input_assets"));
		AddUniqueString(UnsupportedBoundaries, TEXT("input_mapping_context_assets"));
	}
	if (HasAny({ TEXT("material graph editing all nodes"), TEXT("arbitrary material nodes") }))
	{
		AddUniqueString(UnsupportedBoundaries, TEXT("broad_material_graph_editing"));
	}

	if (PrimaryDomains.Num() == 0)
	{
		AddUniqueString(PrimaryDomains, TEXT("broad"));
		AddUniqueString(RelevantTools, TEXT("unreal_get_capabilities"));
		AddUniqueString(RelevantTools, TEXT("unreal_resolve_asset"));
		AddUniqueString(RelevantTools, TEXT("unreal_inspect_asset"));
		AddUniqueString(RelevantTools, TEXT("blueprint_search_project"));
		AddUniqueString(FirstSteps, TEXT("Start with capabilities, resolve/inspect known assets, and use project search before filesystem scraping."));
		AddUniqueString(Reasoning, TEXT("No specific domain matched with high confidence."));
	}

	if (UnsupportedBoundaries.Num() > 0)
	{
		AddUniqueString(FirstSteps, TEXT("Do not fake unsupported work; report the boundary and complete the supported adjacent inspection/authoring steps."));
		AddUniqueString(FirstSteps, TEXT("If this limit blocks the user, ask them to contact Druids so the limitation can be prioritized: Discord https://discord.gg/5Gr5m82B2a or email bm@druids.ai. Feature requests reported on Discord are the fastest path to becoming the next bridge slice."));
	}

	const FString PrimaryDomain = PrimaryDomains.Num() > 0 ? PrimaryDomains[0] : TEXT("broad");
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("task"), Task);
	Result->SetStringField(TEXT("domain_hint"), Domain);
	Result->SetStringField(TEXT("intent_hint"), Intent);
	Result->SetStringField(TEXT("asset_type_hint"), AssetType);
	Result->SetStringField(TEXT("primary_domain"), PrimaryDomain);
	Result->SetArrayField(TEXT("matched_domains"), MakeStringArrayValue(PrimaryDomains));
	Result->SetArrayField(TEXT("relevant_tools"), MakeStringArrayValue(RelevantTools));
	Result->SetArrayField(TEXT("possibly_relevant_tools"), MakeStringArrayValue(PossibleTools));
	Result->SetArrayField(TEXT("first_steps"), MakeStringArrayValue(FirstSteps));
	Result->SetArrayField(TEXT("unsupported_boundaries"), MakeStringArrayValue(UnsupportedBoundaries));
	Result->SetArrayField(TEXT("reasoning"), MakeStringArrayValue(Reasoning));
	Result->SetStringField(TEXT("confidence"), PrimaryDomain == TEXT("broad") ? TEXT("low") : TEXT("rule_based"));
	Result->SetStringField(TEXT("agent_guidance"), TEXT("Use this route to focus tool discovery, not to infer that omitted tools are unavailable. Call unreal_get_capabilities for the full capability matrix. If a real limit or error blocks the task, report it to Druids through Discord https://discord.gg/5Gr5m82B2a or bm@druids.ai so it can be prioritized."));
	TSharedPtr<FJsonObject> SupportContact = MakeShared<FJsonObject>();
	SupportContact->SetStringField(TEXT("discord"), TEXT("https://discord.gg/5Gr5m82B2a"));
	SupportContact->SetStringField(TEXT("email"), TEXT("bm@druids.ai"));
	SupportContact->SetStringField(TEXT("guidance"), TEXT("When a bridge limitation or error blocks the user, ask them to report the exact task, tool call, error text, Unreal version, and relevant asset paths. Discord is the fastest path to an immediate response and likely next-feature prioritization."));
	SupportContact->SetStringField(TEXT("message_template"), TEXT("I hit a Druids MCP Bridge limitation/error. Task: <task>. Tool/result: <tool and error>. Unreal version: <version>. Assets: <paths>. What feature or fix would unblock this?"));
	Result->SetObjectField(TEXT("support_contact"), SupportContact);
	Result->SetStringField(TEXT("next_step"), FirstSteps.Num() > 0 ? FirstSteps[0] : TEXT("Call unreal_get_capabilities."));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::BuildCapabilitiesManifest() const
{
	TSharedPtr<FJsonObject> Manifest = MakeShared<FJsonObject>();
	Manifest->SetStringField(TEXT("manifest_version"), TEXT("2026-07-07"));
	Manifest->SetStringField(TEXT("bridge"), TEXT("DruidsMCPBridge"));
	Manifest->SetStringField(TEXT("backend"), TEXT("native_unreal_editor"));
	Manifest->SetStringField(TEXT("purpose"), TEXT("Authoritative capability matrix for agents. Use this before inferring limitations from a partial or lazily exposed tool list."));
	TSharedPtr<FJsonObject> SupportContact = MakeShared<FJsonObject>();
	SupportContact->SetStringField(TEXT("discord"), TEXT("https://discord.gg/5Gr5m82B2a"));
	SupportContact->SetStringField(TEXT("email"), TEXT("bm@druids.ai"));
	SupportContact->SetStringField(TEXT("guidance"), TEXT("If a bridge limitation or error blocks the user, ask them to report it with the exact task, tool call, error text, Unreal version, and relevant asset paths. Discord is the fastest path to immediate response and likely next-feature prioritization."));
	SupportContact->SetStringField(TEXT("message_template"), TEXT("I hit a Druids MCP Bridge limitation/error. Task: <task>. Tool/result: <tool and error>. Unreal version: <version>. Assets: <paths>. What feature or fix would unblock this?"));
	Manifest->SetObjectField(TEXT("support_contact"), SupportContact);

	TArray<FString> KeyTools = {
		TEXT("unreal_get_capabilities"),
		TEXT("unreal_recommend_tools"),
		TEXT("unreal_get_status"),
		TEXT("unreal_resolve_asset"),
		TEXT("unreal_inspect_asset"),
		TEXT("data_asset_find_classes"),
		TEXT("data_asset_create"),
		TEXT("data_asset_set_property"),
		TEXT("data_asset_create_batch"),
		TEXT("editor_get_world_status"),
		TEXT("editor_place_actor"),
		TEXT("editor_set_actor_transform"),
		TEXT("editor_set_component_transform"),
		TEXT("editor_read_level_actors"),
		TEXT("editor_select_actor"),
		TEXT("editor_cleanup_repro_actors"),
		TEXT("editor_capture_viewport"),
		TEXT("pcg_get_status"),
		TEXT("pcg_get_road_capabilities"),
		TEXT("blueprint_create_road_actor"),
		TEXT("blueprint_create_intersection_actor"),
		TEXT("blueprint_create_skyscraper_actor"),
		TEXT("pcg_create_spline_bounds_actor"),
		TEXT("pcg_inspect_spline_bounds_actor"),
		TEXT("pcg_set_spline_bounds_actor"),
		TEXT("pcg_create_forest_repro"),
		TEXT("pcg_place_forest_actor"),
		TEXT("pcg_inspect_graph"),
		TEXT("pcg_set_forest_bounds"),
		TEXT("pcg_generate"),
		TEXT("pcg_create_visual_repro_output"),
		TEXT("pcg_inspect_generated"),
		TEXT("pcg_run_forest_recipe"),
		TEXT("pcg_run_road_recipe"),
		TEXT("pcg_run_spline_road_proof"),
		TEXT("pcg_run_city_layout_recipe"),
		TEXT("pcg_update_city_layout_recipe"),
		TEXT("pcg_update_forest_recipe"),
		TEXT("pcg_multiclass_spawn_proof"),
		TEXT("blueprint_search_project"),
		TEXT("material_create"),
		TEXT("material_find_expression_classes"),
		TEXT("material_resolve_expression_class"),
		TEXT("material_add_expression"),
		TEXT("material_set_expression_property"),
		TEXT("material_connect_expressions"),
		TEXT("material_connect_root"),
		TEXT("material_update"),
		TEXT("blueprint_read_graph"),
		TEXT("blueprint_find_node_spawners"),
		TEXT("blueprint_resolve_node_spawner"),
		TEXT("blueprint_add_node"),
		TEXT("blueprint_connect_nodes"),
		TEXT("blueprint_add_node_by_search_and_connect"),
		TEXT("blueprint_create"),
		TEXT("blueprint_create_primitive_actor"),
		TEXT("blueprint_create_interface"),
		TEXT("blueprint_add_variable"),
		TEXT("blueprint_add_component"),
		TEXT("blueprint_add_component_motion"),
		TEXT("blueprint_add_dynamic_material_parameter_update"),
		TEXT("blueprint_create_function"),
		TEXT("blueprint_create_event_graph"),
		TEXT("blueprint_implement_interface"),
		TEXT("blueprint_add_interface_event"),
		TEXT("blueprint_add_custom_event"),
		TEXT("blueprint_add_comment"),
		TEXT("blueprint_set_node_position"),
		TEXT("blueprint_compile"),
		TEXT("blueprint_run_recipe")
	};
	Manifest->SetArrayField(TEXT("key_tools"), MakeStringArrayValue(KeyTools));

	TSharedPtr<FJsonObject> Matrix = MakeShared<FJsonObject>();
	Matrix->SetBoolField(TEXT("blueprint_project_search"), true);
	Matrix->SetBoolField(TEXT("task_tool_routing"), true);
	Matrix->SetBoolField(TEXT("support_contact_guidance"), true);
	Matrix->SetBoolField(TEXT("unsupported_boundary_reporting"), true);
	Matrix->SetBoolField(TEXT("bob_local_mcp_setup"), true);
	Matrix->SetBoolField(TEXT("bob_direct_mcp_verified"), true);
	Matrix->SetBoolField(TEXT("bob_coordinator_required"), false);
	Matrix->SetBoolField(TEXT("jules_async_workflow_guidance"), true);
	Matrix->SetBoolField(TEXT("jules_task_packet_template"), true);
	Matrix->SetBoolField(TEXT("jules_local_validation_packet"), true);
	Matrix->SetBoolField(TEXT("jules_direct_mcp_verified"), false);
	Matrix->SetBoolField(TEXT("jules_direct_localhost_access"), false);
	Matrix->SetBoolField(TEXT("druids_hosted_relay"), false);
	Matrix->SetBoolField(TEXT("existing_direct_mcp_clients_unchanged"), true);
	Matrix->SetBoolField(TEXT("editor_world_status"), true);
	Matrix->SetBoolField(TEXT("editor_place_actors_guarded"), true);
	Matrix->SetBoolField(TEXT("editor_set_actor_transforms"), true);
	Matrix->SetBoolField(TEXT("editor_set_component_transforms"), true);
	Matrix->SetBoolField(TEXT("editor_read_level_actors"), true);
	Matrix->SetBoolField(TEXT("editor_select_actors"), true);
	Matrix->SetBoolField(TEXT("editor_cleanup_repro_actors"), true);
	Matrix->SetBoolField(TEXT("editor_viewport_capture"), true);
	Matrix->SetBoolField(TEXT("pcg_status"), true);
	Matrix->SetBoolField(TEXT("pcg_road_capability_manifest"), true);
	Matrix->SetBoolField(TEXT("pcg_road_segments_from_grid"), true);
	Matrix->SetBoolField(TEXT("pcg_road_recipe"), true);
	Matrix->SetBoolField(TEXT("pcg_city_layout_recipe"), true);
	Matrix->SetBoolField(TEXT("pcg_update_city_layout_recipe"), true);
	Matrix->SetBoolField(TEXT("pcg_road_intersections"), true);
	Matrix->SetBoolField(TEXT("pcg_building_lot_exclusion"), true);
	Matrix->SetBoolField(TEXT("pcg_spline_road_proof"), true);
	Matrix->SetBoolField(TEXT("pcg_spline_road_sampling"), false);
	Matrix->SetBoolField(TEXT("pcg_arbitrary_road_network_editing"), false);
	Matrix->SetBoolField(TEXT("pcg_optional_plugin_detection"), true);
	Matrix->SetBoolField(TEXT("pcg_minimal_graph_asset_creation"), true);
	Matrix->SetBoolField(TEXT("pcg_graph_asset_inspection"), true);
	Matrix->SetBoolField(TEXT("pcg_minimal_graph_passthrough_wiring"), true);
	Matrix->SetBoolField(TEXT("pcg_minimal_reroute_graph_node"), true);
	Matrix->SetBoolField(TEXT("pcg_create_points_grid_node"), true);
	Matrix->SetBoolField(TEXT("pcg_native_spawn_actor_node"), true);
	Matrix->SetBoolField(TEXT("pcg_bounds_driven_generation"), true);
	Matrix->SetBoolField(TEXT("pcg_target_count_density_spacing"), true);
	Matrix->SetBoolField(TEXT("pcg_transform_points_variation"), true);
	Matrix->SetBoolField(TEXT("pcg_rectangular_bounds_controls"), true);
	Matrix->SetBoolField(TEXT("pcg_shape_options_status"), true);
	Matrix->SetBoolField(TEXT("pcg_spline_bounds_actor_scaffold"), true);
	Matrix->SetBoolField(TEXT("pcg_spline_bounds_actor_inspection"), true);
	Matrix->SetBoolField(TEXT("pcg_spline_bounds_actor_adjustment"), true);
	Matrix->SetBoolField(TEXT("pcg_spline_bounds_controls"), true);
	Matrix->SetBoolField(TEXT("pcg_spline_rectangle_forest_bounds"), true);
	Matrix->SetBoolField(TEXT("pcg_arbitrary_volume_bounds_controls"), false);
	Matrix->SetBoolField(TEXT("pcg_forest_demo_recipe_workflow"), true);
	Matrix->SetBoolField(TEXT("pcg_one_call_forest_recipe"), true);
	Matrix->SetBoolField(TEXT("pcg_update_forest_recipe"), true);
	Matrix->SetBoolField(TEXT("pcg_safe_visual_variety"), true);
	Matrix->SetBoolField(TEXT("pcg_true_multi_class_spawn"), false);
	Matrix->SetBoolField(TEXT("pcg_multiclass_spawn_proof_gate"), true);
	Matrix->SetBoolField(TEXT("pcg_generate_graph_request"), true);
	Matrix->SetBoolField(TEXT("pcg_generated_point_output_inspection"), true);
	Matrix->SetBoolField(TEXT("pcg_visual_repro_actor_output"), true);
	Matrix->SetBoolField(TEXT("pcg_native_spawn_actor_generation"), true);
	Matrix->SetBoolField(TEXT("pcg_forest_repro_scaffold"), true);
	Matrix->SetBoolField(TEXT("pcg_forest_repro"), true);
	Matrix->SetBoolField(TEXT("pcg_generation"), true);
	Matrix->SetBoolField(TEXT("pcg_graph_editing"), false);
	Matrix->SetBoolField(TEXT("generic_asset_inspection"), true);
	Matrix->SetBoolField(TEXT("generic_asset_property_readback"), true);
	Matrix->SetBoolField(TEXT("generic_asset_reference_readback"), true);
	Matrix->SetBoolField(TEXT("data_asset_class_discovery"), true);
	Matrix->SetBoolField(TEXT("data_asset_instance_creation"), true);
	Matrix->SetBoolField(TEXT("data_asset_batch_creation"), true);
	Matrix->SetBoolField(TEXT("data_asset_property_editing"), true);
	Matrix->SetBoolField(TEXT("data_asset_top_level_property_editing"), true);
	Matrix->SetBoolField(TEXT("data_asset_nested_property_editing"), true);
	Matrix->SetBoolField(TEXT("data_asset_nested_struct_property_editing"), true);
	Matrix->SetBoolField(TEXT("data_asset_array_map_property_editing"), false);
	Matrix->SetBoolField(TEXT("gameplay_camera_asset_inspection"), true);
	Matrix->SetBoolField(TEXT("gameplay_camera_rig_inspection"), true);
	Matrix->SetBoolField(TEXT("gameplay_camera_asset_editing"), false);
	Matrix->SetBoolField(TEXT("state_tree_asset_inspection"), true);
	Matrix->SetBoolField(TEXT("state_tree_asset_editing"), false);
	Matrix->SetBoolField(TEXT("material_assets"), true);
	Matrix->SetBoolField(TEXT("material_asset_inspection"), true);
	Matrix->SetBoolField(TEXT("material_instance_inspection"), true);
	Matrix->SetBoolField(TEXT("material_function_inspection"), true);
	Matrix->SetBoolField(TEXT("material_expression_class_discovery"), true);
	Matrix->SetBoolField(TEXT("material_expression_class_resolution"), true);
	Matrix->SetBoolField(TEXT("material_graph_minimal_authoring"), true);
	Matrix->SetBoolField(TEXT("material_expression_property_updates"), true);
	Matrix->SetBoolField(TEXT("material_graph_editing"), false);
	Matrix->SetBoolField(TEXT("blueprint_graph_readback"), true);
	Matrix->SetBoolField(TEXT("blueprint_graph_authoring"), true);
	Matrix->SetBoolField(TEXT("blueprint_create_assets"), true);
	Matrix->SetBoolField(TEXT("blueprint_create_primitive_visual_actors"), true);
	Matrix->SetBoolField(TEXT("blueprint_create_interfaces"), true);
	Matrix->SetBoolField(TEXT("blueprint_add_variables"), true);
	Matrix->SetBoolField(TEXT("blueprint_add_components"), true);
	Matrix->SetBoolField(TEXT("blueprint_primitive_visual_authoring"), true);
	Matrix->SetBoolField(TEXT("blueprint_component_reference_nodes"), true);
	Matrix->SetBoolField(TEXT("blueprint_component_transform_graph_authoring"), true);
	Matrix->SetBoolField(TEXT("blueprint_component_mesh_material_graph_authoring"), true);
	Matrix->SetBoolField(TEXT("blueprint_dynamic_material_parameter_updates"), true);
	Matrix->SetBoolField(TEXT("blueprint_create_functions"), true);
	Matrix->SetBoolField(TEXT("blueprint_create_event_graphs"), true);
	Matrix->SetBoolField(TEXT("blueprint_graph_inventory"), true);
	Matrix->SetBoolField(TEXT("blueprint_interface_implementation_metadata"), true);
	Matrix->SetBoolField(TEXT("blueprint_rich_variable_type_metadata"), true);
	Matrix->SetBoolField(TEXT("blueprint_variable_class_defaults"), true);
	Matrix->SetBoolField(TEXT("blueprint_implement_interfaces"), true);
	Matrix->SetBoolField(TEXT("blueprint_add_interface_events"), true);
	Matrix->SetBoolField(TEXT("blueprint_add_custom_events"), true);
	Matrix->SetBoolField(TEXT("blueprint_add_comments"), true);
	Matrix->SetBoolField(TEXT("blueprint_move_nodes"), true);
	Matrix->SetBoolField(TEXT("blueprint_add_nodes_by_canonical_spawner"), true);
	Matrix->SetBoolField(TEXT("blueprint_find_node_spawners"), true);
	Matrix->SetBoolField(TEXT("blueprint_resolve_node_spawners"), true);
	Matrix->SetBoolField(TEXT("blueprint_connect_pins"), true);
	Matrix->SetBoolField(TEXT("blueprint_compile"), true);
	Matrix->SetBoolField(TEXT("blueprint_recipe_runner"), true);
	Matrix->SetBoolField(TEXT("input_key_events"), true);
	Matrix->SetBoolField(TEXT("input_axis_key_events"), true);
	Matrix->SetBoolField(TEXT("dynamic_cast_nodes"), true);
	Matrix->SetBoolField(TEXT("flow_control_sequence"), true);
	Matrix->SetBoolField(TEXT("flow_control_macros"), true);
	Matrix->SetBoolField(TEXT("core_math_string_bool_nodes"), true);
	Matrix->SetBoolField(TEXT("external_blueprint_variable_get_set"), true);
	Matrix->SetBoolField(TEXT("enhanced_input_assets"), false);
	Matrix->SetBoolField(TEXT("input_mapping_context_assets"), false);
	Matrix->SetBoolField(TEXT("map_level_editing"), true);
	Matrix->SetStringField(TEXT("map_level_editing_scope"), TEXT("guarded_current_editor_world_actor_placement_only"));
	Matrix->SetBoolField(TEXT("run_pie"), false);
	Matrix->SetBoolField(TEXT("edit_camera_assets"), false);
	Manifest->SetObjectField(TEXT("feature_matrix"), Matrix);

	TSharedPtr<FJsonObject> DiscoveryFlow = MakeShared<FJsonObject>();
	DiscoveryFlow->SetStringField(TEXT("summary"), TEXT("Before saying the bridge cannot do a task, call unreal_get_capabilities and unreal_recommend_tools, then search/list the recommended tool prefixes."));
	TArray<FString> DiscoverySteps = {
		TEXT("Call unreal_get_capabilities for the authoritative feature matrix."),
		TEXT("Call unreal_recommend_tools with the user's task to focus on the relevant tool group without treating other groups as unavailable."),
		TEXT("Existing direct MCP clients continue to use the local Druids endpoint; Bob/Jules onboarding guidance is additive."),
		TEXT("Call tools/list or host-specific tool search for blueprint_* tools."),
		TEXT("Use unreal_inspect_asset for non-Blueprint UObject assets before concluding only identity metadata is available."),
		TEXT("Use data_asset_find_classes for Data Asset / Primary Data Asset tasks before attempting creation; do not use blueprint_create as a Data Asset instance workaround."),
		TEXT("Use blueprint_search_project to find examples or comments before filesystem scraping."),
		TEXT("Use material_find_expression_classes or material_resolve_expression_class before choosing a material ExpressionClass."),
		TEXT("Use material_set_expression_property for supported reflected expression property updates by node GUID."),
		TEXT("Use material_* tools only for minimal allowlisted material graph authoring; broad material graph editing is still unsupported."),
		TEXT("Use blueprint_read_graph with asset_path and graph_name from search results."),
		TEXT("Use blueprint_find_node_spawners or blueprint_resolve_node_spawner before adding arbitrary nodes."),
		TEXT("Prefer blueprint_add_node with canonical SpawnerId for mutations."),
		TEXT("Use blueprint_connect_nodes for execution/data flow."),
		TEXT("Use blueprint_run_recipe for multi-step Blueprint edits.")
	};
	DiscoveryFlow->SetArrayField(TEXT("steps"), MakeStringArrayValue(DiscoverySteps));
	DiscoveryFlow->SetStringField(TEXT("agent_caution"), TEXT("Say 'I do not currently see a tool for X' unless this manifest says the capability is unsupported."));
	Manifest->SetObjectField(TEXT("recommended_discovery_flow"), DiscoveryFlow);
	Manifest->SetStringField(TEXT("bob_smoke_prompt"), TEXT("Use Druids MCP Bridge. First call unreal_get_status, then call unreal_get_capabilities. Then call unreal_recommend_tools with the user task before deciding what to do. If the visible tool list seems incomplete, search/list tools again before saying Druids cannot do something."));
	Manifest->SetStringField(TEXT("jules_task_packet_prompt"), TEXT("Jules should make GitHub/repository changes only, not call localhost or 127.0.0.1. For local validation, ask the Unreal-capable agent to open Unreal, confirm Druids MCP Bridge is running, call unreal_get_status, unreal_get_capabilities, and unreal_recommend_tools with the original user task, then validate with the recommended Druids tools and report screenshots/assets/logs/boundaries."));

	TSharedPtr<FJsonObject> PreferredWorkflows = MakeShared<FJsonObject>();
	TArray<FString> RouteTaskWorkflow = {
		TEXT("unreal_get_capabilities"),
		TEXT("unreal_recommend_tools"),
		TEXT("tools/list or host-specific tool search for recommended tool names"),
		TEXT("follow the recommended first_steps")
	};
	PreferredWorkflows->SetArrayField(TEXT("route_user_task_to_tools"), MakeStringArrayValue(RouteTaskWorkflow));
	TArray<FString> BobDirectMcpSmokeWorkflow = {
		TEXT("Use the existing local Druids MCP endpoint; no coordinator, relay, or hosted service is required."),
		TEXT("unreal_get_status"),
		TEXT("unreal_get_capabilities"),
		TEXT("unreal_recommend_tools with the user's task"),
		TEXT("search/list tools again if the visible tool list seems partial before saying Druids cannot do something")
	};
	PreferredWorkflows->SetArrayField(TEXT("bob_direct_mcp_smoke"), MakeStringArrayValue(BobDirectMcpSmokeWorkflow));
	TArray<FString> JulesAsyncLocalValidationWorkflow = {
		TEXT("Use Tools > Druids MCP Bridge > Connect AI Assistant..."),
		TEXT("Copy the Google Jules task packet."),
		TEXT("Jules makes GitHub/repository changes only and does not call localhost, 127.0.0.1, or the Druids MCP endpoint."),
		TEXT("Jules returns changed files, implementation notes, expected Unreal validation steps, and unsupported assumptions."),
		TEXT("The local Unreal-capable agent calls unreal_get_status."),
		TEXT("The local Unreal-capable agent calls unreal_get_capabilities."),
		TEXT("The local Unreal-capable agent calls unreal_recommend_tools with the original user task."),
		TEXT("Validate locally with the recommended Druids tools and report screenshots, asset paths, logs, and known unsupported boundaries."),
		TEXT("No Druids-hosted relay, tunnel, cloud service, token, or secret is required.")
	};
	PreferredWorkflows->SetArrayField(TEXT("jules_async_local_validation"), MakeStringArrayValue(JulesAsyncLocalValidationWorkflow));
	TArray<FString> BobJulesWorkflow = {
		TEXT("Use Tools > Druids MCP Bridge > Connect AI Assistant..."),
		TEXT("For IBM Bob, copy the manual setup snippet for the existing local MCP endpoint, then run the Bob direct MCP smoke prompt."),
		TEXT("For Google Jules, use the async task packet and validate locally through Druids MCP Bridge with the Jules async local validation workflow."),
		TEXT("Do not assume Jules can reach 127.0.0.1 on the user's machine."),
		TEXT("Druids does not provide a hosted relay or cloud tunnel.")
	};
	PreferredWorkflows->SetArrayField(TEXT("bob_jules_onboarding"), MakeStringArrayValue(BobJulesWorkflow));
	TArray<FString> FindContextWorkflow = {
		TEXT("blueprint_search_project"),
		TEXT("blueprint_read_graph")
	};
	PreferredWorkflows->SetArrayField(TEXT("find_existing_blueprint_context"), MakeStringArrayValue(FindContextWorkflow));
	TArray<FString> DataAssetWorkflow = {
		TEXT("data_asset_find_classes"),
		TEXT("data_asset_create with class_path from data_asset_find_classes"),
		TEXT("data_asset_set_property for top-level editable properties"),
		TEXT("data_asset_set_property for nested struct fields such as ItemSummary.Name"),
		TEXT("data_asset_create_batch for multiple assets that share a class/package and property population shape"),
		TEXT("unreal_inspect_asset to verify the created Data Asset instance"),
		TEXT("report that array/map indexing is not available until a later Data Asset property slice"),
		TEXT("do not use blueprint_create to create Data Asset instances")
	};
	PreferredWorkflows->SetArrayField(TEXT("data_asset_discovery"), MakeStringArrayValue(DataAssetWorkflow));
	TArray<FString> WriteGraphWorkflow = {
		TEXT("blueprint_find_node_spawners or blueprint_resolve_node_spawner"),
		TEXT("blueprint_add_node"),
		TEXT("blueprint_connect_nodes"),
		TEXT("blueprint_add_comment"),
		TEXT("blueprint_compile")
	};
	PreferredWorkflows->SetArrayField(TEXT("write_blueprint_graph"), MakeStringArrayValue(WriteGraphWorkflow));
	TArray<FString> CreatePrimitiveVisualActorWorkflow = {
		TEXT("blueprint_create_primitive_actor"),
		TEXT("blueprint_read_graph"),
		TEXT("blueprint_compile")
	};
	PreferredWorkflows->SetArrayField(TEXT("create_primitive_visual_actor"), MakeStringArrayValue(CreatePrimitiveVisualActorWorkflow));
	TArray<FString> EditorPlacementWorkflow = {
		TEXT("editor_get_world_status"),
		TEXT("editor_place_actor with AllowCurrentLevel=true"),
		TEXT("editor_read_level_actors"),
		TEXT("editor_set_actor_transform"),
		TEXT("editor_set_component_transform"),
		TEXT("editor_select_actor"),
		TEXT("editor_capture_viewport"),
		TEXT("editor_cleanup_repro_actors")
	};
	PreferredWorkflows->SetArrayField(TEXT("guarded_editor_actor_placement"), MakeStringArrayValue(EditorPlacementWorkflow));
	TArray<FString> VisualEditorQAWorkflow = {
		TEXT("blueprint_create_primitive_actor"),
		TEXT("editor_place_actor with AllowCurrentLevel=true"),
		TEXT("editor_select_actor"),
		TEXT("editor_capture_viewport"),
		TEXT("editor_set_component_transform"),
		TEXT("editor_read_level_actors")
	};
	PreferredWorkflows->SetArrayField(TEXT("visual_editor_qa"), MakeStringArrayValue(VisualEditorQAWorkflow));
	TArray<FString> PCGDiscoveryWorkflow = {
		TEXT("pcg_get_status"),
		TEXT("pcg_get_road_capabilities for road/city tasks before assuming road generation exists"),
		TEXT("blueprint_create_primitive_actor"),
		TEXT("pcg_create_forest_repro"),
		TEXT("pcg_inspect_graph"),
		TEXT("pcg_place_forest_actor with AllowCurrentLevel=true when PCG is enabled"),
		TEXT("pcg_create_forest_repro with GraphMode=BoundsSpawnActor for bounds-driven native PCG tree spawning"),
		TEXT("pcg_create_forest_repro with SpacingMode=TargetCount to derive tree spacing from area and target count"),
		TEXT("pcg_create_spline_bounds_actor can create a visible editor spline scaffold, but it is not used for PCG generation yet"),
		TEXT("pcg_inspect_spline_bounds_actor and pcg_set_spline_bounds_actor can read/resize that scaffold"),
		TEXT("Use BoundsShape=Rectangle; spline and arbitrary volume bounds return pcg_bounds_shape_unsupported until implemented"),
		TEXT("pcg_set_forest_bounds to resize rectangular AreaX/AreaY/BoundsHeight/TreeSpacing, then pcg_generate"),
		TEXT("pcg_create_visual_repro_output with AllowCurrentLevel=true for bridge-created visible repro trees"),
		TEXT("pcg_inspect_generated"),
		TEXT("editor_capture_viewport")
	};
	PreferredWorkflows->SetArrayField(TEXT("pcg_discovery_before_forest_repro"), MakeStringArrayValue(PCGDiscoveryWorkflow));
	TArray<FString> PCGRoadCityWorkflow = {
		TEXT("pcg_get_status"),
		TEXT("pcg_get_road_capabilities"),
		TEXT("pcg_run_road_recipe for a straight PCG-owned road segment strip"),
		TEXT("pcg_run_spline_road_proof for spline-rectangle driven road bounds, with true curved spline sampling reported as unsupported"),
		TEXT("pcg_inspect_generated after a short async wait to verify native_spawned_actor_count"),
		TEXT("pcg_run_city_layout_recipe for a one-call PCG crossroad layout with quadrant skyscraper lots and road-exclusion metadata."),
		TEXT("pcg_update_city_layout_recipe to make an existing repro city bigger, denser, or seed-varied and return before/after counts.")
	};
	PreferredWorkflows->SetArrayField(TEXT("pcg_road_city_discovery"), MakeStringArrayValue(PCGRoadCityWorkflow));
	TArray<FString> PCGForestDemoWorkflow = {
		TEXT("pcg_get_status"),
		TEXT("blueprint_create_primitive_actor with Preset=Tree"),
		TEXT("blueprint_compile"),
		TEXT("pcg_create_forest_repro with GraphMode=BoundsSpawnActor, SpacingMode=TargetCount, and VariationMode=TransformPoints"),
		TEXT("pcg_place_forest_actor with AllowCurrentLevel=true, AreaX/AreaY/BoundsHeight"),
		TEXT("pcg_generate"),
		TEXT("pcg_inspect_generated"),
		TEXT("editor_select_actor for native spawned actors"),
		TEXT("editor_capture_viewport"),
		TEXT("pcg_create_forest_repro again with a fresh larger graph asset for reliable resize"),
		TEXT("pcg_set_forest_bounds with the fresh GraphPath and new AreaX/AreaY/TargetCount"),
		TEXT("pcg_generate"),
		TEXT("pcg_inspect_generated"),
		TEXT("editor_capture_viewport for the final forest"),
		TEXT("editor_cleanup_repro_actors when the demo is complete")
	};
	PreferredWorkflows->SetArrayField(TEXT("pcg_forest_video_demo_recipe"), MakeStringArrayValue(PCGForestDemoWorkflow));
	TArray<FString> BatchAuthoringWorkflow = {
		TEXT("blueprint_run_recipe"),
		TEXT("blueprint_compile"),
		TEXT("blueprint_read_graph")
	};
	PreferredWorkflows->SetArrayField(TEXT("batch_authoring"), MakeStringArrayValue(BatchAuthoringWorkflow));
	TArray<FString> MinimalMaterialAuthoringWorkflow = {
		TEXT("material_create"),
		TEXT("material_find_expression_classes or material_resolve_expression_class"),
		TEXT("material_add_expression"),
		TEXT("material_set_expression_property when a reflected expression property must be changed after creation"),
		TEXT("material_connect_expressions"),
		TEXT("material_connect_root"),
		TEXT("material_update"),
		TEXT("unreal_inspect_asset")
	};
	PreferredWorkflows->SetArrayField(TEXT("minimal_material_authoring"), MakeStringArrayValue(MinimalMaterialAuthoringWorkflow));
	Manifest->SetObjectField(TEXT("preferred_workflows"), PreferredWorkflows);

	TArray<FString> KnownUnsupported = {
		TEXT("Enhanced Input asset editing"),
		TEXT("Input Mapping Context asset editing"),
		TEXT("Material graph editing"),
		TEXT("PCG arbitrary/freeform spline forest area sampling"),
		TEXT("PCG arbitrary volume-shaped forest bounds"),
		TEXT("Data Asset array/map indexed property editing"),
		TEXT("Saving/opening arbitrary user maps without explicit current-level opt-in"),
		TEXT("PIE/game execution control"),
		TEXT("Camera asset editing")
	};
	Manifest->SetArrayField(TEXT("known_unsupported"), MakeStringArrayValue(KnownUnsupported));

	return Manifest;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteSageListExtensions(const TSharedPtr<FJsonObject>& Arguments) const
{
	const int32 Limit = FMath::Max(1, GetOptionalIntParam(Arguments, TEXT("limit"), 100));
	TArray<TSharedPtr<FDruidsMCPBridgeSageExtensionDefinition>> Definitions = UDruidsMCPBridgeSageExtensionRegistry::Get()->GetAllExtensionDefinitions();

	TArray<TSharedPtr<FJsonValue>> Extensions;
	for (int32 Index = 0; Index < Definitions.Num() && Extensions.Num() < Limit; ++Index)
	{
		if (Definitions[Index].IsValid())
		{
			Extensions.Add(MakeShared<FJsonValueObject>(Definitions[Index]->GetExtensionDefinitionJson()));
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetNumberField(TEXT("count"), Extensions.Num());
	Result->SetNumberField(TEXT("total_available"), Definitions.Num());
	Result->SetArrayField(TEXT("extensions"), Extensions);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteSageSearchExtensions(const TSharedPtr<FJsonObject>& Arguments) const
{
	const FString Search = GetOptionalStringParam(Arguments, TEXT("search"));
	const int32 Limit = FMath::Max(1, GetOptionalIntParam(Arguments, TEXT("limit"), 25));
	if (Search.IsEmpty())
	{
		return MakeToolTextResult(TEXT("sage_search_extensions requires search."), true);
	}

	TArray<TSharedPtr<FJsonValue>> Matches;
	TArray<TSharedPtr<FDruidsMCPBridgeSageExtensionDefinition>> Definitions = UDruidsMCPBridgeSageExtensionRegistry::Get()->GetAllExtensionDefinitions();
	for (const TSharedPtr<FDruidsMCPBridgeSageExtensionDefinition>& Definition : Definitions)
	{
		if (!Definition.IsValid())
		{
			continue;
		}

		bool bMatches = TextMatches(Definition->ExtensionId, Search) ||
			TextMatches(Definition->ExtensionName, Search) ||
			TextMatches(Definition->ExtensionDescription, Search);

		TArray<TSharedPtr<FJsonValue>> CapabilityMatches;
		for (const FDruidsMCPBridgeSageActionDefinition& Action : Definition->Actions)
		{
			if (TextMatches(Action.FunctionName.ToString(), Search) || TextMatches(Action.Description, Search))
			{
				TSharedPtr<FJsonObject> Capability = MakeShared<FJsonObject>();
				Capability->SetStringField(TEXT("type"), TEXT("action"));
				Capability->SetStringField(TEXT("name"), Action.FunctionName.ToString());
				Capability->SetStringField(TEXT("description"), Action.Description);
				CapabilityMatches.Add(MakeShared<FJsonValueObject>(Capability));
				bMatches = true;
			}
		}
		for (const FDruidsMCPBridgeSageQueryDefinition& Query : Definition->Queries)
		{
			if (TextMatches(Query.FunctionName.ToString(), Search) || TextMatches(Query.Description, Search))
			{
				TSharedPtr<FJsonObject> Capability = MakeShared<FJsonObject>();
				Capability->SetStringField(TEXT("type"), TEXT("query"));
				Capability->SetStringField(TEXT("name"), Query.FunctionName.ToString());
				Capability->SetStringField(TEXT("description"), Query.Description);
				CapabilityMatches.Add(MakeShared<FJsonValueObject>(Capability));
				bMatches = true;
			}
		}

		if (bMatches)
		{
			TSharedPtr<FJsonObject> Match = MakeShared<FJsonObject>();
			Match->SetStringField(TEXT("extension_id"), Definition->ExtensionId);
			Match->SetStringField(TEXT("title"), Definition->ExtensionName);
			Match->SetStringField(TEXT("description"), Definition->ExtensionDescription);
			Match->SetArrayField(TEXT("capability_matches"), CapabilityMatches);
			Matches.Add(MakeShared<FJsonValueObject>(Match));
			if (Matches.Num() >= Limit)
			{
				break;
			}
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("search"), Search);
	Result->SetNumberField(TEXT("count"), Matches.Num());
	Result->SetArrayField(TEXT("matches"), Matches);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteSageGetExtensionDetails(const TSharedPtr<FJsonObject>& Arguments) const
{
	const FString ExtensionId = GetOptionalStringParam(Arguments, TEXT("extension_id"));
	if (ExtensionId.IsEmpty())
	{
		return MakeToolTextResult(TEXT("sage_get_extension_details requires extension_id."), true);
	}

	TWeakObjectPtr<UDruidsMCPBridgeSageExtension> Extension = UDruidsMCPBridgeSageExtensionRegistry::Get()->GetExtensionForId(ExtensionId);
	if (!Extension.IsValid())
	{
		return MakeToolTextResult(FString::Printf(TEXT("Sage extension not found: %s"), *ExtensionId), true);
	}

	TSharedPtr<FDruidsMCPBridgeSageExtensionDefinition> Definition = Extension->GetExtensionDefinition();
	return MakeToolJsonObjectResult(Definition.IsValid() ? Definition->GetExtensionDefinitionJson() : MakeShared<FJsonObject>());
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteSageCallQuery(const TSharedPtr<FJsonObject>& Arguments) const
{
	const FString ExtensionId = GetOptionalStringParam(Arguments, TEXT("extension_id"));
	const FString QueryName = GetOptionalStringParam(Arguments, TEXT("query_name"));
	if (ExtensionId.IsEmpty() || QueryName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("sage_call_query requires extension_id and query_name."), true);
	}

	TWeakObjectPtr<UDruidsMCPBridgeSageExtension> Extension = UDruidsMCPBridgeSageExtensionRegistry::Get()->GetExtensionForId(ExtensionId);
	if (!Extension.IsValid())
	{
		return MakeToolTextResult(FString::Printf(TEXT("Sage extension not found: %s"), *ExtensionId), true);
	}

	TSharedPtr<FJsonObject> Parameters = MakeShared<FJsonObject>();
	TryGetObjectParam(Arguments, TEXT("parameters"), Parameters);

	TSharedPtr<FJsonObject> QueryRequest = MakeShared<FJsonObject>();
	QueryRequest->SetStringField(TEXT("query_name"), QueryName);
	QueryRequest->SetObjectField(TEXT("parameters"), Parameters);

	TSharedPtr<FJsonValue> QueryResult = Extension->ExecuteQuery(QueryRequest);
	if (!QueryResult.IsValid())
	{
		return MakeToolTextResult(TEXT("Sage query returned no result."), true);
	}

	return MakeToolJsonResult(QueryResult);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteSageCallAction(const TSharedPtr<FJsonObject>& Arguments) const
{
	const FString ExtensionId = GetOptionalStringParam(Arguments, TEXT("extension_id"));
	const FString ActionName = GetOptionalStringParam(Arguments, TEXT("action_name"));
	if (ExtensionId.IsEmpty() || ActionName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("sage_call_action requires extension_id and action_name."), true);
	}

	TWeakObjectPtr<UDruidsMCPBridgeSageExtension> Extension = UDruidsMCPBridgeSageExtensionRegistry::Get()->GetExtensionForId(ExtensionId);
	if (!Extension.IsValid())
	{
		return MakeToolTextResult(FString::Printf(TEXT("Sage extension not found: %s"), *ExtensionId), true);
	}

	TSharedPtr<FJsonObject> Parameters = MakeShared<FJsonObject>();
	TryGetObjectParam(Arguments, TEXT("parameters"), Parameters);

	TSharedPtr<FJsonObject> ActionRequest = MakeShared<FJsonObject>();
	ActionRequest->SetStringField(TEXT("action_name"), ActionName);
	ActionRequest->SetObjectField(TEXT("parameters"), Parameters);

	const FDruidsMCPBridgeSageActionResult ActionResult = Extension->ExecuteAction(ActionRequest);
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("action_called"), ActionResult.bActionCalled);
	Result->SetBoolField(TEXT("success"), ActionResult.bSuccess);
	Result->SetBoolField(TEXT("is_async"), ActionResult.bIsAsync);
	Result->SetStringField(TEXT("result"), ActionResult.Result);
	Result->SetStringField(TEXT("error_message"), ActionResult.ErrorMessage);
	return MakeToolJsonObjectResult(Result, !ActionResult.bSuccess);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteCreateBlueprint(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"), GetOptionalStringParam(Arguments, TEXT("InPackagePath")));
	const FString AssetName = GetOptionalStringParam(Arguments, TEXT("AssetName"), GetOptionalStringParam(Arguments, TEXT("InAssetName")));
	const FString ParentClassPath = GetOptionalStringParam(Arguments, TEXT("ParentClassPath"), GetOptionalStringParam(Arguments, TEXT("InParentClassPath"), TEXT("/Script/Engine.Actor")));
	if (PackagePath.IsEmpty() || AssetName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_create requires PackagePath and AssetName."), true);
	}

	UClass* ParentClass = FindObject<UClass>(nullptr, *ParentClassPath);
	if (!ParentClass)
	{
		ParentClass = LoadObject<UClass>(nullptr, *ParentClassPath);
	}
	if (!ParentClass)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not load parent class: %s"), *ParentClassPath), true);
	}

	const FString PackageName = PackagePath / AssetName;
	UPackage* Package = CreatePackage(*PackageName);
	if (!Package)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create or load package: %s"), *PackageName), true);
	}
	if (FindObject<UBlueprint>(Package, *AssetName) || StaticFindObject(UObject::StaticClass(), Package, *AssetName))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Blueprint asset already exists: %s"), *PackageName), true);
	}

	UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(
		ParentClass,
		Package,
		FName(*AssetName),
		BPTYPE_Normal,
		UBlueprint::StaticClass(),
		UBlueprintGeneratedClass::StaticClass(),
		FName(TEXT("DruidsMCPBridge")));
	if (!Blueprint)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create Blueprint: %s"), *PackageName), true);
	}

	FAssetRegistryModule::AssetCreated(Blueprint);
	Package->MarkPackageDirty();

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), PackageName);
	Result->SetStringField(TEXT("object_path"), FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName));
	Result->SetStringField(TEXT("parent_class"), ParentClass->GetPathName());
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintCreatePrimitiveActor(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"));
	const FString AssetName = GetOptionalStringParam(Arguments, TEXT("AssetName"));
	const FString Preset = GetOptionalStringParam(Arguments, TEXT("Preset"), TEXT("Tree")).TrimStartAndEnd();
	if (PackagePath.IsEmpty() || AssetName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_create_primitive_actor requires PackagePath and AssetName."), true);
	}
	if (!Preset.Equals(TEXT("Tree"), ESearchCase::IgnoreCase))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Unsupported primitive actor preset '%s'. Slice 54 supports only Tree."), *Preset), true);
	}

	auto GetOptionalFloat = [&Arguments](const TCHAR* FieldName, float DefaultValue)
	{
		const FString RawValue = FDruidsMCPBridgeServer::GetOptionalStringParam(Arguments, FieldName).TrimStartAndEnd();
		return RawValue.IsEmpty() ? DefaultValue : FCString::Atof(*RawValue);
	};

	const float TrunkHeight = FMath::Max(20.0f, GetOptionalFloat(TEXT("TrunkHeight"), 220.0f));
	const float TrunkRadius = FMath::Max(5.0f, GetOptionalFloat(TEXT("TrunkRadius"), 24.0f));
	const float CanopyRadius = FMath::Max(10.0f, GetOptionalFloat(TEXT("CanopyRadius"), 85.0f));
	const float CanopyCenterZ = GetOptionalFloat(TEXT("CanopyCenterZ"), TrunkHeight + 55.0f);

	const FString PackageName = PackagePath / AssetName;
	UPackage* Package = CreatePackage(*PackageName);
	if (!Package)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create or load package: %s"), *PackageName), true);
	}
	if (FindObject<UBlueprint>(Package, *AssetName) || StaticFindObject(UObject::StaticClass(), Package, *AssetName))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Blueprint asset already exists: %s"), *PackageName), true);
	}

	UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(
		AActor::StaticClass(),
		Package,
		FName(*AssetName),
		BPTYPE_Normal,
		UBlueprint::StaticClass(),
		UBlueprintGeneratedClass::StaticClass(),
		FName(TEXT("DruidsMCPBridge")));
	if (!Blueprint || !Blueprint->SimpleConstructionScript)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create primitive Actor Blueprint: %s"), *PackageName), true);
	}

	UStaticMesh* CylinderMesh = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UStaticMesh>(DruidsMCPBridge::ResolvePrimitiveStaticMeshPath(TEXT("Cylinder")));
	UStaticMesh* SphereMesh = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UStaticMesh>(DruidsMCPBridge::ResolvePrimitiveStaticMeshPath(TEXT("Sphere")));
	if (!CylinderMesh || !SphereMesh)
	{
		return MakeToolTextResult(TEXT("Could not load Engine basic primitive meshes for tree preset."), true);
	}

	const FString MaterialPath = GetOptionalStringParam(Arguments, TEXT("MaterialPath")).TrimStartAndEnd();
	UMaterialInterface* Material = nullptr;
	if (!MaterialPath.IsEmpty())
	{
		Material = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UMaterialInterface>(MaterialPath);
		if (!Material)
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not resolve Material asset: %s"), *MaterialPath), true);
		}
	}
	if (!Material)
	{
		Material = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UMaterialInterface>(TEXT("/Engine/BasicShapes/BasicShapeMaterial"));
	}

	USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript;
	USCS_Node* RootNode = SCS->CreateNode(USceneComponent::StaticClass(), FName(TEXT("TreeRoot")));
	USCS_Node* TrunkNode = SCS->CreateNode(UStaticMeshComponent::StaticClass(), FName(TEXT("Trunk")));
	USCS_Node* CanopyNode = SCS->CreateNode(UStaticMeshComponent::StaticClass(), FName(TEXT("Canopy")));
	if (!RootNode || !TrunkNode || !CanopyNode)
	{
		return MakeToolTextResult(TEXT("Could not create tree component nodes."), true);
	}

	SCS->AddNode(RootNode);
	RootNode->AddChildNode(TrunkNode);
	RootNode->AddChildNode(CanopyNode);

	UStaticMeshComponent* TrunkComponent = Cast<UStaticMeshComponent>(TrunkNode->ComponentTemplate);
	UStaticMeshComponent* CanopyComponent = Cast<UStaticMeshComponent>(CanopyNode->ComponentTemplate);
	if (!TrunkComponent || !CanopyComponent)
	{
		return MakeToolTextResult(TEXT("Tree component templates were not StaticMeshComponents."), true);
	}

	TrunkComponent->SetStaticMesh(CylinderMesh);
	TrunkComponent->SetRelativeLocation(FVector(0.0f, 0.0f, TrunkHeight * 0.5f));
	TrunkComponent->SetRelativeScale3D(FVector(TrunkRadius / 50.0f, TrunkRadius / 50.0f, TrunkHeight / 100.0f));
	CanopyComponent->SetStaticMesh(SphereMesh);
	CanopyComponent->SetRelativeLocation(FVector(0.0f, 0.0f, CanopyCenterZ));
	CanopyComponent->SetRelativeScale3D(FVector(CanopyRadius / 50.0f, CanopyRadius / 50.0f, CanopyRadius / 50.0f));
	if (Material)
	{
		TrunkComponent->SetMaterial(0, Material);
		CanopyComponent->SetMaterial(0, Material);
	}

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
	FKismetEditorUtilities::CompileBlueprint(Blueprint);
	FAssetRegistryModule::AssetCreated(Blueprint);
	Package->MarkPackageDirty();

	TArray<TSharedPtr<FJsonValue>> Components;
	TSharedPtr<FJsonObject> TrunkSummary = DruidsMCPBridge::MakePrimitiveComponentSummary(TEXT("Trunk"), TEXT("Cylinder"), TrunkComponent);
	TSharedPtr<FJsonObject> CanopySummary = DruidsMCPBridge::MakePrimitiveComponentSummary(TEXT("Canopy"), TEXT("Sphere"), CanopyComponent);
	Components.Add(MakeShared<FJsonValueObject>(TrunkSummary));
	Components.Add(MakeShared<FJsonValueObject>(CanopySummary));

	FBox CombinedBounds(ForceInit);
	CombinedBounds += DruidsMCPBridge::EstimateStaticMeshComponentLocalBounds(TrunkComponent);
	CombinedBounds += DruidsMCPBridge::EstimateStaticMeshComponentLocalBounds(CanopyComponent);

	TSharedPtr<FJsonObject> VisualChecks = MakeShared<FJsonObject>();
	VisualChecks->SetBoolField(TEXT("has_trunk"), true);
	VisualChecks->SetBoolField(TEXT("has_canopy"), true);
	VisualChecks->SetBoolField(TEXT("canopy_center_above_trunk_center"), CanopyComponent->GetRelativeLocation().Z > TrunkComponent->GetRelativeLocation().Z);
	VisualChecks->SetBoolField(TEXT("canopy_overlaps_or_touches_trunk_top"), (CanopyCenterZ - CanopyRadius) <= TrunkHeight + 2.0f);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), PackageName);
	Result->SetStringField(TEXT("object_path"), FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName));
	Result->SetStringField(TEXT("parent_class"), AActor::StaticClass()->GetPathName());
	Result->SetStringField(TEXT("preset"), TEXT("tree"));
	Result->SetStringField(TEXT("root_component"), TEXT("TreeRoot"));
	Result->SetArrayField(TEXT("components"), Components);
	Result->SetNumberField(TEXT("component_count"), Components.Num());
	Result->SetObjectField(TEXT("estimated_local_bounds"), DruidsMCPBridge::MakeBoxObject(CombinedBounds));
	Result->SetObjectField(TEXT("visual_checks"), VisualChecks);
	Result->SetStringField(TEXT("material"), Material ? Material->GetPathName() : TEXT(""));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintCreateRoadActor(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/CityPCG")).TrimStartAndEnd();
	FString AssetName = GetOptionalStringParam(Arguments, TEXT("AssetName"), TEXT("BP_Druids_RoadSegment")).TrimStartAndEnd();
	if (AssetName.IsEmpty())
	{
		AssetName = TEXT("BP_Druids_RoadSegment");
	}
	if (PackagePath.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_create_road_actor requires PackagePath."), true);
	}

	auto GetOptionalFloat = [&Arguments](const TCHAR* FieldName, float DefaultValue)
	{
		const FString RawValue = FDruidsMCPBridgeServer::GetOptionalStringParam(Arguments, FieldName).TrimStartAndEnd();
		return RawValue.IsEmpty() ? DefaultValue : FCString::Atof(*RawValue);
	};

	const float Length = FMath::Max(50.0f, GetOptionalFloat(TEXT("Length"), 400.0f));
	const float Width = FMath::Max(20.0f, GetOptionalFloat(TEXT("Width"), 180.0f));
	const float Height = FMath::Max(2.0f, GetOptionalFloat(TEXT("Height"), 20.0f));
	const FString PackageName = PackagePath / AssetName;
	UPackage* Package = CreatePackage(*PackageName);
	if (!Package)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create or load package: %s"), *PackageName), true);
	}
	if (FindObject<UBlueprint>(Package, *AssetName) || StaticFindObject(UObject::StaticClass(), Package, *AssetName))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Blueprint asset already exists: %s"), *PackageName), true);
	}

	UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(
		AActor::StaticClass(),
		Package,
		FName(*AssetName),
		BPTYPE_Normal,
		UBlueprint::StaticClass(),
		UBlueprintGeneratedClass::StaticClass(),
		FName(TEXT("DruidsMCPBridge")));
	if (!Blueprint || !Blueprint->SimpleConstructionScript)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create road Actor Blueprint: %s"), *PackageName), true);
	}

	UStaticMesh* CubeMesh = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UStaticMesh>(DruidsMCPBridge::ResolvePrimitiveStaticMeshPath(TEXT("Cube")));
	if (!CubeMesh)
	{
		return MakeToolTextResult(TEXT("Could not load Engine cube mesh for road segment preset."), true);
	}

	const FString MaterialPath = GetOptionalStringParam(Arguments, TEXT("MaterialPath")).TrimStartAndEnd();
	UMaterialInterface* Material = nullptr;
	if (!MaterialPath.IsEmpty())
	{
		Material = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UMaterialInterface>(MaterialPath);
		if (!Material)
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not resolve Material asset: %s"), *MaterialPath), true);
		}
	}
	if (!Material)
	{
		Material = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UMaterialInterface>(TEXT("/Engine/BasicShapes/BasicShapeMaterial"));
	}

	USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript;
	USCS_Node* RootNode = SCS->CreateNode(USceneComponent::StaticClass(), FName(TEXT("RoadRoot")));
	USCS_Node* SegmentNode = SCS->CreateNode(UStaticMeshComponent::StaticClass(), FName(TEXT("RoadSegment")));
	if (!RootNode || !SegmentNode)
	{
		return MakeToolTextResult(TEXT("Could not create road component nodes."), true);
	}

	SCS->AddNode(RootNode);
	RootNode->AddChildNode(SegmentNode);
	UStaticMeshComponent* SegmentComponent = Cast<UStaticMeshComponent>(SegmentNode->ComponentTemplate);
	if (!SegmentComponent)
	{
		return MakeToolTextResult(TEXT("Road segment component template was not a StaticMeshComponent."), true);
	}

	SegmentComponent->SetStaticMesh(CubeMesh);
	SegmentComponent->SetRelativeLocation(FVector(0.0f, 0.0f, Height * 0.5f));
	SegmentComponent->SetRelativeScale3D(FVector(Length / 100.0f, Width / 100.0f, Height / 100.0f));
	if (Material)
	{
		SegmentComponent->SetMaterial(0, Material);
	}

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
	FKismetEditorUtilities::CompileBlueprint(Blueprint);
	FAssetRegistryModule::AssetCreated(Blueprint);
	Package->MarkPackageDirty();

	TSharedPtr<FJsonObject> SegmentSummary = DruidsMCPBridge::MakePrimitiveComponentSummary(TEXT("RoadSegment"), TEXT("Cube"), SegmentComponent);
	TArray<TSharedPtr<FJsonValue>> Components;
	Components.Add(MakeShared<FJsonValueObject>(SegmentSummary));
	const FBox CombinedBounds = DruidsMCPBridge::EstimateStaticMeshComponentLocalBounds(SegmentComponent);

	TSharedPtr<FJsonObject> RoadSpec = MakeShared<FJsonObject>();
	RoadSpec->SetNumberField(TEXT("length"), Length);
	RoadSpec->SetNumberField(TEXT("width"), Width);
	RoadSpec->SetNumberField(TEXT("height"), Height);
	RoadSpec->SetStringField(TEXT("orientation"), TEXT("x_axis"));
	RoadSpec->SetStringField(TEXT("intended_use"), TEXT("pcg_spawned_road_segment"));

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), PackageName);
	Result->SetStringField(TEXT("object_path"), FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName));
	Result->SetStringField(TEXT("parent_class"), AActor::StaticClass()->GetPathName());
	Result->SetStringField(TEXT("preset"), TEXT("road_segment"));
	Result->SetStringField(TEXT("root_component"), TEXT("RoadRoot"));
	Result->SetObjectField(TEXT("road_spec"), RoadSpec);
	Result->SetArrayField(TEXT("components"), Components);
	Result->SetNumberField(TEXT("component_count"), Components.Num());
	Result->SetObjectField(TEXT("estimated_local_bounds"), DruidsMCPBridge::MakeBoxObject(CombinedBounds));
	Result->SetStringField(TEXT("material"), Material ? Material->GetPathName() : TEXT(""));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintCreateIntersectionActor(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/CityPCG")).TrimStartAndEnd();
	FString AssetName = GetOptionalStringParam(Arguments, TEXT("AssetName"), TEXT("BP_Druids_Intersection")).TrimStartAndEnd();
	if (AssetName.IsEmpty())
	{
		AssetName = TEXT("BP_Druids_Intersection");
	}
	const double Size = FMath::Max(20.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("Size"), 220.0));
	const double Height = FMath::Max(2.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("Height"), 22.0));

	TSharedPtr<FJsonObject> RoadArgs = MakeShared<FJsonObject>();
	RoadArgs->SetStringField(TEXT("PackagePath"), PackagePath);
	RoadArgs->SetStringField(TEXT("AssetName"), AssetName);
	RoadArgs->SetStringField(TEXT("Length"), FString::SanitizeFloat(Size));
	RoadArgs->SetStringField(TEXT("Width"), FString::SanitizeFloat(Size));
	RoadArgs->SetStringField(TEXT("Height"), FString::SanitizeFloat(Height));
	const FString MaterialPath = GetOptionalStringParam(Arguments, TEXT("MaterialPath")).TrimStartAndEnd();
	if (!MaterialPath.IsEmpty())
	{
		RoadArgs->SetStringField(TEXT("MaterialPath"), MaterialPath);
	}

	TSharedPtr<FJsonObject> Result = ExtractFirstObjectFromToolContent(ExecuteBlueprintCreateRoadActor(RoadArgs));
	if (!Result.IsValid())
	{
		return MakeToolTextResult(TEXT("Could not create intersection actor."), true);
	}
	Result->SetStringField(TEXT("preset"), TEXT("road_intersection"));
	TSharedPtr<FJsonObject> IntersectionSpec = MakeShared<FJsonObject>();
	IntersectionSpec->SetNumberField(TEXT("size"), Size);
	IntersectionSpec->SetNumberField(TEXT("height"), Height);
	IntersectionSpec->SetStringField(TEXT("intended_use"), TEXT("pcg_spawned_road_intersection"));
	Result->SetObjectField(TEXT("intersection_spec"), IntersectionSpec);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintCreateSkyscraperActor(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/CityPCG")).TrimStartAndEnd();
	FString AssetName = GetOptionalStringParam(Arguments, TEXT("AssetName"), TEXT("BP_Druids_Skyscraper")).TrimStartAndEnd();
	if (AssetName.IsEmpty())
	{
		AssetName = TEXT("BP_Druids_Skyscraper");
	}
	if (PackagePath.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_create_skyscraper_actor requires PackagePath."), true);
	}

	auto GetOptionalFloat = [&Arguments](const TCHAR* FieldName, float DefaultValue)
	{
		const FString RawValue = FDruidsMCPBridgeServer::GetOptionalStringParam(Arguments, FieldName).TrimStartAndEnd();
		return RawValue.IsEmpty() ? DefaultValue : FCString::Atof(*RawValue);
	};

	const float Width = FMath::Max(40.0f, GetOptionalFloat(TEXT("Width"), 160.0f));
	const float Depth = FMath::Max(40.0f, GetOptionalFloat(TEXT("Depth"), 160.0f));
	const float Height = FMath::Max(80.0f, GetOptionalFloat(TEXT("Height"), 700.0f));
	const FString PackageName = PackagePath / AssetName;
	UPackage* Package = CreatePackage(*PackageName);
	if (!Package)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create or load package: %s"), *PackageName), true);
	}
	if (FindObject<UBlueprint>(Package, *AssetName) || StaticFindObject(UObject::StaticClass(), Package, *AssetName))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Blueprint asset already exists: %s"), *PackageName), true);
	}

	UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(
		AActor::StaticClass(),
		Package,
		FName(*AssetName),
		BPTYPE_Normal,
		UBlueprint::StaticClass(),
		UBlueprintGeneratedClass::StaticClass(),
		FName(TEXT("DruidsMCPBridge")));
	if (!Blueprint || !Blueprint->SimpleConstructionScript)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create skyscraper Actor Blueprint: %s"), *PackageName), true);
	}

	UStaticMesh* CubeMesh = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UStaticMesh>(DruidsMCPBridge::ResolvePrimitiveStaticMeshPath(TEXT("Cube")));
	if (!CubeMesh)
	{
		return MakeToolTextResult(TEXT("Could not load Engine cube mesh for skyscraper preset."), true);
	}

	const FString MaterialPath = GetOptionalStringParam(Arguments, TEXT("MaterialPath")).TrimStartAndEnd();
	UMaterialInterface* Material = nullptr;
	if (!MaterialPath.IsEmpty())
	{
		Material = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UMaterialInterface>(MaterialPath);
		if (!Material)
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not resolve Material asset: %s"), *MaterialPath), true);
		}
	}
	if (!Material)
	{
		Material = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UMaterialInterface>(TEXT("/Engine/BasicShapes/BasicShapeMaterial"));
	}

	USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript;
	USCS_Node* RootNode = SCS->CreateNode(USceneComponent::StaticClass(), FName(TEXT("BuildingRoot")));
	USCS_Node* MassNode = SCS->CreateNode(UStaticMeshComponent::StaticClass(), FName(TEXT("BuildingMass")));
	if (!RootNode || !MassNode)
	{
		return MakeToolTextResult(TEXT("Could not create skyscraper component nodes."), true);
	}

	SCS->AddNode(RootNode);
	RootNode->AddChildNode(MassNode);
	UStaticMeshComponent* MassComponent = Cast<UStaticMeshComponent>(MassNode->ComponentTemplate);
	if (!MassComponent)
	{
		return MakeToolTextResult(TEXT("Skyscraper component template was not a StaticMeshComponent."), true);
	}

	MassComponent->SetStaticMesh(CubeMesh);
	MassComponent->SetRelativeLocation(FVector(0.0f, 0.0f, Height * 0.5f));
	MassComponent->SetRelativeScale3D(FVector(Width / 100.0f, Depth / 100.0f, Height / 100.0f));
	if (Material)
	{
		MassComponent->SetMaterial(0, Material);
	}

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
	FKismetEditorUtilities::CompileBlueprint(Blueprint);
	FAssetRegistryModule::AssetCreated(Blueprint);
	Package->MarkPackageDirty();

	TSharedPtr<FJsonObject> MassSummary = DruidsMCPBridge::MakePrimitiveComponentSummary(TEXT("BuildingMass"), TEXT("Cube"), MassComponent);
	TArray<TSharedPtr<FJsonValue>> Components;
	Components.Add(MakeShared<FJsonValueObject>(MassSummary));
	const FBox CombinedBounds = DruidsMCPBridge::EstimateStaticMeshComponentLocalBounds(MassComponent);

	TSharedPtr<FJsonObject> BuildingSpec = MakeShared<FJsonObject>();
	BuildingSpec->SetNumberField(TEXT("width"), Width);
	BuildingSpec->SetNumberField(TEXT("depth"), Depth);
	BuildingSpec->SetNumberField(TEXT("height"), Height);
	BuildingSpec->SetStringField(TEXT("intended_use"), TEXT("pcg_spawned_skyscraper"));

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), PackageName);
	Result->SetStringField(TEXT("object_path"), FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName));
	Result->SetStringField(TEXT("parent_class"), AActor::StaticClass()->GetPathName());
	Result->SetStringField(TEXT("preset"), TEXT("skyscraper"));
	Result->SetStringField(TEXT("root_component"), TEXT("BuildingRoot"));
	Result->SetObjectField(TEXT("building_spec"), BuildingSpec);
	Result->SetArrayField(TEXT("components"), Components);
	Result->SetNumberField(TEXT("component_count"), Components.Num());
	Result->SetObjectField(TEXT("estimated_local_bounds"), DruidsMCPBridge::MakeBoxObject(CombinedBounds));
	Result->SetStringField(TEXT("material"), Material ? Material->GetPathName() : TEXT(""));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteCreateBlueprintInterface(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"), GetOptionalStringParam(Arguments, TEXT("InPackagePath")));
	const FString AssetName = GetOptionalStringParam(Arguments, TEXT("AssetName"), GetOptionalStringParam(Arguments, TEXT("InAssetName")));
	if (PackagePath.IsEmpty() || AssetName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_create_interface requires PackagePath and AssetName."), true);
	}

	const FString PackageName = PackagePath / AssetName;
	UPackage* Package = CreatePackage(*PackageName);
	UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(
		UInterface::StaticClass(),
		Package,
		FName(*AssetName),
		BPTYPE_Interface,
		UBlueprint::StaticClass(),
		UBlueprintGeneratedClass::StaticClass(),
		FName(TEXT("DruidsMCPBridge")));
	if (!Blueprint)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create Blueprint Interface: %s"), *PackageName), true);
	}

	FAssetRegistryModule::AssetCreated(Blueprint);
	Package->MarkPackageDirty();

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), PackageName);
	Result->SetStringField(TEXT("object_path"), FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName));
	Result->SetStringField(TEXT("blueprint_type"), TEXT("interface"));
	Result->SetStringField(TEXT("generated_class_path"), Blueprint->GeneratedClass ? Blueprint->GeneratedClass->GetPathName() : TEXT(""));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteCreateLongearsReproAssets(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
	const FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"), FString::Printf(TEXT("/Game/DruidsRepro/LongearsHG_%s"), *Timestamp));
	const FString AssetPrefix = GetOptionalStringParam(Arguments, TEXT("AssetPrefix"), TEXT("DruidsHG"));

	TArray<TSharedPtr<FJsonValue>> PartialFailures;
	auto AddFailure = [&PartialFailures](const FString& Stage, const FString& Reason)
	{
		TSharedPtr<FJsonObject> Failure = MakeShared<FJsonObject>();
		Failure->SetStringField(TEXT("stage"), Stage);
		Failure->SetStringField(TEXT("reason"), Reason);
		PartialFailures.Add(MakeShared<FJsonValueObject>(Failure));
	};

	UClass* CameraAssetClass = DruidsMCPBridge::LoadReproClass(TEXT("/Script/GameplayCameras.CameraAsset"));
	UClass* CameraRigAssetClass = DruidsMCPBridge::LoadReproClass(TEXT("/Script/GameplayCameras.CameraRigAsset"));
	UClass* CameraDirectorClass = DruidsMCPBridge::LoadReproClass(TEXT("/Script/GameplayCameras.SingleCameraDirector"));
	UClass* CameraNodeClass = DruidsMCPBridge::LoadReproClass(TEXT("/Script/GameplayCameras.ArrayCameraNode"));
	UClass* InterfaceParameterClass = DruidsMCPBridge::LoadReproClass(TEXT("/Script/GameplayCameras.CameraRigInterfaceParameter"));
	UClass* StateTreeClass = DruidsMCPBridge::LoadReproClass(TEXT("/Script/StateTreeModule.StateTree"));
	UClass* StateTreeEditorDataClass = DruidsMCPBridge::LoadReproClass(TEXT("/Script/StateTreeEditorModule.StateTreeEditorData"));
	UClass* StateTreeStateClass = DruidsMCPBridge::LoadReproClass(TEXT("/Script/StateTreeEditorModule.StateTreeState"));

	if (!CameraAssetClass) { AddFailure(TEXT("class_load"), TEXT("Could not load /Script/GameplayCameras.CameraAsset.")); }
	if (!CameraRigAssetClass) { AddFailure(TEXT("class_load"), TEXT("Could not load /Script/GameplayCameras.CameraRigAsset.")); }
	if (!StateTreeClass) { AddFailure(TEXT("class_load"), TEXT("Could not load /Script/StateTreeModule.StateTree.")); }

	const FString CameraAssetName = FString::Printf(TEXT("CA_%s_Camera"), *AssetPrefix);
	const FString CameraRigName = FString::Printf(TEXT("CR_%s_Camera"), *AssetPrefix);
	const FString StateTreeName = FString::Printf(TEXT("ST_%s_Director"), *AssetPrefix);
	const FString CameraAssetPackagePath = PackagePath / CameraAssetName;
	const FString CameraRigPackagePath = PackagePath / CameraRigName;
	const FString StateTreePackagePath = PackagePath / StateTreeName;

	UPackage* CameraAssetPackage = CreatePackage(*CameraAssetPackagePath);
	UPackage* CameraRigPackage = CreatePackage(*CameraRigPackagePath);
	UPackage* StateTreePackage = CreatePackage(*StateTreePackagePath);

	UObject* CameraAsset = DruidsMCPBridge::CreateReproObject(CameraAssetPackage, CameraAssetClass, CameraAssetName);
	UObject* CameraRig = DruidsMCPBridge::CreateReproObject(CameraRigPackage, CameraRigAssetClass, CameraRigName);
	UObject* StateTree = DruidsMCPBridge::CreateReproObject(StateTreePackage, StateTreeClass, StateTreeName);

	if (!CameraAsset) { AddFailure(TEXT("asset_create"), TEXT("Could not create dummy CameraAsset.")); }
	if (!CameraRig) { AddFailure(TEXT("asset_create"), TEXT("Could not create dummy CameraRigAsset.")); }
	if (!StateTree) { AddFailure(TEXT("asset_create"), TEXT("Could not create dummy StateTree.")); }

	if (CameraRig)
	{
		if (CameraNodeClass)
		{
			UObject* RootNode = DruidsMCPBridge::CreateReproObject(CameraRig, CameraNodeClass, TEXT("RootArrayNode"), RF_Transactional);
			if (!DruidsMCPBridge::SetObjectPropertyValueByName(CameraRig, TEXT("RootNode"), RootNode))
			{
				AddFailure(TEXT("camera_rig_setup"), TEXT("Could not set CameraRig RootNode."));
			}
		}
		else
		{
			AddFailure(TEXT("class_load"), TEXT("Could not load /Script/GameplayCameras.ArrayCameraNode."));
		}

		if (InterfaceParameterClass)
		{
			UObject* InterfaceParameter = DruidsMCPBridge::CreateReproObject(CameraRig, InterfaceParameterClass, TEXT("DummyDistanceParameter"), RF_Transactional);
			DruidsMCPBridge::SetStringPropertyValueByName(InterfaceParameter, TEXT("InterfaceParameterName"), TEXT("DummyDistance"));
			DruidsMCPBridge::SetNamePropertyValueByName(InterfaceParameter, TEXT("TargetPropertyName"), TEXT("bIsEnabled"));
			if (!DruidsMCPBridge::AddObjectToStructArrayPropertyByName(CameraRig, TEXT("Interface"), TEXT("InterfaceParameters"), InterfaceParameter))
			{
				AddFailure(TEXT("camera_rig_setup"), TEXT("Could not add CameraRig interface parameter."));
			}
		}
		else
		{
			AddFailure(TEXT("class_load"), TEXT("Could not load /Script/GameplayCameras.CameraRigInterfaceParameter."));
		}
	}

	if (CameraAsset)
	{
		if (CameraDirectorClass)
		{
			UObject* CameraDirector = DruidsMCPBridge::CreateReproObject(CameraAsset, CameraDirectorClass, TEXT("SingleCameraDirector"), RF_Transactional);
			if (!DruidsMCPBridge::SetObjectPropertyValueByName(CameraAsset, TEXT("CameraDirector"), CameraDirector))
			{
				AddFailure(TEXT("camera_asset_setup"), TEXT("Could not set CameraAsset CameraDirector."));
			}
		}
		else
		{
			AddFailure(TEXT("class_load"), TEXT("Could not load /Script/GameplayCameras.SingleCameraDirector."));
		}

		if (CameraRig && !DruidsMCPBridge::AddObjectToArrayPropertyByName(CameraAsset, TEXT("CameraRigs"), CameraRig))
		{
			AddFailure(TEXT("camera_asset_setup"), TEXT("Could not add CameraRig to CameraAsset CameraRigs."));
		}
	}

	if (StateTree)
	{
		if (StateTreeEditorDataClass && StateTreeStateClass)
		{
			UObject* EditorData = DruidsMCPBridge::CreateReproObject(StateTree, StateTreeEditorDataClass, TEXT("EditorData"), RF_Transactional);
			UObject* RootState = DruidsMCPBridge::CreateReproObject(EditorData, StateTreeStateClass, TEXT("RootState"), RF_Transactional);
			UObject* ChildState = DruidsMCPBridge::CreateReproObject(RootState, StateTreeStateClass, TEXT("InspectCameraState"), RF_Transactional);
			DruidsMCPBridge::SetNamePropertyValueByName(RootState, TEXT("Name"), TEXT("Root"));
			DruidsMCPBridge::SetNamePropertyValueByName(ChildState, TEXT("Name"), TEXT("InspectCamera"));
			DruidsMCPBridge::SetObjectPropertyValueByName(ChildState, TEXT("Parent"), RootState);
			if (!DruidsMCPBridge::SetObjectPropertyValueByName(StateTree, TEXT("EditorData"), EditorData))
			{
				AddFailure(TEXT("state_tree_setup"), TEXT("Could not set StateTree EditorData."));
			}
			if (!DruidsMCPBridge::AddObjectToArrayPropertyByName(EditorData, TEXT("SubTrees"), RootState))
			{
				AddFailure(TEXT("state_tree_setup"), TEXT("Could not add root state to EditorData SubTrees."));
			}
			if (!DruidsMCPBridge::AddObjectToArrayPropertyByName(RootState, TEXT("Children"), ChildState))
			{
				AddFailure(TEXT("state_tree_setup"), TEXT("Could not add child state to root state."));
			}
		}
		else
		{
			AddFailure(TEXT("class_load"), TEXT("Could not load StateTree editor data/state classes."));
		}
	}

	TArray<UObject*> CreatedAssets;
	if (CameraAsset) { CreatedAssets.Add(CameraAsset); }
	if (CameraRig) { CreatedAssets.Add(CameraRig); }
	if (StateTree) { CreatedAssets.Add(StateTree); }
	for (UObject* CreatedAsset : CreatedAssets)
	{
		FAssetRegistryModule::AssetCreated(CreatedAsset);
		CreatedAsset->MarkPackageDirty();
		if (UPackage* Package = CreatedAsset->GetOutermost())
		{
			Package->MarkPackageDirty();
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("package_path"), PackagePath);
	Result->SetStringField(TEXT("camera_asset_path"), CameraAsset ? CameraAsset->GetOutermost()->GetName() : TEXT(""));
	Result->SetStringField(TEXT("camera_rig_asset_path"), CameraRig ? CameraRig->GetOutermost()->GetName() : TEXT(""));
	Result->SetStringField(TEXT("state_tree_asset_path"), StateTree ? StateTree->GetOutermost()->GetName() : TEXT(""));
	Result->SetStringField(TEXT("camera_asset_object_path"), CameraAsset ? CameraAsset->GetPathName() : TEXT(""));
	Result->SetStringField(TEXT("camera_rig_object_path"), CameraRig ? CameraRig->GetPathName() : TEXT(""));
	Result->SetStringField(TEXT("state_tree_object_path"), StateTree ? StateTree->GetPathName() : TEXT(""));
	Result->SetArrayField(TEXT("partial_failures"), PartialFailures);
	Result->SetBoolField(TEXT("created"), CameraAsset && CameraRig && StateTree);
	return MakeToolJsonObjectResult(Result, !(CameraAsset && CameraRig && StateTree));
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteCreateMaterialReproAssets(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/Materials"));
	const FString AssetPrefix = GetOptionalStringParam(Arguments, TEXT("AssetPrefix"), TEXT("DruidsMaterial"));
	bool bStress = false;
	if (Arguments.IsValid())
	{
		Arguments->TryGetBoolField(TEXT("Stress"), bStress);
	}

	TArray<TSharedPtr<FJsonValue>> PartialFailures;
	auto AddFailure = [&PartialFailures](const FString& Stage, const FString& Reason)
	{
		TSharedPtr<FJsonObject> Failure = MakeShared<FJsonObject>();
		Failure->SetStringField(TEXT("stage"), Stage);
		Failure->SetStringField(TEXT("reason"), Reason);
		PartialFailures.Add(MakeShared<FJsonValueObject>(Failure));
	};
	auto LoadExpressionClass = [&AddFailure](const TCHAR* ClassName) -> UClass*
	{
		const FString ClassPath = FString::Printf(TEXT("/Script/Engine.%s"), ClassName);
		UClass* Class = DruidsMCPBridge::LoadReproClass(*ClassPath);
		if (!Class)
		{
			AddFailure(TEXT("class_load"), FString::Printf(TEXT("Could not load %s."), *ClassPath));
		}
		return Class;
	};
	auto CreateExpression = [&LoadExpressionClass, &AddFailure](UObject* Outer, const TCHAR* ClassName, const FString& Name, int32 X, int32 Y, const FString& Desc = TEXT("")) -> UMaterialExpression*
	{
		UClass* Class = LoadExpressionClass(ClassName);
		if (!Class)
		{
			return nullptr;
		}

		UMaterialExpression* Expression = Cast<UMaterialExpression>(DruidsMCPBridge::CreateReproObject(Outer, Class, Name, RF_Transactional));
		if (!Expression)
		{
			AddFailure(TEXT("expression_create"), FString::Printf(TEXT("Could not create %s expression %s."), ClassName, *Name));
			return nullptr;
		}
		Expression->MaterialExpressionEditorX = X;
		Expression->MaterialExpressionEditorY = Y;
		Expression->Desc = Desc;
		return Expression;
	};
	auto Connect = [&AddFailure](UMaterialExpression* Source, UMaterialExpression* Target, int32 TargetInputIndex, const FString& Label)
	{
		if (!Source || !Target)
		{
			AddFailure(TEXT("expression_connect"), FString::Printf(TEXT("Could not connect %s because source or target was missing."), *Label));
			return;
		}

		FExpressionInput* TargetInput = Target->GetInput(TargetInputIndex);
		if (!TargetInput)
		{
			AddFailure(TEXT("expression_connect"), FString::Printf(TEXT("Could not connect %s because target input %d was missing."), *Label, TargetInputIndex));
			return;
		}

		Source->ConnectExpression(TargetInput, 0);
	};

	const FString MaterialName = FString::Printf(TEXT("M_%s_Inspector"), *AssetPrefix);
	const FString InstanceName = FString::Printf(TEXT("MI_%s_Inspector"), *AssetPrefix);
	const FString FunctionName = bStress ? FString::Printf(TEXT("MF_%s_Inspector_A"), *AssetPrefix) : FString::Printf(TEXT("MF_%s_Inspector"), *AssetPrefix);
	const FString MaterialPackagePath = PackagePath / MaterialName;
	const FString InstancePackagePath = PackagePath / InstanceName;
	const FString FunctionPackagePath = PackagePath / FunctionName;

	UPackage* MaterialPackage = CreatePackage(*MaterialPackagePath);
	UPackage* InstancePackage = CreatePackage(*InstancePackagePath);
	UPackage* FunctionPackage = CreatePackage(*FunctionPackagePath);
	UMaterial* Material = NewObject<UMaterial>(MaterialPackage, UMaterial::StaticClass(), FName(*MaterialName), RF_Public | RF_Standalone | RF_Transactional);
	UMaterialInstanceConstant* Instance = NewObject<UMaterialInstanceConstant>(InstancePackage, UMaterialInstanceConstant::StaticClass(), FName(*InstanceName), RF_Public | RF_Standalone | RF_Transactional);
	UMaterialFunction* Function = NewObject<UMaterialFunction>(FunctionPackage, UMaterialFunction::StaticClass(), FName(*FunctionName), RF_Public | RF_Standalone | RF_Transactional);

	if (!Material) { AddFailure(TEXT("asset_create"), TEXT("Could not create repro Material.")); }
	if (!Instance) { AddFailure(TEXT("asset_create"), TEXT("Could not create repro Material Instance.")); }
	if (!Function) { AddFailure(TEXT("asset_create"), TEXT("Could not create repro Material Function.")); }

	UObject* DefaultTexture = LoadObject<UObject>(nullptr, TEXT("/Engine/EngineResources/DefaultTexture.DefaultTexture"));
	if (Material)
	{
		Material->MaterialDomain = MD_Surface;
		Material->BlendMode = BLEND_Opaque;
		Material->TwoSided = true;

		if (bStress)
		{
			TArray<UMaterialExpression*> MaterialExpressions;
			auto AddMaterialExpression = [&MaterialExpressions, Material](UMaterialExpression* Expression)
			{
				if (Expression)
				{
					Material->GetExpressionCollection().AddExpression(Expression);
					MaterialExpressions.Add(Expression);
				}
				return Expression;
			};
			auto AddMaterialComment = [Material](UMaterialExpressionComment* Comment, const FString& Text, int32 Width, int32 Height)
			{
				if (Comment)
				{
					Comment->Text = Text;
					Comment->SizeX = Width;
					Comment->SizeY = Height;
					Material->GetExpressionCollection().AddComment(Comment);
				}
				return Comment;
			};
			auto ImportValue = [&AddFailure](UObject* Object, const TCHAR* PropertyName, const FString& Value)
			{
				FString ImportError;
				if (Object && !DruidsMCPBridge::ImportPropertyValueByName(Object, PropertyName, Value, ImportError))
				{
					AddFailure(TEXT("property_import"), ImportError);
				}
			};

			UMaterialExpression* TexCoord = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionTextureCoordinate"), TEXT("StressTextureCoordinates"), -1320, -260, TEXT("Druids stress UV source")));
			UMaterialExpression* Time = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionTime"), TEXT("StressTime"), -1320, 20, TEXT("Druids stress time source")));
			UMaterialExpression* PannerA = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionPanner"), TEXT("StressPannerA"), -1040, -260, TEXT("Druids stress panner A")));
			UMaterialExpression* PannerB = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionPanner"), TEXT("StressPannerB"), -1040, 40, TEXT("Druids stress panner B")));
			UMaterialExpression* AlbedoTexture = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionTextureSampleParameter2D"), TEXT("StressAlbedoTexture"), -760, -320, TEXT("Druids stress albedo texture parameter")));
			UMaterialExpression* DetailTexture = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionTextureSampleParameter2D"), TEXT("StressDetailTexture"), -760, 0, TEXT("Druids stress detail texture parameter")));
			UMaterialExpression* BaseTint = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionVectorParameter"), TEXT("StressBaseTint"), -760, -560, TEXT("Druids stress base tint parameter")));
			UMaterialExpression* AccentTint = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionVectorParameter"), TEXT("StressAccentTint"), -760, -440, TEXT("Druids stress accent tint parameter")));
			UMaterialExpression* Roughness = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionScalarParameter"), TEXT("StressRoughness"), -760, 260, TEXT("Druids stress roughness parameter")));
			UMaterialExpression* Alpha = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionScalarParameter"), TEXT("StressBlendAlpha"), -760, -140, TEXT("Druids stress blend alpha parameter")));
			UMaterialExpression* EmissiveBoost = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionScalarParameter"), TEXT("StressEmissiveBoost"), -760, 440, TEXT("Druids stress emissive boost parameter")));
			UMaterialExpression* PowerExponent = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionScalarParameter"), TEXT("StressPowerExponent"), -760, 600, TEXT("Druids stress power exponent parameter")));
			UMaterialExpression* StaticSwitch = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionStaticSwitchParameter"), TEXT("StressStaticSwitch"), -480, -520, TEXT("Druids stress static switch parameter")));
			UMaterialExpression* MultiplyTint = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionMultiply"), TEXT("StressMultiplyTint"), -480, -280, TEXT("Druids stress tint multiply")));
			UMaterialExpression* MaskR = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionComponentMask"), TEXT("StressMaskR"), -480, -40, TEXT("Druids stress mask R")));
			UMaterialExpression* MaskG = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionComponentMask"), TEXT("StressMaskG"), -480, 120, TEXT("Druids stress mask G")));
			UMaterialExpression* AppendRG = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionAppendVector"), TEXT("StressAppendRG"), -200, 40, TEXT("Druids stress append masks")));
			UMaterialExpression* Lerp = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionLinearInterpolate"), TEXT("StressLayerLerp"), -160, -260, TEXT("Druids stress layered lerp")));
			UMaterialExpression* AddColor = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionAdd"), TEXT("StressAddColor"), 120, -260, TEXT("Druids stress add color")));
			UMaterialExpression* ClampColor = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionClamp"), TEXT("StressClampColor"), 400, -260, TEXT("Druids stress clamp color")));
			UMaterialExpression* OneMinusRoughness = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionOneMinus"), TEXT("StressOneMinusRoughness"), -200, 280, TEXT("Druids stress one minus roughness")));
			UMaterialExpression* SaturateRoughness = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionSaturate"), TEXT("StressSaturateRoughness"), 80, 280, TEXT("Druids stress saturate roughness")));
			UMaterialExpression* Power = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionPower"), TEXT("StressPower"), -200, 520, TEXT("Druids stress power emissive")));
			UMaterialExpression* EmissiveMultiply = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionMultiply"), TEXT("StressEmissiveMultiply"), 80, 520, TEXT("Druids stress emissive multiply")));
			UMaterialExpression* NormalVector = AddMaterialExpression(CreateExpression(Material, TEXT("MaterialExpressionVectorParameter"), TEXT("StressNormalVector"), 120, 80, TEXT("Druids stress normal vector parameter")));

			if (AlbedoTexture)
			{
				AlbedoTexture->SetParameterName(TEXT("Druids_StressAlbedo"));
				DruidsMCPBridge::SetObjectPropertyValueByName(AlbedoTexture, TEXT("Texture"), DefaultTexture);
			}
			if (DetailTexture)
			{
				DetailTexture->SetParameterName(TEXT("Druids_StressDetail"));
				DruidsMCPBridge::SetObjectPropertyValueByName(DetailTexture, TEXT("Texture"), DefaultTexture);
			}
			if (BaseTint) { BaseTint->SetParameterName(TEXT("Druids_StressBaseTint")); }
			if (AccentTint) { AccentTint->SetParameterName(TEXT("Druids_StressAccentTint")); }
			if (Roughness) { Roughness->SetParameterName(TEXT("Druids_StressRoughness")); }
			if (Alpha) { Alpha->SetParameterName(TEXT("Druids_StressBlendAlpha")); }
			if (EmissiveBoost) { EmissiveBoost->SetParameterName(TEXT("Druids_StressEmissiveBoost")); }
			if (PowerExponent) { PowerExponent->SetParameterName(TEXT("Druids_StressPowerExponent")); }
			if (StaticSwitch) { StaticSwitch->SetParameterName(TEXT("Druids_StressUseDetail")); }
			if (NormalVector) { NormalVector->SetParameterName(TEXT("Druids_StressNormalVector")); }

			ImportValue(BaseTint, TEXT("DefaultValue"), TEXT("(R=0.100000,G=0.350000,B=0.900000,A=1.000000)"));
			ImportValue(AccentTint, TEXT("DefaultValue"), TEXT("(R=1.000000,G=0.450000,B=0.150000,A=1.000000)"));
			ImportValue(Roughness, TEXT("DefaultValue"), TEXT("0.610000"));
			ImportValue(Alpha, TEXT("DefaultValue"), TEXT("0.350000"));
			ImportValue(EmissiveBoost, TEXT("DefaultValue"), TEXT("1.750000"));
			ImportValue(PowerExponent, TEXT("DefaultValue"), TEXT("2.200000"));
			ImportValue(NormalVector, TEXT("DefaultValue"), TEXT("(R=0.500000,G=0.500000,B=1.000000,A=1.000000)"));
			ImportValue(PannerA, TEXT("SpeedX"), TEXT("0.120000"));
			ImportValue(PannerA, TEXT("SpeedY"), TEXT("0.040000"));
			ImportValue(PannerB, TEXT("SpeedX"), TEXT("-0.060000"));
			ImportValue(PannerB, TEXT("SpeedY"), TEXT("0.180000"));
			ImportValue(MaskR, TEXT("R"), TEXT("True"));
			ImportValue(MaskG, TEXT("G"), TEXT("True"));

			UMaterialExpressionComment* TextureLayerComment = Cast<UMaterialExpressionComment>(CreateExpression(Material, TEXT("MaterialExpressionComment"), TEXT("StressCommentTextureLayering"), -1360, -680, TEXT("")));
			UMaterialExpressionComment* RoughnessComment = Cast<UMaterialExpressionComment>(CreateExpression(Material, TEXT("MaterialExpressionComment"), TEXT("StressCommentRoughness"), -820, 180, TEXT("")));
			UMaterialExpressionComment* EmissiveComment = Cast<UMaterialExpressionComment>(CreateExpression(Material, TEXT("MaterialExpressionComment"), TEXT("StressCommentEmissive"), -820, 400, TEXT("")));
			AddMaterialComment(TextureLayerComment, TEXT("Druids stress comment: texture layering and panners"), 980, 520);
			AddMaterialComment(RoughnessComment, TEXT("Druids stress comment: roughness inversion chain"), 780, 320);
			AddMaterialComment(EmissiveComment, TEXT("Druids stress comment: emissive power branch"), 780, 340);

			Connect(TexCoord, PannerA, 0, TEXT("stress texcoord to panner A"));
			Connect(Time, PannerA, 1, TEXT("stress time to panner A"));
			Connect(TexCoord, PannerB, 0, TEXT("stress texcoord to panner B"));
			Connect(Time, PannerB, 1, TEXT("stress time to panner B"));
			Connect(PannerA, AlbedoTexture, 0, TEXT("stress panner A to albedo texture"));
			Connect(PannerB, DetailTexture, 0, TEXT("stress panner B to detail texture"));
			Connect(AlbedoTexture, MultiplyTint, 0, TEXT("stress albedo to tint multiply"));
			Connect(BaseTint, MultiplyTint, 1, TEXT("stress base tint to multiply"));
			Connect(DetailTexture, MaskR, 0, TEXT("stress detail to mask R"));
			Connect(DetailTexture, MaskG, 0, TEXT("stress detail to mask G"));
			Connect(MaskR, AppendRG, 0, TEXT("stress mask R to append"));
			Connect(MaskG, AppendRG, 1, TEXT("stress mask G to append"));
			Connect(MultiplyTint, StaticSwitch, 0, TEXT("stress multiply to static switch"));
			Connect(AccentTint, StaticSwitch, 1, TEXT("stress accent to static switch"));
			Connect(MultiplyTint, Lerp, 0, TEXT("stress multiply to lerp A"));
			Connect(StaticSwitch, Lerp, 1, TEXT("stress switch to lerp B"));
			Connect(Alpha, Lerp, 2, TEXT("stress alpha to lerp"));
			Connect(Lerp, AddColor, 0, TEXT("stress lerp to add"));
			Connect(AppendRG, AddColor, 1, TEXT("stress append to add"));
			Connect(AddColor, ClampColor, 0, TEXT("stress add to clamp"));
			Connect(Roughness, OneMinusRoughness, 0, TEXT("stress roughness to one minus"));
			Connect(OneMinusRoughness, SaturateRoughness, 0, TEXT("stress one minus to saturate"));
			Connect(MaskR, Power, 0, TEXT("stress mask R to power"));
			Connect(PowerExponent, Power, 1, TEXT("stress exponent to power"));
			Connect(Power, EmissiveMultiply, 0, TEXT("stress power to emissive multiply"));
			Connect(EmissiveBoost, EmissiveMultiply, 1, TEXT("stress emissive boost to multiply"));

			if (FExpressionInput* BaseColorInput = Material->GetExpressionInputForProperty(MP_BaseColor))
			{
				if (ClampColor) { ClampColor->ConnectExpression(BaseColorInput, 0); }
			}
			else
			{
				AddFailure(TEXT("root_connect"), TEXT("Could not find material BaseColor input."));
			}
			if (FExpressionInput* RoughnessInput = Material->GetExpressionInputForProperty(MP_Roughness))
			{
				if (SaturateRoughness) { SaturateRoughness->ConnectExpression(RoughnessInput, 0); }
			}
			else
			{
				AddFailure(TEXT("root_connect"), TEXT("Could not find material Roughness input."));
			}
			if (FExpressionInput* EmissiveInput = Material->GetExpressionInputForProperty(MP_EmissiveColor))
			{
				if (EmissiveMultiply) { EmissiveMultiply->ConnectExpression(EmissiveInput, 0); }
			}
			else
			{
				AddFailure(TEXT("root_connect"), TEXT("Could not find material EmissiveColor input."));
			}
			if (FExpressionInput* NormalInput = Material->GetExpressionInputForProperty(MP_Normal))
			{
				if (NormalVector) { NormalVector->ConnectExpression(NormalInput, 0); }
			}
			else
			{
				AddFailure(TEXT("root_connect"), TEXT("Could not find material Normal input."));
			}
		}
		else
		{
			UMaterialExpression* BaseTint = CreateExpression(Material, TEXT("MaterialExpressionVectorParameter"), TEXT("BaseTintParameter"), -720, -120, TEXT("Druids repro base color parameter"));
			UMaterialExpression* Roughness = CreateExpression(Material, TEXT("MaterialExpressionScalarParameter"), TEXT("RoughnessParameter"), -720, 120, TEXT("Druids repro roughness parameter"));
			UMaterialExpression* TextureParam = CreateExpression(Material, TEXT("MaterialExpressionTextureSampleParameter2D"), TEXT("AlbedoTextureParameter"), -720, 300, TEXT("Druids repro texture parameter"));
			UMaterialExpression* Multiply = CreateExpression(Material, TEXT("MaterialExpressionMultiply"), TEXT("TintMultiply"), -360, 0, TEXT("Druids repro connected math"));
			UMaterialExpressionComment* Comment = Cast<UMaterialExpressionComment>(CreateExpression(Material, TEXT("MaterialExpressionComment"), TEXT("DruidsMaterialComment"), -800, -220, TEXT("")));

			if (BaseTint) { BaseTint->SetParameterName(TEXT("Druids_BaseTint")); }
			if (Roughness) { Roughness->SetParameterName(TEXT("Druids_Roughness")); }
			if (TextureParam)
			{
				TextureParam->SetParameterName(TEXT("Druids_AlbedoTexture"));
				DruidsMCPBridge::SetObjectPropertyValueByName(TextureParam, TEXT("Texture"), DefaultTexture);
			}

			FString ImportError;
			if (BaseTint && !DruidsMCPBridge::ImportPropertyValueByName(BaseTint, TEXT("DefaultValue"), TEXT("(R=0.200000,G=0.600000,B=1.000000,A=1.000000)"), ImportError))
			{
				AddFailure(TEXT("property_import"), ImportError);
			}
			if (Roughness && !DruidsMCPBridge::ImportPropertyValueByName(Roughness, TEXT("DefaultValue"), TEXT("0.420000"), ImportError))
			{
				AddFailure(TEXT("property_import"), ImportError);
			}

			if (BaseTint) { Material->GetExpressionCollection().AddExpression(BaseTint); }
			if (Roughness) { Material->GetExpressionCollection().AddExpression(Roughness); }
			if (TextureParam) { Material->GetExpressionCollection().AddExpression(TextureParam); }
			if (Multiply) { Material->GetExpressionCollection().AddExpression(Multiply); }
			if (Comment)
			{
				Comment->Text = TEXT("Druids material repro comment: connected tint and roughness");
				Comment->SizeX = 720;
				Comment->SizeY = 360;
				Material->GetExpressionCollection().AddComment(Comment);
			}

			Connect(BaseTint, Multiply, 0, TEXT("base tint to multiply"));
			Connect(TextureParam, Multiply, 1, TEXT("texture to multiply"));
			if (FExpressionInput* BaseColorInput = Material->GetExpressionInputForProperty(MP_BaseColor))
			{
				(Multiply ? Multiply : BaseTint)->ConnectExpression(BaseColorInput, 0);
			}
			else
			{
				AddFailure(TEXT("root_connect"), TEXT("Could not find material BaseColor input."));
			}
			if (FExpressionInput* RoughnessInput = Material->GetExpressionInputForProperty(MP_Roughness))
			{
				if (Roughness) { Roughness->ConnectExpression(RoughnessInput, 0); }
			}
			else
			{
				AddFailure(TEXT("root_connect"), TEXT("Could not find material Roughness input."));
			}
		}

		Material->PostEditChange();
	}

	if (Instance && Material)
	{
		Instance->SetParentEditorOnly(Material, false);
		Instance->SetVectorParameterValueEditorOnly(FMaterialParameterInfo(bStress ? TEXT("Druids_StressBaseTint") : TEXT("Druids_BaseTint")), FLinearColor(1.0f, 0.25f, 0.1f, 1.0f));
		Instance->SetScalarParameterValueEditorOnly(FMaterialParameterInfo(bStress ? TEXT("Druids_StressRoughness") : TEXT("Druids_Roughness")), 0.73f);
		if (DefaultTexture)
		{
			Instance->SetTextureParameterValueEditorOnly(FMaterialParameterInfo(bStress ? TEXT("Druids_StressAlbedo") : TEXT("Druids_AlbedoTexture")), Cast<UTexture>(DefaultTexture));
		}
		Instance->PostEditChange();
	}

	if (Function)
	{
		Function->Description = TEXT("Druids material function repro for inspection tests.");
		UMaterialExpression* FunctionInput = CreateExpression(Function, TEXT("MaterialExpressionFunctionInput"), TEXT("DruidsFunctionInput"), -520, 0, TEXT("Druids repro function input"));
		UMaterialExpression* FunctionScalar = CreateExpression(Function, TEXT("MaterialExpressionScalarParameter"), TEXT("DruidsFunctionScale"), -520, 180, TEXT("Druids repro function parameter"));
		UMaterialExpression* FunctionMultiply = CreateExpression(Function, TEXT("MaterialExpressionMultiply"), TEXT("DruidsFunctionMultiply"), -200, 80, TEXT("Druids repro function connected math"));
		UMaterialExpression* FunctionOutput = CreateExpression(Function, TEXT("MaterialExpressionFunctionOutput"), TEXT("DruidsFunctionOutput"), 160, 80, TEXT("Druids repro function output"));
		UMaterialExpressionComment* FunctionComment = Cast<UMaterialExpressionComment>(CreateExpression(Function, TEXT("MaterialExpressionComment"), TEXT("DruidsFunctionComment"), -580, -120, TEXT("")));

		DruidsMCPBridge::SetNamePropertyValueByName(FunctionInput, TEXT("InputName"), TEXT("DruidsInput"));
		DruidsMCPBridge::SetNamePropertyValueByName(FunctionOutput, TEXT("OutputName"), TEXT("DruidsOutput"));
		if (FunctionScalar)
		{
			FunctionScalar->SetParameterName(TEXT("Druids_FunctionScale"));
			FString ImportError;
			if (!DruidsMCPBridge::ImportPropertyValueByName(FunctionScalar, TEXT("DefaultValue"), TEXT("2.000000"), ImportError))
			{
				AddFailure(TEXT("property_import"), ImportError);
			}
		}

		if (FunctionInput) { Function->GetExpressionCollection().AddExpression(FunctionInput); }
		if (FunctionScalar) { Function->GetExpressionCollection().AddExpression(FunctionScalar); }
		if (FunctionMultiply) { Function->GetExpressionCollection().AddExpression(FunctionMultiply); }
		if (FunctionOutput) { Function->GetExpressionCollection().AddExpression(FunctionOutput); }
		if (FunctionComment)
		{
			FunctionComment->Text = TEXT("Druids material function repro comment: input multiplied by scale");
			FunctionComment->SizeX = 620;
			FunctionComment->SizeY = 300;
			Function->GetExpressionCollection().AddComment(FunctionComment);
		}

		Connect(FunctionInput, FunctionMultiply, 0, TEXT("function input to multiply"));
		Connect(FunctionScalar, FunctionMultiply, 1, TEXT("function scalar to multiply"));
		Connect(FunctionMultiply, FunctionOutput, 0, TEXT("function multiply to output"));
		Function->PostEditChange();
	}

	TArray<UObject*> CreatedAssets;
	if (Material) { CreatedAssets.Add(Material); }
	if (Instance) { CreatedAssets.Add(Instance); }
	if (Function) { CreatedAssets.Add(Function); }
	for (UObject* CreatedAsset : CreatedAssets)
	{
		FAssetRegistryModule::AssetCreated(CreatedAsset);
		CreatedAsset->MarkPackageDirty();
		if (UPackage* Package = CreatedAsset->GetOutermost())
		{
			Package->MarkPackageDirty();
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("package_path"), PackagePath);
	Result->SetStringField(TEXT("material_asset_path"), Material ? Material->GetOutermost()->GetName() : TEXT(""));
	Result->SetStringField(TEXT("material_instance_asset_path"), Instance ? Instance->GetOutermost()->GetName() : TEXT(""));
	Result->SetStringField(TEXT("material_function_asset_path"), Function ? Function->GetOutermost()->GetName() : TEXT(""));
	Result->SetStringField(TEXT("material_object_path"), Material ? Material->GetPathName() : TEXT(""));
	Result->SetStringField(TEXT("material_instance_object_path"), Instance ? Instance->GetPathName() : TEXT(""));
	Result->SetStringField(TEXT("material_function_object_path"), Function ? Function->GetPathName() : TEXT(""));
	Result->SetBoolField(TEXT("stress"), bStress);
	if (bStress)
	{
		TSharedPtr<FJsonObject> Expected = MakeShared<FJsonObject>();
		Expected->SetNumberField(TEXT("min_expression_count"), 20);
		Expected->SetNumberField(TEXT("min_connection_count"), 20);
		Expected->SetNumberField(TEXT("min_comment_count"), 3);
		Expected->SetArrayField(TEXT("required_parameter_names"), MakeStringArrayValue({
			TEXT("Druids_StressBaseTint"),
			TEXT("Druids_StressAccentTint"),
			TEXT("Druids_StressRoughness"),
			TEXT("Druids_StressBlendAlpha"),
			TEXT("Druids_StressAlbedo"),
			TEXT("Druids_StressDetail"),
			TEXT("Druids_StressUseDetail")
		}));
		Expected->SetArrayField(TEXT("required_root_inputs"), MakeStringArrayValue({
			TEXT("BaseColor"),
			TEXT("Roughness"),
			TEXT("EmissiveColor"),
			TEXT("Normal")
		}));
		Expected->SetArrayField(TEXT("required_expression_classes"), MakeStringArrayValue({
			TEXT("MaterialExpressionTextureCoordinate"),
			TEXT("MaterialExpressionPanner"),
			TEXT("MaterialExpressionTextureSampleParameter2D"),
			TEXT("MaterialExpressionComponentMask"),
			TEXT("MaterialExpressionAppendVector"),
			TEXT("MaterialExpressionLinearInterpolate"),
			TEXT("MaterialExpressionClamp"),
			TEXT("MaterialExpressionPower"),
			TEXT("MaterialExpressionStaticSwitchParameter")
		}));
		Result->SetObjectField(TEXT("expected"), Expected);
	}
	Result->SetArrayField(TEXT("partial_failures"), PartialFailures);
	Result->SetBoolField(TEXT("created"), Material && Instance && Function);
	return MakeToolJsonObjectResult(Result, !(Material && Instance && Function));
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteResolveAsset(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	const TSharedPtr<FJsonObject> Result = ResolveAssetToObject(
		GetOptionalStringParam(Arguments, TEXT("name_or_path")),
		GetOptionalStringParam(Arguments, TEXT("type"), TEXT("Blueprint")),
		GetOptionalIntParam(Arguments, TEXT("limit"), 10),
		AssetPath,
		Error);
	return MakeToolJsonObjectResult(Result.IsValid() ? Result : MakeShared<FJsonObject>(), AssetPath.IsEmpty());
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteInspectAsset(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	const FString AssetNameOrPath = GetOptionalStringParam(Arguments, TEXT("AssetNameOrPath"), GetOptionalStringParam(Arguments, TEXT("name_or_path")));
	const FString Type = GetOptionalStringParam(Arguments, TEXT("Type"), GetOptionalStringParam(Arguments, TEXT("type"), TEXT("Any")));
	const int32 PropertyLimit = GetOptionalIntParam(Arguments, TEXT("PropertyLimit"), 200);

	TArray<TSharedPtr<FJsonValue>> PartialFailures;
	TSharedPtr<FJsonObject> ResolveResult = ResolveAssetToObject(AssetNameOrPath, Type, 10, AssetPath, Error);
	if (AssetPath.IsEmpty())
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("query"), AssetNameOrPath);
		Result->SetStringField(TEXT("error"), Error);
		if (ResolveResult.IsValid())
		{
			Result->SetObjectField(TEXT("resolve_result"), ResolveResult);
		}
		return MakeToolJsonObjectResult(Result, true);
	}

	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
	TArray<FAssetData> PackageAssets;
	AssetRegistryModule.Get().GetAssetsByPackageName(FName(*AssetPath), PackageAssets);

	FAssetData AssetData;
	if (PackageAssets.Num() > 0)
	{
		AssetData = PackageAssets[0];
		for (const FAssetData& Candidate : PackageAssets)
		{
			if (Candidate.PackageName.ToString().Equals(AssetPath, ESearchCase::IgnoreCase))
			{
				AssetData = Candidate;
				break;
			}
		}
	}
	else
	{
		TSharedPtr<FJsonObject> Failure = MakeShared<FJsonObject>();
		Failure->SetStringField(TEXT("stage"), TEXT("asset_registry"));
		Failure->SetStringField(TEXT("reason"), FString::Printf(TEXT("No asset registry data found for package '%s'."), *AssetPath));
		PartialFailures.Add(MakeShared<FJsonValueObject>(Failure));
	}

	const FString ObjectPath = AssetData.IsValid()
		? AssetData.GetSoftObjectPath().ToString()
		: FString::Printf(TEXT("%s.%s"), *AssetPath, *FPackageName::GetShortName(AssetPath));

	UObject* AssetObject = LoadObject<UObject>(nullptr, *ObjectPath);
	if (!AssetObject && AssetPath.Contains(TEXT(".")))
	{
		AssetObject = LoadObject<UObject>(nullptr, *AssetPath);
	}
	if (!AssetObject)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("query"), AssetNameOrPath);
		Result->SetStringField(TEXT("asset_path"), AssetPath);
		Result->SetStringField(TEXT("object_path"), ObjectPath);
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("Could not load asset object: %s"), *ObjectPath));
		Result->SetArrayField(TEXT("partial_failures"), PartialFailures);
		return MakeToolJsonObjectResult(Result, true);
	}

	TArray<TSharedPtr<FJsonValue>> References;
	TSharedPtr<FJsonObject> Properties = DruidsMCPBridge::ExportInspectableProperties(AssetObject, PropertyLimit, References, PartialFailures);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("query"), AssetNameOrPath);
	Result->SetStringField(TEXT("asset_path"), AssetObject->GetOutermost() ? AssetObject->GetOutermost()->GetName() : AssetPath);
	Result->SetStringField(TEXT("object_path"), AssetObject->GetPathName());
	Result->SetStringField(TEXT("asset_name"), AssetObject->GetName());
	Result->SetStringField(TEXT("package_path"), AssetObject->GetOutermost() ? AssetObject->GetOutermost()->GetName() : TEXT(""));
	Result->SetStringField(TEXT("class_path"), AssetObject->GetClass() ? AssetObject->GetClass()->GetPathName() : TEXT(""));
	Result->SetStringField(TEXT("class_name"), AssetObject->GetClass() ? AssetObject->GetClass()->GetName() : TEXT(""));
	Result->SetStringField(TEXT("native_class_path"), AssetObject->GetClass() ? AssetObject->GetClass()->GetPathName() : TEXT(""));
	Result->SetStringField(TEXT("native_class_name"), AssetObject->GetClass() ? AssetObject->GetClass()->GetName() : TEXT(""));
	Result->SetBoolField(TEXT("is_blueprint"), AssetObject->IsA<UBlueprint>());
	if (AssetData.IsValid())
	{
		Result->SetStringField(TEXT("asset_registry_class_path"), AssetData.AssetClassPath.ToString());
		Result->SetStringField(TEXT("asset_registry_package_name"), AssetData.PackageName.ToString());

		TSharedPtr<FJsonObject> Tags = MakeShared<FJsonObject>();
		for (const TPair<FName, FAssetTagValueRef>& Pair : AssetData.TagsAndValues)
		{
			Tags->SetStringField(Pair.Key.ToString(), Pair.Value.AsString());
		}
		Result->SetObjectField(TEXT("asset_registry_tags"), Tags);
	}

	Result->SetObjectField(TEXT("properties"), Properties);
	Result->SetArrayField(TEXT("references"), References);
	Result->SetNumberField(TEXT("reference_count"), References.Num());
	if (DruidsMCPBridge::IsGameplayCameraObject(AssetObject))
	{
		Result->SetObjectField(TEXT("gameplay_cameras"), DruidsMCPBridge::ExportGameplayCameraInspection(AssetObject));
	}
	if (DruidsMCPBridge::IsStateTreeObject(AssetObject))
	{
		Result->SetObjectField(TEXT("state_tree"), DruidsMCPBridge::ExportStateTreeInspection(AssetObject));
	}
	if (DruidsMCPBridge::IsMaterialObject(AssetObject))
	{
		Result->SetObjectField(TEXT("material"), DruidsMCPBridge::ExportMaterialInspection(AssetObject));
	}
	Result->SetArrayField(TEXT("partial_failures"), PartialFailures);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteEditorGetWorldStatus(const TSharedPtr<FJsonObject>& Arguments) const
{
	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("editor_available"), GEditor != nullptr);
	Result->SetBoolField(TEXT("actor_subsystem_available"), ActorSubsystem != nullptr);
	Result->SetBoolField(TEXT("world_available"), World != nullptr);
	Result->SetStringField(TEXT("placement_scope"), TEXT("current_editor_world_guarded"));
	Result->SetBoolField(TEXT("requires_allow_current_level"), true);
	Result->SetBoolField(TEXT("repro_map_creation_supported"), false);
	Result->SetStringField(TEXT("safety_note"), TEXT("Slice 55 mutates only the current editor world when AllowCurrentLevel=true and tags all placed actors for deterministic cleanup."));

	if (World)
	{
		Result->SetStringField(TEXT("world_name"), World->GetName());
		Result->SetStringField(TEXT("world_path"), World->GetPathName());
		Result->SetStringField(TEXT("package_name"), World->GetOutermost() ? World->GetOutermost()->GetName() : TEXT(""));
	}

	TArray<TSharedPtr<FJsonValue>> SelectedActors;
	if (ActorSubsystem)
	{
		const TArray<AActor*> Actors = ActorSubsystem->GetAllLevelActors();
		const TArray<AActor*> Selected = ActorSubsystem->GetSelectedLevelActors();
		Result->SetNumberField(TEXT("actor_count"), Actors.Num());
		Result->SetNumberField(TEXT("selected_actor_count"), Selected.Num());
		for (AActor* Actor : Selected)
		{
			SelectedActors.Add(MakeShared<FJsonValueObject>(DruidsMCPBridge::MakeEditorActorSummary(Actor, true)));
		}
	}
	else
	{
		Result->SetNumberField(TEXT("actor_count"), 0);
		Result->SetNumberField(TEXT("selected_actor_count"), 0);
	}
	Result->SetArrayField(TEXT("selected_actors"), SelectedActors);
	return MakeToolJsonObjectResult(Result, !World || !ActorSubsystem);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteEditorPlaceActor(const TSharedPtr<FJsonObject>& Arguments)
{
	const bool bAllowCurrentLevel = DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("AllowCurrentLevel"), false);
	if (!bAllowCurrentLevel)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("editor_place_actor requires AllowCurrentLevel=true because this mutates the current editor world."));
		Result->SetBoolField(TEXT("placed"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
	if (!ActorSubsystem || !World)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("Editor actor subsystem or editor world is unavailable."));
		Result->SetBoolField(TEXT("placed"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FString BlueprintNameOrPath = GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath"));
	const FString ActorClassPath = GetOptionalStringParam(Arguments, TEXT("ActorClassPath"));
	UClass* ActorClass = nullptr;
	FString ResolvedAssetPath;
	if (!BlueprintNameOrPath.IsEmpty())
	{
		FString Error;
		TSharedPtr<FJsonObject> ResolveResult = ResolveAssetToObject(BlueprintNameOrPath, TEXT("Blueprint"), 10, ResolvedAssetPath, Error);
		const FString ObjectPath = !ResolvedAssetPath.IsEmpty()
			? FString::Printf(TEXT("%s.%s"), *ResolvedAssetPath, *FPackageName::GetShortName(ResolvedAssetPath))
			: BlueprintNameOrPath;
		UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *ObjectPath);
		if (!Blueprint && BlueprintNameOrPath.Contains(TEXT(".")))
		{
			Blueprint = LoadObject<UBlueprint>(nullptr, *BlueprintNameOrPath);
		}
		if (Blueprint)
		{
			FKismetEditorUtilities::CompileBlueprint(Blueprint);
			ActorClass = Blueprint->GeneratedClass;
		}
		if (!ActorClass)
		{
			TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
			Result->SetStringField(TEXT("error"), Error.IsEmpty() ? FString::Printf(TEXT("Could not resolve Blueprint actor class from '%s'."), *BlueprintNameOrPath) : Error);
			Result->SetBoolField(TEXT("placed"), false);
			if (ResolveResult.IsValid())
			{
				Result->SetObjectField(TEXT("resolve_result"), ResolveResult);
			}
			return MakeToolJsonObjectResult(Result, true);
		}
	}
	else if (!ActorClassPath.IsEmpty())
	{
		ActorClass = LoadObject<UClass>(nullptr, *ActorClassPath);
		if (!ActorClass)
		{
			ActorClass = FindObject<UClass>(nullptr, *ActorClassPath);
		}
	}

	if (!ActorClass || !ActorClass->IsChildOf(AActor::StaticClass()))
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("editor_place_actor requires BlueprintNameOrPath for an Actor Blueprint or ActorClassPath for an Actor class."));
		Result->SetBoolField(TEXT("placed"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FVector Location = DruidsMCPBridge::GetVectorFromArgs(Arguments, TEXT("Location"), FVector::ZeroVector);
	const FRotator Rotation = DruidsMCPBridge::GetRotatorFromArgs(Arguments, FRotator::ZeroRotator);
	const FVector Scale = DruidsMCPBridge::GetVectorFromArgs(Arguments, TEXT("Scale"), FVector::OneVector);

	AActor* Actor = ActorSubsystem->SpawnActorFromClass(ActorClass, Location, Rotation, false);
	if (!Actor)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("Could not spawn actor of class '%s'."), *ActorClass->GetPathName()));
		Result->SetBoolField(TEXT("placed"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	Actor->SetActorScale3D(Scale);
	const FString ActorLabel = GetOptionalStringParam(Arguments, TEXT("ActorLabel"));
	if (!ActorLabel.IsEmpty())
	{
		Actor->SetActorLabel(ActorLabel);
	}
	Actor->Tags.AddUnique(TEXT("DruidsMCPBridge"));
	Actor->Tags.AddUnique(TEXT("DruidsRepro"));
	Actor->Tags.AddUnique(TEXT("DruidsVisualPCG"));
	Actor->Modify();

	ActorSubsystem->SelectNothing();
	ActorSubsystem->SetActorSelectionState(Actor, true);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("placed"), true);
	Result->SetStringField(TEXT("blueprint_asset_path"), ResolvedAssetPath);
	Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(Actor, true));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteEditorSetActorTransform(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString ActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ActorNameOrLabel"));
	AActor* Actor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ActorNameOrLabel);
	if (!Actor)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("No editor actor found for '%s'."), *ActorNameOrLabel));
		Result->SetBoolField(TEXT("updated"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FTransform Existing = Actor->GetActorTransform();
	const FVector Location = DruidsMCPBridge::GetVectorFromArgs(Arguments, TEXT("Location"), Existing.GetLocation());
	const FRotator Rotation = DruidsMCPBridge::GetRotatorFromArgs(Arguments, Existing.Rotator());
	const FVector Scale = DruidsMCPBridge::GetVectorFromArgs(Arguments, TEXT("Scale"), Existing.GetScale3D());

	Actor->Modify();
	Actor->SetActorTransform(FTransform(Rotation, Location, Scale));

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("updated"), true);
	Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(Actor, false));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteEditorSetComponentTransform(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString ActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ActorNameOrLabel"));
	const FString ComponentName = GetOptionalStringParam(Arguments, TEXT("ComponentName")).TrimStartAndEnd();
	AActor* Actor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ActorNameOrLabel);
	if (!Actor)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("No editor actor found for '%s'."), *ActorNameOrLabel));
		Result->SetBoolField(TEXT("updated"), false);
		return MakeToolJsonObjectResult(Result, true);
	}
	if (ComponentName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("editor_set_component_transform requires ComponentName."), true);
	}

	USceneComponent* TargetComponent = nullptr;
	TArray<USceneComponent*> SceneComponents;
	Actor->GetComponents<USceneComponent>(SceneComponents);
	for (USceneComponent* Component : SceneComponents)
	{
		if (Component && Component->GetName().Equals(ComponentName, ESearchCase::IgnoreCase))
		{
			TargetComponent = Component;
			break;
		}
	}
	if (!TargetComponent)
	{
		TArray<TSharedPtr<FJsonValue>> AvailableComponents;
		for (USceneComponent* Component : SceneComponents)
		{
			if (Component)
			{
				AvailableComponents.Add(MakeShared<FJsonValueString>(Component->GetName()));
			}
		}
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("No component named '%s' found on actor '%s'."), *ComponentName, *ActorNameOrLabel));
		Result->SetBoolField(TEXT("updated"), false);
		Result->SetArrayField(TEXT("available_components"), AvailableComponents);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FTransform Existing = TargetComponent->GetRelativeTransform();
	const FVector Location = DruidsMCPBridge::GetVectorFromArgs(Arguments, TEXT("Location"), Existing.GetLocation());
	const FRotator Rotation = DruidsMCPBridge::GetRotatorFromArgs(Arguments, Existing.Rotator());
	const FVector Scale = DruidsMCPBridge::GetVectorFromArgs(Arguments, TEXT("Scale"), Existing.GetScale3D());

	Actor->Modify();
	TargetComponent->Modify();
	TargetComponent->SetRelativeTransform(FTransform(Rotation, Location, Scale));
	TargetComponent->UpdateComponentToWorld();

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("updated"), true);
	Result->SetStringField(TEXT("component_name"), TargetComponent->GetName());
	Result->SetObjectField(TEXT("component_relative_transform"), DruidsMCPBridge::MakeTransformObject(TargetComponent->GetRelativeTransform()));
	Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(Actor, false));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteEditorReadLevelActors(const TSharedPtr<FJsonObject>& Arguments) const
{
	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	if (!ActorSubsystem)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("Editor actor subsystem is unavailable."));
		Result->SetNumberField(TEXT("result_count"), 0);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FString Filter = GetOptionalStringParam(Arguments, TEXT("Filter"));
	const bool bReproOnly = DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("ReproOnly"), false);
	const int32 Limit = FMath::Max(1, GetOptionalIntParam(Arguments, TEXT("Limit"), 100));
	const TArray<AActor*> Actors = ActorSubsystem->GetAllLevelActors();
	const TArray<AActor*> SelectedActors = ActorSubsystem->GetSelectedLevelActors();

	TSet<TWeakObjectPtr<AActor>> SelectedSet;
	for (AActor* Actor : SelectedActors)
	{
		SelectedSet.Add(Actor);
	}

	TArray<TSharedPtr<FJsonValue>> Results;
	int32 MatchedCount = 0;
	for (AActor* Actor : Actors)
	{
		if (!Actor)
		{
			continue;
		}
		const bool bDruidsActor = Actor->Tags.Contains(TEXT("DruidsRepro")) || Actor->Tags.Contains(TEXT("DruidsMCPBridge"));
		if (bReproOnly && !bDruidsActor)
		{
			continue;
		}
		if (!Filter.IsEmpty() &&
			!Actor->GetName().Contains(Filter, ESearchCase::IgnoreCase) &&
			!Actor->GetActorLabel().Contains(Filter, ESearchCase::IgnoreCase) &&
			!Actor->GetPathName().Contains(Filter, ESearchCase::IgnoreCase))
		{
			continue;
		}
		++MatchedCount;
		if (Results.Num() < Limit)
		{
			Results.Add(MakeShared<FJsonValueObject>(DruidsMCPBridge::MakeEditorActorSummary(Actor, SelectedSet.Contains(Actor))));
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("filter"), Filter);
	Result->SetBoolField(TEXT("repro_only"), bReproOnly);
	Result->SetNumberField(TEXT("matched_count"), MatchedCount);
	Result->SetNumberField(TEXT("result_count"), Results.Num());
	Result->SetArrayField(TEXT("actors"), Results);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteEditorSelectActor(const TSharedPtr<FJsonObject>& Arguments)
{
	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	const FString ActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ActorNameOrLabel"));
	AActor* Actor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ActorNameOrLabel);
	if (!ActorSubsystem || !Actor)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("No editor actor found for '%s'."), *ActorNameOrLabel));
		Result->SetBoolField(TEXT("selected"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("ClearSelection"), true))
	{
		ActorSubsystem->SelectNothing();
	}
	ActorSubsystem->SetActorSelectionState(Actor, true);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("selected"), true);
	Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(Actor, true));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteEditorCleanupReproActors(const TSharedPtr<FJsonObject>& Arguments)
{
	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	if (!ActorSubsystem)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("Editor actor subsystem is unavailable."));
		Result->SetNumberField(TEXT("deleted_count"), 0);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FString Filter = GetOptionalStringParam(Arguments, TEXT("Filter"));
	const bool bDryRun = DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("DryRun"), false);
	TArray<AActor*> ToDelete;
	TArray<TSharedPtr<FJsonValue>> MatchedActors;
	for (AActor* Actor : ActorSubsystem->GetAllLevelActors())
	{
		if (!Actor)
		{
			continue;
		}
		const bool bDruidsActor = Actor->Tags.Contains(TEXT("DruidsRepro")) || Actor->Tags.Contains(TEXT("DruidsMCPBridge"));
		if (!bDruidsActor)
		{
			continue;
		}
		bool bTagMatchesFilter = false;
		if (!Filter.IsEmpty())
		{
			for (const FName& Tag : Actor->Tags)
			{
				if (Tag.ToString().Contains(Filter, ESearchCase::IgnoreCase))
				{
					bTagMatchesFilter = true;
					break;
				}
			}
		}
		if (!Filter.IsEmpty() &&
			!Actor->GetName().Contains(Filter, ESearchCase::IgnoreCase) &&
			!Actor->GetActorLabel().Contains(Filter, ESearchCase::IgnoreCase) &&
			!Actor->GetPathName().Contains(Filter, ESearchCase::IgnoreCase) &&
			!bTagMatchesFilter)
		{
			continue;
		}
		ToDelete.Add(Actor);
		MatchedActors.Add(MakeShared<FJsonValueObject>(DruidsMCPBridge::MakeEditorActorSummary(Actor, false)));
	}

	int32 DeletedCount = 0;
	if (!bDryRun && ToDelete.Num() > 0)
	{
		DeletedCount = ActorSubsystem->DestroyActors(ToDelete) ? ToDelete.Num() : 0;
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("filter"), Filter);
	Result->SetBoolField(TEXT("dry_run"), bDryRun);
	Result->SetNumberField(TEXT("matched_count"), ToDelete.Num());
	Result->SetNumberField(TEXT("deleted_count"), DeletedCount);
	Result->SetArrayField(TEXT("matched_actors"), MatchedActors);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteEditorCaptureViewport(const TSharedPtr<FJsonObject>& Arguments)
{
	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	if (!ActorSubsystem)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("Editor actor subsystem is unavailable."));
		Result->SetBoolField(TEXT("captured"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	TArray<AActor*> TargetActors;
	const FString ActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ActorNameOrLabel")).TrimStartAndEnd();
	if (!ActorNameOrLabel.IsEmpty())
	{
		if (AActor* Actor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ActorNameOrLabel))
		{
			TargetActors.Add(Actor);
		}
	}
	else
	{
		TargetActors = ActorSubsystem->GetSelectedLevelActors();
	}

	if (TargetActors.Num() == 0)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), ActorNameOrLabel.IsEmpty() ? TEXT("No selected editor actors to capture.") : FString::Printf(TEXT("No editor actor found for '%s'."), *ActorNameOrLabel));
		Result->SetBoolField(TEXT("captured"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>(TEXT("LevelEditor"));
	TSharedPtr<IAssetViewport> ActiveViewport = LevelEditorModule.GetFirstActiveViewport();
	if (!ActiveViewport.IsValid())
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("No active Level Editor viewport is available for capture."));
		Result->SetBoolField(TEXT("captured"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	FEditorViewportClient& ViewportClient = ActiveViewport->GetAssetViewportClient();
	FViewport* Viewport = ActiveViewport->GetActiveViewport();
	if (!Viewport)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("Active Level Editor viewport has no FViewport."));
		Result->SetBoolField(TEXT("captured"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	FBox CombinedBounds(ForceInit);
	for (AActor* Actor : TargetActors)
	{
		if (!Actor)
		{
			continue;
		}
		FVector Origin = FVector::ZeroVector;
		FVector Extent = FVector::ZeroVector;
		Actor->GetActorBounds(false, Origin, Extent);
		CombinedBounds += FBox::BuildAABB(Origin, Extent);
	}
	const bool bFocused = DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("Focus"), true) && CombinedBounds.IsValid;
	if (bFocused)
	{
		ViewportClient.FocusViewportOnBox(CombinedBounds, true);
		ViewportClient.Invalidate();
		Viewport->Draw();
	}

	TArray<FColor> Bitmap;
	if (!Viewport->ReadPixels(Bitmap) || Bitmap.Num() == 0)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("Could not read pixels from active Level Editor viewport."));
		Result->SetBoolField(TEXT("captured"), false);
		return MakeToolJsonObjectResult(Result, true);
	}
	for (FColor& Pixel : Bitmap)
	{
		Pixel.A = 255;
	}

	const FIntPoint Size = Viewport->GetSizeXY();
	TArray64<uint8> CompressedPng;
	FImageUtils::PNGCompressImageArray(Size.X, Size.Y, TArrayView64<const FColor>(Bitmap.GetData(), Bitmap.Num()), CompressedPng);
	if (CompressedPng.Num() == 0)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), TEXT("Could not compress viewport pixels to PNG."));
		Result->SetBoolField(TEXT("captured"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FString Directory = FPaths::ProjectSavedDir() / TEXT("DruidsMCPBridge") / TEXT("Screenshots");
	IFileManager::Get().MakeDirectory(*Directory, true);
	FString FilenamePrefix = GetOptionalStringParam(Arguments, TEXT("FilenamePrefix"), TEXT("DruidsViewport")).TrimStartAndEnd();
	if (FilenamePrefix.IsEmpty())
	{
		FilenamePrefix = TEXT("DruidsViewport");
	}
	const FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S_%s"));
	const FString ScreenshotPath = Directory / FString::Printf(TEXT("%s_%s.png"), *FilenamePrefix, *Timestamp);
	TArray<uint8> FileBytes;
	FileBytes.Append(CompressedPng.GetData(), CompressedPng.Num());
	if (!FFileHelper::SaveArrayToFile(FileBytes, *ScreenshotPath))
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("Could not save screenshot to '%s'."), *ScreenshotPath));
		Result->SetBoolField(TEXT("captured"), false);
		return MakeToolJsonObjectResult(Result, true);
	}

	TArray<TSharedPtr<FJsonValue>> ActorValues;
	for (AActor* Actor : TargetActors)
	{
		ActorValues.Add(MakeShared<FJsonValueObject>(DruidsMCPBridge::MakeEditorActorSummary(Actor, true)));
	}

	TSharedPtr<FJsonObject> Camera = MakeShared<FJsonObject>();
	Camera->SetObjectField(TEXT("location"), DruidsMCPBridge::MakeVectorObject(ViewportClient.GetViewLocation()));
	Camera->SetObjectField(TEXT("rotation"), DruidsMCPBridge::MakeRotatorObject(ViewportClient.GetViewRotation()));
	Camera->SetStringField(TEXT("viewport_type"), DruidsMCPBridge::GetLevelViewportTypeName(ViewportClient.GetViewportType()));

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("captured"), true);
	Result->SetBoolField(TEXT("focused"), bFocused);
	Result->SetStringField(TEXT("screenshot_path"), ScreenshotPath);
	Result->SetNumberField(TEXT("width"), Size.X);
	Result->SetNumberField(TEXT("height"), Size.Y);
	Result->SetNumberField(TEXT("byte_count"), FileBytes.Num());
	Result->SetObjectField(TEXT("camera"), Camera);
	Result->SetObjectField(TEXT("target_bounds"), CombinedBounds.IsValid ? DruidsMCPBridge::MakeBoxObject(CombinedBounds) : MakeShared<FJsonObject>());
	Result->SetArrayField(TEXT("actors"), ActorValues);
	Result->SetNumberField(TEXT("actor_count"), TargetActors.Num());
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGGetStatus(const TSharedPtr<FJsonObject>& Arguments) const
{
	const TSharedPtr<IPlugin> PCGPlugin = IPluginManager::Get().FindPlugin(TEXT("PCG"));
	const bool bInstalled = PCGPlugin.IsValid();
	const bool bEnabled = bInstalled && PCGPlugin->IsEnabled();
	const bool bModuleExists = FModuleManager::Get().ModuleExists(TEXT("PCG"));
	const bool bModuleLoaded = FModuleManager::Get().IsModuleLoaded(TEXT("PCG"));
	const FEngineVersion EngineVersion = FEngineVersion::Current();

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("pcg_installed"), bInstalled);
	Result->SetBoolField(TEXT("pcg_enabled"), bEnabled);
	Result->SetBoolField(TEXT("pcg_available"), bInstalled && bEnabled && bModuleExists);
	Result->SetBoolField(TEXT("pcg_module_exists"), bModuleExists);
	Result->SetBoolField(TEXT("pcg_module_loaded"), bModuleLoaded);
	Result->SetBoolField(TEXT("pcg_tools_enabled"), bInstalled && bEnabled && bModuleExists);
	Result->SetStringField(TEXT("bridge_support_level"), TEXT("one_call_rectangular_and_spline_rectangle_forest_recipe_slice_78"));
	Result->SetStringField(TEXT("mutation_support"), TEXT("pcg_graph_asset_scaffold_grid_generation_request_visual_repro_native_spawn_actor_rectangular_and_spline_rectangle_recipe_update_only"));
	Result->SetStringField(TEXT("engine_version"), EngineVersion.ToString());
	Result->SetBoolField(TEXT("supported_engine_version"), EngineVersion.GetMajor() == 5 && EngineVersion.GetMinor() >= 5 && EngineVersion.GetMinor() <= 8);
	Result->SetStringField(TEXT("next_step"), bInstalled && bEnabled && bModuleExists
		? TEXT("PCG is available for minimal graph asset creation, pass-through/reroute/grid/spawn-actor/bounds-spawn graph wiring, target-count spacing derivation, Transform Points variation, graph inspection, scaffold placement, rectangular and spline-rectangle forest recipe workflows, point-output generation requests, native PCG Spawn Actor graph generation, and safe visual variety.")
		: TEXT("PCG is unavailable or disabled. Later PCG tools should return structured unavailable results rather than mutating assets."));
	Result->SetObjectField(TEXT("road_capabilities"), DruidsMCPBridge::MakePCGRoadCapabilitiesSummary());

	TSharedPtr<FJsonObject> ShapeSupport = MakeShared<FJsonObject>();
	ShapeSupport->SetBoolField(TEXT("rectangle"), true);
	ShapeSupport->SetBoolField(TEXT("rect"), true);
	ShapeSupport->SetBoolField(TEXT("box"), true);
	ShapeSupport->SetBoolField(TEXT("spline"), false);
	ShapeSupport->SetBoolField(TEXT("spline_rectangle"), true);
	ShapeSupport->SetBoolField(TEXT("spline_actor_scaffold"), true);
	ShapeSupport->SetBoolField(TEXT("arbitrary_volume"), false);
	ShapeSupport->SetStringField(TEXT("supported_input_field"), TEXT("BoundsShape"));
	ShapeSupport->SetStringField(TEXT("unsupported_result_status"), TEXT("pcg_bounds_shape_unsupported"));
	ShapeSupport->SetStringField(TEXT("next_shape_slice"), TEXT("arbitrary_spline_area_sampling"));
	ShapeSupport->SetStringField(TEXT("note"), TEXT("Rectangle/Rect/Box are aliases for the stable rectangular PCG volume workflow. BoundsShape=SplineRectangle in pcg_run_forest_recipe/pcg_update_forest_recipe uses a visible rectangular spline scaffold as the bounds source. Arbitrary spline area sampling remains unsupported."));
	Result->SetObjectField(TEXT("shape_support"), ShapeSupport);

	TArray<TSharedPtr<FJsonValue>> Boundaries;
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Minimal PCGGraph asset creation and scaffold attachment are supported when PCG is enabled.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("Only deterministic built-in Input/Output pass-through, one reroute middle node, one Create Points Grid source node, one Transform Points variation node, one native Spawn Actor node targeting a supplied tree Blueprint, and rectangular bounds updates for that grid node are supported in this slice.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("SpacingMode=TargetCount derives square grid spacing from AreaX, AreaY, and TargetCount; actual generated count is reported because grid counts are quantized.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("VariationMode=TransformPoints supports deterministic XY jitter, yaw jitter, and uniform scale variation through one PCG Transform Points node.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("pcg_generate requests graph execution and reports point-output data and native managed spawned actors when the graph uses GraphMode=SpawnActor or BoundsSpawnActor.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("pcg_create_visual_repro_output can create bridge-owned visible tree actors from the deterministic grid contract for visual QA; this is not native PCG SpawnActor graph generation.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("BoundsShape=SplineRectangle is supported only as bounds_from_spline_rectangle in the high-level forest recipe tools; arbitrary spline and volume-shaped forests are reported as unsupported before mutation.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("pcg_run_forest_recipe and pcg_update_forest_recipe compose the proven tree Blueprint, bounds graph, placement, generation, inspection, and viewport capture tools.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("TreeStyle and transform variation provide safe visual variety; true multi-class PCG spawning remains behind pcg_multiclass_spawn_proof and reports unsupported until proven.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("No automatic plugin enabling.")));
	Boundaries.Add(MakeShared<FJsonValueString>(TEXT("No arbitrary PCG graph editing, spline forest authoring, landscape projection, or PIE execution.")));
	Result->SetArrayField(TEXT("boundaries"), Boundaries);

	if (PCGPlugin.IsValid())
	{
		const FPluginDescriptor& Descriptor = PCGPlugin->GetDescriptor();
		TSharedPtr<FJsonObject> Plugin = MakeShared<FJsonObject>();
		Plugin->SetStringField(TEXT("name"), PCGPlugin->GetName());
		Plugin->SetStringField(TEXT("friendly_name"), Descriptor.FriendlyName);
		Plugin->SetStringField(TEXT("description"), Descriptor.Description);
		Plugin->SetStringField(TEXT("version_name"), Descriptor.VersionName);
		Plugin->SetNumberField(TEXT("version"), Descriptor.Version);
		Plugin->SetStringField(TEXT("base_dir"), PCGPlugin->GetBaseDir());
		Plugin->SetStringField(TEXT("mounted_asset_path"), PCGPlugin->GetMountedAssetPath());
		Plugin->SetBoolField(TEXT("can_contain_content"), Descriptor.bCanContainContent);
		Plugin->SetBoolField(TEXT("is_beta"), Descriptor.bIsBetaVersion);
		Plugin->SetBoolField(TEXT("is_experimental"), Descriptor.bIsExperimentalVersion);
		Plugin->SetBoolField(TEXT("enabled"), bEnabled);

		TArray<TSharedPtr<FJsonValue>> Modules;
		for (const FModuleDescriptor& Module : Descriptor.Modules)
		{
			const FString ModuleName = Module.Name.ToString();
			TSharedPtr<FJsonObject> ModuleObject = MakeShared<FJsonObject>();
			ModuleObject->SetStringField(TEXT("name"), ModuleName);
			ModuleObject->SetStringField(TEXT("type"), EHostType::ToString(Module.Type));
			ModuleObject->SetStringField(TEXT("loading_phase"), ELoadingPhase::ToString(Module.LoadingPhase));
			ModuleObject->SetBoolField(TEXT("loaded"), FModuleManager::Get().IsModuleLoaded(Module.Name));
			ModuleObject->SetBoolField(TEXT("module_exists"), FModuleManager::Get().ModuleExists(*ModuleName));
			Modules.Add(MakeShared<FJsonValueObject>(ModuleObject));
		}
		Plugin->SetArrayField(TEXT("modules"), Modules);
		Result->SetObjectField(TEXT("plugin"), Plugin);
	}
	else
	{
		TSharedPtr<FJsonObject> Plugin = MakeShared<FJsonObject>();
		Plugin->SetStringField(TEXT("name"), TEXT("PCG"));
		Plugin->SetStringField(TEXT("status"), TEXT("not_installed_or_not_discovered"));
		Result->SetObjectField(TEXT("plugin"), Plugin);
	}

	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGGetRoadCapabilities(const TSharedPtr<FJsonObject>& Arguments) const
{
	return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGRoadCapabilitiesSummary());
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGCreateSplineBoundsActor(const TSharedPtr<FJsonObject>& Arguments)
{
	if (!DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("AllowCurrentLevel"), false))
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("created"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_spline_bounds_requires_explicit_level_mutation"));
		Result->SetStringField(TEXT("error"), TEXT("pcg_create_spline_bounds_actor requires AllowCurrentLevel=true because this creates an editor-world actor."));
		return MakeToolJsonObjectResult(Result, true);
	}

	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
	if (!ActorSubsystem || !World)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("created"), false);
		Result->SetStringField(TEXT("status"), TEXT("editor_world_unavailable"));
		Result->SetStringField(TEXT("error"), TEXT("Editor actor subsystem or editor world is unavailable."));
		return MakeToolJsonObjectResult(Result, true);
	}

	FString ActorName = GetOptionalStringParam(Arguments, TEXT("ActorName"), TEXT("Druids_PCG_Spline_Bounds")).TrimStartAndEnd();
	if (ActorName.IsEmpty())
	{
		ActorName = TEXT("Druids_PCG_Spline_Bounds");
	}
	const FVector Location = DruidsMCPBridge::GetVectorFromArgs(Arguments, TEXT("Location"), FVector::ZeroVector);
	const double Width = FMath::Max(1.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("Width"), 1200.0));
	const double Depth = FMath::Max(1.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("Depth"), 800.0));
	const bool bClosedLoop = DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("ClosedLoop"), true);

	AActor* Actor = ActorSubsystem->SpawnActorFromClass(AActor::StaticClass(), Location, FRotator::ZeroRotator, false);
	if (!Actor)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("created"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_spline_bounds_actor_create_failed"));
		Result->SetStringField(TEXT("error"), TEXT("Could not spawn spline bounds actor."));
		return MakeToolJsonObjectResult(Result, true);
	}

	Actor->SetActorLabel(ActorName);
	Actor->Tags.AddUnique(TEXT("DruidsMCPBridge"));
	Actor->Tags.AddUnique(TEXT("DruidsRepro"));
	Actor->Tags.AddUnique(TEXT("DruidsVisualPCG"));
	Actor->Tags.AddUnique(TEXT("DruidsPCGSplineBounds"));

	USceneComponent* Root = NewObject<USceneComponent>(Actor, TEXT("DruidsSplineBoundsRoot"));
	Root->CreationMethod = EComponentCreationMethod::Instance;
	Root->SetMobility(EComponentMobility::Movable);
	Actor->SetRootComponent(Root);
	Actor->AddInstanceComponent(Root);
	Root->RegisterComponent();

	USplineComponent* Spline = NewObject<USplineComponent>(Actor, TEXT("DruidsSplineBounds"));
	Spline->CreationMethod = EComponentCreationMethod::Instance;
	Spline->SetMobility(EComponentMobility::Movable);
	Spline->SetupAttachment(Root);
	Actor->AddInstanceComponent(Spline);
	Spline->RegisterComponent();

	DruidsMCPBridge::ConfigureRectangleSplineBounds(Spline, Width, Depth, bClosedLoop);

	Actor->Modify();
	ActorSubsystem->SelectNothing();
	ActorSubsystem->SetActorSelectionState(Actor, true);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), true);
	Result->SetBoolField(TEXT("created"), true);
	Result->SetStringField(TEXT("status"), TEXT("pcg_spline_bounds_actor_created"));
	Result->SetStringField(TEXT("actor_name"), ActorName);
	Result->SetNumberField(TEXT("width"), Width);
	Result->SetNumberField(TEXT("depth"), Depth);
	Result->SetBoolField(TEXT("pcg_generation_uses_spline"), false);
	Result->SetStringField(TEXT("generation_status"), TEXT("spline_bounds_actor_scaffold_only"));
	Result->SetStringField(TEXT("next_step"), TEXT("Use editor_capture_viewport or editor_read_level_actors to inspect this spline. BoundsShape=Spline PCG generation remains unsupported until a later slice."));
	Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(Actor, true));
	Result->SetObjectField(TEXT("spline"), DruidsMCPBridge::MakeSplineBoundsSummary(Spline));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGInspectSplineBoundsActor(const TSharedPtr<FJsonObject>& Arguments) const
{
	const FString ActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ActorNameOrLabel")).TrimStartAndEnd();
	AActor* Actor = ActorNameOrLabel.IsEmpty() ? nullptr : DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ActorNameOrLabel);
	USplineComponent* Spline = DruidsMCPBridge::FindSplineBoundsComponent(Actor);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), Actor != nullptr && Spline != nullptr);
	Result->SetStringField(TEXT("actor_name_or_label"), ActorNameOrLabel);
	Result->SetStringField(TEXT("status"), Actor && Spline ? TEXT("pcg_spline_bounds_actor_inspected") : TEXT("pcg_spline_bounds_actor_not_found"));
	Result->SetBoolField(TEXT("pcg_generation_uses_spline"), false);
	Result->SetStringField(TEXT("generation_status"), TEXT("spline_bounds_actor_scaffold_only"));
	if (Actor)
	{
		Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(Actor, false));
	}
	if (Spline)
	{
		Result->SetObjectField(TEXT("spline"), DruidsMCPBridge::MakeSplineBoundsSummary(Spline));
	}
	else
	{
		Result->SetStringField(TEXT("error"), Actor ? TEXT("Actor was found, but it has no spline bounds component.") : TEXT("No spline bounds actor found."));
	}
	return MakeToolJsonObjectResult(Result, !(Actor && Spline));
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGSetSplineBoundsActor(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString ActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ActorNameOrLabel")).TrimStartAndEnd();
	AActor* Actor = ActorNameOrLabel.IsEmpty() ? nullptr : DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ActorNameOrLabel);
	USplineComponent* Spline = DruidsMCPBridge::FindSplineBoundsComponent(Actor);
	if (!Actor || !Spline)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("updated"), false);
		Result->SetStringField(TEXT("actor_name_or_label"), ActorNameOrLabel);
		Result->SetStringField(TEXT("status"), TEXT("pcg_spline_bounds_actor_not_found"));
		Result->SetStringField(TEXT("error"), Actor ? TEXT("Actor was found, but it has no spline bounds component.") : TEXT("No spline bounds actor found."));
		return MakeToolJsonObjectResult(Result, true);
	}

	const FVector ExistingLocation = Actor->GetActorLocation();
	const FVector Location = DruidsMCPBridge::GetVectorFromArgs(Arguments, TEXT("Location"), ExistingLocation);
	const FVector2D ExistingSize = DruidsMCPBridge::EstimateRectangleSplineSize(Spline, FVector2D(1200.0, 800.0));
	const double Width = FMath::Max(1.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("Width"), ExistingSize.X));
	const double Depth = FMath::Max(1.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("Depth"), ExistingSize.Y));
	const bool bClosedLoop = DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("ClosedLoop"), Spline->IsClosedLoop());

	Actor->Modify();
	Spline->Modify();
	Actor->SetActorLocation(Location);
	DruidsMCPBridge::ConfigureRectangleSplineBounds(Spline, Width, Depth, bClosedLoop);

	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	if (ActorSubsystem)
	{
		ActorSubsystem->SelectNothing();
		ActorSubsystem->SetActorSelectionState(Actor, true);
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), true);
	Result->SetBoolField(TEXT("updated"), true);
	Result->SetStringField(TEXT("status"), TEXT("pcg_spline_bounds_actor_updated"));
	Result->SetStringField(TEXT("actor_name_or_label"), ActorNameOrLabel);
	Result->SetNumberField(TEXT("width"), Width);
	Result->SetNumberField(TEXT("depth"), Depth);
	Result->SetBoolField(TEXT("pcg_generation_uses_spline"), false);
	Result->SetStringField(TEXT("generation_status"), TEXT("spline_bounds_actor_scaffold_only"));
	Result->SetStringField(TEXT("next_step"), TEXT("Use pcg_inspect_spline_bounds_actor or editor_capture_viewport to verify the adjusted spline. BoundsShape=Spline PCG generation remains unsupported until a later slice."));
	Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(Actor, true));
	Result->SetObjectField(TEXT("spline"), DruidsMCPBridge::MakeSplineBoundsSummary(Spline));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGCreateForestRepro(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_create_forest_repro"), Arguments, TEXT("create_forest_repro")), true);
	}

	const TSharedPtr<FJsonObject> Request = DruidsMCPBridge::MakePCGForestRequestSummary(Arguments);
	if (!Request->GetBoolField(TEXT("bounds_shape_supported")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnsupportedBoundsShapeResult(TEXT("pcg_create_forest_repro"), Request), true);
	}
	const FString PackagePath = Request->GetStringField(TEXT("package_path"));
	const FString ForestName = Request->GetStringField(TEXT("forest_name"));
	const FString GraphMode = GetOptionalStringParam(Arguments, TEXT("GraphMode"), TEXT("Passthrough")).TrimStartAndEnd();
	const bool bUseRerouteGraph = GraphMode.Equals(TEXT("Reroute"), ESearchCase::IgnoreCase);
	const bool bUseGridGraph = GraphMode.Equals(TEXT("Grid"), ESearchCase::IgnoreCase);
	const bool bUseSpawnActorGraph = GraphMode.Equals(TEXT("SpawnActor"), ESearchCase::IgnoreCase);
	const bool bUseBoundsSpawnActorGraph = GraphMode.Equals(TEXT("BoundsSpawnActor"), ESearchCase::IgnoreCase)
		|| GraphMode.Equals(TEXT("Bounds"), ESearchCase::IgnoreCase);
	const FString GraphName = FString::Printf(TEXT("PCG_%s"), *ForestName);
	const FString GraphPackagePath = PackagePath / GraphName;
	UClass* TreeActorClass = nullptr;
	FString TreeResolveError;
	if (bUseSpawnActorGraph || bUseBoundsSpawnActorGraph)
	{
		const FString TreeBlueprintPath = Request->GetStringField(TEXT("tree_blueprint_path"));
		FString ResolvedTreePath;
		ResolveAssetToObject(TreeBlueprintPath, TEXT("Blueprint"), 10, ResolvedTreePath, TreeResolveError);
		const FString TreeLoadPath = ResolvedTreePath.IsEmpty()
			? DruidsMCPBridge::MakeObjectPathIfPackagePath(TreeBlueprintPath)
			: DruidsMCPBridge::MakeBlueprintObjectPath(ResolvedTreePath);
		UBlueprint* TreeBlueprint = LoadObject<UBlueprint>(nullptr, *TreeLoadPath);
		TreeActorClass = TreeBlueprint ? TreeBlueprint->GeneratedClass : nullptr;
		if (!TreeActorClass || !TreeActorClass->IsChildOf(AActor::StaticClass()))
		{
			TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
			Result->SetBoolField(TEXT("success"), false);
			Result->SetBoolField(TEXT("created"), false);
			Result->SetStringField(TEXT("status"), TEXT("pcg_spawn_actor_tree_blueprint_not_found"));
			Result->SetStringField(TEXT("error"), TreeResolveError.IsEmpty()
				? FString::Printf(TEXT("Could not resolve an Actor Blueprint class from '%s'."), *TreeBlueprintPath)
				: TreeResolveError);
			Result->SetObjectField(TEXT("pcg_status"), Status);
			Result->SetObjectField(TEXT("request"), Request);
			return MakeToolJsonObjectResult(Result, true);
		}
	}
	UPackage* GraphPackage = CreatePackage(*GraphPackagePath);
	UClass* PCGGraphClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGGraph"));
	if (!GraphPackage || !PCGGraphClass)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("created"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_graph_asset_create_failed"));
		Result->SetStringField(TEXT("error"), TEXT("PCG is enabled, but PCGGraph class or target package was unavailable."));
		Result->SetObjectField(TEXT("pcg_status"), Status);
		Result->SetObjectField(TEXT("request"), Request);
		return MakeToolJsonObjectResult(Result, true);
	}

	UObject* Graph = FindObject<UObject>(GraphPackage, *GraphName);
	const bool bExisted = Graph != nullptr;
	if (!Graph)
	{
		Graph = DruidsMCPBridge::CreateReproObject(GraphPackage, PCGGraphClass, GraphName);
		if (Graph)
		{
			FAssetRegistryModule::AssetCreated(Graph);
			Graph->MarkPackageDirty();
			GraphPackage->MarkPackageDirty();
		}
	}

	if (!Graph)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("created"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_graph_asset_create_failed"));
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("Could not create PCGGraph asset: %s"), *GraphPackagePath));
		Result->SetObjectField(TEXT("pcg_status"), Status);
		Result->SetObjectField(TEXT("request"), Request);
		return MakeToolJsonObjectResult(Result, true);
	}

	UObject* InputNode = DruidsMCPBridge::GetObjectPropertyValueByName(Graph, TEXT("InputNode"));
	UObject* OutputNode = DruidsMCPBridge::GetObjectPropertyValueByName(Graph, TEXT("OutputNode"));
	UObject* RerouteNode = nullptr;
	UObject* GridNode = nullptr;
	UObject* TransformPointsNode = nullptr;
	UObject* SpawnActorNode = nullptr;
	FString GraphWireError;
	bool bGraphWired = false;
	const bool bUseTransformVariation = Request->GetStringField(TEXT("variation_mode")) == TEXT("transform_points");
	const FVector FixedGridExtents(400.0, 400.0, 50.0);
	const FVector FixedCellSize(200.0, 200.0, 100.0);
	const FVector GridExtents = bUseBoundsSpawnActorGraph ? DruidsMCPBridge::MakePCGGridExtentsFromRequest(Request) : FixedGridExtents;
	const FVector CellSize = bUseBoundsSpawnActorGraph ? DruidsMCPBridge::MakePCGCellSizeFromRequest(Request) : FixedCellSize;
	if (bUseSpawnActorGraph || bUseBoundsSpawnActorGraph)
	{
		GridNode = DruidsMCPBridge::CreatePCGCreatePointsGridNode(Graph, GridExtents, CellSize, GraphWireError);
		TransformPointsNode = (GridNode && bUseTransformVariation) ? DruidsMCPBridge::CreatePCGTransformPointsNode(Graph, Request, GraphWireError) : nullptr;
		SpawnActorNode = GridNode ? DruidsMCPBridge::CreatePCGSpawnActorNode(Graph, TreeActorClass, ForestName, GraphWireError) : nullptr;
		const bool bGridToVariation = !bUseTransformVariation || (GridNode && TransformPointsNode && DruidsMCPBridge::InvokePCGAddEdgeTo(GridNode, FName(TEXT("Out")), TransformPointsNode, FName(TEXT("In")), GraphWireError));
		const bool bVariationToSpawn = bUseTransformVariation
			? (TransformPointsNode && SpawnActorNode && DruidsMCPBridge::InvokePCGAddEdgeTo(TransformPointsNode, FName(TEXT("Out")), SpawnActorNode, FName(TEXT("In")), GraphWireError))
			: (GridNode && SpawnActorNode && DruidsMCPBridge::InvokePCGAddEdgeTo(GridNode, FName(TEXT("Out")), SpawnActorNode, FName(TEXT("In")), GraphWireError));
		const bool bSpawnToOutput = SpawnActorNode && DruidsMCPBridge::InvokePCGAddEdgeTo(SpawnActorNode, FName(TEXT("Out")), OutputNode, FName(TEXT("Out")), GraphWireError);
		bGraphWired = bGridToVariation && bVariationToSpawn && bSpawnToOutput;
	}
	else if (bUseGridGraph)
	{
		GridNode = DruidsMCPBridge::CreatePCGCreatePointsGridNode(Graph, GridExtents, CellSize, GraphWireError);
		bGraphWired = GridNode && DruidsMCPBridge::InvokePCGAddEdgeTo(GridNode, FName(TEXT("Out")), OutputNode, FName(TEXT("Out")), GraphWireError);
	}
	else if (bUseRerouteGraph)
	{
		RerouteNode = DruidsMCPBridge::CreatePCGRerouteNode(Graph, GraphWireError);
		const bool bInputToReroute = RerouteNode && DruidsMCPBridge::InvokePCGAddEdgeTo(InputNode, FName(TEXT("In")), RerouteNode, FName(TEXT("In")), GraphWireError);
		const bool bRerouteToOutput = RerouteNode && DruidsMCPBridge::InvokePCGAddEdgeTo(RerouteNode, FName(TEXT("Out")), OutputNode, FName(TEXT("Out")), GraphWireError);
		bGraphWired = bInputToReroute && bRerouteToOutput;
	}
	else
	{
		bGraphWired = DruidsMCPBridge::InvokePCGAddEdgeTo(InputNode, FName(TEXT("In")), OutputNode, FName(TEXT("Out")), GraphWireError);
	}
	if (bGraphWired)
	{
		Graph->Modify();
		Graph->MarkPackageDirty();
		GraphPackage->MarkPackageDirty();
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), true);
	Result->SetBoolField(TEXT("created"), !bExisted);
	Result->SetBoolField(TEXT("already_existed"), bExisted);
	Result->SetBoolField(TEXT("generated"), false);
	Result->SetStringField(TEXT("graph_mode"), bUseBoundsSpawnActorGraph ? TEXT("bounds_spawn_actor") : (bUseSpawnActorGraph ? TEXT("spawn_actor") : (bUseGridGraph ? TEXT("grid") : (bUseRerouteGraph ? TEXT("reroute") : TEXT("passthrough")))));
	Result->SetBoolField(TEXT("passthrough_wired"), bGraphWired);
	Result->SetBoolField(TEXT("reroute_node_created"), RerouteNode != nullptr);
	Result->SetBoolField(TEXT("grid_node_created"), GridNode != nullptr);
	Result->SetBoolField(TEXT("transform_points_node_created"), TransformPointsNode != nullptr);
	Result->SetBoolField(TEXT("spawn_actor_node_created"), SpawnActorNode != nullptr);
	Result->SetBoolField(TEXT("native_pcg_spawn_actor_graph"), bUseSpawnActorGraph || bUseBoundsSpawnActorGraph);
	Result->SetBoolField(TEXT("bounds_driven_forest_graph"), bUseBoundsSpawnActorGraph);
	Result->SetBoolField(TEXT("natural_variation_graph"), bUseTransformVariation && TransformPointsNode != nullptr);
	Result->SetStringField(TEXT("status"), bGraphWired
		? (bUseBoundsSpawnActorGraph ? TEXT("pcg_bounds_spawn_actor_graph_ready_generation_deferred") : (bUseSpawnActorGraph ? TEXT("pcg_spawn_actor_graph_ready_generation_deferred") : (bUseGridGraph ? TEXT("pcg_grid_graph_ready_generation_deferred") : (bUseRerouteGraph ? TEXT("pcg_reroute_graph_ready_generation_deferred") : TEXT("pcg_passthrough_graph_ready_generation_deferred")))))
		: TEXT("pcg_minimal_graph_asset_ready_generation_deferred"));
	Result->SetStringField(TEXT("message"), bUseBoundsSpawnActorGraph
		? TEXT("Created or reused a bounds-driven PCGGraph asset with Create Points Grid sized from AreaX/AreaY/TreeSpacing, wired into Spawn Actor and then Output.")
		: (bUseSpawnActorGraph
		? TEXT("Created or reused a PCGGraph asset with Create Points Grid wired into Spawn Actor and then Output. The Spawn Actor node targets the provided tree Blueprint class.")
		: (bUseGridGraph
		? TEXT("Created or reused a minimal PCGGraph asset with one deterministic Create Points Grid source node wired to Output(Out). Spawn graph authoring remains deferred.")
		: (bUseRerouteGraph
			? TEXT("Created or reused a minimal PCGGraph asset with one deterministic reroute node between Input(In) and Output(Out). Scatter/spawn graph authoring remains deferred.")
			: TEXT("Created or reused a minimal PCGGraph asset and attempted deterministic Input(In) to Output(Out) pass-through wiring. Scatter/spawn graph authoring remains deferred.")))));
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("request"), Request);
	Result->SetStringField(TEXT("graph_asset_path"), GraphPackagePath);
	Result->SetStringField(TEXT("graph_object_path"), Graph->GetPathName());
	Result->SetObjectField(TEXT("graph"), DruidsMCPBridge::MakePCGGraphSummary(Graph));
	if (RerouteNode)
	{
		Result->SetObjectField(TEXT("reroute_node"), DruidsMCPBridge::MakeObjectReferenceObject(RerouteNode));
	}
	if (GridNode)
	{
		Result->SetObjectField(TEXT("grid_node"), DruidsMCPBridge::MakeObjectReferenceObject(GridNode));
		TSharedPtr<FJsonObject> Grid = MakeShared<FJsonObject>();
		Grid->SetObjectField(TEXT("grid_extents"), DruidsMCPBridge::MakeVectorObject(GridExtents));
		Grid->SetObjectField(TEXT("cell_size"), DruidsMCPBridge::MakeVectorObject(CellSize));
		DruidsMCPBridge::AddPCGDensityFields(Grid, Request, GridExtents, CellSize);
		Result->SetObjectField(TEXT("grid"), Grid);
	}
	if (TransformPointsNode)
	{
		Result->SetObjectField(TEXT("transform_points_node"), DruidsMCPBridge::MakeObjectReferenceObject(TransformPointsNode));
		TSharedPtr<FJsonObject> Variation = MakeShared<FJsonObject>();
		Variation->SetStringField(TEXT("variation_mode"), Request->GetStringField(TEXT("variation_mode")));
		Variation->SetNumberField(TEXT("position_jitter"), Request->GetNumberField(TEXT("position_jitter")));
		Variation->SetNumberField(TEXT("offset_x"), Request->GetNumberField(TEXT("offset_x")));
		Variation->SetNumberField(TEXT("offset_y"), Request->GetNumberField(TEXT("offset_y")));
		Variation->SetNumberField(TEXT("offset_z"), Request->GetNumberField(TEXT("offset_z")));
		Variation->SetNumberField(TEXT("min_scale"), Request->GetNumberField(TEXT("min_scale")));
		Variation->SetNumberField(TEXT("max_scale"), Request->GetNumberField(TEXT("max_scale")));
		Variation->SetNumberField(TEXT("yaw_min"), Request->GetNumberField(TEXT("yaw_min")));
		Variation->SetNumberField(TEXT("yaw_max"), Request->GetNumberField(TEXT("yaw_max")));
		Variation->SetStringField(TEXT("node"), TEXT("PCGTransformPointsSettings"));
		Result->SetObjectField(TEXT("variation"), Variation);
	}
	if (SpawnActorNode)
	{
		Result->SetObjectField(TEXT("spawn_actor_node"), DruidsMCPBridge::MakeObjectReferenceObject(SpawnActorNode));
		Result->SetStringField(TEXT("spawn_actor_template_class_path"), TreeActorClass ? TreeActorClass->GetPathName() : TEXT(""));
	}
	if (!bGraphWired)
	{
		Result->SetStringField(TEXT("graph_wire_error"), GraphWireError);
	}
	Result->SetStringField(TEXT("next_step"), TEXT("Call pcg_inspect_graph with GraphPath, then pcg_place_forest_actor with GraphPath and AllowCurrentLevel=true."));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGPlaceForestActor(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_place_forest_actor"), Arguments, TEXT("place_forest_actor")), true);
	}

	if (!DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("AllowCurrentLevel"), false))
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("placed"), false);
		Result->SetStringField(TEXT("error"), TEXT("pcg_place_forest_actor requires AllowCurrentLevel=true because this mutates the current editor world."));
		Result->SetObjectField(TEXT("pcg_status"), Status);
		Result->SetObjectField(TEXT("request"), DruidsMCPBridge::MakePCGForestRequestSummary(Arguments));
		return MakeToolJsonObjectResult(Result, true);
	}

	const TSharedPtr<FJsonObject> Request = DruidsMCPBridge::MakePCGForestRequestSummary(Arguments);
	if (!Request->GetBoolField(TEXT("bounds_shape_supported")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnsupportedBoundsShapeResult(TEXT("pcg_place_forest_actor"), Request), true);
	}

	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	UClass* PCGVolumeClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGVolume"));
	if (!ActorSubsystem || !PCGVolumeClass)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("placed"), false);
		Result->SetStringField(TEXT("error"), TEXT("PCG is enabled but PCGVolume class or editor actor subsystem was not available."));
		Result->SetObjectField(TEXT("pcg_status"), Status);
		Result->SetObjectField(TEXT("request"), Request);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FVector Location = DruidsMCPBridge::GetVectorFromArgs(Arguments, TEXT("Location"), FVector::ZeroVector);
	AActor* ForestActor = ActorSubsystem->SpawnActorFromClass(PCGVolumeClass, Location, FRotator::ZeroRotator, false);
	if (!ForestActor)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("placed"), false);
		Result->SetStringField(TEXT("error"), TEXT("Could not spawn PCGVolume actor."));
		Result->SetObjectField(TEXT("pcg_status"), Status);
		Result->SetObjectField(TEXT("request"), Request);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FString ForestName = Request->GetStringField(TEXT("forest_name"));
	const FString GraphPath = GetOptionalStringParam(Arguments, TEXT("GraphPath")).TrimStartAndEnd();
	UObject* Graph = GraphPath.IsEmpty() ? nullptr : DruidsMCPBridge::LoadPCGGraphObject(GraphPath);
	UObject* PCGComponent = DruidsMCPBridge::FindPCGComponent(ForestActor);
	bool bGraphAttached = false;
	FString GraphAttachError;
	if (Graph && PCGComponent)
	{
		bGraphAttached = DruidsMCPBridge::InvokePCGSetGraph(PCGComponent, Graph, GraphAttachError);
		if (bGraphAttached)
		{
			PCGComponent->Modify();
			PCGComponent->MarkPackageDirty();
		}
	}
	DruidsMCPBridge::SetIntPropertyValueByName(PCGComponent, TEXT("Seed"), static_cast<int32>(Request->GetNumberField(TEXT("seed"))));
	FString GenerationTriggerError;
	DruidsMCPBridge::ImportPropertyValueByName(PCGComponent, TEXT("GenerationTrigger"), TEXT("GenerateOnDemand"), GenerationTriggerError);

	ForestActor->SetActorLabel(ForestName);
	ForestActor->Tags.AddUnique(TEXT("DruidsMCPBridge"));
	ForestActor->Tags.AddUnique(TEXT("DruidsRepro"));
	ForestActor->Tags.AddUnique(TEXT("DruidsVisualPCG"));
	ForestActor->Tags.AddUnique(TEXT("DruidsPCGForest"));
	ForestActor->Tags.AddUnique(FName(*FString::Printf(TEXT("DruidsPCGSeed_%d"), static_cast<int32>(Request->GetNumberField(TEXT("seed"))))));
	ForestActor->Tags.AddUnique(FName(*FString::Printf(TEXT("DruidsPCGTarget_%d"), static_cast<int32>(Request->GetNumberField(TEXT("target_count"))))));
	DruidsMCPBridge::ApplyPCGRectangleBoundsToActor(ForestActor, Request);
	ForestActor->Modify();
	ActorSubsystem->SelectNothing();
	ActorSubsystem->SetActorSelectionState(ForestActor, true);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), true);
	Result->SetBoolField(TEXT("placed"), true);
	Result->SetBoolField(TEXT("generated"), false);
	Result->SetStringField(TEXT("status"), TEXT("pcg_volume_scaffold_placed_graph_generation_deferred"));
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("request"), Request);
	Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(ForestActor, true));
	Result->SetObjectField(TEXT("rectangle_bounds"), DruidsMCPBridge::MakePCGRectangleBoundsSummary(ForestActor, Request));
	Result->SetBoolField(TEXT("graph_attached"), bGraphAttached);
	Result->SetStringField(TEXT("graph_path"), GraphPath);
	if (Graph)
	{
		Result->SetObjectField(TEXT("graph"), DruidsMCPBridge::MakePCGGraphSummary(Graph));
	}
	if (PCGComponent)
	{
		Result->SetObjectField(TEXT("pcg_component"), DruidsMCPBridge::MakeObjectReferenceObject(PCGComponent));
	}
	if (!GraphPath.IsEmpty() && !bGraphAttached)
	{
		Result->SetStringField(TEXT("graph_attach_error"), GraphAttachError.IsEmpty() ? TEXT("Graph or PCG component was unavailable.") : GraphAttachError);
	}
	Result->SetStringField(TEXT("next_step"), TEXT("Minimal PCG graph is attached when graph_attached=true. Scatter/spawn graph generation is still deferred; pcg_generate will not fake output."));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGInspectGraph(const TSharedPtr<FJsonObject>& Arguments) const
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_inspect_graph"), Arguments, TEXT("inspect_graph")), true);
	}

	const FString GraphPath = GetOptionalStringParam(Arguments, TEXT("GraphPath")).TrimStartAndEnd();
	UObject* Graph = DruidsMCPBridge::LoadPCGGraphObject(GraphPath);
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), Graph != nullptr);
	Result->SetStringField(TEXT("graph_path"), GraphPath);
	Result->SetObjectField(TEXT("pcg_status"), Status);
	if (!Graph)
	{
		Result->SetStringField(TEXT("status"), TEXT("pcg_graph_not_found"));
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("Could not load PCG graph asset: %s"), *GraphPath));
		return MakeToolJsonObjectResult(Result, true);
	}

	Result->SetStringField(TEXT("status"), TEXT("pcg_graph_inspected"));
	Result->SetObjectField(TEXT("graph"), DruidsMCPBridge::MakePCGGraphSummary(Graph));
	Result->SetBoolField(TEXT("generated"), false);
	Result->SetStringField(TEXT("next_step"), TEXT("Use this graph as the attachment target for pcg_place_forest_actor; deterministic scatter/spawn nodes are the next slice."));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGSetForestBounds(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_set_forest_bounds"), Arguments, TEXT("set_forest_bounds")), true);
	}

	const FString ForestActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ForestActorNameOrLabel")).TrimStartAndEnd();
	AActor* ForestActor = ForestActorNameOrLabel.IsEmpty() ? nullptr : DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ForestActorNameOrLabel);
	UObject* PCGComponent = DruidsMCPBridge::FindPCGComponent(ForestActor);
	const FString GraphPath = GetOptionalStringParam(Arguments, TEXT("GraphPath")).TrimStartAndEnd();
	UObject* Graph = GraphPath.IsEmpty() ? DruidsMCPBridge::ResolvePCGGraphFromComponent(PCGComponent) : DruidsMCPBridge::LoadPCGGraphObject(GraphPath);
	UObject* GridNode = DruidsMCPBridge::FindPCGNodeByName(Graph, TEXT("DruidsCreatePointsGrid"));
	const TSharedPtr<FJsonObject> Request = DruidsMCPBridge::MakePCGForestRequestSummary(Arguments);
	if (!Request->GetBoolField(TEXT("bounds_shape_supported")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnsupportedBoundsShapeResult(TEXT("pcg_set_forest_bounds"), Request), true);
	}
	const FVector GridExtents = DruidsMCPBridge::MakePCGGridExtentsFromRequest(Request);
	const FVector CellSize = DruidsMCPBridge::MakePCGCellSizeFromRequest(Request);

	FString ConfigureError;
	const bool bConfigured = DruidsMCPBridge::ConfigurePCGCreatePointsGridNode(GridNode, GridExtents, CellSize, ConfigureError);
	if (Graph && bConfigured)
	{
		Graph->Modify();
		Graph->MarkPackageDirty();
	}
	FString GraphRefreshError;
	FString GraphClearError;
	const bool bGraphCleared = bConfigured && PCGComponent && DruidsMCPBridge::InvokePCGSetGraph(PCGComponent, nullptr, GraphClearError);
	const bool bGraphRefreshed = bConfigured && PCGComponent && Graph && DruidsMCPBridge::InvokePCGSetGraph(PCGComponent, Graph, GraphRefreshError);
	FString CleanupError;
	bool bCleanedGeneratedOutput = false;
	FString CleanupMethod;
	if (bGraphRefreshed)
	{
		bCleanedGeneratedOutput = DruidsMCPBridge::InvokePCGCleanupLocalImmediate(PCGComponent, true, true, CleanupError);
		CleanupMethod = bCleanedGeneratedOutput ? TEXT("CleanupLocalImmediate") : TEXT("");
		if (!bCleanedGeneratedOutput)
		{
			CleanupError.Reset();
			bCleanedGeneratedOutput = DruidsMCPBridge::InvokePCGCleanupLocal(PCGComponent, true, false, CleanupError);
			CleanupMethod = bCleanedGeneratedOutput ? TEXT("CleanupLocal") : TEXT("");
		}
	}
	if (PCGComponent && (bGraphRefreshed || bCleanedGeneratedOutput))
	{
		DruidsMCPBridge::SetBoolPropertyValueByName(PCGComponent, TEXT("bGenerated"), false);
		DruidsMCPBridge::SetBoolPropertyValueByName(PCGComponent, TEXT("bDirtyGenerated"), true);
		PCGComponent->Modify();
		PCGComponent->MarkPackageDirty();
	}
	if (ForestActor && bConfigured)
	{
		DruidsMCPBridge::ApplyPCGRectangleBoundsToActor(ForestActor, Request);
		ForestActor->Modify();
		ForestActor->MarkPackageDirty();
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), bConfigured);
	Result->SetStringField(TEXT("status"), bConfigured ? TEXT("pcg_forest_bounds_updated") : TEXT("pcg_forest_bounds_update_failed"));
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("request"), Request);
	Result->SetStringField(TEXT("forest_actor_name_or_label"), ForestActorNameOrLabel);
	Result->SetStringField(TEXT("graph_path"), GraphPath);
	Result->SetBoolField(TEXT("actor_found"), ForestActor != nullptr);
	Result->SetBoolField(TEXT("pcg_component_found"), PCGComponent != nullptr);
	Result->SetBoolField(TEXT("graph_found"), Graph != nullptr);
	Result->SetBoolField(TEXT("grid_node_found"), GridNode != nullptr);
	Result->SetBoolField(TEXT("bounds_driven_forest_graph"), bConfigured);
	Result->SetBoolField(TEXT("graph_cleared_before_refresh"), bGraphCleared);
	Result->SetBoolField(TEXT("graph_refreshed_on_component"), bGraphRefreshed);
	Result->SetBoolField(TEXT("cleaned_generated_output"), bCleanedGeneratedOutput);
	Result->SetStringField(TEXT("cleanup_method"), CleanupMethod);
	if (ForestActor)
	{
		Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(ForestActor, false));
		Result->SetObjectField(TEXT("rectangle_bounds"), DruidsMCPBridge::MakePCGRectangleBoundsSummary(ForestActor, Request));
	}
	if (GridNode)
	{
		Result->SetObjectField(TEXT("grid_node"), DruidsMCPBridge::MakeObjectReferenceObject(GridNode));
	}

	TSharedPtr<FJsonObject> Grid = MakeShared<FJsonObject>();
	Grid->SetObjectField(TEXT("grid_extents"), DruidsMCPBridge::MakeVectorObject(GridExtents));
	Grid->SetObjectField(TEXT("cell_size"), DruidsMCPBridge::MakeVectorObject(CellSize));
	DruidsMCPBridge::AddPCGDensityFields(Grid, Request, GridExtents, CellSize);
	Grid->SetStringField(TEXT("count_rule"), TEXT("floor(AreaX / TreeSpacing) * floor(AreaY / TreeSpacing) * 1, clamped to at least one cell per axis"));
	Result->SetObjectField(TEXT("grid"), Grid);
	if (Graph)
	{
		Result->SetObjectField(TEXT("graph"), DruidsMCPBridge::MakePCGGraphSummary(Graph));
	}
	if (!bConfigured)
	{
		Result->SetStringField(TEXT("error"), ConfigureError.IsEmpty() ? TEXT("Could not find or configure the DruidsCreatePointsGrid node.") : ConfigureError);
	}
	if (!GraphRefreshError.IsEmpty())
	{
		Result->SetStringField(TEXT("graph_refresh_error"), GraphRefreshError);
	}
	if (!GraphClearError.IsEmpty())
	{
		Result->SetStringField(TEXT("graph_clear_error"), GraphClearError);
	}
	if (!CleanupError.IsEmpty())
	{
		Result->SetStringField(TEXT("cleanup_error"), CleanupError);
	}
	Result->SetStringField(TEXT("next_step"), bConfigured ? TEXT("Call pcg_generate for this forest actor, then pcg_inspect_generated to confirm the new native spawned actor count.") : TEXT("Create a BoundsSpawnActor graph with pcg_create_forest_repro, attach it with pcg_place_forest_actor, then retry."));
	return MakeToolJsonObjectResult(Result, !bConfigured);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGGenerate(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_generate"), Arguments, TEXT("generate")), true);
	}
	const FString ForestActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ForestActorNameOrLabel"));
	AActor* ForestActor = ForestActorNameOrLabel.IsEmpty() ? nullptr : DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ForestActorNameOrLabel);
	UObject* PCGComponent = DruidsMCPBridge::FindPCGComponent(ForestActor);
	FString GenerateError;
	const bool bRequested = DruidsMCPBridge::InvokePCGGenerateLocal(PCGComponent, true, GenerateError);
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), bRequested);
	Result->SetBoolField(TEXT("generation_requested"), bRequested);
	Result->SetStringField(TEXT("status"), bRequested ? TEXT("pcg_generation_requested") : TEXT("pcg_generation_request_failed"));
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetStringField(TEXT("forest_actor_name_or_label"), ForestActorNameOrLabel);
	if (ForestActor)
	{
		Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(ForestActor, false));
	}
	if (PCGComponent)
	{
		Result->SetObjectField(TEXT("pcg_component"), DruidsMCPBridge::MakePCGComponentGenerationSummary(PCGComponent));
		Result->SetBoolField(TEXT("generated"), DruidsMCPBridge::GetBoolPropertyValueByName(PCGComponent, TEXT("bGenerated")));
		Result->SetStringField(TEXT("note"), TEXT("This requests graph execution and reports PCG component output state. Visible forest/spawn generation is still not implemented."));
	}
	else
	{
		Result->SetBoolField(TEXT("generated"), false);
		Result->SetStringField(TEXT("error"), ForestActor ? TEXT("Forest actor has no PCG component.") : TEXT("Forest actor was not found."));
	}
	if (!bRequested && !GenerateError.IsEmpty())
	{
		Result->SetStringField(TEXT("generate_error"), GenerateError);
	}
	return MakeToolJsonObjectResult(Result, !bRequested);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGCreateVisualReproOutput(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_create_visual_repro_output"), Arguments, TEXT("create_visual_repro_output")), true);
	}

	if (!DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("AllowCurrentLevel"), false))
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("visual_output_created"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_visual_output_requires_explicit_level_mutation"));
		Result->SetStringField(TEXT("error"), TEXT("pcg_create_visual_repro_output requires AllowCurrentLevel=true because it creates visible repro actors in the current editor world."));
		Result->SetObjectField(TEXT("pcg_status"), Status);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FString ForestActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ForestActorNameOrLabel")).TrimStartAndEnd();
	const FString TreeBlueprintPath = GetOptionalStringParam(Arguments, TEXT("TreeBlueprintPath")).TrimStartAndEnd();
	AActor* ForestActor = ForestActorNameOrLabel.IsEmpty() ? nullptr : DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ForestActorNameOrLabel);
	UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
	if (!ForestActor || !ActorSubsystem)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("visual_output_created"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_visual_output_forest_actor_not_found"));
		Result->SetStringField(TEXT("error"), ForestActor ? TEXT("Editor actor subsystem was unavailable.") : FString::Printf(TEXT("Could not find forest actor '%s'."), *ForestActorNameOrLabel));
		Result->SetObjectField(TEXT("pcg_status"), Status);
		return MakeToolJsonObjectResult(Result, true);
	}

	FString ResolvedTreePath;
	FString ResolveError;
	ResolveAssetToObject(TreeBlueprintPath, TEXT("Blueprint"), 10, ResolvedTreePath, ResolveError);
	const FString TreeLoadPath = ResolvedTreePath.IsEmpty()
		? DruidsMCPBridge::MakeObjectPathIfPackagePath(TreeBlueprintPath)
		: DruidsMCPBridge::MakeBlueprintObjectPath(ResolvedTreePath);
	UBlueprint* TreeBlueprint = LoadObject<UBlueprint>(nullptr, *TreeLoadPath);
	UClass* TreeClass = TreeBlueprint ? TreeBlueprint->GeneratedClass : nullptr;
	if (!TreeClass || !TreeClass->IsChildOf(AActor::StaticClass()))
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetBoolField(TEXT("visual_output_created"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_visual_output_tree_blueprint_not_found"));
		Result->SetStringField(TEXT("error"), ResolveError.IsEmpty()
			? FString::Printf(TEXT("Could not resolve an Actor Blueprint class from '%s'."), *TreeBlueprintPath)
			: ResolveError);
		Result->SetStringField(TEXT("tree_blueprint_path"), TreeBlueprintPath);
		Result->SetObjectField(TEXT("pcg_status"), Status);
		return MakeToolJsonObjectResult(Result, true);
	}

	const FString ForestLabel = ForestActor->GetActorLabel();
	const FName VisualOutputTag(TEXT("DruidsPCGVisualOutput"));
	const FName ForestOutputTag(*FString::Printf(TEXT("DruidsPCGForestOutput_%s"), *ForestLabel));
	int32 DestroyedExisting = 0;
	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CleanupExisting"), true))
	{
		TArray<AActor*> ActorsToDestroy;
		for (AActor* Actor : ActorSubsystem->GetAllLevelActors())
		{
			if (Actor && Actor->Tags.Contains(VisualOutputTag) && Actor->Tags.Contains(ForestOutputTag))
			{
				ActorsToDestroy.Add(Actor);
			}
		}
		for (AActor* Actor : ActorsToDestroy)
		{
			if (ActorSubsystem->DestroyActor(Actor))
			{
				++DestroyedExisting;
			}
		}
	}

	const double Spacing = FMath::Max(1, GetOptionalIntParam(Arguments, TEXT("Spacing"), 200));
	const FVector Origin = ForestActor->GetActorLocation();
	TArray<TSharedPtr<FJsonValue>> ActorValues;
	TArray<TSharedPtr<FJsonValue>> PointValues;
	FBox CombinedBounds(EForceInit::ForceInit);
	int32 SpawnedCount = 0;
	ActorSubsystem->SelectNothing();
	for (int32 Y = 0; Y < 4; ++Y)
	{
		for (int32 X = 0; X < 4; ++X)
		{
			const int32 PointIndex = Y * 4 + X;
			const FVector Offset((static_cast<double>(X) - 1.5) * Spacing, (static_cast<double>(Y) - 1.5) * Spacing, 0.0);
			const FVector Location = Origin + Offset;
			AActor* TreeActor = ActorSubsystem->SpawnActorFromClass(TreeClass, Location, FRotator::ZeroRotator, false);
			if (!TreeActor)
			{
				continue;
			}

			TreeActor->SetActorLabel(FString::Printf(TEXT("%s_Tree_%02d"), *ForestLabel, PointIndex));
			TreeActor->Tags.AddUnique(TEXT("DruidsMCPBridge"));
			TreeActor->Tags.AddUnique(TEXT("DruidsRepro"));
			TreeActor->Tags.AddUnique(TEXT("DruidsVisualPCG"));
			TreeActor->Tags.AddUnique(TEXT("DruidsPCGForestOutput"));
			TreeActor->Tags.AddUnique(VisualOutputTag);
			TreeActor->Tags.AddUnique(ForestOutputTag);
			TreeActor->Tags.AddUnique(FName(*FString::Printf(TEXT("DruidsPCGPoint_%02d"), PointIndex)));
			TreeActor->Modify();
			ActorSubsystem->SetActorSelectionState(TreeActor, true);

			CombinedBounds += TreeActor->GetComponentsBoundingBox(false);
			ActorValues.Add(MakeShared<FJsonValueObject>(DruidsMCPBridge::MakeEditorActorSummary(TreeActor, true)));
			TSharedPtr<FJsonObject> Point = MakeShared<FJsonObject>();
			Point->SetNumberField(TEXT("index"), PointIndex);
			Point->SetObjectField(TEXT("offset"), DruidsMCPBridge::MakeVectorObject(Offset));
			Point->SetObjectField(TEXT("location"), DruidsMCPBridge::MakeVectorObject(Location));
			PointValues.Add(MakeShared<FJsonValueObject>(Point));
			++SpawnedCount;
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), SpawnedCount == 16);
	Result->SetBoolField(TEXT("visual_output_created"), SpawnedCount > 0);
	Result->SetBoolField(TEXT("native_pcg_spawn_actor_generation"), false);
	Result->SetStringField(TEXT("status"), SpawnedCount == 16 ? TEXT("pcg_visual_repro_output_created") : TEXT("pcg_visual_repro_output_partial"));
	Result->SetStringField(TEXT("visualization_source"), TEXT("deterministic_grid_contract"));
	Result->SetStringField(TEXT("contract"), TEXT("Slice 62 Create Points Grid: GridExtents=(400,400,50), CellSize=(200,200,100), expected 4x4x1 = 16 points."));
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("forest_actor"), DruidsMCPBridge::MakeEditorActorSummary(ForestActor, false));
	Result->SetStringField(TEXT("forest_actor_name_or_label"), ForestActorNameOrLabel);
	Result->SetStringField(TEXT("tree_blueprint_path"), TreeBlueprintPath);
	Result->SetStringField(TEXT("tree_generated_class_path"), TreeClass->GetPathName());
	Result->SetNumberField(TEXT("expected_visual_output_count"), 16);
	Result->SetNumberField(TEXT("visual_output_count"), SpawnedCount);
	Result->SetNumberField(TEXT("destroyed_existing_visual_output_count"), DestroyedExisting);
	Result->SetNumberField(TEXT("spacing"), Spacing);
	Result->SetArrayField(TEXT("actors"), ActorValues);
	Result->SetArrayField(TEXT("points"), PointValues);
	Result->SetObjectField(TEXT("combined_bounds"), CombinedBounds.IsValid ? DruidsMCPBridge::MakeBoxObject(CombinedBounds) : MakeShared<FJsonObject>());
	Result->SetStringField(TEXT("next_step"), TEXT("Call editor_capture_viewport with selected actors or read pcg_inspect_generated to confirm visible repro output count. Native PCG SpawnActor graph generation is still a later slice."));
	return MakeToolJsonObjectResult(Result, SpawnedCount == 0);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGInspectGenerated(const TSharedPtr<FJsonObject>& Arguments) const
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	const FString ForestActorNameOrLabel = GetOptionalStringParam(Arguments, TEXT("ForestActorNameOrLabel"));
	AActor* ForestActor = ForestActorNameOrLabel.IsEmpty() ? nullptr : DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ForestActorNameOrLabel);
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), ForestActor != nullptr);
	UObject* PCGComponent = DruidsMCPBridge::FindPCGComponent(ForestActor);
	const bool bGenerated = DruidsMCPBridge::GetBoolPropertyValueByName(PCGComponent, TEXT("bGenerated"));
	Result->SetBoolField(TEXT("generated"), bGenerated);
	Result->SetNumberField(TEXT("generated_count"), 0);
	Result->SetNumberField(TEXT("visual_output_count"), 0);
	Result->SetNumberField(TEXT("native_spawned_actor_count"), 0);
	Result->SetBoolField(TEXT("native_pcg_spawn_actor_generation"), false);
	Result->SetStringField(TEXT("status"), Status->GetBoolField(TEXT("pcg_available")) ? TEXT("pcg_generated_output_inspected") : TEXT("pcg_unavailable"));
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetStringField(TEXT("forest_actor_name_or_label"), ForestActorNameOrLabel);
	if (ForestActor)
	{
		Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(ForestActor, false));
		if (PCGComponent)
		{
			TSharedPtr<FJsonObject> ComponentSummary = DruidsMCPBridge::MakePCGComponentGenerationSummary(PCGComponent);
			Result->SetObjectField(TEXT("pcg_component"), ComponentSummary);
			Result->SetNumberField(TEXT("native_spawned_actor_count"), static_cast<int32>(ComponentSummary->GetNumberField(TEXT("native_spawned_actor_count"))));
			if (const TSharedPtr<FJsonObject>* Output = nullptr; ComponentSummary->TryGetObjectField(TEXT("generated_graph_output"), Output) && Output && Output->IsValid())
			{
				Result->SetNumberField(TEXT("generated_count"), static_cast<int32>((*Output)->GetNumberField(TEXT("total_point_count"))));
			}
		}
		UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
		const FName VisualOutputTag(TEXT("DruidsPCGVisualOutput"));
		const FName ForestOutputTag(*FString::Printf(TEXT("DruidsPCGForestOutput_%s"), *ForestActor->GetActorLabel()));
		TArray<TSharedPtr<FJsonValue>> VisualActors;
		if (ActorSubsystem)
		{
			for (AActor* Actor : ActorSubsystem->GetAllLevelActors())
			{
				if (Actor && Actor->Tags.Contains(VisualOutputTag) && Actor->Tags.Contains(ForestOutputTag))
				{
					VisualActors.Add(MakeShared<FJsonValueObject>(DruidsMCPBridge::MakeEditorActorSummary(Actor, false)));
				}
			}
		}
		Result->SetNumberField(TEXT("visual_output_count"), VisualActors.Num());
		Result->SetArrayField(TEXT("visual_output_actors"), VisualActors);
		Result->SetBoolField(TEXT("native_pcg_spawn_actor_generation"), Result->GetNumberField(TEXT("native_spawned_actor_count")) > 0);
		Result->SetStringField(TEXT("note"), TEXT("generated_count reports PCG point-output count when available. native_spawned_actor_count reports actors owned by PCG managed resources. visual_output_count reports bridge-created repro tree actors from pcg_create_visual_repro_output."));
	}
	else
	{
		Result->SetStringField(TEXT("error"), TEXT("No forest actor found for inspection, or PCG generation is unavailable."));
	}
	return MakeToolJsonObjectResult(Result, ForestActor == nullptr);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGRunForestRecipe(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_run_forest_recipe"), Arguments, TEXT("run_forest_recipe")), true);
	}

	const FString PackagePath = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/VisualPCG"));
	FString TreeName = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("TreeName"), TEXT("BP_Druids_Tree")).TrimStartAndEnd();
	if (TreeName.IsEmpty())
	{
		TreeName = TEXT("BP_Druids_Tree");
	}
	FString ForestName = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("ForestName"), TEXT("Druids_Forest")).TrimStartAndEnd();
	if (ForestName.IsEmpty())
	{
		ForestName = TEXT("Druids_Forest");
	}

	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CleanupExisting"), true))
	{
		TSharedPtr<FJsonObject> CleanupArgs = MakeShared<FJsonObject>();
		CleanupArgs->SetStringField(TEXT("Filter"), ForestName);
		CleanupArgs->SetStringField(TEXT("DryRun"), TEXT("false"));
		ExecuteEditorCleanupReproActors(CleanupArgs);
	}

	const FString TreeStyle = DruidsMCPBridge::GetPCGTreeStyle(Arguments);
	FString TreeBlueprintPath = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("TreeBlueprintPath")).TrimStartAndEnd();
	TSharedPtr<FJsonObject> TreeResult;
	TSharedPtr<FJsonObject> CompileResult;
	if (TreeBlueprintPath.IsEmpty())
	{
		TreeBlueprintPath = FString::Printf(TEXT("%s/%s"), *PackagePath, *TreeName);
		TSharedPtr<FJsonObject> TreeArgs = MakeShared<FJsonObject>();
		TreeArgs->SetStringField(TEXT("PackagePath"), PackagePath);
		TreeArgs->SetStringField(TEXT("AssetName"), TreeName);
		TreeArgs->SetStringField(TEXT("Preset"), TEXT("Tree"));
		DruidsMCPBridge::ApplyPCGTreeStyleDefaults(TreeStyle, TreeArgs);
		TreeResult = ExtractFirstObjectFromToolContent(ExecuteBlueprintCreatePrimitiveActor(TreeArgs));

		TSharedPtr<FJsonObject> CompileArgs = MakeShared<FJsonObject>();
		CompileArgs->SetStringField(TEXT("BlueprintNameOrPath"), TreeBlueprintPath);
		CompileResult = ExtractFirstObjectFromToolContent(ExecuteBlueprintCompile(CompileArgs));
	}

	TSharedPtr<FJsonObject> EffectiveArgs = DruidsMCPBridge::MakePCGRecipeForestArguments(Arguments, TreeBlueprintPath, ForestName, FString::Printf(TEXT("%s_Graph"), *ForestName));
	TSharedPtr<FJsonObject> SplineResult;
	const FString BoundsShape = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("BoundsShape"), TEXT("Rectangle"));
	const bool bSplineRectangle = BoundsShape.Equals(TEXT("SplineRectangle"), ESearchCase::IgnoreCase) || BoundsShape.Equals(TEXT("SplineRect"), ESearchCase::IgnoreCase);
	if (bSplineRectangle)
	{
		FString SplineActorName = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("SplineBoundsActor"), FString::Printf(TEXT("%s_SplineBounds"), *ForestName)).TrimStartAndEnd();
		AActor* SplineActor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(SplineActorName);
		USplineComponent* Spline = DruidsMCPBridge::FindSplineBoundsComponent(SplineActor);
		if (!Spline)
		{
			TSharedPtr<FJsonObject> SplineArgs = MakeShared<FJsonObject>();
			SplineArgs->SetStringField(TEXT("ActorName"), SplineActorName);
			SplineArgs->SetStringField(TEXT("AllowCurrentLevel"), TEXT("true"));
			SplineArgs->SetStringField(TEXT("Width"), FString::SanitizeFloat(DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("AreaX"), 1600.0)));
			SplineArgs->SetStringField(TEXT("Depth"), FString::SanitizeFloat(DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("AreaY"), 1600.0)));
			SplineResult = ExtractFirstObjectFromToolContent(ExecutePCGCreateSplineBoundsActor(SplineArgs));
			SplineActor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(SplineActorName);
			Spline = DruidsMCPBridge::FindSplineBoundsComponent(SplineActor);
		}
		else
		{
			TSharedPtr<FJsonObject> SplineArgs = MakeShared<FJsonObject>();
			SplineArgs->SetStringField(TEXT("ActorNameOrLabel"), SplineActorName);
			SplineResult = ExtractFirstObjectFromToolContent(ExecutePCGInspectSplineBoundsActor(SplineArgs));
		}
		const FVector2D SplineSize = DruidsMCPBridge::EstimateRectangleSplineSize(Spline, FVector2D(
			DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("AreaX"), 1600.0),
			DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("AreaY"), 1600.0)));
		EffectiveArgs->SetStringField(TEXT("AreaX"), FString::SanitizeFloat(SplineSize.X));
		EffectiveArgs->SetStringField(TEXT("AreaY"), FString::SanitizeFloat(SplineSize.Y));
	}

	TSharedPtr<FJsonObject> CreateResult = ExtractFirstObjectFromToolContent(ExecutePCGCreateForestRepro(EffectiveArgs));
	const FString GraphPath = CreateResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(CreateResult, TEXT("graph_asset_path")) : TEXT("");
	TSharedPtr<FJsonObject> PlaceArgs = DruidsMCPBridge::MakePCGRecipePlaceArguments(EffectiveArgs, TreeBlueprintPath, ForestName, GraphPath);
	TSharedPtr<FJsonObject> PlaceResult = ExtractFirstObjectFromToolContent(ExecutePCGPlaceForestActor(PlaceArgs));

	TSharedPtr<FJsonObject> GenerateArgs = MakeShared<FJsonObject>();
	GenerateArgs->SetStringField(TEXT("ForestActorNameOrLabel"), ForestName);
	TSharedPtr<FJsonObject> GenerateResult = ExtractFirstObjectFromToolContent(ExecutePCGGenerate(GenerateArgs));
	TSharedPtr<FJsonObject> InspectResult = ExtractFirstObjectFromToolContent(ExecutePCGInspectGenerated(GenerateArgs));

	TSharedPtr<FJsonObject> CaptureResult;
	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CaptureViewport"), true))
	{
		UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
		if (ActorSubsystem && InspectResult.IsValid())
		{
			ActorSubsystem->SelectNothing();
			const TSharedPtr<FJsonObject>* ComponentObject = nullptr;
			if (InspectResult->TryGetObjectField(TEXT("pcg_component"), ComponentObject) && ComponentObject && ComponentObject->IsValid())
			{
				const TArray<TSharedPtr<FJsonValue>>* NativeActors = nullptr;
				if ((*ComponentObject)->TryGetArrayField(TEXT("native_spawned_actors"), NativeActors) && NativeActors)
				{
					for (const TSharedPtr<FJsonValue>& ActorValue : *NativeActors)
					{
						const TSharedPtr<FJsonObject> ActorObject = ActorValue.IsValid() ? ActorValue->AsObject() : nullptr;
						const FString ActorPath = ActorObject.IsValid() ? DruidsMCPBridge::GetOptionalStringField(ActorObject, TEXT("path")) : TEXT("");
						if (AActor* Actor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ActorPath))
						{
							ActorSubsystem->SetActorSelectionState(Actor, true);
						}
					}
				}
			}
		}
		TSharedPtr<FJsonObject> CaptureArgs = MakeShared<FJsonObject>();
		CaptureArgs->SetStringField(TEXT("Focus"), TEXT("true"));
		CaptureArgs->SetStringField(TEXT("FilenamePrefix"), FString::Printf(TEXT("PCGForestRecipe_%s"), *ForestName));
		CaptureResult = ExtractFirstObjectFromToolContent(ExecuteEditorCaptureViewport(CaptureArgs));
	}

	TSharedPtr<FJsonObject> Variety = MakeShared<FJsonObject>();
	Variety->SetStringField(TEXT("tree_style"), TreeStyle);
	Variety->SetStringField(TEXT("variety_mode"), TEXT("safe_visual_transform_variation"));
	Variety->SetBoolField(TEXT("true_multi_class_spawn_supported"), false);
	Variety->SetStringField(TEXT("true_multi_class_spawn_tool"), TEXT("pcg_multiclass_spawn_proof"));
	Variety->SetObjectField(TEXT("variation_request"), DruidsMCPBridge::MakePCGForestRequestSummary(EffectiveArgs));

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), InspectResult.IsValid() && DruidsMCPBridge::GetOptionalStringField(InspectResult, TEXT("status")).Equals(TEXT("pcg_generated_output_inspected")));
	Result->SetStringField(TEXT("status"), TEXT("pcg_forest_recipe_completed"));
	Result->SetStringField(TEXT("tree_blueprint_path"), TreeBlueprintPath);
	Result->SetStringField(TEXT("forest_name"), ForestName);
	Result->SetStringField(TEXT("graph_path"), GraphPath);
	Result->SetBoolField(TEXT("generated"), GenerateResult.IsValid() && DruidsMCPBridge::GetOptionalBoolField(GenerateResult, TEXT("generation_requested"), false));
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("request"), EffectiveArgs);
	Result->SetObjectField(TEXT("variety"), Variety);
	Result->SetStringField(TEXT("bounds_shape"), bSplineRectangle ? TEXT("spline_rectangle") : TEXT("rectangle"));
	Result->SetStringField(TEXT("generation_mode"), bSplineRectangle ? TEXT("bounds_from_spline_rectangle") : TEXT("rectangle_bounds_spawn_actor"));
	if (TreeResult.IsValid())
	{
		Result->SetObjectField(TEXT("tree"), TreeResult);
	}
	if (CompileResult.IsValid())
	{
		Result->SetObjectField(TEXT("tree_compile"), CompileResult);
	}
	if (SplineResult.IsValid())
	{
		Result->SetObjectField(TEXT("spline_bounds_actor"), SplineResult);
	}
	Result->SetObjectField(TEXT("create_graph"), CreateResult.IsValid() ? CreateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("place_actor"), PlaceResult.IsValid() ? PlaceResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("generate"), GenerateResult.IsValid() ? GenerateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("inspect"), InspectResult.IsValid() ? InspectResult : MakeShared<FJsonObject>());
	if (CaptureResult.IsValid())
	{
		Result->SetObjectField(TEXT("viewport_capture"), CaptureResult);
		Result->SetStringField(TEXT("screenshot_path"), DruidsMCPBridge::GetOptionalStringField(CaptureResult, TEXT("screenshot_path")));
	}
	TArray<FString> NextTools = { TEXT("pcg_update_forest_recipe"), TEXT("pcg_inspect_generated"), TEXT("editor_capture_viewport") };
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue(NextTools));
	Result->SetStringField(TEXT("next_step"), TEXT("Use pcg_update_forest_recipe to resize, change target count, or make the forest more natural."));
	return MakeToolJsonObjectResult(Result, !Result->GetBoolField(TEXT("success")));
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGRunRoadRecipe(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_run_road_recipe"), Arguments, TEXT("run_road_recipe")), true);
	}

	const FString PackagePath = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/CityPCG"));
	FString RoadName = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("RoadName"), TEXT("BP_Druids_RoadSegment")).TrimStartAndEnd();
	if (RoadName.IsEmpty())
	{
		RoadName = TEXT("BP_Druids_RoadSegment");
	}
	FString RoadNetworkName = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("RoadNetworkName"), TEXT("Druids_Roads")).TrimStartAndEnd();
	if (RoadNetworkName.IsEmpty())
	{
		RoadNetworkName = TEXT("Druids_Roads");
	}

	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CleanupExisting"), true))
	{
		TSharedPtr<FJsonObject> CleanupArgs = MakeShared<FJsonObject>();
		CleanupArgs->SetStringField(TEXT("Filter"), RoadNetworkName);
		CleanupArgs->SetStringField(TEXT("DryRun"), TEXT("false"));
		ExecuteEditorCleanupReproActors(CleanupArgs);
	}

	const double SegmentLength = FMath::Max(50.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("SegmentLength"), 400.0));
	const double RoadWidth = FMath::Max(20.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("RoadWidth"), 180.0));
	const double RoadHeight = FMath::Max(2.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("RoadHeight"), 20.0));
	const int32 SegmentCount = FMath::Max(1, DruidsMCPBridge::GetOptionalIntField(Arguments, TEXT("SegmentCount"), 5));
	const double RoadLength = FMath::Max(SegmentLength, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("RoadLength"), SegmentLength * SegmentCount));
	FString Axis = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("Axis"), TEXT("X")).TrimStartAndEnd().ToUpper();
	if (Axis != TEXT("Y"))
	{
		Axis = TEXT("X");
	}
	const double BlueprintLength = Axis == TEXT("Y") ? RoadWidth : SegmentLength;
	const double BlueprintWidth = Axis == TEXT("Y") ? SegmentLength : RoadWidth;
	const double GraphAreaX = Axis == TEXT("Y") ? RoadWidth : RoadLength;
	const double GraphAreaY = Axis == TEXT("Y") ? RoadLength : RoadWidth;

	FString RoadBlueprintPath = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("RoadBlueprintPath")).TrimStartAndEnd();
	TSharedPtr<FJsonObject> RoadResult;
	TSharedPtr<FJsonObject> CompileResult;
	if (RoadBlueprintPath.IsEmpty())
	{
		RoadBlueprintPath = FString::Printf(TEXT("%s/%s"), *PackagePath, *RoadName);
		FString LoadError;
		if (!LoadBlueprint(RoadBlueprintPath, LoadError))
		{
			TSharedPtr<FJsonObject> RoadArgs = MakeShared<FJsonObject>();
			RoadArgs->SetStringField(TEXT("PackagePath"), PackagePath);
			RoadArgs->SetStringField(TEXT("AssetName"), RoadName);
			RoadArgs->SetStringField(TEXT("Length"), FString::SanitizeFloat(BlueprintLength));
			RoadArgs->SetStringField(TEXT("Width"), FString::SanitizeFloat(BlueprintWidth));
			RoadArgs->SetStringField(TEXT("Height"), FString::SanitizeFloat(RoadHeight));
			RoadResult = ExtractFirstObjectFromToolContent(ExecuteBlueprintCreateRoadActor(RoadArgs));
		}
	}

	TSharedPtr<FJsonObject> CompileArgs = MakeShared<FJsonObject>();
	CompileArgs->SetStringField(TEXT("BlueprintNameOrPath"), RoadBlueprintPath);
	CompileResult = ExtractFirstObjectFromToolContent(ExecuteBlueprintCompile(CompileArgs));

	TSharedPtr<FJsonObject> GraphArgs = MakeShared<FJsonObject>();
	GraphArgs->SetStringField(TEXT("TreeBlueprintPath"), RoadBlueprintPath);
	GraphArgs->SetStringField(TEXT("PackagePath"), PackagePath);
	GraphArgs->SetStringField(TEXT("ForestName"), FString::Printf(TEXT("%s_Graph"), *RoadNetworkName));
	GraphArgs->SetStringField(TEXT("GraphMode"), TEXT("BoundsSpawnActor"));
	GraphArgs->SetStringField(TEXT("BoundsShape"), TEXT("Rectangle"));
	GraphArgs->SetStringField(TEXT("SpacingMode"), TEXT("Explicit"));
	GraphArgs->SetStringField(TEXT("VariationMode"), TEXT("None"));
	GraphArgs->SetStringField(TEXT("Seed"), FString::FromInt(DruidsMCPBridge::GetOptionalIntField(Arguments, TEXT("Seed"), 2024)));
	GraphArgs->SetStringField(TEXT("TargetCount"), FString::FromInt(SegmentCount));
	GraphArgs->SetStringField(TEXT("AreaX"), FString::SanitizeFloat(GraphAreaX));
	GraphArgs->SetStringField(TEXT("AreaY"), FString::SanitizeFloat(GraphAreaY));
	GraphArgs->SetStringField(TEXT("BoundsHeight"), TEXT("80"));
	GraphArgs->SetStringField(TEXT("TreeSpacing"), FString::SanitizeFloat(SegmentLength));
	TSharedPtr<FJsonObject> CreateResult = ExtractFirstObjectFromToolContent(ExecutePCGCreateForestRepro(GraphArgs));
	const FString GraphPath = CreateResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(CreateResult, TEXT("graph_asset_path")) : TEXT("");

	TSharedPtr<FJsonObject> PlaceArgs = MakeShared<FJsonObject>();
	PlaceArgs->SetStringField(TEXT("TreeBlueprintPath"), RoadBlueprintPath);
	PlaceArgs->SetStringField(TEXT("ForestName"), RoadNetworkName);
	PlaceArgs->SetStringField(TEXT("GraphPath"), GraphPath);
	PlaceArgs->SetStringField(TEXT("AllowCurrentLevel"), TEXT("true"));
	PlaceArgs->SetStringField(TEXT("Seed"), FString::FromInt(DruidsMCPBridge::GetOptionalIntField(Arguments, TEXT("Seed"), 2024)));
	PlaceArgs->SetStringField(TEXT("AreaX"), FString::SanitizeFloat(GraphAreaX));
	PlaceArgs->SetStringField(TEXT("AreaY"), FString::SanitizeFloat(GraphAreaY));
	PlaceArgs->SetStringField(TEXT("BoundsHeight"), TEXT("80"));
	PlaceArgs->SetStringField(TEXT("TreeSpacing"), FString::SanitizeFloat(SegmentLength));
	PlaceArgs->SetStringField(TEXT("SpacingMode"), TEXT("Explicit"));
	TSharedPtr<FJsonObject> PlaceResult = ExtractFirstObjectFromToolContent(ExecutePCGPlaceForestActor(PlaceArgs));

	AActor* RoadNetworkActor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(RoadNetworkName);
	if (RoadNetworkActor)
	{
		RoadNetworkActor->Tags.AddUnique(TEXT("DruidsPCGRoads"));
		RoadNetworkActor->Tags.AddUnique(FName(*FString::Printf(TEXT("DruidsPCGRoadSegmentCount_%d"), SegmentCount)));
		RoadNetworkActor->Tags.AddUnique(FName(*FString::Printf(TEXT("DruidsPCGRoadWidth_%d"), FMath::RoundToInt(RoadWidth))));
		RoadNetworkActor->Modify();
	}

	TSharedPtr<FJsonObject> GenerateArgs = MakeShared<FJsonObject>();
	GenerateArgs->SetStringField(TEXT("ForestActorNameOrLabel"), RoadNetworkName);
	TSharedPtr<FJsonObject> GenerateResult = ExtractFirstObjectFromToolContent(ExecutePCGGenerate(GenerateArgs));
	TSharedPtr<FJsonObject> InspectResult = ExtractFirstObjectFromToolContent(ExecutePCGInspectGenerated(GenerateArgs));

	TSharedPtr<FJsonObject> CaptureResult;
	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CaptureViewport"), true))
	{
		TSharedPtr<FJsonObject> CaptureArgs = MakeShared<FJsonObject>();
		CaptureArgs->SetStringField(TEXT("ActorNameOrLabel"), RoadNetworkName);
		CaptureArgs->SetStringField(TEXT("Focus"), TEXT("true"));
		CaptureArgs->SetStringField(TEXT("FilenamePrefix"), FString::Printf(TEXT("PCGRoadRecipe_%s"), *RoadNetworkName));
		CaptureResult = ExtractFirstObjectFromToolContent(ExecuteEditorCaptureViewport(CaptureArgs));
	}

	TSharedPtr<FJsonObject> RoadNetwork = MakeShared<FJsonObject>();
	RoadNetwork->SetStringField(TEXT("type"), TEXT("straight_segment_strip"));
	RoadNetwork->SetStringField(TEXT("orientation"), Axis == TEXT("Y") ? TEXT("y_axis") : TEXT("x_axis"));
	RoadNetwork->SetStringField(TEXT("axis"), Axis);
	RoadNetwork->SetNumberField(TEXT("segment_count"), SegmentCount);
	RoadNetwork->SetNumberField(TEXT("road_length"), RoadLength);
	RoadNetwork->SetNumberField(TEXT("segment_length"), SegmentLength);
	RoadNetwork->SetNumberField(TEXT("road_width"), RoadWidth);
	RoadNetwork->SetNumberField(TEXT("road_height"), RoadHeight);
	RoadNetwork->SetNumberField(TEXT("graph_area_x"), GraphAreaX);
	RoadNetwork->SetNumberField(TEXT("graph_area_y"), GraphAreaY);
	RoadNetwork->SetStringField(TEXT("generation_contract"), TEXT("PCG Create Points Grid with AreaY smaller than SegmentLength yields one lane of SegmentCount points, then native Spawn Actor emits road segment actors."));

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), PlaceResult.IsValid() && GenerateResult.IsValid() && DruidsMCPBridge::GetOptionalBoolField(GenerateResult, TEXT("generation_requested"), false));
	Result->SetStringField(TEXT("status"), TEXT("pcg_road_recipe_generation_requested"));
	Result->SetStringField(TEXT("road_blueprint_path"), RoadBlueprintPath);
	Result->SetStringField(TEXT("road_network_name"), RoadNetworkName);
	Result->SetStringField(TEXT("graph_path"), GraphPath);
	Result->SetBoolField(TEXT("generated"), GenerateResult.IsValid() && DruidsMCPBridge::GetOptionalBoolField(GenerateResult, TEXT("generation_requested"), false));
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("road_network"), RoadNetwork);
	if (RoadResult.IsValid())
	{
		Result->SetObjectField(TEXT("road_blueprint"), RoadResult);
	}
	if (CompileResult.IsValid())
	{
		Result->SetObjectField(TEXT("road_compile"), CompileResult);
	}
	Result->SetObjectField(TEXT("create_graph"), CreateResult.IsValid() ? CreateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("place_actor"), PlaceResult.IsValid() ? PlaceResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("generate"), GenerateResult.IsValid() ? GenerateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("inspect"), InspectResult.IsValid() ? InspectResult : MakeShared<FJsonObject>());
	if (CaptureResult.IsValid())
	{
		Result->SetObjectField(TEXT("viewport_capture"), CaptureResult);
		Result->SetStringField(TEXT("screenshot_path"), DruidsMCPBridge::GetOptionalStringField(CaptureResult, TEXT("screenshot_path")));
	}
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue({ TEXT("pcg_inspect_generated"), TEXT("editor_capture_viewport"), TEXT("pcg_get_road_capabilities") }));
	Result->SetStringField(TEXT("next_step"), TEXT("Wait briefly for async PCG generation, then call pcg_inspect_generated for RoadNetworkName and verify native_spawned_actor_count equals segment_count."));
	return MakeToolJsonObjectResult(Result, !Result->GetBoolField(TEXT("success")));
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGRunSplineRoadProof(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_run_spline_road_proof"), Arguments, TEXT("run_spline_road_proof")), true);
	}

	const UClass* SplineSamplerSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGSplineSamplerSettings"));
	const UClass* GetSplineDataSettingsClass = LoadObject<UClass>(nullptr, TEXT("/Script/PCG.PCGGetSplineDataSettings"));
	const bool bSplineSamplerClassDetected = SplineSamplerSettingsClass != nullptr || GetSplineDataSettingsClass != nullptr;

	const FString PackagePath = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/CityPCG"));
	FString ProofName = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("ProofName"), TEXT("Druids_SplineRoadProof")).TrimStartAndEnd();
	if (ProofName.IsEmpty())
	{
		ProofName = TEXT("Druids_SplineRoadProof");
	}

	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CleanupExisting"), true))
	{
		TSharedPtr<FJsonObject> CleanupArgs = MakeShared<FJsonObject>();
		CleanupArgs->SetStringField(TEXT("Filter"), ProofName);
		CleanupArgs->SetStringField(TEXT("DryRun"), TEXT("false"));
		ExecuteEditorCleanupReproActors(CleanupArgs);
	}

	const double InitialWidth = FMath::Max(400.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("Width"), 1600.0));
	const double Depth = FMath::Max(100.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("Depth"), 240.0));
	const double ResizedWidth = FMath::Max(400.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("ResizedWidth"), 2400.0));
	const double RoadWidth = FMath::Max(20.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("RoadWidth"), 180.0));
	const double SegmentLength = FMath::Max(50.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("SegmentLength"), 400.0));
	const int32 Seed = DruidsMCPBridge::GetOptionalIntField(Arguments, TEXT("Seed"), 4096);
	const FString SplineActorName = FString::Printf(TEXT("%s_SplineBounds"), *ProofName);

	TSharedPtr<FJsonObject> SplineCreateArgs = MakeShared<FJsonObject>();
	SplineCreateArgs->SetStringField(TEXT("ActorName"), SplineActorName);
	SplineCreateArgs->SetStringField(TEXT("AllowCurrentLevel"), TEXT("true"));
	SplineCreateArgs->SetStringField(TEXT("Width"), FString::SanitizeFloat(InitialWidth));
	SplineCreateArgs->SetStringField(TEXT("Depth"), FString::SanitizeFloat(Depth));
	SplineCreateArgs->SetStringField(TEXT("ClosedLoop"), TEXT("true"));
	TSharedPtr<FJsonObject> SplineCreateResult = ExtractFirstObjectFromToolContent(ExecutePCGCreateSplineBoundsActor(SplineCreateArgs));
	TSharedPtr<FJsonObject> SplineInitialInspectArgs = MakeShared<FJsonObject>();
	SplineInitialInspectArgs->SetStringField(TEXT("ActorNameOrLabel"), SplineActorName);
	TSharedPtr<FJsonObject> SplineInitialInspect = ExtractFirstObjectFromToolContent(ExecutePCGInspectSplineBoundsActor(SplineInitialInspectArgs));

	const int32 InitialSegmentCount = FMath::Max(1, FMath::RoundToInt(InitialWidth / SegmentLength));
	const FString InitialRoadNetworkName = FString::Printf(TEXT("%s_SplineRoad_A"), *ProofName);
	TSharedPtr<FJsonObject> InitialRoadArgs = MakeShared<FJsonObject>();
	InitialRoadArgs->SetStringField(TEXT("PackagePath"), PackagePath);
	InitialRoadArgs->SetStringField(TEXT("RoadName"), FString::Printf(TEXT("BP_%s_RoadSegment"), *ProofName));
	InitialRoadArgs->SetStringField(TEXT("RoadNetworkName"), InitialRoadNetworkName);
	InitialRoadArgs->SetStringField(TEXT("RoadLength"), FString::SanitizeFloat(InitialWidth));
	InitialRoadArgs->SetStringField(TEXT("RoadWidth"), FString::SanitizeFloat(RoadWidth));
	InitialRoadArgs->SetStringField(TEXT("SegmentLength"), FString::SanitizeFloat(SegmentLength));
	InitialRoadArgs->SetStringField(TEXT("SegmentCount"), FString::FromInt(InitialSegmentCount));
	InitialRoadArgs->SetStringField(TEXT("Seed"), FString::FromInt(Seed));
	InitialRoadArgs->SetStringField(TEXT("Axis"), TEXT("X"));
	InitialRoadArgs->SetStringField(TEXT("CaptureViewport"), TEXT("false"));
	InitialRoadArgs->SetStringField(TEXT("CleanupExisting"), TEXT("false"));
	TSharedPtr<FJsonObject> InitialRoadResult = ExtractFirstObjectFromToolContent(ExecutePCGRunRoadRecipe(InitialRoadArgs));

	TSharedPtr<FJsonObject> SplineResizeArgs = MakeShared<FJsonObject>();
	SplineResizeArgs->SetStringField(TEXT("ActorNameOrLabel"), SplineActorName);
	SplineResizeArgs->SetStringField(TEXT("Width"), FString::SanitizeFloat(ResizedWidth));
	SplineResizeArgs->SetStringField(TEXT("Depth"), FString::SanitizeFloat(Depth));
	SplineResizeArgs->SetStringField(TEXT("ClosedLoop"), TEXT("true"));
	TSharedPtr<FJsonObject> SplineResizeResult = ExtractFirstObjectFromToolContent(ExecutePCGSetSplineBoundsActor(SplineResizeArgs));
	TSharedPtr<FJsonObject> SplineResizedInspect = ExtractFirstObjectFromToolContent(ExecutePCGInspectSplineBoundsActor(SplineInitialInspectArgs));

	const int32 ResizedSegmentCount = FMath::Max(1, FMath::RoundToInt(ResizedWidth / SegmentLength));
	const FString ResizedRoadNetworkName = FString::Printf(TEXT("%s_SplineRoad_B"), *ProofName);
	TSharedPtr<FJsonObject> ResizedRoadArgs = MakeShared<FJsonObject>();
	ResizedRoadArgs->SetStringField(TEXT("PackagePath"), PackagePath);
	ResizedRoadArgs->SetStringField(TEXT("RoadName"), FString::Printf(TEXT("BP_%s_RoadSegment"), *ProofName));
	ResizedRoadArgs->SetStringField(TEXT("RoadNetworkName"), ResizedRoadNetworkName);
	ResizedRoadArgs->SetStringField(TEXT("RoadLength"), FString::SanitizeFloat(ResizedWidth));
	ResizedRoadArgs->SetStringField(TEXT("RoadWidth"), FString::SanitizeFloat(RoadWidth));
	ResizedRoadArgs->SetStringField(TEXT("SegmentLength"), FString::SanitizeFloat(SegmentLength));
	ResizedRoadArgs->SetStringField(TEXT("SegmentCount"), FString::FromInt(ResizedSegmentCount));
	ResizedRoadArgs->SetStringField(TEXT("Seed"), FString::FromInt(Seed + 1));
	ResizedRoadArgs->SetStringField(TEXT("Axis"), TEXT("X"));
	ResizedRoadArgs->SetStringField(TEXT("CaptureViewport"), TEXT("false"));
	ResizedRoadArgs->SetStringField(TEXT("CleanupExisting"), TEXT("false"));
	TSharedPtr<FJsonObject> ResizedRoadResult = ExtractFirstObjectFromToolContent(ExecutePCGRunRoadRecipe(ResizedRoadArgs));

	TSharedPtr<FJsonObject> CaptureResult;
	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CaptureViewport"), true))
	{
		TSharedPtr<FJsonObject> CaptureArgs = MakeShared<FJsonObject>();
		CaptureArgs->SetStringField(TEXT("ActorNameOrLabel"), ResizedRoadNetworkName);
		CaptureArgs->SetStringField(TEXT("Focus"), TEXT("true"));
		CaptureArgs->SetStringField(TEXT("FilenamePrefix"), FString::Printf(TEXT("PCGSplineRoadProof_%s"), *ProofName));
		CaptureResult = ExtractFirstObjectFromToolContent(ExecuteEditorCaptureViewport(CaptureArgs));
	}

	TSharedPtr<FJsonObject> Proof = MakeShared<FJsonObject>();
	Proof->SetStringField(TEXT("generation_mode"), TEXT("bounds_from_spline_rectangle_road_strip"));
	Proof->SetBoolField(TEXT("true_spline_sampling_supported"), false);
	Proof->SetBoolField(TEXT("spline_sampler_class_detected"), bSplineSamplerClassDetected);
	Proof->SetStringField(TEXT("unsupported_reason"), TEXT("This proof derives straight PCG road strips from the bounds of a visible spline rectangle. It does not sample arbitrary/curved spline points through PCG spline sampler nodes yet."));
	Proof->SetNumberField(TEXT("initial_width"), InitialWidth);
	Proof->SetNumberField(TEXT("resized_width"), ResizedWidth);
	Proof->SetNumberField(TEXT("depth"), Depth);
	Proof->SetNumberField(TEXT("segment_length"), SegmentLength);
	Proof->SetNumberField(TEXT("initial_segment_count"), InitialSegmentCount);
	Proof->SetNumberField(TEXT("resized_segment_count"), ResizedSegmentCount);
	Proof->SetBoolField(TEXT("resize_changed_generated_count"), InitialSegmentCount != ResizedSegmentCount);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"),
		SplineCreateResult.IsValid() && SplineResizeResult.IsValid() && InitialRoadResult.IsValid() && ResizedRoadResult.IsValid() &&
		DruidsMCPBridge::GetOptionalBoolField(InitialRoadResult, TEXT("generated"), false) &&
		DruidsMCPBridge::GetOptionalBoolField(ResizedRoadResult, TEXT("generated"), false) &&
		InitialSegmentCount != ResizedSegmentCount);
	Result->SetStringField(TEXT("status"), TEXT("pcg_spline_road_proof_completed"));
	Result->SetStringField(TEXT("proof_name"), ProofName);
	Result->SetStringField(TEXT("spline_bounds_actor"), SplineActorName);
	Result->SetStringField(TEXT("initial_road_network_name"), InitialRoadNetworkName);
	Result->SetStringField(TEXT("resized_road_network_name"), ResizedRoadNetworkName);
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("proof"), Proof);
	Result->SetObjectField(TEXT("spline_create"), SplineCreateResult.IsValid() ? SplineCreateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("spline_initial_inspect"), SplineInitialInspect.IsValid() ? SplineInitialInspect : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("spline_resize"), SplineResizeResult.IsValid() ? SplineResizeResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("spline_resized_inspect"), SplineResizedInspect.IsValid() ? SplineResizedInspect : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("initial_road"), InitialRoadResult.IsValid() ? InitialRoadResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("resized_road"), ResizedRoadResult.IsValid() ? ResizedRoadResult : MakeShared<FJsonObject>());
	if (CaptureResult.IsValid())
	{
		Result->SetObjectField(TEXT("viewport_capture"), CaptureResult);
		Result->SetStringField(TEXT("screenshot_path"), DruidsMCPBridge::GetOptionalStringField(CaptureResult, TEXT("screenshot_path")));
	}
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue({ TEXT("pcg_inspect_spline_bounds_actor"), TEXT("pcg_inspect_generated"), TEXT("editor_capture_viewport"), TEXT("pcg_get_road_capabilities") }));
	Result->SetStringField(TEXT("next_step"), TEXT("Use resized_road_network_name with pcg_inspect_generated to verify the adjusted spline rectangle changed the road strip output. Do not describe this as arbitrary curved spline-road sampling."));
	return MakeToolJsonObjectResult(Result, !Result->GetBoolField(TEXT("success")));
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGRunCityLayoutRecipe(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_run_city_layout_recipe"), Arguments, TEXT("run_city_layout_recipe")), true);
	}

	const FString PackagePath = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/CityPCG"));
	FString CityName = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("CityName"), TEXT("Druids_City")).TrimStartAndEnd();
	if (CityName.IsEmpty())
	{
		CityName = TEXT("Druids_City");
	}

	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CleanupExisting"), true))
	{
		TSharedPtr<FJsonObject> CleanupArgs = MakeShared<FJsonObject>();
		CleanupArgs->SetStringField(TEXT("Filter"), CityName);
		CleanupArgs->SetStringField(TEXT("DryRun"), TEXT("false"));
		ExecuteEditorCleanupReproActors(CleanupArgs);
	}

	const double AreaX = FMath::Max(400.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("AreaX"), 2200.0));
	const double AreaY = FMath::Max(400.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("AreaY"), 1600.0));
	const double RoadWidth = FMath::Max(40.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("RoadWidth"), 180.0));
	const double RoadSegmentLength = FMath::Max(50.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("RoadSegmentLength"), 400.0));
	const int32 RoadSegmentCount = FMath::Max(1, DruidsMCPBridge::GetOptionalIntField(Arguments, TEXT("RoadSegmentCount"), FMath::Max(1, FMath::RoundToInt(AreaX / RoadSegmentLength))));
	const int32 VerticalRoadSegmentCount = FMath::Max(1, DruidsMCPBridge::GetOptionalIntField(Arguments, TEXT("VerticalRoadSegmentCount"), FMath::Max(1, FMath::RoundToInt(AreaY / RoadSegmentLength))));
	const int32 BuildingTargetCount = FMath::Max(1, DruidsMCPBridge::GetOptionalIntField(Arguments, TEXT("BuildingTargetCount"), 18));
	const int32 Seed = DruidsMCPBridge::GetOptionalIntField(Arguments, TEXT("Seed"), 3031);
	const double RoadSetback = FMath::Max(0.0, DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("RoadSetback"), 120.0));

	const FString RoadNetworkName = FString::Printf(TEXT("%s_Roads_H"), *CityName);
	const FString VerticalRoadNetworkName = FString::Printf(TEXT("%s_Roads_V"), *CityName);
	const FString RoadName = FString::Printf(TEXT("BP_%s_RoadSegment_H"), *CityName);
	const FString VerticalRoadName = FString::Printf(TEXT("BP_%s_RoadSegment_V"), *CityName);
	TSharedPtr<FJsonObject> RoadArgs = MakeShared<FJsonObject>();
	RoadArgs->SetStringField(TEXT("PackagePath"), PackagePath);
	RoadArgs->SetStringField(TEXT("RoadName"), RoadName);
	RoadArgs->SetStringField(TEXT("RoadNetworkName"), RoadNetworkName);
	RoadArgs->SetStringField(TEXT("RoadLength"), FString::SanitizeFloat(AreaX));
	RoadArgs->SetStringField(TEXT("RoadWidth"), FString::SanitizeFloat(RoadWidth));
	RoadArgs->SetStringField(TEXT("SegmentLength"), FString::SanitizeFloat(RoadSegmentLength));
	RoadArgs->SetStringField(TEXT("SegmentCount"), FString::FromInt(RoadSegmentCount));
	RoadArgs->SetStringField(TEXT("Seed"), FString::FromInt(Seed));
	RoadArgs->SetStringField(TEXT("Axis"), TEXT("X"));
	RoadArgs->SetStringField(TEXT("CaptureViewport"), TEXT("false"));
	RoadArgs->SetStringField(TEXT("CleanupExisting"), TEXT("false"));
	TSharedPtr<FJsonObject> RoadResult = ExtractFirstObjectFromToolContent(ExecutePCGRunRoadRecipe(RoadArgs));

	TSharedPtr<FJsonObject> VerticalRoadArgs = MakeShared<FJsonObject>();
	VerticalRoadArgs->SetStringField(TEXT("PackagePath"), PackagePath);
	VerticalRoadArgs->SetStringField(TEXT("RoadName"), VerticalRoadName);
	VerticalRoadArgs->SetStringField(TEXT("RoadNetworkName"), VerticalRoadNetworkName);
	VerticalRoadArgs->SetStringField(TEXT("RoadLength"), FString::SanitizeFloat(AreaY));
	VerticalRoadArgs->SetStringField(TEXT("RoadWidth"), FString::SanitizeFloat(RoadWidth));
	VerticalRoadArgs->SetStringField(TEXT("SegmentLength"), FString::SanitizeFloat(RoadSegmentLength));
	VerticalRoadArgs->SetStringField(TEXT("SegmentCount"), FString::FromInt(VerticalRoadSegmentCount));
	VerticalRoadArgs->SetStringField(TEXT("Seed"), FString::FromInt(Seed + 3));
	VerticalRoadArgs->SetStringField(TEXT("Axis"), TEXT("Y"));
	VerticalRoadArgs->SetStringField(TEXT("CaptureViewport"), TEXT("false"));
	VerticalRoadArgs->SetStringField(TEXT("CleanupExisting"), TEXT("false"));
	TSharedPtr<FJsonObject> VerticalRoadResult = ExtractFirstObjectFromToolContent(ExecutePCGRunRoadRecipe(VerticalRoadArgs));

	const FString IntersectionName = FString::Printf(TEXT("BP_%s_Intersection"), *CityName);
	const FString IntersectionBlueprintPath = FString::Printf(TEXT("%s/%s"), *PackagePath, *IntersectionName);
	TSharedPtr<FJsonObject> IntersectionResult;
	FString IntersectionLoadError;
	if (!LoadBlueprint(IntersectionBlueprintPath, IntersectionLoadError))
	{
		TSharedPtr<FJsonObject> IntersectionArgs = MakeShared<FJsonObject>();
		IntersectionArgs->SetStringField(TEXT("PackagePath"), PackagePath);
		IntersectionArgs->SetStringField(TEXT("AssetName"), IntersectionName);
		IntersectionArgs->SetStringField(TEXT("Size"), FString::SanitizeFloat(RoadWidth));
		IntersectionArgs->SetStringField(TEXT("Height"), TEXT("24"));
		IntersectionResult = ExtractFirstObjectFromToolContent(ExecuteBlueprintCreateIntersectionActor(IntersectionArgs));
	}
	TSharedPtr<FJsonObject> IntersectionCompileArgs = MakeShared<FJsonObject>();
	IntersectionCompileArgs->SetStringField(TEXT("BlueprintNameOrPath"), IntersectionBlueprintPath);
	TSharedPtr<FJsonObject> IntersectionCompileResult = ExtractFirstObjectFromToolContent(ExecuteBlueprintCompile(IntersectionCompileArgs));

	const FString IntersectionNetworkName = FString::Printf(TEXT("%s_Intersection_00"), *CityName);
	TSharedPtr<FJsonObject> IntersectionGraphArgs = MakeShared<FJsonObject>();
	IntersectionGraphArgs->SetStringField(TEXT("TreeBlueprintPath"), IntersectionBlueprintPath);
	IntersectionGraphArgs->SetStringField(TEXT("PackagePath"), PackagePath);
	IntersectionGraphArgs->SetStringField(TEXT("ForestName"), FString::Printf(TEXT("%s_Graph"), *IntersectionNetworkName));
	IntersectionGraphArgs->SetStringField(TEXT("GraphMode"), TEXT("BoundsSpawnActor"));
	IntersectionGraphArgs->SetStringField(TEXT("BoundsShape"), TEXT("Rectangle"));
	IntersectionGraphArgs->SetStringField(TEXT("SpacingMode"), TEXT("Explicit"));
	IntersectionGraphArgs->SetStringField(TEXT("VariationMode"), TEXT("None"));
	IntersectionGraphArgs->SetStringField(TEXT("Seed"), FString::FromInt(Seed + 7));
	IntersectionGraphArgs->SetStringField(TEXT("TargetCount"), TEXT("1"));
	IntersectionGraphArgs->SetStringField(TEXT("AreaX"), FString::SanitizeFloat(RoadWidth));
	IntersectionGraphArgs->SetStringField(TEXT("AreaY"), FString::SanitizeFloat(RoadWidth));
	IntersectionGraphArgs->SetStringField(TEXT("BoundsHeight"), TEXT("80"));
	IntersectionGraphArgs->SetStringField(TEXT("TreeSpacing"), FString::SanitizeFloat(FMath::Max(RoadWidth, RoadSegmentLength)));
	TSharedPtr<FJsonObject> IntersectionCreateResult = ExtractFirstObjectFromToolContent(ExecutePCGCreateForestRepro(IntersectionGraphArgs));
	const FString IntersectionGraphPath = IntersectionCreateResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(IntersectionCreateResult, TEXT("graph_asset_path")) : TEXT("");

	TSharedPtr<FJsonObject> IntersectionPlaceArgs = MakeShared<FJsonObject>();
	IntersectionPlaceArgs->SetStringField(TEXT("TreeBlueprintPath"), IntersectionBlueprintPath);
	IntersectionPlaceArgs->SetStringField(TEXT("ForestName"), IntersectionNetworkName);
	IntersectionPlaceArgs->SetStringField(TEXT("GraphPath"), IntersectionGraphPath);
	IntersectionPlaceArgs->SetStringField(TEXT("AllowCurrentLevel"), TEXT("true"));
	IntersectionPlaceArgs->SetStringField(TEXT("Seed"), FString::FromInt(Seed + 7));
	IntersectionPlaceArgs->SetStringField(TEXT("AreaX"), FString::SanitizeFloat(RoadWidth));
	IntersectionPlaceArgs->SetStringField(TEXT("AreaY"), FString::SanitizeFloat(RoadWidth));
	IntersectionPlaceArgs->SetStringField(TEXT("BoundsHeight"), TEXT("80"));
	IntersectionPlaceArgs->SetStringField(TEXT("TreeSpacing"), FString::SanitizeFloat(FMath::Max(RoadWidth, RoadSegmentLength)));
	IntersectionPlaceArgs->SetStringField(TEXT("SpacingMode"), TEXT("Explicit"));
	TSharedPtr<FJsonObject> IntersectionPlaceResult = ExtractFirstObjectFromToolContent(ExecutePCGPlaceForestActor(IntersectionPlaceArgs));

	AActor* IntersectionActor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(IntersectionNetworkName);
	if (IntersectionActor)
	{
		IntersectionActor->Tags.AddUnique(TEXT("DruidsPCGCityIntersection"));
		IntersectionActor->Tags.AddUnique(FName(*FString::Printf(TEXT("DruidsPCGCity_%s"), *CityName)));
		IntersectionActor->Modify();
	}
	TSharedPtr<FJsonObject> IntersectionGenerateArgs = MakeShared<FJsonObject>();
	IntersectionGenerateArgs->SetStringField(TEXT("ForestActorNameOrLabel"), IntersectionNetworkName);
	TSharedPtr<FJsonObject> IntersectionGenerateResult = ExtractFirstObjectFromToolContent(ExecutePCGGenerate(IntersectionGenerateArgs));
	TSharedPtr<FJsonObject> IntersectionInspectResult = ExtractFirstObjectFromToolContent(ExecutePCGInspectGenerated(IntersectionGenerateArgs));

	const FString BuildingName = FString::Printf(TEXT("BP_%s_Skyscraper"), *CityName);
	const FString BuildingBlueprintPath = FString::Printf(TEXT("%s/%s"), *PackagePath, *BuildingName);
	TSharedPtr<FJsonObject> BuildingResult;
	FString LoadError;
	if (!LoadBlueprint(BuildingBlueprintPath, LoadError))
	{
		TSharedPtr<FJsonObject> BuildingArgs = MakeShared<FJsonObject>();
		BuildingArgs->SetStringField(TEXT("PackagePath"), PackagePath);
		BuildingArgs->SetStringField(TEXT("AssetName"), BuildingName);
		BuildingArgs->SetStringField(TEXT("Width"), TEXT("150"));
		BuildingArgs->SetStringField(TEXT("Depth"), TEXT("150"));
		BuildingArgs->SetStringField(TEXT("Height"), TEXT("700"));
		BuildingResult = ExtractFirstObjectFromToolContent(ExecuteBlueprintCreateSkyscraperActor(BuildingArgs));
	}

	TSharedPtr<FJsonObject> BuildingCompileArgs = MakeShared<FJsonObject>();
	BuildingCompileArgs->SetStringField(TEXT("BlueprintNameOrPath"), BuildingBlueprintPath);
	TSharedPtr<FJsonObject> BuildingCompileResult = ExtractFirstObjectFromToolContent(ExecuteBlueprintCompile(BuildingCompileArgs));

	const double LotAreaX = FMath::Max(200.0, ((AreaX - RoadWidth) * 0.5) - RoadSetback);
	const double LotAreaY = FMath::Max(200.0, ((AreaY - RoadWidth) * 0.5) - RoadSetback);
	const double LotCenterX = (RoadWidth * 0.5) + RoadSetback + (LotAreaX * 0.5);
	const double LotCenterY = (RoadWidth * 0.5) + RoadSetback + (LotAreaY * 0.5);
	const double RoadCorridorHalfWidth = RoadWidth * 0.5;
	const TCHAR* LotIds[] = { TEXT("NE"), TEXT("NW"), TEXT("SE"), TEXT("SW") };
	const double LotSignsX[] = { 1.0, -1.0, 1.0, -1.0 };
	const double LotSignsY[] = { 1.0, 1.0, -1.0, -1.0 };
	TArray<FString> BuildingNetworkNames;
	TArray<FString> BuildingGraphPaths;
	TArray<TSharedPtr<FJsonValue>> BuildingLots;
	TArray<TSharedPtr<FJsonValue>> BuildingLotSummaries;
	TArray<TSharedPtr<FJsonValue>> BuildingCreateResults;
	TArray<TSharedPtr<FJsonValue>> BuildingPlaceResults;
	TArray<TSharedPtr<FJsonValue>> BuildingGenerateResults;
	TArray<TSharedPtr<FJsonValue>> BuildingInspectResults;
	TSharedPtr<FJsonObject> BuildingCreateResult;
	TSharedPtr<FJsonObject> BuildingPlaceResult;
	TSharedPtr<FJsonObject> BuildingGenerateResult;
	TSharedPtr<FJsonObject> BuildingInspectResult;
	FString BuildingNetworkName;
	FString BuildingGraphPath;
	bool bAllBuildingLotsGenerated = true;
	int32 RequestedBuildingLotTotal = 0;
	const int32 BaseLotTarget = BuildingTargetCount / 4;
	const int32 RemainderLotTarget = BuildingTargetCount % 4;
	for (int32 LotIndex = 0; LotIndex < 4; ++LotIndex)
	{
		const FString LotId(LotIds[LotIndex]);
		const int32 LotTargetCount = FMath::Max(1, BaseLotTarget + (LotIndex < RemainderLotTarget ? 1 : 0));
		RequestedBuildingLotTotal += LotTargetCount;
		const double CenterX = LotSignsX[LotIndex] * LotCenterX;
		const double CenterY = LotSignsY[LotIndex] * LotCenterY;
		const FString LotNetworkName = FString::Printf(TEXT("%s_Buildings_%s"), *CityName, *LotId);

		TSharedPtr<FJsonObject> BuildingGraphArgs = MakeShared<FJsonObject>();
		BuildingGraphArgs->SetStringField(TEXT("TreeBlueprintPath"), BuildingBlueprintPath);
		BuildingGraphArgs->SetStringField(TEXT("PackagePath"), PackagePath);
		BuildingGraphArgs->SetStringField(TEXT("ForestName"), FString::Printf(TEXT("%s_Graph"), *LotNetworkName));
		BuildingGraphArgs->SetStringField(TEXT("GraphMode"), TEXT("BoundsSpawnActor"));
		BuildingGraphArgs->SetStringField(TEXT("BoundsShape"), TEXT("Rectangle"));
		BuildingGraphArgs->SetStringField(TEXT("SpacingMode"), TEXT("TargetCount"));
		BuildingGraphArgs->SetStringField(TEXT("VariationMode"), TEXT("TransformPoints"));
		BuildingGraphArgs->SetStringField(TEXT("Seed"), FString::FromInt(Seed + 17 + LotIndex));
		BuildingGraphArgs->SetStringField(TEXT("TargetCount"), FString::FromInt(LotTargetCount));
		BuildingGraphArgs->SetStringField(TEXT("AreaX"), FString::SanitizeFloat(LotAreaX));
		BuildingGraphArgs->SetStringField(TEXT("AreaY"), FString::SanitizeFloat(LotAreaY));
		BuildingGraphArgs->SetStringField(TEXT("BoundsHeight"), TEXT("900"));
		BuildingGraphArgs->SetStringField(TEXT("PositionJitter"), TEXT("60"));
		BuildingGraphArgs->SetStringField(TEXT("OffsetX"), FString::SanitizeFloat(CenterX + (LotSignsX[LotIndex] * LotAreaX * 0.25)));
		BuildingGraphArgs->SetStringField(TEXT("OffsetY"), FString::SanitizeFloat(CenterY + (LotSignsY[LotIndex] * LotAreaY * 0.30)));
		BuildingGraphArgs->SetStringField(TEXT("MinScale"), TEXT("0.75"));
		BuildingGraphArgs->SetStringField(TEXT("MaxScale"), TEXT("1.35"));
		BuildingGraphArgs->SetStringField(TEXT("YawJitter"), TEXT("8"));
		TSharedPtr<FJsonObject> LotCreateResult = ExtractFirstObjectFromToolContent(ExecutePCGCreateForestRepro(BuildingGraphArgs));
		const FString LotGraphPath = LotCreateResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(LotCreateResult, TEXT("graph_asset_path")) : TEXT("");

		TSharedPtr<FJsonObject> BuildingPlaceArgs = MakeShared<FJsonObject>();
		BuildingPlaceArgs->SetStringField(TEXT("TreeBlueprintPath"), BuildingBlueprintPath);
		BuildingPlaceArgs->SetStringField(TEXT("ForestName"), LotNetworkName);
		BuildingPlaceArgs->SetStringField(TEXT("GraphPath"), LotGraphPath);
		BuildingPlaceArgs->SetStringField(TEXT("AllowCurrentLevel"), TEXT("true"));
		BuildingPlaceArgs->SetStringField(TEXT("Seed"), FString::FromInt(Seed + 17 + LotIndex));
		BuildingPlaceArgs->SetStringField(TEXT("AreaX"), FString::SanitizeFloat(LotAreaX));
		BuildingPlaceArgs->SetStringField(TEXT("AreaY"), FString::SanitizeFloat(LotAreaY));
		BuildingPlaceArgs->SetStringField(TEXT("BoundsHeight"), TEXT("900"));
		BuildingPlaceArgs->SetStringField(TEXT("SpacingMode"), TEXT("TargetCount"));
		BuildingPlaceArgs->SetStringField(TEXT("TargetCount"), FString::FromInt(LotTargetCount));
		BuildingPlaceArgs->SetStringField(TEXT("LocationX"), FString::SanitizeFloat(CenterX));
		BuildingPlaceArgs->SetStringField(TEXT("LocationY"), FString::SanitizeFloat(CenterY));
		TSharedPtr<FJsonObject> LotPlaceResult = ExtractFirstObjectFromToolContent(ExecutePCGPlaceForestActor(BuildingPlaceArgs));

		AActor* BuildingActor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(LotNetworkName);
		if (BuildingActor)
		{
			BuildingActor->Tags.AddUnique(TEXT("DruidsPCGCityBuildings"));
			BuildingActor->Tags.AddUnique(TEXT("DruidsPCGCityBuildingLot"));
			BuildingActor->Tags.AddUnique(FName(*FString::Printf(TEXT("DruidsPCGCity_%s"), *CityName)));
			BuildingActor->Modify();
		}

		TSharedPtr<FJsonObject> BuildingGenerateArgs = MakeShared<FJsonObject>();
		BuildingGenerateArgs->SetStringField(TEXT("ForestActorNameOrLabel"), LotNetworkName);
		TSharedPtr<FJsonObject> LotGenerateResult = ExtractFirstObjectFromToolContent(ExecutePCGGenerate(BuildingGenerateArgs));
		TSharedPtr<FJsonObject> LotInspectResult = ExtractFirstObjectFromToolContent(ExecutePCGInspectGenerated(BuildingGenerateArgs));
		bAllBuildingLotsGenerated = bAllBuildingLotsGenerated && LotGenerateResult.IsValid() && DruidsMCPBridge::GetOptionalBoolField(LotGenerateResult, TEXT("generation_requested"), false);

		const double MinAbsX = FMath::Abs(CenterX) - (LotAreaX * 0.5);
		const double MinAbsY = FMath::Abs(CenterY) - (LotAreaY * 0.5);
		const double ClearanceX = MinAbsX - RoadCorridorHalfWidth;
		const double ClearanceY = MinAbsY - RoadCorridorHalfWidth;
		TSharedPtr<FJsonObject> Exclusion = MakeShared<FJsonObject>();
		Exclusion->SetStringField(TEXT("mode"), TEXT("quadrant_bounds_clearance"));
		Exclusion->SetNumberField(TEXT("road_corridor_half_width"), RoadCorridorHalfWidth);
		Exclusion->SetNumberField(TEXT("min_abs_x"), MinAbsX);
		Exclusion->SetNumberField(TEXT("min_abs_y"), MinAbsY);
		Exclusion->SetNumberField(TEXT("clearance_x"), ClearanceX);
		Exclusion->SetNumberField(TEXT("clearance_y"), ClearanceY);
		Exclusion->SetBoolField(TEXT("overlaps_road_corridor"), ClearanceX < -KINDA_SMALL_NUMBER || ClearanceY < -KINDA_SMALL_NUMBER);

		TSharedPtr<FJsonObject> Lot = MakeShared<FJsonObject>();
		Lot->SetStringField(TEXT("lot_id"), LotId);
		Lot->SetStringField(TEXT("network_name"), LotNetworkName);
		Lot->SetStringField(TEXT("graph_path"), LotGraphPath);
		Lot->SetObjectField(TEXT("center"), DruidsMCPBridge::MakeVectorObject(FVector(CenterX, CenterY, 0.0)));
		Lot->SetNumberField(TEXT("area_x"), LotAreaX);
		Lot->SetNumberField(TEXT("area_y"), LotAreaY);
		Lot->SetNumberField(TEXT("target_count"), LotTargetCount);
		Lot->SetObjectField(TEXT("road_exclusion"), Exclusion);
		Lot->SetObjectField(TEXT("create_graph"), LotCreateResult.IsValid() ? LotCreateResult : MakeShared<FJsonObject>());
		Lot->SetObjectField(TEXT("place_actor"), LotPlaceResult.IsValid() ? LotPlaceResult : MakeShared<FJsonObject>());
		Lot->SetObjectField(TEXT("generate"), LotGenerateResult.IsValid() ? LotGenerateResult : MakeShared<FJsonObject>());
		Lot->SetObjectField(TEXT("inspect"), LotInspectResult.IsValid() ? LotInspectResult : MakeShared<FJsonObject>());

		TSharedPtr<FJsonObject> LotSummary = MakeShared<FJsonObject>();
		LotSummary->SetStringField(TEXT("lot_id"), LotId);
		LotSummary->SetStringField(TEXT("network_name"), LotNetworkName);
		LotSummary->SetObjectField(TEXT("center"), DruidsMCPBridge::MakeVectorObject(FVector(CenterX, CenterY, 0.0)));
		LotSummary->SetNumberField(TEXT("area_x"), LotAreaX);
		LotSummary->SetNumberField(TEXT("area_y"), LotAreaY);
		LotSummary->SetNumberField(TEXT("target_count"), LotTargetCount);
		LotSummary->SetObjectField(TEXT("road_exclusion"), Exclusion);

		BuildingNetworkNames.Add(LotNetworkName);
		BuildingGraphPaths.Add(LotGraphPath);
		BuildingLots.Add(MakeShared<FJsonValueObject>(Lot));
		BuildingLotSummaries.Add(MakeShared<FJsonValueObject>(LotSummary));
		BuildingCreateResults.Add(MakeShared<FJsonValueObject>(LotCreateResult.IsValid() ? LotCreateResult : MakeShared<FJsonObject>()));
		BuildingPlaceResults.Add(MakeShared<FJsonValueObject>(LotPlaceResult.IsValid() ? LotPlaceResult : MakeShared<FJsonObject>()));
		BuildingGenerateResults.Add(MakeShared<FJsonValueObject>(LotGenerateResult.IsValid() ? LotGenerateResult : MakeShared<FJsonObject>()));
		BuildingInspectResults.Add(MakeShared<FJsonValueObject>(LotInspectResult.IsValid() ? LotInspectResult : MakeShared<FJsonObject>()));

		if (LotIndex == 0)
		{
			BuildingNetworkName = LotNetworkName;
			BuildingGraphPath = LotGraphPath;
			BuildingCreateResult = LotCreateResult;
			BuildingPlaceResult = LotPlaceResult;
			BuildingGenerateResult = LotGenerateResult;
			BuildingInspectResult = LotInspectResult;
		}
	}

	TSharedPtr<FJsonObject> CaptureResult;
	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CaptureViewport"), true))
	{
		TSharedPtr<FJsonObject> CaptureArgs = MakeShared<FJsonObject>();
		CaptureArgs->SetStringField(TEXT("ActorNameOrLabel"), BuildingNetworkName);
		CaptureArgs->SetStringField(TEXT("Focus"), TEXT("true"));
		CaptureArgs->SetStringField(TEXT("FilenamePrefix"), FString::Printf(TEXT("PCGCityLayout_%s"), *CityName));
		CaptureResult = ExtractFirstObjectFromToolContent(ExecuteEditorCaptureViewport(CaptureArgs));
	}

	TSharedPtr<FJsonObject> Layout = MakeShared<FJsonObject>();
	Layout->SetStringField(TEXT("type"), TEXT("orthogonal_crossroads_plus_quadrant_building_lots"));
	Layout->SetNumberField(TEXT("area_x"), AreaX);
	Layout->SetNumberField(TEXT("area_y"), AreaY);
	Layout->SetNumberField(TEXT("horizontal_road_segment_count"), RoadSegmentCount);
	Layout->SetNumberField(TEXT("vertical_road_segment_count"), VerticalRoadSegmentCount);
	Layout->SetNumberField(TEXT("intersection_count"), 1);
	Layout->SetNumberField(TEXT("road_segment_count"), RoadSegmentCount + VerticalRoadSegmentCount);
	Layout->SetNumberField(TEXT("building_target_count"), BuildingTargetCount);
	Layout->SetNumberField(TEXT("building_requested_count"), RequestedBuildingLotTotal);
	Layout->SetNumberField(TEXT("building_lot_count"), BuildingNetworkNames.Num());
	Layout->SetNumberField(TEXT("road_setback"), RoadSetback);
	Layout->SetNumberField(TEXT("lot_area_x"), LotAreaX);
	Layout->SetNumberField(TEXT("lot_area_y"), LotAreaY);
	Layout->SetBoolField(TEXT("intersections_supported"), true);
	Layout->SetBoolField(TEXT("building_lot_exclusion_supported"), true);
	Layout->SetStringField(TEXT("building_lot_exclusion_mode"), TEXT("quadrant_bounds_clearance"));
	Layout->SetStringField(TEXT("boundary_note"), TEXT("Slice 83 composes PCG horizontal roads, vertical roads, one PCG-owned intersection, and four PCG-owned building lot zones offset from the road corridors. This proves deterministic rectangular road exclusion; arbitrary polygon clipping remains a later slice."));

	TArray<TSharedPtr<FJsonValue>> Roads;
	TSharedPtr<FJsonObject> HorizontalRoad = MakeShared<FJsonObject>();
	HorizontalRoad->SetStringField(TEXT("road_id"), TEXT("road_h_0"));
	HorizontalRoad->SetStringField(TEXT("axis"), TEXT("X"));
	HorizontalRoad->SetStringField(TEXT("network_name"), RoadNetworkName);
	HorizontalRoad->SetNumberField(TEXT("segment_count"), RoadSegmentCount);
	HorizontalRoad->SetNumberField(TEXT("road_width"), RoadWidth);
	Roads.Add(MakeShared<FJsonValueObject>(HorizontalRoad));
	TSharedPtr<FJsonObject> VerticalRoad = MakeShared<FJsonObject>();
	VerticalRoad->SetStringField(TEXT("road_id"), TEXT("road_v_0"));
	VerticalRoad->SetStringField(TEXT("axis"), TEXT("Y"));
	VerticalRoad->SetStringField(TEXT("network_name"), VerticalRoadNetworkName);
	VerticalRoad->SetNumberField(TEXT("segment_count"), VerticalRoadSegmentCount);
	VerticalRoad->SetNumberField(TEXT("road_width"), RoadWidth);
	Roads.Add(MakeShared<FJsonValueObject>(VerticalRoad));
	Layout->SetArrayField(TEXT("roads"), Roads);

	TArray<TSharedPtr<FJsonValue>> Intersections;
	TSharedPtr<FJsonObject> IntersectionMetadata = MakeShared<FJsonObject>();
	IntersectionMetadata->SetStringField(TEXT("intersection_id"), TEXT("intersection_0_0"));
	IntersectionMetadata->SetStringField(TEXT("network_name"), IntersectionNetworkName);
	IntersectionMetadata->SetStringField(TEXT("horizontal_road_id"), TEXT("road_h_0"));
	IntersectionMetadata->SetStringField(TEXT("vertical_road_id"), TEXT("road_v_0"));
	IntersectionMetadata->SetObjectField(TEXT("location"), DruidsMCPBridge::MakeVectorObject(FVector::ZeroVector));
	IntersectionMetadata->SetNumberField(TEXT("road_width"), RoadWidth);
	Intersections.Add(MakeShared<FJsonValueObject>(IntersectionMetadata));
	Layout->SetArrayField(TEXT("intersections"), Intersections);
	Layout->SetArrayField(TEXT("building_lots"), BuildingLotSummaries);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"),
		RoadResult.IsValid() && VerticalRoadResult.IsValid() && IntersectionGenerateResult.IsValid() && bAllBuildingLotsGenerated &&
		DruidsMCPBridge::GetOptionalBoolField(RoadResult, TEXT("generated"), false) &&
		DruidsMCPBridge::GetOptionalBoolField(VerticalRoadResult, TEXT("generated"), false) &&
		DruidsMCPBridge::GetOptionalBoolField(IntersectionGenerateResult, TEXT("generation_requested"), false));
	Result->SetStringField(TEXT("status"), TEXT("pcg_city_layout_generation_requested"));
	Result->SetStringField(TEXT("city_name"), CityName);
	Result->SetStringField(TEXT("road_network_name"), RoadNetworkName);
	Result->SetStringField(TEXT("horizontal_road_network_name"), RoadNetworkName);
	Result->SetStringField(TEXT("vertical_road_network_name"), VerticalRoadNetworkName);
	Result->SetStringField(TEXT("intersection_network_name"), IntersectionNetworkName);
	Result->SetStringField(TEXT("building_network_name"), BuildingNetworkName);
	Result->SetArrayField(TEXT("building_network_names"), MakeStringArrayValue(BuildingNetworkNames));
	Result->SetStringField(TEXT("road_blueprint_path"), RoadResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(RoadResult, TEXT("road_blueprint_path")) : TEXT(""));
	Result->SetStringField(TEXT("vertical_road_blueprint_path"), VerticalRoadResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(VerticalRoadResult, TEXT("road_blueprint_path")) : TEXT(""));
	Result->SetStringField(TEXT("intersection_blueprint_path"), IntersectionBlueprintPath);
	Result->SetStringField(TEXT("building_blueprint_path"), BuildingBlueprintPath);
	Result->SetStringField(TEXT("road_graph_path"), RoadResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(RoadResult, TEXT("graph_path")) : TEXT(""));
	Result->SetStringField(TEXT("horizontal_road_graph_path"), RoadResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(RoadResult, TEXT("graph_path")) : TEXT(""));
	Result->SetStringField(TEXT("vertical_road_graph_path"), VerticalRoadResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(VerticalRoadResult, TEXT("graph_path")) : TEXT(""));
	Result->SetStringField(TEXT("intersection_graph_path"), IntersectionGraphPath);
	Result->SetStringField(TEXT("building_graph_path"), BuildingGraphPath);
	Result->SetArrayField(TEXT("building_graph_paths"), MakeStringArrayValue(BuildingGraphPaths));
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("layout"), Layout);
	Result->SetObjectField(TEXT("road_recipe"), RoadResult.IsValid() ? RoadResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("horizontal_road_recipe"), RoadResult.IsValid() ? RoadResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("vertical_road_recipe"), VerticalRoadResult.IsValid() ? VerticalRoadResult : MakeShared<FJsonObject>());
	if (IntersectionResult.IsValid())
	{
		Result->SetObjectField(TEXT("intersection_blueprint"), IntersectionResult);
	}
	Result->SetObjectField(TEXT("intersection_compile"), IntersectionCompileResult.IsValid() ? IntersectionCompileResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("intersection_create_graph"), IntersectionCreateResult.IsValid() ? IntersectionCreateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("intersection_place_actor"), IntersectionPlaceResult.IsValid() ? IntersectionPlaceResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("intersection_generate"), IntersectionGenerateResult.IsValid() ? IntersectionGenerateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("intersection_inspect"), IntersectionInspectResult.IsValid() ? IntersectionInspectResult : MakeShared<FJsonObject>());
	if (BuildingResult.IsValid())
	{
		Result->SetObjectField(TEXT("building_blueprint"), BuildingResult);
	}
	Result->SetObjectField(TEXT("building_compile"), BuildingCompileResult.IsValid() ? BuildingCompileResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("building_create_graph"), BuildingCreateResult.IsValid() ? BuildingCreateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("building_place_actor"), BuildingPlaceResult.IsValid() ? BuildingPlaceResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("building_generate"), BuildingGenerateResult.IsValid() ? BuildingGenerateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("building_inspect"), BuildingInspectResult.IsValid() ? BuildingInspectResult : MakeShared<FJsonObject>());
	Result->SetArrayField(TEXT("building_lots"), BuildingLots);
	Result->SetArrayField(TEXT("building_create_graphs"), BuildingCreateResults);
	Result->SetArrayField(TEXT("building_place_actors"), BuildingPlaceResults);
	Result->SetArrayField(TEXT("building_generates"), BuildingGenerateResults);
	Result->SetArrayField(TEXT("building_inspects"), BuildingInspectResults);
	if (CaptureResult.IsValid())
	{
		Result->SetObjectField(TEXT("viewport_capture"), CaptureResult);
		Result->SetStringField(TEXT("screenshot_path"), DruidsMCPBridge::GetOptionalStringField(CaptureResult, TEXT("screenshot_path")));
	}
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue({ TEXT("pcg_inspect_generated"), TEXT("editor_capture_viewport"), TEXT("pcg_get_road_capabilities") }));
	Result->SetStringField(TEXT("next_step"), TEXT("Wait briefly for async PCG generation, inspect road_network_name plus each building_network_names entry, then verify building lot road_exclusion.overlaps_road_corridor is false before capturing the full city view."));
	return MakeToolJsonObjectResult(Result, !Result->GetBoolField(TEXT("success")));
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGUpdateCityLayoutRecipe(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_update_city_layout_recipe"), Arguments, TEXT("update_city_layout_recipe")), true);
	}

	const FString PackagePath = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/CityPCG"));
	FString CityName = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("CityName"), TEXT("Druids_City")).TrimStartAndEnd();
	if (CityName.IsEmpty())
	{
		CityName = TEXT("Druids_City");
	}

	TSharedPtr<FJsonObject> BeforeArgs = MakeShared<FJsonObject>();
	BeforeArgs->SetStringField(TEXT("Filter"), CityName);
	BeforeArgs->SetStringField(TEXT("DryRun"), TEXT("true"));
	TSharedPtr<FJsonObject> Before = ExtractFirstObjectFromToolContent(ExecuteEditorCleanupReproActors(BeforeArgs));
	const int32 BeforeActorCount = Before.IsValid() ? static_cast<int32>(DruidsMCPBridge::GetOptionalNumberField(Before, TEXT("matched_count"), 0.0)) : 0;
	const bool bAllowCreateIfMissing = DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("AllowCreateIfMissing"), false);
	if (BeforeActorCount <= 0 && !bAllowCreateIfMissing)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_update_city_not_found"));
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("No Druids repro city actors matched '%s'. Pass AllowCreateIfMissing=true to create a fresh city instead."), *CityName));
		Result->SetStringField(TEXT("city_name"), CityName);
		Result->SetObjectField(TEXT("pcg_status"), Status);
		Result->SetObjectField(TEXT("before"), Before.IsValid() ? Before : MakeShared<FJsonObject>());
		return MakeToolJsonObjectResult(Result, true);
	}

	TSharedPtr<FJsonObject> RecipeArgs = MakeShared<FJsonObject>();
	RecipeArgs->SetStringField(TEXT("CityName"), CityName);
	RecipeArgs->SetStringField(TEXT("PackagePath"), PackagePath);
	RecipeArgs->SetStringField(TEXT("CleanupExisting"), TEXT("true"));
	RecipeArgs->SetStringField(TEXT("CaptureViewport"), DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CaptureViewport"), true) ? TEXT("true") : TEXT("false"));
	const TArray<FString> ForwardedFields = {
		TEXT("AreaX"),
		TEXT("AreaY"),
		TEXT("RoadWidth"),
		TEXT("RoadSegmentLength"),
		TEXT("RoadSegmentCount"),
		TEXT("VerticalRoadSegmentCount"),
		TEXT("BuildingTargetCount"),
		TEXT("RoadSetback"),
		TEXT("Seed")
	};
	for (const FString& Field : ForwardedFields)
	{
		FString StringValue;
		double NumberValue = 0.0;
		if (Arguments->TryGetStringField(Field, StringValue))
		{
			RecipeArgs->SetStringField(Field, StringValue);
		}
		else if (Arguments->TryGetNumberField(Field, NumberValue))
		{
			RecipeArgs->SetStringField(Field, FString::SanitizeFloat(NumberValue));
		}
	}

	TSharedPtr<FJsonObject> UpdatedCity = ExtractFirstObjectFromToolContent(ExecutePCGRunCityLayoutRecipe(RecipeArgs));
	TSharedPtr<FJsonObject> AfterArgs = MakeShared<FJsonObject>();
	AfterArgs->SetStringField(TEXT("Filter"), CityName);
	AfterArgs->SetStringField(TEXT("DryRun"), TEXT("true"));
	TSharedPtr<FJsonObject> After = ExtractFirstObjectFromToolContent(ExecuteEditorCleanupReproActors(AfterArgs));
	const int32 AfterActorCount = After.IsValid() ? static_cast<int32>(DruidsMCPBridge::GetOptionalNumberField(After, TEXT("matched_count"), 0.0)) : 0;

	TSharedPtr<FJsonObject> ChangeSummary = MakeShared<FJsonObject>();
	ChangeSummary->SetNumberField(TEXT("before_actor_count"), BeforeActorCount);
	ChangeSummary->SetNumberField(TEXT("after_actor_count"), AfterActorCount);
	ChangeSummary->SetBoolField(TEXT("existing_city_found"), BeforeActorCount > 0);
	ChangeSummary->SetBoolField(TEXT("created_if_missing"), BeforeActorCount <= 0 && bAllowCreateIfMissing);
	if (UpdatedCity.IsValid())
	{
		const TSharedPtr<FJsonObject>* Layout = nullptr;
		if (UpdatedCity->TryGetObjectField(TEXT("layout"), Layout) && Layout && Layout->IsValid())
		{
			ChangeSummary->SetNumberField(TEXT("updated_area_x"), DruidsMCPBridge::GetOptionalNumberField(*Layout, TEXT("area_x"), 0.0));
			ChangeSummary->SetNumberField(TEXT("updated_area_y"), DruidsMCPBridge::GetOptionalNumberField(*Layout, TEXT("area_y"), 0.0));
			ChangeSummary->SetNumberField(TEXT("updated_road_segment_count"), DruidsMCPBridge::GetOptionalNumberField(*Layout, TEXT("road_segment_count"), 0.0));
			ChangeSummary->SetNumberField(TEXT("updated_building_requested_count"), DruidsMCPBridge::GetOptionalNumberField(*Layout, TEXT("building_requested_count"), 0.0));
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	const bool bUpdated = UpdatedCity.IsValid() && DruidsMCPBridge::GetOptionalBoolField(UpdatedCity, TEXT("success"), false);
	Result->SetBoolField(TEXT("success"), bUpdated);
	Result->SetStringField(TEXT("status"), bUpdated ? TEXT("pcg_city_layout_update_completed") : TEXT("pcg_city_layout_update_failed"));
	Result->SetStringField(TEXT("city_name"), CityName);
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("before"), Before.IsValid() ? Before : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("after"), After.IsValid() ? After : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("change_summary"), ChangeSummary);
	Result->SetObjectField(TEXT("updated_city"), UpdatedCity.IsValid() ? UpdatedCity : MakeShared<FJsonObject>());
	if (UpdatedCity.IsValid())
	{
		Result->SetStringField(TEXT("screenshot_path"), DruidsMCPBridge::GetOptionalStringField(UpdatedCity, TEXT("screenshot_path")));
		Result->SetStringField(TEXT("road_network_name"), DruidsMCPBridge::GetOptionalStringField(UpdatedCity, TEXT("road_network_name")));
		Result->SetStringField(TEXT("vertical_road_network_name"), DruidsMCPBridge::GetOptionalStringField(UpdatedCity, TEXT("vertical_road_network_name")));
		Result->SetStringField(TEXT("intersection_network_name"), DruidsMCPBridge::GetOptionalStringField(UpdatedCity, TEXT("intersection_network_name")));
		const TArray<TSharedPtr<FJsonValue>>* BuildingNames = nullptr;
		if (UpdatedCity->TryGetArrayField(TEXT("building_network_names"), BuildingNames) && BuildingNames)
		{
			Result->SetArrayField(TEXT("building_network_names"), *BuildingNames);
		}
	}
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue({ TEXT("pcg_inspect_generated"), TEXT("editor_capture_viewport"), TEXT("pcg_get_road_capabilities") }));
	Result->SetStringField(TEXT("next_step"), TEXT("Inspect road_network_name and each building_network_names entry, then compare change_summary before/after counts and screenshot_path."));
	return MakeToolJsonObjectResult(Result, !bUpdated);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGUpdateForestRecipe(const TSharedPtr<FJsonObject>& Arguments)
{
	const TSharedPtr<FJsonObject> Status = DruidsMCPBridge::GetPCGAvailabilitySummary();
	if (!Status->GetBoolField(TEXT("pcg_available")))
	{
		return MakeToolJsonObjectResult(DruidsMCPBridge::MakePCGUnavailableResult(TEXT("pcg_update_forest_recipe"), Arguments, TEXT("update_forest_recipe")), true);
	}

	FString ForestName = GetOptionalStringParam(Arguments, TEXT("ForestActor"), GetOptionalStringParam(Arguments, TEXT("ForestName"), TEXT("Druids_Forest"))).TrimStartAndEnd();
	if (ForestName.IsEmpty())
	{
		ForestName = TEXT("Druids_Forest");
	}
	AActor* ForestActor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ForestName);
	if (!ForestActor)
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_update_forest_actor_not_found"));
		Result->SetStringField(TEXT("error"), FString::Printf(TEXT("Could not find forest actor '%s'."), *ForestName));
		Result->SetObjectField(TEXT("pcg_status"), Status);
		return MakeToolJsonObjectResult(Result, true);
	}
	const bool bIsReproActor = ForestActor->Tags.Contains(FName(TEXT("DruidsRepro"))) || ForestActor->Tags.Contains(FName(TEXT("DruidsMCPBridge"))) || ForestActor->Tags.Contains(FName(TEXT("DruidsPCGForest")));
	if (!bIsReproActor && !DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("AllowCurrentLevelMutation"), false))
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("success"), false);
		Result->SetStringField(TEXT("status"), TEXT("pcg_update_requires_repro_actor_or_opt_in"));
		Result->SetStringField(TEXT("error"), TEXT("pcg_update_forest_recipe only mutates Druids repro forest actors unless AllowCurrentLevelMutation=true."));
		Result->SetObjectField(TEXT("actor"), DruidsMCPBridge::MakeEditorActorSummary(ForestActor, false));
		return MakeToolJsonObjectResult(Result, true);
	}

	FString TreeBlueprintPath = GetOptionalStringParam(Arguments, TEXT("TreeBlueprintPath")).TrimStartAndEnd();
	if (TreeBlueprintPath.IsEmpty())
	{
		TreeBlueprintPath = FString::Printf(TEXT("%s/%s"), *DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("PackagePath"), TEXT("/Game/DruidsRepro/VisualPCG")), TEXT("BP_Druids_Tree"));
	}

	TSharedPtr<FJsonObject> EffectiveArgs = DruidsMCPBridge::MakePCGRecipeForestArguments(Arguments, TreeBlueprintPath, ForestActor->GetActorLabel(), FString::Printf(TEXT("%s_UpdateGraph_%s"), *ForestActor->GetActorLabel(), *FDateTime::Now().ToString(TEXT("%H%M%S"))));
	TSharedPtr<FJsonObject> SplineResult;
	const FString BoundsShape = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("BoundsShape"), TEXT("Rectangle"));
	const bool bSplineRectangle = BoundsShape.Equals(TEXT("SplineRectangle"), ESearchCase::IgnoreCase) || BoundsShape.Equals(TEXT("SplineRect"), ESearchCase::IgnoreCase);
	if (bSplineRectangle)
	{
		const FString SplineActorName = DruidsMCPBridge::GetOptionalStringField(Arguments, TEXT("SplineBoundsActor"), FString::Printf(TEXT("%s_SplineBounds"), *ForestActor->GetActorLabel()));
		AActor* SplineActor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(SplineActorName);
		USplineComponent* Spline = DruidsMCPBridge::FindSplineBoundsComponent(SplineActor);
		TSharedPtr<FJsonObject> SplineArgs = MakeShared<FJsonObject>();
		SplineArgs->SetStringField(TEXT("ActorNameOrLabel"), SplineActorName);
		SplineResult = ExtractFirstObjectFromToolContent(ExecutePCGInspectSplineBoundsActor(SplineArgs));
		if (!Spline)
		{
			TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
			Result->SetBoolField(TEXT("success"), false);
			Result->SetStringField(TEXT("status"), TEXT("pcg_spline_bounds_actor_not_found"));
			Result->SetStringField(TEXT("error"), FString::Printf(TEXT("BoundsShape=SplineRectangle requires a rectangular spline bounds actor; '%s' was not found."), *SplineActorName));
			Result->SetObjectField(TEXT("pcg_status"), Status);
			return MakeToolJsonObjectResult(Result, true);
		}
		const FVector2D SplineSize = DruidsMCPBridge::EstimateRectangleSplineSize(Spline, FVector2D(
			DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("AreaX"), 1600.0),
			DruidsMCPBridge::GetOptionalNumberField(Arguments, TEXT("AreaY"), 1600.0)));
		EffectiveArgs->SetStringField(TEXT("AreaX"), FString::SanitizeFloat(SplineSize.X));
		EffectiveArgs->SetStringField(TEXT("AreaY"), FString::SanitizeFloat(SplineSize.Y));
	}

	TSharedPtr<FJsonObject> CreateResult = ExtractFirstObjectFromToolContent(ExecutePCGCreateForestRepro(EffectiveArgs));
	const FString GraphPath = CreateResult.IsValid() ? DruidsMCPBridge::GetOptionalStringField(CreateResult, TEXT("graph_asset_path")) : TEXT("");
	TSharedPtr<FJsonObject> BoundsArgs = DruidsMCPBridge::MakePCGRecipeForestArguments(EffectiveArgs, TreeBlueprintPath, ForestActor->GetActorLabel(), ForestActor->GetActorLabel());
	BoundsArgs->SetStringField(TEXT("ForestActorNameOrLabel"), ForestActor->GetPathName());
	BoundsArgs->SetStringField(TEXT("GraphPath"), GraphPath);
	TSharedPtr<FJsonObject> BeforeInspectArgs = MakeShared<FJsonObject>();
	BeforeInspectArgs->SetStringField(TEXT("ForestActorNameOrLabel"), ForestActor->GetPathName());
	TSharedPtr<FJsonObject> BeforeInspect = ExtractFirstObjectFromToolContent(ExecutePCGInspectGenerated(BeforeInspectArgs));
	TSharedPtr<FJsonObject> BoundsResult = ExtractFirstObjectFromToolContent(ExecutePCGSetForestBounds(BoundsArgs));

	TSharedPtr<FJsonObject> GenerateResult;
	TSharedPtr<FJsonObject> InspectResult;
	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("Regenerate"), true))
	{
		GenerateResult = ExtractFirstObjectFromToolContent(ExecutePCGGenerate(BeforeInspectArgs));
		InspectResult = ExtractFirstObjectFromToolContent(ExecutePCGInspectGenerated(BeforeInspectArgs));
	}

	TSharedPtr<FJsonObject> CaptureResult;
	if (DruidsMCPBridge::GetOptionalBoolField(Arguments, TEXT("CaptureViewport"), true))
	{
		UEditorActorSubsystem* ActorSubsystem = DruidsMCPBridge::GetEditorActorSubsystem();
		if (ActorSubsystem && InspectResult.IsValid())
		{
			ActorSubsystem->SelectNothing();
			const TSharedPtr<FJsonObject>* ComponentObject = nullptr;
			if (InspectResult->TryGetObjectField(TEXT("pcg_component"), ComponentObject) && ComponentObject && ComponentObject->IsValid())
			{
				const TArray<TSharedPtr<FJsonValue>>* NativeActors = nullptr;
				if ((*ComponentObject)->TryGetArrayField(TEXT("native_spawned_actors"), NativeActors) && NativeActors)
				{
					for (const TSharedPtr<FJsonValue>& ActorValue : *NativeActors)
					{
						const TSharedPtr<FJsonObject> ActorObject = ActorValue.IsValid() ? ActorValue->AsObject() : nullptr;
						const FString ActorPath = ActorObject.IsValid() ? DruidsMCPBridge::GetOptionalStringField(ActorObject, TEXT("path")) : TEXT("");
						if (AActor* Actor = DruidsMCPBridge::FindEditorActorByNameLabelOrPath(ActorPath))
						{
							ActorSubsystem->SetActorSelectionState(Actor, true);
						}
					}
				}
			}
		}
		TSharedPtr<FJsonObject> CaptureArgs = MakeShared<FJsonObject>();
		CaptureArgs->SetStringField(TEXT("Focus"), TEXT("true"));
		CaptureArgs->SetStringField(TEXT("FilenamePrefix"), FString::Printf(TEXT("PCGForestUpdate_%s"), *ForestActor->GetActorLabel()));
		CaptureResult = ExtractFirstObjectFromToolContent(ExecuteEditorCaptureViewport(CaptureArgs));
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), BoundsResult.IsValid() && DruidsMCPBridge::GetOptionalStringField(BoundsResult, TEXT("status")).Equals(TEXT("pcg_forest_bounds_updated")));
	Result->SetStringField(TEXT("status"), Result->GetBoolField(TEXT("success")) ? TEXT("pcg_forest_recipe_updated") : TEXT("pcg_forest_recipe_update_failed"));
	Result->SetStringField(TEXT("forest_actor"), ForestActor->GetPathName());
	Result->SetStringField(TEXT("forest_name"), ForestActor->GetActorLabel());
	Result->SetStringField(TEXT("tree_blueprint_path"), TreeBlueprintPath);
	Result->SetStringField(TEXT("graph_path"), GraphPath);
	Result->SetStringField(TEXT("bounds_shape"), bSplineRectangle ? TEXT("spline_rectangle") : TEXT("rectangle"));
	Result->SetStringField(TEXT("generation_mode"), bSplineRectangle ? TEXT("bounds_from_spline_rectangle") : TEXT("rectangle_bounds_spawn_actor"));
	Result->SetBoolField(TEXT("true_multi_class_spawn_supported"), false);
	Result->SetObjectField(TEXT("pcg_status"), Status);
	Result->SetObjectField(TEXT("before_inspect"), BeforeInspect.IsValid() ? BeforeInspect : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("create_graph"), CreateResult.IsValid() ? CreateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("set_bounds"), BoundsResult.IsValid() ? BoundsResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("generate"), GenerateResult.IsValid() ? GenerateResult : MakeShared<FJsonObject>());
	Result->SetObjectField(TEXT("inspect"), InspectResult.IsValid() ? InspectResult : MakeShared<FJsonObject>());
	if (SplineResult.IsValid())
	{
		Result->SetObjectField(TEXT("spline_bounds_actor"), SplineResult);
	}
	if (CaptureResult.IsValid())
	{
		Result->SetObjectField(TEXT("viewport_capture"), CaptureResult);
		Result->SetStringField(TEXT("screenshot_path"), DruidsMCPBridge::GetOptionalStringField(CaptureResult, TEXT("screenshot_path")));
	}
	Result->SetStringField(TEXT("next_step"), TEXT("Use pcg_update_forest_recipe again for additional area/density/naturalness changes, or editor_capture_viewport to inspect the current view."));
	return MakeToolJsonObjectResult(Result, !Result->GetBoolField(TEXT("success")));
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecutePCGMultiClassProof(const TSharedPtr<FJsonObject>& Arguments)
{
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), true);
	Result->SetBoolField(TEXT("proved"), false);
	Result->SetBoolField(TEXT("true_multi_class_spawn_supported"), false);
	Result->SetStringField(TEXT("status"), TEXT("pcg_multiclass_spawn_unsupported"));
	Result->SetStringField(TEXT("forest_name"), GetOptionalStringParam(Arguments, TEXT("ForestName"), TEXT("Druids_Forest")));
	Result->SetStringField(TEXT("unsupported_reason"), TEXT("True multi-class PCG spawning is intentionally behind this proof gate. The shippable Slice 77 path uses one selected tree class plus transform/style variation; no arbitrary multi-class graph is generated yet."));
	Result->SetArrayField(TEXT("supported_variety_modes"), MakeStringArrayValue({ TEXT("TreeStyle"), TEXT("TransformPoints"), TEXT("PositionJitter"), TEXT("ScaleVariation"), TEXT("YawRandomization") }));
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue({ TEXT("pcg_run_forest_recipe"), TEXT("pcg_update_forest_recipe") }));
	Result->SetStringField(TEXT("next_step"), TEXT("Use pcg_run_forest_recipe or pcg_update_forest_recipe with TreeStyle and transform variation for current safe visual variety."));
	return MakeToolJsonObjectResult(Result);
}

namespace
{
	static bool IsAllowlistedMaterialExpressionClass(const FString& ClassName)
	{
		static const TSet<FString> Allowed = {
			TEXT("MaterialExpressionScalarParameter"),
			TEXT("MaterialExpressionVectorParameter"),
			TEXT("MaterialExpressionTextureSampleParameter2D"),
			TEXT("MaterialExpressionMultiply"),
			TEXT("MaterialExpressionAdd"),
			TEXT("MaterialExpressionLinearInterpolate"),
			TEXT("MaterialExpressionClamp"),
			TEXT("MaterialExpressionOneMinus"),
			TEXT("MaterialExpressionSaturate"),
			TEXT("MaterialExpressionTextureCoordinate"),
			TEXT("MaterialExpressionTime"),
			TEXT("MaterialExpressionPanner"),
			TEXT("MaterialExpressionComponentMask"),
			TEXT("MaterialExpressionAppendVector"),
			TEXT("MaterialExpressionPower"),
			TEXT("MaterialExpressionStaticSwitchParameter")
		};
		return Allowed.Contains(ClassName);
	}

	static FString NormalizeMaterialExpressionText(FString Text)
	{
		Text.TrimStartAndEndInline();
		Text.ToLowerInline();
		Text.ReplaceInline(TEXT("materialexpression"), TEXT(""));
		Text.ReplaceInline(TEXT("material expression"), TEXT(""));
		Text.ReplaceInline(TEXT("expression"), TEXT(""));
		Text.ReplaceInline(TEXT("node"), TEXT(""));
		Text.ReplaceInline(TEXT("_"), TEXT(""));
		Text.ReplaceInline(TEXT("-"), TEXT(""));
		Text.ReplaceInline(TEXT(" "), TEXT(""));
		return Text;
	}

	static void AddUniqueMaterialAlias(TArray<FString>& Aliases, const FString& Alias)
	{
		if (!Alias.IsEmpty() && !Aliases.ContainsByPredicate([&Alias](const FString& Existing) { return Existing.Equals(Alias, ESearchCase::IgnoreCase); }))
		{
			Aliases.Add(Alias);
		}
	}

	static TArray<FString> GetMaterialExpressionAliases(const FString& ClassName)
	{
		TArray<FString> Aliases;
		const FString ShortName = ClassName.StartsWith(TEXT("MaterialExpression")) ? ClassName.RightChop(18) : ClassName;
		AddUniqueMaterialAlias(Aliases, ShortName);

		if (ClassName == TEXT("MaterialExpressionLinearInterpolate"))
		{
			AddUniqueMaterialAlias(Aliases, TEXT("Lerp"));
			AddUniqueMaterialAlias(Aliases, TEXT("Linear Interpolate"));
		}
		else if (ClassName == TEXT("MaterialExpressionTextureCoordinate"))
		{
			AddUniqueMaterialAlias(Aliases, TEXT("Texture Coordinate"));
			AddUniqueMaterialAlias(Aliases, TEXT("TexCoord"));
			AddUniqueMaterialAlias(Aliases, TEXT("UV"));
		}
		else if (ClassName == TEXT("MaterialExpressionTextureSampleParameter2D"))
		{
			AddUniqueMaterialAlias(Aliases, TEXT("Texture Sample Parameter 2D"));
			AddUniqueMaterialAlias(Aliases, TEXT("Texture Parameter"));
		}
		else if (ClassName == TEXT("MaterialExpressionComponentMask"))
		{
			AddUniqueMaterialAlias(Aliases, TEXT("Component Mask"));
			AddUniqueMaterialAlias(Aliases, TEXT("Mask"));
		}
		else if (ClassName == TEXT("MaterialExpressionAppendVector"))
		{
			AddUniqueMaterialAlias(Aliases, TEXT("Append Vector"));
			AddUniqueMaterialAlias(Aliases, TEXT("Append"));
		}
		else if (ClassName == TEXT("MaterialExpressionStaticSwitchParameter"))
		{
			AddUniqueMaterialAlias(Aliases, TEXT("Static Switch Parameter"));
			AddUniqueMaterialAlias(Aliases, TEXT("Static Switch"));
		}
		else if (ClassName == TEXT("MaterialExpressionMaterialFunctionCall"))
		{
			AddUniqueMaterialAlias(Aliases, TEXT("Material Function Call"));
			AddUniqueMaterialAlias(Aliases, TEXT("Function Call"));
		}
		return Aliases;
	}

	static FString GetMaterialExpressionSupportReason(const FString& ClassName)
	{
		if (IsAllowlistedMaterialExpressionClass(ClassName))
		{
			return TEXT("Supported by material_add_expression allowlist and covered by material authoring smoke tests.");
		}
		return TEXT("Discoverable but not currently supported for material_add_expression mutation.");
	}

	static TSharedPtr<FJsonObject> MakeMaterialExpressionClassInfo(UClass* Class)
	{
		if (!Class)
		{
			return nullptr;
		}
		const FString ClassName = Class->GetName();
		TSharedPtr<FJsonObject> Info = MakeShared<FJsonObject>();
		Info->SetStringField(TEXT("class_name"), ClassName);
		Info->SetStringField(TEXT("canonical_class"), ClassName);
		Info->SetStringField(TEXT("class_path"), Class->GetPathName());
		Info->SetStringField(TEXT("display_name"), Class->GetDisplayNameText().ToString());
		Info->SetBoolField(TEXT("supported_for_authoring"), IsAllowlistedMaterialExpressionClass(ClassName));
		Info->SetStringField(TEXT("support_reason"), GetMaterialExpressionSupportReason(ClassName));
		Info->SetBoolField(TEXT("abstract"), Class->HasAnyClassFlags(CLASS_Abstract));
		Info->SetBoolField(TEXT("deprecated"), Class->HasAnyClassFlags(CLASS_Deprecated));
		TArray<TSharedPtr<FJsonValue>> AliasValues;
		for (const FString& Alias : GetMaterialExpressionAliases(ClassName))
		{
			AliasValues.Add(MakeShared<FJsonValueString>(Alias));
		}
		Info->SetArrayField(TEXT("aliases"), AliasValues);
		return Info;
	}

	static TArray<TSharedPtr<FJsonObject>> GetMaterialExpressionClassCatalog()
	{
		TArray<TSharedPtr<FJsonObject>> Catalog;
		TSet<FString> SeenClassNames;

		auto AddClass = [&Catalog, &SeenClassNames](UClass* Class)
		{
			if (!Class || !Class->IsChildOf(UMaterialExpression::StaticClass()))
			{
				return;
			}
			const FString ClassName = Class->GetName();
			if (SeenClassNames.Contains(ClassName) || Class->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
			{
				return;
			}
			if (TSharedPtr<FJsonObject> Info = MakeMaterialExpressionClassInfo(Class))
			{
				SeenClassNames.Add(ClassName);
				Catalog.Add(Info);
			}
		};

		const TArray<FString> KnownClassNames = {
			TEXT("MaterialExpressionScalarParameter"),
			TEXT("MaterialExpressionVectorParameter"),
			TEXT("MaterialExpressionTextureSampleParameter2D"),
			TEXT("MaterialExpressionMultiply"),
			TEXT("MaterialExpressionAdd"),
			TEXT("MaterialExpressionLinearInterpolate"),
			TEXT("MaterialExpressionClamp"),
			TEXT("MaterialExpressionOneMinus"),
			TEXT("MaterialExpressionSaturate"),
			TEXT("MaterialExpressionTextureCoordinate"),
			TEXT("MaterialExpressionTime"),
			TEXT("MaterialExpressionPanner"),
			TEXT("MaterialExpressionComponentMask"),
			TEXT("MaterialExpressionAppendVector"),
			TEXT("MaterialExpressionPower"),
			TEXT("MaterialExpressionStaticSwitchParameter"),
			TEXT("MaterialExpressionMaterialFunctionCall"),
			TEXT("MaterialExpressionNoise"),
			TEXT("MaterialExpressionFresnel")
		};
		for (const FString& KnownClassName : KnownClassNames)
		{
			AddClass(LoadObject<UClass>(nullptr, *FString::Printf(TEXT("/Script/Engine.%s"), *KnownClassName)));
		}

		for (TObjectIterator<UClass> It; It; ++It)
		{
			AddClass(*It);
		}

		Catalog.Sort([](const TSharedPtr<FJsonObject>& A, const TSharedPtr<FJsonObject>& B)
		{
			const bool bASupported = A.IsValid() && A->GetBoolField(TEXT("supported_for_authoring"));
			const bool bBSupported = B.IsValid() && B->GetBoolField(TEXT("supported_for_authoring"));
			if (bASupported != bBSupported)
			{
				return bASupported;
			}
			return A->GetStringField(TEXT("class_name")) < B->GetStringField(TEXT("class_name"));
		});
		return Catalog;
	}

	static int32 ScoreMaterialExpressionClassCandidate(const TSharedPtr<FJsonObject>& Candidate, const FString& Query, FString& OutReason)
	{
		if (!Candidate.IsValid())
		{
			return 0;
		}

		const FString NormalizedQuery = NormalizeMaterialExpressionText(Query);
		if (NormalizedQuery.IsEmpty())
		{
			OutReason = TEXT("empty_query");
			return 1;
		}

		const FString ClassName = Candidate->GetStringField(TEXT("class_name"));
		const FString DisplayName = Candidate->GetStringField(TEXT("display_name"));
		if (ClassName.Equals(Query, ESearchCase::IgnoreCase))
		{
			OutReason = TEXT("exact_canonical_class");
			return 100;
		}
		if (NormalizeMaterialExpressionText(ClassName).Equals(NormalizedQuery, ESearchCase::IgnoreCase))
		{
			OutReason = TEXT("normalized_canonical_class");
			return 98;
		}
		if (NormalizeMaterialExpressionText(DisplayName).Equals(NormalizedQuery, ESearchCase::IgnoreCase))
		{
			OutReason = TEXT("display_name_exact");
			return 92;
		}

		const TArray<TSharedPtr<FJsonValue>>* AliasValues = nullptr;
		if (Candidate->TryGetArrayField(TEXT("aliases"), AliasValues) && AliasValues)
		{
			for (const TSharedPtr<FJsonValue>& AliasValue : *AliasValues)
			{
				const FString Alias = AliasValue.IsValid() ? AliasValue->AsString() : FString();
				if (NormalizeMaterialExpressionText(Alias).Equals(NormalizedQuery, ESearchCase::IgnoreCase))
				{
					OutReason = TEXT("alias_exact");
					return 94;
				}
			}
		}

		if (NormalizeMaterialExpressionText(ClassName).Contains(NormalizedQuery) ||
			NormalizeMaterialExpressionText(DisplayName).Contains(NormalizedQuery))
		{
			OutReason = TEXT("substring_match");
			return 55;
		}
		if (AliasValues)
		{
			for (const TSharedPtr<FJsonValue>& AliasValue : *AliasValues)
			{
				const FString Alias = AliasValue.IsValid() ? AliasValue->AsString() : FString();
				if (NormalizeMaterialExpressionText(Alias).Contains(NormalizedQuery))
				{
					OutReason = TEXT("alias_substring_match");
					return 58;
				}
			}
		}

		OutReason = TEXT("no_match");
		return 0;
	}

	static FString JsonValueToImportText(const TSharedPtr<FJsonValue>& Value)
	{
		if (!Value.IsValid())
		{
			return FString();
		}

		switch (Value->Type)
		{
		case EJson::String:
			return Value->AsString();
		case EJson::Number:
			return FString::SanitizeFloat(Value->AsNumber());
		case EJson::Boolean:
			return Value->AsBool() ? TEXT("True") : TEXT("False");
		default:
			{
				FString Serialized;
				TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Serialized);
				FJsonSerializer::Serialize(Value, TEXT(""), Writer);
				return Serialized;
			}
		}
	}

	static FString ResolveMaterialExpressionPropertyAlias(UMaterialExpression* Expression, const FString& RequestedPropertyName, TArray<TSharedPtr<FJsonValue>>& OutAliasesApplied)
	{
		FString PropertyName = RequestedPropertyName.TrimStartAndEnd();
		if (!Expression || PropertyName.IsEmpty())
		{
			return PropertyName;
		}

		const UClass* ExpressionClass = Expression->GetClass();
		if (ExpressionClass && ExpressionClass->GetName() == TEXT("MaterialExpressionPower") && PropertyName.Equals(TEXT("Exponent"), ESearchCase::IgnoreCase))
		{
			OutAliasesApplied.Add(MakeShared<FJsonValueString>(TEXT("Exponent->ConstExponent")));
			return TEXT("ConstExponent");
		}

		return PropertyName;
	}

	static UMaterial* LoadMaterialForAuthoring(const FString& AssetPath)
	{
		UObject* Object = LoadObject<UObject>(nullptr, *FString::Printf(TEXT("%s.%s"), *AssetPath, *FPackageName::GetShortName(AssetPath)));
		if (!Object)
		{
			Object = LoadObject<UObject>(nullptr, *AssetPath);
		}
		return Cast<UMaterial>(Object);
	}

	static UMaterialExpression* FindMaterialExpressionByGuid(UMaterial* Material, const FString& NodeGuid)
	{
		if (!Material)
		{
			return nullptr;
		}
		const FString NormalizedGuid = NodeGuid.TrimStartAndEnd();
#if WITH_EDITORONLY_DATA
		for (UMaterialExpression* Expression : Material->GetExpressions())
		{
			if (!Expression)
			{
				continue;
			}
			if (Expression->MaterialExpressionGuid.ToString(EGuidFormats::DigitsWithHyphens).Equals(NormalizedGuid, ESearchCase::IgnoreCase) ||
				Expression->MaterialExpressionGuid.ToString(EGuidFormats::Digits).Equals(NormalizedGuid, ESearchCase::IgnoreCase) ||
				Expression->GetPathName().Equals(NormalizedGuid, ESearchCase::IgnoreCase))
			{
				return Expression;
			}
		}
#endif
		return nullptr;
	}

	static FExpressionInput* FindMaterialExpressionInputByExactName(UMaterialExpression* Expression, const FString& InputName, FString& OutAvailableInputs)
	{
		if (!Expression)
		{
			return nullptr;
		}

		TArray<FString> AvailableInputs;
		int32 InputIndex = 0;
		for (FExpressionInputIterator It(Expression); It; ++It, ++InputIndex)
		{
			const FString CandidateName = Expression->GetInputName(InputIndex).ToString();
			AvailableInputs.Add(CandidateName);
			if (CandidateName.Equals(InputName, ESearchCase::CaseSensitive))
			{
				return It.Input;
			}
		}
		OutAvailableInputs = FString::Join(AvailableInputs, TEXT(", "));
		return nullptr;
	}

	static bool TryResolveMaterialRootInput(UMaterial* Material, const FString& RootInputName, FExpressionInput*& OutInput, EMaterialProperty& OutProperty)
	{
		if (!Material)
		{
			return false;
		}

		const TMap<FString, EMaterialProperty> RootInputs = {
			{ TEXT("BaseColor"), MP_BaseColor },
			{ TEXT("Roughness"), MP_Roughness },
			{ TEXT("EmissiveColor"), MP_EmissiveColor },
			{ TEXT("Normal"), MP_Normal },
			{ TEXT("Metallic"), MP_Metallic },
			{ TEXT("Specular"), MP_Specular },
			{ TEXT("Opacity"), MP_Opacity }
		};
		const EMaterialProperty* Property = RootInputs.Find(RootInputName);
		if (!Property)
		{
			return false;
		}

		OutProperty = *Property;
		OutInput = Material->GetExpressionInputForProperty(OutProperty);
		return OutInput != nullptr;
	}

	static void MarkMaterialEdited(UMaterial* Material)
	{
		if (!Material)
		{
			return;
		}

		Material->PostEditChange();
		Material->MarkPackageDirty();
		if (UPackage* Package = Material->GetOutermost())
		{
			Package->MarkPackageDirty();
		}
	}
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteMaterialCreate(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"), GetOptionalStringParam(Arguments, TEXT("InPackagePath")));
	const FString AssetName = GetOptionalStringParam(Arguments, TEXT("AssetName"), GetOptionalStringParam(Arguments, TEXT("InAssetName")));
	if (PackagePath.IsEmpty() || AssetName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("material_create requires PackagePath and AssetName."), true);
	}

	const FString PackageName = PackagePath / AssetName;
	UPackage* Package = CreatePackage(*PackageName);
	UMaterial* Material = NewObject<UMaterial>(Package, UMaterial::StaticClass(), FName(*AssetName), RF_Public | RF_Standalone | RF_Transactional);
	if (!Material)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create Material: %s"), *PackageName), true);
	}

	Material->MaterialDomain = MD_Surface;
	Material->BlendMode = BLEND_Opaque;
	FAssetRegistryModule::AssetCreated(Material);
	MarkMaterialEdited(Material);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), PackageName);
	Result->SetStringField(TEXT("object_path"), Material->GetPathName());
	Result->SetObjectField(TEXT("material"), DruidsMCPBridge::ExportMaterialInspection(Material));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteMaterialFindExpressionClasses(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString Query = GetOptionalStringParam(Arguments, TEXT("Query")).TrimStartAndEnd();
	const bool bSupportedOnly = GetOptionalStringParam(Arguments, TEXT("SupportedOnly")).Equals(TEXT("true"), ESearchCase::IgnoreCase);
	const int32 Limit = FMath::Max(1, GetOptionalIntParam(Arguments, TEXT("Limit"), 50));

	TArray<TSharedPtr<FJsonObject>> Catalog = GetMaterialExpressionClassCatalog();
	TArray<TSharedPtr<FJsonObject>> Matches;
	for (const TSharedPtr<FJsonObject>& Candidate : Catalog)
	{
		if (!Candidate.IsValid())
		{
			continue;
		}
		if (bSupportedOnly && !Candidate->GetBoolField(TEXT("supported_for_authoring")))
		{
			continue;
		}
		FString MatchReason;
		const int32 Score = Query.IsEmpty() ? (Candidate->GetBoolField(TEXT("supported_for_authoring")) ? 75 : 25) : ScoreMaterialExpressionClassCandidate(Candidate, Query, MatchReason);
		if (!Query.IsEmpty() && Score <= 0)
		{
			continue;
		}
		TSharedPtr<FJsonObject> Match = CloneJsonObject(Candidate);
		Match->SetNumberField(TEXT("score"), Score);
		Match->SetStringField(TEXT("match_reason"), Query.IsEmpty() ? TEXT("listed") : MatchReason);
		Matches.Add(Match);
	}

	Matches.Sort([](const TSharedPtr<FJsonObject>& A, const TSharedPtr<FJsonObject>& B)
	{
		const int32 AScore = A.IsValid() ? static_cast<int32>(A->GetNumberField(TEXT("score"))) : 0;
		const int32 BScore = B.IsValid() ? static_cast<int32>(B->GetNumberField(TEXT("score"))) : 0;
		if (AScore != BScore)
		{
			return AScore > BScore;
		}
		const bool bASupported = A.IsValid() && A->GetBoolField(TEXT("supported_for_authoring"));
		const bool bBSupported = B.IsValid() && B->GetBoolField(TEXT("supported_for_authoring"));
		if (bASupported != bBSupported)
		{
			return bASupported;
		}
		return A->GetStringField(TEXT("class_name")) < B->GetStringField(TEXT("class_name"));
	});

	TArray<TSharedPtr<FJsonValue>> ResultValues;
	for (int32 Index = 0; Index < Matches.Num() && ResultValues.Num() < Limit; ++Index)
	{
		ResultValues.Add(MakeShared<FJsonValueObject>(Matches[Index]));
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("query"), Query);
	Result->SetBoolField(TEXT("supported_only"), bSupportedOnly);
	Result->SetNumberField(TEXT("total_catalog_count"), Catalog.Num());
	Result->SetNumberField(TEXT("result_count"), ResultValues.Num());
	Result->SetArrayField(TEXT("classes"), ResultValues);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteMaterialResolveExpressionClass(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString Query = GetOptionalStringParam(Arguments, TEXT("Query")).TrimStartAndEnd();
	if (Query.IsEmpty())
	{
		return MakeToolTextResult(TEXT("material_resolve_expression_class requires Query."), true);
	}
	const bool bRequireSupported = GetOptionalStringParam(Arguments, TEXT("RequireSupported")).Equals(TEXT("true"), ESearchCase::IgnoreCase);

	TArray<TSharedPtr<FJsonObject>> Ranked;
	for (const TSharedPtr<FJsonObject>& Candidate : GetMaterialExpressionClassCatalog())
	{
		FString MatchReason;
		const int32 Score = ScoreMaterialExpressionClassCandidate(Candidate, Query, MatchReason);
		if (Score <= 0)
		{
			continue;
		}
		if (bRequireSupported && Candidate.IsValid() && !Candidate->GetBoolField(TEXT("supported_for_authoring")))
		{
			continue;
		}
		TSharedPtr<FJsonObject> RankedCandidate = CloneJsonObject(Candidate);
		RankedCandidate->SetNumberField(TEXT("score"), Score);
		RankedCandidate->SetStringField(TEXT("match_reason"), MatchReason);
		Ranked.Add(RankedCandidate);
	}

	Ranked.Sort([](const TSharedPtr<FJsonObject>& A, const TSharedPtr<FJsonObject>& B)
	{
		const int32 AScore = A.IsValid() ? static_cast<int32>(A->GetNumberField(TEXT("score"))) : 0;
		const int32 BScore = B.IsValid() ? static_cast<int32>(B->GetNumberField(TEXT("score"))) : 0;
		if (AScore != BScore)
		{
			return AScore > BScore;
		}
		const bool bASupported = A.IsValid() && A->GetBoolField(TEXT("supported_for_authoring"));
		const bool bBSupported = B.IsValid() && B->GetBoolField(TEXT("supported_for_authoring"));
		if (bASupported != bBSupported)
		{
			return bASupported;
		}
		return A->GetStringField(TEXT("class_name")) < B->GetStringField(TEXT("class_name"));
	});

	TArray<TSharedPtr<FJsonValue>> CandidateValues;
	for (int32 Index = 0; Index < Ranked.Num() && CandidateValues.Num() < 10; ++Index)
	{
		CandidateValues.Add(MakeShared<FJsonValueObject>(Ranked[Index]));
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("query"), Query);
	Result->SetBoolField(TEXT("require_supported"), bRequireSupported);
	Result->SetNumberField(TEXT("candidate_count"), Ranked.Num());
	Result->SetArrayField(TEXT("candidates"), CandidateValues);

	TSharedPtr<FJsonObject> Best;
	FString Confidence = TEXT("none");
	FString RejectedReason;
	if (Ranked.Num() > 0)
	{
		const int32 BestScore = static_cast<int32>(Ranked[0]->GetNumberField(TEXT("score")));
		const bool bBestSupported = Ranked[0]->GetBoolField(TEXT("supported_for_authoring"));
		const bool bAmbiguous = Ranked.Num() > 1 && static_cast<int32>(Ranked[1]->GetNumberField(TEXT("score"))) == BestScore;
		if (BestScore >= 90 && !bAmbiguous)
		{
			Best = Ranked[0];
			Confidence = bBestSupported ? TEXT("high") : TEXT("high_unsupported");
		}
		else
		{
			RejectedReason = bAmbiguous ? TEXT("ambiguous_equal_score_candidates") : TEXT("no_high_confidence_candidate");
			Confidence = BestScore >= 55 ? TEXT("low") : TEXT("none");
		}
	}
	else
	{
		RejectedReason = TEXT("no_candidates");
	}

	Result->SetStringField(TEXT("confidence"), Confidence);
	if (Best.IsValid())
	{
		Result->SetObjectField(TEXT("best"), Best);
		Result->SetStringField(TEXT("canonical_class"), Best->GetStringField(TEXT("canonical_class")));
		Result->SetBoolField(TEXT("supported_for_authoring"), Best->GetBoolField(TEXT("supported_for_authoring")));
		Result->SetStringField(TEXT("next_step"), Best->GetBoolField(TEXT("supported_for_authoring"))
			? TEXT("Use canonical_class as ExpressionClass with material_add_expression.")
			: TEXT("This class is discoverable but not supported by material_add_expression yet."));
	}
	else
	{
		Result->SetStringField(TEXT("rejected_reason"), RejectedReason);
		Result->SetStringField(TEXT("next_step"), TEXT("Inspect candidates and use an exact canonical supported class, or expand the allowlist in a later slice."));
	}
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteMaterialAddExpression(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("MaterialNameOrPath")), TEXT("Material"), 10, AssetPath, Error);
	UMaterial* Material = LoadMaterialForAuthoring(AssetPath);
	if (!Material)
	{
		return MakeToolTextResult(Error.IsEmpty() ? TEXT("Could not load Material for material_add_expression.") : Error, true);
	}

	const FString ExpressionClassName = GetOptionalStringParam(Arguments, TEXT("ExpressionClass")).TrimStartAndEnd();
	if (!IsAllowlistedMaterialExpressionClass(ExpressionClassName))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Unsupported material expression class '%s'. Use an allowlisted canonical class name."), *ExpressionClassName), true);
	}

	UClass* ExpressionClass = LoadObject<UClass>(nullptr, *FString::Printf(TEXT("/Script/Engine.%s"), *ExpressionClassName));
	if (!ExpressionClass || !ExpressionClass->IsChildOf(UMaterialExpression::StaticClass()))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not load material expression class: %s"), *ExpressionClassName), true);
	}

	const FString NodeName = GetOptionalStringParam(Arguments, TEXT("NodeName"), ExpressionClassName);
	UMaterialExpression* Expression = NewObject<UMaterialExpression>(Material, ExpressionClass, FName(*NodeName), RF_Transactional);
	if (!Expression)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create material expression: %s"), *ExpressionClassName), true);
	}

	Expression->Material = Material;
	Expression->MaterialExpressionEditorX = GetOptionalIntParam(Arguments, TEXT("NodePosX"), 0);
	Expression->MaterialExpressionEditorY = GetOptionalIntParam(Arguments, TEXT("NodePosY"), 0);
	Expression->Desc = GetOptionalStringParam(Arguments, TEXT("Description"));

	const FString ParameterName = GetOptionalStringParam(Arguments, TEXT("ParameterName")).TrimStartAndEnd();
	if (!ParameterName.IsEmpty())
	{
		if (!Expression->HasAParameterName())
		{
			return MakeToolTextResult(FString::Printf(TEXT("Expression class '%s' does not support ParameterName."), *ExpressionClassName), true);
		}
		Expression->SetParameterName(FName(*ParameterName));
	}

	const FString DefaultValue = GetOptionalStringParam(Arguments, TEXT("DefaultValue")).TrimStartAndEnd();
	TArray<TSharedPtr<FJsonValue>> PropertyAliasesApplied;
	if (!DefaultValue.IsEmpty())
	{
		FString ImportError;
		const TCHAR* DefaultPropertyName = ExpressionClassName == TEXT("MaterialExpressionPower") ? TEXT("ConstExponent") : TEXT("DefaultValue");
		if (!DruidsMCPBridge::ImportPropertyValueByName(Expression, DefaultPropertyName, DefaultValue, ImportError))
		{
			return MakeToolTextResult(ImportError, true);
		}
		if (ExpressionClassName == TEXT("MaterialExpressionPower"))
		{
			PropertyAliasesApplied.Add(MakeShared<FJsonValueString>(TEXT("DefaultValue->ConstExponent")));
		}
	}

	const TSharedPtr<FJsonObject>* PropertyValuesObject = nullptr;
	if (Arguments->TryGetObjectField(TEXT("PropertyValues"), PropertyValuesObject) && PropertyValuesObject && PropertyValuesObject->IsValid())
	{
		for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : (*PropertyValuesObject)->Values)
		{
			FString PropertyName = Pair.Key.TrimStartAndEnd();
			if (PropertyName.IsEmpty())
			{
				return MakeToolTextResult(TEXT("PropertyValues contains an empty property name."), true);
			}
			PropertyName = ResolveMaterialExpressionPropertyAlias(Expression, PropertyName, PropertyAliasesApplied);

			const FString ImportText = JsonValueToImportText(Pair.Value);
			FString ImportError;
			if (!DruidsMCPBridge::ImportPropertyValueByName(Expression, *PropertyName, ImportText, ImportError))
			{
				return MakeToolTextResult(ImportError, true);
			}
		}
	}

	const FString TexturePath = GetOptionalStringParam(Arguments, TEXT("TexturePath")).TrimStartAndEnd();
	if (!TexturePath.IsEmpty())
	{
		UObject* Texture = LoadObject<UObject>(nullptr, *TexturePath);
		if (!Texture)
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not load texture: %s"), *TexturePath), true);
		}
		if (!DruidsMCPBridge::SetObjectPropertyValueByName(Expression, TEXT("Texture"), Texture))
		{
			return MakeToolTextResult(FString::Printf(TEXT("Expression class '%s' does not expose a Texture property."), *ExpressionClassName), true);
		}
	}

	Material->GetExpressionCollection().AddExpression(Expression);
	MarkMaterialEdited(Material);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), Material->GetOutermost() ? Material->GetOutermost()->GetName() : AssetPath);
	Result->SetObjectField(TEXT("expression"), DruidsMCPBridge::ExportMaterialExpression(Expression));
	Result->SetStringField(TEXT("node_guid"), DruidsMCPBridge::GetMaterialExpressionId(Expression));
	Result->SetStringField(TEXT("expression_class"), ExpressionClassName);
	Result->SetArrayField(TEXT("property_aliases_applied"), PropertyAliasesApplied);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteMaterialSetExpressionProperty(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("MaterialNameOrPath")), TEXT("Material"), 10, AssetPath, Error);
	UMaterial* Material = LoadMaterialForAuthoring(AssetPath);
	if (!Material)
	{
		return MakeToolTextResult(Error.IsEmpty() ? TEXT("Could not load Material for material_set_expression_property.") : Error, true);
	}

	const FString NodeGuid = GetOptionalStringParam(Arguments, TEXT("NodeGuid")).TrimStartAndEnd();
	if (NodeGuid.IsEmpty())
	{
		return MakeToolTextResult(TEXT("material_set_expression_property requires NodeGuid."), true);
	}

	UMaterialExpression* Expression = FindMaterialExpressionByGuid(Material, NodeGuid);
	if (!Expression)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not resolve material expression by NodeGuid '%s'."), *NodeGuid), true);
	}

	const FString RequestedPropertyName = GetOptionalStringParam(Arguments, TEXT("PropertyName")).TrimStartAndEnd();
	if (RequestedPropertyName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("material_set_expression_property requires PropertyName."), true);
	}

	const TSharedPtr<FJsonValue> ValueField = Arguments.IsValid() ? Arguments->TryGetField(TEXT("Value")) : nullptr;
	if (!ValueField.IsValid())
	{
		return MakeToolTextResult(TEXT("material_set_expression_property requires Value."), true);
	}

	TArray<TSharedPtr<FJsonValue>> PropertyAliasesApplied;
	const FString EffectivePropertyName = ResolveMaterialExpressionPropertyAlias(Expression, RequestedPropertyName, PropertyAliasesApplied);
	const FString ImportText = JsonValueToImportText(ValueField);
	FString ImportError;
	if (!DruidsMCPBridge::ImportPropertyValueByName(Expression, *EffectivePropertyName, ImportText, ImportError))
	{
		return MakeToolTextResult(ImportError, true);
	}

	MarkMaterialEdited(Material);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), Material->GetOutermost() ? Material->GetOutermost()->GetName() : AssetPath);
	Result->SetStringField(TEXT("node_guid"), DruidsMCPBridge::GetMaterialExpressionId(Expression));
	Result->SetStringField(TEXT("expression_class"), Expression->GetClass() ? Expression->GetClass()->GetName() : FString());
	Result->SetStringField(TEXT("property_name"), RequestedPropertyName);
	Result->SetStringField(TEXT("effective_property_name"), EffectivePropertyName);
	Result->SetStringField(TEXT("imported_value"), ImportText);
	Result->SetArrayField(TEXT("property_aliases_applied"), PropertyAliasesApplied);
	Result->SetObjectField(TEXT("expression"), DruidsMCPBridge::ExportMaterialExpression(Expression));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteMaterialConnectExpressions(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("MaterialNameOrPath")), TEXT("Material"), 10, AssetPath, Error);
	UMaterial* Material = LoadMaterialForAuthoring(AssetPath);
	if (!Material)
	{
		return MakeToolTextResult(Error.IsEmpty() ? TEXT("Could not load Material for material_connect_expressions.") : Error, true);
	}

	UMaterialExpression* Source = FindMaterialExpressionByGuid(Material, GetOptionalStringParam(Arguments, TEXT("SourceNodeGuid")));
	UMaterialExpression* Target = FindMaterialExpressionByGuid(Material, GetOptionalStringParam(Arguments, TEXT("TargetNodeGuid")));
	if (!Source || !Target)
	{
		return MakeToolTextResult(TEXT("Could not resolve source or target material expression by GUID."), true);
	}

	const FString TargetInputName = GetOptionalStringParam(Arguments, TEXT("TargetInputName"));
	FString AvailableInputs;
	FExpressionInput* TargetInput = FindMaterialExpressionInputByExactName(Target, TargetInputName, AvailableInputs);
	if (!TargetInput)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Target input '%s' was not found. Available inputs: %s"), *TargetInputName, *AvailableInputs), true);
	}

	const int32 SourceOutputIndex = GetOptionalIntParam(Arguments, TEXT("SourceOutputIndex"), 0);
	Source->ConnectExpression(TargetInput, SourceOutputIndex);
	MarkMaterialEdited(Material);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), Material->GetOutermost() ? Material->GetOutermost()->GetName() : AssetPath);
	Result->SetStringField(TEXT("source_node_guid"), DruidsMCPBridge::GetMaterialExpressionId(Source));
	Result->SetStringField(TEXT("target_node_guid"), DruidsMCPBridge::GetMaterialExpressionId(Target));
	Result->SetStringField(TEXT("target_input_name"), TargetInputName);
	Result->SetNumberField(TEXT("source_output_index"), SourceOutputIndex);
	Result->SetBoolField(TEXT("connected"), true);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteMaterialConnectRoot(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("MaterialNameOrPath")), TEXT("Material"), 10, AssetPath, Error);
	UMaterial* Material = LoadMaterialForAuthoring(AssetPath);
	if (!Material)
	{
		return MakeToolTextResult(Error.IsEmpty() ? TEXT("Could not load Material for material_connect_root.") : Error, true);
	}

	UMaterialExpression* Source = FindMaterialExpressionByGuid(Material, GetOptionalStringParam(Arguments, TEXT("SourceNodeGuid")));
	if (!Source)
	{
		return MakeToolTextResult(TEXT("Could not resolve source material expression by GUID."), true);
	}

	FExpressionInput* RootInput = nullptr;
	EMaterialProperty RootProperty = MP_MAX;
	const FString RootInputName = GetOptionalStringParam(Arguments, TEXT("RootInputName"));
	if (!TryResolveMaterialRootInput(Material, RootInputName, RootInput, RootProperty))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Unsupported or unavailable material root input '%s'."), *RootInputName), true);
	}

	const int32 SourceOutputIndex = GetOptionalIntParam(Arguments, TEXT("SourceOutputIndex"), 0);
	Source->ConnectExpression(RootInput, SourceOutputIndex);
	MarkMaterialEdited(Material);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), Material->GetOutermost() ? Material->GetOutermost()->GetName() : AssetPath);
	Result->SetStringField(TEXT("source_node_guid"), DruidsMCPBridge::GetMaterialExpressionId(Source));
	Result->SetStringField(TEXT("root_input_name"), RootInputName);
	Result->SetNumberField(TEXT("source_output_index"), SourceOutputIndex);
	Result->SetBoolField(TEXT("connected"), true);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteMaterialUpdate(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("MaterialNameOrPath")), TEXT("Material"), 10, AssetPath, Error);
	UMaterial* Material = LoadMaterialForAuthoring(AssetPath);
	if (!Material)
	{
		return MakeToolTextResult(Error.IsEmpty() ? TEXT("Could not load Material for material_update.") : Error, true);
	}

	MarkMaterialEdited(Material);
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), Material->GetOutermost() ? Material->GetOutermost()->GetName() : AssetPath);
	Result->SetObjectField(TEXT("material"), DruidsMCPBridge::ExportMaterialInspection(Material));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintAddVariable(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString VarName = GetOptionalStringParam(Arguments, TEXT("VarName"));
	const FString RequestedType = GetOptionalStringParam(Arguments, TEXT("Type"));
	if (VarName.IsEmpty() || RequestedType.TrimStartAndEnd().IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_add_variable requires VarName and Type."), true);
	}

	FEdGraphPinType PinType;
	FString CanonicalType;
	if (!DruidsMCPBridge::TryResolveVariablePinType(RequestedType, GetOptionalStringParam(Arguments, TEXT("ObjectClassPath")), PinType, CanonicalType, Error))
	{
		return MakeToolTextResult(Error, true);
	}

	const FName VarFName(*VarName);
	if (FBlueprintEditorUtils::FindNewVariableIndex(Blueprint, VarFName) == INDEX_NONE)
	{
		if (!FBlueprintEditorUtils::AddMemberVariable(Blueprint, VarFName, PinType, GetOptionalStringParam(Arguments, TEXT("DefaultValue"))))
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not add Blueprint variable '%s'."), *VarName), true);
		}
	}
	if (const FString Category = GetOptionalStringParam(Arguments, TEXT("Category")); !Category.IsEmpty())
	{
		FBlueprintEditorUtils::SetBlueprintVariableCategory(Blueprint, VarFName, nullptr, FText::FromString(Category));
	}

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("variable"), VarName);
	Result->SetStringField(TEXT("type"), CanonicalType);
	Result->SetStringField(TEXT("requested_type"), RequestedType);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintAddComponent(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString ComponentName = GetOptionalStringParam(Arguments, TEXT("ComponentName"));
	const FString ComponentClassPath = GetOptionalStringParam(Arguments, TEXT("ComponentClassPath"), GetOptionalStringParam(Arguments, TEXT("ComponentClassName")));
	if (ComponentName.IsEmpty() || ComponentClassPath.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_add_component requires ComponentName and ComponentClassPath."), true);
	}

	UClass* ComponentClass = FindObject<UClass>(nullptr, *ComponentClassPath);
	if (!ComponentClass)
	{
		ComponentClass = LoadObject<UClass>(nullptr, *ComponentClassPath);
	}
	if (!ComponentClass && !ComponentClassPath.StartsWith(TEXT("/Script/")))
	{
		ComponentClass = FindObject<UClass>(nullptr, *FString::Printf(TEXT("/Script/Engine.%s"), *ComponentClassPath));
	}
	if (!ComponentClass || !ComponentClass->IsChildOf(UActorComponent::StaticClass()))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not load component class: %s"), *ComponentClassPath), true);
	}

	const FString PrimitiveShape = GetOptionalStringParam(Arguments, TEXT("PrimitiveShape"));
	FString StaticMeshPath = GetOptionalStringParam(Arguments, TEXT("StaticMeshPath"));
	if (!PrimitiveShape.IsEmpty())
	{
		const FString PrimitivePath = DruidsMCPBridge::ResolvePrimitiveStaticMeshPath(PrimitiveShape);
		if (PrimitivePath.IsEmpty())
		{
			return MakeToolTextResult(FString::Printf(TEXT("Unsupported PrimitiveShape '%s'. Supported shapes: Cube, Sphere, Cylinder, Cone, Plane."), *PrimitiveShape), true);
		}
		if (!StaticMeshPath.IsEmpty() && !StaticMeshPath.Equals(PrimitivePath, ESearchCase::IgnoreCase))
		{
			return MakeToolTextResult(TEXT("Specify either PrimitiveShape or StaticMeshPath, not both."), true);
		}
		StaticMeshPath = PrimitivePath;
	}

	UStaticMesh* StaticMesh = nullptr;
	if (!StaticMeshPath.IsEmpty())
	{
		if (!ComponentClass->IsChildOf(UStaticMeshComponent::StaticClass()))
		{
			return MakeToolTextResult(TEXT("StaticMeshPath/PrimitiveShape can only be used with StaticMeshComponent or a subclass."), true);
		}
		StaticMesh = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UStaticMesh>(StaticMeshPath);
		if (!StaticMesh)
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not resolve StaticMesh asset: %s"), *StaticMeshPath), true);
		}
	}

	const FString MaterialPath = GetOptionalStringParam(Arguments, TEXT("MaterialPath"));
	UMaterialInterface* Material = nullptr;
	if (!MaterialPath.IsEmpty())
	{
		if (!ComponentClass->IsChildOf(UMeshComponent::StaticClass()))
		{
			return MakeToolTextResult(TEXT("MaterialPath can only be used with MeshComponent or a subclass."), true);
		}
		Material = DruidsMCPBridge::LoadAssetByPackageOrObjectPath<UMaterialInterface>(MaterialPath);
		if (!Material)
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not resolve Material asset: %s"), *MaterialPath), true);
		}
	}

	USimpleConstructionScript* SCS = Blueprint->SimpleConstructionScript;
	if (!SCS)
	{
		return MakeToolTextResult(TEXT("Blueprint has no SimpleConstructionScript."), true);
	}

	for (USCS_Node* Node : SCS->GetAllNodes())
	{
		if (Node && Node->GetVariableName().ToString().Equals(ComponentName, ESearchCase::IgnoreCase))
		{
			return MakeToolTextResult(FString::Printf(TEXT("Blueprint component already exists: %s"), *ComponentName), true);
		}
	}

	const FString ParentComponentName = GetOptionalStringParam(Arguments, TEXT("ParentComponentName"));
	USCS_Node* ParentNode = nullptr;
	if (!ParentComponentName.IsEmpty())
	{
		for (USCS_Node* Node : SCS->GetAllNodes())
		{
			if (Node && Node->GetVariableName().ToString().Equals(ParentComponentName, ESearchCase::IgnoreCase))
			{
				ParentNode = Node;
				break;
			}
		}
		if (!ParentNode)
		{
			return MakeToolTextResult(FString::Printf(TEXT("Parent component does not exist: %s"), *ParentComponentName), true);
		}
	}

	TSharedPtr<FJsonObject> Properties;
	TSharedPtr<FJsonObject> ImportedProperties = MakeShared<FJsonObject>();
	if (TryGetObjectParam(Arguments, TEXT("Properties"), Properties))
	{
		UActorComponent* ValidationComponent = NewObject<UActorComponent>(GetTransientPackage(), ComponentClass);
		if (!ValidationComponent)
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not create validation component for class: %s"), *ComponentClass->GetPathName()), true);
		}

		for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : Properties->Values)
		{
			FProperty* Property = ComponentClass->FindPropertyByName(FName(*Pair.Key));
			if (!Property)
			{
				return MakeToolTextResult(FString::Printf(TEXT("Component property does not exist on %s: %s"), *ComponentClass->GetPathName(), *Pair.Key), true);
			}
			if (!Property->HasAnyPropertyFlags(CPF_Edit) || Property->HasAnyPropertyFlags(CPF_EditConst | CPF_Deprecated))
			{
				return MakeToolTextResult(FString::Printf(TEXT("Component property is not editable on %s: %s"), *ComponentClass->GetPathName(), *Pair.Key), true);
			}

			const FString Value = DruidsMCPBridge::JsonValueToPropertyImportText(Pair.Value);
			if (!DruidsMCPBridge::TryImportPropertyText(ValidationComponent, Property, Value, Error))
			{
				return MakeToolTextResult(Error, true);
			}
			ImportedProperties->SetStringField(Pair.Key, Value);
		}
	}

	USCS_Node* NewNode = SCS->CreateNode(ComponentClass, FName(*ComponentName));
	if (ParentNode)
	{
		ParentNode->AddChildNode(NewNode);
	}
	else
	{
		SCS->AddNode(NewNode);
	}

	if (Properties.IsValid() && NewNode->ComponentTemplate)
	{
		for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : Properties->Values)
		{
			if (FProperty* Property = ComponentClass->FindPropertyByName(FName(*Pair.Key)))
			{
				const FString Value = DruidsMCPBridge::JsonValueToPropertyImportText(Pair.Value);
				if (!DruidsMCPBridge::TryImportPropertyText(NewNode->ComponentTemplate, Property, Value, Error))
				{
					SCS->RemoveNode(NewNode);
					return MakeToolTextResult(Error, true);
				}
			}
		}
	}

	FString AssignedStaticMeshPath;
	FString AssignedMaterialPath;
	if (NewNode->ComponentTemplate)
	{
		if (UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(NewNode->ComponentTemplate))
		{
			if (StaticMesh)
			{
				StaticMeshComponent->SetStaticMesh(StaticMesh);
				AssignedStaticMeshPath = StaticMesh->GetPathName();
			}
		}
		if (UMeshComponent* MeshComponent = Cast<UMeshComponent>(NewNode->ComponentTemplate))
		{
			if (Material)
			{
				MeshComponent->SetMaterial(0, Material);
				AssignedMaterialPath = Material->GetPathName();
			}
		}
	}

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("component"), ComponentName);
	Result->SetStringField(TEXT("component_class"), ComponentClass->GetPathName());
	Result->SetStringField(TEXT("parent_component"), ParentNode ? ParentNode->GetVariableName().ToString() : TEXT(""));
	Result->SetStringField(TEXT("static_mesh"), AssignedStaticMeshPath);
	Result->SetStringField(TEXT("material"), AssignedMaterialPath);
	Result->SetObjectField(TEXT("properties"), ImportedProperties);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintAddComponentMotion(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	UEdGraph* Graph = FindBlueprintGraph(Blueprint, GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph")), Error);
	if (!Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString SourceTitleOrGuid = GetOptionalStringParam(Arguments, TEXT("SourceNodeTitleOrGuid")).TrimStartAndEnd();
	const FString SourcePinName = GetOptionalStringParam(Arguments, TEXT("SourcePinName"), TEXT("then"));
	const FString ComponentName = GetOptionalStringParam(Arguments, TEXT("ComponentName")).TrimStartAndEnd();
	const FString Mode = GetOptionalStringParam(Arguments, TEXT("Mode")).TrimStartAndEnd().ToLower();
	if (SourceTitleOrGuid.IsEmpty() || ComponentName.IsEmpty() || Mode.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_add_component_motion requires SourceNodeTitleOrGuid, ComponentName, and Mode."), true);
	}

	auto MakeFailure = [&](const FString& Message, const TArray<FString>& Missing, const TArray<FString>& RolledBackGuids) -> TSharedPtr<FJsonObject>
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("completed_motion"), false);
		Result->SetBoolField(TEXT("graph_mutated"), false);
		Result->SetBoolField(TEXT("partial_success"), true);
		Result->SetStringField(TEXT("message"), Message);
		Result->SetArrayField(TEXT("missing"), MakeStringArrayValue(Missing));
		Result->SetArrayField(TEXT("rolled_back_node_guids"), MakeStringArrayValue(RolledBackGuids));
		return MakeToolJsonObjectResult(Result, true);
	};

	if (!DruidsMCPBridge::FindSCSNodeByName(Blueprint, ComponentName))
	{
		return MakeFailure(FString::Printf(TEXT("Component does not exist: %s"), *ComponentName), { TEXT("component") }, {});
	}

	UEdGraphNode* SourceNode = nullptr;
	for (UEdGraphNode* Node : Graph->Nodes)
	{
		if (!Node)
		{
			continue;
		}
		const FString Guid = Node->NodeGuid.ToString(EGuidFormats::Digits);
		const FString FullTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
		const FString MenuTitle = Node->GetNodeTitle(ENodeTitleType::MenuTitle).ToString();
		if (Guid.Equals(SourceTitleOrGuid, ESearchCase::IgnoreCase) ||
			FullTitle.Contains(SourceTitleOrGuid, ESearchCase::IgnoreCase) ||
			MenuTitle.Contains(SourceTitleOrGuid, ESearchCase::IgnoreCase))
		{
			SourceNode = Node;
			break;
		}
	}
	if (!SourceNode)
	{
		return MakeFailure(FString::Printf(TEXT("Source exec node was not found: %s"), *SourceTitleOrGuid), { TEXT("source_node") }, {});
	}

	FString MotionSpawnerId;
	FString MotionVectorPin;
	if (Mode == TEXT("set_relative_location"))
	{
		MotionSpawnerId = TEXT("func::/Script/Engine.SceneComponent:K2_SetRelativeLocation");
		MotionVectorPin = TEXT("NewLocation");
	}
	else if (Mode == TEXT("add_local_offset"))
	{
		MotionSpawnerId = TEXT("func::/Script/Engine.SceneComponent:K2_AddLocalOffset");
		MotionVectorPin = TEXT("DeltaLocation");
	}
	else if (Mode == TEXT("add_relative_location"))
	{
		MotionSpawnerId = TEXT("func::/Script/Engine.SceneComponent:K2_AddRelativeLocation");
		MotionVectorPin = TEXT("DeltaLocation");
	}
	else
	{
		return MakeFailure(FString::Printf(TEXT("Unsupported motion mode: %s"), *Mode), { TEXT("mode") }, {});
	}

	auto RemoveCreatedNodes = [&](const TArray<FString>& Guids)
	{
		for (const FString& Guid : Guids)
		{
			for (UEdGraphNode* Node : Graph->Nodes)
			{
				if (Node && Node->NodeGuid.ToString(EGuidFormats::Digits).Equals(Guid, ESearchCase::IgnoreCase))
				{
					Graph->RemoveNode(Node);
					break;
				}
			}
		}
		FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
	};

	const int32 BaseX = GetOptionalIntParam(Arguments, TEXT("NodePosX"), SourceNode->NodePosX + 260);
	const int32 BaseY = GetOptionalIntParam(Arguments, TEXT("NodePosY"), SourceNode->NodePosY + 220);
	TArray<FString> CreatedGuids;
	TArray<TSharedPtr<FJsonValue>> CreatedNodes;
	TArray<TSharedPtr<FJsonValue>> Connections;

	auto SetCreatedNodePosition = [&](const FString& Guid, int32 X, int32 Y)
	{
		for (UEdGraphNode* Node : Graph->Nodes)
		{
			if (Node && Node->NodeGuid.ToString(EGuidFormats::Digits).Equals(Guid, ESearchCase::IgnoreCase))
			{
				Node->NodePosX = X;
				Node->NodePosY = Y;
				break;
			}
		}
	};

	auto AddConnectionRecord = [&](const FString& FromGuid, const FString& FromPin, const FString& ToGuid, const FString& ToPin)
	{
		TSharedPtr<FJsonObject> Connection = MakeShared<FJsonObject>();
		Connection->SetStringField(TEXT("source_node_guid"), FromGuid);
		Connection->SetStringField(TEXT("source_pin"), FromPin);
		Connection->SetStringField(TEXT("target_node_guid"), ToGuid);
		Connection->SetStringField(TEXT("target_pin"), ToPin);
		Connections.Add(MakeShared<FJsonValueObject>(Connection));
	};

	auto AddNodeOrFail = [&](const FString& SpawnerId, const FString& NodeName, const TSharedPtr<FJsonObject>& PinDefaults, TSharedPtr<FJsonObject>& OutNodeResult) -> bool
	{
		OutNodeResult = AddNativeBlueprintNode(Blueprint, Graph, SpawnerId, NodeName, PinDefaults, Error);
		if (!OutNodeResult.IsValid())
		{
			return false;
		}
		const FString Guid = OutNodeResult->GetStringField(TEXT("node_guid"));
		CreatedGuids.Add(Guid);
		CreatedNodes.Add(MakeShared<FJsonValueObject>(OutNodeResult));
		return true;
	};

	TSharedPtr<FJsonObject> ComponentNode;
	if (!AddNodeOrFail(FString::Printf(TEXT("var_get::%s"), *ComponentName), TEXT("GetComponent"), nullptr, ComponentNode))
	{
		RemoveCreatedNodes(CreatedGuids);
		return MakeFailure(Error, { TEXT("component_reference_node") }, CreatedGuids);
	}
	SetCreatedNodePosition(ComponentNode->GetStringField(TEXT("node_guid")), BaseX, BaseY);

	TSharedPtr<FJsonObject> VectorDefaults = MakeShared<FJsonObject>();
	VectorDefaults->SetNumberField(TEXT("X"), 0.0);
	VectorDefaults->SetNumberField(TEXT("Y"), 0.0);
	VectorDefaults->SetNumberField(TEXT("Z"), 0.0);
	TSharedPtr<FJsonObject> RequestedVector;
	if (TryGetObjectParam(Arguments, TEXT("Vector"), RequestedVector))
	{
		for (const TCHAR* Axis : { TEXT("X"), TEXT("Y"), TEXT("Z") })
		{
			double Value = 0.0;
			if (RequestedVector->TryGetNumberField(Axis, Value))
			{
				VectorDefaults->SetNumberField(Axis, Value);
			}
			else
			{
				FString StringValue;
				if (RequestedVector->TryGetStringField(Axis, StringValue))
				{
					VectorDefaults->SetStringField(Axis, StringValue);
				}
			}
		}
	}

	TSharedPtr<FJsonObject> MakeVectorNode;
	if (!AddNodeOrFail(TEXT("func::/Script/Engine.KismetMathLibrary:MakeVector"), TEXT("MakeMotionVector"), VectorDefaults, MakeVectorNode))
	{
		RemoveCreatedNodes(CreatedGuids);
		return MakeFailure(Error, { TEXT("make_vector") }, CreatedGuids);
	}
	SetCreatedNodePosition(MakeVectorNode->GetStringField(TEXT("node_guid")), BaseX, BaseY + 110);

	TSharedPtr<FJsonObject> MotionNode;
	if (!AddNodeOrFail(MotionSpawnerId, TEXT("MoveComponent"), nullptr, MotionNode))
	{
		RemoveCreatedNodes(CreatedGuids);
		return MakeFailure(Error, { TEXT("motion_function") }, CreatedGuids);
	}
	SetCreatedNodePosition(MotionNode->GetStringField(TEXT("node_guid")), BaseX + 360, BaseY + 50);

	auto ConnectOrFail = [&](const FString& FromGuid, const FString& FromPin, const FString& ToGuid, const FString& ToPin, const FString& MissingName) -> bool
	{
		(void)MissingName;
		if (!ConnectNativeBlueprintPins(Blueprint, Graph, FromGuid, FromPin, ToGuid, ToPin, Error))
		{
			RemoveCreatedNodes(CreatedGuids);
			return false;
		}
		AddConnectionRecord(FromGuid, FromPin, ToGuid, ToPin);
		return true;
	};

	const FString SourceGuid = SourceNode->NodeGuid.ToString(EGuidFormats::Digits);
	const FString ComponentGuid = ComponentNode->GetStringField(TEXT("node_guid"));
	const FString VectorGuid = MakeVectorNode->GetStringField(TEXT("node_guid"));
	const FString MotionGuid = MotionNode->GetStringField(TEXT("node_guid"));
	if (!ConnectOrFail(SourceGuid, SourcePinName, MotionGuid, TEXT("execute"), TEXT("exec")))
	{
		return MakeFailure(Error, { TEXT("exec_connection") }, CreatedGuids);
	}
	if (!ConnectOrFail(ComponentGuid, ComponentName, MotionGuid, TEXT("self"), TEXT("component_connection")))
	{
		return MakeFailure(Error, { TEXT("component_connection") }, CreatedGuids);
	}
	if (!ConnectOrFail(VectorGuid, TEXT("ReturnValue"), MotionGuid, MotionVectorPin, TEXT("vector_connection")))
	{
		return MakeFailure(Error, { TEXT("vector_connection") }, CreatedGuids);
	}

	TSharedPtr<FJsonObject> VariableToAxis;
	if (TryGetObjectParam(Arguments, TEXT("VariableToAxis"), VariableToAxis))
	{
		for (const TCHAR* Axis : { TEXT("X"), TEXT("Y"), TEXT("Z") })
		{
			FString VariableName;
			if (!VariableToAxis->TryGetStringField(Axis, VariableName) || VariableName.TrimStartAndEnd().IsEmpty())
			{
				continue;
			}
			VariableName.TrimStartAndEndInline();
			if (!DruidsMCPBridge::HasBlueprintVariableOrComponent(Blueprint, VariableName))
			{
				RemoveCreatedNodes(CreatedGuids);
				return MakeFailure(FString::Printf(TEXT("Variable mapped to %s does not exist: %s"), Axis, *VariableName), { FString::Printf(TEXT("variable_%s"), Axis) }, CreatedGuids);
			}
			TSharedPtr<FJsonObject> VariableNode;
			if (!AddNodeOrFail(FString::Printf(TEXT("var_get::%s"), *VariableName), FString::Printf(TEXT("Get%s"), *VariableName), nullptr, VariableNode))
			{
				RemoveCreatedNodes(CreatedGuids);
				return MakeFailure(Error, { FString::Printf(TEXT("variable_get_%s"), Axis) }, CreatedGuids);
			}
			const FString VariableGuid = VariableNode->GetStringField(TEXT("node_guid"));
			SetCreatedNodePosition(VariableGuid, BaseX - 220, BaseY + 110);
			if (!ConnectOrFail(VariableGuid, VariableName, VectorGuid, Axis, FString::Printf(TEXT("variable_axis_%s"), Axis)))
			{
				return MakeFailure(Error, { FString::Printf(TEXT("variable_axis_%s"), Axis) }, CreatedGuids);
			}
		}
	}

	FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("completed_motion"), true);
	Result->SetBoolField(TEXT("graph_mutated"), true);
	Result->SetBoolField(TEXT("partial_success"), false);
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("graph_name"), Graph->GetName());
	Result->SetStringField(TEXT("component"), ComponentName);
	Result->SetStringField(TEXT("mode"), Mode);
	Result->SetStringField(TEXT("motion_node_guid"), MotionGuid);
	Result->SetArrayField(TEXT("created_node_guids"), MakeStringArrayValue(CreatedGuids));
	Result->SetArrayField(TEXT("created_nodes"), CreatedNodes);
	Result->SetArrayField(TEXT("connections"), Connections);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintAddDynamicMaterialParameterUpdate(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	UEdGraph* Graph = FindBlueprintGraph(Blueprint, GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph")), Error);
	if (!Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString SourceTitleOrGuid = GetOptionalStringParam(Arguments, TEXT("SourceNodeTitleOrGuid")).TrimStartAndEnd();
	const FString SourcePinName = GetOptionalStringParam(Arguments, TEXT("SourcePinName"), TEXT("then"));
	const FString ComponentName = GetOptionalStringParam(Arguments, TEXT("ComponentName")).TrimStartAndEnd();
	const FString ParameterName = GetOptionalStringParam(Arguments, TEXT("ParameterName")).TrimStartAndEnd();
	const FString ValueVariableName = GetOptionalStringParam(Arguments, TEXT("ValueVariableName")).TrimStartAndEnd();
	const FString LiteralValue = GetOptionalStringParam(Arguments, TEXT("Value")).TrimStartAndEnd();
	const int32 MaterialIndex = GetOptionalIntParam(Arguments, TEXT("MaterialIndex"), 0);
	if (SourceTitleOrGuid.IsEmpty() || ComponentName.IsEmpty() || ParameterName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_add_dynamic_material_parameter_update requires SourceNodeTitleOrGuid, ComponentName, and ParameterName."), true);
	}

	auto MakeFailure = [&](const FString& Message, const TArray<FString>& Missing, const TArray<FString>& RolledBackGuids) -> TSharedPtr<FJsonObject>
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetBoolField(TEXT("completed_dynamic_material_update"), false);
		Result->SetBoolField(TEXT("graph_mutated"), false);
		Result->SetBoolField(TEXT("partial_success"), true);
		Result->SetStringField(TEXT("message"), Message);
		Result->SetArrayField(TEXT("missing"), MakeStringArrayValue(Missing));
		Result->SetArrayField(TEXT("rolled_back_node_guids"), MakeStringArrayValue(RolledBackGuids));
		return MakeToolJsonObjectResult(Result, true);
	};

	USCS_Node* ComponentSCSNode = DruidsMCPBridge::FindSCSNodeByName(Blueprint, ComponentName);
	if (!ComponentSCSNode)
	{
		return MakeFailure(FString::Printf(TEXT("Component does not exist: %s"), *ComponentName), { TEXT("component") }, {});
	}
	if (!ComponentSCSNode->ComponentClass || !ComponentSCSNode->ComponentClass->IsChildOf(UPrimitiveComponent::StaticClass()))
	{
		return MakeFailure(FString::Printf(TEXT("Component is not a PrimitiveComponent: %s"), *ComponentName), { TEXT("primitive_component") }, {});
	}
	if (!ValueVariableName.IsEmpty() && !DruidsMCPBridge::HasBlueprintVariableOrComponent(Blueprint, ValueVariableName))
	{
		return MakeFailure(FString::Printf(TEXT("Value variable does not exist: %s"), *ValueVariableName), { TEXT("value_variable") }, {});
	}

	UEdGraphNode* SourceNode = nullptr;
	for (UEdGraphNode* Node : Graph->Nodes)
	{
		if (!Node)
		{
			continue;
		}
		const FString Guid = Node->NodeGuid.ToString(EGuidFormats::Digits);
		const FString FullTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
		const FString MenuTitle = Node->GetNodeTitle(ENodeTitleType::MenuTitle).ToString();
		if (Guid.Equals(SourceTitleOrGuid, ESearchCase::IgnoreCase) ||
			FullTitle.Contains(SourceTitleOrGuid, ESearchCase::IgnoreCase) ||
			MenuTitle.Contains(SourceTitleOrGuid, ESearchCase::IgnoreCase))
		{
			SourceNode = Node;
			break;
		}
	}
	if (!SourceNode)
	{
		return MakeFailure(FString::Printf(TEXT("Source exec node was not found: %s"), *SourceTitleOrGuid), { TEXT("source_node") }, {});
	}

	auto RemoveCreatedNodes = [&](const TArray<FString>& Guids)
	{
		for (const FString& Guid : Guids)
		{
			for (UEdGraphNode* Node : Graph->Nodes)
			{
				if (Node && Node->NodeGuid.ToString(EGuidFormats::Digits).Equals(Guid, ESearchCase::IgnoreCase))
				{
					Graph->RemoveNode(Node);
					break;
				}
			}
		}
		FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
	};

	const int32 BaseX = GetOptionalIntParam(Arguments, TEXT("NodePosX"), SourceNode->NodePosX + 280);
	const int32 BaseY = GetOptionalIntParam(Arguments, TEXT("NodePosY"), SourceNode->NodePosY + 360);
	TArray<FString> CreatedGuids;
	TArray<TSharedPtr<FJsonValue>> CreatedNodes;
	TArray<TSharedPtr<FJsonValue>> Connections;

	auto SetCreatedNodePosition = [&](const FString& Guid, int32 X, int32 Y)
	{
		for (UEdGraphNode* Node : Graph->Nodes)
		{
			if (Node && Node->NodeGuid.ToString(EGuidFormats::Digits).Equals(Guid, ESearchCase::IgnoreCase))
			{
				Node->NodePosX = X;
				Node->NodePosY = Y;
				break;
			}
		}
	};

	auto AddConnectionRecord = [&](const FString& FromGuid, const FString& FromPin, const FString& ToGuid, const FString& ToPin)
	{
		TSharedPtr<FJsonObject> Connection = MakeShared<FJsonObject>();
		Connection->SetStringField(TEXT("source_node_guid"), FromGuid);
		Connection->SetStringField(TEXT("source_pin"), FromPin);
		Connection->SetStringField(TEXT("target_node_guid"), ToGuid);
		Connection->SetStringField(TEXT("target_pin"), ToPin);
		Connections.Add(MakeShared<FJsonValueObject>(Connection));
	};

	auto AddNodeOrFail = [&](const FString& SpawnerId, const FString& NodeName, const TSharedPtr<FJsonObject>& PinDefaults, TSharedPtr<FJsonObject>& OutNodeResult) -> bool
	{
		OutNodeResult = AddNativeBlueprintNode(Blueprint, Graph, SpawnerId, NodeName, PinDefaults, Error);
		if (!OutNodeResult.IsValid())
		{
			return false;
		}
		const FString Guid = OutNodeResult->GetStringField(TEXT("node_guid"));
		CreatedGuids.Add(Guid);
		CreatedNodes.Add(MakeShared<FJsonValueObject>(OutNodeResult));
		return true;
	};

	auto ConnectOrFail = [&](const FString& FromGuid, const FString& FromPin, const FString& ToGuid, const FString& ToPin) -> bool
	{
		if (!ConnectNativeBlueprintPins(Blueprint, Graph, FromGuid, FromPin, ToGuid, ToPin, Error))
		{
			RemoveCreatedNodes(CreatedGuids);
			return false;
		}
		AddConnectionRecord(FromGuid, FromPin, ToGuid, ToPin);
		return true;
	};

	TSharedPtr<FJsonObject> ComponentNode;
	if (!AddNodeOrFail(FString::Printf(TEXT("var_get::%s"), *ComponentName), TEXT("GetMaterialComponent"), nullptr, ComponentNode))
	{
		RemoveCreatedNodes(CreatedGuids);
		return MakeFailure(Error, { TEXT("component_reference_node") }, CreatedGuids);
	}
	SetCreatedNodePosition(ComponentNode->GetStringField(TEXT("node_guid")), BaseX, BaseY + 90);

	TSharedPtr<FJsonObject> CreateMIDDefaults = MakeShared<FJsonObject>();
	CreateMIDDefaults->SetNumberField(TEXT("ElementIndex"), MaterialIndex);
	TSharedPtr<FJsonObject> CreateMIDNode;
	if (!AddNodeOrFail(TEXT("func::/Script/Engine.PrimitiveComponent:CreateDynamicMaterialInstance"), TEXT("CreateDynamicMaterialInstance"), CreateMIDDefaults, CreateMIDNode))
	{
		RemoveCreatedNodes(CreatedGuids);
		return MakeFailure(Error, { TEXT("create_dynamic_material_instance") }, CreatedGuids);
	}
	SetCreatedNodePosition(CreateMIDNode->GetStringField(TEXT("node_guid")), BaseX + 260, BaseY);

	TSharedPtr<FJsonObject> SetParameterDefaults = MakeShared<FJsonObject>();
	SetParameterDefaults->SetStringField(TEXT("ParameterName"), ParameterName);
	if (ValueVariableName.IsEmpty() && !LiteralValue.IsEmpty())
	{
		SetParameterDefaults->SetStringField(TEXT("Value"), LiteralValue);
	}
	TSharedPtr<FJsonObject> SetScalarNode;
	if (!AddNodeOrFail(TEXT("func::/Script/Engine.MaterialInstanceDynamic:SetScalarParameterValue"), TEXT("SetScalarParameterValue"), SetParameterDefaults, SetScalarNode))
	{
		RemoveCreatedNodes(CreatedGuids);
		return MakeFailure(Error, { TEXT("set_scalar_parameter_value") }, CreatedGuids);
	}
	SetCreatedNodePosition(SetScalarNode->GetStringField(TEXT("node_guid")), BaseX + 620, BaseY);

	const FString SourceGuid = SourceNode->NodeGuid.ToString(EGuidFormats::Digits);
	const FString ComponentGuid = ComponentNode->GetStringField(TEXT("node_guid"));
	const FString CreateMIDGuid = CreateMIDNode->GetStringField(TEXT("node_guid"));
	const FString SetScalarGuid = SetScalarNode->GetStringField(TEXT("node_guid"));
	if (!ConnectOrFail(SourceGuid, SourcePinName, CreateMIDGuid, TEXT("execute")))
	{
		return MakeFailure(Error, { TEXT("exec_to_create_mid") }, CreatedGuids);
	}
	if (!ConnectOrFail(ComponentGuid, ComponentName, CreateMIDGuid, TEXT("self")))
	{
		return MakeFailure(Error, { TEXT("component_to_create_mid") }, CreatedGuids);
	}
	if (!ConnectOrFail(CreateMIDGuid, TEXT("then"), SetScalarGuid, TEXT("execute")))
	{
		return MakeFailure(Error, { TEXT("create_mid_to_set_scalar_exec") }, CreatedGuids);
	}
	if (!ConnectOrFail(CreateMIDGuid, TEXT("ReturnValue"), SetScalarGuid, TEXT("self")))
	{
		return MakeFailure(Error, { TEXT("mid_to_set_scalar_target") }, CreatedGuids);
	}

	if (!ValueVariableName.IsEmpty())
	{
		TSharedPtr<FJsonObject> ValueVariableNode;
		if (!AddNodeOrFail(FString::Printf(TEXT("var_get::%s"), *ValueVariableName), FString::Printf(TEXT("Get%s"), *ValueVariableName), nullptr, ValueVariableNode))
		{
			RemoveCreatedNodes(CreatedGuids);
			return MakeFailure(Error, { TEXT("value_variable_get") }, CreatedGuids);
		}
		const FString ValueVariableGuid = ValueVariableNode->GetStringField(TEXT("node_guid"));
		SetCreatedNodePosition(ValueVariableGuid, BaseX + 360, BaseY + 170);
		if (!ConnectOrFail(ValueVariableGuid, ValueVariableName, SetScalarGuid, TEXT("Value")))
		{
			return MakeFailure(Error, { TEXT("value_variable_to_set_scalar") }, CreatedGuids);
		}
	}

	FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("completed_dynamic_material_update"), true);
	Result->SetBoolField(TEXT("graph_mutated"), true);
	Result->SetBoolField(TEXT("partial_success"), false);
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("graph_name"), Graph->GetName());
	Result->SetStringField(TEXT("component"), ComponentName);
	Result->SetStringField(TEXT("parameter_name"), ParameterName);
	Result->SetStringField(TEXT("value_variable_name"), ValueVariableName);
	Result->SetNumberField(TEXT("material_index"), MaterialIndex);
	Result->SetStringField(TEXT("create_mid_node_guid"), CreateMIDGuid);
	Result->SetStringField(TEXT("set_scalar_node_guid"), SetScalarGuid);
	Result->SetArrayField(TEXT("created_node_guids"), MakeStringArrayValue(CreatedGuids));
	Result->SetArrayField(TEXT("created_nodes"), CreatedNodes);
	Result->SetArrayField(TEXT("connections"), Connections);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintCreateFunction(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString FunctionName = GetOptionalStringParam(Arguments, TEXT("FunctionName")).TrimStartAndEnd();
	if (FunctionName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_create_function requires FunctionName."), true);
	}

	const FName FunctionFName(*FunctionName);
	if (!FBlueprintEditorUtils::IsGraphNameUnique(Blueprint, FunctionFName))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Blueprint graph/function already exists: %s"), *FunctionName), true);
	}

	struct FFunctionPinSpec
	{
		FName Name;
		FEdGraphPinType PinType;
		FString Type;
	};

	auto ParsePinSpecs = [&Error](const TSharedPtr<FJsonObject>& SourceObject, const FString& FieldName, TArray<FFunctionPinSpec>& OutSpecs) -> bool
	{
		const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
		if (!SourceObject->TryGetArrayField(FieldName, Values) || !Values)
		{
			return true;
		}

		TSet<FName> Names;
		for (const TSharedPtr<FJsonValue>& Value : *Values)
		{
			const TSharedPtr<FJsonObject> SpecObject = Value.IsValid() ? Value->AsObject() : nullptr;
			if (!SpecObject.IsValid())
			{
				Error = FString::Printf(TEXT("%s entries must be objects with Name and Type."), *FieldName);
				return false;
			}

			const FString PinName = FDruidsMCPBridgeServer::GetOptionalStringParam(SpecObject, TEXT("Name")).TrimStartAndEnd();
			const FString PinTypeText = FDruidsMCPBridgeServer::GetOptionalStringParam(SpecObject, TEXT("Type")).TrimStartAndEnd();
			if (PinName.IsEmpty() || PinTypeText.IsEmpty())
			{
				Error = FString::Printf(TEXT("%s entries require Name and Type."), *FieldName);
				return false;
			}

			const FName PinFName(*PinName);
			if (Names.Contains(PinFName))
			{
				Error = FString::Printf(TEXT("Duplicate %s pin name: %s"), *FieldName, *PinName);
				return false;
			}
			Names.Add(PinFName);

			FEdGraphPinType PinType;
			FString CanonicalType;
			if (!DruidsMCPBridge::TryResolveVariablePinType(PinTypeText, FDruidsMCPBridgeServer::GetOptionalStringParam(SpecObject, TEXT("ObjectClassPath")), PinType, CanonicalType, Error))
			{
				return false;
			}

			FFunctionPinSpec ParsedSpec;
			ParsedSpec.Name = PinFName;
			ParsedSpec.PinType = PinType;
			ParsedSpec.Type = CanonicalType;
			OutSpecs.Add(ParsedSpec);
		}

		return true;
	};

	TArray<FFunctionPinSpec> InputSpecs;
	TArray<FFunctionPinSpec> OutputSpecs;
	if (!ParsePinSpecs(Arguments, TEXT("Inputs"), InputSpecs) || !ParsePinSpecs(Arguments, TEXT("Outputs"), OutputSpecs))
	{
		return MakeToolTextResult(Error, true);
	}

	UEdGraph* Graph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, FunctionFName, UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
	if (!Graph)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create function graph: %s"), *FunctionName), true);
	}

	FBlueprintEditorUtils::AddFunctionGraph<UFunction>(Blueprint, Graph, true, nullptr);

	TArray<UK2Node_FunctionEntry*> EntryNodes;
	Graph->GetNodesOfClass<UK2Node_FunctionEntry>(EntryNodes);
	if (EntryNodes.Num() == 0 || !EntryNodes[0])
	{
		return MakeToolTextResult(FString::Printf(TEXT("Function graph has no entry node: %s"), *FunctionName), true);
	}

	UK2Node_FunctionEntry* EntryNode = EntryNodes[0];
	UK2Node_FunctionResult* ResultNode = FBlueprintEditorUtils::FindOrCreateFunctionResultNode(EntryNode);
	if (!ResultNode)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Function graph has no result node: %s"), *FunctionName), true);
	}

	for (const FFunctionPinSpec& Spec : InputSpecs)
	{
		FText PinError;
		if (!EntryNode->CanCreateUserDefinedPin(Spec.PinType, EGPD_Output, PinError) || !EntryNode->CreateUserDefinedPin(Spec.Name, Spec.PinType, EGPD_Output, false))
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not add input pin '%s' to function '%s': %s"), *Spec.Name.ToString(), *FunctionName, *PinError.ToString()), true);
		}
	}

	for (const FFunctionPinSpec& Spec : OutputSpecs)
	{
		FText PinError;
		if (!ResultNode->CanCreateUserDefinedPin(Spec.PinType, EGPD_Input, PinError) || !ResultNode->CreateUserDefinedPin(Spec.Name, Spec.PinType, EGPD_Input, false))
		{
			return MakeToolTextResult(FString::Printf(TEXT("Could not add output pin '%s' to function '%s': %s"), *Spec.Name.ToString(), *FunctionName, *PinError.ToString()), true);
		}
	}

	const UEdGraphSchema_K2* Schema = Cast<UEdGraphSchema_K2>(Graph->GetSchema());
	if (Schema)
	{
		UEdGraphPin* EntryThenPin = Schema->FindExecutionPin(*EntryNode, EGPD_Output);
		UEdGraphPin* ResultExecPin = Schema->FindExecutionPin(*ResultNode, EGPD_Input);
		if (EntryThenPin && ResultExecPin && EntryThenPin->LinkedTo.Num() == 0)
		{
			Schema->TryCreateConnection(EntryThenPin, ResultExecPin);
		}
	}

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);

	auto MakePinSpecValues = [](const TArray<FFunctionPinSpec>& Specs) -> TArray<TSharedPtr<FJsonValue>>
	{
		TArray<TSharedPtr<FJsonValue>> Values;
		for (const FFunctionPinSpec& Spec : Specs)
		{
			TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
			Object->SetStringField(TEXT("name"), Spec.Name.ToString());
			Object->SetStringField(TEXT("type"), Spec.Type);
			Object->SetStringField(TEXT("pin_category"), Spec.PinType.PinCategory.ToString());
			Object->SetStringField(TEXT("pin_subcategory"), Spec.PinType.PinSubCategory.ToString());
			UObject* SubCategoryObject = Spec.PinType.PinSubCategoryObject.Get();
			Object->SetStringField(TEXT("pin_subcategory_object"), SubCategoryObject ? SubCategoryObject->GetPathName() : TEXT(""));
			Object->SetStringField(TEXT("container_type"), DruidsMCPBridge::GetPinContainerTypeName(Spec.PinType));
			Object->SetStringField(TEXT("type_string"), DruidsMCPBridge::MakePinTypeString(Spec.PinType));
			Object->SetObjectField(TEXT("type"), DruidsMCPBridge::MakePinTypeObject(Spec.PinType));
			Values.Add(MakeShared<FJsonValueObject>(Object));
		}
		return Values;
	};

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("function_name"), FunctionName);
	Result->SetStringField(TEXT("graph_name"), Graph->GetName());
	Result->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("bp_func::%s"), *FunctionName));
	Result->SetArrayField(TEXT("inputs"), MakePinSpecValues(InputSpecs));
	Result->SetArrayField(TEXT("outputs"), MakePinSpecValues(OutputSpecs));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintCreateEventGraph(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString GraphName = GetOptionalStringParam(Arguments, TEXT("GraphName")).TrimStartAndEnd();
	if (GraphName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_create_event_graph requires GraphName."), true);
	}

	const FName GraphFName(*GraphName);
	if (!FBlueprintEditorUtils::IsGraphNameUnique(Blueprint, GraphFName))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Blueprint graph already exists: %s"), *GraphName), true);
	}

	UEdGraph* Graph = FBlueprintEditorUtils::CreateNewGraph(Blueprint, GraphFName, UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
	if (!Graph)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create event graph: %s"), *GraphName), true);
	}

	FBlueprintEditorUtils::AddUbergraphPage(Blueprint, Graph);
	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("graph_name"), Graph->GetName());
	Result->SetStringField(TEXT("graph_type"), TEXT("ubergraph"));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintImplementInterface(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString InterfaceNameOrPath = GetOptionalStringParam(Arguments, TEXT("InterfaceNameOrPath")).TrimStartAndEnd();
	if (InterfaceNameOrPath.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_implement_interface requires InterfaceNameOrPath."), true);
	}

	UClass* InterfaceClass = FindObject<UClass>(nullptr, *InterfaceNameOrPath);
	if (!InterfaceClass)
	{
		InterfaceClass = LoadObject<UClass>(nullptr, *InterfaceNameOrPath);
	}
	if (!InterfaceClass)
	{
		FString InterfaceAssetPath;
		FString InterfaceError;
		ResolveAssetToObject(InterfaceNameOrPath, TEXT("Blueprint"), 10, InterfaceAssetPath, InterfaceError);
		if (UBlueprint* InterfaceBlueprint = LoadBlueprint(InterfaceAssetPath, InterfaceError))
		{
			InterfaceClass = InterfaceBlueprint->GeneratedClass ? InterfaceBlueprint->GeneratedClass : InterfaceBlueprint->SkeletonGeneratedClass;
		}
	}
	if (!InterfaceClass || !InterfaceClass->IsChildOf(UInterface::StaticClass()))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not resolve Blueprint Interface class: %s"), *InterfaceNameOrPath), true);
	}

	bool bAlreadyImplemented = false;
	for (const FBPInterfaceDescription& InterfaceDescription : Blueprint->ImplementedInterfaces)
	{
		if (InterfaceDescription.Interface == InterfaceClass)
		{
			bAlreadyImplemented = true;
			break;
		}
	}

	if (!bAlreadyImplemented && !FBlueprintEditorUtils::ImplementNewInterface(Blueprint, InterfaceClass->GetClassPathName()))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not implement interface '%s' on %s."), *InterfaceClass->GetPathName(), *Blueprint->GetName()), true);
	}

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("interface_class_path"), InterfaceClass->GetPathName());
	Result->SetStringField(TEXT("interface_class_name"), InterfaceClass->GetName());
	Result->SetBoolField(TEXT("already_implemented"), bAlreadyImplemented);
	Result->SetArrayField(TEXT("available_graphs"), MakeBlueprintGraphInventory(Blueprint, AssetPath));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintAddInterfaceEvent(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	UEdGraph* Graph = Blueprint ? FindBlueprintGraph(Blueprint, GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph")), Error) : nullptr;
	if (!Blueprint || !Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString InterfaceNameOrPath = GetOptionalStringParam(Arguments, TEXT("InterfaceNameOrPath")).TrimStartAndEnd();
	const FString FunctionName = GetOptionalStringParam(Arguments, TEXT("FunctionName")).TrimStartAndEnd();
	if (InterfaceNameOrPath.IsEmpty() || FunctionName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_add_interface_event requires InterfaceNameOrPath and FunctionName."), true);
	}

	UClass* InterfaceClass = FindObject<UClass>(nullptr, *InterfaceNameOrPath);
	if (!InterfaceClass)
	{
		InterfaceClass = LoadObject<UClass>(nullptr, *InterfaceNameOrPath);
	}
	if (!InterfaceClass)
	{
		FString InterfaceAssetPath;
		FString InterfaceError;
		ResolveAssetToObject(InterfaceNameOrPath, TEXT("Blueprint"), 10, InterfaceAssetPath, InterfaceError);
		if (UBlueprint* InterfaceBlueprint = LoadBlueprint(InterfaceAssetPath, InterfaceError))
		{
			InterfaceClass = InterfaceBlueprint->GeneratedClass ? InterfaceBlueprint->GeneratedClass : InterfaceBlueprint->SkeletonGeneratedClass;
		}
	}
	if (!InterfaceClass || !InterfaceClass->IsChildOf(UInterface::StaticClass()))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not resolve Blueprint Interface class: %s"), *InterfaceNameOrPath), true);
	}

	bool bImplementsInterface = false;
	for (const FBPInterfaceDescription& InterfaceDescription : Blueprint->ImplementedInterfaces)
	{
		if (InterfaceDescription.Interface == InterfaceClass)
		{
			bImplementsInterface = true;
			break;
		}
	}
	if (!bImplementsInterface)
	{
		return MakeToolTextResult(FString::Printf(TEXT("%s does not implement interface %s."), *Blueprint->GetName(), *InterfaceClass->GetPathName()), true);
	}

	const FName FunctionFName(*FunctionName);
	UFunction* InterfaceFunction = InterfaceClass->FindFunctionByName(FunctionFName);
	if (!InterfaceFunction)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Interface function does not exist: %s.%s"), *InterfaceClass->GetPathName(), *FunctionName), true);
	}

	TArray<UEdGraph*> Graphs;
	Blueprint->GetAllGraphs(Graphs);
	for (UEdGraph* CandidateGraph : Graphs)
	{
		if (!CandidateGraph)
		{
			continue;
		}

		TArray<UK2Node_Event*> ExistingEvents;
		CandidateGraph->GetNodesOfClass<UK2Node_Event>(ExistingEvents);
		for (const UK2Node_Event* ExistingEvent : ExistingEvents)
		{
			if (!ExistingEvent)
			{
				continue;
			}
			UClass* ExistingOwnerClass = ExistingEvent->EventReference.GetMemberParentClass(ExistingEvent->GetBlueprintClassFromNode());
			if (ExistingOwnerClass == InterfaceClass && ExistingEvent->EventReference.GetMemberName() == FunctionFName)
			{
				return MakeToolTextResult(FString::Printf(TEXT("Blueprint interface event already exists: %s.%s"), *InterfaceClass->GetName(), *FunctionName), true);
			}
		}
	}

	UK2Node_Event* EventNode = NewObject<UK2Node_Event>(Graph);
	EventNode->EventReference.SetFromField<UFunction>(InterfaceFunction, false);
	EventNode->bOverrideFunction = true;
	EventNode->NodePosX = GetOptionalIntParam(Arguments, TEXT("NodePosX"), 0);
	EventNode->NodePosY = GetOptionalIntParam(Arguments, TEXT("NodePosY"), 0);
	Graph->AddNode(EventNode, true, false);
	EventNode->CreateNewGuid();
	EventNode->PostPlacedNewNode();
	EventNode->AllocateDefaultPins();

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);

	TArray<TSharedPtr<FJsonValue>> Parameters;
	for (UEdGraphPin* Pin : EventNode->Pins)
	{
		if (!Pin ||
			Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec ||
			Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Delegate)
		{
			continue;
		}
		TSharedPtr<FJsonObject> Parameter = MakeShared<FJsonObject>();
		UObject* SubCategoryObject = Pin->PinType.PinSubCategoryObject.Get();
		Parameter->SetStringField(TEXT("name"), Pin->PinName.ToString());
		Parameter->SetStringField(TEXT("pin_category"), Pin->PinType.PinCategory.ToString());
		Parameter->SetStringField(TEXT("pin_subcategory"), Pin->PinType.PinSubCategory.ToString());
		Parameter->SetStringField(TEXT("pin_subcategory_object"), SubCategoryObject ? SubCategoryObject->GetPathName() : TEXT(""));
		Parameter->SetStringField(TEXT("container_type"), DruidsMCPBridge::GetPinContainerTypeName(Pin->PinType));
		Parameter->SetStringField(TEXT("type_string"), DruidsMCPBridge::MakePinTypeString(Pin->PinType));
		Parameter->SetObjectField(TEXT("type"), DruidsMCPBridge::MakePinTypeObject(Pin->PinType));
		Parameters.Add(MakeShared<FJsonValueObject>(Parameter));
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("graph_name"), Graph->GetName());
	Result->SetStringField(TEXT("node_guid"), EventNode->NodeGuid.ToString(EGuidFormats::Digits));
	Result->SetStringField(TEXT("interface_class_path"), InterfaceClass->GetPathName());
	Result->SetStringField(TEXT("interface_class_name"), InterfaceClass->GetName());
	Result->SetStringField(TEXT("function_name"), FunctionName);
	Result->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("interface_event::%s:%s"), *InterfaceClass->GetPathName(), *FunctionName));
	Result->SetArrayField(TEXT("parameters"), Parameters);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintAddCustomEvent(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	UEdGraph* Graph = Blueprint ? FindBlueprintGraph(Blueprint, GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph")), Error) : nullptr;
	if (!Blueprint || !Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString EventName = GetOptionalStringParam(Arguments, TEXT("EventName")).TrimStartAndEnd();
	if (EventName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_add_custom_event requires EventName."), true);
	}

	TArray<UEdGraph*> Graphs;
	Blueprint->GetAllGraphs(Graphs);
	for (UEdGraph* CandidateGraph : Graphs)
	{
		if (!CandidateGraph)
		{
			continue;
		}

		TArray<UK2Node_CustomEvent*> ExistingEvents;
		CandidateGraph->GetNodesOfClass<UK2Node_CustomEvent>(ExistingEvents);
		for (const UK2Node_CustomEvent* ExistingEvent : ExistingEvents)
		{
			if (ExistingEvent && ExistingEvent->CustomFunctionName.ToString().Equals(EventName, ESearchCase::IgnoreCase))
			{
				return MakeToolTextResult(FString::Printf(TEXT("Blueprint custom event already exists: %s"), *EventName), true);
			}
		}
	}

	struct FCustomEventPinSpec
	{
		FName Name;
		FEdGraphPinType PinType;
		FString Type;
	};

	TArray<FCustomEventPinSpec> ParameterSpecs;
	const TArray<TSharedPtr<FJsonValue>>* ParameterValues = nullptr;
	if (Arguments->TryGetArrayField(TEXT("Parameters"), ParameterValues) && ParameterValues)
	{
		TSet<FName> Names;
		for (const TSharedPtr<FJsonValue>& Value : *ParameterValues)
		{
			const TSharedPtr<FJsonObject> SpecObject = Value.IsValid() ? Value->AsObject() : nullptr;
			if (!SpecObject.IsValid())
			{
				return MakeToolTextResult(TEXT("Parameters entries must be objects with Name and Type."), true);
			}

			const FString PinName = GetOptionalStringParam(SpecObject, TEXT("Name")).TrimStartAndEnd();
			const FString PinTypeText = GetOptionalStringParam(SpecObject, TEXT("Type")).TrimStartAndEnd();
			if (PinName.IsEmpty() || PinTypeText.IsEmpty())
			{
				return MakeToolTextResult(TEXT("Parameters entries require Name and Type."), true);
			}

			const FName PinFName(*PinName);
			if (Names.Contains(PinFName))
			{
				return MakeToolTextResult(FString::Printf(TEXT("Duplicate Parameters pin name: %s"), *PinName), true);
			}
			Names.Add(PinFName);

			FEdGraphPinType PinType;
			FString CanonicalType;
			if (!DruidsMCPBridge::TryResolveVariablePinType(PinTypeText, GetOptionalStringParam(SpecObject, TEXT("ObjectClassPath")), PinType, CanonicalType, Error))
			{
				return MakeToolTextResult(Error, true);
			}

			FCustomEventPinSpec ParsedSpec;
			ParsedSpec.Name = PinFName;
			ParsedSpec.PinType = PinType;
			ParsedSpec.Type = CanonicalType;
			ParameterSpecs.Add(ParsedSpec);
		}
	}

	UK2Node_CustomEvent* EventNode = NewObject<UK2Node_CustomEvent>(Graph);
	EventNode->CustomFunctionName = FName(*EventName);
	bool bCallInEditor = false;
	Arguments->TryGetBoolField(TEXT("CallInEditor"), bCallInEditor);
	EventNode->bCallInEditor = bCallInEditor;

	Graph->Modify();
	Graph->AddNode(EventNode, true, false);
	EventNode->CreateNewGuid();
	EventNode->NodePosX = 300 + Graph->Nodes.Num() * 32;
	EventNode->NodePosY = 100 + Graph->Nodes.Num() * 24;
	EventNode->PostPlacedNewNode();
	EventNode->AllocateDefaultPins();

	for (const FCustomEventPinSpec& Spec : ParameterSpecs)
	{
		if (!EventNode->CreateUserDefinedPin(Spec.Name, Spec.PinType, EGPD_Output, false))
		{
			Graph->RemoveNode(EventNode);
			return MakeToolTextResult(FString::Printf(TEXT("Could not add parameter pin '%s' to custom event '%s'."), *Spec.Name.ToString(), *EventName), true);
		}
	}

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);

	auto MakePinSpecValues = [](const TArray<FCustomEventPinSpec>& Specs) -> TArray<TSharedPtr<FJsonValue>>
	{
		TArray<TSharedPtr<FJsonValue>> Values;
		for (const FCustomEventPinSpec& Spec : Specs)
		{
			TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
			Object->SetStringField(TEXT("name"), Spec.Name.ToString());
			Object->SetStringField(TEXT("type"), Spec.Type);
			Object->SetStringField(TEXT("pin_category"), Spec.PinType.PinCategory.ToString());
			Object->SetStringField(TEXT("pin_subcategory"), Spec.PinType.PinSubCategory.ToString());
			UObject* SubCategoryObject = Spec.PinType.PinSubCategoryObject.Get();
			Object->SetStringField(TEXT("pin_subcategory_object"), SubCategoryObject ? SubCategoryObject->GetPathName() : TEXT(""));
			Object->SetStringField(TEXT("container_type"), DruidsMCPBridge::GetPinContainerTypeName(Spec.PinType));
			Object->SetStringField(TEXT("type_string"), DruidsMCPBridge::MakePinTypeString(Spec.PinType));
			Object->SetObjectField(TEXT("type"), DruidsMCPBridge::MakePinTypeObject(Spec.PinType));
			Values.Add(MakeShared<FJsonValueObject>(Object));
		}
		return Values;
	};

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("graph_name"), Graph->GetName());
	Result->SetStringField(TEXT("event_name"), EventName);
	Result->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("custom_event::%s"), *EventName));
	Result->SetStringField(TEXT("node_guid"), EventNode->NodeGuid.ToString(EGuidFormats::Digits));
	Result->SetBoolField(TEXT("call_in_editor"), bCallInEditor);
	Result->SetArrayField(TEXT("parameters"), MakePinSpecValues(ParameterSpecs));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintAddComment(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	UEdGraph* Graph = Blueprint ? FindBlueprintGraph(Blueprint, GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph")), Error) : nullptr;
	if (!Blueprint || !Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString CommentText = GetOptionalStringParam(Arguments, TEXT("CommentText")).TrimStartAndEnd();
	if (CommentText.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_add_comment requires CommentText."), true);
	}

	const int32 NodePosX = GetOptionalIntParam(Arguments, TEXT("NodePosX"), 0);
	const int32 NodePosY = GetOptionalIntParam(Arguments, TEXT("NodePosY"), 0);
	const int32 Width = FMath::Max(1, GetOptionalIntParam(Arguments, TEXT("Width"), 400));
	const int32 Height = FMath::Max(1, GetOptionalIntParam(Arguments, TEXT("Height"), 220));
	const int32 FontSize = GetOptionalIntParam(Arguments, TEXT("FontSize"), 0);

	UEdGraphNode_Comment* CommentNode = NewObject<UEdGraphNode_Comment>(Graph);
	CommentNode->NodeComment = CommentText;
	CommentNode->SetBounds(FSlateRect(NodePosX, NodePosY, NodePosX + Width, NodePosY + Height));
	if (FontSize > 0)
	{
		CommentNode->FontSize = FontSize;
	}

	Graph->Modify();
	Graph->AddNode(CommentNode, true, false);
	CommentNode->CreateNewGuid();
	CommentNode->PostPlacedNewNode();
	CommentNode->OnRenameNode(CommentText);
	CommentNode->NodeComment = CommentText;
	CommentNode->AllocateDefaultPins();

	FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);

	TSharedPtr<FJsonObject> Bounds = MakeShared<FJsonObject>();
	Bounds->SetNumberField(TEXT("x"), CommentNode->NodePosX);
	Bounds->SetNumberField(TEXT("y"), CommentNode->NodePosY);
	Bounds->SetNumberField(TEXT("width"), CommentNode->NodeWidth);
	Bounds->SetNumberField(TEXT("height"), CommentNode->NodeHeight);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("graph_name"), Graph->GetName());
	Result->SetStringField(TEXT("node_guid"), CommentNode->NodeGuid.ToString(EGuidFormats::Digits));
	Result->SetStringField(TEXT("comment_text"), CommentNode->NodeComment);
	Result->SetObjectField(TEXT("bounds"), Bounds);
	Result->SetNumberField(TEXT("font_size"), CommentNode->FontSize);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintReadGraph(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	const bool bGraphNameProvided = Arguments.IsValid() && Arguments->HasField(TEXT("GraphName"));
	const FString RequestedGraphName = GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph"));
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	UEdGraph* Graph = Blueprint ? FindBlueprintGraph(Blueprint, RequestedGraphName, Error) : nullptr;
	if (!Blueprint || !Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	TSharedPtr<FJsonObject> Result = ReadBlueprintGraph(Blueprint, Graph);
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	TArray<TSharedPtr<FJsonValue>> AvailableGraphs = MakeBlueprintGraphInventory(Blueprint, AssetPath);
	Result->SetArrayField(TEXT("available_graphs"), AvailableGraphs);
	Result->SetNumberField(TEXT("available_graph_count"), AvailableGraphs.Num());
	TArray<TSharedPtr<FJsonValue>> Warnings;
	if (!bGraphNameProvided && RequestedGraphName.Equals(TEXT("EventGraph"), ESearchCase::IgnoreCase))
	{
		int32 UbergraphCount = 0;
		TArray<FString> OtherUbergraphs;
		for (const TSharedPtr<FJsonValue>& GraphValue : AvailableGraphs)
		{
			TSharedPtr<FJsonObject> GraphObject = GraphValue.IsValid() ? GraphValue->AsObject() : nullptr;
			if (!GraphObject.IsValid())
			{
				continue;
			}
			const FString InventoryGraphType = DruidsMCPBridge::TryGetJsonStringField(GraphObject, TEXT("graph_type"));
			if (!InventoryGraphType.Equals(TEXT("ubergraph"), ESearchCase::IgnoreCase))
			{
				continue;
			}
			++UbergraphCount;
			const FString InventoryGraphName = DruidsMCPBridge::TryGetJsonStringField(GraphObject, TEXT("graph_name"));
			if (!InventoryGraphName.Equals(Graph->GetName(), ESearchCase::IgnoreCase))
			{
				OtherUbergraphs.Add(InventoryGraphName);
			}
		}
		if (UbergraphCount > 1)
		{
			Warnings.Add(MakeShared<FJsonValueString>(FString::Printf(
				TEXT("blueprint_read_graph defaulted to '%s', but this Blueprint has %d Ubergraphs. Read additional graphs explicitly with GraphName. Other Ubergraphs: %s"),
				*Graph->GetName(),
				UbergraphCount,
				*FString::Join(OtherUbergraphs, TEXT(", ")))));
		}
	}
	Result->SetArrayField(TEXT("warnings"), Warnings);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintCompile(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	auto BlueprintStatusToString = [](EBlueprintStatus Status) -> FString
	{
		switch (Status)
		{
		case BS_Unknown:
			return TEXT("unknown");
		case BS_Dirty:
			return TEXT("dirty");
		case BS_Error:
			return TEXT("error");
		case BS_UpToDate:
			return TEXT("up_to_date");
		case BS_BeingCreated:
			return TEXT("being_created");
		case BS_UpToDateWithWarnings:
			return TEXT("up_to_date_with_warnings");
		default:
			return TEXT("unknown");
		}
	};

	FCompilerResultsLog MessageLog;
	MessageLog.bSilentMode = true;
	MessageLog.SetSourcePath(AssetPath);

	const double StartSeconds = FPlatformTime::Seconds();
	FKismetEditorUtilities::CompileBlueprint(Blueprint, EBlueprintCompileOptions::SkipSave, &MessageLog);
	const double DurationSeconds = FPlatformTime::Seconds() - StartSeconds;

	TArray<TSharedPtr<FJsonValue>> MessageValues;
	for (const TSharedRef<FTokenizedMessage>& Message : MessageLog.Messages)
	{
		TSharedPtr<FJsonObject> MessageObject = MakeShared<FJsonObject>();
		MessageObject->SetStringField(TEXT("severity"), FTokenizedMessage::GetSeverityText(Message->GetSeverity()).ToString().ToLower());
		MessageObject->SetStringField(TEXT("text"), Message->ToText().ToString());
		MessageValues.Add(MakeShared<FJsonValueObject>(MessageObject));
	}

	const bool bHasGeneratedClass = Blueprint->GeneratedClass != nullptr;
	const bool bHasSkeletonClass = Blueprint->SkeletonGeneratedClass != nullptr;
	const bool bSuccess = MessageLog.NumErrors == 0 && Blueprint->Status != BS_Error && bHasGeneratedClass;

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("object_path"), Blueprint->GetPathName());
	Result->SetBoolField(TEXT("success"), bSuccess);
	Result->SetStringField(TEXT("status"), BlueprintStatusToString(Blueprint->Status));
	Result->SetNumberField(TEXT("error_count"), MessageLog.NumErrors);
	Result->SetNumberField(TEXT("warning_count"), MessageLog.NumWarnings);
	Result->SetNumberField(TEXT("message_count"), MessageLog.Messages.Num());
	Result->SetNumberField(TEXT("duration_seconds"), DurationSeconds);
	Result->SetBoolField(TEXT("has_generated_class"), bHasGeneratedClass);
	Result->SetBoolField(TEXT("has_skeleton_generated_class"), bHasSkeletonClass);
	Result->SetStringField(TEXT("generated_class_path"), bHasGeneratedClass ? Blueprint->GeneratedClass->GetPathName() : TEXT(""));
	Result->SetStringField(TEXT("skeleton_generated_class_path"), bHasSkeletonClass ? Blueprint->SkeletonGeneratedClass->GetPathName() : TEXT(""));
	Result->SetArrayField(TEXT("messages"), MessageValues);
	return MakeToolJsonObjectResult(Result, !bSuccess);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintFindNodeSpawners(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString SearchQuery = GetOptionalStringParam(Arguments, TEXT("SearchQuery"));
	TArray<TSharedPtr<FJsonValue>> Nodes = FindNativeNodeSpawners(Blueprint, SearchQuery, GetOptionalStringParam(Arguments, TEXT("InputType")), GetOptionalStringParam(Arguments, TEXT("OutputType")), GetOptionalIntParam(Arguments, TEXT("Limit"), 20));
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("search_query"), SearchQuery);
	Result->SetArrayField(TEXT("nodes"), Nodes);
	Result->SetNumberField(TEXT("node_count"), Nodes.Num());
	if (Nodes.Num() == 0 && IsUnsupportedInputEventRequest(SearchQuery, TEXT("event")))
	{
		Result->SetStringField(TEXT("unsupported_request_type"), TEXT("input_event"));
		Result->SetStringField(TEXT("rejected_reason"), FString::Printf(TEXT("Native input event node creation is not supported for '%s'. Use a dedicated input event helper or an explicit future input-event spawner."), *SearchQuery));
	}
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintResolveNodeSpawner(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	if (!Blueprint)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString Query = GetOptionalStringParam(Arguments, TEXT("Query"));
	const FString Intent = GetOptionalStringParam(Arguments, TEXT("Intent"));
	const FString Operator = GetOptionalStringParam(Arguments, TEXT("Operator"));
	const FString ValueType = GetOptionalStringParam(Arguments, TEXT("ValueType"));
	if (IsUnsupportedInputEventRequest(Query, Intent))
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("asset_path"), AssetPath);
		Result->SetStringField(TEXT("query"), Query);
		Result->SetArrayField(TEXT("searches"), MakeStringArrayValue({ Query }));
		Result->SetArrayField(TEXT("candidates"), TArray<TSharedPtr<FJsonValue>>());
		Result->SetNumberField(TEXT("candidate_count"), 0);
		Result->SetStringField(TEXT("unsupported_request_type"), TEXT("input_event"));
		Result->SetStringField(TEXT("rejected_reason"), FString::Printf(TEXT("Native input event node creation is not supported for '%s'. Use a dedicated input event helper or an explicit future input-event spawner."), *Query));
		return MakeToolJsonObjectResult(Result, true);
	}
	TArray<FString> Searches = BuildSpawnerResolverQueries(Query, Intent, Operator, ValueType);
	TArray<TSharedPtr<FJsonValue>> Candidates;
	for (const FString& Search : Searches)
	{
		for (const TSharedPtr<FJsonValue>& Candidate : FindNativeNodeSpawners(Blueprint, Search, TEXT(""), TEXT(""), 10))
		{
			const TSharedPtr<FJsonObject> CandidateObject = Candidate->AsObject();
			const int32 Score = ScoreSpawnerCandidate(CandidateObject, Query, Intent, Operator, ValueType);
			FString ConfidenceReason;
			const bool bHighConfidence = IsHighConfidenceSpawnerCandidate(CandidateObject, Query, Intent, Operator, ValueType, Score, ConfidenceReason);
			CandidateObject->SetNumberField(TEXT("confidence_score"), Score);
			CandidateObject->SetBoolField(TEXT("high_confidence"), bHighConfidence);
			CandidateObject->SetStringField(TEXT("match_reason"), ConfidenceReason);
			Candidates.Add(Candidate);
		}
	}
	Candidates.Sort([](const TSharedPtr<FJsonValue>& Left, const TSharedPtr<FJsonValue>& Right)
	{
		const TSharedPtr<FJsonObject> LeftObject = Left->AsObject();
		const TSharedPtr<FJsonObject> RightObject = Right->AsObject();
		double LeftScore = 0.0;
		double RightScore = 0.0;
		if (LeftObject.IsValid()) LeftObject->TryGetNumberField(TEXT("confidence_score"), LeftScore);
		if (RightObject.IsValid()) RightObject->TryGetNumberField(TEXT("confidence_score"), RightScore);
		return LeftScore > RightScore;
	});

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("query"), Query);
	Result->SetArrayField(TEXT("searches"), MakeStringArrayValue(Searches));
	Result->SetArrayField(TEXT("candidates"), Candidates);
	Result->SetNumberField(TEXT("candidate_count"), Candidates.Num());
	TSharedPtr<FJsonObject> BestCandidate;
	for (const TSharedPtr<FJsonValue>& Candidate : Candidates)
	{
		const TSharedPtr<FJsonObject> CandidateObject = Candidate->AsObject();
		bool bHighConfidence = false;
		if (CandidateObject.IsValid() && CandidateObject->TryGetBoolField(TEXT("high_confidence"), bHighConfidence) && bHighConfidence)
		{
			BestCandidate = CandidateObject;
			break;
		}
	}
	if (BestCandidate.IsValid())
	{
		Result->SetObjectField(TEXT("best"), BestCandidate);
		FString SpawnerId;
		if (BestCandidate->TryGetStringField(TEXT("spawner_id"), SpawnerId))
		{
			Result->SetStringField(TEXT("best_spawner_id"), SpawnerId);
		}
	}
	else
	{
		if (IsUnsupportedInputEventRequest(Query, Intent))
		{
			Result->SetStringField(TEXT("unsupported_request_type"), TEXT("input_event"));
			Result->SetStringField(TEXT("rejected_reason"), FString::Printf(TEXT("Native input event node creation is not supported for '%s'. Use a dedicated input event helper or an explicit future input-event spawner."), *Query));
		}
		else
		{
			Result->SetStringField(TEXT("rejected_reason"), FString::Printf(TEXT("No high-confidence native spawner found for '%s'."), *Query));
		}
	}
	return MakeToolJsonObjectResult(Result, !BestCandidate.IsValid());
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintAddNode(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	UEdGraph* Graph = Blueprint ? FindBlueprintGraph(Blueprint, GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph")), Error) : nullptr;
	if (!Blueprint || !Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	TSharedPtr<FJsonObject> PinDefaults;
	TryGetObjectParam(Arguments, TEXT("PinDefaults"), PinDefaults);
	TSharedPtr<FJsonObject> AddResult = AddNativeBlueprintNode(Blueprint, Graph, GetOptionalStringParam(Arguments, TEXT("SpawnerId")), GetOptionalStringParam(Arguments, TEXT("NodeName"), TEXT("MCPAddedNode")), PinDefaults, Error);
	if (!AddResult.IsValid())
	{
		return MakeToolTextResult(Error, true);
	}

	bool bHasNodePosX = false;
	bool bHasNodePosY = false;
	int32 NodePosX = GetOptionalIntParam(Arguments, TEXT("NodePosX"), 0);
	int32 NodePosY = GetOptionalIntParam(Arguments, TEXT("NodePosY"), 0);
	{
		double IgnoredNumber = 0.0;
		FString IgnoredString;
		bHasNodePosX = Arguments->TryGetNumberField(TEXT("NodePosX"), IgnoredNumber) || Arguments->TryGetStringField(TEXT("NodePosX"), IgnoredString);
		bHasNodePosY = Arguments->TryGetNumberField(TEXT("NodePosY"), IgnoredNumber) || Arguments->TryGetStringField(TEXT("NodePosY"), IgnoredString);
	}
	if (bHasNodePosX || bHasNodePosY)
	{
		FString NodeGuid;
		AddResult->TryGetStringField(TEXT("node_guid"), NodeGuid);
		for (UEdGraphNode* Node : Graph->Nodes)
		{
			if (Node && Node->NodeGuid.ToString(EGuidFormats::Digits).Equals(NodeGuid, ESearchCase::IgnoreCase))
			{
				Node->Modify();
				if (bHasNodePosX)
				{
					Node->NodePosX = NodePosX;
				}
				if (bHasNodePosY)
				{
					Node->NodePosY = NodePosY;
				}
				AddResult->SetNumberField(TEXT("node_pos_x"), Node->NodePosX);
				AddResult->SetNumberField(TEXT("node_pos_y"), Node->NodePosY);
				FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
				break;
			}
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetObjectField(TEXT("add_result"), AddResult);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintSetNodePosition(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	UEdGraph* Graph = Blueprint ? FindBlueprintGraph(Blueprint, GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph")), Error) : nullptr;
	if (!Blueprint || !Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString NodeGuid = GetOptionalStringParam(Arguments, TEXT("NodeGuid")).TrimStartAndEnd();
	if (NodeGuid.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_set_node_position requires NodeGuid."), true);
	}

	UEdGraphNode* TargetNode = nullptr;
	for (UEdGraphNode* Node : Graph->Nodes)
	{
		if (Node && Node->NodeGuid.ToString(EGuidFormats::Digits).Equals(NodeGuid, ESearchCase::IgnoreCase))
		{
			TargetNode = Node;
			break;
		}
	}

	if (!TargetNode)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not resolve node GUID for positioning: %s"), *NodeGuid), true);
	}

	const int32 NodePosX = GetOptionalIntParam(Arguments, TEXT("NodePosX"), TargetNode->NodePosX);
	const int32 NodePosY = GetOptionalIntParam(Arguments, TEXT("NodePosY"), TargetNode->NodePosY);
	TargetNode->Modify();
	TargetNode->NodePosX = NodePosX;
	TargetNode->NodePosY = NodePosY;
	FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("graph_name"), Graph->GetName());
	Result->SetStringField(TEXT("node_guid"), TargetNode->NodeGuid.ToString(EGuidFormats::Digits));
	Result->SetStringField(TEXT("title"), TargetNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
	Result->SetNumberField(TEXT("node_pos_x"), TargetNode->NodePosX);
	Result->SetNumberField(TEXT("node_pos_y"), TargetNode->NodePosY);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintConnectNodes(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	UEdGraph* Graph = Blueprint ? FindBlueprintGraph(Blueprint, GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph")), Error) : nullptr;
	if (!Blueprint || !Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	if (!ConnectNativeBlueprintPins(Blueprint, Graph, GetOptionalStringParam(Arguments, TEXT("SourceNodeGuid")), GetOptionalStringParam(Arguments, TEXT("SourcePinName")), GetOptionalStringParam(Arguments, TEXT("TargetNodeGuid")), GetOptionalStringParam(Arguments, TEXT("TargetPinName")), Error))
	{
		return MakeToolTextResult(Error, true);
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("result"), TEXT("connected"));
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintAddNodeBySearchAndConnect(const TSharedPtr<FJsonObject>& Arguments)
{
	FString AssetPath;
	FString Error;
	ResolveAssetToObject(GetOptionalStringParam(Arguments, TEXT("BlueprintNameOrPath")), TEXT("Blueprint"), 10, AssetPath, Error);
	UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
	const FString GraphName = GetOptionalStringParam(Arguments, TEXT("GraphName"), TEXT("EventGraph"));
	UEdGraph* Graph = Blueprint ? FindBlueprintGraph(Blueprint, GraphName, Error) : nullptr;
	if (!Blueprint || !Graph)
	{
		return MakeToolTextResult(Error, true);
	}

	const FString SourceNodeTitleOrGuid = GetOptionalStringParam(Arguments, TEXT("SourceNodeTitleOrGuid"));
	const FString SearchQuery = GetOptionalStringParam(Arguments, TEXT("SearchQuery"));
	const FString SourcePinName = GetOptionalStringParam(Arguments, TEXT("SourcePinName"), TEXT("then"));
	const FString TargetPinName = GetOptionalStringParam(Arguments, TEXT("TargetPinName"), TEXT("execute"));
	if (SourceNodeTitleOrGuid.IsEmpty() || SearchQuery.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_add_node_by_search_and_connect requires SourceNodeTitleOrGuid and SearchQuery."), true);
	}

	TSharedPtr<FJsonObject> GraphResult = ReadBlueprintGraph(Blueprint, Graph);
	TSharedPtr<FJsonObject> SourceNode = FindNodeByTitleOrGuid(MakeShared<FJsonValueObject>(GraphResult), SourceNodeTitleOrGuid);
	FString SourceNodeGuid;
	if (SourceNode.IsValid())
	{
		TryFindStringFieldRecursive(MakeShared<FJsonValueObject>(SourceNode), { TEXT("node_guid"), TEXT("guid") }, SourceNodeGuid);
	}
	if (SourceNodeGuid.IsEmpty())
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not find source node matching '%s'."), *SourceNodeTitleOrGuid), true);
	}

	TArray<TSharedPtr<FJsonValue>> Spawners = FindNativeNodeSpawners(Blueprint, SearchQuery, TEXT(""), TEXT(""), 10);
	if (Spawners.Num() == 0)
	{
		if (IsUnsupportedInputEventRequest(SearchQuery, TEXT("event")))
		{
			TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
			Result->SetStringField(TEXT("asset_path"), AssetPath);
			Result->SetStringField(TEXT("graph_name"), GraphName);
			Result->SetStringField(TEXT("search_query"), SearchQuery);
			Result->SetBoolField(TEXT("graph_mutated"), false);
			Result->SetStringField(TEXT("unsupported_request_type"), TEXT("input_event"));
			Result->SetStringField(TEXT("rejected_reason"), FString::Printf(TEXT("Native input event node creation is not supported for '%s'. Use a dedicated input event helper or an explicit future input-event spawner."), *SearchQuery));
			return MakeToolJsonObjectResult(Result, true);
		}
		return MakeToolTextResult(FString::Printf(TEXT("No native spawner found for '%s'."), *SearchQuery), true);
	}
	Spawners.Sort([&SearchQuery](const TSharedPtr<FJsonValue>& Left, const TSharedPtr<FJsonValue>& Right)
	{
		const TSharedPtr<FJsonObject> LeftObject = Left->AsObject();
		const TSharedPtr<FJsonObject> RightObject = Right->AsObject();
		const int32 LeftScore = ScoreSpawnerCandidate(LeftObject, SearchQuery, TEXT(""), TEXT(""), TEXT(""));
		const int32 RightScore = ScoreSpawnerCandidate(RightObject, SearchQuery, TEXT(""), TEXT(""), TEXT(""));
		return LeftScore > RightScore;
	});
	const TSharedPtr<FJsonObject> BestSpawner = Spawners[0]->AsObject();
	const int32 BestScore = ScoreSpawnerCandidate(BestSpawner, SearchQuery, TEXT(""), TEXT(""), TEXT(""));
	FString ConfidenceReason;
	if (!IsHighConfidenceSpawnerCandidate(BestSpawner, SearchQuery, TEXT(""), TEXT(""), TEXT(""), BestScore, ConfidenceReason))
	{
		return MakeToolTextResult(FString::Printf(TEXT("No high-confidence native spawner found for '%s'. Best candidate was rejected: %s"), *SearchQuery, *ConfidenceReason), true);
	}
	FString SpawnerId;
	BestSpawner->TryGetStringField(TEXT("spawner_id"), SpawnerId);

	TSharedPtr<FJsonObject> PinDefaults;
	TryGetObjectParam(Arguments, TEXT("PinDefaults"), PinDefaults);
	TSharedPtr<FJsonObject> AddResult = AddNativeBlueprintNode(Blueprint, Graph, SpawnerId, TEXT("MCPAddedNode"), PinDefaults, Error);
	if (!AddResult.IsValid())
	{
		return MakeToolTextResult(Error, true);
	}

	const FString NewNodeGuid = AddResult->GetStringField(TEXT("node_guid"));
	if (!ConnectNativeBlueprintPins(Blueprint, Graph, SourceNodeGuid, SourcePinName, NewNodeGuid, TargetPinName, Error))
	{
		for (UEdGraphNode* Node : Graph->Nodes)
		{
			if (Node && Node->NodeGuid.ToString(EGuidFormats::Digits).Equals(NewNodeGuid, ESearchCase::IgnoreCase))
			{
				Graph->Modify();
				Node->Modify();
				Node->BreakAllNodeLinks();
				Graph->RemoveNode(Node);
				FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
				break;
			}
		}
		return MakeToolTextResult(Error, true);
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("graph_name"), GraphName);
	Result->SetStringField(TEXT("source_node_guid"), SourceNodeGuid);
	Result->SetStringField(TEXT("new_node_guid"), NewNodeGuid);
	Result->SetStringField(TEXT("spawner_id"), SpawnerId);
	Result->SetObjectField(TEXT("add_result"), AddResult);
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintSearchProject(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString Query = GetOptionalStringParam(Arguments, TEXT("Query")).TrimStartAndEnd();
	if (Query.IsEmpty())
	{
		return MakeToolTextResult(TEXT("blueprint_search_project requires Query."), true);
	}
	FString Intent = GetOptionalStringParam(Arguments, TEXT("Intent"), TEXT("broad")).TrimStartAndEnd().ToLower();
	if (Intent.IsEmpty())
	{
		Intent = TEXT("broad");
	}
	const TSet<FString> AllowedIntents = {
		TEXT("broad"), TEXT("find_example"), TEXT("find_comment"), TEXT("find_definition"),
		TEXT("find_usage"), TEXT("find_asset_reference"), TEXT("system_overview")
	};
	if (!AllowedIntents.Contains(Intent))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Unsupported blueprint_search_project intent '%s'."), *Intent), true);
	}

	const TArray<FString> DefaultTargets = {
		TEXT("assets"), TEXT("graphs"), TEXT("nodes"), TEXT("comments"),
		TEXT("variables"), TEXT("functions"), TEXT("components"), TEXT("metadata")
	};
	TSet<FString> AllowedTargets;
	for (const FString& Target : DefaultTargets)
	{
		AllowedTargets.Add(Target);
	}

	TArray<FString> TargetValues = DruidsMCPBridge::ReadStringArrayField(Arguments, TEXT("Targets"), DefaultTargets);
	TSet<FString> Targets;
	for (FString Target : TargetValues)
	{
		Target.TrimStartAndEndInline();
		Target = Target.ToLower();
		if (Target.Equals(TEXT("all"), ESearchCase::IgnoreCase))
		{
			Targets.Add(TEXT("all"));
			continue;
		}
		if (!AllowedTargets.Contains(Target))
		{
			return MakeToolTextResult(FString::Printf(TEXT("Unsupported blueprint_search_project target '%s'."), *Target), true);
		}
		Targets.Add(Target);
	}
	if (Targets.Num() == 0)
	{
		for (const FString& Target : DefaultTargets)
		{
			Targets.Add(Target);
		}
	}

	const TArray<FString> DefaultPackagePaths = { TEXT("/Game") };
	TArray<FString> PackagePaths = DruidsMCPBridge::ReadStringArrayField(Arguments, TEXT("PackagePaths"), DefaultPackagePaths);
	for (FString& PackagePath : PackagePaths)
	{
		PackagePath.TrimStartAndEndInline();
		if (PackagePath.IsEmpty())
		{
			PackagePath = TEXT("/Game");
		}
		if (!PackagePath.StartsWith(TEXT("/")))
		{
			PackagePath = FString::Printf(TEXT("/%s"), *PackagePath);
		}
	}
	PackagePaths.Sort();

	bool bRefresh = false;
	Arguments->TryGetBoolField(TEXT("Refresh"), bRefresh);
	const int32 Limit = FMath::Clamp(GetOptionalIntParam(Arguments, TEXT("Limit"), 25), 0, 200);
	const FString CacheKey = FString::Join(PackagePaths, TEXT("|"));
	bool bCacheRefreshed = false;

	if (bRefresh || !bBlueprintSearchCacheValid || BlueprintSearchCacheKey != CacheKey)
	{
		bCacheRefreshed = true;
		BlueprintSearchCache.Empty();
		BlueprintSearchCacheFailures.Empty();
		BlueprintSearchCacheKey = CacheKey;

		FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
		FARFilter AllAssetFilter;
		AllAssetFilter.bRecursivePaths = true;
		for (const FString& PackagePath : PackagePaths)
		{
			AllAssetFilter.PackagePaths.Add(*PackagePath);
		}
		FARFilter BlueprintFilter = AllAssetFilter;
		BlueprintFilter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());

		TArray<FAssetData> BlueprintAssets;
		AssetRegistryModule.Get().GetAssets(BlueprintFilter, BlueprintAssets);
		BlueprintAssets.Sort([](const FAssetData& A, const FAssetData& B)
		{
			return A.PackageName.ToString() < B.PackageName.ToString();
		});

		for (const FAssetData& Asset : BlueprintAssets)
		{
			const FString AssetPath = Asset.PackageName.ToString();
			const FString AssetName = Asset.AssetName.ToString();
			FString Error;
			UBlueprint* Blueprint = LoadBlueprint(AssetPath, Error);
			if (!Blueprint)
			{
				TSharedPtr<FJsonObject> Failure = MakeShared<FJsonObject>();
				Failure->SetStringField(TEXT("asset_path"), AssetPath);
				Failure->SetStringField(TEXT("asset_name"), AssetName);
				Failure->SetStringField(TEXT("error"), Error);
				BlueprintSearchCacheFailures.Add(MakeShared<FJsonValueObject>(Failure));
				continue;
			}

			TSharedPtr<FJsonObject> Summary = MakeShared<FJsonObject>();
			Summary->SetStringField(TEXT("asset_path"), AssetPath);
			Summary->SetStringField(TEXT("object_path"), Asset.GetSoftObjectPath().ToString());
			Summary->SetStringField(TEXT("asset_name"), AssetName);
			Summary->SetStringField(TEXT("class"), Asset.AssetClassPath.ToString());
			Summary->SetStringField(TEXT("asset_domain"), TEXT("blueprint"));
			Summary->SetStringField(TEXT("inspectability"), TEXT("deep_blueprint"));
			Summary->SetStringField(TEXT("parent_class"), Blueprint->ParentClass ? Blueprint->ParentClass->GetPathName() : TEXT(""));

			TArray<TSharedPtr<FJsonValue>> GraphValues = MakeBlueprintGraphInventory(Blueprint, AssetPath);
			TArray<TSharedPtr<FJsonValue>> NodeValues;
			TArray<TSharedPtr<FJsonValue>> CommentValues;
			TArray<TSharedPtr<FJsonValue>> VariableValues;
			TArray<TSharedPtr<FJsonValue>> ComponentValues;
			TArray<TSharedPtr<FJsonValue>> CustomEventValues;
			TArray<TSharedPtr<FJsonValue>> ImplementedInterfaceValues;
			TArray<TSharedPtr<FJsonValue>> InterfaceImplementationValues;
			TArray<TSharedPtr<FJsonValue>> FunctionValues;
			bool bCopiedAssetMetadata = false;

			TArray<UEdGraph*> Graphs;
			Blueprint->GetAllGraphs(Graphs);
			for (UEdGraph* Graph : Graphs)
			{
				if (!Graph)
				{
					continue;
				}

				TSharedPtr<FJsonObject> ReadResult = ReadBlueprintGraph(Blueprint, Graph);
				const TSharedPtr<FJsonObject>* ParsedGraphPtr = nullptr;
				TSharedPtr<FJsonObject> ParsedGraph;
				if (ReadResult.IsValid() && ReadResult->TryGetObjectField(TEXT("parsed_graph"), ParsedGraphPtr) && ParsedGraphPtr)
				{
					ParsedGraph = *ParsedGraphPtr;
				}
				if (!ParsedGraph.IsValid())
				{
					TSharedPtr<FJsonObject> Failure = MakeShared<FJsonObject>();
					Failure->SetStringField(TEXT("asset_path"), AssetPath);
					Failure->SetStringField(TEXT("asset_name"), AssetName);
					Failure->SetStringField(TEXT("graph_name"), Graph->GetName());
					Failure->SetStringField(TEXT("error"), TEXT("Could not read parsed graph data."));
					BlueprintSearchCacheFailures.Add(MakeShared<FJsonValueObject>(Failure));
					continue;
				}

				auto AppendGraphArray = [this, Graph, &AssetPath, &AssetName](const TSharedPtr<FJsonObject>& Source, const FString& FieldName, TArray<TSharedPtr<FJsonValue>>& Destination)
				{
					const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
					if (!Source.IsValid() || !Source->TryGetArrayField(FieldName, Values) || !Values)
					{
						return;
					}
					for (const TSharedPtr<FJsonValue>& Value : *Values)
					{
						TSharedPtr<FJsonObject> Object = Value.IsValid() ? Value->AsObject() : nullptr;
						if (!Object.IsValid())
						{
							continue;
						}
						TSharedPtr<FJsonObject> Copy = CloneJsonObject(Object);
						Copy->SetStringField(TEXT("asset_path"), AssetPath);
						Copy->SetStringField(TEXT("asset_name"), AssetName);
						Copy->SetStringField(TEXT("graph_name"), Graph->GetName());
						Destination.Add(MakeShared<FJsonValueObject>(Copy));
					}
				};

				AppendGraphArray(ParsedGraph, TEXT("nodes"), NodeValues);
				AppendGraphArray(ParsedGraph, TEXT("comments"), CommentValues);
				AppendGraphArray(ParsedGraph, TEXT("custom_events"), CustomEventValues);
				AppendGraphArray(ParsedGraph, TEXT("interface_implementations"), InterfaceImplementationValues);
				if (!bCopiedAssetMetadata)
				{
					AppendGraphArray(ParsedGraph, TEXT("variables"), VariableValues);
					AppendGraphArray(ParsedGraph, TEXT("components"), ComponentValues);
					AppendGraphArray(ParsedGraph, TEXT("implemented_interfaces"), ImplementedInterfaceValues);
					AppendGraphArray(ParsedGraph, TEXT("functions"), FunctionValues);
					bCopiedAssetMetadata = true;
				}
			}

			Summary->SetArrayField(TEXT("graphs"), GraphValues);
			Summary->SetArrayField(TEXT("nodes"), NodeValues);
			Summary->SetArrayField(TEXT("comments"), CommentValues);
			Summary->SetArrayField(TEXT("variables"), VariableValues);
			Summary->SetArrayField(TEXT("components"), ComponentValues);
			Summary->SetArrayField(TEXT("custom_events"), CustomEventValues);
			Summary->SetArrayField(TEXT("implemented_interfaces"), ImplementedInterfaceValues);
			Summary->SetArrayField(TEXT("interface_implementations"), InterfaceImplementationValues);
			Summary->SetArrayField(TEXT("functions"), FunctionValues);
			BlueprintSearchCache.Add(MakeShared<FJsonValueObject>(Summary));
		}

		TArray<FAssetData> AllAssets;
		AssetRegistryModule.Get().GetAssets(AllAssetFilter, AllAssets);
		AllAssets.Sort([](const FAssetData& A, const FAssetData& B)
		{
			return A.PackageName.ToString() < B.PackageName.ToString();
		});

		for (const FAssetData& Asset : AllAssets)
		{
			const FString AssetClassPath = Asset.AssetClassPath.ToString();
			const FString AssetDomain = DruidsMCPBridge::GetSearchAssetDomainFromClassPath(AssetClassPath);
			if (AssetDomain == TEXT("blueprint"))
			{
				continue;
			}

			const FString AssetPath = Asset.PackageName.ToString();
			const FString AssetName = Asset.AssetName.ToString();
			TSharedPtr<FJsonObject> Summary = MakeShared<FJsonObject>();
			Summary->SetStringField(TEXT("asset_path"), AssetPath);
			Summary->SetStringField(TEXT("object_path"), Asset.GetSoftObjectPath().ToString());
			Summary->SetStringField(TEXT("asset_name"), AssetName);
			Summary->SetStringField(TEXT("class"), AssetClassPath);
			Summary->SetStringField(TEXT("asset_registry_class_path"), AssetClassPath);
			Summary->SetStringField(TEXT("asset_domain"), AssetDomain);
			Summary->SetStringField(TEXT("inspectability"), TEXT("shallow_asset_metadata"));

			TSharedPtr<FJsonObject> InspectArgs = DruidsMCPBridge::MakeInspectAssetArguments(AssetPath);
			Summary->SetObjectField(TEXT("inspect_asset_arguments"), InspectArgs);

			TSharedPtr<FJsonObject> Tags = MakeShared<FJsonObject>();
			TArray<FString> TagFragments;
			for (const TPair<FName, FAssetTagValueRef>& Pair : Asset.TagsAndValues)
			{
				const FString Key = Pair.Key.ToString();
				const FString Value = Pair.Value.AsString();
				Tags->SetStringField(Key, Value);
				if (!Value.IsEmpty())
				{
					TagFragments.Add(FString::Printf(TEXT("%s=%s"), *Key, *Value));
				}
			}
			Summary->SetObjectField(TEXT("asset_registry_tags"), Tags);
			Summary->SetStringField(TEXT("asset_registry_search_text"), FString::Join(TagFragments, TEXT(" ")));

			if (DruidsMCPBridge::IsSpecializedSearchAssetDomain(AssetDomain))
			{
				UObject* AssetObject = LoadObject<UObject>(nullptr, *Asset.GetSoftObjectPath().ToString());
				if (AssetObject)
				{
					TSharedPtr<FJsonObject> SpecializedInspection;
					if (AssetDomain == TEXT("state_tree") && DruidsMCPBridge::IsStateTreeObject(AssetObject))
					{
						SpecializedInspection = DruidsMCPBridge::ExportStateTreeInspection(AssetObject);
						Summary->SetStringField(TEXT("specialized_reader"), TEXT("state_tree"));
					}
					else if (AssetDomain == TEXT("gameplay_camera") && DruidsMCPBridge::IsGameplayCameraObject(AssetObject))
					{
						SpecializedInspection = DruidsMCPBridge::ExportGameplayCameraInspection(AssetObject);
						Summary->SetStringField(TEXT("specialized_reader"), TEXT("gameplay_cameras"));
					}
					else if (AssetDomain == TEXT("material") && DruidsMCPBridge::IsMaterialObject(AssetObject))
					{
						SpecializedInspection = DruidsMCPBridge::ExportMaterialInspection(AssetObject);
						Summary->SetStringField(TEXT("specialized_reader"), TEXT("material"));
					}

					if (SpecializedInspection.IsValid())
					{
						Summary->SetStringField(TEXT("inspectability"), TEXT("deep_specialized_asset"));
						Summary->SetObjectField(TEXT("specialized_inspection"), SpecializedInspection);
						Summary->SetStringField(TEXT("specialized_search_text"), JsonObjectToString(SpecializedInspection));
					}
				}
				else
				{
					TSharedPtr<FJsonObject> Failure = MakeShared<FJsonObject>();
					Failure->SetStringField(TEXT("asset_path"), AssetPath);
					Failure->SetStringField(TEXT("asset_name"), AssetName);
					Failure->SetStringField(TEXT("asset_domain"), AssetDomain);
					Failure->SetStringField(TEXT("error"), TEXT("Could not load specialized asset for deep project search indexing."));
					BlueprintSearchCacheFailures.Add(MakeShared<FJsonValueObject>(Failure));
				}
			}

			BlueprintSearchCache.Add(MakeShared<FJsonValueObject>(Summary));
		}

		bBlueprintSearchCacheValid = true;
	}

	const FString NormalizedQuery = DruidsMCPBridge::NormalizeSearchText(Query);
	TArray<TSharedPtr<FJsonValue>> Results;
	auto SearchObjectStringFields = [&Query, &NormalizedQuery](const TSharedPtr<FJsonObject>& Object, const TArray<FString>& Fields, int32 ExactScore, int32 SubstringScore, FString& OutMatchedText, FString& OutReason) -> int32
	{
		int32 BestScore = 0;
		for (const FString& Field : Fields)
		{
			const FString Text = DruidsMCPBridge::TryGetJsonStringField(Object, Field);
			FString Reason;
			const int32 Score = DruidsMCPBridge::ScoreSearchText(Text, Query, NormalizedQuery, ExactScore, SubstringScore, Reason);
			if (Score > BestScore)
			{
				BestScore = Score;
				OutMatchedText = Text;
				OutReason = Field + TEXT(":") + Reason;
			}
		}
		return BestScore;
	};

	for (const TSharedPtr<FJsonValue>& AssetValue : BlueprintSearchCache)
	{
		TSharedPtr<FJsonObject> AssetSummary = AssetValue.IsValid() ? AssetValue->AsObject() : nullptr;
		if (!AssetSummary.IsValid())
		{
			continue;
		}

		if (DruidsMCPBridge::HasBlueprintSearchTarget(Targets, TEXT("assets")))
		{
			FString MatchedText;
			FString Reason;
			const int32 Score = SearchObjectStringFields(AssetSummary, { TEXT("asset_path"), TEXT("asset_name"), TEXT("object_path"), TEXT("parent_class"), TEXT("class"), TEXT("asset_registry_class_path"), TEXT("asset_domain"), TEXT("asset_registry_search_text") }, 620, 360, MatchedText, Reason);
			DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("assets"), MatchedText, Reason, Score);
		}

		if (DruidsMCPBridge::HasBlueprintSearchTarget(Targets, TEXT("metadata")))
		{
			FString MatchedText;
			FString Reason;
			const int32 SpecializedScore = SearchObjectStringFields(AssetSummary, { TEXT("specialized_search_text") }, 760, 540, MatchedText, Reason);
			if (SpecializedScore > 0)
			{
				const TSharedPtr<FJsonObject>* SpecializedInspection = nullptr;
				TSharedPtr<FJsonObject> MetadataObject;
				if (AssetSummary->TryGetObjectField(TEXT("specialized_inspection"), SpecializedInspection) && SpecializedInspection)
				{
					MetadataObject = *SpecializedInspection;
				}
				DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("metadata"), DruidsMCPBridge::MakeSpecializedSearchMatchedText(Query, AssetSummary), TEXT("specialized_asset:") + Reason, SpecializedScore, TEXT(""), nullptr, MetadataObject);
			}
		}

		if (DruidsMCPBridge::HasBlueprintSearchTarget(Targets, TEXT("graphs")))
		{
			const TArray<TSharedPtr<FJsonValue>>* Graphs = nullptr;
			if (AssetSummary->TryGetArrayField(TEXT("graphs"), Graphs) && Graphs)
			{
				for (const TSharedPtr<FJsonValue>& GraphValue : *Graphs)
				{
					TSharedPtr<FJsonObject> GraphObject = GraphValue.IsValid() ? GraphValue->AsObject() : nullptr;
					FString MatchedText;
					FString Reason;
					const int32 Score = SearchObjectStringFields(GraphObject, { TEXT("name"), TEXT("graph_type") }, 760, 560, MatchedText, Reason);
					DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("graphs"), MatchedText, Reason, Score, DruidsMCPBridge::TryGetJsonStringField(GraphObject, TEXT("name")), nullptr, GraphObject);

					const TArray<TSharedPtr<FJsonValue>>* EventNames = nullptr;
					if (GraphObject.IsValid() && GraphObject->TryGetArrayField(TEXT("event_names"), EventNames) && EventNames)
					{
						for (const TSharedPtr<FJsonValue>& EventNameValue : *EventNames)
						{
							FString EventName;
							if (!EventNameValue.IsValid() || !EventNameValue->TryGetString(EventName))
							{
								continue;
							}

							FString EventReason;
							const int32 EventScore = DruidsMCPBridge::ScoreSearchText(EventName, Query, NormalizedQuery, 790, 590, EventReason);
							DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("graphs"), EventName, TEXT("event_names:") + EventReason, EventScore, DruidsMCPBridge::TryGetJsonStringField(GraphObject, TEXT("name")), nullptr, GraphObject);
						}
					}
				}
			}
		}

		const TArray<TSharedPtr<FJsonValue>>* Nodes = nullptr;
		if (AssetSummary->TryGetArrayField(TEXT("nodes"), Nodes) && Nodes)
		{
			for (const TSharedPtr<FJsonValue>& NodeValue : *Nodes)
			{
				TSharedPtr<FJsonObject> NodeObject = NodeValue.IsValid() ? NodeValue->AsObject() : nullptr;
				if (!NodeObject.IsValid())
				{
					continue;
				}
				const FString GraphName = DruidsMCPBridge::TryGetJsonStringField(NodeObject, TEXT("graph"));
				if (DruidsMCPBridge::HasBlueprintSearchTarget(Targets, TEXT("nodes")))
				{
					FString MatchedText;
					FString Reason;
					const int32 Score = SearchObjectStringFields(NodeObject, { TEXT("title"), TEXT("node_kind"), TEXT("node_class"), TEXT("spawner_id"), TEXT("function_name") }, 820, 620, MatchedText, Reason);
					DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("nodes"), MatchedText, Reason, Score, GraphName, NodeObject);
				}

				if (DruidsMCPBridge::HasBlueprintSearchTarget(Targets, TEXT("metadata")))
				{
					for (const FString& MetadataField : TArray<FString>{ TEXT("spawner_id"), TEXT("function_reference"), TEXT("variable_reference"), TEXT("cast_reference"), TEXT("macro_reference"), TEXT("input_event"), TEXT("custom_event"), TEXT("event_reference"), TEXT("interface_implementation") })
					{
						FString MetadataText;
						TSharedPtr<FJsonObject> MetadataObject;
						if (MetadataField == TEXT("spawner_id"))
						{
							MetadataText = DruidsMCPBridge::TryGetJsonStringField(NodeObject, TEXT("spawner_id"));
						}
						else
						{
							const TSharedPtr<FJsonObject>* FoundMetadata = nullptr;
							if (NodeObject->TryGetObjectField(MetadataField, FoundMetadata) && FoundMetadata)
							{
								MetadataObject = *FoundMetadata;
								MetadataText = JsonObjectToString(MetadataObject);
							}
						}

						FString Reason;
						const int32 Score = MetadataField == TEXT("spawner_id")
							? DruidsMCPBridge::ScoreSearchText(MetadataText, Query, NormalizedQuery, 1000, 760, Reason)
							: DruidsMCPBridge::ScoreSearchText(MetadataText, Query, NormalizedQuery, 930, 700, Reason);
						if (Score > 0)
						{
							DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("metadata"), MetadataText, MetadataField + TEXT(":") + Reason, Score, GraphName, NodeObject, MetadataObject);
						}
					}
				}
			}
		}

		if (DruidsMCPBridge::HasBlueprintSearchTarget(Targets, TEXT("comments")))
		{
			const TArray<TSharedPtr<FJsonValue>>* Comments = nullptr;
			if (AssetSummary->TryGetArrayField(TEXT("comments"), Comments) && Comments)
			{
				for (const TSharedPtr<FJsonValue>& CommentValue : *Comments)
				{
					TSharedPtr<FJsonObject> CommentObject = CommentValue.IsValid() ? CommentValue->AsObject() : nullptr;
					FString MatchedText;
					FString Reason;
					const int32 Score = SearchObjectStringFields(CommentObject, { TEXT("text"), TEXT("node_guid") }, 840, 660, MatchedText, Reason);
					if (Score <= 0)
					{
						continue;
					}

					TSharedPtr<FJsonObject> CommentMetadata = CloneJsonObject(CommentObject);
					TSet<FString> ContainedNodeGuids;
					const TArray<TSharedPtr<FJsonValue>>* ContainedGuidValues = nullptr;
					if (CommentMetadata->TryGetArrayField(TEXT("contained_node_guids"), ContainedGuidValues) && ContainedGuidValues)
					{
						for (const TSharedPtr<FJsonValue>& GuidValue : *ContainedGuidValues)
						{
							FString Guid;
							if (GuidValue.IsValid() && GuidValue->TryGetString(Guid) && !Guid.IsEmpty())
							{
								ContainedNodeGuids.Add(Guid);
							}
						}
					}

					TArray<TSharedPtr<FJsonValue>> ContainedNodeSummaries;
					const TArray<TSharedPtr<FJsonValue>>* AssetNodes = nullptr;
					if (ContainedNodeGuids.Num() > 0 && AssetSummary->TryGetArrayField(TEXT("nodes"), AssetNodes) && AssetNodes)
					{
						const FString CommentGraphName = DruidsMCPBridge::TryGetJsonStringField(CommentObject, TEXT("graph_name"));
						for (const TSharedPtr<FJsonValue>& NodeValue : *AssetNodes)
						{
							TSharedPtr<FJsonObject> NodeObject = NodeValue.IsValid() ? NodeValue->AsObject() : nullptr;
							const FString NodeGuid = DruidsMCPBridge::TryGetJsonStringField(NodeObject, TEXT("node_guid"));
							if (!NodeObject.IsValid() || !ContainedNodeGuids.Contains(NodeGuid))
							{
								continue;
							}
							if (!CommentGraphName.IsEmpty() && !DruidsMCPBridge::TryGetJsonStringField(NodeObject, TEXT("graph")).Equals(CommentGraphName, ESearchCase::IgnoreCase))
							{
								continue;
							}

							TSharedPtr<FJsonObject> NodeSummary = MakeShared<FJsonObject>();
							NodeSummary->SetStringField(TEXT("node_guid"), NodeGuid);
							NodeSummary->SetStringField(TEXT("node_title"), DruidsMCPBridge::TryGetJsonStringField(NodeObject, TEXT("title")));
							NodeSummary->SetStringField(TEXT("node_kind"), DruidsMCPBridge::TryGetJsonStringField(NodeObject, TEXT("node_kind")));
							NodeSummary->SetStringField(TEXT("spawner_id"), DruidsMCPBridge::TryGetJsonStringField(NodeObject, TEXT("spawner_id")));
							NodeSummary->SetStringField(TEXT("graph_name"), DruidsMCPBridge::TryGetJsonStringField(NodeObject, TEXT("graph")));
							ContainedNodeSummaries.Add(MakeShared<FJsonValueObject>(NodeSummary));
						}
					}
					CommentMetadata->SetArrayField(TEXT("contained_nodes"), ContainedNodeSummaries);
					CommentMetadata->SetNumberField(TEXT("contained_node_count"), ContainedNodeSummaries.Num());
					DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("comments"), MatchedText, Reason, Score, DruidsMCPBridge::TryGetJsonStringField(CommentObject, TEXT("graph_name")), nullptr, CommentMetadata);
				}
			}
		}

		if (DruidsMCPBridge::HasBlueprintSearchTarget(Targets, TEXT("variables")))
		{
			const TArray<TSharedPtr<FJsonValue>>* Variables = nullptr;
			if (AssetSummary->TryGetArrayField(TEXT("variables"), Variables) && Variables)
			{
				for (const TSharedPtr<FJsonValue>& VariableValue : *Variables)
				{
					TSharedPtr<FJsonObject> VariableObject = VariableValue.IsValid() ? VariableValue->AsObject() : nullptr;
					FString MatchedText;
					FString Reason;
					const int32 Score = SearchObjectStringFields(VariableObject, { TEXT("name"), TEXT("guid"), TEXT("category"), TEXT("default_value"), TEXT("class_default_value"), TEXT("pin_category"), TEXT("pin_subcategory"), TEXT("pin_subcategory_object"), TEXT("container_type"), TEXT("type_string") }, 800, 620, MatchedText, Reason);
					DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("variables"), MatchedText, Reason, Score, TEXT(""), nullptr, VariableObject);
				}
			}
		}

		if (DruidsMCPBridge::HasBlueprintSearchTarget(Targets, TEXT("functions")))
		{
			const TArray<TSharedPtr<FJsonValue>>* Functions = nullptr;
			if (AssetSummary->TryGetArrayField(TEXT("functions"), Functions) && Functions)
			{
				for (const TSharedPtr<FJsonValue>& FunctionValue : *Functions)
				{
					TSharedPtr<FJsonObject> FunctionObject = FunctionValue.IsValid() ? FunctionValue->AsObject() : nullptr;
					FString MatchedText;
					FString Reason;
					const int32 Score = SearchObjectStringFields(FunctionObject, { TEXT("name"), TEXT("graph"), TEXT("spawner_id") }, 830, 640, MatchedText, Reason);
					DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("functions"), MatchedText, Reason, Score, DruidsMCPBridge::TryGetJsonStringField(FunctionObject, TEXT("graph")), nullptr, FunctionObject);
				}
			}
			const TArray<TSharedPtr<FJsonValue>>* CustomEvents = nullptr;
			if (AssetSummary->TryGetArrayField(TEXT("custom_events"), CustomEvents) && CustomEvents)
			{
				for (const TSharedPtr<FJsonValue>& CustomEventValue : *CustomEvents)
				{
					TSharedPtr<FJsonObject> CustomEventObject = CustomEventValue.IsValid() ? CustomEventValue->AsObject() : nullptr;
					FString MatchedText;
					FString Reason;
					const int32 Score = SearchObjectStringFields(CustomEventObject, { TEXT("name"), TEXT("graph"), TEXT("spawner_id") }, 810, 630, MatchedText, Reason);
					DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("functions"), MatchedText, Reason, Score, DruidsMCPBridge::TryGetJsonStringField(CustomEventObject, TEXT("graph")), nullptr, CustomEventObject);
				}
			}
			const TArray<TSharedPtr<FJsonValue>>* InterfaceImplementations = nullptr;
			if (AssetSummary->TryGetArrayField(TEXT("interface_implementations"), InterfaceImplementations) && InterfaceImplementations)
			{
				for (const TSharedPtr<FJsonValue>& InterfaceImplementationValue : *InterfaceImplementations)
				{
					TSharedPtr<FJsonObject> InterfaceImplementationObject = InterfaceImplementationValue.IsValid() ? InterfaceImplementationValue->AsObject() : nullptr;
					FString MatchedText;
					FString Reason;
					const int32 Score = SearchObjectStringFields(InterfaceImplementationObject, { TEXT("function_name"), TEXT("graph"), TEXT("spawner_id"), TEXT("interface_class_name"), TEXT("interface_class_path"), TEXT("interface_blueprint_path"), TEXT("interface_asset_path") }, 850, 670, MatchedText, Reason);
					DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("functions"), MatchedText, Reason, Score, DruidsMCPBridge::TryGetJsonStringField(InterfaceImplementationObject, TEXT("graph")), nullptr, InterfaceImplementationObject);
				}
			}
		}

		if (DruidsMCPBridge::HasBlueprintSearchTarget(Targets, TEXT("components")))
		{
			const TArray<TSharedPtr<FJsonValue>>* Components = nullptr;
			if (AssetSummary->TryGetArrayField(TEXT("components"), Components) && Components)
			{
				for (const TSharedPtr<FJsonValue>& ComponentValue : *Components)
				{
					TSharedPtr<FJsonObject> ComponentObject = ComponentValue.IsValid() ? ComponentValue->AsObject() : nullptr;
					FString MatchedText;
					FString Reason;
					const int32 Score = SearchObjectStringFields(ComponentObject, { TEXT("name"), TEXT("component_class"), TEXT("template_path"), TEXT("parent_component") }, 800, 620, MatchedText, Reason);
					DruidsMCPBridge::AddBlueprintSearchResult(Results, AssetSummary, TEXT("components"), MatchedText, Reason, Score, TEXT(""), nullptr, ComponentObject);
				}
			}
		}
	}

	for (const TSharedPtr<FJsonValue>& ResultValue : Results)
	{
		TSharedPtr<FJsonObject> ResultObject = ResultValue.IsValid() ? ResultValue->AsObject() : nullptr;
		if (!ResultObject.IsValid())
		{
			continue;
		}

		const FString MatchTarget = DruidsMCPBridge::TryGetJsonStringField(ResultObject, TEXT("match_target"));
		const FString Inspectability = DruidsMCPBridge::TryGetJsonStringField(ResultObject, TEXT("inspectability"));
		const FString AssetDomain = DruidsMCPBridge::TryGetJsonStringField(ResultObject, TEXT("asset_domain"));
		const bool bHasFollowupTool = ResultObject->HasTypedField<EJson::Array>(TEXT("recommended_next_tools"));
		bool bHasComments = MatchTarget == TEXT("comments") || ResultObject->HasTypedField<EJson::Object>(TEXT("comment"));
		bool bHasDefaults = false;
		bool bHasConnectedFlow = false;

		const TSharedPtr<FJsonObject>* CommentObject = nullptr;
		if (ResultObject->TryGetObjectField(TEXT("comment"), CommentObject) && CommentObject && CommentObject->IsValid())
		{
			bHasComments = true;
			bHasConnectedFlow = GetOptionalIntParam(*CommentObject, TEXT("contained_node_count"), 0) > 1;
		}

		const TSharedPtr<FJsonObject>* VariableReference = nullptr;
		if (ResultObject->TryGetObjectField(TEXT("variable_reference"), VariableReference) && VariableReference && VariableReference->IsValid())
		{
			bHasDefaults = !DruidsMCPBridge::TryGetJsonStringField(*VariableReference, TEXT("default_value")).IsEmpty() ||
				!DruidsMCPBridge::TryGetJsonStringField(*VariableReference, TEXT("class_default_value")).IsEmpty();
		}

		int32 EvidenceCount = 1;
		if (bHasComments) { ++EvidenceCount; }
		if (bHasConnectedFlow) { ++EvidenceCount; }
		if (bHasDefaults) { ++EvidenceCount; }
		if (bHasFollowupTool) { ++EvidenceCount; }
		if (Inspectability == TEXT("deep_blueprint") || Inspectability == TEXT("deep_specialized_asset")) { ++EvidenceCount; }

		int32 ExampleQuality = 0;
		if (Inspectability == TEXT("deep_blueprint")) { ExampleQuality += 40; }
		else if (Inspectability == TEXT("deep_specialized_asset")) { ExampleQuality += 34; }
		else if (Inspectability == TEXT("shallow_asset_metadata")) { ExampleQuality += 8; }
		if (MatchTarget == TEXT("comments")) { ExampleQuality += 28; }
		else if (MatchTarget == TEXT("functions")) { ExampleQuality += 26; }
		else if (MatchTarget == TEXT("nodes")) { ExampleQuality += 22; }
		else if (MatchTarget == TEXT("metadata")) { ExampleQuality += 20; }
		else if (MatchTarget == TEXT("variables") || MatchTarget == TEXT("components")) { ExampleQuality += 16; }
		if (bHasConnectedFlow) { ExampleQuality += 22; }
		if (bHasDefaults) { ExampleQuality += 12; }
		if (bHasFollowupTool) { ExampleQuality += 10; }
		ExampleQuality += FMath::Min(EvidenceCount * 4, 24);

		int32 IntentBoost = 0;
		if (Intent == TEXT("find_comment") && MatchTarget == TEXT("comments"))
		{
			IntentBoost = 220;
		}
		else if (Intent == TEXT("find_definition") && (MatchTarget == TEXT("functions") || MatchTarget == TEXT("variables") || MatchTarget == TEXT("components") || MatchTarget == TEXT("assets")))
		{
			IntentBoost = 180;
		}
		else if (Intent == TEXT("find_usage") && (MatchTarget == TEXT("nodes") || MatchTarget == TEXT("metadata")))
		{
			IntentBoost = 170;
		}
		else if (Intent == TEXT("find_asset_reference") && (MatchTarget == TEXT("assets") || MatchTarget == TEXT("metadata") || MatchTarget == TEXT("components")))
		{
			IntentBoost = 160;
		}
		else if (Intent == TEXT("system_overview") && (MatchTarget == TEXT("assets") || MatchTarget == TEXT("graphs")))
		{
			IntentBoost = 130;
		}
		else if (Intent == TEXT("find_example") && (Inspectability == TEXT("deep_blueprint") || Inspectability == TEXT("deep_specialized_asset")))
		{
			IntentBoost = 150;
			if (MatchTarget == TEXT("comments") || MatchTarget == TEXT("functions") || MatchTarget == TEXT("nodes"))
			{
				IntentBoost += 50;
			}
		}

		const double BaseScore = ResultObject->GetNumberField(TEXT("score"));
		ResultObject->SetNumberField(TEXT("example_quality"), ExampleQuality);
		ResultObject->SetNumberField(TEXT("evidence_count"), EvidenceCount);
		ResultObject->SetBoolField(TEXT("has_comments"), bHasComments);
		ResultObject->SetBoolField(TEXT("has_connected_flow"), bHasConnectedFlow);
		ResultObject->SetBoolField(TEXT("has_defaults"), bHasDefaults);
		ResultObject->SetBoolField(TEXT("has_followup_tool"), bHasFollowupTool);
		ResultObject->SetNumberField(TEXT("intent_boost"), IntentBoost);
		ResultObject->SetNumberField(TEXT("adjusted_score"), BaseScore + ExampleQuality + IntentBoost);
		if (DruidsMCPBridge::TryGetJsonStringField(ResultObject, TEXT("next_step")).IsEmpty() && AssetDomain == TEXT("blueprint"))
		{
			ResultObject->SetStringField(TEXT("next_step"), TEXT("Use the asset path, graph name, or node GUID from this result to inspect the matching Blueprint context."));
		}
	}

	Results.Sort([](const TSharedPtr<FJsonValue>& A, const TSharedPtr<FJsonValue>& B)
	{
		TSharedPtr<FJsonObject> AObject = A.IsValid() ? A->AsObject() : nullptr;
		TSharedPtr<FJsonObject> BObject = B.IsValid() ? B->AsObject() : nullptr;
		if (!AObject.IsValid() || !BObject.IsValid())
		{
			return AObject.IsValid();
		}
		const double AScore = AObject->HasField(TEXT("adjusted_score")) ? AObject->GetNumberField(TEXT("adjusted_score")) : AObject->GetNumberField(TEXT("score"));
		const double BScore = BObject->HasField(TEXT("adjusted_score")) ? BObject->GetNumberField(TEXT("adjusted_score")) : BObject->GetNumberField(TEXT("score"));
		if (!FMath::IsNearlyEqual(AScore, BScore))
		{
			return AScore > BScore;
		}
		const FString AKey = DruidsMCPBridge::TryGetJsonStringField(AObject, TEXT("asset_path")) + TEXT("|") +
			DruidsMCPBridge::TryGetJsonStringField(AObject, TEXT("graph_name")) + TEXT("|") +
			DruidsMCPBridge::TryGetJsonStringField(AObject, TEXT("node_guid")) + TEXT("|") +
			DruidsMCPBridge::TryGetJsonStringField(AObject, TEXT("match_target"));
		const FString BKey = DruidsMCPBridge::TryGetJsonStringField(BObject, TEXT("asset_path")) + TEXT("|") +
			DruidsMCPBridge::TryGetJsonStringField(BObject, TEXT("graph_name")) + TEXT("|") +
			DruidsMCPBridge::TryGetJsonStringField(BObject, TEXT("node_guid")) + TEXT("|") +
			DruidsMCPBridge::TryGetJsonStringField(BObject, TEXT("match_target"));
		return AKey < BKey;
	});

	const int32 RawResultCount = Results.Num();
	TArray<TSharedPtr<FJsonValue>> DedupedResults;
	TMap<FString, int32> ResultIndexByNodeKey;
	for (const TSharedPtr<FJsonValue>& ResultValue : Results)
	{
		TSharedPtr<FJsonObject> ResultObject = ResultValue.IsValid() ? ResultValue->AsObject() : nullptr;
		if (!ResultObject.IsValid())
		{
			continue;
		}

		const FString NodeGuid = DruidsMCPBridge::TryGetJsonStringField(ResultObject, TEXT("node_guid"));
		if (NodeGuid.IsEmpty())
		{
			DedupedResults.Add(ResultValue);
			continue;
		}

		const FString NodeKey = DruidsMCPBridge::TryGetJsonStringField(ResultObject, TEXT("asset_path")) + TEXT("|") +
			DruidsMCPBridge::TryGetJsonStringField(ResultObject, TEXT("graph_name")) + TEXT("|") +
			NodeGuid;
		int32* ExistingIndex = ResultIndexByNodeKey.Find(NodeKey);
		if (!ExistingIndex)
		{
			TArray<FString> InitialMatchTargets;
			AddUniqueString(InitialMatchTargets, DruidsMCPBridge::TryGetJsonStringField(ResultObject, TEXT("match_target")));
			ResultObject->SetArrayField(TEXT("deduped_match_targets"), MakeStringArrayValue(InitialMatchTargets));
			ResultObject->SetNumberField(TEXT("deduped_duplicate_count"), 0);
			ResultIndexByNodeKey.Add(NodeKey, DedupedResults.Num());
			DedupedResults.Add(ResultValue);
			continue;
		}

		TSharedPtr<FJsonObject> ExistingObject = DedupedResults[*ExistingIndex].IsValid() ? DedupedResults[*ExistingIndex]->AsObject() : nullptr;
		if (!ExistingObject.IsValid())
		{
			continue;
		}

		TArray<FString> MatchTargets;
		const TArray<TSharedPtr<FJsonValue>>* ExistingTargets = nullptr;
		if (ExistingObject->TryGetArrayField(TEXT("deduped_match_targets"), ExistingTargets) && ExistingTargets)
		{
			for (const TSharedPtr<FJsonValue>& TargetValue : *ExistingTargets)
			{
				FString Target;
				if (TargetValue.IsValid() && TargetValue->TryGetString(Target))
				{
					AddUniqueString(MatchTargets, Target);
				}
			}
		}
		AddUniqueString(MatchTargets, DruidsMCPBridge::TryGetJsonStringField(ExistingObject, TEXT("match_target")));
		AddUniqueString(MatchTargets, DruidsMCPBridge::TryGetJsonStringField(ResultObject, TEXT("match_target")));
		ExistingObject->SetArrayField(TEXT("deduped_match_targets"), MakeStringArrayValue(MatchTargets));
		ExistingObject->SetNumberField(TEXT("deduped_duplicate_count"), GetOptionalIntParam(ExistingObject, TEXT("deduped_duplicate_count"), 0) + 1);
	}
	Results = DedupedResults;

	if (Limit < Results.Num())
	{
		Results.SetNum(Limit);
	}

	TArray<TSharedPtr<FJsonValue>> TargetOutput;
	TArray<FString> SortedTargets = Targets.Array();
	SortedTargets.Sort();
	for (const FString& Target : SortedTargets)
	{
		if (Target != TEXT("all"))
		{
			TargetOutput.Add(MakeShared<FJsonValueString>(Target));
		}
	}
	if (TargetOutput.Num() == 0)
	{
		TargetOutput = MakeStringArrayValue(DefaultTargets);
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("query"), Query);
	Result->SetStringField(TEXT("intent"), Intent);
	Result->SetArrayField(TEXT("package_paths"), MakeStringArrayValue(PackagePaths));
	Result->SetArrayField(TEXT("targets"), TargetOutput);
	Result->SetNumberField(TEXT("result_count"), Results.Num());
	Result->SetNumberField(TEXT("raw_result_count"), RawResultCount);
	Result->SetNumberField(TEXT("deduped_result_count"), Results.Num());
	Result->SetArrayField(TEXT("results"), Results);
	Result->SetArrayField(TEXT("partial_failures"), BlueprintSearchCacheFailures);
	Result->SetBoolField(TEXT("cache_refreshed"), bCacheRefreshed);
	Result->SetNumberField(TEXT("cache_asset_count"), BlueprintSearchCache.Num());

	TSharedPtr<FJsonObject> SearchedCounts = MakeShared<FJsonObject>();
	TSharedPtr<FJsonObject> SkippedCounts = MakeShared<FJsonObject>();
	int32 BlueprintAssetCount = 0;
	int32 GenericAssetCount = 0;
	int32 StateTreeAssetCount = 0;
	int32 GameplayCameraAssetCount = 0;
	int32 MaterialAssetCount = 0;
	int32 DeepSpecializedCount = 0;
	for (const TSharedPtr<FJsonValue>& AssetValue : BlueprintSearchCache)
	{
		TSharedPtr<FJsonObject> AssetSummary = AssetValue.IsValid() ? AssetValue->AsObject() : nullptr;
		if (!AssetSummary.IsValid())
		{
			continue;
		}
		const FString AssetDomain = DruidsMCPBridge::TryGetJsonStringField(AssetSummary, TEXT("asset_domain"));
		const FString Inspectability = DruidsMCPBridge::TryGetJsonStringField(AssetSummary, TEXT("inspectability"));
		if (AssetDomain == TEXT("blueprint"))
		{
			++BlueprintAssetCount;
		}
		else if (AssetDomain == TEXT("state_tree"))
		{
			++StateTreeAssetCount;
		}
		else if (AssetDomain == TEXT("gameplay_camera"))
		{
			++GameplayCameraAssetCount;
		}
		else if (AssetDomain == TEXT("material"))
		{
			++MaterialAssetCount;
		}
		else
		{
			++GenericAssetCount;
		}
		if (Inspectability == TEXT("deep_specialized_asset"))
		{
			++DeepSpecializedCount;
		}
	}
	SearchedCounts->SetNumberField(TEXT("blueprint_assets"), BlueprintAssetCount);
	SearchedCounts->SetNumberField(TEXT("generic_uobject_assets"), GenericAssetCount);
	SearchedCounts->SetNumberField(TEXT("state_tree_assets"), StateTreeAssetCount);
	SearchedCounts->SetNumberField(TEXT("gameplay_camera_assets"), GameplayCameraAssetCount);
	SearchedCounts->SetNumberField(TEXT("material_assets"), MaterialAssetCount);
	SearchedCounts->SetNumberField(TEXT("deep_specialized_assets"), DeepSpecializedCount);
	SkippedCounts->SetNumberField(TEXT("generic_uobject_deep_inspection"), GenericAssetCount);

	TSharedPtr<FJsonObject> Coverage = MakeShared<FJsonObject>();
	Coverage->SetStringField(TEXT("blueprint_graphs"), TEXT("deep"));
	Coverage->SetStringField(TEXT("blueprint_comments"), TEXT("deep"));
	Coverage->SetStringField(TEXT("blueprint_variables_functions_components"), TEXT("deep"));
	Coverage->SetStringField(TEXT("generic_uobject_assets"), TEXT("shallow_asset_metadata"));
	Coverage->SetStringField(TEXT("state_tree_assets"), TEXT("deep_when_specialized_reader_available"));
	Coverage->SetStringField(TEXT("gameplay_camera_assets"), TEXT("deep_when_specialized_reader_available"));
	Coverage->SetStringField(TEXT("material_assets"), TEXT("deep_when_specialized_reader_available"));
	Coverage->SetObjectField(TEXT("searched_asset_counts"), SearchedCounts);
	Coverage->SetObjectField(TEXT("skipped_asset_counts"), SkippedCounts);
	Result->SetObjectField(TEXT("search_coverage"), Coverage);

	TSharedPtr<FJsonObject> FailureGroups = MakeShared<FJsonObject>();
	for (const TSharedPtr<FJsonValue>& FailureValue : BlueprintSearchCacheFailures)
	{
		TSharedPtr<FJsonObject> FailureObject = FailureValue.IsValid() ? FailureValue->AsObject() : nullptr;
		if (!FailureObject.IsValid())
		{
			continue;
		}
		FString Reason = DruidsMCPBridge::TryGetJsonStringField(FailureObject, TEXT("error"));
		if (Reason.IsEmpty())
		{
			Reason = DruidsMCPBridge::TryGetJsonStringField(FailureObject, TEXT("reason"));
		}
		if (Reason.IsEmpty())
		{
			Reason = TEXT("unknown");
		}
		const int32 Existing = GetOptionalIntParam(FailureGroups, Reason, 0);
		FailureGroups->SetNumberField(Reason, Existing + 1);
	}

	TSharedPtr<FJsonObject> Guidance = MakeShared<FJsonObject>();
	Guidance->SetStringField(TEXT("summary"), TEXT("Use result-level recommended_next_tools and arguments before falling back to filesystem search."));
	Guidance->SetObjectField(TEXT("searched_asset_counts"), SearchedCounts);
	Guidance->SetObjectField(TEXT("skipped_asset_counts"), SkippedCounts);
	Guidance->SetObjectField(TEXT("partial_failures_grouped_by_reason"), FailureGroups);
	Result->SetObjectField(TEXT("search_guidance"), Guidance);

	if (BlueprintSearchCache.Num() == 0 && BlueprintSearchCacheFailures.Num() > 0)
	{
		Result->SetStringField(TEXT("error"), TEXT("Blueprint search could not index any matching Blueprint assets."));
		return MakeToolJsonObjectResult(Result, true);
	}
	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteBlueprintRunRecipe(const TSharedPtr<FJsonObject>& Arguments)
{
	const TArray<TSharedPtr<FJsonValue>>* StepValues = nullptr;
	if (!Arguments->TryGetArrayField(TEXT("Steps"), StepValues) || !StepValues)
	{
		return MakeToolTextResult(TEXT("blueprint_run_recipe requires Steps array."), true);
	}

	const TSet<FString> AllowedTools = {
		TEXT("unreal_resolve_asset"),
		TEXT("unreal_inspect_asset"),
		TEXT("blueprint_create"),
		TEXT("blueprint_create_interface"),
		TEXT("blueprint_add_variable"),
		TEXT("blueprint_add_component"),
		TEXT("blueprint_add_component_motion"),
		TEXT("blueprint_create_function"),
		TEXT("blueprint_create_event_graph"),
		TEXT("blueprint_implement_interface"),
		TEXT("blueprint_add_interface_event"),
		TEXT("blueprint_add_custom_event"),
		TEXT("blueprint_add_comment"),
		TEXT("blueprint_read_graph"),
		TEXT("blueprint_compile"),
		TEXT("blueprint_find_node_spawners"),
		TEXT("blueprint_resolve_node_spawner"),
		TEXT("blueprint_add_node"),
		TEXT("blueprint_set_node_position"),
		TEXT("blueprint_connect_nodes"),
		TEXT("blueprint_add_node_by_search_and_connect"),
		TEXT("blueprint_search_project")
	};

	TMap<FString, TSharedPtr<FJsonObject>> StepOutputs;
	TArray<TSharedPtr<FJsonValue>> StepResults;
	bool bRecipePassed = true;
	FString FailureReason;
	int32 FailedStepIndex = INDEX_NONE;
	FString FailedStepId;
	FString FailedToolName;

	auto MakeContentText = [](const TSharedPtr<FJsonObject>& ToolResult) -> FString
	{
		TArray<FString> Texts;
		const TArray<TSharedPtr<FJsonValue>>* Content = nullptr;
		if (ToolResult.IsValid() && ToolResult->TryGetArrayField(TEXT("content"), Content) && Content)
		{
			for (const TSharedPtr<FJsonValue>& Value : *Content)
			{
				const TSharedPtr<FJsonObject> ContentObject = Value.IsValid() ? Value->AsObject() : nullptr;
				FString Text;
				if (ContentObject.IsValid() && ContentObject->TryGetStringField(TEXT("text"), Text))
				{
					Texts.Add(Text);
				}
			}
		}
		return FString::Join(Texts, TEXT("\n"));
	};

	TFunction<TSharedPtr<FJsonValue>(const TSharedPtr<FJsonObject>&, const FString&)> FindPathValue;
	FindPathValue = [&FindPathValue](const TSharedPtr<FJsonObject>& RootObject, const FString& Path) -> TSharedPtr<FJsonValue>
	{
		if (!RootObject.IsValid())
		{
			return nullptr;
		}
		if (Path.IsEmpty())
		{
			return MakeShared<FJsonValueObject>(FDruidsMCPBridgeServer::CloneJsonObject(RootObject));
		}

		TArray<FString> Parts;
		Path.ParseIntoArray(Parts, TEXT("."), true);
		TSharedPtr<FJsonValue> CurrentValue = MakeShared<FJsonValueObject>(RootObject);
		for (const FString& Part : Parts)
		{
			if (!CurrentValue.IsValid())
			{
				return nullptr;
			}

			if (CurrentValue->Type == EJson::Object)
			{
				const TSharedPtr<FJsonObject> CurrentObject = CurrentValue->AsObject();
				CurrentValue = CurrentObject.IsValid() ? CurrentObject->TryGetField(Part) : nullptr;
			}
			else if (CurrentValue->Type == EJson::Array && Part.IsNumeric())
			{
				const TArray<TSharedPtr<FJsonValue>>& Array = CurrentValue->AsArray();
				const int32 Index = FCString::Atoi(*Part);
				CurrentValue = Array.IsValidIndex(Index) ? Array[Index] : nullptr;
			}
			else
			{
				return nullptr;
			}
		}

		return CurrentValue.IsValid() ? FDruidsMCPBridgeServer::CloneJsonValue(CurrentValue) : nullptr;
	};

	TFunction<bool(const FString&, FString&)> ResolveReferenceString;
	ResolveReferenceString = [&StepOutputs, &FindPathValue](const FString& ReferenceString, FString& OutResolvedString) -> bool
	{
		const FString Reference = ReferenceString.TrimStartAndEnd();
		FString StepId;
		FString Path;
		if (!Reference.Split(TEXT("."), &StepId, &Path, ESearchCase::CaseSensitive, ESearchDir::FromStart))
		{
			StepId = Reference;
			Path.Empty();
		}

		if (StepId.IsEmpty() || !StepOutputs.Contains(StepId))
		{
			return false;
		}

		TSharedPtr<FJsonValue> Resolved = FindPathValue(StepOutputs[StepId], Path);
		if (!Resolved.IsValid())
		{
			return false;
		}

		if (Resolved->Type == EJson::String)
		{
			OutResolvedString = Resolved->AsString();
			return true;
		}
		if (Resolved->Type == EJson::Number)
		{
			OutResolvedString = FString::SanitizeFloat(Resolved->AsNumber());
			return true;
		}
		if (Resolved->Type == EJson::Boolean)
		{
			OutResolvedString = Resolved->AsBool() ? TEXT("true") : TEXT("false");
			return true;
		}
		if (Resolved->Type == EJson::Null)
		{
			OutResolvedString.Empty();
			return true;
		}

		return false;
	};

	TFunction<TSharedPtr<FJsonValue>(const TSharedPtr<FJsonValue>&, FString&)> ResolveValue;
	ResolveValue = [&ResolveValue, &StepOutputs, &FindPathValue, &ResolveReferenceString](const TSharedPtr<FJsonValue>& Value, FString& OutError) -> TSharedPtr<FJsonValue>
	{
		if (!Value.IsValid())
		{
			return MakeShared<FJsonValueNull>();
		}

		if (Value->Type == EJson::String)
		{
			const FString StringValue = Value->AsString();
			if (StringValue.Contains(TEXT("${")))
			{
				FString ResolvedString = StringValue;
				int32 SearchStart = 0;
				while (true)
				{
					const int32 OpenIndex = ResolvedString.Find(TEXT("${"), ESearchCase::CaseSensitive, ESearchDir::FromStart, SearchStart);
					if (OpenIndex == INDEX_NONE)
					{
						break;
					}

					const int32 ReferenceStart = OpenIndex + 2;
					const int32 CloseIndex = ResolvedString.Find(TEXT("}"), ESearchCase::CaseSensitive, ESearchDir::FromStart, ReferenceStart);
					if (CloseIndex == INDEX_NONE)
					{
						OutError = FString::Printf(TEXT("Could not resolve recipe reference in '%s': missing closing '}'."), *StringValue);
						return nullptr;
					}

					const FString Reference = ResolvedString.Mid(ReferenceStart, CloseIndex - ReferenceStart);
					FString Replacement;
					if (!ResolveReferenceString(Reference, Replacement))
					{
						OutError = FString::Printf(TEXT("Could not resolve recipe reference '${%s}' in '%s'."), *Reference, *StringValue);
						return nullptr;
					}

					ResolvedString = ResolvedString.Left(OpenIndex) + Replacement + ResolvedString.Mid(CloseIndex + 1);
					SearchStart = OpenIndex + Replacement.Len();
				}
				return MakeShared<FJsonValueString>(ResolvedString);
			}

			if (!StringValue.StartsWith(TEXT("$")) || StringValue.Len() < 2)
			{
				return FDruidsMCPBridgeServer::CloneJsonValue(Value);
			}

			const FString Reference = StringValue.RightChop(1);
			FString StepId;
			FString Path;
			if (!Reference.Split(TEXT("."), &StepId, &Path, ESearchCase::CaseSensitive, ESearchDir::FromStart))
			{
				StepId = Reference;
				Path.Empty();
			}

			if (StepId.IsEmpty() || !StepOutputs.Contains(StepId))
			{
				OutError = FString::Printf(TEXT("Could not resolve recipe reference '%s': unknown step id '%s'."), *StringValue, *StepId);
				return nullptr;
			}

			TSharedPtr<FJsonValue> Resolved = FindPathValue(StepOutputs[StepId], Path);
			if (!Resolved.IsValid())
			{
				OutError = FString::Printf(TEXT("Could not resolve recipe reference '%s': missing path '%s'."), *StringValue, *Path);
				return nullptr;
			}
			return Resolved;
		}

		if (Value->Type == EJson::Object)
		{
			const TSharedPtr<FJsonObject> SourceObject = Value->AsObject();
			TSharedPtr<FJsonObject> ResolvedObject = MakeShared<FJsonObject>();
			if (SourceObject.IsValid())
			{
				for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : SourceObject->Values)
				{
					TSharedPtr<FJsonValue> ResolvedField = ResolveValue(Pair.Value, OutError);
					if (!ResolvedField.IsValid())
					{
						return nullptr;
					}
					ResolvedObject->SetField(Pair.Key, ResolvedField);
				}
			}
			return MakeShared<FJsonValueObject>(ResolvedObject);
		}

		if (Value->Type == EJson::Array)
		{
			TArray<TSharedPtr<FJsonValue>> ResolvedArray;
			for (const TSharedPtr<FJsonValue>& Item : Value->AsArray())
			{
				TSharedPtr<FJsonValue> ResolvedItem = ResolveValue(Item, OutError);
				if (!ResolvedItem.IsValid())
				{
					return nullptr;
				}
				ResolvedArray.Add(ResolvedItem);
			}
			return MakeShared<FJsonValueArray>(ResolvedArray);
		}

		return FDruidsMCPBridgeServer::CloneJsonValue(Value);
	};

	for (int32 StepIndex = 0; StepIndex < StepValues->Num(); ++StepIndex)
	{
		const TSharedPtr<FJsonObject> StepObject = (*StepValues)[StepIndex].IsValid() ? (*StepValues)[StepIndex]->AsObject() : nullptr;
		if (!StepObject.IsValid())
		{
			bRecipePassed = false;
			FailedStepIndex = StepIndex;
			FailureReason = FString::Printf(TEXT("Recipe step %d must be an object."), StepIndex);
			break;
		}

		const FString StepId = GetOptionalStringParam(StepObject, TEXT("Id"), FString::Printf(TEXT("Step%d"), StepIndex + 1)).TrimStartAndEnd();
		FString ToolName = GetOptionalStringParam(StepObject, TEXT("ToolName")).TrimStartAndEnd();
		if (ToolName.IsEmpty())
		{
			ToolName = GetOptionalStringParam(StepObject, TEXT("Name")).TrimStartAndEnd();
		}

		TSharedPtr<FJsonObject> StepResult = MakeShared<FJsonObject>();
		StepResult->SetNumberField(TEXT("step_index"), StepIndex);
		StepResult->SetStringField(TEXT("id"), StepId);
		StepResult->SetStringField(TEXT("tool_name"), ToolName);

		if (StepId.IsEmpty())
		{
			bRecipePassed = false;
			FailedStepIndex = StepIndex;
			FailedToolName = ToolName;
			FailureReason = FString::Printf(TEXT("Recipe step %d has an empty Id."), StepIndex);
			StepResult->SetBoolField(TEXT("success"), false);
			StepResult->SetStringField(TEXT("error"), FailureReason);
			StepResults.Add(MakeShared<FJsonValueObject>(StepResult));
			break;
		}
		if (StepOutputs.Contains(StepId))
		{
			bRecipePassed = false;
			FailedStepIndex = StepIndex;
			FailedStepId = StepId;
			FailedToolName = ToolName;
			FailureReason = FString::Printf(TEXT("Duplicate recipe step Id: %s"), *StepId);
			StepResult->SetBoolField(TEXT("success"), false);
			StepResult->SetStringField(TEXT("error"), FailureReason);
			StepResults.Add(MakeShared<FJsonValueObject>(StepResult));
			break;
		}
		if (!AllowedTools.Contains(ToolName))
		{
			bRecipePassed = false;
			FailedStepIndex = StepIndex;
			FailedStepId = StepId;
			FailedToolName = ToolName;
			FailureReason = FString::Printf(TEXT("Unsupported recipe tool '%s' at step '%s'."), *ToolName, *StepId);
			StepResult->SetBoolField(TEXT("success"), false);
			StepResult->SetStringField(TEXT("error"), FailureReason);
			StepResults.Add(MakeShared<FJsonValueObject>(StepResult));
			break;
		}

		TSharedPtr<FJsonObject> RawStepArguments;
		TryGetObjectParam(StepObject, TEXT("Arguments"), RawStepArguments);
		if (!RawStepArguments.IsValid())
		{
			RawStepArguments = MakeShared<FJsonObject>();
		}

		FString ResolveError;
		TSharedPtr<FJsonValue> ResolvedArgumentValue = ResolveValue(MakeShared<FJsonValueObject>(RawStepArguments), ResolveError);
		TSharedPtr<FJsonObject> ResolvedArguments = ResolvedArgumentValue.IsValid() ? ResolvedArgumentValue->AsObject() : nullptr;
		if (!ResolvedArguments.IsValid())
		{
			bRecipePassed = false;
			FailedStepIndex = StepIndex;
			FailedStepId = StepId;
			FailedToolName = ToolName;
			FailureReason = ResolveError.IsEmpty() ? FString::Printf(TEXT("Could not resolve arguments for recipe step '%s'."), *StepId) : ResolveError;
			StepResult->SetBoolField(TEXT("success"), false);
			StepResult->SetStringField(TEXT("error"), FailureReason);
			StepResults.Add(MakeShared<FJsonValueObject>(StepResult));
			break;
		}

		StepResult->SetObjectField(TEXT("arguments"), CloneJsonObject(ResolvedArguments));
		TSharedPtr<FJsonObject> ToolResult = ExecuteTool(ToolName, ResolvedArguments);
		if (!ToolResult.IsValid())
		{
			bRecipePassed = false;
			FailedStepIndex = StepIndex;
			FailedStepId = StepId;
			FailedToolName = ToolName;
			FailureReason = FString::Printf(TEXT("Recipe tool '%s' returned no result at step '%s'."), *ToolName, *StepId);
			StepResult->SetBoolField(TEXT("success"), false);
			StepResult->SetStringField(TEXT("error"), FailureReason);
			StepResults.Add(MakeShared<FJsonValueObject>(StepResult));
			break;
		}

		bool bStepIsError = false;
		ToolResult->TryGetBoolField(TEXT("isError"), bStepIsError);
		TSharedPtr<FJsonObject> Structured = ExtractFirstObjectFromToolContent(ToolResult);
		if (!Structured.IsValid())
		{
			Structured = MakeShared<FJsonObject>();
		}

		StepOutputs.Add(StepId, Structured);
		StepResult->SetObjectField(TEXT("structuredContent"), Structured);
		const FString ContentText = MakeContentText(ToolResult);
		if (!ContentText.IsEmpty())
		{
			StepResult->SetStringField(TEXT("content_text"), ContentText);
		}
		StepResult->SetBoolField(TEXT("is_error"), bStepIsError);
		StepResult->SetBoolField(TEXT("success"), !bStepIsError);
		StepResults.Add(MakeShared<FJsonValueObject>(StepResult));

		if (bStepIsError)
		{
			bRecipePassed = false;
			FailedStepIndex = StepIndex;
			FailedStepId = StepId;
			FailedToolName = ToolName;
			FailureReason = ContentText.IsEmpty() ? FString::Printf(TEXT("Recipe step '%s' failed."), *StepId) : ContentText;
			break;
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("recipe_name"), GetOptionalStringParam(Arguments, TEXT("RecipeName")));
	Result->SetBoolField(TEXT("success"), bRecipePassed);
	Result->SetNumberField(TEXT("step_count"), StepValues->Num());
	Result->SetNumberField(TEXT("completed_step_count"), bRecipePassed ? StepValues->Num() : FMath::Max(0, StepResults.Num() - 1));
	Result->SetStringField(TEXT("mutation_policy"), TEXT("sequential_no_rollback"));
	Result->SetArrayField(TEXT("steps"), StepResults);
	if (!bRecipePassed)
	{
		Result->SetNumberField(TEXT("failed_step_index"), FailedStepIndex);
		Result->SetStringField(TEXT("failed_step_id"), FailedStepId);
		Result->SetStringField(TEXT("failed_tool_name"), FailedToolName);
		Result->SetStringField(TEXT("failure_reason"), FailureReason);
		Result->SetBoolField(TEXT("failed_step_mutation_unknown"), true);
	}

	return MakeToolJsonObjectResult(Result, !bRecipePassed);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteDataAssetFindClasses(const TSharedPtr<FJsonObject>& Arguments) const
{
	const FString Query = GetOptionalStringParam(Arguments, TEXT("Query")).TrimStartAndEnd();
	const bool bIncludeNative = DruidsMCPBridge::TryGetJsonBoolField(Arguments, TEXT("IncludeNative"), true);
	const bool bIncludeBlueprint = DruidsMCPBridge::TryGetJsonBoolField(Arguments, TEXT("IncludeBlueprint"), true);
	const bool bOnlyInstantiable = DruidsMCPBridge::TryGetJsonBoolField(Arguments, TEXT("OnlyInstantiable"), true);
	const int32 Limit = FMath::Max(1, GetOptionalIntParam(Arguments, TEXT("Limit"), 100));

	const TArray<FString> DefaultPackagePaths = { TEXT("/Game") };
	TArray<FString> PackagePaths = DruidsMCPBridge::ReadStringArrayField(Arguments, TEXT("PackagePaths"), DefaultPackagePaths);
	for (FString& PackagePath : PackagePaths)
	{
		PackagePath.TrimStartAndEndInline();
		if (PackagePath.EndsWith(TEXT("/")))
		{
			PackagePath.LeftChopInline(1);
		}
	}
	PackagePaths.RemoveAll([](const FString& PackagePath) { return PackagePath.IsEmpty(); });
	if (PackagePaths.Num() == 0)
	{
		PackagePaths = DefaultPackagePaths;
	}

	auto MatchesQuery = [&Query](const TArray<FString>& Fields)
	{
		if (Query.IsEmpty())
		{
			return true;
		}
		for (const FString& Field : Fields)
		{
			if (FDruidsMCPBridgeServer::TextMatches(Field, Query))
			{
				return true;
			}
		}
		return false;
	};

	auto PackageMatches = [&PackagePaths](const FString& PackageName)
	{
		for (const FString& PackagePath : PackagePaths)
		{
			if (PackageName.Equals(PackagePath, ESearchCase::IgnoreCase) || PackageName.StartsWith(PackagePath + TEXT("/"), ESearchCase::IgnoreCase))
			{
				return true;
			}
		}
		return false;
	};

	auto IsInstantiableClass = [](const UClass* Class)
	{
		return Class &&
			Class->IsChildOf(UDataAsset::StaticClass()) &&
			!Class->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists);
	};

	auto AddClassResult = [&](TArray<TSharedPtr<FJsonValue>>& Results, TSet<FString>& SeenClassPaths, UClass* Class, const FString& AssetPath, const FString& Source, bool bIsBlueprintClass)
	{
		if (!Class || !Class->IsChildOf(UDataAsset::StaticClass()))
		{
			return;
		}

		const bool bIsInstantiable = IsInstantiableClass(Class);
		if (bOnlyInstantiable && !bIsInstantiable)
		{
			return;
		}

		const FString ClassPath = Class->GetPathName();
		if (SeenClassPaths.Contains(ClassPath))
		{
			return;
		}

		const FString ClassName = Class->GetName();
		const FString DisplayName = Class->GetDisplayNameText().ToString();
		const FString PackageName = Class->GetOutermost() ? Class->GetOutermost()->GetName() : TEXT("");
		UClass* ParentClass = Class->GetSuperClass();
		const FString ParentClassPath = ParentClass ? ParentClass->GetPathName() : TEXT("");
		const FString ParentClassName = ParentClass ? ParentClass->GetName() : TEXT("");

		if (!MatchesQuery({ ClassName, DisplayName, ClassPath, AssetPath, PackageName, ParentClassName, ParentClassPath }))
		{
			return;
		}

		TSharedPtr<FJsonObject> Item = MakeShared<FJsonObject>();
		Item->SetStringField(TEXT("class_name"), ClassName);
		Item->SetStringField(TEXT("class_path"), ClassPath);
		Item->SetStringField(TEXT("asset_path"), AssetPath);
		Item->SetStringField(TEXT("display_name"), DisplayName);
		Item->SetStringField(TEXT("package_path"), PackageName);
		Item->SetStringField(TEXT("parent_class"), ParentClassName);
		Item->SetStringField(TEXT("parent_class_path"), ParentClassPath);
		Item->SetBoolField(TEXT("is_data_asset"), true);
		Item->SetBoolField(TEXT("is_primary_data_asset"), Class->IsChildOf(UPrimaryDataAsset::StaticClass()));
		Item->SetBoolField(TEXT("is_blueprint_class"), bIsBlueprintClass);
		Item->SetBoolField(TEXT("is_native_class"), !bIsBlueprintClass);
		Item->SetBoolField(TEXT("is_abstract"), Class->HasAnyClassFlags(CLASS_Abstract));
		Item->SetBoolField(TEXT("is_deprecated"), Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists));
		Item->SetBoolField(TEXT("is_instantiable"), bIsInstantiable);
		Item->SetStringField(TEXT("source"), Source);
		Item->SetStringField(TEXT("intended_asset_kind"), TEXT("data_asset_instance_class"));
		Item->SetStringField(TEXT("creation_warning"), TEXT("Use this class for a real Data Asset instance. Do not use blueprint_create; that creates a Blueprint Class child, not a Data Asset asset instance."));
		TArray<FString> RecommendedNextTools = { TEXT("unreal_inspect_asset") };
		Item->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue(RecommendedNextTools));

		SeenClassPaths.Add(ClassPath);
		Results.Add(MakeShared<FJsonValueObject>(Item));
	};

	TArray<TSharedPtr<FJsonValue>> Results;
	TArray<TSharedPtr<FJsonValue>> PartialFailures;
	TSet<FString> SeenClassPaths;

	if (bIncludeBlueprint)
	{
		FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
		FARFilter BlueprintFilter;
		BlueprintFilter.bRecursivePaths = true;
		BlueprintFilter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
		for (const FString& PackagePath : PackagePaths)
		{
			if (PackagePath.StartsWith(TEXT("/Game"), ESearchCase::IgnoreCase))
			{
				BlueprintFilter.PackagePaths.Add(*PackagePath);
			}
		}

		if (BlueprintFilter.PackagePaths.Num() > 0)
		{
			TArray<FAssetData> BlueprintAssets;
			AssetRegistryModule.Get().GetAssets(BlueprintFilter, BlueprintAssets);
			for (const FAssetData& AssetData : BlueprintAssets)
			{
				if (Results.Num() >= Limit)
				{
					break;
				}

				const FString AssetPath = AssetData.PackageName.ToString();
				if (!PackageMatches(AssetPath))
				{
					continue;
				}

				UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
				if (!Blueprint)
				{
					TSharedPtr<FJsonObject> Failure = MakeShared<FJsonObject>();
					Failure->SetStringField(TEXT("asset_path"), AssetPath);
					Failure->SetStringField(TEXT("reason"), TEXT("failed_to_load_blueprint_for_data_asset_class_discovery"));
					PartialFailures.Add(MakeShared<FJsonValueObject>(Failure));
					continue;
				}

				UClass* GeneratedClass = Blueprint->GeneratedClass ? Blueprint->GeneratedClass : Blueprint->SkeletonGeneratedClass;
				AddClassResult(Results, SeenClassPaths, GeneratedClass, AssetPath, TEXT("blueprint_generated_class"), true);
			}
		}
	}

	if (bIncludeNative)
	{
		for (TObjectIterator<UClass> It; It; ++It)
		{
			if (Results.Num() >= Limit)
			{
				break;
			}

			UClass* Class = *It;
			if (!Class || !Class->IsChildOf(UDataAsset::StaticClass()))
			{
				continue;
			}

			const FString PackageName = Class->GetOutermost() ? Class->GetOutermost()->GetName() : TEXT("");
			if (!PackageMatches(PackageName) && Query.IsEmpty())
			{
				continue;
			}

			AddClassResult(Results, SeenClassPaths, Class, PackageName, TEXT("native_class"), false);
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("tool"), TEXT("data_asset_find_classes"));
	Result->SetStringField(TEXT("query"), Query);
	Result->SetArrayField(TEXT("package_paths"), MakeStringArrayValue(PackagePaths));
	Result->SetBoolField(TEXT("include_native"), bIncludeNative);
	Result->SetBoolField(TEXT("include_blueprint"), bIncludeBlueprint);
	Result->SetBoolField(TEXT("only_instantiable"), bOnlyInstantiable);
	Result->SetNumberField(TEXT("result_count"), Results.Num());
	Result->SetArrayField(TEXT("results"), Results);
	Result->SetArrayField(TEXT("partial_failures"), PartialFailures);
	Result->SetStringField(TEXT("agent_guidance"), TEXT("These are Data Asset classes suitable for real Data Asset instance creation with data_asset_create. They are not asset instances themselves, and blueprint_create must not be used as a substitute."));
	TArray<FString> RecommendedNextTools = { TEXT("data_asset_create"), TEXT("unreal_inspect_asset") };
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue(RecommendedNextTools));
	TArray<FString> PlannedNextTools = { TEXT("data_asset_set_property") };
	Result->SetArrayField(TEXT("planned_next_tools"), MakeStringArrayValue(PlannedNextTools));

	return MakeToolJsonObjectResult(Result);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteDataAssetCreate(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString ClassPath = GetOptionalStringParam(Arguments, TEXT("ClassPath"), GetOptionalStringParam(Arguments, TEXT("AssetClassPath"))).TrimStartAndEnd();
	FString PackagePath = GetOptionalStringParam(Arguments, TEXT("PackagePath"), GetOptionalStringParam(Arguments, TEXT("InPackagePath"))).TrimStartAndEnd();
	const FString AssetName = GetOptionalStringParam(Arguments, TEXT("AssetName"), GetOptionalStringParam(Arguments, TEXT("InAssetName"))).TrimStartAndEnd();
	const bool bSave = DruidsMCPBridge::TryGetJsonBoolField(Arguments, TEXT("Save"), true);

	if (ClassPath.IsEmpty() || PackagePath.IsEmpty() || AssetName.IsEmpty())
	{
		return MakeToolTextResult(TEXT("data_asset_create requires ClassPath, PackagePath, and AssetName."), true);
	}
	if (!PackagePath.StartsWith(TEXT("/")))
	{
		return MakeToolTextResult(FString::Printf(TEXT("PackagePath must be a long package path such as /Game/Halcyon/DataAssets, got '%s'."), *PackagePath), true);
	}
	if (PackagePath.EndsWith(TEXT("/")))
	{
		PackagePath.LeftChopInline(1);
	}

	UClass* DataAssetClass = FindObject<UClass>(nullptr, *ClassPath);
	if (!DataAssetClass)
	{
		DataAssetClass = LoadObject<UClass>(nullptr, *ClassPath);
	}
	if (!DataAssetClass)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not load Data Asset class: %s. Call data_asset_find_classes first and pass its class_path."), *ClassPath), true);
	}
	if (!DataAssetClass->IsChildOf(UDataAsset::StaticClass()))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Class '%s' is not a UDataAsset/UPrimaryDataAsset class."), *DataAssetClass->GetPathName()), true);
	}
	if (DataAssetClass->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Class '%s' is not instantiable because it is abstract or deprecated."), *DataAssetClass->GetPathName()), true);
	}

	const FString PackageName = PackagePath / AssetName;
	UPackage* Package = CreatePackage(*PackageName);
	if (!Package)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create or load package: %s"), *PackageName), true);
	}
	if (StaticFindObject(UObject::StaticClass(), Package, *AssetName))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Asset already exists: %s.%s"), *PackageName, *AssetName), true);
	}

	UObject* CreatedAsset = NewObject<UObject>(Package, DataAssetClass, FName(*AssetName), RF_Public | RF_Standalone | RF_Transactional);
	if (!CreatedAsset)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not create Data Asset instance: %s.%s"), *PackageName, *AssetName), true);
	}

	FAssetRegistryModule::AssetCreated(CreatedAsset);
	CreatedAsset->MarkPackageDirty();
	Package->MarkPackageDirty();
	CreatedAsset->PostEditChange();

	bool bSaved = false;
	FString SaveError;
	if (bSave)
	{
		const FString PackageFilename = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
		FSavePackageArgs SaveArgs;
		SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
		SaveArgs.SaveFlags = SAVE_NoError;
		bSaved = UPackage::SavePackage(Package, CreatedAsset, *PackageFilename, SaveArgs);
		if (!bSaved)
		{
			SaveError = FString::Printf(TEXT("UPackage::SavePackage returned false for %s."), *PackageFilename);
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("tool"), TEXT("data_asset_create"));
	Result->SetStringField(TEXT("asset_path"), PackageName);
	Result->SetStringField(TEXT("object_path"), CreatedAsset->GetPathName());
	Result->SetStringField(TEXT("asset_name"), AssetName);
	Result->SetStringField(TEXT("package_path"), PackagePath);
	Result->SetStringField(TEXT("class_name"), DataAssetClass->GetName());
	Result->SetStringField(TEXT("class_path"), DataAssetClass->GetPathName());
	Result->SetBoolField(TEXT("is_data_asset_instance"), CreatedAsset->IsA(UDataAsset::StaticClass()));
	Result->SetBoolField(TEXT("is_primary_data_asset"), CreatedAsset->IsA(UPrimaryDataAsset::StaticClass()));
	Result->SetBoolField(TEXT("is_blueprint_asset"), CreatedAsset->IsA(UBlueprint::StaticClass()));
	Result->SetBoolField(TEXT("created_blueprint_class"), false);
	Result->SetBoolField(TEXT("saved"), bSave ? bSaved : false);
	Result->SetBoolField(TEXT("save_requested"), bSave);
	Result->SetStringField(TEXT("asset_kind"), TEXT("data_asset_instance"));
	Result->SetStringField(TEXT("property_editing"), TEXT("supported_for_top_level_and_nested_struct_properties"));
	if (!SaveError.IsEmpty())
	{
		Result->SetStringField(TEXT("save_error"), SaveError);
	}
	TArray<FString> RecommendedNextTools = {
		TEXT("unreal_inspect_asset")
	};
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue(RecommendedNextTools));
	Result->SetStringField(TEXT("next_step"), TEXT("Call data_asset_set_property for top-level or nested struct fields, then unreal_inspect_asset to verify."));

	return MakeToolJsonObjectResult(Result, bSave && !bSaved);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteDataAssetSetProperty(const TSharedPtr<FJsonObject>& Arguments)
{
	const FString AssetNameOrPath = GetOptionalStringParam(Arguments, TEXT("AssetNameOrPath"), GetOptionalStringParam(Arguments, TEXT("name_or_path"))).TrimStartAndEnd();
	const FString PropertyName = GetOptionalStringParam(Arguments, TEXT("PropertyName")).TrimStartAndEnd();
	const TSharedPtr<FJsonValue> ValueField = Arguments.IsValid() ? Arguments->TryGetField(TEXT("Value")) : nullptr;
	const bool bSave = DruidsMCPBridge::TryGetJsonBoolField(Arguments, TEXT("Save"), true);

	if (AssetNameOrPath.IsEmpty() || PropertyName.IsEmpty() || !ValueField.IsValid())
	{
		return MakeToolTextResult(TEXT("data_asset_set_property requires AssetNameOrPath, PropertyName, and Value."), true);
	}
	if (PropertyName.Contains(TEXT("[")) || PropertyName.Contains(TEXT("]")))
	{
		return MakeToolTextResult(TEXT("Data Asset property paths support nested structs only. Array/map indexing is not supported in this slice."), true);
	}

	FString AssetPath;
	FString Error;
	TSharedPtr<FJsonObject> ResolveResult = ResolveAssetToObject(AssetNameOrPath, TEXT("Any"), 10, AssetPath, Error);
	if (AssetPath.IsEmpty())
	{
		TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
		Result->SetStringField(TEXT("query"), AssetNameOrPath);
		Result->SetStringField(TEXT("error"), Error);
		if (ResolveResult.IsValid())
		{
			Result->SetObjectField(TEXT("resolve_result"), ResolveResult);
		}
		return MakeToolJsonObjectResult(Result, true);
	}

	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
	TArray<FAssetData> PackageAssets;
	AssetRegistryModule.Get().GetAssetsByPackageName(FName(*AssetPath), PackageAssets);
	const FString ObjectPath = PackageAssets.Num() > 0
		? PackageAssets[0].GetSoftObjectPath().ToString()
		: FString::Printf(TEXT("%s.%s"), *AssetPath, *FPackageName::GetShortName(AssetPath));

	UObject* AssetObject = LoadObject<UObject>(nullptr, *ObjectPath);
	if (!AssetObject)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not load Data Asset object: %s"), *ObjectPath), true);
	}
	if (!AssetObject->IsA(UDataAsset::StaticClass()))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Asset '%s' is not a Data Asset instance."), *AssetObject->GetPathName()), true);
	}
	if (AssetObject->IsA(UBlueprint::StaticClass()))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Asset '%s' is a Blueprint asset, not a real Data Asset instance."), *AssetObject->GetPathName()), true);
	}

	TArray<FString> PathParts;
	PropertyName.ParseIntoArray(PathParts, TEXT("."), true);
	PathParts.RemoveAll([](const FString& Part) { return Part.TrimStartAndEnd().IsEmpty(); });
	if (PathParts.Num() == 0)
	{
		return MakeToolTextResult(TEXT("PropertyName must include at least one property segment."), true);
	}

	UStruct* CurrentStruct = AssetObject->GetClass();
	void* CurrentContainer = AssetObject;
	FProperty* RootProperty = nullptr;
	FProperty* Property = nullptr;
	void* PropertyValuePtr = nullptr;
	TArray<FString> TraversedPath;
	for (int32 PartIndex = 0; PartIndex < PathParts.Num(); ++PartIndex)
	{
		FString Part = PathParts[PartIndex].TrimStartAndEnd();
		Property = CurrentStruct ? CurrentStruct->FindPropertyByName(FName(*Part)) : nullptr;
		if (!Property)
		{
			return MakeToolTextResult(FString::Printf(TEXT("Property path '%s' could not be resolved. Segment '%s' was not found on %s."), *PropertyName, *Part, CurrentStruct ? *CurrentStruct->GetName() : TEXT("<invalid>")), true);
		}
		if (!RootProperty)
		{
			RootProperty = Property;
		}

		PropertyValuePtr = Property->ContainerPtrToValuePtr<void>(CurrentContainer);
		TraversedPath.Add(Part);
		const bool bLastPart = PartIndex == PathParts.Num() - 1;
		if (!bLastPart)
		{
			FStructProperty* StructProperty = CastField<FStructProperty>(Property);
			if (!StructProperty || !StructProperty->Struct)
			{
				return MakeToolTextResult(FString::Printf(TEXT("Property path '%s' is unsupported. Segment '%s' is %s, not a reflected struct."), *PropertyName, *FString::Join(TraversedPath, TEXT(".")), Property ? *Property->GetCPPType() : TEXT("<invalid>")), true);
			}
			CurrentStruct = StructProperty->Struct;
			CurrentContainer = PropertyValuePtr;
		}
	}

	if (!Property || !PropertyValuePtr || !RootProperty)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Property path '%s' resolved to an invalid value."), *PropertyName), true);
	}
	if (!RootProperty->HasAnyPropertyFlags(CPF_Edit))
	{
		return MakeToolTextResult(FString::Printf(TEXT("Property path '%s' starts at '%s', which is not editor-editable."), *PropertyName, *RootProperty->GetName()), true);
	}

	FString BeforeValue;
	Property->ExportText_Direct(BeforeValue, PropertyValuePtr, nullptr, AssetObject, PPF_None);
	const FString ImportText = DruidsMCPBridge::JsonValueToPropertyImportText(ValueField);
	const TCHAR* ImportResult = Property->ImportText_Direct(*ImportText, PropertyValuePtr, AssetObject, PPF_None);
	if (!ImportResult)
	{
		return MakeToolTextResult(FString::Printf(TEXT("Could not import value '%s' for property path '%s'."), *ImportText, *PropertyName), true);
	}

	AssetObject->Modify();
	AssetObject->PostEditChange();
	AssetObject->MarkPackageDirty();
	UPackage* Package = AssetObject->GetOutermost();
	if (Package)
	{
		Package->MarkPackageDirty();
	}

	FString AfterValue;
	Property->ExportText_Direct(AfterValue, PropertyValuePtr, nullptr, AssetObject, PPF_None);

	bool bSaved = false;
	FString SaveError;
	if (bSave && Package)
	{
		const FString PackageFilename = FPackageName::LongPackageNameToFilename(Package->GetName(), FPackageName::GetAssetPackageExtension());
		FSavePackageArgs SaveArgs;
		SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
		SaveArgs.SaveFlags = SAVE_NoError;
		bSaved = UPackage::SavePackage(Package, AssetObject, *PackageFilename, SaveArgs);
		if (!bSaved)
		{
			SaveError = FString::Printf(TEXT("UPackage::SavePackage returned false for %s."), *PackageFilename);
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("tool"), TEXT("data_asset_set_property"));
	Result->SetStringField(TEXT("asset_path"), AssetPath);
	Result->SetStringField(TEXT("object_path"), AssetObject->GetPathName());
	Result->SetStringField(TEXT("class_name"), AssetObject->GetClass()->GetName());
	Result->SetStringField(TEXT("property_name"), PropertyName);
	Result->SetArrayField(TEXT("property_path"), MakeStringArrayValue(PathParts));
	Result->SetStringField(TEXT("property_cpp_type"), Property->GetCPPType());
	Result->SetStringField(TEXT("imported_value"), ImportText);
	Result->SetStringField(TEXT("before_value"), BeforeValue);
	Result->SetStringField(TEXT("after_value"), AfterValue);
	Result->SetBoolField(TEXT("is_data_asset_instance"), true);
	Result->SetBoolField(TEXT("is_primary_data_asset"), AssetObject->IsA(UPrimaryDataAsset::StaticClass()));
	Result->SetBoolField(TEXT("saved"), bSave ? bSaved : false);
	Result->SetBoolField(TEXT("save_requested"), bSave);
	Result->SetStringField(TEXT("scope"), PathParts.Num() > 1 ? TEXT("nested_struct_property") : TEXT("top_level_editable_property"));
	Result->SetBoolField(TEXT("nested_struct_property_supported"), true);
	Result->SetStringField(TEXT("array_map_indexing"), TEXT("not_supported_in_this_slice"));
	if (!SaveError.IsEmpty())
	{
		Result->SetStringField(TEXT("save_error"), SaveError);
	}
	TArray<FString> RecommendedNextTools = { TEXT("unreal_inspect_asset") };
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue(RecommendedNextTools));

	return MakeToolJsonObjectResult(Result, bSave && !bSaved);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExecuteDataAssetCreateBatch(const TSharedPtr<FJsonObject>& Arguments)
{
	const TArray<TSharedPtr<FJsonValue>>* ItemValues = nullptr;
	if (!Arguments.IsValid() || !Arguments->TryGetArrayField(TEXT("Items"), ItemValues) || !ItemValues)
	{
		return MakeToolTextResult(TEXT("data_asset_create_batch requires Items array."), true);
	}

	const FString DefaultClassPath = GetOptionalStringParam(Arguments, TEXT("DefaultClassPath")).TrimStartAndEnd();
	FString DefaultPackagePath = GetOptionalStringParam(Arguments, TEXT("DefaultPackagePath")).TrimStartAndEnd();
	const bool bSave = DruidsMCPBridge::TryGetJsonBoolField(Arguments, TEXT("Save"), true);
	const bool bContinueOnError = DruidsMCPBridge::TryGetJsonBoolField(Arguments, TEXT("ContinueOnError"), false);
	if (DefaultPackagePath.EndsWith(TEXT("/")))
	{
		DefaultPackagePath.LeftChopInline(1);
	}

	TArray<TSharedPtr<FJsonValue>> ItemResults;
	int32 CreatedCount = 0;
	int32 PropertySetCount = 0;
	int32 FailedCount = 0;
	bool bBatchPassed = true;

	auto ToolResultIsError = [](const TSharedPtr<FJsonObject>& ToolResult)
	{
		bool bIsError = false;
		return ToolResult.IsValid() && ToolResult->TryGetBoolField(TEXT("isError"), bIsError) && bIsError;
	};

	for (int32 ItemIndex = 0; ItemIndex < ItemValues->Num(); ++ItemIndex)
	{
		const TSharedPtr<FJsonObject> Item = (*ItemValues)[ItemIndex].IsValid() ? (*ItemValues)[ItemIndex]->AsObject() : nullptr;
		TSharedPtr<FJsonObject> ItemResult = MakeShared<FJsonObject>();
		ItemResult->SetNumberField(TEXT("index"), ItemIndex);
		ItemResults.Add(MakeShared<FJsonValueObject>(ItemResult));
		if (!Item.IsValid())
		{
			++FailedCount;
			bBatchPassed = false;
			ItemResult->SetBoolField(TEXT("success"), false);
			ItemResult->SetStringField(TEXT("error"), TEXT("Batch item must be an object."));
			if (!bContinueOnError)
			{
				break;
			}
			continue;
		}

		const FString ClassPath = GetOptionalStringParam(Item, TEXT("ClassPath"), DefaultClassPath).TrimStartAndEnd();
		FString PackagePath = GetOptionalStringParam(Item, TEXT("PackagePath"), DefaultPackagePath).TrimStartAndEnd();
		const FString AssetName = GetOptionalStringParam(Item, TEXT("AssetName")).TrimStartAndEnd();
		if (PackagePath.EndsWith(TEXT("/")))
		{
			PackagePath.LeftChopInline(1);
		}

		ItemResult->SetStringField(TEXT("asset_name"), AssetName);
		ItemResult->SetStringField(TEXT("class_path"), ClassPath);
		ItemResult->SetStringField(TEXT("package_path"), PackagePath);
		if (ClassPath.IsEmpty() || PackagePath.IsEmpty() || AssetName.IsEmpty())
		{
			++FailedCount;
			bBatchPassed = false;
			ItemResult->SetBoolField(TEXT("success"), false);
			ItemResult->SetStringField(TEXT("error"), TEXT("Each batch item requires AssetName plus ClassPath/PackagePath or matching defaults."));
			if (!bContinueOnError)
			{
				break;
			}
			continue;
		}

		TSharedPtr<FJsonObject> CreateArgs = MakeShared<FJsonObject>();
		CreateArgs->SetStringField(TEXT("ClassPath"), ClassPath);
		CreateArgs->SetStringField(TEXT("PackagePath"), PackagePath);
		CreateArgs->SetStringField(TEXT("AssetName"), AssetName);
		CreateArgs->SetBoolField(TEXT("Save"), bSave);
		TSharedPtr<FJsonObject> CreateToolResult = ExecuteDataAssetCreate(CreateArgs);
		TSharedPtr<FJsonObject> CreateResult = ExtractFirstObjectFromToolContent(CreateToolResult);
		const bool bCreateFailed = ToolResultIsError(CreateToolResult) || !CreateResult.IsValid();
		if (CreateResult.IsValid())
		{
			ItemResult->SetObjectField(TEXT("create_result"), CreateResult);
		}
		if (bCreateFailed)
		{
			++FailedCount;
			bBatchPassed = false;
			ItemResult->SetBoolField(TEXT("success"), false);
			ItemResult->SetStringField(TEXT("error"), TEXT("Data Asset creation failed."));
			ItemResult->SetObjectField(TEXT("raw_create_tool_result"), CreateToolResult.IsValid() ? CreateToolResult : MakeShared<FJsonObject>());
			if (!bContinueOnError)
			{
				break;
			}
			continue;
		}

		++CreatedCount;
		const FString CreatedAssetPath = DruidsMCPBridge::TryGetJsonStringField(CreateResult, TEXT("asset_path"));
		ItemResult->SetStringField(TEXT("asset_path"), CreatedAssetPath);
		ItemResult->SetStringField(TEXT("object_path"), DruidsMCPBridge::TryGetJsonStringField(CreateResult, TEXT("object_path")));

		TArray<TSharedPtr<FJsonValue>> PropertyResults;
		const TSharedPtr<FJsonObject>* Properties = nullptr;
		if (Item->TryGetObjectField(TEXT("Properties"), Properties) && Properties && Properties->IsValid())
		{
			for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : (*Properties)->Values)
			{
				TSharedPtr<FJsonObject> SetArgs = MakeShared<FJsonObject>();
				SetArgs->SetStringField(TEXT("AssetNameOrPath"), CreatedAssetPath);
				SetArgs->SetStringField(TEXT("PropertyName"), Pair.Key);
				SetArgs->SetField(TEXT("Value"), Pair.Value);
				SetArgs->SetBoolField(TEXT("Save"), bSave);
				TSharedPtr<FJsonObject> SetToolResult = ExecuteDataAssetSetProperty(SetArgs);
				TSharedPtr<FJsonObject> SetResult = ExtractFirstObjectFromToolContent(SetToolResult);
				const bool bSetFailed = ToolResultIsError(SetToolResult) || !SetResult.IsValid();
				TSharedPtr<FJsonObject> PropertyResult = MakeShared<FJsonObject>();
				PropertyResult->SetStringField(TEXT("property_name"), Pair.Key);
				PropertyResult->SetBoolField(TEXT("success"), !bSetFailed);
				if (SetResult.IsValid())
				{
					PropertyResult->SetObjectField(TEXT("set_result"), SetResult);
				}
				if (bSetFailed)
				{
					PropertyResult->SetStringField(TEXT("error"), TEXT("Property set failed."));
					PropertyResult->SetObjectField(TEXT("raw_set_tool_result"), SetToolResult.IsValid() ? SetToolResult : MakeShared<FJsonObject>());
					++FailedCount;
					bBatchPassed = false;
				}
				else
				{
					++PropertySetCount;
				}
				PropertyResults.Add(MakeShared<FJsonValueObject>(PropertyResult));
				if (bSetFailed && !bContinueOnError)
				{
					break;
				}
			}
		}
		ItemResult->SetArrayField(TEXT("property_results"), PropertyResults);
		ItemResult->SetBoolField(TEXT("success"), bBatchPassed || bContinueOnError || FailedCount == 0);
		if (!bBatchPassed && !bContinueOnError)
		{
			break;
		}
	}

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("tool"), TEXT("data_asset_create_batch"));
	Result->SetNumberField(TEXT("requested_count"), ItemValues->Num());
	Result->SetNumberField(TEXT("processed_count"), ItemResults.Num());
	Result->SetNumberField(TEXT("created_count"), CreatedCount);
	Result->SetNumberField(TEXT("property_set_count"), PropertySetCount);
	Result->SetNumberField(TEXT("failed_count"), FailedCount);
	Result->SetBoolField(TEXT("success"), bBatchPassed);
	Result->SetBoolField(TEXT("save_requested"), bSave);
	Result->SetBoolField(TEXT("continue_on_error"), bContinueOnError);
	Result->SetArrayField(TEXT("items"), ItemResults);
	Result->SetStringField(TEXT("mutation_policy"), TEXT("sequential_no_rollback"));
	Result->SetStringField(TEXT("property_scope"), TEXT("top_level_and_nested_struct_properties"));
	Result->SetStringField(TEXT("array_map_indexing"), TEXT("not_supported_in_this_slice"));
	TArray<FString> RecommendedNextTools = { TEXT("unreal_inspect_asset"), TEXT("data_asset_set_property") };
	Result->SetArrayField(TEXT("recommended_next_tools"), MakeStringArrayValue(RecommendedNextTools));

	return MakeToolJsonObjectResult(Result, !bBatchPassed);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ResolveAssetToObject(const FString& NameOrPath, const FString& Type, int32 Limit, FString& OutAssetPath, FString& OutError)
{
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("query"), NameOrPath);
	Result->SetStringField(TEXT("type"), Type);

	if (NameOrPath.IsEmpty())
	{
		OutError = TEXT("Asset name or path is required.");
		Result->SetStringField(TEXT("error"), OutError);
		return Result;
	}

	if (NameOrPath.StartsWith(TEXT("/")))
	{
		OutAssetPath = NameOrPath.Contains(TEXT(".")) ? FPackageName::ObjectPathToPackageName(NameOrPath) : NameOrPath;
		Result->SetStringField(TEXT("best_asset_path"), OutAssetPath);
		Result->SetArrayField(TEXT("candidates"), { MakeShared<FJsonValueString>(OutAssetPath) });
		return Result;
	}

	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
	FARFilter Filter;
	Filter.bRecursivePaths = true;
	Filter.PackagePaths.Add(TEXT("/Game"));
	if (Type.Equals(TEXT("Blueprint"), ESearchCase::IgnoreCase))
	{
		Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
	}

	TArray<FAssetData> Assets;
	AssetRegistryModule.Get().GetAssets(Filter, Assets);
	TArray<TSharedPtr<FJsonValue>> Candidates;
	for (const FAssetData& Asset : Assets)
	{
		const FString PackageName = Asset.PackageName.ToString();
		const FString AssetName = Asset.AssetName.ToString();
		if (!AssetName.Contains(NameOrPath, ESearchCase::IgnoreCase) && !PackageName.Contains(NameOrPath, ESearchCase::IgnoreCase))
		{
			continue;
		}

		TSharedPtr<FJsonObject> Candidate = MakeShared<FJsonObject>();
		Candidate->SetStringField(TEXT("asset_path"), PackageName);
		Candidate->SetStringField(TEXT("object_path"), Asset.GetSoftObjectPath().ToString());
		Candidate->SetStringField(TEXT("asset_name"), AssetName);
		Candidate->SetStringField(TEXT("class"), Asset.AssetClassPath.ToString());
		Candidates.Add(MakeShared<FJsonValueObject>(Candidate));
		if (Candidates.Num() >= Limit)
		{
			break;
		}
	}

	if (Candidates.Num() > 0)
	{
		OutAssetPath = Candidates[0]->AsObject()->GetStringField(TEXT("asset_path"));
		Result->SetStringField(TEXT("best_asset_path"), OutAssetPath);
	}
	else
	{
		OutError = FString::Printf(TEXT("Could not resolve asset from query '%s'."), *NameOrPath);
		Result->SetStringField(TEXT("error"), OutError);
	}

	Result->SetArrayField(TEXT("candidates"), Candidates);
	Result->SetNumberField(TEXT("count"), Candidates.Num());
	return Result;
}

UBlueprint* FDruidsMCPBridgeServer::LoadBlueprint(const FString& AssetPath, FString& OutError) const
{
	if (AssetPath.IsEmpty())
	{
		OutError = TEXT("Blueprint asset path is empty.");
		return nullptr;
	}

	UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *AssetPath);
	if (!Blueprint)
	{
		const FString AssetName = FPackageName::GetShortName(AssetPath);
		Blueprint = LoadObject<UBlueprint>(nullptr, *FString::Printf(TEXT("%s.%s"), *AssetPath, *AssetName));
	}
	if (!Blueprint)
	{
		OutError = FString::Printf(TEXT("Could not load Blueprint: %s"), *AssetPath);
	}
	return Blueprint;
}

UEdGraph* FDruidsMCPBridgeServer::FindBlueprintGraph(UBlueprint* Blueprint, const FString& GraphName, FString& OutError) const
{
	if (!Blueprint)
	{
		OutError = TEXT("Blueprint is null.");
		return nullptr;
	}

	TArray<UEdGraph*> Graphs;
	Blueprint->GetAllGraphs(Graphs);
	for (UEdGraph* Graph : Graphs)
	{
		if (Graph && Graph->GetName().Equals(GraphName, ESearchCase::IgnoreCase))
		{
			return Graph;
		}
	}

	OutError = FString::Printf(TEXT("Could not find graph '%s' in %s."), *GraphName, *Blueprint->GetName());
	return nullptr;
}

TArray<TSharedPtr<FJsonValue>> FDruidsMCPBridgeServer::MakeBlueprintGraphInventory(UBlueprint* Blueprint, const FString& AssetPath) const
{
	TArray<TSharedPtr<FJsonValue>> GraphValues;
	if (!Blueprint)
	{
		return GraphValues;
	}

	TArray<UEdGraph*> Graphs;
	Blueprint->GetAllGraphs(Graphs);
	for (UEdGraph* Graph : Graphs)
	{
		if (!Graph)
		{
			continue;
		}

		const bool bIsFunctionGraph = Blueprint->FunctionGraphs.Contains(Graph);
		const bool bIsUbergraph = Blueprint->UbergraphPages.Contains(Graph);
		const bool bIsMacroGraph = Blueprint->MacroGraphs.Contains(Graph);
		FString GraphType = TEXT("graph");
		if (bIsFunctionGraph)
		{
			GraphType = TEXT("function");
		}
		else if (bIsUbergraph)
		{
			GraphType = TEXT("ubergraph");
		}
		else if (bIsMacroGraph)
		{
			GraphType = TEXT("macro");
		}

		TArray<TSharedPtr<FJsonValue>> EventNames;
		TSet<FString> SeenEventNames;
		int32 CommentCount = 0;
		int32 ConnectionCount = 0;
		for (UEdGraphNode* Node : Graph->Nodes)
		{
			if (!Node)
			{
				continue;
			}

			if (Node->IsA<UEdGraphNode_Comment>())
			{
				++CommentCount;
			}

			for (UEdGraphPin* Pin : Node->Pins)
			{
				if (Pin && Pin->Direction == EGPD_Output)
				{
					ConnectionCount += Pin->LinkedTo.Num();
				}
			}

			FString EventName;
			if (const UK2Node_CustomEvent* CustomEventNode = Cast<UK2Node_CustomEvent>(Node))
			{
				EventName = CustomEventNode->CustomFunctionName.ToString();
			}
			else if (const UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node))
			{
				EventName = EventNode->EventReference.GetMemberName().ToString();
				if (EventName.IsEmpty())
				{
					EventName = Node->GetNodeTitle(ENodeTitleType::MenuTitle).ToString();
				}
			}

			if (!EventName.IsEmpty() && !SeenEventNames.Contains(EventName))
			{
				SeenEventNames.Add(EventName);
				EventNames.Add(MakeShared<FJsonValueString>(EventName));
			}
		}

		TSharedPtr<FJsonObject> ReadGraphArguments = MakeShared<FJsonObject>();
		if (!AssetPath.IsEmpty())
		{
			ReadGraphArguments->SetStringField(TEXT("BlueprintNameOrPath"), AssetPath);
		}
		ReadGraphArguments->SetStringField(TEXT("GraphName"), Graph->GetName());

		TSharedPtr<FJsonObject> GraphObject = MakeShared<FJsonObject>();
		GraphObject->SetStringField(TEXT("name"), Graph->GetName());
		GraphObject->SetStringField(TEXT("graph_name"), Graph->GetName());
		GraphObject->SetStringField(TEXT("graph_type"), GraphType);
		GraphObject->SetNumberField(TEXT("node_count"), Graph->Nodes.Num());
		GraphObject->SetNumberField(TEXT("connection_count"), ConnectionCount);
		GraphObject->SetNumberField(TEXT("comment_count"), CommentCount);
		GraphObject->SetArrayField(TEXT("event_names"), EventNames);
		GraphObject->SetObjectField(TEXT("read_graph_arguments"), ReadGraphArguments);
		GraphValues.Add(MakeShared<FJsonValueObject>(GraphObject));
	}

	return GraphValues;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ReadBlueprintGraph(UBlueprint* Blueprint, UEdGraph* Graph) const
{
	TArray<TSharedPtr<FJsonValue>> NodeValues;
	TArray<TSharedPtr<FJsonValue>> ConnectionValues;
	const bool bIsFunctionGraph = Blueprint->FunctionGraphs.Contains(Graph);
	const FString GraphType = bIsFunctionGraph ? TEXT("function") : TEXT("ubergraph");
	FString RawText = FString::Printf(TEXT("# BLUEPRINT: %s\nParent Class: %s\n# GRAPH %s type=%s %d\n"),
		*Blueprint->GetName(),
		Blueprint->ParentClass ? *Blueprint->ParentClass->GetName() : TEXT("None"),
		*Graph->GetName(),
		*GraphType,
		Graph->Nodes.Num());

	auto MakePinParameterObject = [](const UEdGraphPin* Pin) -> TSharedPtr<FJsonObject>
	{
		TSharedPtr<FJsonObject> Parameter = MakeShared<FJsonObject>();
		if (!Pin)
		{
			return Parameter;
		}
		UObject* SubCategoryObject = Pin->PinType.PinSubCategoryObject.Get();
		Parameter->SetStringField(TEXT("name"), Pin->PinName.ToString());
		Parameter->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("input") : TEXT("output"));
		Parameter->SetStringField(TEXT("pin_category"), Pin->PinType.PinCategory.ToString());
		Parameter->SetStringField(TEXT("pin_subcategory"), Pin->PinType.PinSubCategory.ToString());
		Parameter->SetStringField(TEXT("pin_subcategory_object"), SubCategoryObject ? SubCategoryObject->GetPathName() : TEXT(""));
		Parameter->SetStringField(TEXT("container_type"), DruidsMCPBridge::GetPinContainerTypeName(Pin->PinType));
		Parameter->SetStringField(TEXT("type_string"), DruidsMCPBridge::MakePinTypeString(Pin->PinType));
		Parameter->SetObjectField(TEXT("type"), DruidsMCPBridge::MakePinTypeObject(Pin->PinType));
		Parameter->SetStringField(TEXT("default_value"), Pin->DefaultValue);
		return Parameter;
	};

	for (UEdGraphNode* Node : Graph->Nodes)
	{
		if (!Node)
		{
			continue;
		}

		const FString NodeGuid = Node->NodeGuid.ToString(EGuidFormats::Digits);
		const FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
		TArray<TSharedPtr<FJsonValue>> PinValues;
		TArray<TSharedPtr<FJsonValue>> PinDetailValues;
		TArray<FString> PinTexts;
		for (UEdGraphPin* Pin : Node->Pins)
		{
			if (!Pin)
			{
				continue;
			}

			FString PinText = Pin->PinName.ToString();
			if (!Pin->DefaultValue.IsEmpty())
			{
				PinText += FString::Printf(TEXT("='%s'"), *Pin->DefaultValue);
			}
			PinValues.Add(MakeShared<FJsonValueString>(PinText));
			PinTexts.Add(PinText);

			TSharedPtr<FJsonObject> PinDetail = MakeShared<FJsonObject>();
			PinDetail->SetStringField(TEXT("name"), Pin->PinName.ToString());
			PinDetail->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("input") : TEXT("output"));
			PinDetail->SetStringField(TEXT("category"), Pin->PinType.PinCategory.ToString());
			PinDetail->SetStringField(TEXT("subcategory"), Pin->PinType.PinSubCategory.ToString());
			PinDetail->SetStringField(TEXT("subcategory_object"), DruidsMCPBridge::GetPinSubCategoryObjectPath(Pin->PinType.PinSubCategoryObject));
			PinDetail->SetStringField(TEXT("container_type"), DruidsMCPBridge::GetPinContainerTypeName(Pin->PinType));
			PinDetail->SetStringField(TEXT("type_string"), DruidsMCPBridge::MakePinTypeString(Pin->PinType));
			PinDetail->SetObjectField(TEXT("type"), DruidsMCPBridge::MakePinTypeObject(Pin->PinType));
			PinDetail->SetStringField(TEXT("default_value"), Pin->DefaultValue);
			PinDetail->SetBoolField(TEXT("has_default_value"), !Pin->DefaultValue.IsEmpty());
			TArray<TSharedPtr<FJsonValue>> LinkedPins;
			for (UEdGraphPin* LinkedTo : Pin->LinkedTo)
			{
				if (LinkedTo && LinkedTo->GetOwningNode())
				{
					TSharedPtr<FJsonObject> LinkedPin = MakeShared<FJsonObject>();
					LinkedPin->SetStringField(TEXT("node_guid"), LinkedTo->GetOwningNode()->NodeGuid.ToString(EGuidFormats::Digits));
					LinkedPin->SetStringField(TEXT("pin_name"), LinkedTo->PinName.ToString());
					LinkedPins.Add(MakeShared<FJsonValueObject>(LinkedPin));
				}
			}
			PinDetail->SetArrayField(TEXT("linked_to"), LinkedPins);
			PinDetailValues.Add(MakeShared<FJsonValueObject>(PinDetail));

			for (UEdGraphPin* LinkedTo : Pin->LinkedTo)
			{
				if (LinkedTo && LinkedTo->GetOwningNode() && Pin->Direction == EGPD_Output)
				{
					TSharedPtr<FJsonObject> Connection = MakeShared<FJsonObject>();
					Connection->SetStringField(TEXT("graph"), Graph->GetName());
					Connection->SetStringField(TEXT("source_node_guid"), NodeGuid);
					Connection->SetStringField(TEXT("source_pin"), Pin->PinName.ToString());
					Connection->SetStringField(TEXT("target_node_guid"), LinkedTo->GetOwningNode()->NodeGuid.ToString(EGuidFormats::Digits));
					Connection->SetStringField(TEXT("target_pin"), LinkedTo->PinName.ToString());
					ConnectionValues.Add(MakeShared<FJsonValueObject>(Connection));
				}
			}
		}

		TSharedPtr<FJsonObject> NodeObject = MakeShared<FJsonObject>();
		NodeObject->SetStringField(TEXT("graph"), Graph->GetName());
		NodeObject->SetStringField(TEXT("graph_type"), GraphType);
		NodeObject->SetStringField(TEXT("node_guid"), NodeGuid);
		NodeObject->SetStringField(TEXT("node_class"), Node->GetClass()->GetPathName());
		NodeObject->SetStringField(TEXT("title"), Title);
		NodeObject->SetNumberField(TEXT("node_pos_x"), Node->NodePosX);
		NodeObject->SetNumberField(TEXT("node_pos_y"), Node->NodePosY);
		NodeObject->SetNumberField(TEXT("node_width"), Node->NodeWidth);
		NodeObject->SetNumberField(TEXT("node_height"), Node->NodeHeight);
		NodeObject->SetStringField(TEXT("node_comment"), Node->NodeComment);
		if (const UEdGraphNode_Comment* CommentNode = Cast<UEdGraphNode_Comment>(Node))
		{
			NodeObject->SetStringField(TEXT("node_kind"), TEXT("comment"));
			TSharedPtr<FJsonObject> CommentObject = MakeShared<FJsonObject>();
			CommentObject->SetStringField(TEXT("text"), CommentNode->NodeComment);
			CommentObject->SetNumberField(TEXT("font_size"), CommentNode->FontSize);
			CommentObject->SetNumberField(TEXT("x"), CommentNode->NodePosX);
			CommentObject->SetNumberField(TEXT("y"), CommentNode->NodePosY);
			CommentObject->SetNumberField(TEXT("width"), CommentNode->NodeWidth);
			CommentObject->SetNumberField(TEXT("height"), CommentNode->NodeHeight);
			CommentObject->SetStringField(TEXT("move_mode"), CommentNode->MoveMode == ECommentBoxMode::GroupMovement ? TEXT("group_movement") : TEXT("comment"));
			TSharedPtr<FJsonObject> ColorObject = MakeShared<FJsonObject>();
			ColorObject->SetNumberField(TEXT("r"), CommentNode->CommentColor.R);
			ColorObject->SetNumberField(TEXT("g"), CommentNode->CommentColor.G);
			ColorObject->SetNumberField(TEXT("b"), CommentNode->CommentColor.B);
			ColorObject->SetNumberField(TEXT("a"), CommentNode->CommentColor.A);
			CommentObject->SetObjectField(TEXT("color"), ColorObject);

			TArray<TSharedPtr<FJsonValue>> ContainedNodeGuids;
			const int32 Right = CommentNode->NodePosX + CommentNode->NodeWidth;
			const int32 Bottom = CommentNode->NodePosY + CommentNode->NodeHeight;
			for (const UEdGraphNode* CandidateNode : Graph->Nodes)
			{
				if (!CandidateNode || CandidateNode == CommentNode)
				{
					continue;
				}
				if (CandidateNode->NodePosX >= CommentNode->NodePosX &&
					CandidateNode->NodePosY >= CommentNode->NodePosY &&
					CandidateNode->NodePosX <= Right &&
					CandidateNode->NodePosY <= Bottom)
				{
					ContainedNodeGuids.Add(MakeShared<FJsonValueString>(CandidateNode->NodeGuid.ToString(EGuidFormats::Digits)));
				}
			}
			CommentObject->SetArrayField(TEXT("contained_node_guids"), ContainedNodeGuids);
			NodeObject->SetObjectField(TEXT("comment"), CommentObject);
		}
		else if (const UK2Node_CustomEvent* CustomEventNode = Cast<UK2Node_CustomEvent>(Node))
		{
			const FString EventName = CustomEventNode->CustomFunctionName.ToString();
			NodeObject->SetStringField(TEXT("node_kind"), TEXT("custom_event"));
			NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("custom_event::%s"), *EventName));
			TSharedPtr<FJsonObject> CustomEvent = MakeShared<FJsonObject>();
			CustomEvent->SetStringField(TEXT("event_name"), EventName);
			CustomEvent->SetBoolField(TEXT("call_in_editor"), CustomEventNode->bCallInEditor != 0);
			TArray<TSharedPtr<FJsonValue>> Parameters;
			for (const TSharedPtr<FUserPinInfo>& PinInfo : CustomEventNode->UserDefinedPins)
			{
				if (!PinInfo.IsValid())
				{
					continue;
				}

				UObject* SubCategoryObject = PinInfo->PinType.PinSubCategoryObject.Get();
				TSharedPtr<FJsonObject> Parameter = MakeShared<FJsonObject>();
				Parameter->SetStringField(TEXT("name"), PinInfo->PinName.ToString());
				Parameter->SetStringField(TEXT("pin_category"), PinInfo->PinType.PinCategory.ToString());
				Parameter->SetStringField(TEXT("pin_subcategory"), PinInfo->PinType.PinSubCategory.ToString());
				Parameter->SetStringField(TEXT("pin_subcategory_object"), SubCategoryObject ? SubCategoryObject->GetPathName() : TEXT(""));
				Parameter->SetStringField(TEXT("container_type"), DruidsMCPBridge::GetPinContainerTypeName(PinInfo->PinType));
				Parameter->SetStringField(TEXT("type_string"), DruidsMCPBridge::MakePinTypeString(PinInfo->PinType));
				Parameter->SetObjectField(TEXT("type"), DruidsMCPBridge::MakePinTypeObject(PinInfo->PinType));
				Parameter->SetStringField(TEXT("default_value"), PinInfo->PinDefaultValue);
				Parameters.Add(MakeShared<FJsonValueObject>(Parameter));
			}
			CustomEvent->SetArrayField(TEXT("parameters"), Parameters);
			NodeObject->SetObjectField(TEXT("custom_event"), CustomEvent);
		}
		else if (const UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node))
		{
			const FName EventFName = EventNode->EventReference.GetMemberName();
			const FString EventName = EventFName.ToString();
			UClass* EventOwnerClass = EventNode->EventReference.GetMemberParentClass(EventNode->GetBlueprintClassFromNode());
			const bool bInterfaceEvent = EventOwnerClass && EventOwnerClass->IsChildOf(UInterface::StaticClass());
			NodeObject->SetStringField(TEXT("node_kind"), bInterfaceEvent ? TEXT("interface_event") : TEXT("event"));
			if (!EventName.IsEmpty())
			{
				NodeObject->SetStringField(TEXT("spawner_id"), bInterfaceEvent && EventOwnerClass
					? FString::Printf(TEXT("interface_event::%s:%s"), *EventOwnerClass->GetPathName(), *EventName)
					: FString::Printf(TEXT("event::%s"), *EventName));
			}

			TArray<TSharedPtr<FJsonValue>> Parameters;
			for (UEdGraphPin* Pin : EventNode->Pins)
			{
				if (!Pin ||
					Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec ||
					Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Delegate)
				{
					continue;
				}
				Parameters.Add(MakeShared<FJsonValueObject>(MakePinParameterObject(Pin)));
			}

			TSharedPtr<FJsonObject> EventReference = MakeShared<FJsonObject>();
			EventReference->SetStringField(TEXT("event_name"), EventName);
			EventReference->SetStringField(TEXT("owner_class_path"), EventOwnerClass ? EventOwnerClass->GetPathName() : TEXT(""));
			EventReference->SetStringField(TEXT("owner_class_name"), EventOwnerClass ? EventOwnerClass->GetName() : TEXT(""));
			EventReference->SetBoolField(TEXT("is_interface_event"), bInterfaceEvent);
			EventReference->SetBoolField(TEXT("override_function"), EventNode->bOverrideFunction != 0);
			EventReference->SetBoolField(TEXT("internal_event"), EventNode->bInternalEvent != 0);
			EventReference->SetStringField(TEXT("custom_function_name"), EventNode->CustomFunctionName.ToString());
			EventReference->SetArrayField(TEXT("parameters"), Parameters);
			NodeObject->SetObjectField(TEXT("event_reference"), EventReference);

			if (bInterfaceEvent)
			{
				TSharedPtr<FJsonObject> InterfaceImplementation = MakeShared<FJsonObject>();
				InterfaceImplementation->SetStringField(TEXT("interface_class_path"), EventOwnerClass->GetPathName());
				InterfaceImplementation->SetStringField(TEXT("interface_class_name"), EventOwnerClass->GetName());
				if (const UBlueprint* InterfaceBlueprint = Cast<UBlueprint>(EventOwnerClass->ClassGeneratedBy))
				{
					InterfaceImplementation->SetStringField(TEXT("interface_blueprint_path"), InterfaceBlueprint->GetPathName());
					InterfaceImplementation->SetStringField(TEXT("interface_asset_path"), InterfaceBlueprint->GetOutermost()->GetName());
				}
				InterfaceImplementation->SetStringField(TEXT("function_name"), EventName);
				InterfaceImplementation->SetStringField(TEXT("graph"), Graph->GetName());
				InterfaceImplementation->SetStringField(TEXT("node_guid"), NodeGuid);
				InterfaceImplementation->SetArrayField(TEXT("parameters"), Parameters);
				NodeObject->SetObjectField(TEXT("interface_implementation"), InterfaceImplementation);
			}
		}
		else if (const UK2Node_FunctionEntry* FunctionEntryNode = Cast<UK2Node_FunctionEntry>(Node))
		{
			const FString FunctionName = FunctionEntryNode->CustomGeneratedFunctionName != NAME_None
				? FunctionEntryNode->CustomGeneratedFunctionName.ToString()
				: FunctionEntryNode->FunctionReference.GetMemberName().ToString();
			UClass* FunctionOwnerClass = FunctionEntryNode->FunctionReference.GetMemberParentClass(Blueprint->GeneratedClass);
			const bool bInterfaceFunction = FunctionOwnerClass && FunctionOwnerClass->IsChildOf(UInterface::StaticClass());
			NodeObject->SetStringField(TEXT("node_kind"), bInterfaceFunction ? TEXT("interface_function_entry") : TEXT("function_entry"));
			NodeObject->SetStringField(TEXT("function_name"), FunctionName.IsEmpty() ? Graph->GetName() : FunctionName);
			NodeObject->SetStringField(TEXT("spawner_id"), bInterfaceFunction && FunctionOwnerClass
				? FString::Printf(TEXT("interface_function::%s:%s"), *FunctionOwnerClass->GetPathName(), *(FunctionName.IsEmpty() ? Graph->GetName() : FunctionName))
				: FString::Printf(TEXT("bp_func_entry::%s"), *(FunctionName.IsEmpty() ? Graph->GetName() : FunctionName)));
			if (bInterfaceFunction)
			{
				TArray<TSharedPtr<FJsonValue>> Parameters;
				for (UEdGraphPin* Pin : FunctionEntryNode->Pins)
				{
					if (!Pin ||
						Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec ||
						Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Delegate)
					{
						continue;
					}
					Parameters.Add(MakeShared<FJsonValueObject>(MakePinParameterObject(Pin)));
				}

				TSharedPtr<FJsonObject> InterfaceImplementation = MakeShared<FJsonObject>();
				InterfaceImplementation->SetStringField(TEXT("interface_class_path"), FunctionOwnerClass->GetPathName());
				InterfaceImplementation->SetStringField(TEXT("interface_class_name"), FunctionOwnerClass->GetName());
				if (const UBlueprint* InterfaceBlueprint = Cast<UBlueprint>(FunctionOwnerClass->ClassGeneratedBy))
				{
					InterfaceImplementation->SetStringField(TEXT("interface_blueprint_path"), InterfaceBlueprint->GetPathName());
					InterfaceImplementation->SetStringField(TEXT("interface_asset_path"), InterfaceBlueprint->GetOutermost()->GetName());
				}
				InterfaceImplementation->SetStringField(TEXT("function_name"), FunctionName.IsEmpty() ? Graph->GetName() : FunctionName);
				InterfaceImplementation->SetStringField(TEXT("graph"), Graph->GetName());
				InterfaceImplementation->SetStringField(TEXT("node_guid"), NodeGuid);
				InterfaceImplementation->SetStringField(TEXT("implementation_kind"), TEXT("function_entry"));
				InterfaceImplementation->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("interface_function::%s:%s"), *FunctionOwnerClass->GetPathName(), *(FunctionName.IsEmpty() ? Graph->GetName() : FunctionName)));
				InterfaceImplementation->SetArrayField(TEXT("parameters"), Parameters);
				NodeObject->SetObjectField(TEXT("interface_implementation"), InterfaceImplementation);
			}
		}
		else if (const UK2Node_FunctionResult* FunctionResultNode = Cast<UK2Node_FunctionResult>(Node))
		{
			NodeObject->SetStringField(TEXT("node_kind"), TEXT("function_result"));
			NodeObject->SetStringField(TEXT("function_name"), Graph->GetName());
			NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("bp_func_result::%s"), *Graph->GetName()));
		}
		else if (const UK2Node_DynamicCast* DynamicCastNode = Cast<UK2Node_DynamicCast>(Node))
		{
			UClass* TargetClass = DynamicCastNode->TargetType.Get();
			const FString TargetClassPath = TargetClass ? TargetClass->GetPathName() : TEXT("");
			NodeObject->SetStringField(TEXT("node_kind"), TEXT("dynamic_cast"));
			if (!TargetClassPath.IsEmpty())
			{
				NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("cast::%s"), *TargetClassPath));
			}

			TSharedPtr<FJsonObject> CastReference = MakeShared<FJsonObject>();
			CastReference->SetStringField(TEXT("target_class_path"), TargetClassPath);
			CastReference->SetStringField(TEXT("target_class_name"), TargetClass ? TargetClass->GetName() : TEXT(""));
			if (const UBlueprint* TargetBlueprint = TargetClass ? UBlueprint::GetBlueprintFromClass(TargetClass) : nullptr)
			{
				CastReference->SetStringField(TEXT("target_blueprint_path"), TargetBlueprint->GetPathName());
			}
			NodeObject->SetObjectField(TEXT("cast_reference"), CastReference);
		}
		else if (const UK2Node_CallFunction* FunctionNode = Cast<UK2Node_CallFunction>(Node))
		{
			NodeObject->SetStringField(TEXT("node_kind"), TEXT("function_call"));
			if (const UFunction* Function = FunctionNode->GetTargetFunction())
			{
				const UClass* OwnerClass = Function->GetOwnerClass();
				const FString FunctionName = Function->GetName();
				const FString OwnerClassPath = OwnerClass ? OwnerClass->GetPathName() : TEXT("");
				TSharedPtr<FJsonObject> FunctionReference = MakeShared<FJsonObject>();
				FunctionReference->SetStringField(TEXT("class_path"), OwnerClassPath);
				FunctionReference->SetStringField(TEXT("function_name"), FunctionName);
				FunctionReference->SetStringField(TEXT("path"), Function->GetPathName());
				NodeObject->SetObjectField(TEXT("function_reference"), FunctionReference);
				if (!OwnerClassPath.IsEmpty() && !FunctionName.IsEmpty())
				{
					if (OwnerClass == Blueprint->GeneratedClass || OwnerClass == Blueprint->SkeletonGeneratedClass)
					{
						NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("bp_func::%s"), *FunctionName));
					}
					else
					{
						NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("func::%s:%s"), *OwnerClassPath, *FunctionName));
					}
				}
			}
			else if (FunctionNode->FunctionReference.GetMemberName() != NAME_None)
			{
				const FString FunctionName = FunctionNode->FunctionReference.GetMemberName().ToString();
				TSharedPtr<FJsonObject> FunctionReference = MakeShared<FJsonObject>();
				FunctionReference->SetStringField(TEXT("class_path"), FunctionNode->FunctionReference.IsSelfContext() ? TEXT("self") : TEXT(""));
				FunctionReference->SetStringField(TEXT("function_name"), FunctionName);
				FunctionReference->SetStringField(TEXT("path"), FunctionNode->FunctionReference.IsSelfContext() ? FString::Printf(TEXT("self:%s"), *FunctionName) : FunctionName);
				NodeObject->SetObjectField(TEXT("function_reference"), FunctionReference);
				if (FunctionNode->FunctionReference.IsSelfContext())
				{
					NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("bp_func::%s"), *FunctionName));
				}
			}
		}
		else if (const UK2Node_Variable* VariableNode = Cast<UK2Node_Variable>(Node))
		{
			const bool bIsSet = Node->IsA<UK2Node_VariableSet>();
			const FString VariableName = VariableNode->GetVarNameString();
			const bool bSelfContext = VariableNode->VariableReference.IsSelfContext();
			NodeObject->SetStringField(TEXT("node_kind"), bIsSet ? TEXT("variable_set") : TEXT("variable_get"));
			TSharedPtr<FJsonObject> VariableReference = MakeShared<FJsonObject>();
			VariableReference->SetStringField(TEXT("variable_name"), VariableName);
			VariableReference->SetStringField(TEXT("scope"), bSelfContext ? TEXT("self") : (VariableNode->VariableReference.IsLocalScope() ? TEXT("local") : TEXT("external")));
			VariableReference->SetStringField(TEXT("member_guid"), VariableNode->VariableReference.GetMemberGuid().ToString(EGuidFormats::Digits));
			if (UClass* VariableOwnerClass = VariableNode->VariableReference.GetMemberParentClass(Blueprint->GeneratedClass))
			{
				VariableReference->SetStringField(TEXT("owner_class_path"), VariableOwnerClass->GetPathName());
				VariableReference->SetStringField(TEXT("owner_class_name"), VariableOwnerClass->GetName());
				if (const UBlueprint* OwnerBlueprint = Cast<UBlueprint>(VariableOwnerClass->ClassGeneratedBy))
				{
					VariableReference->SetStringField(TEXT("owner_blueprint_path"), OwnerBlueprint->GetPathName());
				}
			}
			NodeObject->SetObjectField(TEXT("variable_reference"), VariableReference);
			if (!VariableName.IsEmpty())
			{
				FString OwnerClassPath;
				VariableReference->TryGetStringField(TEXT("owner_class_path"), OwnerClassPath);
				if (!bSelfContext && !OwnerClassPath.IsEmpty())
				{
					NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("var_%s::%s:%s"), bIsSet ? TEXT("set") : TEXT("get"), *OwnerClassPath, *VariableName));
				}
				else
				{
					NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("var_%s::%s"), bIsSet ? TEXT("set") : TEXT("get"), *VariableName));
				}
			}
		}
		else if (Node->IsA<UK2Node_IfThenElse>())
		{
			NodeObject->SetStringField(TEXT("node_kind"), TEXT("branch"));
			NodeObject->SetStringField(TEXT("spawner_id"), TEXT("node::branch"));
		}
		else if (const UK2Node_ExecutionSequence* SequenceNode = Cast<UK2Node_ExecutionSequence>(Node))
		{
			int32 ThenPinCount = 0;
			for (UEdGraphPin* Pin : SequenceNode->Pins)
			{
				if (Pin && Pin->Direction == EGPD_Output && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec && Pin->PinName.ToString().StartsWith(UEdGraphSchema_K2::PN_Then.ToString()))
				{
					++ThenPinCount;
				}
			}

			NodeObject->SetStringField(TEXT("node_kind"), TEXT("sequence"));
			NodeObject->SetStringField(TEXT("spawner_id"), TEXT("node::sequence"));
			NodeObject->SetNumberField(TEXT("then_pin_count"), ThenPinCount);
		}
		else if (const UK2Node_MacroInstance* MacroNode = Cast<UK2Node_MacroInstance>(Node))
		{
			UEdGraph* MacroGraph = MacroNode->GetMacroGraph();
			const FString MacroName = MacroGraph ? MacroGraph->GetName() : TEXT("");
			const bool bSupportedStandardFlowMacro = DruidsMCPBridge::IsSupportedStandardFlowMacro(MacroName);
			NodeObject->SetStringField(TEXT("node_kind"), bSupportedStandardFlowMacro ? TEXT("flow_control_macro") : TEXT("macro_instance"));
			if (bSupportedStandardFlowMacro)
			{
				NodeObject->SetStringField(TEXT("spawner_id"), DruidsMCPBridge::GetStandardMacroSpawnerId(MacroName));
			}

			TSharedPtr<FJsonObject> MacroReference = MakeShared<FJsonObject>();
			MacroReference->SetStringField(TEXT("macro_name"), MacroName);
			MacroReference->SetStringField(TEXT("macro_graph_path"), MacroGraph ? MacroGraph->GetPathName() : TEXT(""));
			if (const UBlueprint* SourceBlueprint = MacroNode->GetSourceBlueprint())
			{
				MacroReference->SetStringField(TEXT("blueprint_path"), SourceBlueprint->GetPathName());
			}
			MacroReference->SetBoolField(TEXT("standard_flow_control"), bSupportedStandardFlowMacro);
			NodeObject->SetObjectField(TEXT("macro_reference"), MacroReference);
		}
		else if (const UK2Node_InputAxisKeyEvent* AxisKeyNode = Cast<UK2Node_InputAxisKeyEvent>(Node))
		{
			NodeObject->SetStringField(TEXT("node_kind"), TEXT("input_axis_key_event"));
			NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("input_axis_key::%s"), *AxisKeyNode->AxisKey.ToString()));
			TSharedPtr<FJsonObject> InputEvent = MakeShared<FJsonObject>();
			InputEvent->SetStringField(TEXT("key"), AxisKeyNode->AxisKey.ToString());
			InputEvent->SetStringField(TEXT("event_type"), TEXT("axis_key"));
			InputEvent->SetBoolField(TEXT("axis"), true);
			InputEvent->SetBoolField(TEXT("consume_input"), AxisKeyNode->bConsumeInput != 0);
			InputEvent->SetBoolField(TEXT("execute_when_paused"), AxisKeyNode->bExecuteWhenPaused != 0);
			InputEvent->SetBoolField(TEXT("override_parent_binding"), AxisKeyNode->bOverrideParentBinding != 0);
			NodeObject->SetObjectField(TEXT("input_event"), InputEvent);
		}
		else if (const UK2Node_InputKey* InputKeyNode = Cast<UK2Node_InputKey>(Node))
		{
			NodeObject->SetStringField(TEXT("node_kind"), TEXT("input_key_event"));
			NodeObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("input_key::%s"), *InputKeyNode->InputKey.ToString()));
			TSharedPtr<FJsonObject> InputEvent = MakeShared<FJsonObject>();
			InputEvent->SetStringField(TEXT("key"), InputKeyNode->InputKey.ToString());
			InputEvent->SetStringField(TEXT("event_type"), TEXT("key"));
			InputEvent->SetBoolField(TEXT("axis"), false);
			InputEvent->SetBoolField(TEXT("consume_input"), InputKeyNode->bConsumeInput != 0);
			InputEvent->SetBoolField(TEXT("execute_when_paused"), InputKeyNode->bExecuteWhenPaused != 0);
			InputEvent->SetBoolField(TEXT("override_parent_binding"), InputKeyNode->bOverrideParentBinding != 0);
			InputEvent->SetBoolField(TEXT("control"), InputKeyNode->bControl != 0);
			InputEvent->SetBoolField(TEXT("alt"), InputKeyNode->bAlt != 0);
			InputEvent->SetBoolField(TEXT("shift"), InputKeyNode->bShift != 0);
			InputEvent->SetBoolField(TEXT("command"), InputKeyNode->bCommand != 0);
			NodeObject->SetObjectField(TEXT("input_event"), InputEvent);
		}
		else
		{
			NodeObject->SetStringField(TEXT("node_kind"), TEXT("unknown"));
		}
		NodeObject->SetArrayField(TEXT("pins"), PinValues);
		NodeObject->SetArrayField(TEXT("pin_details"), PinDetailValues);
		NodeValues.Add(MakeShared<FJsonValueObject>(NodeObject));
		RawText += FString::Printf(TEXT("%s\t%s\t%s\n"), *NodeGuid, *Title, *FString::Join(PinTexts, TEXT(",")));
	}

	RawText += FString::Printf(TEXT("\n# CONNECTIONS %s %d\n"), *Graph->GetName(), ConnectionValues.Num());
	for (const TSharedPtr<FJsonValue>& ConnectionValue : ConnectionValues)
	{
		const TSharedPtr<FJsonObject> Connection = ConnectionValue->AsObject();
		RawText += FString::Printf(TEXT("%s\t%s\t%s\t%s\n"),
			*Connection->GetStringField(TEXT("source_node_guid")),
			*Connection->GetStringField(TEXT("source_pin")),
			*Connection->GetStringField(TEXT("target_node_guid")),
			*Connection->GetStringField(TEXT("target_pin")));
	}

	TArray<TSharedPtr<FJsonValue>> VariableValues;
	RawText += FString::Printf(TEXT("\n# VARIABLES %d\n"), Blueprint->NewVariables.Num());
	for (const FBPVariableDescription& Variable : Blueprint->NewVariables)
	{
		const FEdGraphPinType& VarType = Variable.VarType;
		UObject* SubCategoryObject = VarType.PinSubCategoryObject.Get();
		const FString ClassDefaultValue = DruidsMCPBridge::ExportBlueprintVariableDefaultValue(Blueprint, Variable);
		TSharedPtr<FJsonObject> VariableObject = MakeShared<FJsonObject>();
		VariableObject->SetStringField(TEXT("name"), Variable.VarName.ToString());
		VariableObject->SetStringField(TEXT("guid"), Variable.VarGuid.ToString(EGuidFormats::Digits));
		VariableObject->SetStringField(TEXT("category"), Variable.Category.ToString());
		VariableObject->SetStringField(TEXT("default_value"), Variable.DefaultValue);
		VariableObject->SetBoolField(TEXT("has_default_value"), !Variable.DefaultValue.IsEmpty());
		VariableObject->SetStringField(TEXT("class_default_value"), ClassDefaultValue);
		VariableObject->SetBoolField(TEXT("has_class_default_value"), !ClassDefaultValue.IsEmpty());
		VariableObject->SetStringField(TEXT("pin_category"), VarType.PinCategory.ToString());
		VariableObject->SetStringField(TEXT("pin_subcategory"), VarType.PinSubCategory.ToString());
		VariableObject->SetStringField(TEXT("pin_subcategory_object"), SubCategoryObject ? SubCategoryObject->GetPathName() : TEXT(""));
		VariableObject->SetStringField(TEXT("container_type"), DruidsMCPBridge::GetPinContainerTypeName(VarType));
		VariableObject->SetStringField(TEXT("type_string"), DruidsMCPBridge::MakePinTypeString(VarType));
		VariableObject->SetObjectField(TEXT("type"), DruidsMCPBridge::MakePinTypeObject(VarType));
		VariableObject->SetBoolField(TEXT("is_array"), VarType.ContainerType == EPinContainerType::Array);
		VariableObject->SetBoolField(TEXT("is_set"), VarType.ContainerType == EPinContainerType::Set);
		VariableObject->SetBoolField(TEXT("is_map"), VarType.ContainerType == EPinContainerType::Map);
		VariableValues.Add(MakeShared<FJsonValueObject>(VariableObject));

		RawText += FString::Printf(TEXT("%s\t%s\t%s\t%s\t%s\t%s\n"),
			*Variable.VarName.ToString(),
			*VarType.PinCategory.ToString(),
			*VarType.PinSubCategory.ToString(),
			SubCategoryObject ? *SubCategoryObject->GetPathName() : TEXT(""),
			*DruidsMCPBridge::GetPinContainerTypeName(VarType),
			*DruidsMCPBridge::MakePinTypeString(VarType));
	}

	TArray<TSharedPtr<FJsonValue>> ComponentValues;
	if (Blueprint->SimpleConstructionScript)
	{
		TMap<USCS_Node*, FString> ParentComponentNames;
		for (USCS_Node* Node : Blueprint->SimpleConstructionScript->GetAllNodes())
		{
			if (!Node)
			{
				continue;
			}

			for (USCS_Node* ChildNode : Node->GetChildNodes())
			{
				if (ChildNode)
				{
					ParentComponentNames.Add(ChildNode, Node->GetVariableName().ToString());
				}
			}
		}

		RawText += FString::Printf(TEXT("\n# COMPONENTS %d\n"), Blueprint->SimpleConstructionScript->GetAllNodes().Num());
		for (USCS_Node* Node : Blueprint->SimpleConstructionScript->GetAllNodes())
		{
			if (!Node)
			{
				continue;
			}

			UActorComponent* ComponentTemplate = Node->ComponentTemplate;
			TSharedPtr<FJsonObject> ComponentObject = MakeShared<FJsonObject>();
			ComponentObject->SetStringField(TEXT("name"), Node->GetVariableName().ToString());
			ComponentObject->SetStringField(TEXT("component_class"), ComponentTemplate ? ComponentTemplate->GetClass()->GetPathName() : TEXT(""));
			ComponentObject->SetStringField(TEXT("template_path"), ComponentTemplate ? ComponentTemplate->GetPathName() : TEXT(""));
			ComponentObject->SetStringField(TEXT("parent_component"), ParentComponentNames.Contains(Node) ? ParentComponentNames[Node] : TEXT(""));
			FString StaticMeshPath;
			if (const UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(ComponentTemplate))
			{
				StaticMeshPath = StaticMeshComponent->GetStaticMesh() ? StaticMeshComponent->GetStaticMesh()->GetPathName() : TEXT("");
				ComponentObject->SetStringField(TEXT("static_mesh"), StaticMeshPath);
			}
			TArray<TSharedPtr<FJsonValue>> Materials;
			if (const UMeshComponent* MeshComponent = Cast<UMeshComponent>(ComponentTemplate))
			{
				const int32 MaterialCount = MeshComponent->GetNumMaterials();
				for (int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex)
				{
					TSharedPtr<FJsonObject> MaterialObject = MakeShared<FJsonObject>();
					MaterialObject->SetNumberField(TEXT("slot"), MaterialIndex);
					UMaterialInterface* SlotMaterial = MeshComponent->GetMaterial(MaterialIndex);
					MaterialObject->SetStringField(TEXT("material"), SlotMaterial ? SlotMaterial->GetPathName() : TEXT(""));
					Materials.Add(MakeShared<FJsonValueObject>(MaterialObject));
				}
			}
			ComponentObject->SetArrayField(TEXT("materials"), Materials);
			ComponentObject->SetObjectField(TEXT("properties"), DruidsMCPBridge::ExportEditablePropertyValues(ComponentTemplate));
			ComponentValues.Add(MakeShared<FJsonValueObject>(ComponentObject));

			RawText += FString::Printf(TEXT("%s\t%s\tmesh=%s\tparent=%s\n"),
				*Node->GetVariableName().ToString(),
				ComponentTemplate ? *ComponentTemplate->GetClass()->GetPathName() : TEXT(""),
				*StaticMeshPath,
				ParentComponentNames.Contains(Node) ? *ParentComponentNames[Node] : TEXT(""));
		}
	}

	TArray<TSharedPtr<FJsonValue>> CommentValues;
	TArray<UEdGraphNode_Comment*> CommentNodes;
	Graph->GetNodesOfClass<UEdGraphNode_Comment>(CommentNodes);
	RawText += FString::Printf(TEXT("\n# COMMENTS %d\n"), CommentNodes.Num());
	for (const UEdGraphNode_Comment* CommentNode : CommentNodes)
	{
		if (!CommentNode)
		{
			continue;
		}

		TSharedPtr<FJsonObject> CommentObject = MakeShared<FJsonObject>();
		CommentObject->SetStringField(TEXT("node_guid"), CommentNode->NodeGuid.ToString(EGuidFormats::Digits));
		CommentObject->SetStringField(TEXT("text"), CommentNode->NodeComment);
		CommentObject->SetNumberField(TEXT("x"), CommentNode->NodePosX);
		CommentObject->SetNumberField(TEXT("y"), CommentNode->NodePosY);
		CommentObject->SetNumberField(TEXT("width"), CommentNode->NodeWidth);
		CommentObject->SetNumberField(TEXT("height"), CommentNode->NodeHeight);
		CommentObject->SetNumberField(TEXT("font_size"), CommentNode->FontSize);
		CommentObject->SetStringField(TEXT("move_mode"), CommentNode->MoveMode == ECommentBoxMode::GroupMovement ? TEXT("group_movement") : TEXT("comment"));
		TArray<TSharedPtr<FJsonValue>> ContainedNodeGuids;
		const int32 Right = CommentNode->NodePosX + CommentNode->NodeWidth;
		const int32 Bottom = CommentNode->NodePosY + CommentNode->NodeHeight;
		for (const UEdGraphNode* CandidateNode : Graph->Nodes)
		{
			if (!CandidateNode || CandidateNode == CommentNode)
			{
				continue;
			}
			if (CandidateNode->NodePosX >= CommentNode->NodePosX &&
				CandidateNode->NodePosY >= CommentNode->NodePosY &&
				CandidateNode->NodePosX <= Right &&
				CandidateNode->NodePosY <= Bottom)
			{
				ContainedNodeGuids.Add(MakeShared<FJsonValueString>(CandidateNode->NodeGuid.ToString(EGuidFormats::Digits)));
			}
		}
		CommentObject->SetArrayField(TEXT("contained_node_guids"), ContainedNodeGuids);
		CommentValues.Add(MakeShared<FJsonValueObject>(CommentObject));

		RawText += FString::Printf(TEXT("%s\t%s\t%d,%d,%d,%d\tcontained=%d\n"),
			*CommentNode->NodeGuid.ToString(EGuidFormats::Digits),
			*CommentNode->NodeComment,
			CommentNode->NodePosX,
			CommentNode->NodePosY,
			CommentNode->NodeWidth,
			CommentNode->NodeHeight,
			ContainedNodeGuids.Num());
	}

	auto MakeUserPinInfoObject = [](const FUserPinInfo& PinInfo) -> TSharedPtr<FJsonObject>
	{
		UObject* SubCategoryObject = PinInfo.PinType.PinSubCategoryObject.Get();
		TSharedPtr<FJsonObject> Object = MakeShared<FJsonObject>();
		Object->SetStringField(TEXT("name"), PinInfo.PinName.ToString());
		Object->SetStringField(TEXT("pin_category"), PinInfo.PinType.PinCategory.ToString());
		Object->SetStringField(TEXT("pin_subcategory"), PinInfo.PinType.PinSubCategory.ToString());
		Object->SetStringField(TEXT("pin_subcategory_object"), SubCategoryObject ? SubCategoryObject->GetPathName() : TEXT(""));
		Object->SetStringField(TEXT("container_type"), DruidsMCPBridge::GetPinContainerTypeName(PinInfo.PinType));
		Object->SetStringField(TEXT("type_string"), DruidsMCPBridge::MakePinTypeString(PinInfo.PinType));
		Object->SetObjectField(TEXT("type"), DruidsMCPBridge::MakePinTypeObject(PinInfo.PinType));
		Object->SetStringField(TEXT("default_value"), PinInfo.PinDefaultValue);
		return Object;
	};

	TArray<TSharedPtr<FJsonValue>> CustomEventValues;
	TArray<UK2Node_CustomEvent*> CustomEventNodes;
	Graph->GetNodesOfClass<UK2Node_CustomEvent>(CustomEventNodes);
	RawText += FString::Printf(TEXT("\n# CUSTOM_EVENTS %d\n"), CustomEventNodes.Num());
	for (const UK2Node_CustomEvent* CustomEventNode : CustomEventNodes)
	{
		if (!CustomEventNode)
		{
			continue;
		}

		const FString EventName = CustomEventNode->CustomFunctionName.ToString();
		TArray<TSharedPtr<FJsonValue>> Parameters;
		for (const TSharedPtr<FUserPinInfo>& PinInfo : CustomEventNode->UserDefinedPins)
		{
			if (PinInfo.IsValid())
			{
				Parameters.Add(MakeShared<FJsonValueObject>(MakeUserPinInfoObject(*PinInfo)));
			}
		}

		TSharedPtr<FJsonObject> CustomEventObject = MakeShared<FJsonObject>();
		CustomEventObject->SetStringField(TEXT("name"), EventName);
		CustomEventObject->SetStringField(TEXT("graph"), Graph->GetName());
		CustomEventObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("custom_event::%s"), *EventName));
		CustomEventObject->SetStringField(TEXT("node_guid"), CustomEventNode->NodeGuid.ToString(EGuidFormats::Digits));
		CustomEventObject->SetBoolField(TEXT("call_in_editor"), CustomEventNode->bCallInEditor != 0);
		CustomEventObject->SetArrayField(TEXT("parameters"), Parameters);
		CustomEventValues.Add(MakeShared<FJsonValueObject>(CustomEventObject));

		RawText += FString::Printf(TEXT("%s\t%s\tparameters=%d\n"), *EventName, *Graph->GetName(), Parameters.Num());
	}

	TArray<TSharedPtr<FJsonValue>> ImplementedInterfaceValues;
	RawText += FString::Printf(TEXT("\n# IMPLEMENTED_INTERFACES %d\n"), Blueprint->ImplementedInterfaces.Num());
	for (const FBPInterfaceDescription& InterfaceDescription : Blueprint->ImplementedInterfaces)
	{
		UClass* InterfaceClass = InterfaceDescription.Interface;
		if (!InterfaceClass)
		{
			continue;
		}

		TArray<TSharedPtr<FJsonValue>> InterfaceGraphValues;
		for (UEdGraph* InterfaceGraph : InterfaceDescription.Graphs)
		{
			if (InterfaceGraph)
			{
				InterfaceGraphValues.Add(MakeShared<FJsonValueString>(InterfaceGraph->GetName()));
			}
		}

		TSharedPtr<FJsonObject> InterfaceObject = MakeShared<FJsonObject>();
		InterfaceObject->SetStringField(TEXT("interface_class_path"), InterfaceClass->GetPathName());
		InterfaceObject->SetStringField(TEXT("interface_class_name"), InterfaceClass->GetName());
		if (const UBlueprint* InterfaceBlueprint = Cast<UBlueprint>(InterfaceClass->ClassGeneratedBy))
		{
			InterfaceObject->SetStringField(TEXT("interface_blueprint_path"), InterfaceBlueprint->GetPathName());
			InterfaceObject->SetStringField(TEXT("interface_asset_path"), InterfaceBlueprint->GetOutermost()->GetName());
		}
		InterfaceObject->SetArrayField(TEXT("graphs"), InterfaceGraphValues);
		ImplementedInterfaceValues.Add(MakeShared<FJsonValueObject>(InterfaceObject));
		RawText += FString::Printf(TEXT("%s\tgraphs=%d\n"), *InterfaceClass->GetPathName(), InterfaceGraphValues.Num());
	}

	TArray<TSharedPtr<FJsonValue>> InterfaceImplementationValues;
	TArray<UK2Node_Event*> EventNodes;
	Graph->GetNodesOfClass<UK2Node_Event>(EventNodes);
	RawText += FString::Printf(TEXT("\n# INTERFACE_IMPLEMENTATIONS %d\n"), EventNodes.Num());
	for (const UK2Node_Event* EventNode : EventNodes)
	{
		if (!EventNode)
		{
			continue;
		}

		UClass* EventOwnerClass = EventNode->EventReference.GetMemberParentClass(EventNode->GetBlueprintClassFromNode());
		if (!EventOwnerClass || !EventOwnerClass->IsChildOf(UInterface::StaticClass()))
		{
			continue;
		}

		TArray<TSharedPtr<FJsonValue>> Parameters;
		for (UEdGraphPin* Pin : EventNode->Pins)
		{
			if (!Pin ||
				Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec ||
				Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Delegate)
			{
				continue;
			}
			Parameters.Add(MakeShared<FJsonValueObject>(MakePinParameterObject(Pin)));
		}

		const FString EventName = EventNode->EventReference.GetMemberName().ToString();
		TSharedPtr<FJsonObject> InterfaceImplementation = MakeShared<FJsonObject>();
		InterfaceImplementation->SetStringField(TEXT("interface_class_path"), EventOwnerClass->GetPathName());
		InterfaceImplementation->SetStringField(TEXT("interface_class_name"), EventOwnerClass->GetName());
		if (const UBlueprint* InterfaceBlueprint = Cast<UBlueprint>(EventOwnerClass->ClassGeneratedBy))
		{
			InterfaceImplementation->SetStringField(TEXT("interface_blueprint_path"), InterfaceBlueprint->GetPathName());
			InterfaceImplementation->SetStringField(TEXT("interface_asset_path"), InterfaceBlueprint->GetOutermost()->GetName());
		}
		InterfaceImplementation->SetStringField(TEXT("function_name"), EventName);
		InterfaceImplementation->SetStringField(TEXT("graph"), Graph->GetName());
		InterfaceImplementation->SetStringField(TEXT("node_guid"), EventNode->NodeGuid.ToString(EGuidFormats::Digits));
		InterfaceImplementation->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("interface_event::%s:%s"), *EventOwnerClass->GetPathName(), *EventName));
		InterfaceImplementation->SetArrayField(TEXT("parameters"), Parameters);
		InterfaceImplementationValues.Add(MakeShared<FJsonValueObject>(InterfaceImplementation));
		RawText += FString::Printf(TEXT("%s\t%s\t%s\tparameters=%d\n"), *EventOwnerClass->GetPathName(), *EventName, *Graph->GetName(), Parameters.Num());
	}

	TArray<UK2Node_FunctionEntry*> InterfaceFunctionEntryNodes;
	Graph->GetNodesOfClass<UK2Node_FunctionEntry>(InterfaceFunctionEntryNodes);
	for (const UK2Node_FunctionEntry* FunctionEntryNode : InterfaceFunctionEntryNodes)
	{
		if (!FunctionEntryNode)
		{
			continue;
		}

		UClass* FunctionOwnerClass = FunctionEntryNode->FunctionReference.GetMemberParentClass(Blueprint->GeneratedClass);
		if (!FunctionOwnerClass || !FunctionOwnerClass->IsChildOf(UInterface::StaticClass()))
		{
			continue;
		}

		const FString FunctionName = FunctionEntryNode->CustomGeneratedFunctionName != NAME_None
			? FunctionEntryNode->CustomGeneratedFunctionName.ToString()
			: FunctionEntryNode->FunctionReference.GetMemberName().ToString();
		const FString ResolvedFunctionName = FunctionName.IsEmpty() ? Graph->GetName() : FunctionName;
		TArray<TSharedPtr<FJsonValue>> Parameters;
		for (UEdGraphPin* Pin : FunctionEntryNode->Pins)
		{
			if (!Pin ||
				Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec ||
				Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Delegate)
			{
				continue;
			}
			Parameters.Add(MakeShared<FJsonValueObject>(MakePinParameterObject(Pin)));
		}

		TSharedPtr<FJsonObject> InterfaceImplementation = MakeShared<FJsonObject>();
		InterfaceImplementation->SetStringField(TEXT("interface_class_path"), FunctionOwnerClass->GetPathName());
		InterfaceImplementation->SetStringField(TEXT("interface_class_name"), FunctionOwnerClass->GetName());
		if (const UBlueprint* InterfaceBlueprint = Cast<UBlueprint>(FunctionOwnerClass->ClassGeneratedBy))
		{
			InterfaceImplementation->SetStringField(TEXT("interface_blueprint_path"), InterfaceBlueprint->GetPathName());
			InterfaceImplementation->SetStringField(TEXT("interface_asset_path"), InterfaceBlueprint->GetOutermost()->GetName());
		}
		InterfaceImplementation->SetStringField(TEXT("function_name"), ResolvedFunctionName);
		InterfaceImplementation->SetStringField(TEXT("graph"), Graph->GetName());
		InterfaceImplementation->SetStringField(TEXT("node_guid"), FunctionEntryNode->NodeGuid.ToString(EGuidFormats::Digits));
		InterfaceImplementation->SetStringField(TEXT("implementation_kind"), TEXT("function_entry"));
		InterfaceImplementation->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("interface_function::%s:%s"), *FunctionOwnerClass->GetPathName(), *ResolvedFunctionName));
		InterfaceImplementation->SetArrayField(TEXT("parameters"), Parameters);
		InterfaceImplementationValues.Add(MakeShared<FJsonValueObject>(InterfaceImplementation));
		RawText += FString::Printf(TEXT("%s\t%s\t%s\tparameters=%d\n"), *FunctionOwnerClass->GetPathName(), *ResolvedFunctionName, *Graph->GetName(), Parameters.Num());
	}

	TArray<TSharedPtr<FJsonValue>> FunctionValues;
	RawText += FString::Printf(TEXT("\n# FUNCTIONS %d\n"), Blueprint->FunctionGraphs.Num());
	for (UEdGraph* FunctionGraph : Blueprint->FunctionGraphs)
	{
		if (!FunctionGraph)
		{
			continue;
		}

		TArray<UK2Node_FunctionEntry*> EntryNodes;
		TArray<UK2Node_FunctionResult*> ResultNodes;
		FunctionGraph->GetNodesOfClass<UK2Node_FunctionEntry>(EntryNodes);
		FunctionGraph->GetNodesOfClass<UK2Node_FunctionResult>(ResultNodes);
		const UK2Node_FunctionEntry* EntryNode = EntryNodes.Num() > 0 ? EntryNodes[0] : nullptr;
		const UK2Node_FunctionResult* ResultNode = ResultNodes.Num() > 0 ? ResultNodes[0] : nullptr;
		const FString FunctionName = EntryNode && EntryNode->CustomGeneratedFunctionName != NAME_None
			? EntryNode->CustomGeneratedFunctionName.ToString()
			: FunctionGraph->GetName();

		TArray<TSharedPtr<FJsonValue>> Inputs;
		TArray<TSharedPtr<FJsonValue>> Outputs;
		if (EntryNode)
		{
			for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
			{
				if (PinInfo.IsValid())
				{
					Inputs.Add(MakeShared<FJsonValueObject>(MakeUserPinInfoObject(*PinInfo)));
				}
			}
		}
		if (ResultNode)
		{
			for (const TSharedPtr<FUserPinInfo>& PinInfo : ResultNode->UserDefinedPins)
			{
				if (PinInfo.IsValid())
				{
					Outputs.Add(MakeShared<FJsonValueObject>(MakeUserPinInfoObject(*PinInfo)));
				}
			}
		}

		TSharedPtr<FJsonObject> FunctionObject = MakeShared<FJsonObject>();
		FunctionObject->SetStringField(TEXT("name"), FunctionName);
		FunctionObject->SetStringField(TEXT("graph"), FunctionGraph->GetName());
		FunctionObject->SetStringField(TEXT("spawner_id"), FString::Printf(TEXT("bp_func::%s"), *FunctionName));
		FunctionObject->SetArrayField(TEXT("inputs"), Inputs);
		FunctionObject->SetArrayField(TEXT("outputs"), Outputs);
		FunctionValues.Add(MakeShared<FJsonValueObject>(FunctionObject));

		RawText += FString::Printf(TEXT("%s\t%s\tinputs=%d\toutputs=%d\n"), *FunctionName, *FunctionGraph->GetName(), Inputs.Num(), Outputs.Num());
	}

	TSharedPtr<FJsonObject> Parsed = MakeShared<FJsonObject>();
	Parsed->SetStringField(TEXT("graph"), Graph->GetName());
	Parsed->SetStringField(TEXT("graph_type"), GraphType);
	Parsed->SetArrayField(TEXT("nodes"), NodeValues);
	Parsed->SetArrayField(TEXT("connections"), ConnectionValues);
	Parsed->SetArrayField(TEXT("variables"), VariableValues);
	Parsed->SetArrayField(TEXT("components"), ComponentValues);
	Parsed->SetArrayField(TEXT("comments"), CommentValues);
	Parsed->SetArrayField(TEXT("custom_events"), CustomEventValues);
	Parsed->SetArrayField(TEXT("implemented_interfaces"), ImplementedInterfaceValues);
	Parsed->SetArrayField(TEXT("interface_implementations"), InterfaceImplementationValues);
	Parsed->SetArrayField(TEXT("functions"), FunctionValues);
	Parsed->SetNumberField(TEXT("node_count"), NodeValues.Num());
	Parsed->SetNumberField(TEXT("connection_count"), ConnectionValues.Num());
	Parsed->SetNumberField(TEXT("variable_count"), VariableValues.Num());
	Parsed->SetNumberField(TEXT("component_count"), ComponentValues.Num());
	Parsed->SetNumberField(TEXT("comment_count"), CommentValues.Num());
	Parsed->SetNumberField(TEXT("custom_event_count"), CustomEventValues.Num());
	Parsed->SetNumberField(TEXT("implemented_interface_count"), ImplementedInterfaceValues.Num());
	Parsed->SetNumberField(TEXT("interface_implementation_count"), InterfaceImplementationValues.Num());
	Parsed->SetNumberField(TEXT("function_count"), FunctionValues.Num());

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetStringField(TEXT("raw_result"), RawText);
	Result->SetObjectField(TEXT("parsed_graph"), Parsed);
	return Result;
}

TArray<TSharedPtr<FJsonValue>> FDruidsMCPBridgeServer::FindNativeNodeSpawners(UBlueprint* Blueprint, const FString& Query, const FString& InputType, const FString& OutputType, int32 Limit) const
{
	TArray<TSharedPtr<FJsonValue>> Nodes;
	auto AddNode = [&Nodes, Limit](const FString& Name, const FString& SpawnerId, const FString& Category, const TArray<FString>& Inputs, const TArray<FString>& Outputs, const TArray<FString>& Flags = {})
	{
		if (Nodes.Num() >= Limit)
		{
			return;
		}
		TSharedPtr<FJsonObject> Node = MakeShared<FJsonObject>();
		Node->SetStringField(TEXT("name"), Name);
		Node->SetStringField(TEXT("spawner_id"), SpawnerId);
		Node->SetStringField(TEXT("category"), Category);
		Node->SetArrayField(TEXT("inputs"), MakeStringArrayValue(Inputs));
		Node->SetArrayField(TEXT("outputs"), MakeStringArrayValue(Outputs));
		Node->SetArrayField(TEXT("flags"), MakeStringArrayValue(Flags));
		Nodes.Add(MakeShared<FJsonValueObject>(Node));
	};

	const FString Normalized = NormalizeResolverText(Query);
	if (IsUnsupportedInputEventRequest(Query, TEXT("")))
	{
		return Nodes;
	}
	if (Query.StartsWith(TEXT("cast::")))
	{
		FString CanonicalClassPath;
		FString ResolveError;
		UClass* TargetClass = DruidsMCPBridge::ResolveDynamicCastTargetClass(Query.RightChop(6), CanonicalClassPath, ResolveError);
		if (TargetClass)
		{
			const FString DisplayName = TargetClass->GetDisplayNameText().ToString();
			AddNode(FString::Printf(TEXT("Cast To %s"), *(DisplayName.IsEmpty() ? TargetClass->GetName() : DisplayName)),
				FString::Printf(TEXT("cast::%s"), *CanonicalClassPath),
				TEXT("Utilities|Casting"),
				{ TEXT("execute (exec)"), TEXT("Object (object)") },
				{ TEXT("then (exec)"), TEXT("CastFailed (exec)"), FString::Printf(TEXT("As %s (object)"), *TargetClass->GetName()) },
				{ TEXT("Cast") });
		}
		return Nodes;
	}
	if (Query.StartsWith(TEXT("var_get::")) || Query.StartsWith(TEXT("var_set::")))
	{
		const bool bSet = Query.StartsWith(TEXT("var_set::"));
		FString ExternalClassPath;
		FString ExternalVarName;
		if (DruidsMCPBridge::ParseExternalVariableSpawnerSpec(Query, bSet ? TEXT("var_set::") : TEXT("var_get::"), ExternalClassPath, ExternalVarName))
		{
			FString CanonicalClassPath;
			FString ResolveError;
			UClass* OwnerClass = nullptr;
			if (DruidsMCPBridge::ResolveExternalVariableReference(ExternalClassPath, ExternalVarName, OwnerClass, CanonicalClassPath, ResolveError))
			{
				TArray<FString> Inputs;
				TArray<FString> Outputs;
				if (bSet)
				{
					Inputs = { TEXT("execute (exec)"), TEXT("self (object)"), FString::Printf(TEXT("%s (value)"), *ExternalVarName) };
					Outputs = { TEXT("then (exec)") };
				}
				else
				{
					Inputs = { TEXT("self (object)") };
					Outputs = { FString::Printf(TEXT("%s (value)"), *ExternalVarName) };
				}

				AddNode(FString::Printf(TEXT("%s %s"), bSet ? TEXT("Set") : TEXT("Get"), *ExternalVarName),
					FString::Printf(TEXT("var_%s::%s:%s"), bSet ? TEXT("set") : TEXT("get"), *CanonicalClassPath, *ExternalVarName),
					TEXT("Variables|External"),
					Inputs,
					Outputs,
					{ TEXT("Variable"), TEXT("External") });
			}
			return Nodes;
		}
	}
	if (Normalized.IsEmpty() || Normalized.Contains(TEXT("print")) || Normalized == TEXT("printstring"))
	{
		AddNode(TEXT("Print String"), TEXT("func::/Script/Engine.KismetSystemLibrary:PrintString"), TEXT("Development"), { TEXT("execute (exec)"), TEXT("InString (string)") }, { TEXT("then (exec)") });
	}
	if (Normalized.IsEmpty() || Normalized.Contains(TEXT("branch")) || Normalized.Contains(TEXT("if")))
	{
		AddNode(TEXT("Branch"), TEXT("node::branch"), TEXT("Flow Control"), { TEXT("execute (exec)"), TEXT("Condition (bool)") }, { TEXT("then (exec)"), TEXT("else (exec)") });
	}
	if (Normalized.IsEmpty() || Query.Equals(TEXT("node::sequence"), ESearchCase::IgnoreCase) || Normalized == TEXT("sequence") || Normalized.Contains(TEXT("executionsequence")))
	{
		AddNode(TEXT("Sequence"), TEXT("node::sequence"), TEXT("Flow Control"), { TEXT("execute (exec)") }, { TEXT("then_0 (exec)"), TEXT("then_1 (exec)") });
	}
	if (Normalized.IsEmpty() || Query.Equals(DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("DoOnce")), ESearchCase::IgnoreCase) || Normalized == TEXT("doonce"))
	{
		AddNode(TEXT("DoOnce"), DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("DoOnce")), TEXT("Utilities|Flow Control"), { TEXT("execute (exec)"), TEXT("Reset (exec)"), TEXT("Start Closed (bool)") }, { TEXT("Completed (exec)") }, { TEXT("Macro") });
	}
	if (Normalized.IsEmpty() || Query.Equals(DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("Gate")), ESearchCase::IgnoreCase) || Normalized == TEXT("gate"))
	{
		AddNode(TEXT("Gate"), DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("Gate")), TEXT("Utilities|Flow Control"), { TEXT("Enter (exec)"), TEXT("Open (exec)"), TEXT("Close (exec)"), TEXT("Toggle (exec)"), TEXT("bStartClosed (bool)") }, { TEXT("Exit (exec)") }, { TEXT("Macro") });
	}
	if (Normalized.IsEmpty() || Query.Equals(DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("FlipFlop")), ESearchCase::IgnoreCase) || Normalized == TEXT("flipflop"))
	{
		AddNode(TEXT("FlipFlop"), DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("FlipFlop")), TEXT("Utilities|Flow Control"), { TEXT("None (exec)") }, { TEXT("A (exec)"), TEXT("B (exec)"), TEXT("IsA (bool)") }, { TEXT("Macro") });
	}
	if (Normalized.IsEmpty() || Normalized.Contains(TEXT("add")) || Normalized.Contains(TEXT("plus")) || Normalized.Contains(TEXT("+")))
	{
		AddNode(TEXT("int + int"), TEXT("func::/Script/Engine.KismetMathLibrary:Add_IntInt"), TEXT("Math|Integer"), { TEXT("A (int)"), TEXT("B (int)") }, { TEXT("ReturnValue (int)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("float+float") || Normalized.Contains(TEXT("floatadd")) || Normalized.Contains(TEXT("adddouble")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:Add_DoubleDouble"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("float + float"), TEXT("func::/Script/Engine.KismetMathLibrary:Add_DoubleDouble"), TEXT("Math|Float"), { TEXT("A (real)"), TEXT("B (real)") }, { TEXT("ReturnValue (real)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("float-float") || Normalized.Contains(TEXT("floatsubtract")) || Normalized.Contains(TEXT("subtractdouble")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:Subtract_DoubleDouble"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("float - float"), TEXT("func::/Script/Engine.KismetMathLibrary:Subtract_DoubleDouble"), TEXT("Math|Float"), { TEXT("A (real)"), TEXT("B (real)") }, { TEXT("ReturnValue (real)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("float*float") || Normalized.Contains(TEXT("floatmultiply")) || Normalized.Contains(TEXT("multiplydouble")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:Multiply_DoubleDouble"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("float * float"), TEXT("func::/Script/Engine.KismetMathLibrary:Multiply_DoubleDouble"), TEXT("Math|Float"), { TEXT("A (real)"), TEXT("B (real)") }, { TEXT("ReturnValue (real)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("float/float") || Normalized.Contains(TEXT("floatdivide")) || Normalized.Contains(TEXT("dividedouble")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:Divide_DoubleDouble"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("float / float"), TEXT("func::/Script/Engine.KismetMathLibrary:Divide_DoubleDouble"), TEXT("Math|Float"), { TEXT("A (real)"), TEXT("B (real)") }, { TEXT("ReturnValue (real)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("integer>integer") || Normalized == TEXT("int>int") || Normalized.Contains(TEXT("greaterint")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:Greater_IntInt"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("integer > integer"), TEXT("func::/Script/Engine.KismetMathLibrary:Greater_IntInt"), TEXT("Math|Integer"), { TEXT("A (int)"), TEXT("B (int)") }, { TEXT("ReturnValue (bool)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("integer<integer") || Normalized == TEXT("int<int") || Normalized.Contains(TEXT("lessint")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:Less_IntInt"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("integer < integer"), TEXT("func::/Script/Engine.KismetMathLibrary:Less_IntInt"), TEXT("Math|Integer"), { TEXT("A (int)"), TEXT("B (int)") }, { TEXT("ReturnValue (bool)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("float>float") || Normalized.Contains(TEXT("greaterdouble")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:Greater_DoubleDouble"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("float > float"), TEXT("func::/Script/Engine.KismetMathLibrary:Greater_DoubleDouble"), TEXT("Math|Float"), { TEXT("A (real)"), TEXT("B (real)") }, { TEXT("ReturnValue (bool)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("float<float") || Normalized.Contains(TEXT("lessdouble")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:Less_DoubleDouble"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("float < float"), TEXT("func::/Script/Engine.KismetMathLibrary:Less_DoubleDouble"), TEXT("Math|Float"), { TEXT("A (real)"), TEXT("B (real)") }, { TEXT("ReturnValue (bool)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("boolean==boolean") || Normalized == TEXT("bool==bool") || Normalized.Contains(TEXT("equalbool")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:EqualEqual_BoolBool"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("Equal (Boolean)"), TEXT("func::/Script/Engine.KismetMathLibrary:EqualEqual_BoolBool"), TEXT("Math|Boolean"), { TEXT("A (bool)"), TEXT("B (bool)") }, { TEXT("ReturnValue (bool)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("string==string") || Normalized.Contains(TEXT("equalstring")) || Query.Equals(TEXT("func::/Script/Engine.KismetStringLibrary:EqualEqual_StrStr"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("Equal Exactly (String)"), TEXT("func::/Script/Engine.KismetStringLibrary:EqualEqual_StrStr"), TEXT("Utilities|String"), { TEXT("A (string)"), TEXT("B (string)") }, { TEXT("ReturnValue (bool)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized == TEXT("makevector") || Normalized.Contains(TEXT("vector")) || Query.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:MakeVector"), ESearchCase::IgnoreCase))
	{
		AddNode(TEXT("Make Vector"), TEXT("func::/Script/Engine.KismetMathLibrary:MakeVector"), TEXT("Math|Vector"), { TEXT("X (real)"), TEXT("Y (real)"), TEXT("Z (real)") }, { TEXT("ReturnValue (vector)") }, { TEXT("Pure") });
	}
	if (Normalized.IsEmpty() || Normalized.Contains(TEXT("tostring")) || Normalized.Contains(TEXT("convert")) || Normalized.Contains(TEXT("convinttostring")))
	{
		AddNode(TEXT("To String (Integer)"), TEXT("func::/Script/Engine.KismetStringLibrary:Conv_IntToString"), TEXT("Utilities|String"), { TEXT("InInt (int)") }, { TEXT("ReturnValue (string)") }, { TEXT("Pure") });
	}
	for (const DruidsMCPBridge::FAllowlistedComponentFunction& Function : DruidsMCPBridge::GetAllowlistedComponentFunctions())
	{
		const FString SearchText = NormalizeResolverText(FString(Function.DisplayName) + TEXT(" ") + Function.FunctionName + TEXT(" ") + Function.Category + TEXT(" ") + Function.SpawnerId);
		if (Normalized.IsEmpty() || Query.Equals(Function.SpawnerId, ESearchCase::IgnoreCase) || SearchText.Contains(Normalized))
		{
			AddNode(Function.DisplayName, Function.SpawnerId, Function.Category,
				{ FString::Printf(TEXT("%s (exec)"), Function.ExecInput), FString::Printf(TEXT("%s (object)"), Function.TargetPin), FString::Printf(TEXT("%s (value)"), Function.VectorPin) },
				{ FString::Printf(TEXT("%s (exec)"), Function.ExecOutput) });
		}
	}
	if (!Normalized.Contains(TEXT("addevent")) &&
		(Query.Equals(TEXT("input_axis_key::MouseWheelAxis"), ESearchCase::IgnoreCase) ||
			Normalized.IsEmpty() ||
			Normalized == TEXT("mousewheelaxis") ||
			Normalized.Contains(TEXT("mousewheel"))))
	{
		AddNode(TEXT("MouseWheelAxis"), TEXT("input_axis_key::MouseWheelAxis"), TEXT("Input|Mouse"), {}, { TEXT("then (exec)"), TEXT("AxisValue (real)") }, { TEXT("Event"), TEXT("InputAxisKey") });
	}
	if (!Normalized.Contains(TEXT("addevent")) &&
		(Query.Equals(TEXT("input_key::LeftMouseButton"), ESearchCase::IgnoreCase) ||
			Normalized.IsEmpty() ||
			Normalized == TEXT("leftmousebutton") ||
			Normalized.Contains(TEXT("leftmouse"))))
	{
		AddNode(TEXT("LeftMouseButton"), TEXT("input_key::LeftMouseButton"), TEXT("Input|Mouse"), {}, { TEXT("Pressed (exec)"), TEXT("Released (exec)"), TEXT("Key (key)") }, { TEXT("Event"), TEXT("InputKey") });
	}
	FString ResolvedInputKeyName;
	if (!Normalized.Contains(TEXT("addevent")) &&
		DruidsMCPBridge::TryResolveInputKeyNameFromQuery(Query, ResolvedInputKeyName))
	{
		const FKey InputKey = FKey(FName(*ResolvedInputKeyName));
		if (InputKey.IsValid() && !InputKey.IsAnalog() && InputKey.IsBindableInBlueprints())
		{
			AddNode(ResolvedInputKeyName, FString::Printf(TEXT("input_key::%s"), *ResolvedInputKeyName), TEXT("Input|Keyboard"), {}, { TEXT("Pressed (exec)"), TEXT("Released (exec)"), TEXT("Key (key)") }, { TEXT("Event"), TEXT("InputKey") });
		}
	}

	if (Query.StartsWith(TEXT("custom_event::"), ESearchCase::IgnoreCase))
	{
		const FString EventName = Query.RightChop(14).TrimStartAndEnd();
		if (!EventName.IsEmpty())
		{
			AddNode(FString::Printf(TEXT("Custom Event %s"), *EventName), FString::Printf(TEXT("custom_event::%s"), *EventName), TEXT("Events|Custom"), {}, { TEXT("then (exec)") }, { TEXT("Event"), TEXT("CustomEvent") });
		}
	}
	if (Query.StartsWith(TEXT("custom_event_call::"), ESearchCase::IgnoreCase))
	{
		const FString EventName = Query.RightChop(19).TrimStartAndEnd();
		if (!EventName.IsEmpty())
		{
			AddNode(FString::Printf(TEXT("Call %s"), *EventName), FString::Printf(TEXT("custom_event_call::%s"), *EventName), TEXT("Events|Custom"), { TEXT("execute (exec)") }, { TEXT("then (exec)") }, { TEXT("Call"), TEXT("CustomEvent") });
		}
	}

	TArray<UEdGraph*> Graphs;
	Blueprint->GetAllGraphs(Graphs);
	for (UEdGraph* CandidateGraph : Graphs)
	{
		if (!CandidateGraph)
		{
			continue;
		}

		TArray<UK2Node_CustomEvent*> CustomEvents;
		CandidateGraph->GetNodesOfClass<UK2Node_CustomEvent>(CustomEvents);
		for (const UK2Node_CustomEvent* CustomEvent : CustomEvents)
		{
			if (!CustomEvent)
			{
				continue;
			}

			const FString EventName = CustomEvent->CustomFunctionName.ToString();
			if (!Normalized.IsEmpty() &&
				!Query.Equals(FString::Printf(TEXT("custom_event::%s"), *EventName), ESearchCase::IgnoreCase) &&
				!NormalizeResolverText(EventName).Contains(Normalized) &&
				!Normalized.Contains(NormalizeResolverText(EventName)))
			{
				continue;
			}

			TArray<FString> Outputs = { TEXT("then (exec)") };
			TArray<FString> CallInputs = { TEXT("execute (exec)") };
			TArray<FString> CallOutputs = { TEXT("then (exec)") };
			for (const TSharedPtr<FUserPinInfo>& PinInfo : CustomEvent->UserDefinedPins)
			{
				if (PinInfo.IsValid())
				{
					Outputs.Add(FString::Printf(TEXT("%s (%s)"), *PinInfo->PinName.ToString(), *PinInfo->PinType.PinCategory.ToString()));
					CallInputs.Add(FString::Printf(TEXT("%s (%s)"), *PinInfo->PinName.ToString(), *PinInfo->PinType.PinCategory.ToString()));
				}
			}
			AddNode(FString::Printf(TEXT("Custom Event %s"), *EventName), FString::Printf(TEXT("custom_event::%s"), *EventName), TEXT("Events|Custom"), {}, Outputs, { TEXT("Event"), TEXT("CustomEvent") });
			AddNode(FString::Printf(TEXT("Call %s"), *EventName), FString::Printf(TEXT("custom_event_call::%s"), *EventName), TEXT("Events|Custom"), CallInputs, CallOutputs, { TEXT("Call"), TEXT("CustomEvent") });
		}
	}

	for (const FBPVariableDescription& Variable : Blueprint->NewVariables)
	{
		const FString VarName = Variable.VarName.ToString();
		if (!Normalized.IsEmpty() && !NormalizeResolverText(VarName).Contains(Normalized) && !Normalized.Contains(NormalizeResolverText(VarName)))
		{
			continue;
		}
		AddNode(FString::Printf(TEXT("Get %s"), *VarName), FString::Printf(TEXT("var_get::%s"), *VarName), TEXT("Variables"), {}, { VarName });
		AddNode(FString::Printf(TEXT("Set %s"), *VarName), FString::Printf(TEXT("var_set::%s"), *VarName), TEXT("Variables"), { TEXT("execute (exec)"), VarName }, { TEXT("then (exec)") });
	}

	if (Blueprint && Blueprint->SimpleConstructionScript)
	{
		for (USCS_Node* ComponentNode : Blueprint->SimpleConstructionScript->GetAllNodes())
		{
			if (!ComponentNode)
			{
				continue;
			}
			const FString ComponentName = ComponentNode->GetVariableName().ToString();
			if (!Normalized.IsEmpty() && !NormalizeResolverText(ComponentName).Contains(Normalized) && !Normalized.Contains(NormalizeResolverText(ComponentName)))
			{
				continue;
			}
			AddNode(FString::Printf(TEXT("Get %s"), *ComponentName), FString::Printf(TEXT("var_get::%s"), *ComponentName), TEXT("Components"), {}, { ComponentName }, { TEXT("Component") });
		}
	}

	for (UEdGraph* FunctionGraph : Blueprint->FunctionGraphs)
	{
		if (!FunctionGraph)
		{
			continue;
		}

		const FString FunctionName = FunctionGraph->GetName();
		if (!Normalized.IsEmpty() &&
			!Query.Equals(FString::Printf(TEXT("bp_func::%s"), *FunctionName), ESearchCase::IgnoreCase) &&
			!NormalizeResolverText(FunctionName).Contains(Normalized) &&
			!Normalized.Contains(NormalizeResolverText(FunctionName)))
		{
			continue;
		}

		TArray<UK2Node_FunctionEntry*> EntryNodes;
		TArray<UK2Node_FunctionResult*> ResultNodes;
		FunctionGraph->GetNodesOfClass<UK2Node_FunctionEntry>(EntryNodes);
		FunctionGraph->GetNodesOfClass<UK2Node_FunctionResult>(ResultNodes);

		TArray<FString> Inputs = { TEXT("execute (exec)") };
		TArray<FString> Outputs = { TEXT("then (exec)") };
		if (EntryNodes.Num() > 0 && EntryNodes[0])
		{
			for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNodes[0]->UserDefinedPins)
			{
				if (PinInfo.IsValid())
				{
					Inputs.Add(FString::Printf(TEXT("%s (%s)"), *PinInfo->PinName.ToString(), *PinInfo->PinType.PinCategory.ToString()));
				}
			}
		}
		if (ResultNodes.Num() > 0 && ResultNodes[0])
		{
			for (const TSharedPtr<FUserPinInfo>& PinInfo : ResultNodes[0]->UserDefinedPins)
			{
				if (PinInfo.IsValid())
				{
					Outputs.Add(FString::Printf(TEXT("%s (%s)"), *PinInfo->PinName.ToString(), *PinInfo->PinType.PinCategory.ToString()));
				}
			}
		}

		AddNode(FString::Printf(TEXT("Call %s"), *FunctionName), FString::Printf(TEXT("bp_func::%s"), *FunctionName), TEXT("Blueprint Functions"), Inputs, Outputs);
	}

	return Nodes;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::AddNativeBlueprintNode(UBlueprint* Blueprint, UEdGraph* Graph, const FString& SpawnerId, const FString& NodeName, const TSharedPtr<FJsonObject>& PinDefaults, FString& OutError) const
{
	if (SpawnerId.IsEmpty())
	{
		OutError = TEXT("SpawnerId is required.");
		return nullptr;
	}

	UEdGraphNode* Node = nullptr;
	FString EffectiveSpawnerId = SpawnerId;
	if (SpawnerId == TEXT("node::branch"))
	{
		Node = NewObject<UK2Node_IfThenElse>(Graph);
	}
	else if (SpawnerId == TEXT("node::sequence"))
	{
		Node = NewObject<UK2Node_ExecutionSequence>(Graph);
	}
	else if (SpawnerId.StartsWith(TEXT("macro::")))
	{
		FString Spec = SpawnerId.RightChop(7);
		FString MacroLibraryPath;
		FString MacroName;
		Spec.Split(TEXT(":"), &MacroLibraryPath, &MacroName, ESearchCase::CaseSensitive, ESearchDir::FromEnd);
		if (!MacroLibraryPath.Equals(DruidsMCPBridge::StandardMacrosSpawnerPath, ESearchCase::IgnoreCase))
		{
			OutError = FString::Printf(TEXT("Unsupported macro spawner library: %s"), *SpawnerId);
			return nullptr;
		}

		FString MacroError;
		UEdGraph* MacroGraph = DruidsMCPBridge::FindStandardFlowMacroGraph(MacroName, MacroError);
		if (!MacroGraph)
		{
			OutError = MacroError;
			return nullptr;
		}

		UK2Node_MacroInstance* MacroNode = NewObject<UK2Node_MacroInstance>(Graph);
		MacroNode->SetMacroGraph(MacroGraph);
		Node = MacroNode;
	}
	else if (SpawnerId.StartsWith(TEXT("cast::")))
	{
		FString CanonicalClassPath;
		FString ResolveError;
		UClass* TargetClass = DruidsMCPBridge::ResolveDynamicCastTargetClass(SpawnerId.RightChop(6), CanonicalClassPath, ResolveError);
		if (!TargetClass)
		{
			OutError = ResolveError.IsEmpty() ? FString::Printf(TEXT("Could not resolve dynamic cast spawner: %s"), *SpawnerId) : ResolveError;
			return nullptr;
		}

		UK2Node_DynamicCast* CastNode = NewObject<UK2Node_DynamicCast>(Graph);
		CastNode->TargetType = TargetClass;
		Node = CastNode;
		EffectiveSpawnerId = FString::Printf(TEXT("cast::%s"), *CanonicalClassPath);
	}
	else if (SpawnerId.StartsWith(TEXT("func::")))
	{
		FString Spec = SpawnerId.RightChop(6);
		FString ClassPath;
		FString FunctionName;
		Spec.Split(TEXT(":"), &ClassPath, &FunctionName, ESearchCase::CaseSensitive, ESearchDir::FromEnd);
		UClass* Class = LoadObject<UClass>(nullptr, *ClassPath);
		UFunction* Function = Class ? Class->FindFunctionByName(FName(*FunctionName)) : nullptr;
		if (!Function)
		{
			OutError = FString::Printf(TEXT("Could not resolve function spawner: %s"), *SpawnerId);
			return nullptr;
		}
		UK2Node_CallFunction* FunctionNode = NewObject<UK2Node_CallFunction>(Graph);
		FunctionNode->FunctionReference.SetExternalMember(Function->GetFName(), Class);
		Node = FunctionNode;
	}
	else if (SpawnerId.StartsWith(TEXT("bp_func::")))
	{
		const FString FunctionName = SpawnerId.RightChop(9);
		if (FunctionName.IsEmpty())
		{
			OutError = FString::Printf(TEXT("Invalid Blueprint function spawner id: %s"), *SpawnerId);
			return nullptr;
		}

		UEdGraph* FunctionGraph = nullptr;
		for (UEdGraph* CandidateGraph : Blueprint->FunctionGraphs)
		{
			if (CandidateGraph && CandidateGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
			{
				FunctionGraph = CandidateGraph;
				break;
			}
		}
		if (!FunctionGraph)
		{
			OutError = FString::Printf(TEXT("Blueprint function does not exist for spawner id: %s"), *SpawnerId);
			return nullptr;
		}

		UK2Node_CallFunction* FunctionNode = NewObject<UK2Node_CallFunction>(Graph);
		FunctionNode->FunctionReference.SetSelfMember(FName(*FunctionName));
		Node = FunctionNode;
	}
	else if (SpawnerId.StartsWith(TEXT("custom_event::")))
	{
		const FString EventName = SpawnerId.RightChop(14).TrimStartAndEnd();
		if (EventName.IsEmpty())
		{
			OutError = FString::Printf(TEXT("Invalid custom event spawner id: %s"), *SpawnerId);
			return nullptr;
		}

		TArray<UEdGraph*> Graphs;
		Blueprint->GetAllGraphs(Graphs);
		for (UEdGraph* CandidateGraph : Graphs)
		{
			if (!CandidateGraph)
			{
				continue;
			}

			TArray<UK2Node_CustomEvent*> ExistingEvents;
			CandidateGraph->GetNodesOfClass<UK2Node_CustomEvent>(ExistingEvents);
			for (const UK2Node_CustomEvent* ExistingEvent : ExistingEvents)
			{
				if (ExistingEvent && ExistingEvent->CustomFunctionName.ToString().Equals(EventName, ESearchCase::IgnoreCase))
				{
					OutError = FString::Printf(TEXT("Blueprint custom event already exists: %s"), *EventName);
					return nullptr;
				}
			}
		}

		UK2Node_CustomEvent* CustomEventNode = NewObject<UK2Node_CustomEvent>(Graph);
		CustomEventNode->CustomFunctionName = FName(*EventName);
		Node = CustomEventNode;
	}
	else if (SpawnerId.StartsWith(TEXT("custom_event_call::")))
	{
		const FString EventName = SpawnerId.RightChop(19).TrimStartAndEnd();
		if (EventName.IsEmpty())
		{
			OutError = FString::Printf(TEXT("Invalid custom event call spawner id: %s"), *SpawnerId);
			return nullptr;
		}

		bool bFoundExistingEvent = false;
		TArray<UEdGraph*> Graphs;
		Blueprint->GetAllGraphs(Graphs);
		for (UEdGraph* CandidateGraph : Graphs)
		{
			if (!CandidateGraph)
			{
				continue;
			}

			TArray<UK2Node_CustomEvent*> ExistingEvents;
			CandidateGraph->GetNodesOfClass<UK2Node_CustomEvent>(ExistingEvents);
			for (const UK2Node_CustomEvent* ExistingEvent : ExistingEvents)
			{
				if (ExistingEvent && ExistingEvent->CustomFunctionName.ToString().Equals(EventName, ESearchCase::IgnoreCase))
				{
					bFoundExistingEvent = true;
					break;
				}
			}
			if (bFoundExistingEvent)
			{
				break;
			}
		}
		if (!bFoundExistingEvent)
		{
			OutError = FString::Printf(TEXT("Blueprint custom event does not exist for call spawner id: %s"), *SpawnerId);
			return nullptr;
		}

		UK2Node_CallFunction* FunctionNode = NewObject<UK2Node_CallFunction>(Graph);
		FunctionNode->FunctionReference.SetSelfMember(FName(*EventName));
		Node = FunctionNode;
	}
	else if (SpawnerId.StartsWith(TEXT("var_get::")) || SpawnerId.StartsWith(TEXT("var_set::")))
	{
		const bool bSet = SpawnerId.StartsWith(TEXT("var_set::"));
		const FString VarName = SpawnerId.RightChop(bSet ? 9 : 9);
		FString ExternalClassPath;
		FString ExternalVarName;
		if (DruidsMCPBridge::ParseExternalVariableSpawnerSpec(SpawnerId, bSet ? TEXT("var_set::") : TEXT("var_get::"), ExternalClassPath, ExternalVarName))
		{
			FString CanonicalClassPath;
			FString ResolveError;
			UClass* OwnerClass = nullptr;
			if (!DruidsMCPBridge::ResolveExternalVariableReference(ExternalClassPath, ExternalVarName, OwnerClass, CanonicalClassPath, ResolveError))
			{
				OutError = ResolveError.IsEmpty() ? FString::Printf(TEXT("Could not resolve external variable spawner id: %s"), *SpawnerId) : ResolveError;
				return nullptr;
			}

			UK2Node_Variable* VariableNode = bSet
				? static_cast<UK2Node_Variable*>(NewObject<UK2Node_VariableSet>(Graph))
				: static_cast<UK2Node_Variable*>(NewObject<UK2Node_VariableGet>(Graph));
			UEdGraphSchema_K2::ConfigureVarNode(VariableNode, FName(*ExternalVarName), OwnerClass, Blueprint);
			Node = VariableNode;
			EffectiveSpawnerId = FString::Printf(TEXT("var_%s::%s:%s"), bSet ? TEXT("set") : TEXT("get"), *CanonicalClassPath, *ExternalVarName);
		}
		else if (!bSet && DruidsMCPBridge::FindSCSNodeByName(Blueprint, VarName))
		{
			UK2Node_VariableGet* VariableNode = NewObject<UK2Node_VariableGet>(Graph);
			VariableNode->VariableReference.SetSelfMember(FName(*VarName));
			Node = VariableNode;
		}
		else if (FBlueprintEditorUtils::FindNewVariableIndex(Blueprint, FName(*VarName)) == INDEX_NONE)
		{
			OutError = FString::Printf(TEXT("Blueprint variable does not exist for spawner id: %s"), *SpawnerId);
			return nullptr;
		}
		else
		{
			UK2Node_Variable* VariableNode = bSet
				? static_cast<UK2Node_Variable*>(NewObject<UK2Node_VariableSet>(Graph))
				: static_cast<UK2Node_Variable*>(NewObject<UK2Node_VariableGet>(Graph));
			VariableNode->VariableReference.SetSelfMember(FName(*VarName));
			Node = VariableNode;
		}
	}
	else if (SpawnerId.StartsWith(TEXT("input_axis_key::")))
	{
		const FString KeyName = SpawnerId.RightChop(16);
		const FKey AxisKey = FKey(FName(*KeyName));
		if (!AxisKey.IsValid())
		{
			OutError = FString::Printf(TEXT("Could not resolve input axis key spawner: %s"), *SpawnerId);
			return nullptr;
		}
		if (!AxisKey.IsAxis1D())
		{
			OutError = FString::Printf(TEXT("Input axis key spawner is not a 1D axis: %s"), *SpawnerId);
			return nullptr;
		}
		if (!AxisKey.IsBindableInBlueprints())
		{
			OutError = FString::Printf(TEXT("Input axis key spawner is not bindable in Blueprints: %s"), *SpawnerId);
			return nullptr;
		}
		UK2Node_InputAxisKeyEvent* AxisKeyNode = NewObject<UK2Node_InputAxisKeyEvent>(Graph);
		AxisKeyNode->Initialize(AxisKey);
		if (!AxisKeyNode->IsCompatibleWithGraph(Graph))
		{
			OutError = FString::Printf(TEXT("Input axis key event '%s' is not compatible with graph '%s'."), *KeyName, *Graph->GetName());
			return nullptr;
		}
		Node = AxisKeyNode;
	}
	else if (SpawnerId.StartsWith(TEXT("input_key::")))
	{
		const FString KeyName = SpawnerId.RightChop(11);
		const FKey InputKey = FKey(FName(*KeyName));
		if (!InputKey.IsValid())
		{
			OutError = FString::Printf(TEXT("Could not resolve input key spawner: %s"), *SpawnerId);
			return nullptr;
		}
		if (InputKey.IsAnalog())
		{
			OutError = FString::Printf(TEXT("Input key spawner is analog and must use input_axis_key instead: %s"), *SpawnerId);
			return nullptr;
		}
		if (!InputKey.IsBindableInBlueprints())
		{
			OutError = FString::Printf(TEXT("Input key spawner is not bindable in Blueprints: %s"), *SpawnerId);
			return nullptr;
		}
		UK2Node_InputKey* InputKeyNode = NewObject<UK2Node_InputKey>(Graph);
		InputKeyNode->InputKey = InputKey;
		if (!InputKeyNode->IsCompatibleWithGraph(Graph))
		{
			OutError = FString::Printf(TEXT("Input key event '%s' is not compatible with graph '%s'."), *KeyName, *Graph->GetName());
			return nullptr;
		}
		Node = InputKeyNode;
	}

	if (!Node)
	{
		if (IsUnsupportedInputEventRequest(SpawnerId, TEXT("event")))
		{
			OutError = FString::Printf(TEXT("Unsupported input event spawner id: %s. Native input event node creation is not supported yet; use a dedicated input event helper or an explicit future input-event spawner."), *SpawnerId);
			return nullptr;
		}
		OutError = FString::Printf(TEXT("Unsupported native spawner id: %s"), *SpawnerId);
		return nullptr;
	}

	Graph->Modify();
	Graph->AddNode(Node, true, false);
	Node->CreateNewGuid();
	Node->NodePosX = 300 + Graph->Nodes.Num() * 32;
	Node->NodePosY = 100 + Graph->Nodes.Num() * 24;
	Node->PostPlacedNewNode();
	Node->AllocateDefaultPins();

	const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
	if (PinDefaults.IsValid())
	{
		for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : PinDefaults->Values)
		{
			if (UEdGraphPin* Pin = Node->FindPin(FName(*Pair.Key)))
			{
				FString DefaultValue;
				if (Pair.Value.IsValid() && Pair.Value->Type == EJson::String)
				{
					DefaultValue = Pair.Value->AsString();
				}
				else if (Pair.Value.IsValid() && Pair.Value->Type == EJson::Number)
				{
					const double NumberValue = Pair.Value->AsNumber();
					DefaultValue = Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Int
						? FString::FromInt(FMath::RoundToInt(NumberValue))
						: FString::SanitizeFloat(NumberValue);
				}
				else if (Pair.Value.IsValid() && Pair.Value->Type == EJson::Boolean)
				{
					DefaultValue = Pair.Value->AsBool() ? TEXT("true") : TEXT("false");
				}
				else
				{
					DefaultValue = JsonValueToString(Pair.Value).TrimQuotes();
				}
				Schema->TrySetDefaultValue(*Pin, DefaultValue);
			}
		}
	}

	if (Node->IsA<UK2Node_CustomEvent>())
	{
		FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
	}
	else
	{
		FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
	}
	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetBoolField(TEXT("success"), true);
	Result->SetStringField(TEXT("node_guid"), Node->NodeGuid.ToString(EGuidFormats::Digits));
	Result->SetStringField(TEXT("title"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
	Result->SetStringField(TEXT("spawner_id"), EffectiveSpawnerId);
	Result->SetNumberField(TEXT("node_pos_x"), Node->NodePosX);
	Result->SetNumberField(TEXT("node_pos_y"), Node->NodePosY);
	if (const UK2Node_DynamicCast* DynamicCastNode = Cast<UK2Node_DynamicCast>(Node))
	{
		if (const UEdGraphPin* CastResultPin = DynamicCastNode->GetCastResultPin())
		{
			Result->SetStringField(TEXT("cast_result_pin"), CastResultPin->PinName.ToString());
		}
		if (const UEdGraphPin* CastSourcePin = DynamicCastNode->GetCastSourcePin())
		{
			Result->SetStringField(TEXT("cast_source_pin"), CastSourcePin->PinName.ToString());
		}
		if (const UEdGraphPin* ValidCastPin = DynamicCastNode->GetValidCastPin())
		{
			Result->SetStringField(TEXT("valid_cast_pin"), ValidCastPin->PinName.ToString());
		}
		if (const UEdGraphPin* InvalidCastPin = DynamicCastNode->GetInvalidCastPin())
		{
			Result->SetStringField(TEXT("invalid_cast_pin"), InvalidCastPin->PinName.ToString());
		}
	}
	if (const UK2Node_Variable* VariableNode = Cast<UK2Node_Variable>(Node))
	{
		const bool bIsSet = Node->IsA<UK2Node_VariableSet>();
		const FString VariableName = VariableNode->GetVarNameString();
		Result->SetStringField(TEXT("variable_name"), VariableName);
		Result->SetStringField(TEXT("variable_scope"), VariableNode->VariableReference.IsSelfContext() ? TEXT("self") : (VariableNode->VariableReference.IsLocalScope() ? TEXT("local") : TEXT("external")));
		if (UClass* VariableOwnerClass = VariableNode->VariableReference.GetMemberParentClass(Blueprint->GeneratedClass))
		{
			Result->SetStringField(TEXT("variable_owner_class_path"), VariableOwnerClass->GetPathName());
		}
		if (!VariableName.IsEmpty())
		{
			Result->SetStringField(TEXT("variable_value_pin"), VariableName);
		}
		if (!VariableNode->VariableReference.IsSelfContext())
		{
			Result->SetStringField(TEXT("variable_target_pin"), UEdGraphSchema_K2::PN_Self.ToString());
		}
		if (bIsSet)
		{
			Result->SetStringField(TEXT("variable_exec_input_pin"), UEdGraphSchema_K2::PN_Execute.ToString());
			Result->SetStringField(TEXT("variable_exec_output_pin"), UEdGraphSchema_K2::PN_Then.ToString());
		}
		if (!bIsSet && DruidsMCPBridge::FindSCSNodeByName(Blueprint, VariableName))
		{
			Result->SetBoolField(TEXT("component_reference"), true);
		}
	}
	return Result;
}

bool FDruidsMCPBridgeServer::ConnectNativeBlueprintPins(UBlueprint* Blueprint, UEdGraph* Graph, const FString& SourceNodeGuid, const FString& SourcePinName, const FString& TargetNodeGuid, const FString& TargetPinName, FString& OutError) const
{
	UEdGraphNode* SourceNode = nullptr;
	UEdGraphNode* TargetNode = nullptr;
	for (UEdGraphNode* Node : Graph->Nodes)
	{
		if (!Node)
		{
			continue;
		}
		const FString Guid = Node->NodeGuid.ToString(EGuidFormats::Digits);
		if (Guid.Equals(SourceNodeGuid, ESearchCase::IgnoreCase)) SourceNode = Node;
		if (Guid.Equals(TargetNodeGuid, ESearchCase::IgnoreCase)) TargetNode = Node;
	}
	if (!SourceNode || !TargetNode)
	{
		OutError = TEXT("Could not resolve source or target node GUID.");
		return false;
	}

	UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*SourcePinName));
	UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*TargetPinName));
	if (!SourcePin && SourcePinName.Equals(TEXT("then"), ESearchCase::IgnoreCase)) SourcePin = SourceNode->FindPin(UEdGraphSchema_K2::PN_Then);
	if (!TargetPin && TargetPinName.Equals(TEXT("execute"), ESearchCase::IgnoreCase)) TargetPin = TargetNode->FindPin(UEdGraphSchema_K2::PN_Execute);
	if (!TargetPin && (TargetPinName.Equals(TEXT("self"), ESearchCase::IgnoreCase) || TargetPinName.Equals(TEXT("target"), ESearchCase::IgnoreCase))) TargetPin = TargetNode->FindPin(UEdGraphSchema_K2::PN_Self);
	if (!SourcePin || !TargetPin)
	{
		OutError = FString::Printf(TEXT("Could not resolve pins %s -> %s."), *SourcePinName, *TargetPinName);
		return false;
	}

	const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
	if (!Schema->TryCreateConnection(SourcePin, TargetPin))
	{
		OutError = FString::Printf(TEXT("Unreal rejected connection %s:%s -> %s:%s."), *SourceNodeGuid, *SourcePinName, *TargetNodeGuid, *TargetPinName);
		return false;
	}

	FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
	return true;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeJsonRpcResult(
	const TSharedPtr<FJsonObject>& RequestObject,
	const TSharedPtr<FJsonObject>& ResultObject) const
{
	TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
	Response->SetStringField(TEXT("jsonrpc"), DruidsMCPBridge::JsonRpcVersion);
	Response->SetField(TEXT("id"), CloneJsonValue(RequestObject->TryGetField(TEXT("id"))));
	Response->SetObjectField(TEXT("result"), ResultObject);
	return Response;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeJsonRpcError(
	const TSharedPtr<FJsonObject>& RequestObject,
	int32 Code,
	const FString& Message) const
{
	return MakeJsonRpcErrorWithId(RequestObject.IsValid() ? RequestObject->TryGetField(TEXT("id")) : nullptr, Code, Message);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeJsonRpcErrorWithId(
	const TSharedPtr<FJsonValue>& IdValue,
	int32 Code,
	const FString& Message) const
{
	TSharedPtr<FJsonObject> Error = MakeShared<FJsonObject>();
	Error->SetNumberField(TEXT("code"), Code);
	Error->SetStringField(TEXT("message"), Message);

	TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
	Response->SetStringField(TEXT("jsonrpc"), DruidsMCPBridge::JsonRpcVersion);
	Response->SetField(TEXT("id"), CloneJsonValue(IdValue));
	Response->SetObjectField(TEXT("error"), Error);
	return Response;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeToolTextResult(const FString& Text, bool bIsError) const
{
	TArray<TSharedPtr<FJsonValue>> Content;
	TSharedPtr<FJsonObject> TextContent = MakeShared<FJsonObject>();
	TextContent->SetStringField(TEXT("type"), TEXT("text"));
	TextContent->SetStringField(TEXT("text"), Text);
	Content.Add(MakeShared<FJsonValueObject>(TextContent));

	TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
	Result->SetArrayField(TEXT("content"), Content);
	if (bIsError)
	{
		Result->SetBoolField(TEXT("isError"), true);
	}
	return Result;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeToolJsonResult(const TSharedPtr<FJsonValue>& Value, bool bIsError) const
{
	return MakeToolTextResult(JsonValueToString(Value), bIsError);
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeToolJsonObjectResult(const TSharedPtr<FJsonObject>& Object, bool bIsError) const
{
	TSharedPtr<FJsonObject> Result = MakeToolTextResult(JsonObjectToString(Object), bIsError);
	Result->SetObjectField(TEXT("structuredContent"), Object);
	return Result;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeToolDefinition(
	const FString& Name,
	const FString& Description,
	const TSharedPtr<FJsonObject>& InputSchema) const
{
	TSharedPtr<FJsonObject> Tool = MakeShared<FJsonObject>();
	Tool->SetStringField(TEXT("name"), Name);
	Tool->SetStringField(TEXT("description"), Description);
	Tool->SetObjectField(TEXT("inputSchema"), InputSchema);
	return Tool;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeObjectSchema() const
{
	TSharedPtr<FJsonObject> Schema = MakeShared<FJsonObject>();
	Schema->SetStringField(TEXT("type"), TEXT("object"));
	Schema->SetObjectField(TEXT("properties"), MakeShared<FJsonObject>());
	return Schema;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeStringProperty(const FString& Description) const
{
	TSharedPtr<FJsonObject> Property = MakeShared<FJsonObject>();
	Property->SetStringField(TEXT("type"), TEXT("string"));
	Property->SetStringField(TEXT("description"), Description);
	return Property;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::MakeObjectProperty(const FString& Description) const
{
	TSharedPtr<FJsonObject> Property = MakeShared<FJsonObject>();
	Property->SetStringField(TEXT("type"), TEXT("object"));
	Property->SetStringField(TEXT("description"), Description);
	Property->SetBoolField(TEXT("additionalProperties"), true);
	return Property;
}

bool FDruidsMCPBridgeServer::TryGetObjectParam(
	const TSharedPtr<FJsonObject>& Object,
	const FString& FieldName,
	TSharedPtr<FJsonObject>& OutObject)
{
	const TSharedPtr<FJsonObject>* FoundObject = nullptr;
	if (Object.IsValid() && Object->TryGetObjectField(FieldName, FoundObject) && FoundObject && FoundObject->IsValid())
	{
		OutObject = *FoundObject;
		return true;
	}

	return false;
}

FString FDruidsMCPBridgeServer::GetOptionalStringParam(const TSharedPtr<FJsonObject>& Object, const FString& FieldName, const FString& DefaultValue)
{
	FString Value;
	if (Object.IsValid() && Object->TryGetStringField(FieldName, Value))
	{
		return Value;
	}

	return DefaultValue;
}

int32 FDruidsMCPBridgeServer::GetOptionalIntParam(const TSharedPtr<FJsonObject>& Object, const FString& FieldName, int32 DefaultValue)
{
	if (!Object.IsValid())
	{
		return DefaultValue;
	}

	int32 Value = DefaultValue;
	if (Object->TryGetNumberField(FieldName, Value))
	{
		return Value;
	}

	FString StringValue;
	if (Object->TryGetStringField(FieldName, StringValue) && StringValue.IsNumeric())
	{
		return FCString::Atoi(*StringValue);
	}

	return DefaultValue;
}

bool FDruidsMCPBridgeServer::TextMatches(const FString& Haystack, const FString& Needle)
{
	if (Needle.IsEmpty())
	{
		return true;
	}

	TArray<FString> Terms;
	Needle.ParseIntoArrayWS(Terms);
	for (const FString& Term : Terms)
	{
		if (!Term.IsEmpty() && !Haystack.Contains(Term, ESearchCase::IgnoreCase))
		{
			return false;
		}
	}

	return true;
}

TSharedPtr<FJsonValue> FDruidsMCPBridgeServer::ParseJsonString(const FString& JsonText)
{
	TSharedPtr<FJsonValue> Value;
	const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonText);
	if (FJsonSerializer::Deserialize(Reader, Value) && Value.IsValid())
	{
		return Value;
	}

	return nullptr;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ParseJsonObjectString(const FString& JsonText)
{
	TSharedPtr<FJsonObject> Object;
	const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonText);
	if (FJsonSerializer::Deserialize(Reader, Object) && Object.IsValid())
	{
		return Object;
	}

	return nullptr;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ExtractFirstObjectFromToolContent(const TSharedPtr<FJsonObject>& ToolObject)
{
	if (!ToolObject.IsValid())
	{
		return nullptr;
	}

	const TSharedPtr<FJsonObject>* StructuredContent = nullptr;
	if (ToolObject->TryGetObjectField(TEXT("structuredContent"), StructuredContent) && StructuredContent && StructuredContent->IsValid())
	{
		return *StructuredContent;
	}

	const TArray<TSharedPtr<FJsonValue>>* Content = nullptr;
	if (ToolObject->TryGetArrayField(TEXT("content"), Content))
	{
		for (const TSharedPtr<FJsonValue>& ContentValue : *Content)
		{
			const TSharedPtr<FJsonObject> ContentObject = ContentValue->AsObject();
			if (!ContentObject.IsValid())
			{
				continue;
			}

			FString Text;
			if (ContentObject->TryGetStringField(TEXT("text"), Text))
			{
				if (const TSharedPtr<FJsonObject> ParsedObject = ParseJsonObjectString(Text))
				{
					return ParsedObject;
				}
			}
		}
	}

	return nullptr;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ParseOutResultObject(const TSharedPtr<FJsonObject>& Object)
{
	if (!Object.IsValid())
	{
		return nullptr;
	}

	FString OutResult;
	if (Object->TryGetStringField(TEXT("OutResult"), OutResult))
	{
		return ParseJsonObjectString(OutResult);
	}

	return nullptr;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::ParseGraphText(const FString& GraphText)
{
	TSharedPtr<FJsonObject> Parsed = MakeShared<FJsonObject>();
	TArray<TSharedPtr<FJsonValue>> Graphs;
	TArray<TSharedPtr<FJsonValue>> Nodes;
	TArray<TSharedPtr<FJsonValue>> Connections;
	TSet<FString> SeenGraphs;
	TSet<FString> SeenNodes;
	TSet<FString> SeenConnections;

	FString CurrentGraph;
	FString CurrentGraphType;
	bool bReadingConnections = false;

	TArray<FString> Lines;
	GraphText.ParseIntoArrayLines(Lines, true);
	for (const FString& RawLine : Lines)
	{
		const FString Line = RawLine.TrimStartAndEnd();
		if (Line.IsEmpty())
		{
			continue;
		}

		if (Line.StartsWith(TEXT("# GRAPH ")))
		{
			bReadingConnections = false;
			TArray<FString> Parts;
			Line.ParseIntoArrayWS(Parts);
			CurrentGraph = Parts.Num() > 2 ? Parts[2] : TEXT("");
			CurrentGraphType.Empty();
			for (const FString& Part : Parts)
			{
				if (Part.StartsWith(TEXT("type=")))
				{
					CurrentGraphType = Part.RightChop(5);
					break;
				}
			}

			const FString GraphKey = CurrentGraph + TEXT("|") + CurrentGraphType;
			if (!SeenGraphs.Contains(GraphKey))
			{
				SeenGraphs.Add(GraphKey);
				TSharedPtr<FJsonObject> GraphObject = MakeShared<FJsonObject>();
				GraphObject->SetStringField(TEXT("name"), CurrentGraph);
				GraphObject->SetStringField(TEXT("type"), CurrentGraphType);
				Graphs.Add(MakeShared<FJsonValueObject>(GraphObject));
			}
			continue;
		}

		if (Line.StartsWith(TEXT("# CONNECTIONS ")))
		{
			bReadingConnections = true;
			TArray<FString> Parts;
			Line.ParseIntoArrayWS(Parts);
			CurrentGraph = Parts.Num() > 2 ? Parts[2] : CurrentGraph;
			continue;
		}

		TArray<FString> Columns;
		Line.ParseIntoArray(Columns, TEXT("\t"), false);
		if (bReadingConnections)
		{
			if (Columns.Num() >= 4)
			{
				const FString ConnectionKey = FString::Printf(TEXT("%s|%s|%s|%s|%s"),
					*CurrentGraph,
					*Columns[0].TrimStartAndEnd(),
					*Columns[1].TrimStartAndEnd(),
					*Columns[2].TrimStartAndEnd(),
					*Columns[3].TrimStartAndEnd());
				if (SeenConnections.Contains(ConnectionKey))
				{
					continue;
				}
				SeenConnections.Add(ConnectionKey);

				TSharedPtr<FJsonObject> ConnectionObject = MakeShared<FJsonObject>();
				ConnectionObject->SetStringField(TEXT("graph"), CurrentGraph);
				ConnectionObject->SetStringField(TEXT("source_node_guid"), Columns[0].TrimStartAndEnd());
				ConnectionObject->SetStringField(TEXT("source_pin"), Columns[1].TrimStartAndEnd());
				ConnectionObject->SetStringField(TEXT("target_node_guid"), Columns[2].TrimStartAndEnd());
				ConnectionObject->SetStringField(TEXT("target_pin"), Columns[3].TrimStartAndEnd());
				Connections.Add(MakeShared<FJsonValueObject>(ConnectionObject));
			}
			continue;
		}

		if (Columns.Num() >= 2)
		{
			const FString NodeGuid = Columns[0].TrimStartAndEnd();
			const FString NodeKey = CurrentGraph + TEXT("|") + NodeGuid;
			if (SeenNodes.Contains(NodeKey))
			{
				continue;
			}
			SeenNodes.Add(NodeKey);

			TSharedPtr<FJsonObject> NodeObject = MakeShared<FJsonObject>();
			NodeObject->SetStringField(TEXT("graph"), CurrentGraph);
			NodeObject->SetStringField(TEXT("graph_type"), CurrentGraphType);
			NodeObject->SetStringField(TEXT("node_guid"), NodeGuid);
			NodeObject->SetStringField(TEXT("title"), Columns[1].TrimStartAndEnd());

			if (Columns.Num() >= 3)
			{
				TArray<TSharedPtr<FJsonValue>> PinValues;
				for (FString Pin : SplitGraphPinList(Columns[2]))
				{
					PinValues.Add(MakeShared<FJsonValueString>(Pin.TrimStartAndEnd()));
				}
				NodeObject->SetArrayField(TEXT("pins"), PinValues);
			}

			Nodes.Add(MakeShared<FJsonValueObject>(NodeObject));
		}
	}

	Parsed->SetArrayField(TEXT("graphs"), Graphs);
	Parsed->SetArrayField(TEXT("nodes"), Nodes);
	Parsed->SetArrayField(TEXT("connections"), Connections);
	Parsed->SetNumberField(TEXT("graph_count"), Graphs.Num());
	Parsed->SetNumberField(TEXT("node_count"), Nodes.Num());
	Parsed->SetNumberField(TEXT("connection_count"), Connections.Num());
	return Parsed;
}

TArray<FString> FDruidsMCPBridgeServer::SplitGraphPinList(const FString& PinText)
{
	TArray<FString> Pins;
	FString Current;
	int32 ParenthesesDepth = 0;
	bool bInSingleQuote = false;

	for (int32 Index = 0; Index < PinText.Len(); ++Index)
	{
		const TCHAR Character = PinText[Index];
		if (Character == TEXT('\''))
		{
			bInSingleQuote = !bInSingleQuote;
			Current.AppendChar(Character);
			continue;
		}

		if (!bInSingleQuote)
		{
			if (Character == TEXT('('))
			{
				++ParenthesesDepth;
			}
			else if (Character == TEXT(')') && ParenthesesDepth > 0)
			{
				--ParenthesesDepth;
			}
			else if (Character == TEXT(',') && ParenthesesDepth == 0)
			{
				Current.TrimStartAndEndInline();
				if (!Current.IsEmpty())
				{
					Pins.Add(Current);
				}
				Current.Empty();
				continue;
			}
		}

		Current.AppendChar(Character);
	}

	Current.TrimStartAndEndInline();
	if (!Current.IsEmpty())
	{
		Pins.Add(Current);
	}

	return Pins;
}

bool FDruidsMCPBridgeServer::TryFindNodeGuidInGraphText(const FString& GraphText, const FString& TitleOrGuid, FString& OutNodeGuid)
{
	TArray<FString> Lines;
	GraphText.ParseIntoArrayLines(Lines, true);
	for (const FString& Line : Lines)
	{
		TArray<FString> Columns;
		Line.ParseIntoArray(Columns, TEXT("\t"), false);
		if (Columns.Num() < 2)
		{
			continue;
		}

		const FString Guid = Columns[0].TrimStartAndEnd();
		const FString Title = Columns[1].TrimStartAndEnd();
		if (Guid.Equals(TitleOrGuid, ESearchCase::IgnoreCase) || Title.Contains(TitleOrGuid, ESearchCase::IgnoreCase))
		{
			OutNodeGuid = Guid;
			return true;
		}
	}

	return false;
}

bool FDruidsMCPBridgeServer::TryFindStringFieldRecursive(const TSharedPtr<FJsonValue>& Value, const TArray<FString>& FieldNames, FString& OutString)
{
	if (!Value.IsValid())
	{
		return false;
	}

	if (Value->Type == EJson::Object)
	{
		const TSharedPtr<FJsonObject> Object = Value->AsObject();
		for (const FString& FieldName : FieldNames)
		{
			if (Object->TryGetStringField(FieldName, OutString) && !OutString.IsEmpty())
			{
				return true;
			}
		}

		for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : Object->Values)
		{
			if (TryFindStringFieldRecursive(Pair.Value, FieldNames, OutString))
			{
				return true;
			}
		}
	}
	else if (Value->Type == EJson::Array)
	{
		for (const TSharedPtr<FJsonValue>& Item : Value->AsArray())
		{
			if (TryFindStringFieldRecursive(Item, FieldNames, OutString))
			{
				return true;
			}
		}
	}

	return false;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::FindNodeByTitleOrGuid(const TSharedPtr<FJsonValue>& Value, const FString& TitleOrGuid)
{
	if (!Value.IsValid() || TitleOrGuid.IsEmpty())
	{
		return nullptr;
	}

	if (Value->Type == EJson::Object)
	{
		const TSharedPtr<FJsonObject> Object = Value->AsObject();
		FString NodeGuid;
		FString NodeTitle;
		Object->TryGetStringField(TEXT("NodeGuid"), NodeGuid);
		Object->TryGetStringField(TEXT("node_guid"), NodeGuid);
		Object->TryGetStringField(TEXT("NodeTitle"), NodeTitle);
		Object->TryGetStringField(TEXT("title"), NodeTitle);
		Object->TryGetStringField(TEXT("name"), NodeTitle);

		if ((!NodeGuid.IsEmpty() && NodeGuid.Equals(TitleOrGuid, ESearchCase::IgnoreCase)) ||
			(!NodeTitle.IsEmpty() && NodeTitle.Contains(TitleOrGuid, ESearchCase::IgnoreCase)))
		{
			return Object;
		}

		for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : Object->Values)
		{
			if (const TSharedPtr<FJsonObject> Found = FindNodeByTitleOrGuid(Pair.Value, TitleOrGuid))
			{
				return Found;
			}
		}
	}
	else if (Value->Type == EJson::Array)
	{
		for (const TSharedPtr<FJsonValue>& Item : Value->AsArray())
		{
			if (const TSharedPtr<FJsonObject> Found = FindNodeByTitleOrGuid(Item, TitleOrGuid))
			{
				return Found;
			}
		}
	}

	return nullptr;
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::FindFirstSpawnerNode(const TSharedPtr<FJsonObject>& SpawnerResult)
{
	if (!SpawnerResult.IsValid())
	{
		return nullptr;
	}

	const TSharedPtr<FJsonObject>* DataObject = nullptr;
	if (SpawnerResult->TryGetObjectField(TEXT("data"), DataObject) && DataObject && DataObject->IsValid())
	{
		const TArray<TSharedPtr<FJsonValue>>* Nodes = nullptr;
		if ((*DataObject)->TryGetArrayField(TEXT("nodes"), Nodes) && Nodes && Nodes->Num() > 0)
		{
			return (*Nodes)[0]->AsObject();
		}
	}

	const TArray<TSharedPtr<FJsonValue>>* Nodes = nullptr;
	if (SpawnerResult->TryGetArrayField(TEXT("nodes"), Nodes) && Nodes && Nodes->Num() > 0)
	{
		return (*Nodes)[0]->AsObject();
	}

	return nullptr;
}

TArray<TSharedPtr<FJsonValue>> FDruidsMCPBridgeServer::MakeStringArrayValue(const TArray<FString>& Strings)
{
	TArray<TSharedPtr<FJsonValue>> Values;
	for (const FString& String : Strings)
	{
		Values.Add(MakeShared<FJsonValueString>(String));
	}
	return Values;
}

TArray<FString> FDruidsMCPBridgeServer::BuildSpawnerResolverQueries(const FString& Query, const FString& Intent, const FString& Operator, const FString& ValueType)
{
	TArray<FString> Queries;
	AddUniqueString(Queries, Query);
	AddUniqueString(Queries, Operator);

	const FString NormalizedIntent = NormalizeResolverText(Intent);
	const FString NormalizedOperator = NormalizeResolverText(Operator.IsEmpty() ? Query : Operator);
	const FString NormalizedType = NormalizeResolverText(ValueType);
	const FString NormalizedQuery = NormalizeResolverText(Query);

	if (NormalizedIntent.Contains(TEXT("math")) || NormalizedOperator.Contains(TEXT("add")) || NormalizedOperator.Contains(TEXT("plus")) || NormalizedOperator.Contains(TEXT("+")))
	{
		if (NormalizedType.IsEmpty() || NormalizedType == TEXT("int") || NormalizedType == TEXT("integer"))
		{
			AddUniqueString(Queries, TEXT("int + int"));
			AddUniqueString(Queries, TEXT("+"));
			AddUniqueString(Queries, TEXT("add"));
			AddUniqueString(Queries, TEXT("plus"));
			AddUniqueString(Queries, TEXT("Integer"));
		}
		if (NormalizedType == TEXT("float") || NormalizedType == TEXT("double") || NormalizedType == TEXT("real"))
		{
			AddUniqueString(Queries, TEXT("float + float"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:Add_DoubleDouble"));
		}
	}

	if (NormalizedIntent.Contains(TEXT("math")) || NormalizedOperator.Contains(TEXT("subtract")) || NormalizedOperator.Contains(TEXT("minus")) || NormalizedOperator.Contains(TEXT("-")))
	{
		if (NormalizedType == TEXT("float") || NormalizedType == TEXT("double") || NormalizedType == TEXT("real"))
		{
			AddUniqueString(Queries, TEXT("float - float"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:Subtract_DoubleDouble"));
		}
	}

	if (NormalizedIntent.Contains(TEXT("math")) || NormalizedOperator.Contains(TEXT("multiply")) || NormalizedOperator.Contains(TEXT("*")))
	{
		if (NormalizedType == TEXT("float") || NormalizedType == TEXT("double") || NormalizedType == TEXT("real"))
		{
			AddUniqueString(Queries, TEXT("float * float"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:Multiply_DoubleDouble"));
		}
	}

	if (NormalizedIntent.Contains(TEXT("math")) || NormalizedOperator.Contains(TEXT("divide")) || NormalizedOperator.Contains(TEXT("/")))
	{
		if (NormalizedType == TEXT("float") || NormalizedType == TEXT("double") || NormalizedType == TEXT("real"))
		{
			AddUniqueString(Queries, TEXT("float / float"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:Divide_DoubleDouble"));
		}
	}

	if (NormalizedIntent.Contains(TEXT("comparison")) || NormalizedOperator.Contains(TEXT(">")) || NormalizedOperator.Contains(TEXT("greater")))
	{
		if (NormalizedType == TEXT("int") || NormalizedType == TEXT("integer"))
		{
			AddUniqueString(Queries, TEXT("integer > integer"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:Greater_IntInt"));
		}
		if (NormalizedType == TEXT("float") || NormalizedType == TEXT("double") || NormalizedType == TEXT("real"))
		{
			AddUniqueString(Queries, TEXT("float > float"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:Greater_DoubleDouble"));
		}
	}

	if (NormalizedIntent.Contains(TEXT("comparison")) || NormalizedOperator.Contains(TEXT("<")) || NormalizedOperator.Contains(TEXT("less")))
	{
		if (NormalizedType == TEXT("int") || NormalizedType == TEXT("integer"))
		{
			AddUniqueString(Queries, TEXT("integer < integer"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:Less_IntInt"));
		}
		if (NormalizedType == TEXT("float") || NormalizedType == TEXT("double") || NormalizedType == TEXT("real"))
		{
			AddUniqueString(Queries, TEXT("float < float"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:Less_DoubleDouble"));
		}
	}

	if (NormalizedIntent.Contains(TEXT("comparison")) || NormalizedOperator.Contains(TEXT("==")) || NormalizedOperator.Contains(TEXT("equal")))
	{
		if (NormalizedType == TEXT("bool") || NormalizedType == TEXT("boolean"))
		{
			AddUniqueString(Queries, TEXT("Equal (Boolean)"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:EqualEqual_BoolBool"));
		}
		if (NormalizedType == TEXT("string"))
		{
			AddUniqueString(Queries, TEXT("Equal Exactly (String)"));
			AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetStringLibrary:EqualEqual_StrStr"));
		}
	}

	if (NormalizedIntent.Contains(TEXT("conversion")) || Query.Contains(TEXT("to string"), ESearchCase::IgnoreCase) || Query.Contains(TEXT("convert"), ESearchCase::IgnoreCase))
	{
		AddUniqueString(Queries, TEXT("To String"));
		AddUniqueString(Queries, TEXT("Conv Int To String"));
	}

	if (NormalizedQuery.Contains(TEXT("makevector")) || NormalizedQuery.Contains(TEXT("vector")) || NormalizedIntent.Contains(TEXT("vector")))
	{
		AddUniqueString(Queries, TEXT("Make Vector"));
		AddUniqueString(Queries, TEXT("func::/Script/Engine.KismetMathLibrary:MakeVector"));
	}

	if (NormalizedQuery.Contains(TEXT("setrelativelocation")) || NormalizedQuery.Contains(TEXT("relativelocation")) || NormalizedQuery.Contains(TEXT("door motion")) || NormalizedIntent.Contains(TEXT("componenttransform")))
	{
		AddUniqueString(Queries, TEXT("Set Relative Location"));
		AddUniqueString(Queries, TEXT("func::/Script/Engine.SceneComponent:K2_SetRelativeLocation"));
	}
	if (NormalizedQuery.Contains(TEXT("addlocaloffset")) || NormalizedQuery.Contains(TEXT("localoffset")) || NormalizedQuery.Contains(TEXT("offset")))
	{
		AddUniqueString(Queries, TEXT("Add Local Offset"));
		AddUniqueString(Queries, TEXT("func::/Script/Engine.SceneComponent:K2_AddLocalOffset"));
	}
	if (NormalizedQuery.Contains(TEXT("addrelativelocation")))
	{
		AddUniqueString(Queries, TEXT("Add Relative Location"));
		AddUniqueString(Queries, TEXT("func::/Script/Engine.SceneComponent:K2_AddRelativeLocation"));
	}
	if (NormalizedQuery.Contains(TEXT("setstaticmesh")) || NormalizedQuery.Contains(TEXT("staticmesh")))
	{
		AddUniqueString(Queries, TEXT("Set Static Mesh"));
		AddUniqueString(Queries, TEXT("func::/Script/Engine.StaticMeshComponent:SetStaticMesh"));
	}
	if (NormalizedQuery.Contains(TEXT("setmaterial")) || NormalizedQuery.Contains(TEXT("material")))
	{
		AddUniqueString(Queries, TEXT("Set Material"));
		AddUniqueString(Queries, TEXT("func::/Script/Engine.PrimitiveComponent:SetMaterial"));
	}

	if (NormalizedIntent.Contains(TEXT("event")) || Query.Contains(TEXT("begin play"), ESearchCase::IgnoreCase))
	{
		AddUniqueString(Queries, TEXT("Event BeginPlay"));
		AddUniqueString(Queries, TEXT("Begin Play"));
	}
	FString ResolvedInputKeyName;
	if (DruidsMCPBridge::TryResolveInputKeyNameFromQuery(Query, ResolvedInputKeyName))
	{
		AddUniqueString(Queries, ResolvedInputKeyName);
		AddUniqueString(Queries, FString::Printf(TEXT("input_key::%s"), *ResolvedInputKeyName));
	}
	if (Query.StartsWith(TEXT("custom_event::"), ESearchCase::IgnoreCase))
	{
		AddUniqueString(Queries, Query);
	}
	if (Query.StartsWith(TEXT("custom_event_call::"), ESearchCase::IgnoreCase))
	{
		AddUniqueString(Queries, Query);
	}
	else if (!Query.TrimStartAndEnd().IsEmpty() && NormalizedIntent.Contains(TEXT("functioncall")))
	{
		AddUniqueString(Queries, FString::Printf(TEXT("custom_event_call::%s"), *Query.TrimStartAndEnd()));
	}

	if (NormalizedIntent.Contains(TEXT("flow")) || NormalizedIntent.Contains(TEXT("control")) || NormalizedQuery == TEXT("sequence") || NormalizedQuery.Contains(TEXT("executionsequence")))
	{
		AddUniqueString(Queries, TEXT("Sequence"));
		AddUniqueString(Queries, TEXT("node::sequence"));
	}

	if (NormalizedIntent.Contains(TEXT("flow")) || NormalizedIntent.Contains(TEXT("control")) || NormalizedQuery == TEXT("doonce"))
	{
		AddUniqueString(Queries, TEXT("DoOnce"));
		AddUniqueString(Queries, DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("DoOnce")));
	}
	if (NormalizedIntent.Contains(TEXT("flow")) || NormalizedIntent.Contains(TEXT("control")) || NormalizedQuery == TEXT("gate"))
	{
		AddUniqueString(Queries, TEXT("Gate"));
		AddUniqueString(Queries, DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("Gate")));
	}
	if (NormalizedIntent.Contains(TEXT("flow")) || NormalizedIntent.Contains(TEXT("control")) || NormalizedQuery == TEXT("flipflop"))
	{
		AddUniqueString(Queries, TEXT("FlipFlop"));
		AddUniqueString(Queries, DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("FlipFlop")));
	}

	if (NormalizedQuery.Contains(TEXT("mousewheel")))
	{
		AddUniqueString(Queries, TEXT("MouseWheelAxis"));
		AddUniqueString(Queries, TEXT("input_axis_key::MouseWheelAxis"));
	}

	if (NormalizedQuery.Contains(TEXT("leftmouse")))
	{
		AddUniqueString(Queries, TEXT("LeftMouseButton"));
		AddUniqueString(Queries, TEXT("input_key::LeftMouseButton"));
	}

	if (Queries.Num() == 0)
	{
		AddUniqueString(Queries, TEXT("Print String"));
	}

	return Queries;
}

int32 FDruidsMCPBridgeServer::ScoreSpawnerCandidate(const TSharedPtr<FJsonObject>& Candidate, const FString& Query, const FString& Intent, const FString& Operator, const FString& ValueType)
{
	if (!Candidate.IsValid())
	{
		return 0;
	}

	FString Name;
	FString Category;
	FString Tooltip;
	FString SpawnerId;
	Candidate->TryGetStringField(TEXT("name"), Name);
	Candidate->TryGetStringField(TEXT("category"), Category);
	Candidate->TryGetStringField(TEXT("tooltip"), Tooltip);
	Candidate->TryGetStringField(TEXT("spawner_id"), SpawnerId);

	const FString Text = NormalizeResolverText(Name + TEXT(" ") + Category + TEXT(" ") + Tooltip);
	const FString NormalizedQuery = NormalizeResolverText(Query);
	const FString NormalizedIntent = NormalizeResolverText(Intent);
	const FString NormalizedOperator = NormalizeResolverText(Operator.IsEmpty() ? Query : Operator);
	const FString NormalizedType = NormalizeResolverText(ValueType);

	int32 Score = 0;
	if (!NormalizedQuery.IsEmpty() && Text.Contains(NormalizedQuery))
	{
		Score += 80;
	}
	if (NormalizeResolverText(Name) == NormalizedQuery)
	{
		Score += 120;
	}
	if (!NormalizedQuery.IsEmpty() && SpawnerId.StartsWith(TEXT("bp_func::")) && NormalizeResolverText(SpawnerId) == NormalizedQuery)
	{
		Score += 700;
	}
	if (!NormalizedQuery.IsEmpty() && SpawnerId.StartsWith(TEXT("bp_func::")) && NormalizeResolverText(SpawnerId).Contains(NormalizedQuery))
	{
		Score += 450;
	}
	if (!NormalizedQuery.IsEmpty() && SpawnerId.StartsWith(TEXT("custom_event::")) && NormalizeResolverText(SpawnerId) == NormalizedQuery)
	{
		Score += 700;
	}

	if ((NormalizedIntent.Contains(TEXT("math")) || NormalizedOperator.Contains(TEXT("add")) || NormalizedOperator.Contains(TEXT("plus")) || NormalizedOperator.Contains(TEXT("+"))) &&
		(NormalizedType.IsEmpty() || NormalizedType == TEXT("int") || NormalizedType == TEXT("integer")))
	{
		if (NormalizeResolverText(Name) == TEXT("int+int"))
		{
			Score += 700;
		}
		if (NormalizeResolverText(Category).Contains(TEXT("math|integer")))
		{
			Score += 150;
		}
		if (Text.Contains(TEXT("add")) || Text.Contains(TEXT("addition")) || Text.Contains(TEXT("+")))
		{
			Score += 120;
		}
		if (Text.Contains(TEXT("map")) || Text.Contains(TEXT("array")) || Text.Contains(TEXT("set")) || Text.Contains(TEXT("date")) || Text.Contains(TEXT("timespan")))
		{
			Score -= 200;
		}
	}

	if ((NormalizedIntent.Contains(TEXT("math")) || NormalizedIntent.Contains(TEXT("comparison"))) &&
		(SpawnerId.Contains(TEXT("KismetMathLibrary")) || SpawnerId.Contains(TEXT("KismetStringLibrary"))) &&
		((NormalizedType == TEXT("float") && Text.Contains(TEXT("float"))) ||
			(NormalizedType == TEXT("double") && Text.Contains(TEXT("float"))) ||
			(NormalizedType == TEXT("real") && Text.Contains(TEXT("float"))) ||
			(NormalizedType == TEXT("int") && Text.Contains(TEXT("integer"))) ||
			(NormalizedType == TEXT("integer") && Text.Contains(TEXT("integer"))) ||
			(NormalizedType == TEXT("bool") && Text.Contains(TEXT("boolean"))) ||
			(NormalizedType == TEXT("boolean") && Text.Contains(TEXT("boolean"))) ||
			(NormalizedType == TEXT("string") && Text.Contains(TEXT("string")))))
	{
		Score += 300;
	}

	if (NormalizedIntent.Contains(TEXT("conversion")) || NormalizedQuery.Contains(TEXT("tostring")) || NormalizedQuery.Contains(TEXT("convert")))
	{
		if (Text.Contains(TEXT("tostring")) || Text.Contains(TEXT("convinttostring")))
		{
			Score += 300;
		}
	}

	if ((NormalizedQuery.Contains(TEXT("makevector")) || NormalizedQuery.Contains(TEXT("vector")) || NormalizedIntent.Contains(TEXT("vector"))) &&
		SpawnerId.Equals(TEXT("func::/Script/Engine.KismetMathLibrary:MakeVector"), ESearchCase::IgnoreCase))
	{
		Score += 500;
	}

	for (const DruidsMCPBridge::FAllowlistedComponentFunction& Function : DruidsMCPBridge::GetAllowlistedComponentFunctions())
	{
		if (SpawnerId.Equals(Function.SpawnerId, ESearchCase::IgnoreCase))
		{
			const FString FunctionText = NormalizeResolverText(FString(Function.DisplayName) + TEXT(" ") + Function.FunctionName);
			if (!NormalizedQuery.IsEmpty() && FunctionText.Contains(NormalizedQuery))
			{
				Score += 500;
			}
			if (NormalizedIntent.Contains(TEXT("component")) || NormalizedIntent.Contains(TEXT("transform")) || NormalizedQuery.Contains(TEXT("motion")))
			{
				Score += 220;
			}
		}
	}

	if (NormalizedQuery.Contains(TEXT("mousewheel")) && SpawnerId.Equals(TEXT("input_axis_key::MouseWheelAxis"), ESearchCase::IgnoreCase))
	{
		Score += 400;
	}
	if (NormalizedQuery.Contains(TEXT("leftmouse")) && SpawnerId.Equals(TEXT("input_key::LeftMouseButton"), ESearchCase::IgnoreCase))
	{
		Score += 400;
	}
	FString ResolvedInputKeyName;
	if (DruidsMCPBridge::TryResolveInputKeyNameFromQuery(Query, ResolvedInputKeyName) &&
		SpawnerId.Equals(FString::Printf(TEXT("input_key::%s"), *ResolvedInputKeyName), ESearchCase::IgnoreCase))
	{
		Score += 650;
	}
	if (!NormalizedQuery.IsEmpty() && SpawnerId.StartsWith(TEXT("custom_event_call::")) && NormalizeResolverText(SpawnerId).Contains(NormalizedQuery))
	{
		Score += 650;
	}
	if ((NormalizedQuery == TEXT("sequence") || NormalizedQuery.Contains(TEXT("executionsequence")) || NormalizedIntent.Contains(TEXT("flow")) || NormalizedIntent.Contains(TEXT("control"))) &&
		SpawnerId.Equals(TEXT("node::sequence"), ESearchCase::IgnoreCase))
	{
		Score += 450;
	}
	if (SpawnerId.StartsWith(TEXT("macro::")) &&
		((NormalizedQuery == TEXT("doonce") && SpawnerId.Equals(DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("DoOnce")), ESearchCase::IgnoreCase)) ||
			(NormalizedQuery == TEXT("gate") && SpawnerId.Equals(DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("Gate")), ESearchCase::IgnoreCase)) ||
			(NormalizedQuery == TEXT("flipflop") && SpawnerId.Equals(DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("FlipFlop")), ESearchCase::IgnoreCase)) ||
			NormalizedIntent.Contains(TEXT("flow")) ||
			NormalizedIntent.Contains(TEXT("control"))))
	{
		Score += 450;
	}

	const TArray<TSharedPtr<FJsonValue>>* Inputs = nullptr;
	if (Candidate->TryGetArrayField(TEXT("inputs"), Inputs))
	{
		for (const TSharedPtr<FJsonValue>& Input : *Inputs)
		{
			if (NormalizeResolverText(Input->AsString()).Contains(NormalizedType))
			{
				Score += 20;
			}
		}
	}

	const TArray<TSharedPtr<FJsonValue>>* Outputs = nullptr;
	if (Candidate->TryGetArrayField(TEXT("outputs"), Outputs))
	{
		for (const TSharedPtr<FJsonValue>& Output : *Outputs)
		{
			if (NormalizeResolverText(Output->AsString()).Contains(NormalizedType))
			{
				Score += 30;
			}
		}
	}

	return Score;
}

bool FDruidsMCPBridgeServer::IsHighConfidenceSpawnerCandidate(const TSharedPtr<FJsonObject>& Candidate, const FString& Query, const FString& Intent, const FString& Operator, const FString& ValueType, int32 Score, FString& OutReason)
{
	if (!Candidate.IsValid())
	{
		OutReason = TEXT("candidate is invalid");
		return false;
	}

	FString Name;
	FString Category;
	FString SpawnerId;
	Candidate->TryGetStringField(TEXT("name"), Name);
	Candidate->TryGetStringField(TEXT("category"), Category);
	Candidate->TryGetStringField(TEXT("spawner_id"), SpawnerId);

	const FString NormalizedName = NormalizeResolverText(Name);
	const FString NormalizedQuery = NormalizeResolverText(Query);
	const FString NormalizedIntent = NormalizeResolverText(Intent);
	const FString NormalizedOperator = NormalizeResolverText(Operator.IsEmpty() ? Query : Operator);
	const FString NormalizedType = NormalizeResolverText(ValueType);

	if (!NormalizedQuery.IsEmpty() && NormalizedName == NormalizedQuery)
	{
		OutReason = TEXT("exact node name match");
		return true;
	}

	if (!SpawnerId.IsEmpty() && !Query.IsEmpty() && SpawnerId.Equals(Query, ESearchCase::IgnoreCase))
	{
		OutReason = TEXT("exact spawner id match");
		return true;
	}

	const bool bGuessedAddEventRequest = NormalizedQuery.Contains(TEXT("addevent"));
	const bool bInputEventIntent = NormalizedIntent.Contains(TEXT("event")) || NormalizedIntent.Contains(TEXT("input"));
	if (!bGuessedAddEventRequest &&
		SpawnerId.Equals(TEXT("input_axis_key::MouseWheelAxis"), ESearchCase::IgnoreCase) &&
		NormalizedQuery.Contains(TEXT("mousewheel")) &&
		(bInputEventIntent || NormalizedName == NormalizedQuery))
	{
		OutReason = TEXT("input axis key event match");
		return true;
	}
	if (!bGuessedAddEventRequest &&
		SpawnerId.Equals(TEXT("input_key::LeftMouseButton"), ESearchCase::IgnoreCase) &&
		NormalizedQuery.Contains(TEXT("leftmouse")) &&
		(bInputEventIntent || NormalizedName == NormalizedQuery))
	{
		OutReason = TEXT("input key event match");
		return true;
	}
	FString ResolvedInputKeyName;
	if (!bGuessedAddEventRequest &&
		DruidsMCPBridge::TryResolveInputKeyNameFromQuery(Query, ResolvedInputKeyName) &&
		SpawnerId.Equals(FString::Printf(TEXT("input_key::%s"), *ResolvedInputKeyName), ESearchCase::IgnoreCase) &&
		(bInputEventIntent || NormalizedName == NormalizeResolverText(ResolvedInputKeyName)))
	{
		OutReason = TEXT("input key event match");
		return true;
	}

	if (SpawnerId.StartsWith(TEXT("custom_event_call::")) && Score >= 500)
	{
		OutReason = TEXT("callable custom event match");
		return true;
	}

	if (SpawnerId.Equals(TEXT("node::sequence"), ESearchCase::IgnoreCase) &&
		(NormalizedName == NormalizedQuery || NormalizedQuery == TEXT("sequence") || NormalizedQuery.Contains(TEXT("executionsequence")) || NormalizedIntent.Contains(TEXT("flow")) || NormalizedIntent.Contains(TEXT("control"))))
	{
		OutReason = TEXT("flow control sequence match");
		return true;
	}

	if (SpawnerId.StartsWith(TEXT("macro::")) &&
		((NormalizedName == NormalizedQuery) || (!Query.IsEmpty() && SpawnerId.Equals(Query, ESearchCase::IgnoreCase))) &&
		(SpawnerId.Equals(DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("DoOnce")), ESearchCase::IgnoreCase) ||
			SpawnerId.Equals(DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("Gate")), ESearchCase::IgnoreCase) ||
			SpawnerId.Equals(DruidsMCPBridge::GetStandardMacroSpawnerId(TEXT("FlipFlop")), ESearchCase::IgnoreCase)))
	{
		OutReason = TEXT("standard flow-control macro match");
		return true;
	}

	const bool bMathIntent = NormalizedIntent.Contains(TEXT("math")) || NormalizedOperator.Contains(TEXT("add")) || NormalizedOperator.Contains(TEXT("plus")) || NormalizedOperator.Contains(TEXT("+"));
	if (bMathIntent && NormalizedName == TEXT("int+int") && (NormalizedType.IsEmpty() || NormalizedType == TEXT("int") || NormalizedType == TEXT("integer")))
	{
		OutReason = TEXT("integer add intent match");
		return true;
	}

	const bool bTypedDataIntent = NormalizedIntent.Contains(TEXT("math")) || NormalizedIntent.Contains(TEXT("comparison"));
	if (bTypedDataIntent && SpawnerId.StartsWith(TEXT("func::")) && Score >= 300)
	{
		OutReason = TEXT("typed data operation intent match");
		return true;
	}

	const bool bConversionIntent = NormalizedIntent.Contains(TEXT("conversion")) || NormalizedQuery.Contains(TEXT("tostring")) || NormalizedQuery.Contains(TEXT("convert"));
	if (bConversionIntent)
	{
		bool bInputTypeMatches = NormalizedType.IsEmpty();
		const TArray<TSharedPtr<FJsonValue>>* Inputs = nullptr;
		if (!bInputTypeMatches && Candidate->TryGetArrayField(TEXT("inputs"), Inputs))
		{
			for (const TSharedPtr<FJsonValue>& Input : *Inputs)
			{
				if (NormalizeResolverText(Input->AsString()).Contains(NormalizedType))
				{
					bInputTypeMatches = true;
					break;
				}
			}
		}

		if ((NormalizedName.Contains(TEXT("tostring")) || SpawnerId.Contains(TEXT("Conv_"))) && bInputTypeMatches)
		{
			OutReason = TEXT("conversion intent and input type match");
			return true;
		}

		OutReason = bInputTypeMatches ? TEXT("conversion candidate is not a supported converter") : TEXT("conversion candidate input type mismatch");
		return false;
	}

	if (Score >= 500)
	{
		OutReason = FString::Printf(TEXT("high resolver score %d"), Score);
		return true;
	}

	OutReason = FString::Printf(TEXT("low resolver score %d"), Score);
	return false;
}

bool FDruidsMCPBridgeServer::IsUnsupportedInputEventRequest(const FString& Query, const FString& Intent)
{
	const FString NormalizedQuery = NormalizeResolverText(Query);
	const FString NormalizedIntent = NormalizeResolverText(Intent);

	const bool bMouseWheelRequest = NormalizedQuery.Contains(TEXT("mousewheel")) || NormalizedQuery.Contains(TEXT("mousewheelaxis"));
	const bool bLeftMouseRequest = NormalizedQuery.Contains(TEXT("leftmouse")) || NormalizedQuery.Contains(TEXT("leftmousebutton"));
	return (bMouseWheelRequest || bLeftMouseRequest) && NormalizedQuery.Contains(TEXT("addevent"));
}

FString FDruidsMCPBridgeServer::NormalizeResolverText(const FString& Text)
{
	FString Result = Text.ToLower();
	Result.ReplaceInline(TEXT(" "), TEXT(""));
	Result.ReplaceInline(TEXT("_"), TEXT(""));
	Result.ReplaceInline(TEXT("-"), TEXT(""));
	Result.ReplaceInline(TEXT("("), TEXT(""));
	Result.ReplaceInline(TEXT(")"), TEXT(""));
	return Result;
}

void FDruidsMCPBridgeServer::AddUniqueString(TArray<FString>& Strings, const FString& Value)
{
	const FString CleanValue = Value.TrimStartAndEnd();
	if (!CleanValue.IsEmpty() && !Strings.ContainsByPredicate([&CleanValue](const FString& Existing)
	{
		return Existing.Equals(CleanValue, ESearchCase::IgnoreCase);
	}))
	{
		Strings.Add(CleanValue);
	}
}

FString FDruidsMCPBridgeServer::JsonObjectToString(const TSharedPtr<FJsonObject>& Object)
{
	FString Output;
	if (Object.IsValid())
	{
		const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
		FJsonSerializer::Serialize(Object.ToSharedRef(), Writer);
	}
	return Output;
}

FString FDruidsMCPBridgeServer::JsonValueToString(const TSharedPtr<FJsonValue>& Value)
{
	FString Output;
	if (Value.IsValid())
	{
		const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
		FJsonSerializer::Serialize(Value, TEXT(""), Writer);
	}
	return Output;
}

TSharedPtr<FJsonValue> FDruidsMCPBridgeServer::CloneJsonValue(const TSharedPtr<FJsonValue>& Value)
{
	if (!Value.IsValid())
	{
		return MakeShared<FJsonValueNull>();
	}

	switch (Value->Type)
	{
	case EJson::String:
		return MakeShared<FJsonValueString>(Value->AsString());
	case EJson::Number:
		return MakeShared<FJsonValueNumber>(Value->AsNumber());
	case EJson::Boolean:
		return MakeShared<FJsonValueBoolean>(Value->AsBool());
	case EJson::Array:
		{
			TArray<TSharedPtr<FJsonValue>> Array;
			for (const TSharedPtr<FJsonValue>& Item : Value->AsArray())
			{
				Array.Add(CloneJsonValue(Item));
			}
			return MakeShared<FJsonValueArray>(Array);
		}
	case EJson::Object:
		return MakeShared<FJsonValueObject>(CloneJsonObject(Value->AsObject()));
	case EJson::Null:
	default:
		return MakeShared<FJsonValueNull>();
	}
}

TSharedPtr<FJsonObject> FDruidsMCPBridgeServer::CloneJsonObject(const TSharedPtr<FJsonObject>& Object)
{
	TSharedPtr<FJsonObject> Clone = MakeShared<FJsonObject>();
	if (!Object.IsValid())
	{
		return Clone;
	}

	for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : Object->Values)
	{
		Clone->SetField(Pair.Key, CloneJsonValue(Pair.Value));
	}

	return Clone;
}
