#
tokens: 17267/50000 3/49 files (page 2/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 2. Use http://codebase.md/alexkissijr/unrealmcp?page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── MCP
│   ├── 0.1.0
│   ├── check_mcp_setup.py
│   ├── check_setup.bat
│   ├── Commands
│   │   ├── __init__.py
│   │   ├── __pycache__
│   │   │   ├── __init__.cpython-312.pyc
│   │   │   ├── commands_materials.cpython-312.pyc
│   │   │   ├── commands_python.cpython-312.pyc
│   │   │   ├── commands_scene.cpython-312.pyc
│   │   │   ├── materials.cpython-312.pyc
│   │   │   ├── python.cpython-312.pyc
│   │   │   └── scene.cpython-312.pyc
│   │   ├── commands_materials.py
│   │   ├── commands_python.py
│   │   └── commands_scene.py
│   ├── cursor_setup.py
│   ├── example_extension_script.py
│   ├── install_mcp.py
│   ├── README_MCP_SETUP.md
│   ├── requirements.txt
│   ├── run_unreal_mcp.bat
│   ├── setup_cursor_mcp.bat
│   ├── setup_unreal_mcp.bat
│   ├── temp_update_config.py
│   ├── TestScripts
│   │   ├── 1_basic_connection.py
│   │   ├── 2_python_execution.py
│   │   ├── 3_string_test.py
│   │   ├── format_test.py
│   │   ├── README.md
│   │   ├── run_all_tests.py
│   │   ├── simple_test_command.py
│   │   ├── test_commands_basic.py
│   │   ├── test_commands_blueprint.py
│   │   └── test_commands_material.py
│   ├── unreal_mcp_bridge.py
│   ├── UserTools
│   │   ├── __pycache__
│   │   │   └── example_tool.cpython-312.pyc
│   │   ├── example_tool.py
│   │   └── README.md
│   └── utils
│       ├── __init__.py
│       ├── __pycache__
│       │   ├── __init__.cpython-312.pyc
│       │   └── command_utils.cpython-312.pyc
│       └── command_utils.py
├── README.md
├── Resources
│   └── Icon128.png
├── Source
│   └── UnrealMCP
│       ├── Private
│       │   ├── MCPCommandHandlers_Blueprints.cpp
│       │   ├── MCPCommandHandlers_Materials.cpp
│       │   ├── MCPCommandHandlers.cpp
│       │   ├── MCPConstants.cpp
│       │   ├── MCPExtensionExample.cpp
│       │   ├── MCPFileLogger.h
│       │   ├── MCPTCPServer.cpp
│       │   └── UnrealMCP.cpp
│       ├── Public
│       │   ├── MCPCommandHandlers_Blueprints.h
│       │   ├── MCPCommandHandlers_Materials.h
│       │   ├── MCPCommandHandlers.h
│       │   ├── MCPConstants.h
│       │   ├── MCPExtensionHandler.h
│       │   ├── MCPSettings.h
│       │   ├── MCPTCPServer.h
│       │   └── UnrealMCP.h
│       └── UnrealMCP.Build.cs
└── UnrealMCP.uplugin
```

# Files

--------------------------------------------------------------------------------
/Source/UnrealMCP/Private/MCPCommandHandlers_Materials.cpp:
--------------------------------------------------------------------------------

```cpp
#include "MCPCommandHandlers_Materials.h"
#include "MCPCommandHandlers.h"
#include "Editor.h"
#include "MCPFileLogger.h"
#include "HAL/PlatformFilemanager.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/Guid.h"
#include "MCPConstants.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "UObject/SavePackage.h"


//
// FMCPCreateMaterialHandler
//
TSharedPtr<FJsonObject> FMCPCreateMaterialHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
{
    MCP_LOG_INFO("Handling create_material command");

    FString PackagePath;
    if (!Params->TryGetStringField(FStringView(TEXT("package_path")), PackagePath))
    {
        MCP_LOG_WARNING("Missing 'package_path' field in create_material command");
        return CreateErrorResponse("Missing 'package_path' field");
    }

    FString MaterialName;
    if (!Params->TryGetStringField(FStringView(TEXT("name")), MaterialName))
    {
        MCP_LOG_WARNING("Missing 'name' field in create_material command");
        return CreateErrorResponse("Missing 'name' field");
    }

    // Get optional properties
    const TSharedPtr<FJsonObject>* Properties = nullptr;
    Params->TryGetObjectField(FStringView(TEXT("properties")), Properties);

    // Create the material
    TPair<UMaterial*, bool> Result = CreateMaterial(PackagePath, MaterialName, Properties ? *Properties : nullptr);

    if (Result.Value)
    {
        TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
        ResultObj->SetStringField("name", Result.Key->GetName());
        ResultObj->SetStringField("path", Result.Key->GetPathName());
        return CreateSuccessResponse(ResultObj);
    }
    else
    {
        return CreateErrorResponse("Failed to create material");
    }
}

TPair<UMaterial*, bool> FMCPCreateMaterialHandler::CreateMaterial(const FString& PackagePath, const FString& MaterialName, const TSharedPtr<FJsonObject>& Properties)
{
    // Create the package path
    FString FullPath = FPaths::Combine(PackagePath, MaterialName);
    UPackage* Package = CreatePackage(*FullPath);
    if (!Package)
    {
        MCP_LOG_ERROR("Failed to create package at path: %s", *FullPath);
        return TPair<UMaterial*, bool>(nullptr, false);
    }

    // Create the material
    UMaterial* NewMaterial = NewObject<UMaterial>(Package, *MaterialName, RF_Public | RF_Standalone);
    if (!NewMaterial)
    {
        MCP_LOG_ERROR("Failed to create material: %s", *MaterialName);
        return TPair<UMaterial*, bool>(nullptr, false);
    }

    // Set default properties
    NewMaterial->SetShadingModel(MSM_DefaultLit);
    NewMaterial->BlendMode = BLEND_Opaque;
    NewMaterial->TwoSided = false;
    NewMaterial->DitheredLODTransition = false;
    NewMaterial->bCastDynamicShadowAsMasked = false;

    // Apply any custom properties if provided
    if (Properties)
    {
        ModifyMaterialProperties(NewMaterial, Properties);
    }

    // Save the package
    Package->SetDirtyFlag(true);
    
    // Construct the full file path for saving
    FString SavePath = FPaths::Combine(FPaths::ProjectContentDir(), PackagePath, MaterialName + TEXT(".uasset"));
    
    // Create save package args
    FSavePackageArgs SaveArgs;
    SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
    SaveArgs.SaveFlags = SAVE_NoError;
    SaveArgs.bForceByteSwapping = false;
    SaveArgs.bWarnOfLongFilename = true;
    
    // Save the package
    if (!UPackage::SavePackage(Package, NewMaterial, *SavePath, SaveArgs))
    {
        MCP_LOG_ERROR("Failed to save material package at path: %s", *SavePath);
        return TPair<UMaterial*, bool>(nullptr, false);
    }
    
    // Trigger material compilation
    NewMaterial->PostEditChange();

    MCP_LOG_INFO("Created material: %s at path: %s", *MaterialName, *FullPath);
    return TPair<UMaterial*, bool>(NewMaterial, true);
}

bool FMCPCreateMaterialHandler::ModifyMaterialProperties(UMaterial* Material, const TSharedPtr<FJsonObject>& Properties)
{
    if (!Material || !Properties)
    {
        return false;
    }

    bool bSuccess = true;

    // Shading Model
    FString ShadingModel;
    if (Properties->TryGetStringField(FStringView(TEXT("shading_model")), ShadingModel))
    {
        if (ShadingModel == "DefaultLit")
            Material->SetShadingModel(MSM_DefaultLit);
        else if (ShadingModel == "Unlit")
            Material->SetShadingModel(MSM_Unlit);
        else if (ShadingModel == "Subsurface")
            Material->SetShadingModel(MSM_Subsurface);
        else if (ShadingModel == "PreintegratedSkin")
            Material->SetShadingModel(MSM_PreintegratedSkin);
        else if (ShadingModel == "ClearCoat")
            Material->SetShadingModel(MSM_ClearCoat);
        else if (ShadingModel == "SubsurfaceProfile")
            Material->SetShadingModel(MSM_SubsurfaceProfile);
        else if (ShadingModel == "TwoSidedFoliage")
            Material->SetShadingModel(MSM_TwoSidedFoliage);
        else if (ShadingModel == "Hair")
            Material->SetShadingModel(MSM_Hair);
        else if (ShadingModel == "Cloth")
            Material->SetShadingModel(MSM_Cloth);
        else if (ShadingModel == "Eye")
            Material->SetShadingModel(MSM_Eye);
        else
            bSuccess = false;
    }

    // Blend Mode
    FString BlendMode;
    if (Properties->TryGetStringField(FStringView(TEXT("blend_mode")), BlendMode))
    {
        if (BlendMode == "Opaque")
            Material->BlendMode = BLEND_Opaque;
        else if (BlendMode == "Masked")
            Material->BlendMode = BLEND_Masked;
        else if (BlendMode == "Translucent")
            Material->BlendMode = BLEND_Translucent;
        else if (BlendMode == "Additive")
            Material->BlendMode = BLEND_Additive;
        else if (BlendMode == "Modulate")
            Material->BlendMode = BLEND_Modulate;
        else if (BlendMode == "AlphaComposite")
            Material->BlendMode = BLEND_AlphaComposite;
        else if (BlendMode == "AlphaHoldout")
            Material->BlendMode = BLEND_AlphaHoldout;
        else
            bSuccess = false;
    }

    // Two Sided
    bool bTwoSided;
    if (Properties->TryGetBoolField(FStringView(TEXT("two_sided")), bTwoSided))
    {
        Material->TwoSided = bTwoSided;
    }

    // Dithered LOD Transition
    bool bDitheredLODTransition;
    if (Properties->TryGetBoolField(FStringView(TEXT("dithered_lod_transition")), bDitheredLODTransition))
    {
        Material->DitheredLODTransition = bDitheredLODTransition;
    }

    // Cast Contact Shadow
    bool bCastContactShadow;
    if (Properties->TryGetBoolField(FStringView(TEXT("cast_contact_shadow")), bCastContactShadow))
    {
        Material->bCastDynamicShadowAsMasked = bCastContactShadow;
    }

    // Base Color
    const TArray<TSharedPtr<FJsonValue>>* BaseColorArray = nullptr;
    if (Properties->TryGetArrayField(FStringView(TEXT("base_color")), BaseColorArray) && BaseColorArray && BaseColorArray->Num() == 4)
    {
        FLinearColor BaseColor(
            (*BaseColorArray)[0]->AsNumber(),
            (*BaseColorArray)[1]->AsNumber(),
            (*BaseColorArray)[2]->AsNumber(),
            (*BaseColorArray)[3]->AsNumber()
        );
        
        // Create a Vector4 constant expression for base color
        UMaterialExpressionVectorParameter* BaseColorParam = NewObject<UMaterialExpressionVectorParameter>(Material);
        BaseColorParam->ParameterName = TEXT("BaseColor");
        BaseColorParam->DefaultValue = BaseColor;
        Material->GetExpressionCollection().AddExpression(BaseColorParam);
        Material->GetEditorOnlyData()->BaseColor.Expression = BaseColorParam;
    }

    // Metallic
    double Metallic;
    if (Properties->TryGetNumberField(FStringView(TEXT("metallic")), Metallic))
    {
        // Create a scalar constant expression for metallic
        UMaterialExpressionScalarParameter* MetallicParam = NewObject<UMaterialExpressionScalarParameter>(Material);
        MetallicParam->ParameterName = TEXT("Metallic");
        MetallicParam->DefaultValue = FMath::Clamp(Metallic, 0.0, 1.0);
        Material->GetExpressionCollection().AddExpression(MetallicParam);
        Material->GetEditorOnlyData()->Metallic.Expression = MetallicParam;
    }

    // Roughness
    double Roughness;
    if (Properties->TryGetNumberField(FStringView(TEXT("roughness")), Roughness))
    {
        // Create a scalar constant expression for roughness
        UMaterialExpressionScalarParameter* RoughnessParam = NewObject<UMaterialExpressionScalarParameter>(Material);
        RoughnessParam->ParameterName = TEXT("Roughness");
        RoughnessParam->DefaultValue = FMath::Clamp(Roughness, 0.0, 1.0);
        Material->GetExpressionCollection().AddExpression(RoughnessParam);
        Material->GetEditorOnlyData()->Roughness.Expression = RoughnessParam;
    }

    return bSuccess;
}

//
// FMCPModifyMaterialHandler
//
TSharedPtr<FJsonObject> FMCPModifyMaterialHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
{
    MCP_LOG_INFO("Handling modify_material command");

    FString MaterialPath;
    if (!Params->TryGetStringField(FStringView(TEXT("path")), MaterialPath))
    {
        MCP_LOG_WARNING("Missing 'path' field in modify_material command");
        return CreateErrorResponse("Missing 'path' field");
    }

    const TSharedPtr<FJsonObject>* Properties = nullptr;
    if (!Params->TryGetObjectField(FStringView(TEXT("properties")), Properties))
    {
        MCP_LOG_WARNING("Missing 'properties' field in modify_material command");
        return CreateErrorResponse("Missing 'properties' field");
    }

    // Load the material
    UMaterial* Material = LoadObject<UMaterial>(nullptr, *MaterialPath);
    if (!Material)
    {
        MCP_LOG_ERROR("Failed to load material at path: %s", *MaterialPath);
        return CreateErrorResponse(FString::Printf(TEXT("Failed to load material at path: %s"), *MaterialPath));
    }

    // Modify the material properties
    bool bSuccess = ModifyMaterialProperties(Material, *Properties);

    if (bSuccess)
    {
        // Save the package
        Material->GetPackage()->SetDirtyFlag(true);
        
        // Create save package args
        FSavePackageArgs SaveArgs;
        SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
        SaveArgs.SaveFlags = SAVE_NoError;
        SaveArgs.bForceByteSwapping = false;
        SaveArgs.bWarnOfLongFilename = true;
        
        // Construct the full file path for saving
        FString SavePath = FPaths::Combine(FPaths::ProjectContentDir(), Material->GetPathName() + TEXT(".uasset"));
        
        // Save the package with the proper args
        if (!UPackage::SavePackage(Material->GetPackage(), Material, *SavePath, SaveArgs))
        {
            MCP_LOG_ERROR("Failed to save material package at path: %s", *SavePath);
            return CreateErrorResponse("Failed to save material package");
        }

        // Trigger material compilation
        Material->PostEditChange();

        TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
        ResultObj->SetStringField("name", Material->GetName());
        ResultObj->SetStringField("path", Material->GetPathName());
        return CreateSuccessResponse(ResultObj);
    }
    else
    {
        return CreateErrorResponse("Failed to modify material properties");
    }
}

bool FMCPModifyMaterialHandler::ModifyMaterialProperties(UMaterial* Material, const TSharedPtr<FJsonObject>& Properties)
{
    if (!Material || !Properties)
    {
        return false;
    }

    bool bSuccess = true;

    // Shading Model
    FString ShadingModel;
    if (Properties->TryGetStringField(FStringView(TEXT("shading_model")), ShadingModel))
    {
        if (ShadingModel == "DefaultLit")
            Material->SetShadingModel(MSM_DefaultLit);
        else if (ShadingModel == "Unlit")
            Material->SetShadingModel(MSM_Unlit);
        else if (ShadingModel == "Subsurface")
            Material->SetShadingModel(MSM_Subsurface);
        else if (ShadingModel == "PreintegratedSkin")
            Material->SetShadingModel(MSM_PreintegratedSkin);
        else if (ShadingModel == "ClearCoat")
            Material->SetShadingModel(MSM_ClearCoat);
        else if (ShadingModel == "SubsurfaceProfile")
            Material->SetShadingModel(MSM_SubsurfaceProfile);
        else if (ShadingModel == "TwoSidedFoliage")
            Material->SetShadingModel(MSM_TwoSidedFoliage);
        else if (ShadingModel == "Hair")
            Material->SetShadingModel(MSM_Hair);
        else if (ShadingModel == "Cloth")
            Material->SetShadingModel(MSM_Cloth);
        else if (ShadingModel == "Eye")
            Material->SetShadingModel(MSM_Eye);
        else
            bSuccess = false;
    }

    // Blend Mode
    FString BlendMode;
    if (Properties->TryGetStringField(FStringView(TEXT("blend_mode")), BlendMode))
    {
        if (BlendMode == "Opaque")
            Material->BlendMode = BLEND_Opaque;
        else if (BlendMode == "Masked")
            Material->BlendMode = BLEND_Masked;
        else if (BlendMode == "Translucent")
            Material->BlendMode = BLEND_Translucent;
        else if (BlendMode == "Additive")
            Material->BlendMode = BLEND_Additive;
        else if (BlendMode == "Modulate")
            Material->BlendMode = BLEND_Modulate;
        else if (BlendMode == "AlphaComposite")
            Material->BlendMode = BLEND_AlphaComposite;
        else if (BlendMode == "AlphaHoldout")
            Material->BlendMode = BLEND_AlphaHoldout;
        else
            bSuccess = false;
    }

    // Two Sided
    bool bTwoSided;
    if (Properties->TryGetBoolField(FStringView(TEXT("two_sided")), bTwoSided))
    {
        Material->TwoSided = bTwoSided;
    }

    // Dithered LOD Transition
    bool bDitheredLODTransition;
    if (Properties->TryGetBoolField(FStringView(TEXT("dithered_lod_transition")), bDitheredLODTransition))
    {
        Material->DitheredLODTransition = bDitheredLODTransition;
    }

    // Cast Contact Shadow
    bool bCastContactShadow;
    if (Properties->TryGetBoolField(FStringView(TEXT("cast_contact_shadow")), bCastContactShadow))
    {
        Material->bCastDynamicShadowAsMasked = bCastContactShadow;
    }

    // Base Color
    const TArray<TSharedPtr<FJsonValue>>* BaseColorArray = nullptr;
    if (Properties->TryGetArrayField(FStringView(TEXT("base_color")), BaseColorArray) && BaseColorArray && BaseColorArray->Num() == 4)
    {
        FLinearColor BaseColor(
            (*BaseColorArray)[0]->AsNumber(),
            (*BaseColorArray)[1]->AsNumber(),
            (*BaseColorArray)[2]->AsNumber(),
            (*BaseColorArray)[3]->AsNumber()
        );
        
        // Create a Vector4 constant expression for base color
        UMaterialExpressionVectorParameter* BaseColorParam = NewObject<UMaterialExpressionVectorParameter>(Material);
        BaseColorParam->ParameterName = TEXT("BaseColor");
        BaseColorParam->DefaultValue = BaseColor;
        Material->GetExpressionCollection().AddExpression(BaseColorParam);
        Material->GetEditorOnlyData()->BaseColor.Expression = BaseColorParam;
    }

    // Metallic
    double Metallic;
    if (Properties->TryGetNumberField(FStringView(TEXT("metallic")), Metallic))
    {
        // Create a scalar constant expression for metallic
        UMaterialExpressionScalarParameter* MetallicParam = NewObject<UMaterialExpressionScalarParameter>(Material);
        MetallicParam->ParameterName = TEXT("Metallic");
        MetallicParam->DefaultValue = FMath::Clamp(Metallic, 0.0, 1.0);
        Material->GetExpressionCollection().AddExpression(MetallicParam);
        Material->GetEditorOnlyData()->Metallic.Expression = MetallicParam;
    }

    // Roughness
    double Roughness;
    if (Properties->TryGetNumberField(FStringView(TEXT("roughness")), Roughness))
    {
        // Create a scalar constant expression for roughness
        UMaterialExpressionScalarParameter* RoughnessParam = NewObject<UMaterialExpressionScalarParameter>(Material);
        RoughnessParam->ParameterName = TEXT("Roughness");
        RoughnessParam->DefaultValue = FMath::Clamp(Roughness, 0.0, 1.0);
        Material->GetExpressionCollection().AddExpression(RoughnessParam);
        Material->GetEditorOnlyData()->Roughness.Expression = RoughnessParam;
    }

    return bSuccess;
}

//
// FMCPGetMaterialInfoHandler
//
TSharedPtr<FJsonObject> FMCPGetMaterialInfoHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
{
    MCP_LOG_INFO("Handling get_material_info command");

    FString MaterialPath;
    if (!Params->TryGetStringField(FStringView(TEXT("path")), MaterialPath))
    {
        MCP_LOG_WARNING("Missing 'path' field in get_material_info command");
        return CreateErrorResponse("Missing 'path' field");
    }

    // Load the material
    UMaterial* Material = LoadObject<UMaterial>(nullptr, *MaterialPath);
    if (!Material)
    {
        MCP_LOG_ERROR("Failed to load material at path: %s", *MaterialPath);
        return CreateErrorResponse(FString::Printf(TEXT("Failed to load material at path: %s"), *MaterialPath));
    }

    // Get material info
    TSharedPtr<FJsonObject> ResultObj = GetMaterialInfo(Material);
    return CreateSuccessResponse(ResultObj);
}

TSharedPtr<FJsonObject> FMCPGetMaterialInfoHandler::GetMaterialInfo(UMaterial* Material)
{
    TSharedPtr<FJsonObject> Info = MakeShared<FJsonObject>();
    
    // Basic info
    Info->SetStringField("name", Material->GetName());
    Info->SetStringField("path", Material->GetPathName());

    // Shading Model
    FString ShadingModel = "Unknown";
    FMaterialShadingModelField ShadingModels = Material->GetShadingModels();
    if (ShadingModels.HasShadingModel(MSM_DefaultLit)) ShadingModel = "DefaultLit";
    else if (ShadingModels.HasShadingModel(MSM_Unlit)) ShadingModel = "Unlit";
    else if (ShadingModels.HasShadingModel(MSM_Subsurface)) ShadingModel = "Subsurface";
    else if (ShadingModels.HasShadingModel(MSM_PreintegratedSkin)) ShadingModel = "PreintegratedSkin";
    else if (ShadingModels.HasShadingModel(MSM_ClearCoat)) ShadingModel = "ClearCoat";
    else if (ShadingModels.HasShadingModel(MSM_SubsurfaceProfile)) ShadingModel = "SubsurfaceProfile";
    else if (ShadingModels.HasShadingModel(MSM_TwoSidedFoliage)) ShadingModel = "TwoSidedFoliage";
    else if (ShadingModels.HasShadingModel(MSM_Hair)) ShadingModel = "Hair";
    else if (ShadingModels.HasShadingModel(MSM_Cloth)) ShadingModel = "Cloth";
    else if (ShadingModels.HasShadingModel(MSM_Eye)) ShadingModel = "Eye";
    Info->SetStringField("shading_model", ShadingModel);

    // Blend Mode
    FString BlendMode;
    switch (Material->GetBlendMode())
    {
        case BLEND_Opaque: BlendMode = "Opaque"; break;
        case BLEND_Masked: BlendMode = "Masked"; break;
        case BLEND_Translucent: BlendMode = "Translucent"; break;
        case BLEND_Additive: BlendMode = "Additive"; break;
        case BLEND_Modulate: BlendMode = "Modulate"; break;
        case BLEND_AlphaComposite: BlendMode = "AlphaComposite"; break;
        case BLEND_AlphaHoldout: BlendMode = "AlphaHoldout"; break;
        default: BlendMode = "Unknown"; break;
    }
    Info->SetStringField("blend_mode", BlendMode);

    // Other properties
    Info->SetBoolField("two_sided", Material->IsTwoSided());
    Info->SetBoolField("dithered_lod_transition", Material->IsDitheredLODTransition());
    Info->SetBoolField("cast_contact_shadow", Material->bContactShadows);

    // Base Color
    TArray<TSharedPtr<FJsonValue>> BaseColorArray;
    FLinearColor BaseColorValue = FLinearColor::White;
    if (Material->GetEditorOnlyData()->BaseColor.Expression)
    {
        if (UMaterialExpressionVectorParameter* BaseColorParam = Cast<UMaterialExpressionVectorParameter>(Material->GetEditorOnlyData()->BaseColor.Expression))
        {
            BaseColorValue = BaseColorParam->DefaultValue;
        }
    }
    BaseColorArray.Add(MakeShared<FJsonValueNumber>(BaseColorValue.R));
    BaseColorArray.Add(MakeShared<FJsonValueNumber>(BaseColorValue.G));
    BaseColorArray.Add(MakeShared<FJsonValueNumber>(BaseColorValue.B));
    BaseColorArray.Add(MakeShared<FJsonValueNumber>(BaseColorValue.A));
    Info->SetArrayField("base_color", BaseColorArray);

    // Metallic
    float MetallicValue = 0.0f;
    if (Material->GetEditorOnlyData()->Metallic.Expression)
    {
        if (UMaterialExpressionScalarParameter* MetallicParam = Cast<UMaterialExpressionScalarParameter>(Material->GetEditorOnlyData()->Metallic.Expression))
        {
            MetallicValue = MetallicParam->DefaultValue;
        }
    }
    Info->SetNumberField("metallic", MetallicValue);

    // Roughness
    float RoughnessValue = 0.5f;
    if (Material->GetEditorOnlyData()->Roughness.Expression)
    {
        if (UMaterialExpressionScalarParameter* RoughnessParam = Cast<UMaterialExpressionScalarParameter>(Material->GetEditorOnlyData()->Roughness.Expression))
        {
            RoughnessValue = RoughnessParam->DefaultValue;
        }
    }
    Info->SetNumberField("roughness", RoughnessValue);

    return Info;
} 
```

--------------------------------------------------------------------------------
/Source/UnrealMCP/Private/MCPCommandHandlers.cpp:
--------------------------------------------------------------------------------

```cpp
#include "MCPCommandHandlers.h"

#include "ActorEditorUtils.h"
#include "Editor.h"
#include "EngineUtils.h"
#include "MCPFileLogger.h"
#include "HAL/PlatformFilemanager.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/Guid.h"
#include "MCPConstants.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Engine/Blueprint.h"
#include "Engine/BlueprintGeneratedClass.h"


//
// FMCPGetSceneInfoHandler
//
TSharedPtr<FJsonObject> FMCPGetSceneInfoHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
{
    MCP_LOG_INFO("Handling get_scene_info command");

    UWorld *World = GEditor->GetEditorWorldContext().World();
    TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
    TArray<TSharedPtr<FJsonValue>> ActorsArray;

    int32 ActorCount = 0;
    int32 TotalActorCount = 0;
    bool bLimitReached = false;

    // First count the total number of actors
    for (TActorIterator<AActor> CountIt(World); CountIt; ++CountIt)
    {
        TotalActorCount++;
    }

    // Then collect actor info up to the limit
    for (TActorIterator<AActor> It(World); It; ++It)
    {
        AActor *Actor = *It;
        TSharedPtr<FJsonObject> ActorInfo = MakeShared<FJsonObject>();
        ActorInfo->SetStringField("name", Actor->GetName());
        ActorInfo->SetStringField("type", Actor->GetClass()->GetName());

        // Add the actor label (user-facing friendly name)
        ActorInfo->SetStringField("label", Actor->GetActorLabel());

        // Add location
        FVector Location = Actor->GetActorLocation();
        TArray<TSharedPtr<FJsonValue>> LocationArray;
        LocationArray.Add(MakeShared<FJsonValueNumber>(Location.X));
        LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Y));
        LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Z));
        ActorInfo->SetArrayField("location", LocationArray);

        ActorsArray.Add(MakeShared<FJsonValueObject>(ActorInfo));
        ActorCount++;
        if (ActorCount >= MCPConstants::MAX_ACTORS_IN_SCENE_INFO)
        {
            bLimitReached = true;
            MCP_LOG_WARNING("Actor limit reached (%d). Only returning %d of %d actors.",
                            MCPConstants::MAX_ACTORS_IN_SCENE_INFO, ActorCount, TotalActorCount);
            break; // Limit for performance
        }
    }

    Result->SetStringField("level", World->GetName());
    Result->SetNumberField("actor_count", TotalActorCount);
    Result->SetNumberField("returned_actor_count", ActorCount);
    Result->SetBoolField("limit_reached", bLimitReached);
    Result->SetArrayField("actors", ActorsArray);

    MCP_LOG_INFO("Sending get_scene_info response with %d/%d actors", ActorCount, TotalActorCount);

    return CreateSuccessResponse(Result);
}

//
// FMCPCreateObjectHandler
//
TSharedPtr<FJsonObject> FMCPCreateObjectHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
{
    UWorld *World = GEditor->GetEditorWorldContext().World();

    FString Type;
    if (!Params->TryGetStringField(FStringView(TEXT("type")), Type))
    {
        MCP_LOG_WARNING("Missing 'type' field in create_object command");
        return CreateErrorResponse("Missing 'type' field");
    }

    // Get location
    const TArray<TSharedPtr<FJsonValue>> *LocationArrayPtr = nullptr;
    if (!Params->TryGetArrayField(FStringView(TEXT("location")), LocationArrayPtr) || !LocationArrayPtr || LocationArrayPtr->Num() != 3)
    {
        MCP_LOG_WARNING("Invalid 'location' field in create_object command");
        return CreateErrorResponse("Invalid 'location' field");
    }

    FVector Location(
        (*LocationArrayPtr)[0]->AsNumber(),
        (*LocationArrayPtr)[1]->AsNumber(),
        (*LocationArrayPtr)[2]->AsNumber());

    // Convert type to lowercase for case-insensitive comparison
    FString TypeLower = Type.ToLower();

    if (Type == "StaticMeshActor")
    {
        // Get mesh path if specified
        FString MeshPath;
        Params->TryGetStringField(FStringView(TEXT("mesh")), MeshPath);

        // Get label if specified
        FString Label;
        Params->TryGetStringField(FStringView(TEXT("label")), Label);

        // Create the actor
        TPair<AStaticMeshActor *, bool> Result = CreateStaticMeshActor(World, Location, MeshPath, Label);

        if (Result.Value)
        {
            TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
            ResultObj->SetStringField("name", Result.Key->GetName());
            ResultObj->SetStringField("label", Result.Key->GetActorLabel());
            return CreateSuccessResponse(ResultObj);
        }
        else
        {
            return CreateErrorResponse("Failed to create StaticMeshActor");
        }
    }
    else if (TypeLower == "cube")
    {
        // Create a cube actor
        FString Label;
        Params->TryGetStringField(FStringView(TEXT("label")), Label);
        TPair<AStaticMeshActor *, bool> Result = CreateCubeActor(World, Location, Label);

        if (Result.Value)
        {
            TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
            ResultObj->SetStringField("name", Result.Key->GetName());
            ResultObj->SetStringField("label", Result.Key->GetActorLabel());
            return CreateSuccessResponse(ResultObj);
        }
        else
        {
            return CreateErrorResponse("Failed to create cube");
        }
    }
    else
    {
        MCP_LOG_WARNING("Unsupported actor type: %s", *Type);
        return CreateErrorResponse(FString::Printf(TEXT("Unsupported actor type: %s"), *Type));
    }
}

TPair<AStaticMeshActor *, bool> FMCPCreateObjectHandler::CreateStaticMeshActor(UWorld *World, const FVector &Location, const FString &MeshPath, const FString &Label)
{
    if (!World)
    {
        return TPair<AStaticMeshActor *, bool>(nullptr, false);
    }

    // Create the actor
    FActorSpawnParameters SpawnParams;
    SpawnParams.Name = NAME_None; // Auto-generate a name
    SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;

    AStaticMeshActor *NewActor = World->SpawnActor<AStaticMeshActor>(Location, FRotator::ZeroRotator, SpawnParams);
    if (NewActor)
    {
        MCP_LOG_INFO("Created StaticMeshActor at location (%f, %f, %f)", Location.X, Location.Y, Location.Z);

        // Set mesh if specified
        if (!MeshPath.IsEmpty())
        {
            UStaticMesh *Mesh = LoadObject<UStaticMesh>(nullptr, *MeshPath);
            if (Mesh)
            {
                NewActor->GetStaticMeshComponent()->SetStaticMesh(Mesh);
                MCP_LOG_INFO("Set mesh to %s", *MeshPath);
            }
            else
            {
                MCP_LOG_WARNING("Failed to load mesh %s", *MeshPath);
            }
        }

        // Set a descriptive label
        if (!Label.IsEmpty())
        {
            NewActor->SetActorLabel(Label);
            MCP_LOG_INFO("Set custom label to %s", *Label);
        }
        else
        {
            NewActor->SetActorLabel(FString::Printf(TEXT("MCP_StaticMesh_%d"), FMath::RandRange(1000, 9999)));
        }

        return TPair<AStaticMeshActor *, bool>(NewActor, true);
    }
    else
    {
        MCP_LOG_ERROR("Failed to create StaticMeshActor");
        return TPair<AStaticMeshActor *, bool>(nullptr, false);
    }
}

TPair<AStaticMeshActor *, bool> FMCPCreateObjectHandler::CreateCubeActor(UWorld *World, const FVector &Location, const FString &Label)
{
    if (!World)
    {
        return TPair<AStaticMeshActor *, bool>(nullptr, false);
    }

    // Create a StaticMeshActor with a cube mesh
    FActorSpawnParameters SpawnParams;
    SpawnParams.Name = NAME_None;
    SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;

    AStaticMeshActor *NewActor = World->SpawnActor<AStaticMeshActor>(Location, FRotator::ZeroRotator, SpawnParams);
    if (NewActor)
    {
        MCP_LOG_INFO("Created Cube at location (%f, %f, %f)", Location.X, Location.Y, Location.Z);

        // Set cube mesh
        UStaticMesh *CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
        if (CubeMesh)
        {
            NewActor->GetStaticMeshComponent()->SetStaticMesh(CubeMesh);
            MCP_LOG_INFO("Set cube mesh");

            // Set a descriptive label
            if (!Label.IsEmpty())
            {
                NewActor->SetActorLabel(Label);
                MCP_LOG_INFO("Set custom label to %s", *Label);
            }
            else
            {
                NewActor->SetActorLabel(FString::Printf(TEXT("MCP_Cube_%d"), FMath::RandRange(1000, 9999)));
            }

            return TPair<AStaticMeshActor *, bool>(NewActor, true);
        }
        else
        {
            MCP_LOG_WARNING("Failed to load cube mesh");
            World->DestroyActor(NewActor);
            return TPair<AStaticMeshActor *, bool>(nullptr, false);
        }
    }
    else
    {
        MCP_LOG_ERROR("Failed to create Cube");
        return TPair<AStaticMeshActor *, bool>(nullptr, false);
    }
}

//
// FMCPModifyObjectHandler
//
TSharedPtr<FJsonObject> FMCPModifyObjectHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
{
    UWorld *World = GEditor->GetEditorWorldContext().World();

    FString ActorName;
    if (!Params->TryGetStringField(FStringView(TEXT("name")), ActorName))
    {
        MCP_LOG_WARNING("Missing 'name' field in modify_object command");
        return CreateErrorResponse("Missing 'name' field");
    }

    AActor *Actor = nullptr;
    for (TActorIterator<AActor> It(World); It; ++It)
    {
        if (It->GetName() == ActorName)
        {
            Actor = *It;
            break;
        }
    }

    if (!Actor)
    {
        MCP_LOG_WARNING("Actor not found: %s", *ActorName);
        return CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
    }

    bool bModified = false;

    // Check for location update
    const TArray<TSharedPtr<FJsonValue>> *LocationArrayPtr = nullptr;
    if (Params->TryGetArrayField(FStringView(TEXT("location")), LocationArrayPtr) && LocationArrayPtr && LocationArrayPtr->Num() == 3)
    {
        FVector NewLocation(
            (*LocationArrayPtr)[0]->AsNumber(),
            (*LocationArrayPtr)[1]->AsNumber(),
            (*LocationArrayPtr)[2]->AsNumber());

        Actor->SetActorLocation(NewLocation);
        MCP_LOG_INFO("Updated location of %s to (%f, %f, %f)", *ActorName, NewLocation.X, NewLocation.Y, NewLocation.Z);
        bModified = true;
    }

    // Check for rotation update
    const TArray<TSharedPtr<FJsonValue>> *RotationArrayPtr = nullptr;
    if (Params->TryGetArrayField(FStringView(TEXT("rotation")), RotationArrayPtr) && RotationArrayPtr && RotationArrayPtr->Num() == 3)
    {
        FRotator NewRotation(
            (*RotationArrayPtr)[0]->AsNumber(),
            (*RotationArrayPtr)[1]->AsNumber(),
            (*RotationArrayPtr)[2]->AsNumber());

        Actor->SetActorRotation(NewRotation);
        MCP_LOG_INFO("Updated rotation of %s to (%f, %f, %f)", *ActorName, NewRotation.Pitch, NewRotation.Yaw, NewRotation.Roll);
        bModified = true;
    }

    // Check for scale update
    const TArray<TSharedPtr<FJsonValue>> *ScaleArrayPtr = nullptr;
    if (Params->TryGetArrayField(FStringView(TEXT("scale")), ScaleArrayPtr) && ScaleArrayPtr && ScaleArrayPtr->Num() == 3)
    {
        FVector NewScale(
            (*ScaleArrayPtr)[0]->AsNumber(),
            (*ScaleArrayPtr)[1]->AsNumber(),
            (*ScaleArrayPtr)[2]->AsNumber());

        Actor->SetActorScale3D(NewScale);
        MCP_LOG_INFO("Updated scale of %s to (%f, %f, %f)", *ActorName, NewScale.X, NewScale.Y, NewScale.Z);
        bModified = true;
    }

    if (bModified)
    {
        // Create a result object with the actor name
        TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
        Result->SetStringField("name", Actor->GetName());

        // Return success with the result object
        return CreateSuccessResponse(Result);
    }
    else
    {
        MCP_LOG_WARNING("No modifications specified for %s", *ActorName);
        TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
        Response->SetStringField("status", "warning");
        Response->SetStringField("message", "No modifications specified");
        return Response;
    }
}

//
// FMCPDeleteObjectHandler
//
TSharedPtr<FJsonObject> FMCPDeleteObjectHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
{
    UWorld *World = GEditor->GetEditorWorldContext().World();

    FString ActorName;
    if (!Params->TryGetStringField(FStringView(TEXT("name")), ActorName))
    {
        MCP_LOG_WARNING("Missing 'name' field in delete_object command");
        return CreateErrorResponse("Missing 'name' field");
    }

    AActor *Actor = nullptr;
    for (TActorIterator<AActor> It(World); It; ++It)
    {
        if (It->GetName() == ActorName)
        {
            Actor = *It;
            break;
        }
    }

    if (!Actor)
    {
        MCP_LOG_WARNING("Actor not found: %s", *ActorName);
        return CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
    }

    // Check if the actor can be deleted
    if (!FActorEditorUtils::IsABuilderBrush(Actor))
    {
        bool bDestroyed = World->DestroyActor(Actor);
        if (bDestroyed)
        {
            MCP_LOG_INFO("Deleted actor: %s", *ActorName);
            return CreateSuccessResponse();
        }
        else
        {
            MCP_LOG_ERROR("Failed to delete actor: %s", *ActorName);
            return CreateErrorResponse(FString::Printf(TEXT("Failed to delete actor: %s"), *ActorName));
        }
    }
    else
    {
        MCP_LOG_WARNING("Cannot delete special actor: %s", *ActorName);
        return CreateErrorResponse(FString::Printf(TEXT("Cannot delete special actor: %s"), *ActorName));
    }
}

//
// FMCPExecutePythonHandler
//
TSharedPtr<FJsonObject> FMCPExecutePythonHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
{
    // Check if we have code or file parameter
    FString PythonCode;
    FString PythonFile;
    bool hasCode = Params->TryGetStringField(FStringView(TEXT("code")), PythonCode);
    bool hasFile = Params->TryGetStringField(FStringView(TEXT("file")), PythonFile);

    // If code/file not found directly, check if they're in a 'data' object
    if (!hasCode && !hasFile)
    {
        const TSharedPtr<FJsonObject> *DataObject;
        if (Params->TryGetObjectField(FStringView(TEXT("data")), DataObject))
        {
            hasCode = (*DataObject)->TryGetStringField(FStringView(TEXT("code")), PythonCode);
            hasFile = (*DataObject)->TryGetStringField(FStringView(TEXT("file")), PythonFile);
        }
    }

    if (!hasCode && !hasFile)
    {
        MCP_LOG_WARNING("Missing 'code' or 'file' field in execute_python command");
        return CreateErrorResponse("Missing 'code' or 'file' field. You must provide either Python code or a file path.");
    }

    FString Result;
    bool bSuccess = false;
    FString ErrorMessage;

    if (hasCode)
    {
        // For code execution, we'll create a temporary file and execute that
        MCP_LOG_INFO("Executing Python code via temporary file");

        // Create a temporary file in the project's Saved/Temp directory
        FString TempDir = FPaths::ProjectSavedDir() / MCPConstants::PYTHON_TEMP_DIR_NAME;
        IPlatformFile &PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

        // Ensure the directory exists
        if (!PlatformFile.DirectoryExists(*TempDir))
        {
            PlatformFile.CreateDirectory(*TempDir);
        }

        // Create a unique filename for the temporary Python script
        FString TempFilePath = TempDir / FString::Printf(TEXT("%s%s.py"), MCPConstants::PYTHON_TEMP_FILE_PREFIX, *FGuid::NewGuid().ToString());

        // Add error handling wrapper to the Python code
        FString WrappedPythonCode = TEXT("import sys\n")
                                        TEXT("import traceback\n")
                                            TEXT("import unreal\n\n")
                                                TEXT("# Create output capture file\n")
                                                    TEXT("output_file = open('") +
                                    TempDir + TEXT("/output.txt', 'w')\n") TEXT("error_file = open('") + TempDir + TEXT("/error.txt', 'w')\n\n") TEXT("# Store original stdout and stderr\n") TEXT("original_stdout = sys.stdout\n") TEXT("original_stderr = sys.stderr\n\n") TEXT("# Redirect stdout and stderr\n") TEXT("sys.stdout = output_file\n") TEXT("sys.stderr = error_file\n\n") TEXT("success = True\n") TEXT("try:\n")
                                    // Instead of directly embedding the code, we'll compile it first to catch syntax errors
                                    TEXT("    # Compile the code to catch syntax errors\n") TEXT("    user_code = '''") +
                                    PythonCode + TEXT("'''\n") TEXT("    try:\n") TEXT("        code_obj = compile(user_code, '<string>', 'exec')\n") TEXT("        # Execute the compiled code\n") TEXT("        exec(code_obj)\n") TEXT("    except SyntaxError as e:\n") TEXT("        traceback.print_exc()\n") TEXT("        success = False\n") TEXT("    except Exception as e:\n") TEXT("        traceback.print_exc()\n") TEXT("        success = False\n") TEXT("except Exception as e:\n") TEXT("    traceback.print_exc()\n") TEXT("    success = False\n") TEXT("finally:\n") TEXT("    # Restore original stdout and stderr\n") TEXT("    sys.stdout = original_stdout\n") TEXT("    sys.stderr = original_stderr\n") TEXT("    output_file.close()\n") TEXT("    error_file.close()\n") TEXT("    # Write success status\n") TEXT("    with open('") + TempDir + TEXT("/status.txt', 'w') as f:\n") TEXT("        f.write('1' if success else '0')\n");

        // Write the Python code to the temporary file
        if (FFileHelper::SaveStringToFile(WrappedPythonCode, *TempFilePath))
        {
            // Execute the temporary file
            FString Command = FString::Printf(TEXT("py \"%s\""), *TempFilePath);
            GEngine->Exec(nullptr, *Command);

            // Read the output, error, and status files
            FString OutputContent;
            FString ErrorContent;
            FString StatusContent;

            FFileHelper::LoadFileToString(OutputContent, *(TempDir / TEXT("output.txt")));
            FFileHelper::LoadFileToString(ErrorContent, *(TempDir / TEXT("error.txt")));
            FFileHelper::LoadFileToString(StatusContent, *(TempDir / TEXT("status.txt")));

            bSuccess = StatusContent.TrimStartAndEnd().Equals(TEXT("1"));

            // Combine output and error for the result
            Result = OutputContent;
            ErrorMessage = ErrorContent;

            // Clean up the temporary files
            PlatformFile.DeleteFile(*TempFilePath);
            PlatformFile.DeleteFile(*(TempDir / TEXT("output.txt")));
            PlatformFile.DeleteFile(*(TempDir / TEXT("error.txt")));
            PlatformFile.DeleteFile(*(TempDir / TEXT("status.txt")));
        }
        else
        {
            MCP_LOG_ERROR("Failed to create temporary Python file at %s", *TempFilePath);
            return CreateErrorResponse(FString::Printf(TEXT("Failed to create temporary Python file at %s"), *TempFilePath));
        }
    }
    else if (hasFile)
    {
        // Execute Python file
        MCP_LOG_INFO("Executing Python file: %s", *PythonFile);

        // Create a temporary directory for output capture
        FString TempDir = FPaths::ProjectSavedDir() / MCPConstants::PYTHON_TEMP_DIR_NAME;
        IPlatformFile &PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

        // Ensure the directory exists
        if (!PlatformFile.DirectoryExists(*TempDir))
        {
            PlatformFile.CreateDirectory(*TempDir);
        }

        // Create a wrapper script that executes the file and captures output
        FString WrapperFilePath = TempDir / FString::Printf(TEXT("%s_wrapper_%s.py"), MCPConstants::PYTHON_TEMP_FILE_PREFIX, *FGuid::NewGuid().ToString());

        FString WrapperCode = TEXT("import sys\n")
                                  TEXT("import traceback\n")
                                      TEXT("import unreal\n\n")
                                          TEXT("# Create output capture file\n")
                                              TEXT("output_file = open('") +
                              TempDir + TEXT("/output.txt', 'w')\n") TEXT("error_file = open('") + TempDir + TEXT("/error.txt', 'w')\n\n") TEXT("# Store original stdout and stderr\n") TEXT("original_stdout = sys.stdout\n") TEXT("original_stderr = sys.stderr\n\n") TEXT("# Redirect stdout and stderr\n") TEXT("sys.stdout = output_file\n") TEXT("sys.stderr = error_file\n\n") TEXT("success = True\n") TEXT("try:\n") TEXT("    # Read the file content\n") TEXT("    with open('") + PythonFile.Replace(TEXT("\\"), TEXT("\\\\")) + TEXT("', 'r') as f:\n") TEXT("        file_content = f.read()\n") TEXT("    # Compile the code to catch syntax errors\n") TEXT("    try:\n") TEXT("        code_obj = compile(file_content, '") + PythonFile.Replace(TEXT("\\"), TEXT("\\\\")) + TEXT("', 'exec')\n") TEXT("        # Execute the compiled code\n") TEXT("        exec(code_obj)\n") TEXT("    except SyntaxError as e:\n") TEXT("        traceback.print_exc()\n") TEXT("        success = False\n") TEXT("    except Exception as e:\n") TEXT("        traceback.print_exc()\n") TEXT("        success = False\n") TEXT("except Exception as e:\n") TEXT("    traceback.print_exc()\n") TEXT("    success = False\n") TEXT("finally:\n") TEXT("    # Restore original stdout and stderr\n") TEXT("    sys.stdout = original_stdout\n") TEXT("    sys.stderr = original_stderr\n") TEXT("    output_file.close()\n") TEXT("    error_file.close()\n") TEXT("    # Write success status\n") TEXT("    with open('") + TempDir + TEXT("/status.txt', 'w') as f:\n") TEXT("        f.write('1' if success else '0')\n");

        if (FFileHelper::SaveStringToFile(WrapperCode, *WrapperFilePath))
        {
            // Execute the wrapper script
            FString Command = FString::Printf(TEXT("py \"%s\""), *WrapperFilePath);
            GEngine->Exec(nullptr, *Command);

            // Read the output, error, and status files
            FString OutputContent;
            FString ErrorContent;
            FString StatusContent;

            FFileHelper::LoadFileToString(OutputContent, *(TempDir / TEXT("output.txt")));
            FFileHelper::LoadFileToString(ErrorContent, *(TempDir / TEXT("error.txt")));
            FFileHelper::LoadFileToString(StatusContent, *(TempDir / TEXT("status.txt")));

            bSuccess = StatusContent.TrimStartAndEnd().Equals(TEXT("1"));

            // Combine output and error for the result
            Result = OutputContent;
            ErrorMessage = ErrorContent;

            // Clean up the temporary files
            PlatformFile.DeleteFile(*WrapperFilePath);
            PlatformFile.DeleteFile(*(TempDir / TEXT("output.txt")));
            PlatformFile.DeleteFile(*(TempDir / TEXT("error.txt")));
            PlatformFile.DeleteFile(*(TempDir / TEXT("status.txt")));
        }
        else
        {
            MCP_LOG_ERROR("Failed to create wrapper Python file at %s", *WrapperFilePath);
            return CreateErrorResponse(FString::Printf(TEXT("Failed to create wrapper Python file at %s"), *WrapperFilePath));
        }
    }

    // Create the response
    TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
    ResultObj->SetStringField("output", Result);

    if (bSuccess)
    {
        MCP_LOG_INFO("Python execution successful");
        return CreateSuccessResponse(ResultObj);
    }
    else
    {
        MCP_LOG_ERROR("Python execution failed: %s", *ErrorMessage);
        ResultObj->SetStringField("error", ErrorMessage);

        // We're returning a success response with error details rather than an error response
        // This allows the client to still access the output and error information
        TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
        Response->SetStringField("status", "error");
        Response->SetStringField("message", "Python execution failed with errors");
        Response->SetObjectField("result", ResultObj);
        return Response;
    }
}
```

--------------------------------------------------------------------------------
/Source/UnrealMCP/Private/MCPCommandHandlers_Blueprints.cpp:
--------------------------------------------------------------------------------

```cpp
#include "MCPCommandHandlers_Blueprints.h"
#include "MCPFileLogger.h"
#include "Editor.h"
#include "EngineUtils.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UObject/SavePackage.h"


//
// FMCPBlueprintUtils
//
TPair<UBlueprint*, bool> FMCPBlueprintUtils::CreateBlueprintAsset(
    const FString& PackagePath,
    const FString& BlueprintName,
    UClass* ParentClass)
{
    // Print debug information about the paths
    FString GameContentDir = FPaths::ProjectContentDir();
    FString PluginContentDir = FPaths::EnginePluginsDir() / TEXT("UnrealMCP") / TEXT("Content");
    
    // Create the full path for the blueprint
    FString FullPackagePath = FString::Printf(TEXT("%s/%s"), *PackagePath, *BlueprintName);
    
    // Get the file paths
    FString DirectoryPath = FPackageName::LongPackageNameToFilename(PackagePath, TEXT(""));
    FString PackageFileName = FPackageName::LongPackageNameToFilename(FullPackagePath, FPackageName::GetAssetPackageExtension());
    
    MCP_LOG_INFO("Creating blueprint asset:");
    MCP_LOG_INFO("  Package Path: %s", *PackagePath);
    MCP_LOG_INFO("  Blueprint Name: %s", *BlueprintName);
    MCP_LOG_INFO("  Full Package Path: %s", *FullPackagePath);
    MCP_LOG_INFO("  Directory Path: %s", *DirectoryPath);
    MCP_LOG_INFO("  Package File Name: %s", *PackageFileName);
    MCP_LOG_INFO("  Game Content Dir: %s", *GameContentDir);
    MCP_LOG_INFO("  Plugin Content Dir: %s", *PluginContentDir);
    
    // Additional logging for debugging
    FString AbsoluteGameDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
    FString AbsoluteContentDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());
    FString AbsolutePackagePath = FPaths::ConvertRelativePathToFull(PackageFileName);
    
    MCP_LOG_INFO("  Absolute Game Dir: %s", *AbsoluteGameDir);
    MCP_LOG_INFO("  Absolute Content Dir: %s", *AbsoluteContentDir);
    MCP_LOG_INFO("  Absolute Package Path: %s", *AbsolutePackagePath);
    
    // Ensure the directory exists
    IFileManager::Get().MakeDirectory(*DirectoryPath, true);
    
    // Verify directory was created
    if (IFileManager::Get().DirectoryExists(*DirectoryPath))
    {
        MCP_LOG_INFO("  Directory exists or was created successfully: %s", *DirectoryPath);
    }
    else
    {
        MCP_LOG_ERROR("  Failed to create directory: %s", *DirectoryPath);
    }

    // Check if a blueprint with this name already exists in the package
    UBlueprint* ExistingBlueprint = LoadObject<UBlueprint>(nullptr, *FullPackagePath);
    if (ExistingBlueprint)
    {
        MCP_LOG_WARNING("Blueprint already exists at path: %s", *FullPackagePath);
        return TPair<UBlueprint*, bool>(ExistingBlueprint, true);
    }

    // Create or load the package for the full path
    UPackage* Package = CreatePackage(*FullPackagePath);
    if (!Package)
    {
        MCP_LOG_ERROR("Failed to create package for blueprint");
        return TPair<UBlueprint*, bool>(nullptr, false);
    }

    Package->FullyLoad();

    // Create the Blueprint
    UBlueprint* NewBlueprint = nullptr;
    
    // Use a try-catch block to handle potential errors in CreateBlueprint
    try
    {
        NewBlueprint = FKismetEditorUtilities::CreateBlueprint(
            ParentClass,
            Package,
            FName(*BlueprintName),
            BPTYPE_Normal,
            UBlueprint::StaticClass(),
            UBlueprintGeneratedClass::StaticClass()
        );
    }
    catch (const std::exception& e)
    {
        MCP_LOG_ERROR("Exception while creating blueprint: %s", ANSI_TO_TCHAR(e.what()));
        return TPair<UBlueprint*, bool>(nullptr, false);
    }
    catch (...)
    {
        MCP_LOG_ERROR("Unknown exception while creating blueprint");
        return TPair<UBlueprint*, bool>(nullptr, false);
    }

    if (!NewBlueprint)
    {
        MCP_LOG_ERROR("Failed to create blueprint");
        return TPair<UBlueprint*, bool>(nullptr, false);
    }

    // Save the package
    Package->MarkPackageDirty();
    MCP_LOG_INFO("  Saving package to: %s", *PackageFileName);
    
    // Use the new SavePackage API
    FSavePackageArgs SaveArgs;
    SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
    SaveArgs.SaveFlags = SAVE_NoError;
    bool bSaveSuccess = UPackage::SavePackage(Package, NewBlueprint, *PackageFileName, SaveArgs);
    
    if (bSaveSuccess)
    {
        MCP_LOG_INFO("  Package saved successfully to: %s", *PackageFileName);
        
        // Check if the file actually exists
        if (IFileManager::Get().FileExists(*PackageFileName))
        {
            MCP_LOG_INFO("  File exists at: %s", *PackageFileName);
        }
        else
        {
            MCP_LOG_ERROR("  File does NOT exist at: %s", *PackageFileName);
        }
    }
    else
    {
        MCP_LOG_ERROR("  Failed to save package to: %s", *PackageFileName);
    }

    // Notify the asset registry
    FAssetRegistryModule::AssetCreated(NewBlueprint);

    return TPair<UBlueprint*, bool>(NewBlueprint, true);
}

TPair<UK2Node_Event*, bool> FMCPBlueprintUtils::AddEventNode(
    UBlueprint* Blueprint,
    const FString& EventName,
    UClass* ParentClass)
{
    if (!Blueprint)
    {
        return TPair<UK2Node_Event*, bool>(nullptr, false);
    }

    // Find or create the event graph
    UEdGraph* EventGraph = FBlueprintEditorUtils::FindEventGraph(Blueprint);
    if (!EventGraph)
    {
        EventGraph = FBlueprintEditorUtils::CreateNewGraph(
            Blueprint,
            FName("EventGraph"),
            UEdGraph::StaticClass(),
            UEdGraphSchema_K2::StaticClass()
        );
        Blueprint->UbergraphPages.Add(EventGraph);
    }

    // Create the custom event node
    UK2Node_Event* EventNode = NewObject<UK2Node_Event>(EventGraph);
    EventNode->EventReference.SetExternalMember(FName(*EventName), ParentClass);
    EventNode->bOverrideFunction = true;
    EventNode->AllocateDefaultPins();
    EventGraph->Nodes.Add(EventNode);

    return TPair<UK2Node_Event*, bool>(EventNode, true);
}

TPair<UK2Node_CallFunction*, bool> FMCPBlueprintUtils::AddPrintStringNode(
    UEdGraph* Graph,
    const FString& Message)
{
    if (!Graph)
    {
        return TPair<UK2Node_CallFunction*, bool>(nullptr, false);
    }

    // Create print string node
    UK2Node_CallFunction* PrintNode = NewObject<UK2Node_CallFunction>(Graph);
    PrintNode->FunctionReference.SetExternalMember(FName("PrintString"), UKismetSystemLibrary::StaticClass());
    PrintNode->AllocateDefaultPins();
    Graph->Nodes.Add(PrintNode);

    // Set the string input
    UEdGraphPin* StringPin = PrintNode->FindPinChecked(FName("InString"));
    StringPin->DefaultValue = Message;

    return TPair<UK2Node_CallFunction*, bool>(PrintNode, true);
}

//
// FMCPCreateBlueprintHandler
//
TSharedPtr<FJsonObject> FMCPCreateBlueprintHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
{
    MCP_LOG_INFO("Handling create_blueprint command");

    FString PackagePath;
    if (!Params->TryGetStringField(TEXT("package_path"), PackagePath))
    {
        MCP_LOG_WARNING("Missing 'package_path' field in create_blueprint command");
        return CreateErrorResponse("Missing 'package_path' field");
    }

    FString BlueprintName;
    if (!Params->TryGetStringField(TEXT("name"), BlueprintName))
    {
        MCP_LOG_WARNING("Missing 'name' field in create_blueprint command");
        return CreateErrorResponse("Missing 'name' field");
    }

    // Get optional properties
    const TSharedPtr<FJsonObject>* Properties = nullptr;
    Params->TryGetObjectField(TEXT("properties"), Properties);

    // Create the blueprint
    TPair<UBlueprint*, bool> Result = CreateBlueprint(PackagePath, BlueprintName, Properties ? *Properties : nullptr);

    if (Result.Value)
    {
        TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
        ResultObj->SetStringField("name", Result.Key->GetName());
        ResultObj->SetStringField("path", Result.Key->GetPathName());
        return CreateSuccessResponse(ResultObj);
    }
    else
    {
        return CreateErrorResponse("Failed to create blueprint");
    }
}

TPair<UBlueprint*, bool> FMCPCreateBlueprintHandler::CreateBlueprint(
    const FString& PackagePath,
    const FString& BlueprintName,
    const TSharedPtr<FJsonObject>& Properties)
{
    // Ensure the package path is correctly formatted
    // We need to create a proper directory structure
    FString DirectoryPath;
    FString AssetName;
    
    // Create a proper directory structure
    // For example, if PackagePath is "/Game/Blueprints" and BlueprintName is "TestBlueprint",
    // we want to create a directory at "/Game/Blueprints" and place "TestBlueprint" inside it
    DirectoryPath = PackagePath;
    AssetName = BlueprintName;
    
    // Create the full path for the blueprint
    FString FullPackagePath = FString::Printf(TEXT("%s/%s"), *DirectoryPath, *AssetName);
    MCP_LOG_INFO("Creating blueprint at path: %s", *FullPackagePath);
    
    // Check if a blueprint with this name already exists
    UBlueprint* ExistingBlueprint = LoadObject<UBlueprint>(nullptr, *FullPackagePath);
    if (ExistingBlueprint)
    {
        MCP_LOG_WARNING("Blueprint already exists at path: %s", *FullPackagePath);
        return TPair<UBlueprint*, bool>(ExistingBlueprint, true);
    }

    // Default to Actor as parent class
    UClass* ParentClass = AActor::StaticClass();

    // Check if a different parent class is specified
    if (Properties.IsValid())
    {
        FString ParentClassName;
        if (Properties->TryGetStringField(TEXT("parent_class"), ParentClassName))
        {
            // First try to find the class using its full path
            UClass* FoundClass = LoadObject<UClass>(nullptr, *ParentClassName);
            
            // If not found with direct path, try to find it in common class paths
            if (!FoundClass)
            {
                // Try with /Script/Engine path (for engine classes)
                FString EnginePath = FString::Printf(TEXT("/Script/Engine.%s"), *ParentClassName);
                FoundClass = LoadObject<UClass>(nullptr, *EnginePath);
                
                // If still not found, try with game's path
                if (!FoundClass)
                {
                    FString GamePath = FString::Printf(TEXT("/Script/%s.%s"), 
                        FApp::GetProjectName(), 
                        *ParentClassName);
                    FoundClass = LoadObject<UClass>(nullptr, *GamePath);
                }
            }

            if (FoundClass)
            {
                ParentClass = FoundClass;
            }
            else
            {
                MCP_LOG_WARNING("Could not find parent class '%s', using default Actor class", *ParentClassName);
            }
        }
    }

    // Create the blueprint directly in the specified directory
    return FMCPBlueprintUtils::CreateBlueprintAsset(DirectoryPath, AssetName, ParentClass);
}

//
// FMCPModifyBlueprintHandler
//
TSharedPtr<FJsonObject> FMCPModifyBlueprintHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
{
    MCP_LOG_INFO("Handling modify_blueprint command");

    FString BlueprintPath;
    if (!Params->TryGetStringField(TEXT("blueprint_path"), BlueprintPath))
    {
        MCP_LOG_WARNING("Missing 'blueprint_path' field in modify_blueprint command");
        return CreateErrorResponse("Missing 'blueprint_path' field");
    }

    UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *BlueprintPath);
    if (!Blueprint)
    {
        return CreateErrorResponse(FString::Printf(TEXT("Failed to load blueprint at path: %s"), *BlueprintPath));
    }

    // Get properties to modify
    const TSharedPtr<FJsonObject>* Properties = nullptr;
    if (!Params->TryGetObjectField(TEXT("properties"), Properties) || !Properties)
    {
        return CreateErrorResponse("Missing 'properties' field");
    }

    if (ModifyBlueprint(Blueprint, *Properties))
    {
        return CreateSuccessResponse();
    }
    else
    {
        return CreateErrorResponse("Failed to modify blueprint");
    }
}

bool FMCPModifyBlueprintHandler::ModifyBlueprint(UBlueprint* Blueprint, const TSharedPtr<FJsonObject>& Properties)
{
    if (!Blueprint || !Properties.IsValid())
    {
        return false;
    }

    bool bModified = false;

    // Handle blueprint description
    FString Description;
    if (Properties->TryGetStringField(TEXT("description"), Description))
    {
        Blueprint->BlueprintDescription = Description;
        bModified = true;
    }

    // Handle blueprint category
    FString Category;
    if (Properties->TryGetStringField(TEXT("category"), Category))
    {
        Blueprint->BlueprintCategory = Category;
        bModified = true;
    }

    // Handle parent class change
    FString ParentClassName;
    if (Properties->TryGetStringField(TEXT("parent_class"), ParentClassName))
    {
        // First try to find the class using its full path
        UClass* FoundClass = LoadObject<UClass>(nullptr, *ParentClassName);
        
        // If not found with direct path, try to find it in common class paths
        if (!FoundClass)
        {
            // Try with /Script/Engine path (for engine classes)
            FString EnginePath = FString::Printf(TEXT("/Script/Engine.%s"), *ParentClassName);
            FoundClass = LoadObject<UClass>(nullptr, *EnginePath);
            
            // If still not found, try with game's path
            if (!FoundClass)
            {
                FString GamePath = FString::Printf(TEXT("/Script/%s.%s"), 
                    FApp::GetProjectName(), 
                    *ParentClassName);
                FoundClass = LoadObject<UClass>(nullptr, *GamePath);
            }
        }

        if (FoundClass)
        {
            Blueprint->ParentClass = FoundClass;
            bModified = true;
        }
        else
        {
            MCP_LOG_WARNING("Could not find parent class '%s' for blueprint modification", *ParentClassName);
        }
    }

    // Handle additional categories to hide
    const TSharedPtr<FJsonObject>* Options = nullptr;
    if (Properties->TryGetObjectField(TEXT("options"), Options) && Options)
    {
        // Handle hide categories
        const TArray<TSharedPtr<FJsonValue>>* HideCategories = nullptr;
        if ((*Options)->TryGetArrayField(TEXT("hide_categories"), HideCategories) && HideCategories)
        {
            for (const TSharedPtr<FJsonValue>& Value : *HideCategories)
            {
                FString CategoryName;
                if (Value->TryGetString(CategoryName) && !CategoryName.IsEmpty())
                {
                    Blueprint->HideCategories.AddUnique(CategoryName);
                    bModified = true;
                }
            }
        }
        
        // Handle namespace
        FString Namespace;
        if ((*Options)->TryGetStringField(TEXT("namespace"), Namespace))
        {
            Blueprint->BlueprintNamespace = Namespace;
            bModified = true;
        }
        
        // Handle display name
        FString DisplayName;
        if ((*Options)->TryGetStringField(TEXT("display_name"), DisplayName))
        {
            Blueprint->BlueprintDisplayName = DisplayName;
            bModified = true;
        }
        
        // Handle compile mode
        FString CompileMode;
        if ((*Options)->TryGetStringField(TEXT("compile_mode"), CompileMode))
        {
            if (CompileMode.Equals(TEXT("Default"), ESearchCase::IgnoreCase))
            {
                Blueprint->CompileMode = EBlueprintCompileMode::Default;
                bModified = true;
            }
            else if (CompileMode.Equals(TEXT("Development"), ESearchCase::IgnoreCase))
            {
                Blueprint->CompileMode = EBlueprintCompileMode::Development;
                bModified = true;
            }
            else if (CompileMode.Equals(TEXT("FinalRelease"), ESearchCase::IgnoreCase))
            {
                Blueprint->CompileMode = EBlueprintCompileMode::FinalRelease;
                bModified = true;
            }
        }
        
        // Handle class options
        bool bGenerateAbstractClass = false;
        if ((*Options)->TryGetBoolField(TEXT("abstract_class"), bGenerateAbstractClass))
        {
            Blueprint->bGenerateAbstractClass = bGenerateAbstractClass;
            bModified = true;
        }
        
        bool bGenerateConstClass = false;
        if ((*Options)->TryGetBoolField(TEXT("const_class"), bGenerateConstClass))
        {
            Blueprint->bGenerateConstClass = bGenerateConstClass;
            bModified = true;
        }
        
        bool bDeprecate = false;
        if ((*Options)->TryGetBoolField(TEXT("deprecate"), bDeprecate))
        {
            Blueprint->bDeprecate = bDeprecate;
            bModified = true;
        }
    }

    if (bModified)
    {
        // Mark the package as dirty
        Blueprint->MarkPackageDirty();
        
        // Recompile the blueprint if it was modified
        FKismetEditorUtilities::CompileBlueprint(Blueprint);
        
        // Save the package
        UPackage* Package = Blueprint->GetOutermost();
        if (Package)
        {
            FString PackagePath = Package->GetName();
            FString SavePackageFileName = FPackageName::LongPackageNameToFilename(
                PackagePath, 
                FPackageName::GetAssetPackageExtension()
            );
            
            // Use the new SavePackage API
            FSavePackageArgs SaveArgs;
            SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
            SaveArgs.SaveFlags = SAVE_NoError;
            UPackage::SavePackage(Package, Blueprint, *SavePackageFileName, SaveArgs);
        }
    }

    return bModified;
}

//
// FMCPGetBlueprintInfoHandler
//
TSharedPtr<FJsonObject> FMCPGetBlueprintInfoHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
{
    MCP_LOG_INFO("Handling get_blueprint_info command");

    FString BlueprintPath;
    if (!Params->TryGetStringField(TEXT("blueprint_path"), BlueprintPath))
    {
        MCP_LOG_WARNING("Missing 'blueprint_path' field in get_blueprint_info command");
        return CreateErrorResponse("Missing 'blueprint_path' field");
    }

    UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *BlueprintPath);
    if (!Blueprint)
    {
        return CreateErrorResponse(FString::Printf(TEXT("Failed to load blueprint at path: %s"), *BlueprintPath));
    }

    return CreateSuccessResponse(GetBlueprintInfo(Blueprint));
}

TSharedPtr<FJsonObject> FMCPGetBlueprintInfoHandler::GetBlueprintInfo(UBlueprint* Blueprint)
{
    TSharedPtr<FJsonObject> Info = MakeShared<FJsonObject>();
    if (!Blueprint)
    {
        return Info;
    }

    Info->SetStringField("name", Blueprint->GetName());
    Info->SetStringField("path", Blueprint->GetPathName());
    Info->SetStringField("parent_class", Blueprint->ParentClass ? Blueprint->ParentClass->GetName() : TEXT("None"));
    
    // Add blueprint-specific properties
    Info->SetStringField("category", Blueprint->BlueprintCategory);
    Info->SetStringField("description", Blueprint->BlueprintDescription);
    Info->SetStringField("display_name", Blueprint->BlueprintDisplayName);
    Info->SetStringField("namespace", Blueprint->BlueprintNamespace);
    
    // Add blueprint type
    FString BlueprintTypeStr;
    switch (Blueprint->BlueprintType)
    {
    case BPTYPE_Normal:
        BlueprintTypeStr = TEXT("Normal");
        break;
    case BPTYPE_Const:
        BlueprintTypeStr = TEXT("Const");
        break;
    case BPTYPE_MacroLibrary:
        BlueprintTypeStr = TEXT("MacroLibrary");
        break;
    case BPTYPE_Interface:
        BlueprintTypeStr = TEXT("Interface");
        break;
    case BPTYPE_LevelScript:
        BlueprintTypeStr = TEXT("LevelScript");
        break;
    case BPTYPE_FunctionLibrary:
        BlueprintTypeStr = TEXT("FunctionLibrary");
        break;
    default:
        BlueprintTypeStr = TEXT("Unknown");
        break;
    }
    Info->SetStringField("blueprint_type", BlueprintTypeStr);
    
    // Add class options
    TSharedPtr<FJsonObject> ClassOptions = MakeShared<FJsonObject>();
    ClassOptions->SetBoolField("abstract_class", Blueprint->bGenerateAbstractClass);
    ClassOptions->SetBoolField("const_class", Blueprint->bGenerateConstClass);
    ClassOptions->SetBoolField("deprecated", Blueprint->bDeprecate);
    
    // Add compile mode
    FString CompileModeStr;
    switch (Blueprint->CompileMode)
    {
    case EBlueprintCompileMode::Default:
        CompileModeStr = TEXT("Default");
        break;
    case EBlueprintCompileMode::Development:
        CompileModeStr = TEXT("Development");
        break;
    case EBlueprintCompileMode::FinalRelease:
        CompileModeStr = TEXT("FinalRelease");
        break;
    default:
        CompileModeStr = TEXT("Unknown");
        break;
    }
    ClassOptions->SetStringField("compile_mode", CompileModeStr);
    
    // Add hide categories
    TArray<TSharedPtr<FJsonValue>> HideCategories;
    for (const FString& Category : Blueprint->HideCategories)
    {
        HideCategories.Add(MakeShared<FJsonValueString>(Category));
    }
    ClassOptions->SetArrayField("hide_categories", HideCategories);
    
    Info->SetObjectField("class_options", ClassOptions);

    // Add information about functions
    TArray<TSharedPtr<FJsonValue>> Functions;
    for (UEdGraph* FuncGraph : Blueprint->FunctionGraphs)
    {
        TSharedPtr<FJsonObject> FuncInfo = MakeShared<FJsonObject>();
        FuncInfo->SetStringField("name", FuncGraph->GetName());
        Functions.Add(MakeShared<FJsonValueObject>(FuncInfo));
    }
    Info->SetArrayField("functions", Functions);

    // Add information about events
    TArray<TSharedPtr<FJsonValue>> Events;
    UEdGraph* EventGraph = FBlueprintEditorUtils::FindEventGraph(Blueprint);
    if (EventGraph)
    {
        for (UEdGraphNode* Node : EventGraph->Nodes)
        {
            if (UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node))
            {
                TSharedPtr<FJsonObject> EventInfo = MakeShared<FJsonObject>();
                EventInfo->SetStringField("name", EventNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
                Events.Add(MakeShared<FJsonValueObject>(EventInfo));
            }
        }
    }
    Info->SetArrayField("events", Events);

    return Info;
}

//
// FMCPCreateBlueprintEventHandler
//
TSharedPtr<FJsonObject> FMCPCreateBlueprintEventHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
{
    UWorld* World = GEditor->GetEditorWorldContext().World();
    if (!World)
    {
        return CreateErrorResponse("Invalid World context");
    }

    // Get event name
    FString EventName;
    if (!Params->TryGetStringField(TEXT("event_name"), EventName))
    {
        return CreateErrorResponse("Missing 'event_name' field");
    }

    // Get blueprint path
    FString BlueprintPath;
    if (!Params->TryGetStringField(TEXT("blueprint_path"), BlueprintPath))
    {
        // If no blueprint path is provided, create a new blueprint
        BlueprintPath = FString::Printf(TEXT("/Game/GeneratedBlueprints/BP_MCP_%s"), *EventName);
    }

    // Get optional event parameters
    const TSharedPtr<FJsonObject>* EventParamsPtr = nullptr;
    Params->TryGetObjectField(TEXT("parameters"), EventParamsPtr);
    TSharedPtr<FJsonObject> EventParams = EventParamsPtr ? *EventParamsPtr : nullptr;

    // Create the blueprint event
    TPair<bool, TSharedPtr<FJsonObject>> Result = CreateBlueprintEvent(World, EventName, BlueprintPath, EventParams);
    
    if (Result.Key)
    {
        return CreateSuccessResponse(Result.Value);
    }
    else
    {
        return CreateErrorResponse("Failed to create blueprint event");
    }
}

TPair<bool, TSharedPtr<FJsonObject>> FMCPCreateBlueprintEventHandler::CreateBlueprintEvent(
    UWorld* World,
    const FString& EventName,
    const FString& BlueprintPath,
    const TSharedPtr<FJsonObject>& EventParameters)
{
    TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();

    // Try to load existing blueprint or create new one
    UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *BlueprintPath);
    if (!Blueprint)
    {
        // Create new blueprint
        FString PackagePath = FPackageName::GetLongPackagePath(BlueprintPath);
        FString BlueprintName = FPackageName::GetShortName(BlueprintPath);
        
        TPair<UBlueprint*, bool> BlueprintResult = FMCPBlueprintUtils::CreateBlueprintAsset(PackagePath, BlueprintName, AActor::StaticClass());
        if (!BlueprintResult.Value || !BlueprintResult.Key)
        {
            MCP_LOG_ERROR("Failed to create blueprint asset");
            return TPair<bool, TSharedPtr<FJsonObject>>(false, nullptr);
        }
        Blueprint = BlueprintResult.Key;
    }

    // Add the event node
    TPair<UK2Node_Event*, bool> EventNodeResult = FMCPBlueprintUtils::AddEventNode(Blueprint, EventName, AActor::StaticClass());
    if (!EventNodeResult.Value || !EventNodeResult.Key)
    {
        MCP_LOG_ERROR("Failed to add event node");
        return TPair<bool, TSharedPtr<FJsonObject>>(false, nullptr);
    }

    // Add a print string node for testing
    UEdGraph* EventGraph = FBlueprintEditorUtils::FindEventGraph(Blueprint);
    if (EventGraph)
    {
        TPair<UK2Node_CallFunction*, bool> PrintNodeResult = FMCPBlueprintUtils::AddPrintStringNode(
            EventGraph,
            FString::Printf(TEXT("Event '%s' triggered!"), *EventName)
        );

        if (PrintNodeResult.Value && PrintNodeResult.Key)
        {
            // Connect the event to the print node
            UEdGraphPin* EventThenPin = EventNodeResult.Key->FindPinChecked(UEdGraphSchema_K2::PN_Then);
            UEdGraphPin* PrintExecPin = PrintNodeResult.Key->FindPinChecked(UEdGraphSchema_K2::PN_Execute);
            EventGraph->GetSchema()->TryCreateConnection(EventThenPin, PrintExecPin);
        }
    }

    // Compile and save the blueprint
    FKismetEditorUtilities::CompileBlueprint(Blueprint);
    
    Result->SetStringField("blueprint", Blueprint->GetName());
    Result->SetStringField("event", EventName);
    Result->SetStringField("path", Blueprint->GetPathName());
    
    return TPair<bool, TSharedPtr<FJsonObject>>(true, Result);
} 
```
Page 2/2FirstPrevNextLast