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);
}
```