#
tokens: 35714/50000 6/49 files (page 2/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 2. Use http://codebase.md/alexkissijr/unrealmcp?lines=true&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

--------------------------------------------------------------------------------
/MCP/check_mcp_setup.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Check MCP Setup Script
  3 | 
  4 | This script verifies the MCP setup for Unreal Engine integration with Cursor and Claude Desktop.
  5 | It checks for necessary components and configurations and provides diagnostic information.
  6 | """
  7 | 
  8 | import os
  9 | import sys
 10 | import json
 11 | import importlib
 12 | import platform
 13 | import subprocess
 14 | 
 15 | def check_mark():
 16 |     """Return a green check mark for success."""
 17 |     return "\033[92m✓\033[0m" if os.name != 'nt' else "\033[92mOK\033[0m"
 18 | 
 19 | def x_mark():
 20 |     """Return a red X mark for failure."""
 21 |     return "\033[91m✗\033[0m" if os.name != 'nt' else "\033[91mFAIL\033[0m"
 22 | 
 23 | def info_mark():
 24 |     """Return a blue info mark."""
 25 |     return "\033[94mi\033[0m" if os.name != 'nt' else "\033[94mINFO\033[0m"
 26 | 
 27 | def print_status(message, success=None):
 28 |     """Print a status message with appropriate formatting."""
 29 |     if success is None:
 30 |         print(f" {info_mark()} {message}")
 31 |     elif success:
 32 |         print(f" {check_mark()} {message}")
 33 |     else:
 34 |         print(f" {x_mark()} {message}")
 35 | 
 36 | def check_python():
 37 |     """Check Python installation."""
 38 |     print("\n=== Python Environment ===")
 39 |     print_status(f"Python version: {platform.python_version()}", True)
 40 |     
 41 |     # Check virtualenv
 42 |     try:
 43 |         import virtualenv
 44 |         print_status(f"virtualenv is installed (version: {virtualenv.__version__})", True)
 45 |     except ImportError:
 46 |         print_status("virtualenv is not installed", False)
 47 |     
 48 |     # Check MCP package
 49 |     try:
 50 |         import mcp
 51 |         version = getattr(mcp, "__version__", "unknown")
 52 |         print_status(f"MCP package is installed (version: {version})", True)
 53 |     except ImportError:
 54 |         print_status("MCP package is not installed", False)
 55 |     
 56 |     # Check if python_env exists
 57 |     script_dir = os.path.dirname(os.path.abspath(__file__))
 58 |     env_dir = os.path.join(script_dir, "python_env")
 59 |     if os.path.exists(env_dir):
 60 |         print_status(f"Python virtual environment exists at: {env_dir}", True)
 61 |     else:
 62 |         print_status(f"Python virtual environment not found at: {env_dir}", False)
 63 |     
 64 |     # Check if run_unreal_mcp.bat exists
 65 |     run_script = os.path.join(script_dir, "run_unreal_mcp.bat")
 66 |     if os.path.exists(run_script):
 67 |         print_status(f"Run script exists at: {run_script}", True)
 68 |     else:
 69 |         print_status(f"Run script not found at: {run_script}", False)
 70 | 
 71 | def check_claude_setup():
 72 |     """Check Claude Desktop setup."""
 73 |     print("\n=== Claude Desktop Setup ===")
 74 |     
 75 |     # Check if Claude Desktop is installed
 76 |     claude_installed = False
 77 |     if os.name == 'nt':  # Windows
 78 |         claude_paths = [
 79 |             os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Claude Desktop', 'Claude Desktop.exe'),
 80 |             os.path.join(os.environ.get('PROGRAMFILES', ''), 'Claude Desktop', 'Claude Desktop.exe'),
 81 |             os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'Claude Desktop', 'Claude Desktop.exe')
 82 |         ]
 83 |         for path in claude_paths:
 84 |             if os.path.exists(path):
 85 |                 print_status(f"Claude Desktop is installed at: {path}", True)
 86 |                 claude_installed = True
 87 |                 break
 88 |     elif os.name == 'darwin':  # macOS
 89 |         claude_paths = [
 90 |             '/Applications/Claude Desktop.app',
 91 |             os.path.expanduser('~/Applications/Claude Desktop.app')
 92 |         ]
 93 |         for path in claude_paths:
 94 |             if os.path.exists(path):
 95 |                 print_status(f"Claude Desktop is installed at: {path}", True)
 96 |                 claude_installed = True
 97 |                 break
 98 |     
 99 |     if not claude_installed:
100 |         print_status("Claude Desktop installation not found", False)
101 |     
102 |     # Check Claude config
103 |     config_file = None
104 |     if os.name == 'nt':  # Windows
105 |         config_file = os.path.join(os.environ.get('APPDATA', ''), 'Claude', 'claude_desktop_config.json')
106 |     elif os.name == 'darwin':  # macOS
107 |         config_file = os.path.expanduser('~/Library/Application Support/Claude/claude_desktop_config.json')
108 |     
109 |     if config_file and os.path.exists(config_file):
110 |         print_status(f"Claude Desktop configuration file exists at: {config_file}", True)
111 |         try:
112 |             with open(config_file, 'r') as f:
113 |                 config = json.load(f)
114 |             if 'mcpServers' in config and 'unreal' in config['mcpServers']:
115 |                 cmd = config['mcpServers']['unreal'].get('command', '')
116 |                 print_status(f"Unreal MCP configuration found with command: {cmd}", True)
117 |                 if not os.path.exists(cmd):
118 |                     print_status(f"Warning: The configured command path does not exist: {cmd}", False)
119 |             else:
120 |                 print_status("Unreal MCP configuration not found in Claude Desktop config", False)
121 |         except (json.JSONDecodeError, FileNotFoundError) as e:
122 |             print_status(f"Error reading Claude Desktop config: {str(e)}", False)
123 |     else:
124 |         print_status(f"Claude Desktop configuration file not found at: {config_file}", False)
125 |     
126 |     # Check Claude logs
127 |     log_file = None
128 |     if os.name == 'nt':  # Windows
129 |         log_file = os.path.join(os.environ.get('APPDATA', ''), 'Claude', 'logs', 'mcp-server-unreal.log')
130 |     elif os.name == 'darwin':  # macOS
131 |         log_file = os.path.expanduser('~/Library/Application Support/Claude/logs/mcp-server-unreal.log')
132 |     
133 |     if log_file and os.path.exists(log_file):
134 |         print_status(f"Claude Desktop MCP log file exists at: {log_file}", True)
135 |         # Optionally show last few lines of log
136 |         try:
137 |             with open(log_file, 'r') as f:
138 |                 lines = f.readlines()
139 |                 if lines:
140 |                     print("\n   Last log entry:")
141 |                     print(f"   {lines[-1].strip()}")
142 |         except Exception:
143 |             pass
144 |     else:
145 |         print_status(f"Claude Desktop MCP log file not found at: {log_file}", None)
146 |         print_status("This is normal if you haven't run the MCP server with Claude Desktop yet", None)
147 | 
148 | def check_cursor_setup():
149 |     """Check Cursor setup."""
150 |     print("\n=== Cursor Setup ===")
151 |     
152 |     # Check if Cursor is installed
153 |     cursor_installed = False
154 |     if os.name == 'nt':  # Windows
155 |         cursor_paths = [
156 |             os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Cursor', 'Cursor.exe'),
157 |             os.path.join(os.environ.get('PROGRAMFILES', ''), 'Cursor', 'Cursor.exe'),
158 |             os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'Cursor', 'Cursor.exe')
159 |         ]
160 |         for path in cursor_paths:
161 |             if os.path.exists(path):
162 |                 print_status(f"Cursor is installed at: {path}", True)
163 |                 cursor_installed = True
164 |                 break
165 |     elif os.name == 'darwin':  # macOS
166 |         cursor_paths = [
167 |             '/Applications/Cursor.app',
168 |             os.path.expanduser('~/Applications/Cursor.app')
169 |         ]
170 |         for path in cursor_paths:
171 |             if os.path.exists(path):
172 |                 print_status(f"Cursor is installed at: {path}", True)
173 |                 cursor_installed = True
174 |                 break
175 |     elif os.name == 'posix':  # Linux
176 |         cursor_paths = [
177 |             '/usr/bin/cursor',
178 |             '/usr/local/bin/cursor',
179 |             os.path.expanduser('~/.local/bin/cursor')
180 |         ]
181 |         for path in cursor_paths:
182 |             if os.path.exists(path):
183 |                 print_status(f"Cursor is installed at: {path}", True)
184 |                 cursor_installed = True
185 |                 break
186 |     
187 |     if not cursor_installed:
188 |         print_status("Cursor installation not found", False)
189 |     
190 |     # Check Cursor config
191 |     config_file = None
192 |     if os.name == 'nt':  # Windows
193 |         config_file = os.path.join(os.environ.get('APPDATA', ''), 'Cursor', 'User', 'settings.json')
194 |     elif os.name == 'darwin':  # macOS
195 |         config_file = os.path.expanduser('~/Library/Application Support/Cursor/User/settings.json')
196 |     elif os.name == 'posix':  # Linux
197 |         config_file = os.path.expanduser('~/.config/Cursor/User/settings.json')
198 |     
199 |     if config_file and os.path.exists(config_file):
200 |         print_status(f"Cursor configuration file exists at: {config_file}", True)
201 |         try:
202 |             with open(config_file, 'r') as f:
203 |                 config = json.load(f)
204 |             mcp_enabled = config.get('mcp', {}).get('enabled', False)
205 |             print_status(f"MCP enabled in Cursor config: {mcp_enabled}", mcp_enabled)
206 |             
207 |             servers = config.get('mcp', {}).get('servers', {})
208 |             if 'unreal' in servers:
209 |                 cmd = servers['unreal'].get('command', '')
210 |                 print_status(f"Unreal MCP configuration found with command: {cmd}", True)
211 |                 if not os.path.exists(cmd):
212 |                     print_status(f"Warning: The configured command path does not exist: {cmd}", False)
213 |             else:
214 |                 print_status("Unreal MCP configuration not found in Cursor config", False)
215 |         except (json.JSONDecodeError, FileNotFoundError) as e:
216 |             print_status(f"Error reading Cursor config: {str(e)}", False)
217 |     else:
218 |         print_status(f"Cursor configuration file not found at: {config_file}", False)
219 | 
220 | def check_unreal_plugin():
221 |     """Check Unreal Engine plugin setup."""
222 |     print("\n=== Unreal Engine Plugin ===")
223 |     
224 |     # Get plugin directory
225 |     script_dir = os.path.dirname(os.path.abspath(__file__))
226 |     plugin_dir = os.path.abspath(os.path.join(script_dir, ".."))
227 |     
228 |     # Check for plugin file
229 |     plugin_file = os.path.join(plugin_dir, "UnrealMCP.uplugin")
230 |     if os.path.exists(plugin_file):
231 |         print_status(f"UnrealMCP plugin file exists at: {plugin_file}", True)
232 |     else:
233 |         print_status(f"UnrealMCP plugin file not found at: {plugin_file}", False)
234 |     
235 |     # Check for Source directory
236 |     source_dir = os.path.join(plugin_dir, "Source")
237 |     if os.path.exists(source_dir) and os.path.isdir(source_dir):
238 |         print_status(f"Plugin Source directory exists at: {source_dir}", True)
239 |     else:
240 |         print_status(f"Plugin Source directory not found at: {source_dir}", False)
241 |     
242 |     # Check if the plugin can be loaded by Unreal
243 |     print_status("Note: To check if the plugin is loaded in Unreal Engine:", None)
244 |     print_status("1. Open your Unreal project", None)
245 |     print_status("2. Go to Edit > Plugins", None)
246 |     print_status("3. Search for 'UnrealMCP' and ensure it's enabled", None)
247 | 
248 | def main():
249 |     """Main function."""
250 |     print("\n========================================================")
251 |     print("           Unreal MCP Setup Diagnosis Tool              ")
252 |     print("========================================================")
253 |     
254 |     check_python()
255 |     check_claude_setup()
256 |     check_cursor_setup()
257 |     check_unreal_plugin()
258 |     
259 |     print("\n========================================================")
260 |     print("                    Diagnosis Complete                  ")
261 |     print("========================================================")
262 |     print("\nIf you encountered any issues, please try running:")
263 |     print("1. setup_unreal_mcp.bat - To set up the Python environment")
264 |     print("2. setup_cursor_mcp.bat - For Cursor integration")
265 |     print("\nFor more help, see the README.md or open an issue on GitHub.")
266 | 
267 | if __name__ == "__main__":
268 |     main() 
```

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

```cpp
  1 | // Copyright Epic Games, Inc. All Rights Reserved.
  2 | 
  3 | #include "UnrealMCP.h"
  4 | #include "MCPTCPServer.h"
  5 | #include "MCPSettings.h"
  6 | #include "MCPConstants.h"
  7 | #include "LevelEditor.h"
  8 | #include "Framework/MultiBox/MultiBoxBuilder.h"
  9 | #include "Styling/SlateStyleRegistry.h"
 10 | #include "Interfaces/IPluginManager.h"
 11 | #include "Styling/SlateStyle.h"
 12 | #include "Styling/SlateStyleMacros.h"
 13 | #include "ISettingsModule.h"
 14 | #include "ToolMenus.h"
 15 | #include "ToolMenuSection.h"
 16 | #include "MCPFileLogger.h"
 17 | #include "Widgets/SWindow.h"
 18 | #include "Widgets/Layout/SBox.h"
 19 | #include "Widgets/Layout/SBorder.h"
 20 | #include "Widgets/Layout/SScrollBox.h"
 21 | #include "Widgets/Text/STextBlock.h"
 22 | #include "Widgets/Input/SButton.h"
 23 | #include "Widgets/Layout/SGridPanel.h"
 24 | #include "Widgets/Layout/SUniformGridPanel.h"
 25 | #include "Framework/Application/SlateApplication.h"
 26 | #include "EditorStyleSet.h"
 27 | 
 28 | // Define the log category
 29 | DEFINE_LOG_CATEGORY(LogMCP);
 30 | 
 31 | #define LOCTEXT_NAMESPACE "FUnrealMCPModule"
 32 | 
 33 | // Define a style set for our plugin
 34 | class FMCPPluginStyle : public FSlateStyleSet
 35 | {
 36 | public:
 37 | 	FMCPPluginStyle() : FSlateStyleSet("MCPPluginStyle")
 38 | 	{
 39 | 		const FVector2D Icon16x16(16.0f, 16.0f);
 40 | 		const FVector2D StatusSize(6.0f, 6.0f);
 41 | 
 42 | 		// Use path constants instead of finding the plugin each time
 43 | 		SetContentRoot(MCPConstants::PluginResourcesPath);
 44 | 
 45 | 		// Register icon
 46 | 		FSlateImageBrush* MCPIconBrush = new FSlateImageBrush(
 47 | 			RootToContentDir(TEXT("Icon128.png")), 
 48 | 			Icon16x16,
 49 | 			FLinearColor::White,  // Tint (white preserves original colors)
 50 | 			ESlateBrushTileType::NoTile  // Ensure no tiling, just the image
 51 | 		);
 52 | 		Set("MCPPlugin.ServerIcon", MCPIconBrush);
 53 | 
 54 | 		// Create status indicator brushes
 55 | 		const FLinearColor RunningColor(0.0f, 0.8f, 0.0f);  // Green
 56 | 		const FLinearColor StoppedColor(0.8f, 0.0f, 0.0f);  // Red
 57 | 		
 58 | 		Set("MCPPlugin.StatusRunning", new FSlateRoundedBoxBrush(RunningColor, 3.0f, FVector2f(StatusSize)));
 59 | 		Set("MCPPlugin.StatusStopped", new FSlateRoundedBoxBrush(StoppedColor, 3.0f, FVector2f(StatusSize)));
 60 | 
 61 | 		// Define a custom button style with hover feedback
 62 | 		FButtonStyle ToolbarButtonStyle = FAppStyle::Get().GetWidgetStyle<FButtonStyle>("LevelEditor.ToolBar.Button");
 63 |         
 64 | 		// Normal state: fully transparent background
 65 | 		ToolbarButtonStyle.SetNormal(FSlateColorBrush(FLinearColor(0, 0, 0, 0))); // Transparent
 66 |         
 67 | 		// Hovered state: subtle overlay (e.g., light gray with low opacity)
 68 | 		ToolbarButtonStyle.SetHovered(FSlateColorBrush(FLinearColor(0.2f, 0.2f, 0.2f, 0.3f))); // Semi-transparent gray
 69 |         
 70 | 		// Pressed state: slightly darker overlay
 71 | 		ToolbarButtonStyle.SetPressed(FSlateColorBrush(FLinearColor(0.1f, 0.1f, 0.1f, 0.5f))); // Darker semi-transparent gray
 72 |         
 73 | 		// Register the custom style
 74 | 		Set("MCPPlugin.TransparentToolbarButton", ToolbarButtonStyle);
 75 | 	}
 76 | 
 77 | 	static void Initialize()
 78 | 	{
 79 | 		if (!Instance.IsValid())
 80 | 		{
 81 | 			Instance = MakeShareable(new FMCPPluginStyle());
 82 | 		}
 83 | 	}
 84 | 
 85 | 	static void Shutdown()
 86 | 	{
 87 | 		if (Instance.IsValid())
 88 | 		{
 89 | 			FSlateStyleRegistry::UnRegisterSlateStyle(*Instance);
 90 | 			Instance.Reset();
 91 | 		}
 92 | 	}
 93 | 
 94 | 	static TSharedPtr<FMCPPluginStyle> Get()
 95 | 	{
 96 | 		return Instance;
 97 | 	}
 98 | 
 99 | private:
100 | 	static TSharedPtr<FMCPPluginStyle> Instance;
101 | };
102 | 
103 | TSharedPtr<FMCPPluginStyle> FMCPPluginStyle::Instance = nullptr;
104 | 
105 | void FUnrealMCPModule::StartupModule()
106 | {
107 | 	// Initialize path constants first
108 | 	MCPConstants::InitializePathConstants();
109 | 	
110 | 	// Initialize our custom log category
111 | 	MCP_LOG_INFO("UnrealMCP Plugin is starting up");
112 | 	
113 | 	// Initialize file logger - now using path constants
114 | 	FString LogFilePath = FPaths::Combine(MCPConstants::PluginLogsPath, TEXT("MCPServer.log"));
115 | 	FMCPFileLogger::Get().Initialize(LogFilePath);
116 | 	
117 | 	// Register style set
118 | 	FMCPPluginStyle::Initialize();
119 | 	FSlateStyleRegistry::RegisterSlateStyle(*FMCPPluginStyle::Get());
120 | 	
121 | 	// More debug logging
122 | 	MCP_LOG_INFO("UnrealMCP Style registered");
123 | 
124 | 	// Register settings
125 | 	if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
126 | 	{
127 | 		SettingsModule->RegisterSettings("Editor", "Plugins", "MCP Settings",
128 | 			LOCTEXT("MCPSettingsName", "MCP Settings"),
129 | 			LOCTEXT("MCPSettingsDescription", "Configure the MCP plugin settings"),
130 | 			GetMutableDefault<UMCPSettings>()
131 | 		);
132 | 	}
133 | 
134 | 	// Register for post engine init to add toolbar button
135 | 	// First, make sure we're not already registered
136 | 	FCoreDelegates::OnPostEngineInit.RemoveAll(this);
137 | 	
138 | 	MCP_LOG_INFO("Registering OnPostEngineInit delegate");
139 | 	FCoreDelegates::OnPostEngineInit.AddRaw(this, &FUnrealMCPModule::ExtendLevelEditorToolbar);
140 | }
141 | 
142 | void FUnrealMCPModule::ShutdownModule()
143 | {
144 | 	// Unregister style set
145 | 	FMCPPluginStyle::Shutdown();
146 | 
147 | 	// Unregister settings
148 | 	if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
149 | 	{
150 | 		SettingsModule->UnregisterSettings("Editor", "Plugins", "MCP Settings");
151 | 	}
152 | 
153 | 	// Stop server if running
154 | 	if (Server)
155 | 	{
156 | 		StopServer();
157 | 	}
158 | 	
159 | 	// Close control panel if open
160 | 	CloseMCPControlPanel();
161 | 	
162 | 	// Clean up delegates
163 | 	FCoreDelegates::OnPostEngineInit.RemoveAll(this);
164 | }
165 | 
166 | void FUnrealMCPModule::ExtendLevelEditorToolbar()
167 | {
168 |     static bool bToolbarExtended = false;
169 |     
170 |     if (bToolbarExtended)
171 |     {
172 |         MCP_LOG_WARNING("ExtendLevelEditorToolbar called but toolbar already extended, skipping");
173 |         return;
174 |     }
175 |     
176 |     MCP_LOG_INFO("ExtendLevelEditorToolbar called - first time");
177 |     
178 |     UToolMenus::Get()->RegisterMenu("LevelEditor.MainMenu", "MainFrame.MainMenu");
179 |     
180 |     UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.User");
181 |     if (ToolbarMenu)
182 |     {
183 |         FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("MCP");
184 |         
185 |         // Add a custom widget instead of a static toolbar button
186 |         Section.AddEntry(FToolMenuEntry::InitWidget(
187 |             "MCPServerControl",
188 |             SNew(SButton)
189 |             .ButtonStyle(FMCPPluginStyle::Get().ToSharedRef(), "MCPPlugin.TransparentToolbarButton")
190 |             //.ButtonStyle(FAppStyle::Get(), "LevelEditor.ToolBar.Button") // Match toolbar style
191 |             .OnClicked(FOnClicked::CreateRaw(this, &FUnrealMCPModule::OpenMCPControlPanel_OnClicked))
192 |             .ToolTipText(LOCTEXT("MCPButtonTooltip", "Open MCP Server Control Panel"))
193 |             .Content()
194 |             [
195 |                 SNew(SOverlay)
196 |                 + SOverlay::Slot()
197 |                 [
198 |                     SNew(SImage)
199 |                     .Image(FMCPPluginStyle::Get()->GetBrush("MCPPlugin.ServerIcon"))
200 |                 	.ColorAndOpacity(FLinearColor::White)  // Ensure no tint overrides transparency
201 |                 ]
202 |                 + SOverlay::Slot()
203 |                 .HAlign(HAlign_Right)
204 |                 .VAlign(VAlign_Bottom)
205 |                 [
206 |                     SNew(SImage)
207 |                     .Image_Lambda([this]() -> const FSlateBrush* {
208 |                         return IsServerRunning() 
209 |                             ? FMCPPluginStyle::Get()->GetBrush("MCPPlugin.StatusRunning") 
210 |                             : FMCPPluginStyle::Get()->GetBrush("MCPPlugin.StatusStopped");
211 |                     })
212 |                 ]
213 |             ],
214 |             FText::GetEmpty(),  // No label needed since the icon is visual
215 |             true,   // bNoIndent
216 |             false,  // bSearchable
217 |             false
218 |         ));
219 |         
220 |         MCP_LOG_INFO("MCP Server button added to main toolbar with dynamic icon");
221 |     }
222 |     
223 |     // Window menu code remains unchanged
224 |     UToolMenu* WindowMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window");
225 |     if (WindowMenu)
226 |     {
227 |         FToolMenuSection& Section = WindowMenu->FindOrAddSection("WindowLayout");
228 |         Section.AddMenuEntry(
229 |             "MCPServerControlWindow",
230 |             LOCTEXT("MCPWindowMenuLabel", "MCP Server Control Panel"),
231 |             LOCTEXT("MCPWindowMenuTooltip", "Open MCP Server Control Panel"),
232 |             FSlateIcon(FMCPPluginStyle::Get()->GetStyleSetName(), "MCPPlugin.ServerIcon"),
233 |             FUIAction(
234 |                 FExecuteAction::CreateRaw(this, &FUnrealMCPModule::OpenMCPControlPanel),
235 |                 FCanExecuteAction()
236 |             )
237 |         );
238 |         MCP_LOG_INFO("MCP Server entry added to Window menu");
239 |     }
240 |     
241 |     bToolbarExtended = true;
242 | }
243 | 
244 | // Legacy toolbar extension method - no longer used
245 | void FUnrealMCPModule::AddToolbarButton(FToolBarBuilder& Builder)
246 | {
247 | 	Builder.AddToolBarButton(
248 | 		FUIAction(
249 | 			FExecuteAction::CreateRaw(this, &FUnrealMCPModule::OpenMCPControlPanel),
250 | 			FCanExecuteAction()
251 | 		),
252 | 		NAME_None,
253 | 		LOCTEXT("MCPButtonLabel", "MCP Server"),
254 | 		LOCTEXT("MCPButtonTooltip", "Open MCP Server Control Panel"),
255 | 		FSlateIcon(FMCPPluginStyle::Get()->GetStyleSetName(), "MCPPlugin.ServerIcon")
256 | 	);
257 | }
258 | 
259 | void FUnrealMCPModule::OpenMCPControlPanel()
260 | {
261 | 	// If the window already exists, just focus it
262 | 	if (MCPControlPanelWindow.IsValid())
263 | 	{
264 | 		MCPControlPanelWindow->BringToFront();
265 | 		return;
266 | 	}
267 | 
268 | 	// Create a new window
269 | 	MCPControlPanelWindow = SNew(SWindow)
270 | 		.Title(LOCTEXT("MCPControlPanelTitle", "MCP Server Control Panel"))
271 | 		.SizingRule(ESizingRule::Autosized)
272 | 		.SupportsMaximize(false)
273 | 		.SupportsMinimize(false)
274 | 		.HasCloseButton(true)
275 | 		.CreateTitleBar(true)
276 | 		.IsTopmostWindow(true)
277 | 		.MinWidth(300)
278 | 		.MinHeight(150);
279 | 
280 | 	// Set the content of the window
281 | 	MCPControlPanelWindow->SetContent(CreateMCPControlPanelContent());
282 | 
283 | 	// Register a callback for when the window is closed
284 | 	MCPControlPanelWindow->GetOnWindowClosedEvent().AddRaw(this, &FUnrealMCPModule::OnMCPControlPanelClosed);
285 | 
286 | 	// Show the window
287 | 	FSlateApplication::Get().AddWindow(MCPControlPanelWindow.ToSharedRef());
288 | 
289 | 	MCP_LOG_INFO("MCP Control Panel opened");
290 | }
291 | 
292 | FReply FUnrealMCPModule::OpenMCPControlPanel_OnClicked()
293 | {
294 | 	OpenMCPControlPanel();
295 | 
296 | 	return FReply::Handled();
297 | }
298 | 
299 | void FUnrealMCPModule::OnMCPControlPanelClosed(const TSharedRef<SWindow>& Window)
300 | {
301 | 	MCPControlPanelWindow.Reset();
302 | 	MCP_LOG_INFO("MCP Control Panel closed");
303 | }
304 | 
305 | void FUnrealMCPModule::CloseMCPControlPanel()
306 | {
307 | 	if (MCPControlPanelWindow.IsValid())
308 | 	{
309 | 		MCPControlPanelWindow->RequestDestroyWindow();
310 | 		MCPControlPanelWindow.Reset();
311 | 		MCP_LOG_INFO("MCP Control Panel closed");
312 | 	}
313 | }
314 | 
315 | TSharedRef<SWidget> FUnrealMCPModule::CreateMCPControlPanelContent()
316 | {
317 | 	const UMCPSettings* Settings = GetDefault<UMCPSettings>();
318 | 	
319 | 	return SNew(SBorder)
320 | 		.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
321 | 		.Padding(8.0f)
322 | 		[
323 | 			SNew(SVerticalBox)
324 | 			
325 | 			// Status section
326 | 			+ SVerticalBox::Slot()
327 | 			.AutoHeight()
328 | 			.Padding(0, 0, 0, 8)
329 | 			[
330 | 				SNew(SHorizontalBox)
331 | 				
332 | 				+ SHorizontalBox::Slot()
333 | 				.AutoWidth()
334 | 				.VAlign(VAlign_Center)
335 | 				.Padding(0, 0, 8, 0)
336 | 				[
337 | 					SNew(STextBlock)
338 | 					.Text(LOCTEXT("ServerStatusLabel", "Server Status:"))
339 | 					.Font(FAppStyle::GetFontStyle("NormalText"))
340 | 				]
341 | 				
342 | 				+ SHorizontalBox::Slot()
343 | 				.FillWidth(1.0f)
344 | 				.VAlign(VAlign_Center)
345 | 				[
346 | 					SNew(STextBlock)
347 | 					.Text_Lambda([this]() -> FText {
348 | 						return IsServerRunning() 
349 | 							? LOCTEXT("ServerRunningStatus", "Running") 
350 | 							: LOCTEXT("ServerStoppedStatus", "Stopped");
351 | 					})
352 | 					.ColorAndOpacity_Lambda([this]() -> FSlateColor {
353 | 						return IsServerRunning() 
354 | 							? FSlateColor(FLinearColor(0.0f, 0.8f, 0.0f)) 
355 | 							: FSlateColor(FLinearColor(0.8f, 0.0f, 0.0f));
356 | 					})
357 | 					.Font(FAppStyle::GetFontStyle("NormalText"))
358 | 				]
359 | 			]
360 | 			
361 | 			// Port information
362 | 			+ SVerticalBox::Slot()
363 | 			.AutoHeight()
364 | 			.Padding(0, 0, 0, 8)
365 | 			[
366 | 				SNew(SHorizontalBox)
367 | 				
368 | 				+ SHorizontalBox::Slot()
369 | 				.AutoWidth()
370 | 				.VAlign(VAlign_Center)
371 | 				.Padding(0, 0, 8, 0)
372 | 				[
373 | 					SNew(STextBlock)
374 | 					.Text(LOCTEXT("ServerPortLabel", "Port:"))
375 | 					.Font(FAppStyle::GetFontStyle("NormalText"))
376 | 				]
377 | 				
378 | 				+ SHorizontalBox::Slot()
379 | 				.FillWidth(1.0f)
380 | 				.VAlign(VAlign_Center)
381 | 				[
382 | 					SNew(STextBlock)
383 | 					.Text(FText::FromString(FString::FromInt(Settings->Port)))
384 | 					.Font(FAppStyle::GetFontStyle("NormalText"))
385 | 				]
386 | 			]
387 | 			
388 | 			// Buttons
389 | 			+ SVerticalBox::Slot()
390 | 			.AutoHeight()
391 | 			.Padding(0, 8, 0, 0)
392 | 			.HAlign(HAlign_Center)
393 | 			[
394 | 				SNew(SUniformGridPanel)
395 | 				.SlotPadding(FMargin(5.0f))
396 | 				.MinDesiredSlotWidth(100.0f)
397 | 				
398 | 				// Start button
399 | 				+ SUniformGridPanel::Slot(0, 0)
400 | 				[
401 | 					SNew(SButton)
402 | 					.HAlign(HAlign_Center)
403 | 					.VAlign(VAlign_Center)
404 | 					.Text(LOCTEXT("StartServerButton", "Start Server"))
405 | 					.IsEnabled_Lambda([this]() -> bool { return !IsServerRunning(); })
406 | 					.OnClicked(FOnClicked::CreateRaw(this, &FUnrealMCPModule::OnStartServerClicked))
407 | 				]
408 | 				
409 | 				// Stop button
410 | 				+ SUniformGridPanel::Slot(1, 0)
411 | 				[
412 | 					SNew(SButton)
413 | 					.HAlign(HAlign_Center)
414 | 					.VAlign(VAlign_Center)
415 | 					.Text(LOCTEXT("StopServerButton", "Stop Server"))
416 | 					.IsEnabled_Lambda([this]() -> bool { return IsServerRunning(); })
417 | 					.OnClicked(FOnClicked::CreateRaw(this, &FUnrealMCPModule::OnStopServerClicked))
418 | 				]
419 | 			]
420 | 			
421 | 			// Settings button
422 | 			+ SVerticalBox::Slot()
423 | 			.AutoHeight()
424 | 			.Padding(0, 16, 0, 0)
425 | 			.HAlign(HAlign_Center)
426 | 			[
427 | 				SNew(SButton)
428 | 				.HAlign(HAlign_Center)
429 | 				.VAlign(VAlign_Center)
430 | 				.Text(LOCTEXT("OpenSettingsButton", "Open Settings"))
431 | 				.OnClicked_Lambda([this]() -> FReply {
432 | 					if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
433 | 					{
434 | 						SettingsModule->ShowViewer("Editor", "Plugins", "MCP Settings");
435 | 					}
436 | 					return FReply::Handled();
437 | 				})
438 | 			]
439 | 		];
440 | }
441 | 
442 | FReply FUnrealMCPModule::OnStartServerClicked()
443 | {
444 | 	StartServer();
445 | 	return FReply::Handled();
446 | }
447 | 
448 | FReply FUnrealMCPModule::OnStopServerClicked()
449 | {
450 | 	StopServer();
451 | 	return FReply::Handled();
452 | }
453 | 
454 | void FUnrealMCPModule::ToggleServer()
455 | {
456 | 	MCP_LOG_WARNING("ToggleServer called - Server state: %s", (Server && Server->IsRunning()) ? TEXT("Running") : TEXT("Not Running"));
457 | 	
458 | 	if (Server && Server->IsRunning())
459 | 	{
460 | 		MCP_LOG_WARNING("Stopping server...");
461 | 		StopServer();
462 | 	}
463 | 	else
464 | 	{
465 | 		MCP_LOG_WARNING("Starting server...");
466 | 		StartServer();
467 | 	}
468 | 	
469 | 	MCP_LOG_WARNING("ToggleServer completed - Server state: %s", (Server && Server->IsRunning()) ? TEXT("Running") : TEXT("Not Running"));
470 | }
471 | 
472 | void FUnrealMCPModule::StartServer()
473 | {
474 | 	// Check if server is already running to prevent double-start
475 | 	if (Server && Server->IsRunning())
476 | 	{
477 | 		MCP_LOG_WARNING("Server is already running, ignoring start request");
478 | 		return;
479 | 	}
480 | 
481 | 	MCP_LOG_WARNING("Creating new server instance");
482 | 	const UMCPSettings* Settings = GetDefault<UMCPSettings>();
483 | 	
484 | 	// Create a config object and set the port from settings
485 | 	FMCPTCPServerConfig Config;
486 | 	Config.Port = Settings->Port;
487 | 	
488 | 	// Create the server with the config
489 | 	Server = MakeUnique<FMCPTCPServer>(Config);
490 | 	
491 | 	if (Server->Start())
492 | 	{
493 | 		// Refresh the toolbar to update the status indicator
494 | 		if (UToolMenus* ToolMenus = UToolMenus::Get())
495 | 		{
496 | 			ToolMenus->RefreshAllWidgets();
497 | 		}
498 | 	}
499 | 	else
500 | 	{
501 | 		MCP_LOG_ERROR("Failed to start MCP Server");
502 | 	}
503 | }
504 | 
505 | void FUnrealMCPModule::StopServer()
506 | {
507 | 	if (Server)
508 | 	{
509 | 		Server->Stop();
510 | 		Server.Reset();
511 | 		MCP_LOG_INFO("MCP Server stopped");
512 | 		
513 | 		// Refresh the toolbar to update the status indicator
514 | 		if (UToolMenus* ToolMenus = UToolMenus::Get())
515 | 		{
516 | 			ToolMenus->RefreshAllWidgets();
517 | 		}
518 | 	}
519 | }
520 | 
521 | bool FUnrealMCPModule::IsServerRunning() const
522 | {
523 | 	return Server && Server->IsRunning();
524 | }
525 | 
526 | #undef LOCTEXT_NAMESPACE
527 | 
528 | IMPLEMENT_MODULE(FUnrealMCPModule, UnrealMCP)
```

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

```cpp
  1 | #include "MCPTCPServer.h"
  2 | #include "Engine/World.h"
  3 | #include "Editor.h"
  4 | #include "LevelEditor.h"
  5 | #include "Engine/StaticMeshActor.h"
  6 | #include "Components/StaticMeshComponent.h"
  7 | #include "JsonObjectConverter.h"
  8 | #include "Sockets.h"
  9 | #include "SocketSubsystem.h"
 10 | #include "IPAddress.h"
 11 | #include "Interfaces/IPv4/IPv4Address.h"
 12 | #include "Interfaces/IPv4/IPv4Endpoint.h"
 13 | #include "ActorEditorUtils.h"
 14 | #include "EngineUtils.h"
 15 | #include "Containers/Ticker.h"
 16 | #include "UnrealMCP.h"
 17 | #include "MCPFileLogger.h"
 18 | #include "MCPCommandHandlers.h"
 19 | #include "MCPCommandHandlers_Blueprints.h"
 20 | #include "MCPCommandHandlers_Materials.h"
 21 | #include "HAL/PlatformFilemanager.h"
 22 | #include "Misc/FileHelper.h"
 23 | #include "Misc/Paths.h"
 24 | #include "Misc/Guid.h"
 25 | #include "MCPConstants.h"
 26 | 
 27 | 
 28 | FMCPTCPServer::FMCPTCPServer(const FMCPTCPServerConfig& InConfig) 
 29 |     : Config(InConfig)
 30 |     , Listener(nullptr)
 31 |     , bRunning(false)
 32 | {
 33 |     // Register default command handlers
 34 |     RegisterCommandHandler(MakeShared<FMCPGetSceneInfoHandler>());
 35 |     RegisterCommandHandler(MakeShared<FMCPCreateObjectHandler>());
 36 |     RegisterCommandHandler(MakeShared<FMCPModifyObjectHandler>());
 37 |     RegisterCommandHandler(MakeShared<FMCPDeleteObjectHandler>());
 38 |     RegisterCommandHandler(MakeShared<FMCPExecutePythonHandler>());
 39 | 
 40 |     // Material command handlers
 41 |     RegisterCommandHandler(MakeShared<FMCPCreateMaterialHandler>());
 42 |     RegisterCommandHandler(MakeShared<FMCPModifyMaterialHandler>());
 43 |     RegisterCommandHandler(MakeShared<FMCPGetMaterialInfoHandler>());
 44 | 
 45 |     // Blueprint command handlers
 46 |     RegisterCommandHandler(MakeShared<FMCPCreateBlueprintHandler>());
 47 |     RegisterCommandHandler(MakeShared<FMCPModifyBlueprintHandler>());
 48 |     RegisterCommandHandler(MakeShared<FMCPGetBlueprintInfoHandler>());
 49 |     RegisterCommandHandler(MakeShared<FMCPCreateBlueprintEventHandler>());
 50 | }
 51 | 
 52 | FMCPTCPServer::~FMCPTCPServer()
 53 | {
 54 |     Stop();
 55 | }
 56 | 
 57 | void FMCPTCPServer::RegisterCommandHandler(TSharedPtr<IMCPCommandHandler> Handler)
 58 | {
 59 |     if (!Handler.IsValid())
 60 |     {
 61 |         MCP_LOG_ERROR("Attempted to register null command handler");
 62 |         return;
 63 |     }
 64 | 
 65 |     FString CommandName = Handler->GetCommandName();
 66 |     if (CommandName.IsEmpty())
 67 |     {
 68 |         MCP_LOG_ERROR("Attempted to register command handler with empty command name");
 69 |         return;
 70 |     }
 71 | 
 72 |     CommandHandlers.Add(CommandName, Handler);
 73 |     MCP_LOG_INFO("Registered command handler for '%s'", *CommandName);
 74 | }
 75 | 
 76 | void FMCPTCPServer::UnregisterCommandHandler(const FString& CommandName)
 77 | {
 78 |     if (CommandHandlers.Remove(CommandName) > 0)
 79 |     {
 80 |         MCP_LOG_INFO("Unregistered command handler for '%s'", *CommandName);
 81 |     }
 82 |     else
 83 |     {
 84 |         MCP_LOG_WARNING("Attempted to unregister non-existent command handler for '%s'", *CommandName);
 85 |     }
 86 | }
 87 | 
 88 | bool FMCPTCPServer::RegisterExternalCommandHandler(TSharedPtr<IMCPCommandHandler> Handler)
 89 | {
 90 |     if (!Handler.IsValid())
 91 |     {
 92 |         MCP_LOG_ERROR("Attempted to register null external command handler");
 93 |         return false;
 94 |     }
 95 | 
 96 |     FString CommandName = Handler->GetCommandName();
 97 |     if (CommandName.IsEmpty())
 98 |     {
 99 |         MCP_LOG_ERROR("Attempted to register external command handler with empty command name");
100 |         return false;
101 |     }
102 | 
103 |     // Check if there's a conflict with an existing handler
104 |     if (CommandHandlers.Contains(CommandName))
105 |     {
106 |         MCP_LOG_WARNING("External command handler for '%s' conflicts with an existing handler", *CommandName);
107 |         return false;
108 |     }
109 | 
110 |     // Register the handler
111 |     CommandHandlers.Add(CommandName, Handler);
112 |     MCP_LOG_INFO("Registered external command handler for '%s'", *CommandName);
113 |     return true;
114 | }
115 | 
116 | bool FMCPTCPServer::UnregisterExternalCommandHandler(const FString& CommandName)
117 | {
118 |     if (CommandName.IsEmpty())
119 |     {
120 |         MCP_LOG_ERROR("Attempted to unregister external command handler with empty command name");
121 |         return false;
122 |     }
123 | 
124 |     // Check if the handler exists
125 |     if (!CommandHandlers.Contains(CommandName))
126 |     {
127 |         MCP_LOG_WARNING("Attempted to unregister non-existent external command handler for '%s'", *CommandName);
128 |         return false;
129 |     }
130 | 
131 |     // Unregister the handler
132 |     CommandHandlers.Remove(CommandName);
133 |     MCP_LOG_INFO("Unregistered external command handler for '%s'", *CommandName);
134 |     return true;
135 | }
136 | 
137 | bool FMCPTCPServer::Start()
138 | {
139 |     if (bRunning)
140 |     {
141 |         MCP_LOG_WARNING("Start called but server is already running, returning true");
142 |         return true;
143 |     }
144 |     
145 |     MCP_LOG_WARNING("Starting MCP server on port %d", Config.Port);
146 |     
147 |     // Use a simple ASCII string for the socket description to avoid encoding issues
148 |     Listener = new FTcpListener(FIPv4Endpoint(FIPv4Address::Any, Config.Port));
149 |     if (!Listener || !Listener->IsActive())
150 |     {
151 |         MCP_LOG_ERROR("Failed to start MCP server on port %d", Config.Port);
152 |         Stop();
153 |         return false;
154 |     }
155 | 
156 |     // Clear any existing client connections
157 |     ClientConnections.Empty();
158 | 
159 |     TickerHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateRaw(this, &FMCPTCPServer::Tick), Config.TickIntervalSeconds);
160 |     bRunning = true;
161 |     MCP_LOG_INFO("MCP Server started on port %d", Config.Port);
162 |     return true;
163 | }
164 | 
165 | void FMCPTCPServer::Stop()
166 | {
167 |     // Clean up all client connections
168 |     CleanupAllClientConnections();
169 |     
170 |     if (Listener)
171 |     {
172 |         delete Listener;
173 |         Listener = nullptr;
174 |     }
175 |     
176 |     if (TickerHandle.IsValid())
177 |     {
178 |         FTSTicker::GetCoreTicker().RemoveTicker(TickerHandle);
179 |         TickerHandle.Reset();
180 |     }
181 |     
182 |     bRunning = false;
183 |     MCP_LOG_INFO("MCP Server stopped");
184 | }
185 | 
186 | bool FMCPTCPServer::Tick(float DeltaTime)
187 | {
188 |     if (!bRunning) return false;
189 |     
190 |     // Normal processing
191 |     ProcessPendingConnections();
192 |     ProcessClientData();
193 |     CheckClientTimeouts(DeltaTime);
194 |     return true;
195 | }
196 | 
197 | void FMCPTCPServer::ProcessPendingConnections()
198 | {
199 |     if (!Listener) return;
200 |     
201 |     // Always accept new connections
202 |     if (!Listener->OnConnectionAccepted().IsBound())
203 |     {
204 |         Listener->OnConnectionAccepted().BindRaw(this, &FMCPTCPServer::HandleConnectionAccepted);
205 |     }
206 | }
207 | 
208 | bool FMCPTCPServer::HandleConnectionAccepted(FSocket* InSocket, const FIPv4Endpoint& Endpoint)
209 | {
210 |     if (!InSocket)
211 |     {
212 |         MCP_LOG_ERROR("HandleConnectionAccepted called with null socket");
213 |         return false;
214 |     }
215 | 
216 |     MCP_LOG_VERBOSE("Connection attempt from %s", *Endpoint.ToString());
217 |     
218 |     // Accept all connections
219 |     InSocket->SetNonBlocking(true);
220 |     
221 |     // Add to our list of client connections
222 |     ClientConnections.Add(FMCPClientConnection(InSocket, Endpoint, Config.ReceiveBufferSize));
223 |     
224 |     MCP_LOG_INFO("MCP Client connected from %s (Total clients: %d)", *Endpoint.ToString(), ClientConnections.Num());
225 |     return true;
226 | }
227 | 
228 | void FMCPTCPServer::ProcessClientData()
229 | {
230 |     // Make a copy of the array since we might modify it during iteration
231 |     TArray<FMCPClientConnection> ConnectionsCopy = ClientConnections;
232 |     
233 |     for (FMCPClientConnection& ClientConnection : ConnectionsCopy)
234 |     {
235 |         if (!ClientConnection.Socket) continue;
236 |         
237 |         // Check if the client is still connected
238 |         uint32 PendingDataSize = 0;
239 |         if (!ClientConnection.Socket->HasPendingData(PendingDataSize))
240 |         {
241 |             // Try to check connection status
242 |             uint8 DummyBuffer[1];
243 |             int32 BytesRead = 0;
244 |             
245 |             bool bConnectionLost = false;
246 |             
247 |             try
248 |             {
249 |                 if (!ClientConnection.Socket->Recv(DummyBuffer, 1, BytesRead, ESocketReceiveFlags::Peek))
250 |                 {
251 |                     // Check if it's a real error or just a non-blocking socket that would block
252 |                     int32 ErrorCode = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->GetLastErrorCode();
253 |                     if (ErrorCode != SE_EWOULDBLOCK)
254 |                     {
255 |                         // Real connection error
256 |                         MCP_LOG_INFO("Client connection from %s appears to be closed (error code %d), cleaning up", 
257 |                             *ClientConnection.Endpoint.ToString(), ErrorCode);
258 |                         bConnectionLost = true;
259 |                     }
260 |                 }
261 |             }
262 |             catch (...)
263 |             {
264 |                 MCP_LOG_ERROR("Exception while checking client connection status for %s", 
265 |                     *ClientConnection.Endpoint.ToString());
266 |                 bConnectionLost = true;
267 |             }
268 |             
269 |             if (bConnectionLost)
270 |             {
271 |                 CleanupClientConnection(ClientConnection);
272 |                 continue; // Skip to the next client
273 |             }
274 |         }
275 |         
276 |         // Reset PendingDataSize and check again to ensure we have the latest value
277 |         PendingDataSize = 0;
278 |         if (ClientConnection.Socket->HasPendingData(PendingDataSize))
279 |         {
280 |             if (Config.bEnableVerboseLogging)
281 |             {
282 |                 MCP_LOG_VERBOSE("Client from %s has %u bytes of pending data", 
283 |                     *ClientConnection.Endpoint.ToString(), PendingDataSize);
284 |             }
285 |             
286 |             // Reset timeout timer since we're receiving data
287 |             ClientConnection.TimeSinceLastActivity = 0.0f;
288 |             
289 |             int32 BytesRead = 0;
290 |             if (ClientConnection.Socket->Recv(ClientConnection.ReceiveBuffer.GetData(), ClientConnection.ReceiveBuffer.Num(), BytesRead))
291 |             {
292 |                 if (BytesRead > 0)
293 |                 {
294 |                     if (Config.bEnableVerboseLogging)
295 |                     {
296 |                         MCP_LOG_VERBOSE("Read %d bytes from client %s", BytesRead, *ClientConnection.Endpoint.ToString());
297 |                     }
298 |                     
299 |                     // Null-terminate the buffer to ensure it's a valid string
300 |                     ClientConnection.ReceiveBuffer[BytesRead] = 0;
301 |                     FString ReceivedData = FString(UTF8_TO_TCHAR(ClientConnection.ReceiveBuffer.GetData()));
302 |                     ProcessCommand(ReceivedData, ClientConnection.Socket);
303 |                 }
304 |             }
305 |             else
306 |             {
307 |                 // Check if it's a real error or just a non-blocking socket that would block
308 |                 int32 ErrorCode = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->GetLastErrorCode();
309 |                 if (ErrorCode != SE_EWOULDBLOCK)
310 |                 {
311 |                     // Real connection error, close the socket
312 |                     MCP_LOG_WARNING("Socket error %d for client %s, closing connection", 
313 |                         ErrorCode, *ClientConnection.Endpoint.ToString());
314 |                     CleanupClientConnection(ClientConnection);
315 |                 }
316 |             }
317 |         }
318 |     }
319 | }
320 | 
321 | void FMCPTCPServer::CheckClientTimeouts(float DeltaTime)
322 | {
323 |     // Make a copy of the array since we might modify it during iteration
324 |     TArray<FMCPClientConnection> ConnectionsCopy = ClientConnections;
325 |     
326 |     for (FMCPClientConnection& ClientConnection : ConnectionsCopy)
327 |     {
328 |         if (!ClientConnection.Socket) continue;
329 |         
330 |         // Increment time since last activity
331 |         ClientConnection.TimeSinceLastActivity += DeltaTime;
332 |         
333 |         // Check if client has timed out
334 |         if (ClientConnection.TimeSinceLastActivity > Config.ClientTimeoutSeconds)
335 |         {
336 |             MCP_LOG_WARNING("Client from %s timed out after %.1f seconds of inactivity, disconnecting", 
337 |                 *ClientConnection.Endpoint.ToString(), ClientConnection.TimeSinceLastActivity);
338 |             CleanupClientConnection(ClientConnection);
339 |         }
340 |     }
341 | }
342 | 
343 | void FMCPTCPServer::CleanupAllClientConnections()
344 | {
345 |     MCP_LOG_INFO("Cleaning up all client connections (%d total)", ClientConnections.Num());
346 |     
347 |     // Make a copy of the array since we'll be modifying it during iteration
348 |     TArray<FMCPClientConnection> ConnectionsCopy = ClientConnections;
349 |     
350 |     for (FMCPClientConnection& Connection : ConnectionsCopy)
351 |     {
352 |         CleanupClientConnection(Connection);
353 |     }
354 |     
355 |     // Ensure the array is empty
356 |     ClientConnections.Empty();
357 | }
358 | 
359 | void FMCPTCPServer::CleanupClientConnection(FSocket* ClientSocket)
360 | {
361 |     if (!ClientSocket) return;
362 |     
363 |     // Find the client connection with this socket
364 |     for (FMCPClientConnection& Connection : ClientConnections)
365 |     {
366 |         if (Connection.Socket == ClientSocket)
367 |         {
368 |             CleanupClientConnection(Connection);
369 |             break;
370 |         }
371 |     }
372 | }
373 | 
374 | void FMCPTCPServer::CleanupClientConnection(FMCPClientConnection& ClientConnection)
375 | {
376 |     if (!ClientConnection.Socket) return;
377 |     
378 |     MCP_LOG_INFO("Cleaning up client connection from %s", *ClientConnection.Endpoint.ToString());
379 |     
380 |     try
381 |     {
382 |         // Get the socket description before closing
383 |         FString SocketDesc = GetSafeSocketDescription(ClientConnection.Socket);
384 |         MCP_LOG_VERBOSE("Closing client socket with description: %s", *SocketDesc);
385 |         
386 |         // First close the socket
387 |         bool bCloseSuccess = ClientConnection.Socket->Close();
388 |         if (!bCloseSuccess)
389 |         {
390 |             MCP_LOG_ERROR("Failed to close client socket");
391 |         }
392 |         
393 |         // Then destroy it
394 |         ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
395 |         if (SocketSubsystem)
396 |         {
397 |             SocketSubsystem->DestroySocket(ClientConnection.Socket);
398 |             MCP_LOG_VERBOSE("Successfully destroyed client socket");
399 |         }
400 |         else
401 |         {
402 |             MCP_LOG_ERROR("Failed to get socket subsystem when cleaning up client connection");
403 |         }
404 |     }
405 |     catch (const std::exception& Ex)
406 |     {
407 |         MCP_LOG_ERROR("Exception while cleaning up client connection: %s", UTF8_TO_TCHAR(Ex.what()));
408 |     }
409 |     catch (...)
410 |     {
411 |         MCP_LOG_ERROR("Unknown exception while cleaning up client connection");
412 |     }
413 |     
414 |     // Remove from our list of connections
415 |     ClientConnections.RemoveAll([&ClientConnection](const FMCPClientConnection& Connection) {
416 |         return Connection.Socket == ClientConnection.Socket;
417 |     });
418 |     
419 |     MCP_LOG_INFO("MCP Client disconnected (Remaining clients: %d)", ClientConnections.Num());
420 | }
421 | 
422 | void FMCPTCPServer::ProcessCommand(const FString& CommandJson, FSocket* ClientSocket)
423 | {
424 |     if (Config.bEnableVerboseLogging)
425 |     {
426 |         MCP_LOG_VERBOSE("Processing command: %s", *CommandJson);
427 |     }
428 |     
429 |     TSharedPtr<FJsonObject> Command;
430 |     TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(CommandJson);
431 |     if (FJsonSerializer::Deserialize(Reader, Command) && Command.IsValid())
432 |     {
433 |         FString Type;
434 |         if (Command->TryGetStringField(FStringView(TEXT("type")), Type))
435 |         {
436 |             TSharedPtr<IMCPCommandHandler> Handler = CommandHandlers.FindRef(Type);
437 |             if (Handler.IsValid())
438 |             {
439 |                 MCP_LOG_INFO("Processing command: %s", *Type);
440 |                 
441 |                 const TSharedPtr<FJsonObject>* ParamsPtr = nullptr;
442 |                 TSharedPtr<FJsonObject> Params = MakeShared<FJsonObject>();
443 |                 
444 |                 if (Command->TryGetObjectField(FStringView(TEXT("params")), ParamsPtr) && ParamsPtr != nullptr)
445 |                 {
446 |                     Params = *ParamsPtr;
447 |                 }
448 |                 
449 |                 // Handle the command and get the response
450 |                 TSharedPtr<FJsonObject> Response = Handler->Execute(Params, ClientSocket);
451 |                 
452 |                 // Send the response
453 |                 SendResponse(ClientSocket, Response);
454 |             }
455 |             else
456 |             {
457 |                 MCP_LOG_WARNING("Unknown command: %s", *Type);
458 |                 
459 |                 TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
460 |                 Response->SetStringField("status", "error");
461 |                 Response->SetStringField("message", FString::Printf(TEXT("Unknown command: %s"), *Type));
462 |                 SendResponse(ClientSocket, Response);
463 |             }
464 |         }
465 |         else
466 |         {
467 |             MCP_LOG_WARNING("Missing 'type' field in command");
468 |             
469 |             TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
470 |             Response->SetStringField("status", "error");
471 |             Response->SetStringField("message", TEXT("Missing 'type' field"));
472 |             SendResponse(ClientSocket, Response);
473 |         }
474 |     }
475 |     else
476 |     {
477 |         MCP_LOG_WARNING("Invalid JSON format: %s", *CommandJson);
478 |         
479 |         TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
480 |         Response->SetStringField("status", "error");
481 |         Response->SetStringField("message", TEXT("Invalid JSON format"));
482 |         SendResponse(ClientSocket, Response);
483 |     }
484 |     
485 |     // Keep the connection open for future commands
486 |     // Do not close the socket here
487 | }
488 | 
489 | void FMCPTCPServer::SendResponse(FSocket* Client, const TSharedPtr<FJsonObject>& Response)
490 | {
491 |     if (!Client) return;
492 |     
493 |     FString ResponseStr;
494 |     TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&ResponseStr);
495 |     FJsonSerializer::Serialize(Response.ToSharedRef(), Writer);
496 |     
497 |     if (Config.bEnableVerboseLogging)
498 |     {
499 |         MCP_LOG_VERBOSE("Preparing to send response: %s", *ResponseStr);
500 |     }
501 |     
502 |     FTCHARToUTF8 Converter(*ResponseStr);
503 |     int32 BytesSent = 0;
504 |     int32 TotalBytes = Converter.Length();
505 |     const uint8* Data = (const uint8*)Converter.Get();
506 |     
507 |     // Ensure all data is sent
508 |     while (BytesSent < TotalBytes)
509 |     {
510 |         int32 SentThisTime = 0;
511 |         if (!Client->Send(Data + BytesSent, TotalBytes - BytesSent, SentThisTime))
512 |         {
513 |             MCP_LOG_WARNING("Failed to send response");
514 |             break;
515 |         }
516 |         
517 |         if (SentThisTime <= 0)
518 |         {
519 |             // Would block, try again next tick
520 |             MCP_LOG_VERBOSE("Socket would block, will try again next tick");
521 |             break;
522 |         }
523 |         
524 |         BytesSent += SentThisTime;
525 |         
526 |         if (Config.bEnableVerboseLogging)
527 |         {
528 |             MCP_LOG_VERBOSE("Sent %d/%d bytes", BytesSent, TotalBytes);
529 |         }
530 |     }
531 |     
532 |     if (BytesSent == TotalBytes)
533 |     {
534 |         MCP_LOG_INFO("Successfully sent complete response (%d bytes)", TotalBytes);
535 |     }
536 |     else
537 |     {
538 |         MCP_LOG_WARNING("Only sent %d/%d bytes of response", BytesSent, TotalBytes);
539 |     }
540 | }
541 | 
542 | FString FMCPTCPServer::GetSafeSocketDescription(FSocket* Socket)
543 | {
544 |     if (!Socket)
545 |     {
546 |         return TEXT("NullSocket");
547 |     }
548 |     
549 |     try
550 |     {
551 |         FString Description = Socket->GetDescription();
552 |         
553 |         // Check if the description contains any non-ASCII characters
554 |         bool bHasNonAscii = false;
555 |         for (TCHAR Char : Description)
556 |         {
557 |             if (Char > 127)
558 |             {
559 |                 bHasNonAscii = true;
560 |                 break;
561 |             }
562 |         }
563 |         
564 |         if (bHasNonAscii)
565 |         {
566 |             // Return a safe description instead
567 |             return TEXT("Socket_") + FString::FromInt(reinterpret_cast<uint64>(Socket));
568 |         }
569 |         
570 |         return Description;
571 |     }
572 |     catch (...)
573 |     {
574 |         // If there's any exception, return a safe description
575 |         return TEXT("Socket_") + FString::FromInt(reinterpret_cast<uint64>(Socket));
576 |     }
577 | } 
```

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

```cpp
  1 | #include "MCPCommandHandlers_Materials.h"
  2 | #include "MCPCommandHandlers.h"
  3 | #include "Editor.h"
  4 | #include "MCPFileLogger.h"
  5 | #include "HAL/PlatformFilemanager.h"
  6 | #include "Misc/FileHelper.h"
  7 | #include "Misc/Paths.h"
  8 | #include "Misc/Guid.h"
  9 | #include "MCPConstants.h"
 10 | #include "Materials/MaterialExpressionScalarParameter.h"
 11 | #include "Materials/MaterialExpressionVectorParameter.h"
 12 | #include "UObject/SavePackage.h"
 13 | 
 14 | 
 15 | //
 16 | // FMCPCreateMaterialHandler
 17 | //
 18 | TSharedPtr<FJsonObject> FMCPCreateMaterialHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
 19 | {
 20 |     MCP_LOG_INFO("Handling create_material command");
 21 | 
 22 |     FString PackagePath;
 23 |     if (!Params->TryGetStringField(FStringView(TEXT("package_path")), PackagePath))
 24 |     {
 25 |         MCP_LOG_WARNING("Missing 'package_path' field in create_material command");
 26 |         return CreateErrorResponse("Missing 'package_path' field");
 27 |     }
 28 | 
 29 |     FString MaterialName;
 30 |     if (!Params->TryGetStringField(FStringView(TEXT("name")), MaterialName))
 31 |     {
 32 |         MCP_LOG_WARNING("Missing 'name' field in create_material command");
 33 |         return CreateErrorResponse("Missing 'name' field");
 34 |     }
 35 | 
 36 |     // Get optional properties
 37 |     const TSharedPtr<FJsonObject>* Properties = nullptr;
 38 |     Params->TryGetObjectField(FStringView(TEXT("properties")), Properties);
 39 | 
 40 |     // Create the material
 41 |     TPair<UMaterial*, bool> Result = CreateMaterial(PackagePath, MaterialName, Properties ? *Properties : nullptr);
 42 | 
 43 |     if (Result.Value)
 44 |     {
 45 |         TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
 46 |         ResultObj->SetStringField("name", Result.Key->GetName());
 47 |         ResultObj->SetStringField("path", Result.Key->GetPathName());
 48 |         return CreateSuccessResponse(ResultObj);
 49 |     }
 50 |     else
 51 |     {
 52 |         return CreateErrorResponse("Failed to create material");
 53 |     }
 54 | }
 55 | 
 56 | TPair<UMaterial*, bool> FMCPCreateMaterialHandler::CreateMaterial(const FString& PackagePath, const FString& MaterialName, const TSharedPtr<FJsonObject>& Properties)
 57 | {
 58 |     // Create the package path
 59 |     FString FullPath = FPaths::Combine(PackagePath, MaterialName);
 60 |     UPackage* Package = CreatePackage(*FullPath);
 61 |     if (!Package)
 62 |     {
 63 |         MCP_LOG_ERROR("Failed to create package at path: %s", *FullPath);
 64 |         return TPair<UMaterial*, bool>(nullptr, false);
 65 |     }
 66 | 
 67 |     // Create the material
 68 |     UMaterial* NewMaterial = NewObject<UMaterial>(Package, *MaterialName, RF_Public | RF_Standalone);
 69 |     if (!NewMaterial)
 70 |     {
 71 |         MCP_LOG_ERROR("Failed to create material: %s", *MaterialName);
 72 |         return TPair<UMaterial*, bool>(nullptr, false);
 73 |     }
 74 | 
 75 |     // Set default properties
 76 |     NewMaterial->SetShadingModel(MSM_DefaultLit);
 77 |     NewMaterial->BlendMode = BLEND_Opaque;
 78 |     NewMaterial->TwoSided = false;
 79 |     NewMaterial->DitheredLODTransition = false;
 80 |     NewMaterial->bCastDynamicShadowAsMasked = false;
 81 | 
 82 |     // Apply any custom properties if provided
 83 |     if (Properties)
 84 |     {
 85 |         ModifyMaterialProperties(NewMaterial, Properties);
 86 |     }
 87 | 
 88 |     // Save the package
 89 |     Package->SetDirtyFlag(true);
 90 |     
 91 |     // Construct the full file path for saving
 92 |     FString SavePath = FPaths::Combine(FPaths::ProjectContentDir(), PackagePath, MaterialName + TEXT(".uasset"));
 93 |     
 94 |     // Create save package args
 95 |     FSavePackageArgs SaveArgs;
 96 |     SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
 97 |     SaveArgs.SaveFlags = SAVE_NoError;
 98 |     SaveArgs.bForceByteSwapping = false;
 99 |     SaveArgs.bWarnOfLongFilename = true;
100 |     
101 |     // Save the package
102 |     if (!UPackage::SavePackage(Package, NewMaterial, *SavePath, SaveArgs))
103 |     {
104 |         MCP_LOG_ERROR("Failed to save material package at path: %s", *SavePath);
105 |         return TPair<UMaterial*, bool>(nullptr, false);
106 |     }
107 |     
108 |     // Trigger material compilation
109 |     NewMaterial->PostEditChange();
110 | 
111 |     MCP_LOG_INFO("Created material: %s at path: %s", *MaterialName, *FullPath);
112 |     return TPair<UMaterial*, bool>(NewMaterial, true);
113 | }
114 | 
115 | bool FMCPCreateMaterialHandler::ModifyMaterialProperties(UMaterial* Material, const TSharedPtr<FJsonObject>& Properties)
116 | {
117 |     if (!Material || !Properties)
118 |     {
119 |         return false;
120 |     }
121 | 
122 |     bool bSuccess = true;
123 | 
124 |     // Shading Model
125 |     FString ShadingModel;
126 |     if (Properties->TryGetStringField(FStringView(TEXT("shading_model")), ShadingModel))
127 |     {
128 |         if (ShadingModel == "DefaultLit")
129 |             Material->SetShadingModel(MSM_DefaultLit);
130 |         else if (ShadingModel == "Unlit")
131 |             Material->SetShadingModel(MSM_Unlit);
132 |         else if (ShadingModel == "Subsurface")
133 |             Material->SetShadingModel(MSM_Subsurface);
134 |         else if (ShadingModel == "PreintegratedSkin")
135 |             Material->SetShadingModel(MSM_PreintegratedSkin);
136 |         else if (ShadingModel == "ClearCoat")
137 |             Material->SetShadingModel(MSM_ClearCoat);
138 |         else if (ShadingModel == "SubsurfaceProfile")
139 |             Material->SetShadingModel(MSM_SubsurfaceProfile);
140 |         else if (ShadingModel == "TwoSidedFoliage")
141 |             Material->SetShadingModel(MSM_TwoSidedFoliage);
142 |         else if (ShadingModel == "Hair")
143 |             Material->SetShadingModel(MSM_Hair);
144 |         else if (ShadingModel == "Cloth")
145 |             Material->SetShadingModel(MSM_Cloth);
146 |         else if (ShadingModel == "Eye")
147 |             Material->SetShadingModel(MSM_Eye);
148 |         else
149 |             bSuccess = false;
150 |     }
151 | 
152 |     // Blend Mode
153 |     FString BlendMode;
154 |     if (Properties->TryGetStringField(FStringView(TEXT("blend_mode")), BlendMode))
155 |     {
156 |         if (BlendMode == "Opaque")
157 |             Material->BlendMode = BLEND_Opaque;
158 |         else if (BlendMode == "Masked")
159 |             Material->BlendMode = BLEND_Masked;
160 |         else if (BlendMode == "Translucent")
161 |             Material->BlendMode = BLEND_Translucent;
162 |         else if (BlendMode == "Additive")
163 |             Material->BlendMode = BLEND_Additive;
164 |         else if (BlendMode == "Modulate")
165 |             Material->BlendMode = BLEND_Modulate;
166 |         else if (BlendMode == "AlphaComposite")
167 |             Material->BlendMode = BLEND_AlphaComposite;
168 |         else if (BlendMode == "AlphaHoldout")
169 |             Material->BlendMode = BLEND_AlphaHoldout;
170 |         else
171 |             bSuccess = false;
172 |     }
173 | 
174 |     // Two Sided
175 |     bool bTwoSided;
176 |     if (Properties->TryGetBoolField(FStringView(TEXT("two_sided")), bTwoSided))
177 |     {
178 |         Material->TwoSided = bTwoSided;
179 |     }
180 | 
181 |     // Dithered LOD Transition
182 |     bool bDitheredLODTransition;
183 |     if (Properties->TryGetBoolField(FStringView(TEXT("dithered_lod_transition")), bDitheredLODTransition))
184 |     {
185 |         Material->DitheredLODTransition = bDitheredLODTransition;
186 |     }
187 | 
188 |     // Cast Contact Shadow
189 |     bool bCastContactShadow;
190 |     if (Properties->TryGetBoolField(FStringView(TEXT("cast_contact_shadow")), bCastContactShadow))
191 |     {
192 |         Material->bCastDynamicShadowAsMasked = bCastContactShadow;
193 |     }
194 | 
195 |     // Base Color
196 |     const TArray<TSharedPtr<FJsonValue>>* BaseColorArray = nullptr;
197 |     if (Properties->TryGetArrayField(FStringView(TEXT("base_color")), BaseColorArray) && BaseColorArray && BaseColorArray->Num() == 4)
198 |     {
199 |         FLinearColor BaseColor(
200 |             (*BaseColorArray)[0]->AsNumber(),
201 |             (*BaseColorArray)[1]->AsNumber(),
202 |             (*BaseColorArray)[2]->AsNumber(),
203 |             (*BaseColorArray)[3]->AsNumber()
204 |         );
205 |         
206 |         // Create a Vector4 constant expression for base color
207 |         UMaterialExpressionVectorParameter* BaseColorParam = NewObject<UMaterialExpressionVectorParameter>(Material);
208 |         BaseColorParam->ParameterName = TEXT("BaseColor");
209 |         BaseColorParam->DefaultValue = BaseColor;
210 |         Material->GetExpressionCollection().AddExpression(BaseColorParam);
211 |         Material->GetEditorOnlyData()->BaseColor.Expression = BaseColorParam;
212 |     }
213 | 
214 |     // Metallic
215 |     double Metallic;
216 |     if (Properties->TryGetNumberField(FStringView(TEXT("metallic")), Metallic))
217 |     {
218 |         // Create a scalar constant expression for metallic
219 |         UMaterialExpressionScalarParameter* MetallicParam = NewObject<UMaterialExpressionScalarParameter>(Material);
220 |         MetallicParam->ParameterName = TEXT("Metallic");
221 |         MetallicParam->DefaultValue = FMath::Clamp(Metallic, 0.0, 1.0);
222 |         Material->GetExpressionCollection().AddExpression(MetallicParam);
223 |         Material->GetEditorOnlyData()->Metallic.Expression = MetallicParam;
224 |     }
225 | 
226 |     // Roughness
227 |     double Roughness;
228 |     if (Properties->TryGetNumberField(FStringView(TEXT("roughness")), Roughness))
229 |     {
230 |         // Create a scalar constant expression for roughness
231 |         UMaterialExpressionScalarParameter* RoughnessParam = NewObject<UMaterialExpressionScalarParameter>(Material);
232 |         RoughnessParam->ParameterName = TEXT("Roughness");
233 |         RoughnessParam->DefaultValue = FMath::Clamp(Roughness, 0.0, 1.0);
234 |         Material->GetExpressionCollection().AddExpression(RoughnessParam);
235 |         Material->GetEditorOnlyData()->Roughness.Expression = RoughnessParam;
236 |     }
237 | 
238 |     return bSuccess;
239 | }
240 | 
241 | //
242 | // FMCPModifyMaterialHandler
243 | //
244 | TSharedPtr<FJsonObject> FMCPModifyMaterialHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
245 | {
246 |     MCP_LOG_INFO("Handling modify_material command");
247 | 
248 |     FString MaterialPath;
249 |     if (!Params->TryGetStringField(FStringView(TEXT("path")), MaterialPath))
250 |     {
251 |         MCP_LOG_WARNING("Missing 'path' field in modify_material command");
252 |         return CreateErrorResponse("Missing 'path' field");
253 |     }
254 | 
255 |     const TSharedPtr<FJsonObject>* Properties = nullptr;
256 |     if (!Params->TryGetObjectField(FStringView(TEXT("properties")), Properties))
257 |     {
258 |         MCP_LOG_WARNING("Missing 'properties' field in modify_material command");
259 |         return CreateErrorResponse("Missing 'properties' field");
260 |     }
261 | 
262 |     // Load the material
263 |     UMaterial* Material = LoadObject<UMaterial>(nullptr, *MaterialPath);
264 |     if (!Material)
265 |     {
266 |         MCP_LOG_ERROR("Failed to load material at path: %s", *MaterialPath);
267 |         return CreateErrorResponse(FString::Printf(TEXT("Failed to load material at path: %s"), *MaterialPath));
268 |     }
269 | 
270 |     // Modify the material properties
271 |     bool bSuccess = ModifyMaterialProperties(Material, *Properties);
272 | 
273 |     if (bSuccess)
274 |     {
275 |         // Save the package
276 |         Material->GetPackage()->SetDirtyFlag(true);
277 |         
278 |         // Create save package args
279 |         FSavePackageArgs SaveArgs;
280 |         SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
281 |         SaveArgs.SaveFlags = SAVE_NoError;
282 |         SaveArgs.bForceByteSwapping = false;
283 |         SaveArgs.bWarnOfLongFilename = true;
284 |         
285 |         // Construct the full file path for saving
286 |         FString SavePath = FPaths::Combine(FPaths::ProjectContentDir(), Material->GetPathName() + TEXT(".uasset"));
287 |         
288 |         // Save the package with the proper args
289 |         if (!UPackage::SavePackage(Material->GetPackage(), Material, *SavePath, SaveArgs))
290 |         {
291 |             MCP_LOG_ERROR("Failed to save material package at path: %s", *SavePath);
292 |             return CreateErrorResponse("Failed to save material package");
293 |         }
294 | 
295 |         // Trigger material compilation
296 |         Material->PostEditChange();
297 | 
298 |         TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
299 |         ResultObj->SetStringField("name", Material->GetName());
300 |         ResultObj->SetStringField("path", Material->GetPathName());
301 |         return CreateSuccessResponse(ResultObj);
302 |     }
303 |     else
304 |     {
305 |         return CreateErrorResponse("Failed to modify material properties");
306 |     }
307 | }
308 | 
309 | bool FMCPModifyMaterialHandler::ModifyMaterialProperties(UMaterial* Material, const TSharedPtr<FJsonObject>& Properties)
310 | {
311 |     if (!Material || !Properties)
312 |     {
313 |         return false;
314 |     }
315 | 
316 |     bool bSuccess = true;
317 | 
318 |     // Shading Model
319 |     FString ShadingModel;
320 |     if (Properties->TryGetStringField(FStringView(TEXT("shading_model")), ShadingModel))
321 |     {
322 |         if (ShadingModel == "DefaultLit")
323 |             Material->SetShadingModel(MSM_DefaultLit);
324 |         else if (ShadingModel == "Unlit")
325 |             Material->SetShadingModel(MSM_Unlit);
326 |         else if (ShadingModel == "Subsurface")
327 |             Material->SetShadingModel(MSM_Subsurface);
328 |         else if (ShadingModel == "PreintegratedSkin")
329 |             Material->SetShadingModel(MSM_PreintegratedSkin);
330 |         else if (ShadingModel == "ClearCoat")
331 |             Material->SetShadingModel(MSM_ClearCoat);
332 |         else if (ShadingModel == "SubsurfaceProfile")
333 |             Material->SetShadingModel(MSM_SubsurfaceProfile);
334 |         else if (ShadingModel == "TwoSidedFoliage")
335 |             Material->SetShadingModel(MSM_TwoSidedFoliage);
336 |         else if (ShadingModel == "Hair")
337 |             Material->SetShadingModel(MSM_Hair);
338 |         else if (ShadingModel == "Cloth")
339 |             Material->SetShadingModel(MSM_Cloth);
340 |         else if (ShadingModel == "Eye")
341 |             Material->SetShadingModel(MSM_Eye);
342 |         else
343 |             bSuccess = false;
344 |     }
345 | 
346 |     // Blend Mode
347 |     FString BlendMode;
348 |     if (Properties->TryGetStringField(FStringView(TEXT("blend_mode")), BlendMode))
349 |     {
350 |         if (BlendMode == "Opaque")
351 |             Material->BlendMode = BLEND_Opaque;
352 |         else if (BlendMode == "Masked")
353 |             Material->BlendMode = BLEND_Masked;
354 |         else if (BlendMode == "Translucent")
355 |             Material->BlendMode = BLEND_Translucent;
356 |         else if (BlendMode == "Additive")
357 |             Material->BlendMode = BLEND_Additive;
358 |         else if (BlendMode == "Modulate")
359 |             Material->BlendMode = BLEND_Modulate;
360 |         else if (BlendMode == "AlphaComposite")
361 |             Material->BlendMode = BLEND_AlphaComposite;
362 |         else if (BlendMode == "AlphaHoldout")
363 |             Material->BlendMode = BLEND_AlphaHoldout;
364 |         else
365 |             bSuccess = false;
366 |     }
367 | 
368 |     // Two Sided
369 |     bool bTwoSided;
370 |     if (Properties->TryGetBoolField(FStringView(TEXT("two_sided")), bTwoSided))
371 |     {
372 |         Material->TwoSided = bTwoSided;
373 |     }
374 | 
375 |     // Dithered LOD Transition
376 |     bool bDitheredLODTransition;
377 |     if (Properties->TryGetBoolField(FStringView(TEXT("dithered_lod_transition")), bDitheredLODTransition))
378 |     {
379 |         Material->DitheredLODTransition = bDitheredLODTransition;
380 |     }
381 | 
382 |     // Cast Contact Shadow
383 |     bool bCastContactShadow;
384 |     if (Properties->TryGetBoolField(FStringView(TEXT("cast_contact_shadow")), bCastContactShadow))
385 |     {
386 |         Material->bCastDynamicShadowAsMasked = bCastContactShadow;
387 |     }
388 | 
389 |     // Base Color
390 |     const TArray<TSharedPtr<FJsonValue>>* BaseColorArray = nullptr;
391 |     if (Properties->TryGetArrayField(FStringView(TEXT("base_color")), BaseColorArray) && BaseColorArray && BaseColorArray->Num() == 4)
392 |     {
393 |         FLinearColor BaseColor(
394 |             (*BaseColorArray)[0]->AsNumber(),
395 |             (*BaseColorArray)[1]->AsNumber(),
396 |             (*BaseColorArray)[2]->AsNumber(),
397 |             (*BaseColorArray)[3]->AsNumber()
398 |         );
399 |         
400 |         // Create a Vector4 constant expression for base color
401 |         UMaterialExpressionVectorParameter* BaseColorParam = NewObject<UMaterialExpressionVectorParameter>(Material);
402 |         BaseColorParam->ParameterName = TEXT("BaseColor");
403 |         BaseColorParam->DefaultValue = BaseColor;
404 |         Material->GetExpressionCollection().AddExpression(BaseColorParam);
405 |         Material->GetEditorOnlyData()->BaseColor.Expression = BaseColorParam;
406 |     }
407 | 
408 |     // Metallic
409 |     double Metallic;
410 |     if (Properties->TryGetNumberField(FStringView(TEXT("metallic")), Metallic))
411 |     {
412 |         // Create a scalar constant expression for metallic
413 |         UMaterialExpressionScalarParameter* MetallicParam = NewObject<UMaterialExpressionScalarParameter>(Material);
414 |         MetallicParam->ParameterName = TEXT("Metallic");
415 |         MetallicParam->DefaultValue = FMath::Clamp(Metallic, 0.0, 1.0);
416 |         Material->GetExpressionCollection().AddExpression(MetallicParam);
417 |         Material->GetEditorOnlyData()->Metallic.Expression = MetallicParam;
418 |     }
419 | 
420 |     // Roughness
421 |     double Roughness;
422 |     if (Properties->TryGetNumberField(FStringView(TEXT("roughness")), Roughness))
423 |     {
424 |         // Create a scalar constant expression for roughness
425 |         UMaterialExpressionScalarParameter* RoughnessParam = NewObject<UMaterialExpressionScalarParameter>(Material);
426 |         RoughnessParam->ParameterName = TEXT("Roughness");
427 |         RoughnessParam->DefaultValue = FMath::Clamp(Roughness, 0.0, 1.0);
428 |         Material->GetExpressionCollection().AddExpression(RoughnessParam);
429 |         Material->GetEditorOnlyData()->Roughness.Expression = RoughnessParam;
430 |     }
431 | 
432 |     return bSuccess;
433 | }
434 | 
435 | //
436 | // FMCPGetMaterialInfoHandler
437 | //
438 | TSharedPtr<FJsonObject> FMCPGetMaterialInfoHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
439 | {
440 |     MCP_LOG_INFO("Handling get_material_info command");
441 | 
442 |     FString MaterialPath;
443 |     if (!Params->TryGetStringField(FStringView(TEXT("path")), MaterialPath))
444 |     {
445 |         MCP_LOG_WARNING("Missing 'path' field in get_material_info command");
446 |         return CreateErrorResponse("Missing 'path' field");
447 |     }
448 | 
449 |     // Load the material
450 |     UMaterial* Material = LoadObject<UMaterial>(nullptr, *MaterialPath);
451 |     if (!Material)
452 |     {
453 |         MCP_LOG_ERROR("Failed to load material at path: %s", *MaterialPath);
454 |         return CreateErrorResponse(FString::Printf(TEXT("Failed to load material at path: %s"), *MaterialPath));
455 |     }
456 | 
457 |     // Get material info
458 |     TSharedPtr<FJsonObject> ResultObj = GetMaterialInfo(Material);
459 |     return CreateSuccessResponse(ResultObj);
460 | }
461 | 
462 | TSharedPtr<FJsonObject> FMCPGetMaterialInfoHandler::GetMaterialInfo(UMaterial* Material)
463 | {
464 |     TSharedPtr<FJsonObject> Info = MakeShared<FJsonObject>();
465 |     
466 |     // Basic info
467 |     Info->SetStringField("name", Material->GetName());
468 |     Info->SetStringField("path", Material->GetPathName());
469 | 
470 |     // Shading Model
471 |     FString ShadingModel = "Unknown";
472 |     FMaterialShadingModelField ShadingModels = Material->GetShadingModels();
473 |     if (ShadingModels.HasShadingModel(MSM_DefaultLit)) ShadingModel = "DefaultLit";
474 |     else if (ShadingModels.HasShadingModel(MSM_Unlit)) ShadingModel = "Unlit";
475 |     else if (ShadingModels.HasShadingModel(MSM_Subsurface)) ShadingModel = "Subsurface";
476 |     else if (ShadingModels.HasShadingModel(MSM_PreintegratedSkin)) ShadingModel = "PreintegratedSkin";
477 |     else if (ShadingModels.HasShadingModel(MSM_ClearCoat)) ShadingModel = "ClearCoat";
478 |     else if (ShadingModels.HasShadingModel(MSM_SubsurfaceProfile)) ShadingModel = "SubsurfaceProfile";
479 |     else if (ShadingModels.HasShadingModel(MSM_TwoSidedFoliage)) ShadingModel = "TwoSidedFoliage";
480 |     else if (ShadingModels.HasShadingModel(MSM_Hair)) ShadingModel = "Hair";
481 |     else if (ShadingModels.HasShadingModel(MSM_Cloth)) ShadingModel = "Cloth";
482 |     else if (ShadingModels.HasShadingModel(MSM_Eye)) ShadingModel = "Eye";
483 |     Info->SetStringField("shading_model", ShadingModel);
484 | 
485 |     // Blend Mode
486 |     FString BlendMode;
487 |     switch (Material->GetBlendMode())
488 |     {
489 |         case BLEND_Opaque: BlendMode = "Opaque"; break;
490 |         case BLEND_Masked: BlendMode = "Masked"; break;
491 |         case BLEND_Translucent: BlendMode = "Translucent"; break;
492 |         case BLEND_Additive: BlendMode = "Additive"; break;
493 |         case BLEND_Modulate: BlendMode = "Modulate"; break;
494 |         case BLEND_AlphaComposite: BlendMode = "AlphaComposite"; break;
495 |         case BLEND_AlphaHoldout: BlendMode = "AlphaHoldout"; break;
496 |         default: BlendMode = "Unknown"; break;
497 |     }
498 |     Info->SetStringField("blend_mode", BlendMode);
499 | 
500 |     // Other properties
501 |     Info->SetBoolField("two_sided", Material->IsTwoSided());
502 |     Info->SetBoolField("dithered_lod_transition", Material->IsDitheredLODTransition());
503 |     Info->SetBoolField("cast_contact_shadow", Material->bContactShadows);
504 | 
505 |     // Base Color
506 |     TArray<TSharedPtr<FJsonValue>> BaseColorArray;
507 |     FLinearColor BaseColorValue = FLinearColor::White;
508 |     if (Material->GetEditorOnlyData()->BaseColor.Expression)
509 |     {
510 |         if (UMaterialExpressionVectorParameter* BaseColorParam = Cast<UMaterialExpressionVectorParameter>(Material->GetEditorOnlyData()->BaseColor.Expression))
511 |         {
512 |             BaseColorValue = BaseColorParam->DefaultValue;
513 |         }
514 |     }
515 |     BaseColorArray.Add(MakeShared<FJsonValueNumber>(BaseColorValue.R));
516 |     BaseColorArray.Add(MakeShared<FJsonValueNumber>(BaseColorValue.G));
517 |     BaseColorArray.Add(MakeShared<FJsonValueNumber>(BaseColorValue.B));
518 |     BaseColorArray.Add(MakeShared<FJsonValueNumber>(BaseColorValue.A));
519 |     Info->SetArrayField("base_color", BaseColorArray);
520 | 
521 |     // Metallic
522 |     float MetallicValue = 0.0f;
523 |     if (Material->GetEditorOnlyData()->Metallic.Expression)
524 |     {
525 |         if (UMaterialExpressionScalarParameter* MetallicParam = Cast<UMaterialExpressionScalarParameter>(Material->GetEditorOnlyData()->Metallic.Expression))
526 |         {
527 |             MetallicValue = MetallicParam->DefaultValue;
528 |         }
529 |     }
530 |     Info->SetNumberField("metallic", MetallicValue);
531 | 
532 |     // Roughness
533 |     float RoughnessValue = 0.5f;
534 |     if (Material->GetEditorOnlyData()->Roughness.Expression)
535 |     {
536 |         if (UMaterialExpressionScalarParameter* RoughnessParam = Cast<UMaterialExpressionScalarParameter>(Material->GetEditorOnlyData()->Roughness.Expression))
537 |         {
538 |             RoughnessValue = RoughnessParam->DefaultValue;
539 |         }
540 |     }
541 |     Info->SetNumberField("roughness", RoughnessValue);
542 | 
543 |     return Info;
544 | } 
```

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

```cpp
  1 | #include "MCPCommandHandlers.h"
  2 | 
  3 | #include "ActorEditorUtils.h"
  4 | #include "Editor.h"
  5 | #include "EngineUtils.h"
  6 | #include "MCPFileLogger.h"
  7 | #include "HAL/PlatformFilemanager.h"
  8 | #include "Misc/FileHelper.h"
  9 | #include "Misc/Paths.h"
 10 | #include "Misc/Guid.h"
 11 | #include "MCPConstants.h"
 12 | #include "Kismet/GameplayStatics.h"
 13 | #include "Kismet/KismetSystemLibrary.h"
 14 | #include "Engine/Blueprint.h"
 15 | #include "Engine/BlueprintGeneratedClass.h"
 16 | 
 17 | 
 18 | //
 19 | // FMCPGetSceneInfoHandler
 20 | //
 21 | TSharedPtr<FJsonObject> FMCPGetSceneInfoHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
 22 | {
 23 |     MCP_LOG_INFO("Handling get_scene_info command");
 24 | 
 25 |     UWorld *World = GEditor->GetEditorWorldContext().World();
 26 |     TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
 27 |     TArray<TSharedPtr<FJsonValue>> ActorsArray;
 28 | 
 29 |     int32 ActorCount = 0;
 30 |     int32 TotalActorCount = 0;
 31 |     bool bLimitReached = false;
 32 | 
 33 |     // First count the total number of actors
 34 |     for (TActorIterator<AActor> CountIt(World); CountIt; ++CountIt)
 35 |     {
 36 |         TotalActorCount++;
 37 |     }
 38 | 
 39 |     // Then collect actor info up to the limit
 40 |     for (TActorIterator<AActor> It(World); It; ++It)
 41 |     {
 42 |         AActor *Actor = *It;
 43 |         TSharedPtr<FJsonObject> ActorInfo = MakeShared<FJsonObject>();
 44 |         ActorInfo->SetStringField("name", Actor->GetName());
 45 |         ActorInfo->SetStringField("type", Actor->GetClass()->GetName());
 46 | 
 47 |         // Add the actor label (user-facing friendly name)
 48 |         ActorInfo->SetStringField("label", Actor->GetActorLabel());
 49 | 
 50 |         // Add location
 51 |         FVector Location = Actor->GetActorLocation();
 52 |         TArray<TSharedPtr<FJsonValue>> LocationArray;
 53 |         LocationArray.Add(MakeShared<FJsonValueNumber>(Location.X));
 54 |         LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Y));
 55 |         LocationArray.Add(MakeShared<FJsonValueNumber>(Location.Z));
 56 |         ActorInfo->SetArrayField("location", LocationArray);
 57 | 
 58 |         ActorsArray.Add(MakeShared<FJsonValueObject>(ActorInfo));
 59 |         ActorCount++;
 60 |         if (ActorCount >= MCPConstants::MAX_ACTORS_IN_SCENE_INFO)
 61 |         {
 62 |             bLimitReached = true;
 63 |             MCP_LOG_WARNING("Actor limit reached (%d). Only returning %d of %d actors.",
 64 |                             MCPConstants::MAX_ACTORS_IN_SCENE_INFO, ActorCount, TotalActorCount);
 65 |             break; // Limit for performance
 66 |         }
 67 |     }
 68 | 
 69 |     Result->SetStringField("level", World->GetName());
 70 |     Result->SetNumberField("actor_count", TotalActorCount);
 71 |     Result->SetNumberField("returned_actor_count", ActorCount);
 72 |     Result->SetBoolField("limit_reached", bLimitReached);
 73 |     Result->SetArrayField("actors", ActorsArray);
 74 | 
 75 |     MCP_LOG_INFO("Sending get_scene_info response with %d/%d actors", ActorCount, TotalActorCount);
 76 | 
 77 |     return CreateSuccessResponse(Result);
 78 | }
 79 | 
 80 | //
 81 | // FMCPCreateObjectHandler
 82 | //
 83 | TSharedPtr<FJsonObject> FMCPCreateObjectHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
 84 | {
 85 |     UWorld *World = GEditor->GetEditorWorldContext().World();
 86 | 
 87 |     FString Type;
 88 |     if (!Params->TryGetStringField(FStringView(TEXT("type")), Type))
 89 |     {
 90 |         MCP_LOG_WARNING("Missing 'type' field in create_object command");
 91 |         return CreateErrorResponse("Missing 'type' field");
 92 |     }
 93 | 
 94 |     // Get location
 95 |     const TArray<TSharedPtr<FJsonValue>> *LocationArrayPtr = nullptr;
 96 |     if (!Params->TryGetArrayField(FStringView(TEXT("location")), LocationArrayPtr) || !LocationArrayPtr || LocationArrayPtr->Num() != 3)
 97 |     {
 98 |         MCP_LOG_WARNING("Invalid 'location' field in create_object command");
 99 |         return CreateErrorResponse("Invalid 'location' field");
100 |     }
101 | 
102 |     FVector Location(
103 |         (*LocationArrayPtr)[0]->AsNumber(),
104 |         (*LocationArrayPtr)[1]->AsNumber(),
105 |         (*LocationArrayPtr)[2]->AsNumber());
106 | 
107 |     // Convert type to lowercase for case-insensitive comparison
108 |     FString TypeLower = Type.ToLower();
109 | 
110 |     if (Type == "StaticMeshActor")
111 |     {
112 |         // Get mesh path if specified
113 |         FString MeshPath;
114 |         Params->TryGetStringField(FStringView(TEXT("mesh")), MeshPath);
115 | 
116 |         // Get label if specified
117 |         FString Label;
118 |         Params->TryGetStringField(FStringView(TEXT("label")), Label);
119 | 
120 |         // Create the actor
121 |         TPair<AStaticMeshActor *, bool> Result = CreateStaticMeshActor(World, Location, MeshPath, Label);
122 | 
123 |         if (Result.Value)
124 |         {
125 |             TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
126 |             ResultObj->SetStringField("name", Result.Key->GetName());
127 |             ResultObj->SetStringField("label", Result.Key->GetActorLabel());
128 |             return CreateSuccessResponse(ResultObj);
129 |         }
130 |         else
131 |         {
132 |             return CreateErrorResponse("Failed to create StaticMeshActor");
133 |         }
134 |     }
135 |     else if (TypeLower == "cube")
136 |     {
137 |         // Create a cube actor
138 |         FString Label;
139 |         Params->TryGetStringField(FStringView(TEXT("label")), Label);
140 |         TPair<AStaticMeshActor *, bool> Result = CreateCubeActor(World, Location, Label);
141 | 
142 |         if (Result.Value)
143 |         {
144 |             TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
145 |             ResultObj->SetStringField("name", Result.Key->GetName());
146 |             ResultObj->SetStringField("label", Result.Key->GetActorLabel());
147 |             return CreateSuccessResponse(ResultObj);
148 |         }
149 |         else
150 |         {
151 |             return CreateErrorResponse("Failed to create cube");
152 |         }
153 |     }
154 |     else
155 |     {
156 |         MCP_LOG_WARNING("Unsupported actor type: %s", *Type);
157 |         return CreateErrorResponse(FString::Printf(TEXT("Unsupported actor type: %s"), *Type));
158 |     }
159 | }
160 | 
161 | TPair<AStaticMeshActor *, bool> FMCPCreateObjectHandler::CreateStaticMeshActor(UWorld *World, const FVector &Location, const FString &MeshPath, const FString &Label)
162 | {
163 |     if (!World)
164 |     {
165 |         return TPair<AStaticMeshActor *, bool>(nullptr, false);
166 |     }
167 | 
168 |     // Create the actor
169 |     FActorSpawnParameters SpawnParams;
170 |     SpawnParams.Name = NAME_None; // Auto-generate a name
171 |     SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
172 | 
173 |     AStaticMeshActor *NewActor = World->SpawnActor<AStaticMeshActor>(Location, FRotator::ZeroRotator, SpawnParams);
174 |     if (NewActor)
175 |     {
176 |         MCP_LOG_INFO("Created StaticMeshActor at location (%f, %f, %f)", Location.X, Location.Y, Location.Z);
177 | 
178 |         // Set mesh if specified
179 |         if (!MeshPath.IsEmpty())
180 |         {
181 |             UStaticMesh *Mesh = LoadObject<UStaticMesh>(nullptr, *MeshPath);
182 |             if (Mesh)
183 |             {
184 |                 NewActor->GetStaticMeshComponent()->SetStaticMesh(Mesh);
185 |                 MCP_LOG_INFO("Set mesh to %s", *MeshPath);
186 |             }
187 |             else
188 |             {
189 |                 MCP_LOG_WARNING("Failed to load mesh %s", *MeshPath);
190 |             }
191 |         }
192 | 
193 |         // Set a descriptive label
194 |         if (!Label.IsEmpty())
195 |         {
196 |             NewActor->SetActorLabel(Label);
197 |             MCP_LOG_INFO("Set custom label to %s", *Label);
198 |         }
199 |         else
200 |         {
201 |             NewActor->SetActorLabel(FString::Printf(TEXT("MCP_StaticMesh_%d"), FMath::RandRange(1000, 9999)));
202 |         }
203 | 
204 |         return TPair<AStaticMeshActor *, bool>(NewActor, true);
205 |     }
206 |     else
207 |     {
208 |         MCP_LOG_ERROR("Failed to create StaticMeshActor");
209 |         return TPair<AStaticMeshActor *, bool>(nullptr, false);
210 |     }
211 | }
212 | 
213 | TPair<AStaticMeshActor *, bool> FMCPCreateObjectHandler::CreateCubeActor(UWorld *World, const FVector &Location, const FString &Label)
214 | {
215 |     if (!World)
216 |     {
217 |         return TPair<AStaticMeshActor *, bool>(nullptr, false);
218 |     }
219 | 
220 |     // Create a StaticMeshActor with a cube mesh
221 |     FActorSpawnParameters SpawnParams;
222 |     SpawnParams.Name = NAME_None;
223 |     SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
224 | 
225 |     AStaticMeshActor *NewActor = World->SpawnActor<AStaticMeshActor>(Location, FRotator::ZeroRotator, SpawnParams);
226 |     if (NewActor)
227 |     {
228 |         MCP_LOG_INFO("Created Cube at location (%f, %f, %f)", Location.X, Location.Y, Location.Z);
229 | 
230 |         // Set cube mesh
231 |         UStaticMesh *CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
232 |         if (CubeMesh)
233 |         {
234 |             NewActor->GetStaticMeshComponent()->SetStaticMesh(CubeMesh);
235 |             MCP_LOG_INFO("Set cube mesh");
236 | 
237 |             // Set a descriptive label
238 |             if (!Label.IsEmpty())
239 |             {
240 |                 NewActor->SetActorLabel(Label);
241 |                 MCP_LOG_INFO("Set custom label to %s", *Label);
242 |             }
243 |             else
244 |             {
245 |                 NewActor->SetActorLabel(FString::Printf(TEXT("MCP_Cube_%d"), FMath::RandRange(1000, 9999)));
246 |             }
247 | 
248 |             return TPair<AStaticMeshActor *, bool>(NewActor, true);
249 |         }
250 |         else
251 |         {
252 |             MCP_LOG_WARNING("Failed to load cube mesh");
253 |             World->DestroyActor(NewActor);
254 |             return TPair<AStaticMeshActor *, bool>(nullptr, false);
255 |         }
256 |     }
257 |     else
258 |     {
259 |         MCP_LOG_ERROR("Failed to create Cube");
260 |         return TPair<AStaticMeshActor *, bool>(nullptr, false);
261 |     }
262 | }
263 | 
264 | //
265 | // FMCPModifyObjectHandler
266 | //
267 | TSharedPtr<FJsonObject> FMCPModifyObjectHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
268 | {
269 |     UWorld *World = GEditor->GetEditorWorldContext().World();
270 | 
271 |     FString ActorName;
272 |     if (!Params->TryGetStringField(FStringView(TEXT("name")), ActorName))
273 |     {
274 |         MCP_LOG_WARNING("Missing 'name' field in modify_object command");
275 |         return CreateErrorResponse("Missing 'name' field");
276 |     }
277 | 
278 |     AActor *Actor = nullptr;
279 |     for (TActorIterator<AActor> It(World); It; ++It)
280 |     {
281 |         if (It->GetName() == ActorName)
282 |         {
283 |             Actor = *It;
284 |             break;
285 |         }
286 |     }
287 | 
288 |     if (!Actor)
289 |     {
290 |         MCP_LOG_WARNING("Actor not found: %s", *ActorName);
291 |         return CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
292 |     }
293 | 
294 |     bool bModified = false;
295 | 
296 |     // Check for location update
297 |     const TArray<TSharedPtr<FJsonValue>> *LocationArrayPtr = nullptr;
298 |     if (Params->TryGetArrayField(FStringView(TEXT("location")), LocationArrayPtr) && LocationArrayPtr && LocationArrayPtr->Num() == 3)
299 |     {
300 |         FVector NewLocation(
301 |             (*LocationArrayPtr)[0]->AsNumber(),
302 |             (*LocationArrayPtr)[1]->AsNumber(),
303 |             (*LocationArrayPtr)[2]->AsNumber());
304 | 
305 |         Actor->SetActorLocation(NewLocation);
306 |         MCP_LOG_INFO("Updated location of %s to (%f, %f, %f)", *ActorName, NewLocation.X, NewLocation.Y, NewLocation.Z);
307 |         bModified = true;
308 |     }
309 | 
310 |     // Check for rotation update
311 |     const TArray<TSharedPtr<FJsonValue>> *RotationArrayPtr = nullptr;
312 |     if (Params->TryGetArrayField(FStringView(TEXT("rotation")), RotationArrayPtr) && RotationArrayPtr && RotationArrayPtr->Num() == 3)
313 |     {
314 |         FRotator NewRotation(
315 |             (*RotationArrayPtr)[0]->AsNumber(),
316 |             (*RotationArrayPtr)[1]->AsNumber(),
317 |             (*RotationArrayPtr)[2]->AsNumber());
318 | 
319 |         Actor->SetActorRotation(NewRotation);
320 |         MCP_LOG_INFO("Updated rotation of %s to (%f, %f, %f)", *ActorName, NewRotation.Pitch, NewRotation.Yaw, NewRotation.Roll);
321 |         bModified = true;
322 |     }
323 | 
324 |     // Check for scale update
325 |     const TArray<TSharedPtr<FJsonValue>> *ScaleArrayPtr = nullptr;
326 |     if (Params->TryGetArrayField(FStringView(TEXT("scale")), ScaleArrayPtr) && ScaleArrayPtr && ScaleArrayPtr->Num() == 3)
327 |     {
328 |         FVector NewScale(
329 |             (*ScaleArrayPtr)[0]->AsNumber(),
330 |             (*ScaleArrayPtr)[1]->AsNumber(),
331 |             (*ScaleArrayPtr)[2]->AsNumber());
332 | 
333 |         Actor->SetActorScale3D(NewScale);
334 |         MCP_LOG_INFO("Updated scale of %s to (%f, %f, %f)", *ActorName, NewScale.X, NewScale.Y, NewScale.Z);
335 |         bModified = true;
336 |     }
337 | 
338 |     if (bModified)
339 |     {
340 |         // Create a result object with the actor name
341 |         TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
342 |         Result->SetStringField("name", Actor->GetName());
343 | 
344 |         // Return success with the result object
345 |         return CreateSuccessResponse(Result);
346 |     }
347 |     else
348 |     {
349 |         MCP_LOG_WARNING("No modifications specified for %s", *ActorName);
350 |         TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
351 |         Response->SetStringField("status", "warning");
352 |         Response->SetStringField("message", "No modifications specified");
353 |         return Response;
354 |     }
355 | }
356 | 
357 | //
358 | // FMCPDeleteObjectHandler
359 | //
360 | TSharedPtr<FJsonObject> FMCPDeleteObjectHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
361 | {
362 |     UWorld *World = GEditor->GetEditorWorldContext().World();
363 | 
364 |     FString ActorName;
365 |     if (!Params->TryGetStringField(FStringView(TEXT("name")), ActorName))
366 |     {
367 |         MCP_LOG_WARNING("Missing 'name' field in delete_object command");
368 |         return CreateErrorResponse("Missing 'name' field");
369 |     }
370 | 
371 |     AActor *Actor = nullptr;
372 |     for (TActorIterator<AActor> It(World); It; ++It)
373 |     {
374 |         if (It->GetName() == ActorName)
375 |         {
376 |             Actor = *It;
377 |             break;
378 |         }
379 |     }
380 | 
381 |     if (!Actor)
382 |     {
383 |         MCP_LOG_WARNING("Actor not found: %s", *ActorName);
384 |         return CreateErrorResponse(FString::Printf(TEXT("Actor not found: %s"), *ActorName));
385 |     }
386 | 
387 |     // Check if the actor can be deleted
388 |     if (!FActorEditorUtils::IsABuilderBrush(Actor))
389 |     {
390 |         bool bDestroyed = World->DestroyActor(Actor);
391 |         if (bDestroyed)
392 |         {
393 |             MCP_LOG_INFO("Deleted actor: %s", *ActorName);
394 |             return CreateSuccessResponse();
395 |         }
396 |         else
397 |         {
398 |             MCP_LOG_ERROR("Failed to delete actor: %s", *ActorName);
399 |             return CreateErrorResponse(FString::Printf(TEXT("Failed to delete actor: %s"), *ActorName));
400 |         }
401 |     }
402 |     else
403 |     {
404 |         MCP_LOG_WARNING("Cannot delete special actor: %s", *ActorName);
405 |         return CreateErrorResponse(FString::Printf(TEXT("Cannot delete special actor: %s"), *ActorName));
406 |     }
407 | }
408 | 
409 | //
410 | // FMCPExecutePythonHandler
411 | //
412 | TSharedPtr<FJsonObject> FMCPExecutePythonHandler::Execute(const TSharedPtr<FJsonObject> &Params, FSocket *ClientSocket)
413 | {
414 |     // Check if we have code or file parameter
415 |     FString PythonCode;
416 |     FString PythonFile;
417 |     bool hasCode = Params->TryGetStringField(FStringView(TEXT("code")), PythonCode);
418 |     bool hasFile = Params->TryGetStringField(FStringView(TEXT("file")), PythonFile);
419 | 
420 |     // If code/file not found directly, check if they're in a 'data' object
421 |     if (!hasCode && !hasFile)
422 |     {
423 |         const TSharedPtr<FJsonObject> *DataObject;
424 |         if (Params->TryGetObjectField(FStringView(TEXT("data")), DataObject))
425 |         {
426 |             hasCode = (*DataObject)->TryGetStringField(FStringView(TEXT("code")), PythonCode);
427 |             hasFile = (*DataObject)->TryGetStringField(FStringView(TEXT("file")), PythonFile);
428 |         }
429 |     }
430 | 
431 |     if (!hasCode && !hasFile)
432 |     {
433 |         MCP_LOG_WARNING("Missing 'code' or 'file' field in execute_python command");
434 |         return CreateErrorResponse("Missing 'code' or 'file' field. You must provide either Python code or a file path.");
435 |     }
436 | 
437 |     FString Result;
438 |     bool bSuccess = false;
439 |     FString ErrorMessage;
440 | 
441 |     if (hasCode)
442 |     {
443 |         // For code execution, we'll create a temporary file and execute that
444 |         MCP_LOG_INFO("Executing Python code via temporary file");
445 | 
446 |         // Create a temporary file in the project's Saved/Temp directory
447 |         FString TempDir = FPaths::ProjectSavedDir() / MCPConstants::PYTHON_TEMP_DIR_NAME;
448 |         IPlatformFile &PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
449 | 
450 |         // Ensure the directory exists
451 |         if (!PlatformFile.DirectoryExists(*TempDir))
452 |         {
453 |             PlatformFile.CreateDirectory(*TempDir);
454 |         }
455 | 
456 |         // Create a unique filename for the temporary Python script
457 |         FString TempFilePath = TempDir / FString::Printf(TEXT("%s%s.py"), MCPConstants::PYTHON_TEMP_FILE_PREFIX, *FGuid::NewGuid().ToString());
458 | 
459 |         // Add error handling wrapper to the Python code
460 |         FString WrappedPythonCode = TEXT("import sys\n")
461 |                                         TEXT("import traceback\n")
462 |                                             TEXT("import unreal\n\n")
463 |                                                 TEXT("# Create output capture file\n")
464 |                                                     TEXT("output_file = open('") +
465 |                                     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")
466 |                                     // Instead of directly embedding the code, we'll compile it first to catch syntax errors
467 |                                     TEXT("    # Compile the code to catch syntax errors\n") TEXT("    user_code = '''") +
468 |                                     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");
469 | 
470 |         // Write the Python code to the temporary file
471 |         if (FFileHelper::SaveStringToFile(WrappedPythonCode, *TempFilePath))
472 |         {
473 |             // Execute the temporary file
474 |             FString Command = FString::Printf(TEXT("py \"%s\""), *TempFilePath);
475 |             GEngine->Exec(nullptr, *Command);
476 | 
477 |             // Read the output, error, and status files
478 |             FString OutputContent;
479 |             FString ErrorContent;
480 |             FString StatusContent;
481 | 
482 |             FFileHelper::LoadFileToString(OutputContent, *(TempDir / TEXT("output.txt")));
483 |             FFileHelper::LoadFileToString(ErrorContent, *(TempDir / TEXT("error.txt")));
484 |             FFileHelper::LoadFileToString(StatusContent, *(TempDir / TEXT("status.txt")));
485 | 
486 |             bSuccess = StatusContent.TrimStartAndEnd().Equals(TEXT("1"));
487 | 
488 |             // Combine output and error for the result
489 |             Result = OutputContent;
490 |             ErrorMessage = ErrorContent;
491 | 
492 |             // Clean up the temporary files
493 |             PlatformFile.DeleteFile(*TempFilePath);
494 |             PlatformFile.DeleteFile(*(TempDir / TEXT("output.txt")));
495 |             PlatformFile.DeleteFile(*(TempDir / TEXT("error.txt")));
496 |             PlatformFile.DeleteFile(*(TempDir / TEXT("status.txt")));
497 |         }
498 |         else
499 |         {
500 |             MCP_LOG_ERROR("Failed to create temporary Python file at %s", *TempFilePath);
501 |             return CreateErrorResponse(FString::Printf(TEXT("Failed to create temporary Python file at %s"), *TempFilePath));
502 |         }
503 |     }
504 |     else if (hasFile)
505 |     {
506 |         // Execute Python file
507 |         MCP_LOG_INFO("Executing Python file: %s", *PythonFile);
508 | 
509 |         // Create a temporary directory for output capture
510 |         FString TempDir = FPaths::ProjectSavedDir() / MCPConstants::PYTHON_TEMP_DIR_NAME;
511 |         IPlatformFile &PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
512 | 
513 |         // Ensure the directory exists
514 |         if (!PlatformFile.DirectoryExists(*TempDir))
515 |         {
516 |             PlatformFile.CreateDirectory(*TempDir);
517 |         }
518 | 
519 |         // Create a wrapper script that executes the file and captures output
520 |         FString WrapperFilePath = TempDir / FString::Printf(TEXT("%s_wrapper_%s.py"), MCPConstants::PYTHON_TEMP_FILE_PREFIX, *FGuid::NewGuid().ToString());
521 | 
522 |         FString WrapperCode = TEXT("import sys\n")
523 |                                   TEXT("import traceback\n")
524 |                                       TEXT("import unreal\n\n")
525 |                                           TEXT("# Create output capture file\n")
526 |                                               TEXT("output_file = open('") +
527 |                               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");
528 | 
529 |         if (FFileHelper::SaveStringToFile(WrapperCode, *WrapperFilePath))
530 |         {
531 |             // Execute the wrapper script
532 |             FString Command = FString::Printf(TEXT("py \"%s\""), *WrapperFilePath);
533 |             GEngine->Exec(nullptr, *Command);
534 | 
535 |             // Read the output, error, and status files
536 |             FString OutputContent;
537 |             FString ErrorContent;
538 |             FString StatusContent;
539 | 
540 |             FFileHelper::LoadFileToString(OutputContent, *(TempDir / TEXT("output.txt")));
541 |             FFileHelper::LoadFileToString(ErrorContent, *(TempDir / TEXT("error.txt")));
542 |             FFileHelper::LoadFileToString(StatusContent, *(TempDir / TEXT("status.txt")));
543 | 
544 |             bSuccess = StatusContent.TrimStartAndEnd().Equals(TEXT("1"));
545 | 
546 |             // Combine output and error for the result
547 |             Result = OutputContent;
548 |             ErrorMessage = ErrorContent;
549 | 
550 |             // Clean up the temporary files
551 |             PlatformFile.DeleteFile(*WrapperFilePath);
552 |             PlatformFile.DeleteFile(*(TempDir / TEXT("output.txt")));
553 |             PlatformFile.DeleteFile(*(TempDir / TEXT("error.txt")));
554 |             PlatformFile.DeleteFile(*(TempDir / TEXT("status.txt")));
555 |         }
556 |         else
557 |         {
558 |             MCP_LOG_ERROR("Failed to create wrapper Python file at %s", *WrapperFilePath);
559 |             return CreateErrorResponse(FString::Printf(TEXT("Failed to create wrapper Python file at %s"), *WrapperFilePath));
560 |         }
561 |     }
562 | 
563 |     // Create the response
564 |     TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
565 |     ResultObj->SetStringField("output", Result);
566 | 
567 |     if (bSuccess)
568 |     {
569 |         MCP_LOG_INFO("Python execution successful");
570 |         return CreateSuccessResponse(ResultObj);
571 |     }
572 |     else
573 |     {
574 |         MCP_LOG_ERROR("Python execution failed: %s", *ErrorMessage);
575 |         ResultObj->SetStringField("error", ErrorMessage);
576 | 
577 |         // We're returning a success response with error details rather than an error response
578 |         // This allows the client to still access the output and error information
579 |         TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
580 |         Response->SetStringField("status", "error");
581 |         Response->SetStringField("message", "Python execution failed with errors");
582 |         Response->SetObjectField("result", ResultObj);
583 |         return Response;
584 |     }
585 | }
```

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

```cpp
  1 | #include "MCPCommandHandlers_Blueprints.h"
  2 | #include "MCPFileLogger.h"
  3 | #include "Editor.h"
  4 | #include "EngineUtils.h"
  5 | #include "Kismet2/BlueprintEditorUtils.h"
  6 | #include "Kismet2/KismetEditorUtilities.h"
  7 | #include "UObject/SavePackage.h"
  8 | 
  9 | 
 10 | //
 11 | // FMCPBlueprintUtils
 12 | //
 13 | TPair<UBlueprint*, bool> FMCPBlueprintUtils::CreateBlueprintAsset(
 14 |     const FString& PackagePath,
 15 |     const FString& BlueprintName,
 16 |     UClass* ParentClass)
 17 | {
 18 |     // Print debug information about the paths
 19 |     FString GameContentDir = FPaths::ProjectContentDir();
 20 |     FString PluginContentDir = FPaths::EnginePluginsDir() / TEXT("UnrealMCP") / TEXT("Content");
 21 |     
 22 |     // Create the full path for the blueprint
 23 |     FString FullPackagePath = FString::Printf(TEXT("%s/%s"), *PackagePath, *BlueprintName);
 24 |     
 25 |     // Get the file paths
 26 |     FString DirectoryPath = FPackageName::LongPackageNameToFilename(PackagePath, TEXT(""));
 27 |     FString PackageFileName = FPackageName::LongPackageNameToFilename(FullPackagePath, FPackageName::GetAssetPackageExtension());
 28 |     
 29 |     MCP_LOG_INFO("Creating blueprint asset:");
 30 |     MCP_LOG_INFO("  Package Path: %s", *PackagePath);
 31 |     MCP_LOG_INFO("  Blueprint Name: %s", *BlueprintName);
 32 |     MCP_LOG_INFO("  Full Package Path: %s", *FullPackagePath);
 33 |     MCP_LOG_INFO("  Directory Path: %s", *DirectoryPath);
 34 |     MCP_LOG_INFO("  Package File Name: %s", *PackageFileName);
 35 |     MCP_LOG_INFO("  Game Content Dir: %s", *GameContentDir);
 36 |     MCP_LOG_INFO("  Plugin Content Dir: %s", *PluginContentDir);
 37 |     
 38 |     // Additional logging for debugging
 39 |     FString AbsoluteGameDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
 40 |     FString AbsoluteContentDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());
 41 |     FString AbsolutePackagePath = FPaths::ConvertRelativePathToFull(PackageFileName);
 42 |     
 43 |     MCP_LOG_INFO("  Absolute Game Dir: %s", *AbsoluteGameDir);
 44 |     MCP_LOG_INFO("  Absolute Content Dir: %s", *AbsoluteContentDir);
 45 |     MCP_LOG_INFO("  Absolute Package Path: %s", *AbsolutePackagePath);
 46 |     
 47 |     // Ensure the directory exists
 48 |     IFileManager::Get().MakeDirectory(*DirectoryPath, true);
 49 |     
 50 |     // Verify directory was created
 51 |     if (IFileManager::Get().DirectoryExists(*DirectoryPath))
 52 |     {
 53 |         MCP_LOG_INFO("  Directory exists or was created successfully: %s", *DirectoryPath);
 54 |     }
 55 |     else
 56 |     {
 57 |         MCP_LOG_ERROR("  Failed to create directory: %s", *DirectoryPath);
 58 |     }
 59 | 
 60 |     // Check if a blueprint with this name already exists in the package
 61 |     UBlueprint* ExistingBlueprint = LoadObject<UBlueprint>(nullptr, *FullPackagePath);
 62 |     if (ExistingBlueprint)
 63 |     {
 64 |         MCP_LOG_WARNING("Blueprint already exists at path: %s", *FullPackagePath);
 65 |         return TPair<UBlueprint*, bool>(ExistingBlueprint, true);
 66 |     }
 67 | 
 68 |     // Create or load the package for the full path
 69 |     UPackage* Package = CreatePackage(*FullPackagePath);
 70 |     if (!Package)
 71 |     {
 72 |         MCP_LOG_ERROR("Failed to create package for blueprint");
 73 |         return TPair<UBlueprint*, bool>(nullptr, false);
 74 |     }
 75 | 
 76 |     Package->FullyLoad();
 77 | 
 78 |     // Create the Blueprint
 79 |     UBlueprint* NewBlueprint = nullptr;
 80 |     
 81 |     // Use a try-catch block to handle potential errors in CreateBlueprint
 82 |     try
 83 |     {
 84 |         NewBlueprint = FKismetEditorUtilities::CreateBlueprint(
 85 |             ParentClass,
 86 |             Package,
 87 |             FName(*BlueprintName),
 88 |             BPTYPE_Normal,
 89 |             UBlueprint::StaticClass(),
 90 |             UBlueprintGeneratedClass::StaticClass()
 91 |         );
 92 |     }
 93 |     catch (const std::exception& e)
 94 |     {
 95 |         MCP_LOG_ERROR("Exception while creating blueprint: %s", ANSI_TO_TCHAR(e.what()));
 96 |         return TPair<UBlueprint*, bool>(nullptr, false);
 97 |     }
 98 |     catch (...)
 99 |     {
100 |         MCP_LOG_ERROR("Unknown exception while creating blueprint");
101 |         return TPair<UBlueprint*, bool>(nullptr, false);
102 |     }
103 | 
104 |     if (!NewBlueprint)
105 |     {
106 |         MCP_LOG_ERROR("Failed to create blueprint");
107 |         return TPair<UBlueprint*, bool>(nullptr, false);
108 |     }
109 | 
110 |     // Save the package
111 |     Package->MarkPackageDirty();
112 |     MCP_LOG_INFO("  Saving package to: %s", *PackageFileName);
113 |     
114 |     // Use the new SavePackage API
115 |     FSavePackageArgs SaveArgs;
116 |     SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
117 |     SaveArgs.SaveFlags = SAVE_NoError;
118 |     bool bSaveSuccess = UPackage::SavePackage(Package, NewBlueprint, *PackageFileName, SaveArgs);
119 |     
120 |     if (bSaveSuccess)
121 |     {
122 |         MCP_LOG_INFO("  Package saved successfully to: %s", *PackageFileName);
123 |         
124 |         // Check if the file actually exists
125 |         if (IFileManager::Get().FileExists(*PackageFileName))
126 |         {
127 |             MCP_LOG_INFO("  File exists at: %s", *PackageFileName);
128 |         }
129 |         else
130 |         {
131 |             MCP_LOG_ERROR("  File does NOT exist at: %s", *PackageFileName);
132 |         }
133 |     }
134 |     else
135 |     {
136 |         MCP_LOG_ERROR("  Failed to save package to: %s", *PackageFileName);
137 |     }
138 | 
139 |     // Notify the asset registry
140 |     FAssetRegistryModule::AssetCreated(NewBlueprint);
141 | 
142 |     return TPair<UBlueprint*, bool>(NewBlueprint, true);
143 | }
144 | 
145 | TPair<UK2Node_Event*, bool> FMCPBlueprintUtils::AddEventNode(
146 |     UBlueprint* Blueprint,
147 |     const FString& EventName,
148 |     UClass* ParentClass)
149 | {
150 |     if (!Blueprint)
151 |     {
152 |         return TPair<UK2Node_Event*, bool>(nullptr, false);
153 |     }
154 | 
155 |     // Find or create the event graph
156 |     UEdGraph* EventGraph = FBlueprintEditorUtils::FindEventGraph(Blueprint);
157 |     if (!EventGraph)
158 |     {
159 |         EventGraph = FBlueprintEditorUtils::CreateNewGraph(
160 |             Blueprint,
161 |             FName("EventGraph"),
162 |             UEdGraph::StaticClass(),
163 |             UEdGraphSchema_K2::StaticClass()
164 |         );
165 |         Blueprint->UbergraphPages.Add(EventGraph);
166 |     }
167 | 
168 |     // Create the custom event node
169 |     UK2Node_Event* EventNode = NewObject<UK2Node_Event>(EventGraph);
170 |     EventNode->EventReference.SetExternalMember(FName(*EventName), ParentClass);
171 |     EventNode->bOverrideFunction = true;
172 |     EventNode->AllocateDefaultPins();
173 |     EventGraph->Nodes.Add(EventNode);
174 | 
175 |     return TPair<UK2Node_Event*, bool>(EventNode, true);
176 | }
177 | 
178 | TPair<UK2Node_CallFunction*, bool> FMCPBlueprintUtils::AddPrintStringNode(
179 |     UEdGraph* Graph,
180 |     const FString& Message)
181 | {
182 |     if (!Graph)
183 |     {
184 |         return TPair<UK2Node_CallFunction*, bool>(nullptr, false);
185 |     }
186 | 
187 |     // Create print string node
188 |     UK2Node_CallFunction* PrintNode = NewObject<UK2Node_CallFunction>(Graph);
189 |     PrintNode->FunctionReference.SetExternalMember(FName("PrintString"), UKismetSystemLibrary::StaticClass());
190 |     PrintNode->AllocateDefaultPins();
191 |     Graph->Nodes.Add(PrintNode);
192 | 
193 |     // Set the string input
194 |     UEdGraphPin* StringPin = PrintNode->FindPinChecked(FName("InString"));
195 |     StringPin->DefaultValue = Message;
196 | 
197 |     return TPair<UK2Node_CallFunction*, bool>(PrintNode, true);
198 | }
199 | 
200 | //
201 | // FMCPCreateBlueprintHandler
202 | //
203 | TSharedPtr<FJsonObject> FMCPCreateBlueprintHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
204 | {
205 |     MCP_LOG_INFO("Handling create_blueprint command");
206 | 
207 |     FString PackagePath;
208 |     if (!Params->TryGetStringField(TEXT("package_path"), PackagePath))
209 |     {
210 |         MCP_LOG_WARNING("Missing 'package_path' field in create_blueprint command");
211 |         return CreateErrorResponse("Missing 'package_path' field");
212 |     }
213 | 
214 |     FString BlueprintName;
215 |     if (!Params->TryGetStringField(TEXT("name"), BlueprintName))
216 |     {
217 |         MCP_LOG_WARNING("Missing 'name' field in create_blueprint command");
218 |         return CreateErrorResponse("Missing 'name' field");
219 |     }
220 | 
221 |     // Get optional properties
222 |     const TSharedPtr<FJsonObject>* Properties = nullptr;
223 |     Params->TryGetObjectField(TEXT("properties"), Properties);
224 | 
225 |     // Create the blueprint
226 |     TPair<UBlueprint*, bool> Result = CreateBlueprint(PackagePath, BlueprintName, Properties ? *Properties : nullptr);
227 | 
228 |     if (Result.Value)
229 |     {
230 |         TSharedPtr<FJsonObject> ResultObj = MakeShared<FJsonObject>();
231 |         ResultObj->SetStringField("name", Result.Key->GetName());
232 |         ResultObj->SetStringField("path", Result.Key->GetPathName());
233 |         return CreateSuccessResponse(ResultObj);
234 |     }
235 |     else
236 |     {
237 |         return CreateErrorResponse("Failed to create blueprint");
238 |     }
239 | }
240 | 
241 | TPair<UBlueprint*, bool> FMCPCreateBlueprintHandler::CreateBlueprint(
242 |     const FString& PackagePath,
243 |     const FString& BlueprintName,
244 |     const TSharedPtr<FJsonObject>& Properties)
245 | {
246 |     // Ensure the package path is correctly formatted
247 |     // We need to create a proper directory structure
248 |     FString DirectoryPath;
249 |     FString AssetName;
250 |     
251 |     // Create a proper directory structure
252 |     // For example, if PackagePath is "/Game/Blueprints" and BlueprintName is "TestBlueprint",
253 |     // we want to create a directory at "/Game/Blueprints" and place "TestBlueprint" inside it
254 |     DirectoryPath = PackagePath;
255 |     AssetName = BlueprintName;
256 |     
257 |     // Create the full path for the blueprint
258 |     FString FullPackagePath = FString::Printf(TEXT("%s/%s"), *DirectoryPath, *AssetName);
259 |     MCP_LOG_INFO("Creating blueprint at path: %s", *FullPackagePath);
260 |     
261 |     // Check if a blueprint with this name already exists
262 |     UBlueprint* ExistingBlueprint = LoadObject<UBlueprint>(nullptr, *FullPackagePath);
263 |     if (ExistingBlueprint)
264 |     {
265 |         MCP_LOG_WARNING("Blueprint already exists at path: %s", *FullPackagePath);
266 |         return TPair<UBlueprint*, bool>(ExistingBlueprint, true);
267 |     }
268 | 
269 |     // Default to Actor as parent class
270 |     UClass* ParentClass = AActor::StaticClass();
271 | 
272 |     // Check if a different parent class is specified
273 |     if (Properties.IsValid())
274 |     {
275 |         FString ParentClassName;
276 |         if (Properties->TryGetStringField(TEXT("parent_class"), ParentClassName))
277 |         {
278 |             // First try to find the class using its full path
279 |             UClass* FoundClass = LoadObject<UClass>(nullptr, *ParentClassName);
280 |             
281 |             // If not found with direct path, try to find it in common class paths
282 |             if (!FoundClass)
283 |             {
284 |                 // Try with /Script/Engine path (for engine classes)
285 |                 FString EnginePath = FString::Printf(TEXT("/Script/Engine.%s"), *ParentClassName);
286 |                 FoundClass = LoadObject<UClass>(nullptr, *EnginePath);
287 |                 
288 |                 // If still not found, try with game's path
289 |                 if (!FoundClass)
290 |                 {
291 |                     FString GamePath = FString::Printf(TEXT("/Script/%s.%s"), 
292 |                         FApp::GetProjectName(), 
293 |                         *ParentClassName);
294 |                     FoundClass = LoadObject<UClass>(nullptr, *GamePath);
295 |                 }
296 |             }
297 | 
298 |             if (FoundClass)
299 |             {
300 |                 ParentClass = FoundClass;
301 |             }
302 |             else
303 |             {
304 |                 MCP_LOG_WARNING("Could not find parent class '%s', using default Actor class", *ParentClassName);
305 |             }
306 |         }
307 |     }
308 | 
309 |     // Create the blueprint directly in the specified directory
310 |     return FMCPBlueprintUtils::CreateBlueprintAsset(DirectoryPath, AssetName, ParentClass);
311 | }
312 | 
313 | //
314 | // FMCPModifyBlueprintHandler
315 | //
316 | TSharedPtr<FJsonObject> FMCPModifyBlueprintHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
317 | {
318 |     MCP_LOG_INFO("Handling modify_blueprint command");
319 | 
320 |     FString BlueprintPath;
321 |     if (!Params->TryGetStringField(TEXT("blueprint_path"), BlueprintPath))
322 |     {
323 |         MCP_LOG_WARNING("Missing 'blueprint_path' field in modify_blueprint command");
324 |         return CreateErrorResponse("Missing 'blueprint_path' field");
325 |     }
326 | 
327 |     UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *BlueprintPath);
328 |     if (!Blueprint)
329 |     {
330 |         return CreateErrorResponse(FString::Printf(TEXT("Failed to load blueprint at path: %s"), *BlueprintPath));
331 |     }
332 | 
333 |     // Get properties to modify
334 |     const TSharedPtr<FJsonObject>* Properties = nullptr;
335 |     if (!Params->TryGetObjectField(TEXT("properties"), Properties) || !Properties)
336 |     {
337 |         return CreateErrorResponse("Missing 'properties' field");
338 |     }
339 | 
340 |     if (ModifyBlueprint(Blueprint, *Properties))
341 |     {
342 |         return CreateSuccessResponse();
343 |     }
344 |     else
345 |     {
346 |         return CreateErrorResponse("Failed to modify blueprint");
347 |     }
348 | }
349 | 
350 | bool FMCPModifyBlueprintHandler::ModifyBlueprint(UBlueprint* Blueprint, const TSharedPtr<FJsonObject>& Properties)
351 | {
352 |     if (!Blueprint || !Properties.IsValid())
353 |     {
354 |         return false;
355 |     }
356 | 
357 |     bool bModified = false;
358 | 
359 |     // Handle blueprint description
360 |     FString Description;
361 |     if (Properties->TryGetStringField(TEXT("description"), Description))
362 |     {
363 |         Blueprint->BlueprintDescription = Description;
364 |         bModified = true;
365 |     }
366 | 
367 |     // Handle blueprint category
368 |     FString Category;
369 |     if (Properties->TryGetStringField(TEXT("category"), Category))
370 |     {
371 |         Blueprint->BlueprintCategory = Category;
372 |         bModified = true;
373 |     }
374 | 
375 |     // Handle parent class change
376 |     FString ParentClassName;
377 |     if (Properties->TryGetStringField(TEXT("parent_class"), ParentClassName))
378 |     {
379 |         // First try to find the class using its full path
380 |         UClass* FoundClass = LoadObject<UClass>(nullptr, *ParentClassName);
381 |         
382 |         // If not found with direct path, try to find it in common class paths
383 |         if (!FoundClass)
384 |         {
385 |             // Try with /Script/Engine path (for engine classes)
386 |             FString EnginePath = FString::Printf(TEXT("/Script/Engine.%s"), *ParentClassName);
387 |             FoundClass = LoadObject<UClass>(nullptr, *EnginePath);
388 |             
389 |             // If still not found, try with game's path
390 |             if (!FoundClass)
391 |             {
392 |                 FString GamePath = FString::Printf(TEXT("/Script/%s.%s"), 
393 |                     FApp::GetProjectName(), 
394 |                     *ParentClassName);
395 |                 FoundClass = LoadObject<UClass>(nullptr, *GamePath);
396 |             }
397 |         }
398 | 
399 |         if (FoundClass)
400 |         {
401 |             Blueprint->ParentClass = FoundClass;
402 |             bModified = true;
403 |         }
404 |         else
405 |         {
406 |             MCP_LOG_WARNING("Could not find parent class '%s' for blueprint modification", *ParentClassName);
407 |         }
408 |     }
409 | 
410 |     // Handle additional categories to hide
411 |     const TSharedPtr<FJsonObject>* Options = nullptr;
412 |     if (Properties->TryGetObjectField(TEXT("options"), Options) && Options)
413 |     {
414 |         // Handle hide categories
415 |         const TArray<TSharedPtr<FJsonValue>>* HideCategories = nullptr;
416 |         if ((*Options)->TryGetArrayField(TEXT("hide_categories"), HideCategories) && HideCategories)
417 |         {
418 |             for (const TSharedPtr<FJsonValue>& Value : *HideCategories)
419 |             {
420 |                 FString CategoryName;
421 |                 if (Value->TryGetString(CategoryName) && !CategoryName.IsEmpty())
422 |                 {
423 |                     Blueprint->HideCategories.AddUnique(CategoryName);
424 |                     bModified = true;
425 |                 }
426 |             }
427 |         }
428 |         
429 |         // Handle namespace
430 |         FString Namespace;
431 |         if ((*Options)->TryGetStringField(TEXT("namespace"), Namespace))
432 |         {
433 |             Blueprint->BlueprintNamespace = Namespace;
434 |             bModified = true;
435 |         }
436 |         
437 |         // Handle display name
438 |         FString DisplayName;
439 |         if ((*Options)->TryGetStringField(TEXT("display_name"), DisplayName))
440 |         {
441 |             Blueprint->BlueprintDisplayName = DisplayName;
442 |             bModified = true;
443 |         }
444 |         
445 |         // Handle compile mode
446 |         FString CompileMode;
447 |         if ((*Options)->TryGetStringField(TEXT("compile_mode"), CompileMode))
448 |         {
449 |             if (CompileMode.Equals(TEXT("Default"), ESearchCase::IgnoreCase))
450 |             {
451 |                 Blueprint->CompileMode = EBlueprintCompileMode::Default;
452 |                 bModified = true;
453 |             }
454 |             else if (CompileMode.Equals(TEXT("Development"), ESearchCase::IgnoreCase))
455 |             {
456 |                 Blueprint->CompileMode = EBlueprintCompileMode::Development;
457 |                 bModified = true;
458 |             }
459 |             else if (CompileMode.Equals(TEXT("FinalRelease"), ESearchCase::IgnoreCase))
460 |             {
461 |                 Blueprint->CompileMode = EBlueprintCompileMode::FinalRelease;
462 |                 bModified = true;
463 |             }
464 |         }
465 |         
466 |         // Handle class options
467 |         bool bGenerateAbstractClass = false;
468 |         if ((*Options)->TryGetBoolField(TEXT("abstract_class"), bGenerateAbstractClass))
469 |         {
470 |             Blueprint->bGenerateAbstractClass = bGenerateAbstractClass;
471 |             bModified = true;
472 |         }
473 |         
474 |         bool bGenerateConstClass = false;
475 |         if ((*Options)->TryGetBoolField(TEXT("const_class"), bGenerateConstClass))
476 |         {
477 |             Blueprint->bGenerateConstClass = bGenerateConstClass;
478 |             bModified = true;
479 |         }
480 |         
481 |         bool bDeprecate = false;
482 |         if ((*Options)->TryGetBoolField(TEXT("deprecate"), bDeprecate))
483 |         {
484 |             Blueprint->bDeprecate = bDeprecate;
485 |             bModified = true;
486 |         }
487 |     }
488 | 
489 |     if (bModified)
490 |     {
491 |         // Mark the package as dirty
492 |         Blueprint->MarkPackageDirty();
493 |         
494 |         // Recompile the blueprint if it was modified
495 |         FKismetEditorUtilities::CompileBlueprint(Blueprint);
496 |         
497 |         // Save the package
498 |         UPackage* Package = Blueprint->GetOutermost();
499 |         if (Package)
500 |         {
501 |             FString PackagePath = Package->GetName();
502 |             FString SavePackageFileName = FPackageName::LongPackageNameToFilename(
503 |                 PackagePath, 
504 |                 FPackageName::GetAssetPackageExtension()
505 |             );
506 |             
507 |             // Use the new SavePackage API
508 |             FSavePackageArgs SaveArgs;
509 |             SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
510 |             SaveArgs.SaveFlags = SAVE_NoError;
511 |             UPackage::SavePackage(Package, Blueprint, *SavePackageFileName, SaveArgs);
512 |         }
513 |     }
514 | 
515 |     return bModified;
516 | }
517 | 
518 | //
519 | // FMCPGetBlueprintInfoHandler
520 | //
521 | TSharedPtr<FJsonObject> FMCPGetBlueprintInfoHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
522 | {
523 |     MCP_LOG_INFO("Handling get_blueprint_info command");
524 | 
525 |     FString BlueprintPath;
526 |     if (!Params->TryGetStringField(TEXT("blueprint_path"), BlueprintPath))
527 |     {
528 |         MCP_LOG_WARNING("Missing 'blueprint_path' field in get_blueprint_info command");
529 |         return CreateErrorResponse("Missing 'blueprint_path' field");
530 |     }
531 | 
532 |     UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *BlueprintPath);
533 |     if (!Blueprint)
534 |     {
535 |         return CreateErrorResponse(FString::Printf(TEXT("Failed to load blueprint at path: %s"), *BlueprintPath));
536 |     }
537 | 
538 |     return CreateSuccessResponse(GetBlueprintInfo(Blueprint));
539 | }
540 | 
541 | TSharedPtr<FJsonObject> FMCPGetBlueprintInfoHandler::GetBlueprintInfo(UBlueprint* Blueprint)
542 | {
543 |     TSharedPtr<FJsonObject> Info = MakeShared<FJsonObject>();
544 |     if (!Blueprint)
545 |     {
546 |         return Info;
547 |     }
548 | 
549 |     Info->SetStringField("name", Blueprint->GetName());
550 |     Info->SetStringField("path", Blueprint->GetPathName());
551 |     Info->SetStringField("parent_class", Blueprint->ParentClass ? Blueprint->ParentClass->GetName() : TEXT("None"));
552 |     
553 |     // Add blueprint-specific properties
554 |     Info->SetStringField("category", Blueprint->BlueprintCategory);
555 |     Info->SetStringField("description", Blueprint->BlueprintDescription);
556 |     Info->SetStringField("display_name", Blueprint->BlueprintDisplayName);
557 |     Info->SetStringField("namespace", Blueprint->BlueprintNamespace);
558 |     
559 |     // Add blueprint type
560 |     FString BlueprintTypeStr;
561 |     switch (Blueprint->BlueprintType)
562 |     {
563 |     case BPTYPE_Normal:
564 |         BlueprintTypeStr = TEXT("Normal");
565 |         break;
566 |     case BPTYPE_Const:
567 |         BlueprintTypeStr = TEXT("Const");
568 |         break;
569 |     case BPTYPE_MacroLibrary:
570 |         BlueprintTypeStr = TEXT("MacroLibrary");
571 |         break;
572 |     case BPTYPE_Interface:
573 |         BlueprintTypeStr = TEXT("Interface");
574 |         break;
575 |     case BPTYPE_LevelScript:
576 |         BlueprintTypeStr = TEXT("LevelScript");
577 |         break;
578 |     case BPTYPE_FunctionLibrary:
579 |         BlueprintTypeStr = TEXT("FunctionLibrary");
580 |         break;
581 |     default:
582 |         BlueprintTypeStr = TEXT("Unknown");
583 |         break;
584 |     }
585 |     Info->SetStringField("blueprint_type", BlueprintTypeStr);
586 |     
587 |     // Add class options
588 |     TSharedPtr<FJsonObject> ClassOptions = MakeShared<FJsonObject>();
589 |     ClassOptions->SetBoolField("abstract_class", Blueprint->bGenerateAbstractClass);
590 |     ClassOptions->SetBoolField("const_class", Blueprint->bGenerateConstClass);
591 |     ClassOptions->SetBoolField("deprecated", Blueprint->bDeprecate);
592 |     
593 |     // Add compile mode
594 |     FString CompileModeStr;
595 |     switch (Blueprint->CompileMode)
596 |     {
597 |     case EBlueprintCompileMode::Default:
598 |         CompileModeStr = TEXT("Default");
599 |         break;
600 |     case EBlueprintCompileMode::Development:
601 |         CompileModeStr = TEXT("Development");
602 |         break;
603 |     case EBlueprintCompileMode::FinalRelease:
604 |         CompileModeStr = TEXT("FinalRelease");
605 |         break;
606 |     default:
607 |         CompileModeStr = TEXT("Unknown");
608 |         break;
609 |     }
610 |     ClassOptions->SetStringField("compile_mode", CompileModeStr);
611 |     
612 |     // Add hide categories
613 |     TArray<TSharedPtr<FJsonValue>> HideCategories;
614 |     for (const FString& Category : Blueprint->HideCategories)
615 |     {
616 |         HideCategories.Add(MakeShared<FJsonValueString>(Category));
617 |     }
618 |     ClassOptions->SetArrayField("hide_categories", HideCategories);
619 |     
620 |     Info->SetObjectField("class_options", ClassOptions);
621 | 
622 |     // Add information about functions
623 |     TArray<TSharedPtr<FJsonValue>> Functions;
624 |     for (UEdGraph* FuncGraph : Blueprint->FunctionGraphs)
625 |     {
626 |         TSharedPtr<FJsonObject> FuncInfo = MakeShared<FJsonObject>();
627 |         FuncInfo->SetStringField("name", FuncGraph->GetName());
628 |         Functions.Add(MakeShared<FJsonValueObject>(FuncInfo));
629 |     }
630 |     Info->SetArrayField("functions", Functions);
631 | 
632 |     // Add information about events
633 |     TArray<TSharedPtr<FJsonValue>> Events;
634 |     UEdGraph* EventGraph = FBlueprintEditorUtils::FindEventGraph(Blueprint);
635 |     if (EventGraph)
636 |     {
637 |         for (UEdGraphNode* Node : EventGraph->Nodes)
638 |         {
639 |             if (UK2Node_Event* EventNode = Cast<UK2Node_Event>(Node))
640 |             {
641 |                 TSharedPtr<FJsonObject> EventInfo = MakeShared<FJsonObject>();
642 |                 EventInfo->SetStringField("name", EventNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
643 |                 Events.Add(MakeShared<FJsonValueObject>(EventInfo));
644 |             }
645 |         }
646 |     }
647 |     Info->SetArrayField("events", Events);
648 | 
649 |     return Info;
650 | }
651 | 
652 | //
653 | // FMCPCreateBlueprintEventHandler
654 | //
655 | TSharedPtr<FJsonObject> FMCPCreateBlueprintEventHandler::Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
656 | {
657 |     UWorld* World = GEditor->GetEditorWorldContext().World();
658 |     if (!World)
659 |     {
660 |         return CreateErrorResponse("Invalid World context");
661 |     }
662 | 
663 |     // Get event name
664 |     FString EventName;
665 |     if (!Params->TryGetStringField(TEXT("event_name"), EventName))
666 |     {
667 |         return CreateErrorResponse("Missing 'event_name' field");
668 |     }
669 | 
670 |     // Get blueprint path
671 |     FString BlueprintPath;
672 |     if (!Params->TryGetStringField(TEXT("blueprint_path"), BlueprintPath))
673 |     {
674 |         // If no blueprint path is provided, create a new blueprint
675 |         BlueprintPath = FString::Printf(TEXT("/Game/GeneratedBlueprints/BP_MCP_%s"), *EventName);
676 |     }
677 | 
678 |     // Get optional event parameters
679 |     const TSharedPtr<FJsonObject>* EventParamsPtr = nullptr;
680 |     Params->TryGetObjectField(TEXT("parameters"), EventParamsPtr);
681 |     TSharedPtr<FJsonObject> EventParams = EventParamsPtr ? *EventParamsPtr : nullptr;
682 | 
683 |     // Create the blueprint event
684 |     TPair<bool, TSharedPtr<FJsonObject>> Result = CreateBlueprintEvent(World, EventName, BlueprintPath, EventParams);
685 |     
686 |     if (Result.Key)
687 |     {
688 |         return CreateSuccessResponse(Result.Value);
689 |     }
690 |     else
691 |     {
692 |         return CreateErrorResponse("Failed to create blueprint event");
693 |     }
694 | }
695 | 
696 | TPair<bool, TSharedPtr<FJsonObject>> FMCPCreateBlueprintEventHandler::CreateBlueprintEvent(
697 |     UWorld* World,
698 |     const FString& EventName,
699 |     const FString& BlueprintPath,
700 |     const TSharedPtr<FJsonObject>& EventParameters)
701 | {
702 |     TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
703 | 
704 |     // Try to load existing blueprint or create new one
705 |     UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *BlueprintPath);
706 |     if (!Blueprint)
707 |     {
708 |         // Create new blueprint
709 |         FString PackagePath = FPackageName::GetLongPackagePath(BlueprintPath);
710 |         FString BlueprintName = FPackageName::GetShortName(BlueprintPath);
711 |         
712 |         TPair<UBlueprint*, bool> BlueprintResult = FMCPBlueprintUtils::CreateBlueprintAsset(PackagePath, BlueprintName, AActor::StaticClass());
713 |         if (!BlueprintResult.Value || !BlueprintResult.Key)
714 |         {
715 |             MCP_LOG_ERROR("Failed to create blueprint asset");
716 |             return TPair<bool, TSharedPtr<FJsonObject>>(false, nullptr);
717 |         }
718 |         Blueprint = BlueprintResult.Key;
719 |     }
720 | 
721 |     // Add the event node
722 |     TPair<UK2Node_Event*, bool> EventNodeResult = FMCPBlueprintUtils::AddEventNode(Blueprint, EventName, AActor::StaticClass());
723 |     if (!EventNodeResult.Value || !EventNodeResult.Key)
724 |     {
725 |         MCP_LOG_ERROR("Failed to add event node");
726 |         return TPair<bool, TSharedPtr<FJsonObject>>(false, nullptr);
727 |     }
728 | 
729 |     // Add a print string node for testing
730 |     UEdGraph* EventGraph = FBlueprintEditorUtils::FindEventGraph(Blueprint);
731 |     if (EventGraph)
732 |     {
733 |         TPair<UK2Node_CallFunction*, bool> PrintNodeResult = FMCPBlueprintUtils::AddPrintStringNode(
734 |             EventGraph,
735 |             FString::Printf(TEXT("Event '%s' triggered!"), *EventName)
736 |         );
737 | 
738 |         if (PrintNodeResult.Value && PrintNodeResult.Key)
739 |         {
740 |             // Connect the event to the print node
741 |             UEdGraphPin* EventThenPin = EventNodeResult.Key->FindPinChecked(UEdGraphSchema_K2::PN_Then);
742 |             UEdGraphPin* PrintExecPin = PrintNodeResult.Key->FindPinChecked(UEdGraphSchema_K2::PN_Execute);
743 |             EventGraph->GetSchema()->TryCreateConnection(EventThenPin, PrintExecPin);
744 |         }
745 |     }
746 | 
747 |     // Compile and save the blueprint
748 |     FKismetEditorUtilities::CompileBlueprint(Blueprint);
749 |     
750 |     Result->SetStringField("blueprint", Blueprint->GetName());
751 |     Result->SetStringField("event", EventName);
752 |     Result->SetStringField("path", Blueprint->GetPathName());
753 |     
754 |     return TPair<bool, TSharedPtr<FJsonObject>>(true, Result);
755 | } 
```
Page 2/2FirstPrevNextLast