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