This is page 1 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
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Unreal Engine Plugin Ignores
2 | Binaries/
3 | Intermediate/
4 | DerivedDataCache/
5 | Saved/
6 | Logs/
7 | *.sln
8 | *.vcxproj
9 | *.vcxproj.filters
10 | *.vcxproj.user
11 | *.pdb
12 |
13 | # Python modules
14 | MCP/python_modules/
15 | MCP/__pycache__/
16 | MCP/Commands/__pycache__/
17 | **/__pycache__/
18 | *.py[cod]
19 | *$py.class
```
--------------------------------------------------------------------------------
/MCP/TestScripts/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Server Test Scripts
2 |
3 | This directory contains test scripts for the Unreal MCP Server.
4 |
5 | ## Overview
6 |
7 | These scripts test various aspects of the MCP Server functionality:
8 |
9 | 1. **Basic Connection Test** (`1_basic_connection.py`): Tests the basic connection to the MCP Server.
10 | 2. **Python Execution Test** (`2_python_execution.py`): Tests executing Python code through the MCP Server.
11 | 3. **String Handling Test** (`3_string_test.py`): Tests various string formats and potential problem areas.
12 |
13 | ## Running the Tests
14 |
15 | You can run individual tests:
16 |
17 | ```bash
18 | python 1_basic_connection.py
19 | python 2_python_execution.py
20 | python 3_string_test.py
21 | ```
22 |
23 | Or run all tests in sequence:
24 |
25 | ```bash
26 | python run_all_tests.py
27 | ```
28 |
29 | ## Test Requirements
30 |
31 | - The MCP Server must be running in Unreal Engine
32 | - Python 3.6 or higher
33 | - Socket and JSON modules (included in standard library)
34 |
35 | ## Command Format
36 |
37 | The MCP Server expects commands in the following format:
38 |
39 | ```json
40 | {
41 | "type": "command_name",
42 | "code": "python_code_here" // For execute_python command
43 | }
44 | ```
45 |
46 | The command should be sent as a JSON string followed by a newline character.
47 |
48 | ## Troubleshooting
49 |
50 | If you encounter issues:
51 |
52 | 1. Make sure the MCP Server is running in Unreal Engine
53 | 2. Check that you're connecting to the correct host and port (default: localhost:13377)
54 | 3. Verify the command format is correct
55 | 4. Check the Unreal Engine log for any error messages
56 |
57 | ## Adding New Tests
58 |
59 | When adding new tests, follow the pattern of the existing tests:
60 |
61 | 1. Connect to the server
62 | 2. Send a command
63 | 3. Receive and process the response
64 | 4. Return success/failure
65 |
66 | Use the `sys.exit()` code to indicate test success (0) or failure (non-zero).
```
--------------------------------------------------------------------------------
/MCP/UserTools/README.md:
--------------------------------------------------------------------------------
```markdown
1 |
2 | ### User Guide: Adding Custom MCP Tools
3 |
4 | To extend the functionality of the UnrealMCP plugin with your own tools, follow these steps:
5 |
6 | 1. **Locate the `user_tools` Directory**
7 | - Find the `user_tools` directory in the plugin’s MCP folder (e.g., `Plugins/UnrealMCP/MCP/user_tools`).
8 | - If it doesn’t exist, create it manually.
9 |
10 | 2. **Create a Python Script**
11 | - Add a new `.py` file in the `user_tools` directory (e.g., `my_tool.py`).
12 | - Define a `register_tools(mcp, utils)` function in your script to register your custom tools.
13 |
14 | 3. **Define Your Tools**
15 | - Use the `@mcp.tool()` decorator to create tools.
16 | - Access the `send_command` function via `utils['send_command']` to interact with Unreal Engine.
17 | - Example:
18 |
19 | ```python
20 | def register_tools(mcp, utils):
21 | send_command = utils['send_command']
22 |
23 | @mcp.tool()
24 | def create_cube(ctx, location: list) -> str:
25 | """Create a cube at the specified location."""
26 | code = f"""
27 | import unreal
28 | location = unreal.Vector({location[0]}, {location[1]}, {location[2]})
29 | unreal.EditorLevelLibrary.spawn_actor_from_class(unreal.StaticMeshActor, location)
30 | """
31 | response = send_command("execute_python", {"code": code})
32 | return "Cube created" if response["status"] == "success" else f"Error: {response['message']}"
33 | ```
34 |
35 | 4. **Run the Bridge**
36 | - Start the MCP bridge as usual with `run_unreal_mcp.bat`. Your tools will be loaded automatically.
37 |
38 | **Notes:**
39 | - Tools run in the bridge’s Python process and communicate with Unreal Engine via the MCP server.
40 | - Use `send_command("execute_python", {"code": "..."})` to execute Python code in Unreal Engine’s interpreter, accessing the `unreal` module.
41 | - Ensure any additional Python packages required by your tools are installed in Unreal Engine’s Python environment (not the bridge’s virtual environment) if using `execute_python`.
42 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # UnrealMCP Plugin
2 |
3 | # VERY WIP REPO
4 | I'm working on adding more tools now and cleaning up the codebase,
5 | I plan to allow for easy tool extension outside the main plugin
6 |
7 | This is very much a work in progress, and I need to clean up a lot of stuff!!!!!
8 |
9 | Also, I only use windows, so I don't know how this would be setup for mac/unix
10 |
11 | ## Overview
12 | UnrealMCP is an Unofficial Unreal Engine plugin designed to control Unreal Engine with AI tools. It implements a Machine Control Protocol (MCP) within Unreal Engine, allowing external AI systems to interact with and manipulate the Unreal environment programmatically.
13 |
14 | I only just learned about MCP a few days ago, so I'm not that familiar with it, I'm still learning so things might be initially pretty rough.
15 | I've implemented this using https://github.com/ahujasid/blender-mcp as a reference, which relies on Claude for Desktop. It now works with both Claude for Desktop and Cursor. If you experiment with other models, please let me know!
16 |
17 | ## ⚠️ DISCLAIMER
18 | This plugin allows AI agents to directly modify your Unreal Engine project. While it can be a powerful tool, it also comes with risks:
19 |
20 | - AI agents may make unexpected changes to your project
21 | - Files could be accidentally deleted or modified
22 | - Project settings could be altered
23 | - Assets could be overwritten
24 |
25 | **IMPORTANT SAFETY MEASURES:**
26 | 1. Always use source control (like Git or Perforce) with your project
27 | 2. Make regular backups of your project
28 | 3. Test the plugin in a separate project first
29 | 4. Review changes before committing them
30 |
31 | By using this plugin, you acknowledge that:
32 | - You are solely responsible for any changes made to your project
33 | - The plugin author is not responsible for any damage, data loss, or issues caused by AI agents
34 | - You use this plugin at your own risk
35 |
36 | ## Features
37 | - TCP server implementation for remote control of Unreal Engine
38 | - JSON-based command protocol for AI tools integration
39 | - Editor UI integration for easy access to MCP functionality
40 | - Comprehensive scene manipulation capabilities
41 | - Python companion scripts for client-side interaction
42 |
43 | ## Roadmap
44 | These are what I have in mind for development as of 3/14/2025
45 | I'm not sure what's possible yet, in theory anything, but it depends on how
46 | good the integrated LLM is at utilizing these tools.
47 | - [X] Basic operations working
48 | - [X] Python working
49 | - [X] Materials
50 | - [ ] User Extensions (in progress)
51 | - [ ] Asset tools
52 | - [ ] Blueprints
53 | - [ ] Niagara VFX
54 | - [ ] Metasound
55 | - [ ] Landscape (I might hold off on this because Epic has mentioned they are going to be updating the landscape tools)
56 | - [ ] Modeling Tools
57 | - [ ] PCG
58 |
59 | ## Requirements
60 | - Unreal Engine 5.5 (I have only tested on this version, may work with earlier, but no official support)
61 | - C++ development environment configured for Unreal Engine
62 | - Python 3.7+ for client-side scripting
63 | - Model to run the commands, in testing I've been using Claude for Desktop https://claude.ai/download
64 |
65 | ## Prerequisites to run
66 | - Unreal Editor Installation (Tested with 5.3, but should work on 5.0+)
67 | - Python 3.7+ (This can run with your existing python install)
68 | - MCP compatible LLM (Claude for Desktop, Cursor, etc.)
69 | - Setup: run setup_unreal_mcp.bat in MCP folder as per instructions in MCP/README_MCP_SETUP.md
70 |
71 | ## Quick Start for Cursor Users
72 | If you want to use UnrealMCP with Cursor, follow these simple steps:
73 |
74 | 1. Clone or download this repository as a zip
75 | 2. Create a new Unreal Project, or open an existing one
76 | 3. Create a "Plugins" folder in your project directory if it doesn't exist
77 | 4. Unzip or copy this repository into the Plugins folder
78 | 5. Run `setup_cursor_mcp.bat` in the MCP folder
79 | 6. Open your Unreal project and enable the plugin in Edit > Plugins (if not already enabled)
80 | 7. Start Cursor and ask it to work with your Unreal project
81 |
82 | That's it! The setup script will automatically configure everything needed for Cursor integration.
83 |
84 | ## Installation
85 |
86 | 1. Clone or download this repository as a zip
87 | 2. Create a new Unreal Project, or open an existing one
88 | 3. Create a "Plugins" folder in your project directory if it doesn't exist
89 | 4. Unzip or copy this repository into the Plugins folder
90 | 5. Setup MCP
91 | - Run the `setup_unreal_mcp.bat` script in the MCP folder (see `MCP/README_MCP_SETUP.md` for details)
92 | - This will configure Python and your AI assistant (Claude for Desktop or Cursor)
93 | 6. Open your Unreal project, the plugin should be available in the Plugins menu
94 | 7. If not, enable the plugin in Edit > Plugins
95 | 8. Choose your preferred AI assistant:
96 | - For Claude for Desktop: follow the instructions in the "With Claude for Desktop" section below
97 | - For Cursor: follow the instructions in the "With Cursor" section below
98 |
99 | ## With Claude for Desktop
100 | You will need to find your installation directory for Claude for Desktop. Find claude_desktop_config.json and add an entry and make it look like so:
101 |
102 | **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
103 |
104 | ```json
105 | {
106 | "mcpServers": {
107 | "unreal": {
108 | "command": "C:/path/to/your/project/Plugins/UnrealMCP/MCP/run_unreal_mcp.bat",
109 | "args": []
110 | }
111 | }
112 | }
113 | ```
114 |
115 | Alternatively the unreal_mcp_setup.bat script should do this for you.
116 |
117 | To find the path to your claude for desktop install you can go into settings and click 'Edit Config'
118 | This is usually in
119 | ```
120 | C:\Users\USERNAME\AppData\Roaming\Claude
121 | ```
122 |
123 | ## With Cursor
124 | Cursor should be automatically configured if you've run the setup script with the Cursor option. If you need to manually configure it:
125 |
126 | **Windows:** `%APPDATA%\Cursor\User\settings.json`
127 |
128 | Add or update the settings with:
129 | ```json
130 | {
131 | "mcp": {
132 | "enabled": true,
133 | "servers": {
134 | "unreal": {
135 | "command": "C:/path/to/your/project/Plugins/UnrealMCP/MCP/run_unreal_mcp.bat",
136 | "args": []
137 | }
138 | }
139 | }
140 | }
141 | ```
142 |
143 | ## Testing
144 | Once everything is setup you need to launch the unreal editor.
145 | Note: Nothing else has to be started or set up to run the mcp bridge, it will run when needed.
146 |
147 | Open Claude for Desktop or Cursor, ensure that the tools have successfully enabled, ask your AI assistant to work in Unreal.
148 |
149 | Here are some example prompts to try:
150 | - "What actors are in the current level?"
151 | - "Create a cube at position (0, 0, 100)"
152 | - "List available commands I can use with Unreal Engine"
153 |
154 | ## Usage
155 | ### In Unreal Editor
156 | Once the plugin is enabled, you'll find MCP controls in the editor toolbar button.
157 | 
158 |
159 | 
160 |
161 | The TCP server can be started/stopped from here.
162 | Check the output log under log filter LogMCP for extra information.
163 |
164 | Once the server is confirmed up and running from the editor.
165 | Open Claude for Desktop, ensure that the tools have successfully enabled, ask Claude to work in unreal.
166 |
167 | Currently only basic operations are supported, creating objects, modfiying their transforms, getting scene info, and running python scripts.
168 | Claude makes a lot of errors with unreal python as I believe there aren't a ton of examples for it, but let it run and it will usually figure things out.
169 | I would really like to improve this aspect of how it works but it's low hanging fruit for adding functionality into unreal.
170 |
171 | ### Client-Side Integration
172 | Use the provided Python scripts in the `MCP` directory to connect to and control your Unreal Engine instance:
173 |
174 | ```python
175 | from unreal_mcp_client import UnrealMCPClient
176 |
177 | # Connect to the Unreal MCP server
178 | client = UnrealMCPClient("localhost", 13377)
179 |
180 | # Example: Create a cube in the scene
181 | client.create_object(
182 | class_name="StaticMeshActor",
183 | asset_path="/Engine/BasicShapes/Cube.Cube",
184 | location=(0, 0, 100),
185 | rotation=(0, 0, 0),
186 | scale=(1, 1, 1),
187 | name="MCP_Cube"
188 | )
189 | ```
190 |
191 | ## Command Reference
192 | The plugin supports various commands for scene manipulation:
193 | - `get_scene_info`: Retrieve information about the current scene
194 | - `create_object`: Spawn a new object in the scene
195 | - `delete_object`: Remove an object from the scene
196 | - `modify_object`: Change properties of an existing object
197 | - `execute_python`: Run Python commands in Unreal's Python environment
198 | - And more to come...
199 |
200 | Refer to the documentation in the `Docs` directory for a complete command reference.
201 |
202 | ## Security Considerations
203 | - The MCP server accepts connections from any client by default
204 | - Limit server exposure to localhost for development
205 | - Validate all incoming commands to prevent injection attacks
206 |
207 | ## Troubleshooting
208 | - Ensure Unreal Engine is running with the MCP plugin.
209 | - Check logs in Claude for Desktop for stderr output.
210 | - Reach out on the discord, I just made it, but I will check it periodically
211 | Discord (Dreamatron Studios): https://discord.gg/abRftdSe
212 |
213 | ### Project Structure
214 | - `Source/UnrealMCP/`: Core plugin implementation
215 | - `Private/`: Internal implementation files
216 | - `Public/`: Public header files
217 | - `Content/`: Plugin assets
218 | - `MCP/`: Python client scripts and examples
219 | - `Resources/`: Icons and other resources
220 |
221 | ## License
222 | MIT License
223 |
224 | Copyright (c) 2025 kvick
225 |
226 | Permission is hereby granted, free of charge, to any person obtaining a copy
227 | of this software and associated documentation files (the "Software"), to deal
228 | in the Software without restriction, including without limitation the rights
229 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
230 | copies of the Software, and to permit persons to whom the Software is
231 | furnished to do so, subject to the following conditions:
232 |
233 | The above copyright notice and this permission notice shall be included in all
234 | copies or substantial portions of the Software.
235 |
236 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
237 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
238 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
239 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
240 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
241 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
242 | SOFTWARE.
243 |
244 | ## Credits
245 | - Created by: kvick
246 | - X: [@kvickart](https://x.com/kvickart)
247 | - Discord: https://discord.gg/abRftdSe
248 |
249 | ### Thank you to testers!!!
250 | - https://github.com/TheMurphinatur
251 |
252 | - [@sidahuj](https://x.com/sidahuj) for the inspriation
253 |
254 |
255 |
256 | ## Contributing
257 | Contributions are welcome, but I will need some time to wrap my head around things and cleanup first, lol
258 |
```
--------------------------------------------------------------------------------
/MCP/example_extension_script.py:
--------------------------------------------------------------------------------
```python
1 |
```
--------------------------------------------------------------------------------
/MCP/Commands/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Command modules for the UnrealMCP bridge."""
```
--------------------------------------------------------------------------------
/MCP/requirements.txt:
--------------------------------------------------------------------------------
```
1 | mcp>=0.1.0
2 | # socket and json are part of Python's standard library and don't need to be listed
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Public/MCPSettings.h:
--------------------------------------------------------------------------------
```
1 | #pragma once
2 | #include "CoreMinimal.h"
3 | #include "Engine/DeveloperSettings.h"
4 | #include "MCPConstants.h"
5 | #include "MCPSettings.generated.h"
6 |
7 | UCLASS(config = Editor, defaultconfig)
8 | class UNREALMCP_API UMCPSettings : public UDeveloperSettings
9 | {
10 | GENERATED_BODY()
11 | public:
12 | UPROPERTY(config, EditAnywhere, Category = "MCP", meta = (ClampMin = "1024", ClampMax = "65535"))
13 | int32 Port = MCPConstants::DEFAULT_PORT;
14 | };
```
--------------------------------------------------------------------------------
/MCP/setup_cursor_mcp.bat:
--------------------------------------------------------------------------------
```
1 | @echo off
2 | echo ========================================================
3 | echo Unreal MCP - Cursor Setup
4 | echo ========================================================
5 | echo.
6 | echo This script will set up the MCP bridge for Cursor.
7 | echo.
8 |
9 | REM Get the directory where this script is located
10 | set "SCRIPT_DIR=%~dp0"
11 | set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
12 |
13 | REM Run the main setup script with the cursor configuration flag
14 | call "%SCRIPT_DIR%\setup_unreal_mcp.bat" --configure-cursor
15 |
16 | echo.
17 | echo Setup complete! You can now use UnrealMCP with Cursor.
18 | echo.
```
--------------------------------------------------------------------------------
/MCP/run_unreal_mcp.bat:
--------------------------------------------------------------------------------
```
1 | @echo off
2 | setlocal
3 |
4 | REM Get the directory where this script is located
5 | set "SCRIPT_DIR=%~dp0"
6 | set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
7 |
8 | REM Set paths for local environment
9 | set "ENV_DIR=%SCRIPT_DIR%\python_env"
10 | set "PYTHON_PATH=%ENV_DIR%\Scripts\python.exe"
11 |
12 | REM Check if Python environment exists
13 | if not exist "%PYTHON_PATH%" (
14 | echo ERROR: Python environment not found. Please run setup_unreal_mcp.bat first. >&2
15 | goto :end
16 | )
17 |
18 | REM Activate the virtual environment silently
19 | call "%ENV_DIR%\Scripts\activate.bat" >nul 2>&1
20 |
21 | REM Log start message to stderr
22 | echo Starting Unreal MCP bridge... >&2
23 |
24 | REM Run the Python bridge script, keeping stdout clean for MCP
25 | python "%SCRIPT_DIR%\unreal_mcp_bridge.py" %*
26 |
27 | :end
```
--------------------------------------------------------------------------------
/MCP/check_setup.bat:
--------------------------------------------------------------------------------
```
1 | @echo off
2 | echo ========================================================
3 | echo Unreal MCP - Setup Diagnosis Tool
4 | echo ========================================================
5 | echo.
6 | echo This tool will check your MCP setup and provide diagnostic information.
7 | echo.
8 |
9 | REM Get the directory where this script is located
10 | set "SCRIPT_DIR=%~dp0"
11 | set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
12 |
13 | REM Try to run with the virtual environment first
14 | if exist "%SCRIPT_DIR%\python_env\Scripts\python.exe" (
15 | echo Using Python from virtual environment...
16 | call "%SCRIPT_DIR%\python_env\Scripts\activate.bat"
17 | python "%SCRIPT_DIR%\check_mcp_setup.py"
18 | ) else (
19 | echo Using system Python...
20 | python "%SCRIPT_DIR%\check_mcp_setup.py"
21 | )
22 |
23 | echo.
24 | echo Press any key to exit...
25 | pause >nul
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/UnrealMCP.Build.cs:
--------------------------------------------------------------------------------
```csharp
1 | // Copyright Epic Games, Inc. All Rights Reserved.
2 |
3 | using UnrealBuildTool;
4 |
5 | public class UnrealMCP : ModuleRules
6 | {
7 | public UnrealMCP(ReadOnlyTargetRules Target) : base(Target)
8 | {
9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
10 |
11 | PublicIncludePaths.AddRange(
12 | new string[] {
13 | // ... add public include paths required here ...
14 | }
15 | );
16 |
17 |
18 | PrivateIncludePaths.AddRange(
19 | new string[] {
20 | // ... add other private include paths required here ...
21 | }
22 | );
23 |
24 |
25 | PublicDependencyModuleNames.AddRange(
26 | new string[] {
27 | "Core", "CoreUObject", "Engine", "UnrealEd",
28 | "Networking", "Sockets", "Slate", "SlateCore", "EditorStyle",
29 | "DeveloperSettings", "Projects", "ToolMenus",
30 | "BlueprintGraph", "GraphEditor", "KismetCompiler"
31 | }
32 | );
33 |
34 |
35 | PrivateDependencyModuleNames.AddRange(
36 | new string[] {
37 | "Json", "JsonUtilities", "Settings", "InputCore", "PythonScriptPlugin",
38 | "Kismet", "KismetWidgets"
39 | }
40 | );
41 |
42 |
43 | DynamicallyLoadedModuleNames.AddRange(
44 | new string[]
45 | {
46 | // ... add any modules that your module loads dynamically here ...
47 | }
48 | );
49 | }
50 | }
51 |
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Public/UnrealMCP.h:
--------------------------------------------------------------------------------
```
1 | // Copyright Epic Games, Inc. All Rights Reserved.
2 |
3 | #pragma once
4 |
5 | #include "CoreMinimal.h"
6 | #include "Modules/ModuleManager.h"
7 |
8 | // Declare custom log category
9 | UNREALMCP_API DECLARE_LOG_CATEGORY_EXTERN(LogMCP, Log, All);
10 |
11 | class FMCPTCPServer;
12 | class SWindow;
13 |
14 | class FUnrealMCPModule : public IModuleInterface, public TSharedFromThis<FUnrealMCPModule>
15 | {
16 | public:
17 | /** IModuleInterface implementation */
18 | virtual void StartupModule() override;
19 | virtual void ShutdownModule() override;
20 |
21 | /**
22 | * Get the MCP server instance
23 | * External modules can use this to register custom handlers
24 | * @return The MCP server instance, or nullptr if not available
25 | */
26 | UNREALMCP_API FMCPTCPServer* GetServer() const { return Server.Get(); }
27 |
28 | private:
29 | void ExtendLevelEditorToolbar();
30 | void AddToolbarButton(FToolBarBuilder& Builder);
31 | void ToggleServer();
32 | void StartServer();
33 | void StopServer();
34 | bool IsServerRunning() const;
35 |
36 | // MCP Control Panel functions
37 | void OpenMCPControlPanel();
38 | FReply OpenMCPControlPanel_OnClicked();
39 | void CloseMCPControlPanel();
40 | void OnMCPControlPanelClosed(const TSharedRef<SWindow>& Window);
41 | TSharedRef<class SWidget> CreateMCPControlPanelContent();
42 | FReply OnStartServerClicked();
43 | FReply OnStopServerClicked();
44 |
45 | TUniquePtr<FMCPTCPServer> Server;
46 | TSharedPtr<SWindow> MCPControlPanelWindow;
47 | };
48 |
```
--------------------------------------------------------------------------------
/MCP/UserTools/example_tool.py:
--------------------------------------------------------------------------------
```python
1 | def register_tools(mcp, utils):
2 | send_command = utils['send_command']
3 |
4 | @mcp.tool()
5 | def my_custom_tool(ctx):
6 | return "Hello from custom tool!"
7 |
8 | @mcp.tool()
9 | def get_actor_count(ctx) -> str:
10 | """Get the number of actors in the current Unreal Engine scene."""
11 | try:
12 | response = send_command("get_scene_info")
13 | print(f"Response: {response}")
14 | if response["status"] == "success":
15 | result = response["result"]
16 | total_actor_count = result["actor_count"]
17 | returned_actor_count = result.get("returned_actor_count", len(result["actors"]))
18 | limit_reached = result.get("limit_reached", False)
19 |
20 | response_text = f"Total number of actors: {total_actor_count}\n"
21 |
22 | if limit_reached:
23 | response_text += f"WARNING: Actor limit reached! Only {returned_actor_count} actors were returned in the response.\n"
24 | response_text += f"The remaining {total_actor_count - returned_actor_count} actors are not included in the response.\n"
25 |
26 | return response_text
27 | else:
28 | return f"Error: {response['message']}"
29 | except Exception as e:
30 | return f"Error: {str(e)}"
31 |
32 |
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Public/MCPCommandHandlers_Materials.h:
--------------------------------------------------------------------------------
```
1 | #pragma once
2 |
3 | #include "CoreMinimal.h"
4 | #include "MCPCommandHandlers.h"
5 | #include "Materials/Material.h"
6 | #include "Materials/MaterialExpressionScalarParameter.h"
7 | #include "Materials/MaterialExpressionVectorParameter.h"
8 |
9 | class FMCPCreateMaterialHandler : public FMCPCommandHandlerBase
10 | {
11 | public:
12 | FMCPCreateMaterialHandler() : FMCPCommandHandlerBase(TEXT("create_material")) {}
13 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
14 |
15 | private:
16 | TPair<UMaterial*, bool> CreateMaterial(const FString& PackagePath, const FString& MaterialName, const TSharedPtr<FJsonObject>& Properties);
17 | bool ModifyMaterialProperties(UMaterial* Material, const TSharedPtr<FJsonObject>& Properties);
18 | };
19 |
20 | class FMCPModifyMaterialHandler : public FMCPCommandHandlerBase
21 | {
22 | public:
23 | FMCPModifyMaterialHandler() : FMCPCommandHandlerBase(TEXT("modify_material")) {}
24 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
25 |
26 | private:
27 | bool ModifyMaterialProperties(UMaterial* Material, const TSharedPtr<FJsonObject>& Properties);
28 | };
29 |
30 | class FMCPGetMaterialInfoHandler : public FMCPCommandHandlerBase
31 | {
32 | public:
33 | FMCPGetMaterialInfoHandler() : FMCPCommandHandlerBase(TEXT("get_material_info")) {}
34 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
35 |
36 | private:
37 | TSharedPtr<FJsonObject> GetMaterialInfo(UMaterial* Material);
38 | };
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Public/MCPConstants.h:
--------------------------------------------------------------------------------
```
1 | #pragma once
2 |
3 | #include "CoreMinimal.h"
4 |
5 | /**
6 | * Constants used throughout the MCP plugin
7 | */
8 | namespace MCPConstants
9 | {
10 | // Network constants
11 | constexpr int32 DEFAULT_PORT = 13377;
12 | constexpr int32 DEFAULT_RECEIVE_BUFFER_SIZE = 65536; // 64KB buffer size
13 | constexpr int32 DEFAULT_SEND_BUFFER_SIZE = DEFAULT_RECEIVE_BUFFER_SIZE;
14 | constexpr float DEFAULT_CLIENT_TIMEOUT_SECONDS = 30.0f;
15 | constexpr float DEFAULT_TICK_INTERVAL_SECONDS = 0.1f;
16 |
17 | // Python constants
18 | constexpr const TCHAR* PYTHON_TEMP_DIR_NAME = TEXT("PythonTemp");
19 | constexpr const TCHAR* PYTHON_TEMP_FILE_PREFIX = TEXT("mcp_temp_script_");
20 |
21 | // Logging constants
22 | constexpr bool DEFAULT_VERBOSE_LOGGING = false;
23 |
24 | // Performance constants
25 | constexpr int32 MAX_ACTORS_IN_SCENE_INFO = 1000;
26 |
27 | // Path constants - use these instead of hardcoded paths
28 | // These will be initialized at runtime in the module startup
29 | extern FString ProjectRootPath; // Root path of the project
30 | extern FString PluginRootPath; // Root path of the MCP plugin
31 | extern FString PluginContentPath; // Path to the plugin's content directory
32 | extern FString PluginResourcesPath; // Path to the plugin's resources directory
33 | extern FString PluginLogsPath; // Path to the plugin's logs directory
34 | extern FString PluginMCPScriptsPath; // Path to the plugin's MCP scripts directory
35 |
36 | // Function to initialize all path variables at runtime
37 | void InitializePathConstants();
38 | }
```
--------------------------------------------------------------------------------
/MCP/TestScripts/run_all_tests.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | MCP Server Test Runner
4 |
5 | This script runs all the MCP Server test scripts in sequence.
6 | """
7 |
8 | import subprocess
9 | import sys
10 | import os
11 | import time
12 |
13 | def run_test(test_script):
14 | """Run a test script and return whether it passed."""
15 | script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), test_script)
16 |
17 | print(f"\n{'=' * 60}")
18 | print(f"Running {test_script}...")
19 | print(f"{'=' * 60}")
20 |
21 | try:
22 | result = subprocess.run(
23 | [sys.executable, script_path],
24 | capture_output=False, # Show output in real-time
25 | check=False
26 | )
27 |
28 | if result.returncode == 0:
29 | print(f"\n✓ {test_script} PASSED")
30 | return True
31 | else:
32 | print(f"\n✗ {test_script} FAILED (exit code: {result.returncode})")
33 | return False
34 | except Exception as e:
35 | print(f"\n✗ Error running {test_script}: {e}")
36 | return False
37 |
38 | def main():
39 | """Run all test scripts."""
40 | # List of test scripts to run
41 | test_scripts = [
42 | "1_basic_connection.py",
43 | "2_python_execution.py",
44 | "3_string_test.py"
45 | ]
46 |
47 | # Track results
48 | results = {}
49 |
50 | # Run each test
51 | for script in test_scripts:
52 | results[script] = run_test(script)
53 | # Add a small delay between tests
54 | time.sleep(1)
55 |
56 | # Print summary
57 | print("\n" + "=" * 60)
58 | print("TEST SUMMARY")
59 | print("=" * 60)
60 |
61 | all_passed = True
62 | for script, passed in results.items():
63 | status = "✓ PASSED" if passed else "✗ FAILED"
64 | print(f"{script}: {status}")
65 | if not passed:
66 | all_passed = False
67 |
68 | print("\n" + "=" * 60)
69 | if all_passed:
70 | print("✓ ALL TESTS PASSED")
71 | return 0
72 | else:
73 | print("✗ SOME TESTS FAILED")
74 | return 1
75 |
76 | if __name__ == "__main__":
77 | print("=== MCP Server Test Runner ===")
78 | exit_code = main()
79 | sys.exit(exit_code)
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Private/MCPConstants.cpp:
--------------------------------------------------------------------------------
```cpp
1 | #include "MCPConstants.h"
2 | #include "Misc/Paths.h"
3 | #include "HAL/PlatformFileManager.h"
4 | #include "Interfaces/IPluginManager.h"
5 |
6 | // Initialize the static path variables
7 | FString MCPConstants::ProjectRootPath;
8 | FString MCPConstants::PluginRootPath;
9 | FString MCPConstants::PluginContentPath;
10 | FString MCPConstants::PluginResourcesPath;
11 | FString MCPConstants::PluginLogsPath;
12 | FString MCPConstants::PluginMCPScriptsPath;
13 |
14 | void MCPConstants::InitializePathConstants()
15 | {
16 | // Get the project root path
17 | ProjectRootPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
18 |
19 | // Get the plugin root path
20 | TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin("UnrealMCP");
21 | if (Plugin.IsValid())
22 | {
23 | PluginRootPath = FPaths::ConvertRelativePathToFull(Plugin->GetBaseDir());
24 |
25 | // Derive other paths from the plugin root
26 | PluginContentPath = FPaths::Combine(PluginRootPath, TEXT("Content"));
27 | PluginResourcesPath = FPaths::Combine(PluginRootPath, TEXT("Resources"));
28 | PluginLogsPath = FPaths::Combine(PluginRootPath, TEXT("Logs"));
29 | PluginMCPScriptsPath = FPaths::Combine(PluginRootPath, TEXT("MCP"));
30 |
31 | // Ensure directories exist
32 | IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
33 |
34 | if (!PlatformFile.DirectoryExists(*PluginLogsPath))
35 | {
36 | PlatformFile.CreateDirectory(*PluginLogsPath);
37 | }
38 |
39 | if (!PlatformFile.DirectoryExists(*PluginMCPScriptsPath))
40 | {
41 | PlatformFile.CreateDirectory(*PluginMCPScriptsPath);
42 | }
43 | }
44 | else
45 | {
46 | // Fallback to project-relative paths if plugin is not found
47 | PluginRootPath = FPaths::Combine(ProjectRootPath, TEXT("Plugins/UnrealMCP"));
48 | PluginContentPath = FPaths::Combine(PluginRootPath, TEXT("Content"));
49 | PluginResourcesPath = FPaths::Combine(PluginRootPath, TEXT("Resources"));
50 | PluginLogsPath = FPaths::Combine(PluginRootPath, TEXT("Logs"));
51 | PluginMCPScriptsPath = FPaths::Combine(PluginRootPath, TEXT("MCP"));
52 | }
53 | }
```
--------------------------------------------------------------------------------
/MCP/install_mcp.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python
2 | """
3 | This script installs the MCP package and verifies it's installed correctly.
4 | Run this script with the same Python interpreter that Claude Desktop will use.
5 | """
6 |
7 | import sys
8 | import subprocess
9 | import importlib.util
10 |
11 | def check_mcp_installed():
12 | """Check if the MCP package is installed."""
13 | spec = importlib.util.find_spec("mcp")
14 | return spec is not None
15 |
16 | def install_mcp():
17 | """Install the MCP package."""
18 | print(f"Installing MCP package using Python: {sys.executable}")
19 | try:
20 | subprocess.check_call([sys.executable, "-m", "pip", "install", "mcp>=0.1.0"])
21 | print("MCP package installed successfully!")
22 | return True
23 | except subprocess.CalledProcessError as e:
24 | print(f"Error installing MCP package: {e}")
25 | return False
26 |
27 | def main():
28 | """Main function."""
29 | print(f"Python version: {sys.version}")
30 | print(f"Python executable: {sys.executable}")
31 |
32 | if check_mcp_installed():
33 | print("MCP package is already installed.")
34 | try:
35 | import mcp
36 | print(f"MCP version: {mcp.__version__}")
37 | except (ImportError, AttributeError):
38 | print("MCP package is installed but version could not be determined.")
39 | else:
40 | print("MCP package is not installed.")
41 | if install_mcp():
42 | if check_mcp_installed():
43 | print("Verification: MCP package is now installed.")
44 | try:
45 | import mcp
46 | print(f"MCP version: {mcp.__version__}")
47 | except (ImportError, AttributeError):
48 | print("MCP package is installed but version could not be determined.")
49 | else:
50 | print("ERROR: MCP package installation failed verification.")
51 | print("Please try installing manually with:")
52 | print(f"{sys.executable} -m pip install mcp>=0.1.0")
53 | else:
54 | print("Installation failed. Please try installing manually with:")
55 | print(f"{sys.executable} -m pip install mcp>=0.1.0")
56 |
57 | print("\nTo verify the MCP package is installed in the correct environment:")
58 | print("1. Make sure you run this script with the same Python interpreter that Claude Desktop will use")
59 | print("2. Check that the Python executable path shown above matches the one in your Claude Desktop configuration")
60 |
61 | input("\nPress Enter to exit...")
```
--------------------------------------------------------------------------------
/MCP/TestScripts/test_commands_basic.py:
--------------------------------------------------------------------------------
```python
1 | """Test script for UnrealMCP basic commands.
2 |
3 | This script tests the basic scene and Python execution commands available in the UnrealMCP bridge.
4 | Make sure Unreal Engine is running with the UnrealMCP plugin enabled before running this script.
5 | """
6 |
7 | import sys
8 | import os
9 | import json
10 | from mcp.server.fastmcp import FastMCP, Context
11 |
12 | # Add the MCP directory to sys.path so we can import unreal_mcp_bridge
13 | mcp_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14 | if mcp_dir not in sys.path:
15 | sys.path.insert(0, mcp_dir)
16 |
17 | from unreal_mcp_bridge import send_command
18 |
19 | def test_scene_info():
20 | """Test getting scene information."""
21 | print("\n1. Testing get_scene_info...")
22 | try:
23 | response = send_command("get_scene_info")
24 | print(f"Get Scene Info Response: {json.dumps(response, indent=2)}")
25 | return response["status"] == "success"
26 | except Exception as e:
27 | print(f"Error testing get_scene_info: {e}")
28 | return False
29 |
30 | def test_object_creation():
31 | """Test creating objects in the scene."""
32 | print("\n2. Testing create_object...")
33 | try:
34 | params = {
35 | "type": "StaticMeshActor",
36 | "location": [0, 0, 100],
37 | "label": "TestCube"
38 | }
39 | response = send_command("create_object", params)
40 | print(f"Create Object Response: {json.dumps(response, indent=2)}")
41 | return response["status"] == "success"
42 | except Exception as e:
43 | print(f"Error testing create_object: {e}")
44 | return False
45 |
46 | def test_python_execution():
47 | """Test Python execution in Unreal Engine."""
48 | print("\n3. Testing execute_python...")
49 |
50 | test_code = """
51 | import unreal
52 | print("Python executing in Unreal Engine!")
53 | world = unreal.EditorLevelLibrary.get_editor_world()
54 | print(f"Current level: {world.get_name()}")
55 | actors = unreal.EditorLevelLibrary.get_all_level_actors()
56 | print(f"Number of actors in level: {len(actors)}")
57 | """
58 |
59 | try:
60 | response = send_command("execute_python", {"code": test_code})
61 | print(f"Python Execution Response: {json.dumps(response, indent=2)}")
62 | return response["status"] == "success"
63 | except Exception as e:
64 | print(f"Error testing Python execution: {e}")
65 | return False
66 |
67 | def main():
68 | """Run all basic command tests."""
69 | print("Starting UnrealMCP basic command tests...")
70 | print("Make sure Unreal Engine is running with the UnrealMCP plugin enabled!")
71 |
72 | try:
73 | results = {
74 | "get_scene_info": test_scene_info(),
75 | "create_object": test_object_creation(),
76 | "execute_python": test_python_execution()
77 | }
78 |
79 | print("\nTest Results:")
80 | print("-" * 40)
81 | for test_name, success in results.items():
82 | status = "✓ PASS" if success else "✗ FAIL"
83 | print(f"{status} - {test_name}")
84 | print("-" * 40)
85 |
86 | if all(results.values()):
87 | print("\nAll basic tests passed successfully!")
88 | else:
89 | print("\nSome tests failed. Check the output above for details.")
90 | sys.exit(1)
91 |
92 | except Exception as e:
93 | print(f"\nError during testing: {e}")
94 | sys.exit(1)
95 |
96 | if __name__ == "__main__":
97 | main()
```
--------------------------------------------------------------------------------
/MCP/utils/command_utils.py:
--------------------------------------------------------------------------------
```python
1 | """Utility functions for MCP commands."""
2 |
3 | import json
4 | import socket
5 | import sys
6 |
7 | # Constants (these will be read from MCPConstants.h)
8 | DEFAULT_PORT = 13377
9 | DEFAULT_BUFFER_SIZE = 65536
10 | DEFAULT_TIMEOUT = 10
11 |
12 | try:
13 | # Try to read the port from the C++ constants
14 | import os
15 | plugin_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
16 | constants_path = os.path.join(plugin_dir, "Source", "UnrealMCP", "Public", "MCPConstants.h")
17 |
18 | if os.path.exists(constants_path):
19 | with open(constants_path, 'r') as f:
20 | constants_content = f.read()
21 |
22 | # Extract port from MCPConstants
23 | port_match = constants_content.find("DEFAULT_PORT = ")
24 | if port_match != -1:
25 | port_line = constants_content[port_match:].split(';')[0]
26 | DEFAULT_PORT = int(port_line.split('=')[1].strip())
27 |
28 | # Extract buffer size from MCPConstants
29 | buffer_match = constants_content.find("DEFAULT_RECEIVE_BUFFER_SIZE = ")
30 | if buffer_match != -1:
31 | buffer_line = constants_content[buffer_match:].split(';')[0]
32 | DEFAULT_BUFFER_SIZE = int(buffer_line.split('=')[1].strip())
33 | except Exception as e:
34 | print(f"Warning: Could not read constants from MCPConstants.h: {e}", file=sys.stderr)
35 |
36 | def send_command(command_type, params=None):
37 | """Send a command to the C++ MCP server and return the response."""
38 | try:
39 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
40 | s.settimeout(DEFAULT_TIMEOUT)
41 | s.connect(("localhost", DEFAULT_PORT))
42 | command = {
43 | "type": command_type,
44 | "params": params or {}
45 | }
46 | s.sendall(json.dumps(command).encode('utf-8'))
47 |
48 | chunks = []
49 | response_data = b''
50 |
51 | while True:
52 | try:
53 | chunk = s.recv(DEFAULT_BUFFER_SIZE)
54 | if not chunk:
55 | break
56 | chunks.append(chunk)
57 |
58 | response_data = b''.join(chunks)
59 | try:
60 | json.loads(response_data.decode('utf-8'))
61 | break
62 | except json.JSONDecodeError:
63 | continue
64 | except socket.timeout:
65 | if response_data:
66 | break
67 | raise
68 |
69 | if not response_data:
70 | raise Exception("No data received from server")
71 |
72 | return json.loads(response_data.decode('utf-8'))
73 | except ConnectionRefusedError:
74 | print(f"Error: Could not connect to Unreal MCP server on localhost:{DEFAULT_PORT}.", file=sys.stderr)
75 | print("Make sure your Unreal Engine with MCP plugin is running.", file=sys.stderr)
76 | raise Exception("Failed to connect to Unreal MCP server: Connection refused")
77 | except socket.timeout:
78 | print("Error: Connection timed out while communicating with Unreal MCP server.", file=sys.stderr)
79 | raise Exception("Failed to communicate with Unreal MCP server: Connection timed out")
80 | except Exception as e:
81 | print(f"Error communicating with Unreal MCP server: {str(e)}", file=sys.stderr)
82 | raise Exception(f"Failed to communicate with Unreal MCP server: {str(e)}")
```
--------------------------------------------------------------------------------
/MCP/TestScripts/test_commands_material.py:
--------------------------------------------------------------------------------
```python
1 | """Test script for UnrealMCP material commands.
2 |
3 | This script tests the material-related commands available in the UnrealMCP bridge.
4 | Make sure Unreal Engine is running with the UnrealMCP plugin enabled before running this script.
5 | """
6 |
7 | import sys
8 | import os
9 | import json
10 | from mcp.server.fastmcp import FastMCP, Context
11 |
12 | # Add the MCP directory to sys.path so we can import unreal_mcp_bridge
13 | mcp_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14 | if mcp_dir not in sys.path:
15 | sys.path.insert(0, mcp_dir)
16 |
17 | from unreal_mcp_bridge import send_command
18 |
19 | def test_material_creation():
20 | """Test material creation command."""
21 | print("\n1. Testing create_material...")
22 | try:
23 | params = {
24 | "package_path": "/Game/Materials/Tests",
25 | "name": "TestMaterial",
26 | "properties": {
27 | "shading_model": "DefaultLit",
28 | "base_color": [1.0, 0.0, 0.0, 1.0], # Red material
29 | "metallic": 0.0,
30 | "roughness": 0.5
31 | }
32 | }
33 | response = send_command("create_material", params)
34 | print(f"Create Material Response: {json.dumps(response, indent=2)}")
35 | return response["status"] == "success"
36 | except Exception as e:
37 | print(f"Error testing create_material: {e}")
38 | return False
39 |
40 | def test_material_info():
41 | """Test getting material information."""
42 | print("\n2. Testing get_material_info...")
43 | try:
44 | params = {
45 | "path": "/Game/Materials/Tests/TestMaterial"
46 | }
47 | response = send_command("get_material_info", params)
48 | print(f"Get Material Info Response: {json.dumps(response, indent=2)}")
49 | return response["status"] == "success"
50 | except Exception as e:
51 | print(f"Error testing get_material_info: {e}")
52 | return False
53 |
54 | def test_material_modification():
55 | """Test modifying material properties."""
56 | print("\n3. Testing modify_material...")
57 | try:
58 | params = {
59 | "path": "/Game/Materials/Tests/TestMaterial",
60 | "properties": {
61 | "base_color": [0.0, 1.0, 0.0, 1.0], # Change to green
62 | "metallic": 0.5
63 | }
64 | }
65 | response = send_command("modify_material", params)
66 | print(f"Modify Material Response: {json.dumps(response, indent=2)}")
67 | return response["status"] == "success"
68 | except Exception as e:
69 | print(f"Error testing modify_material: {e}")
70 | return False
71 |
72 | def main():
73 | """Run all material-related tests."""
74 | print("Starting UnrealMCP material command tests...")
75 | print("Make sure Unreal Engine is running with the UnrealMCP plugin enabled!")
76 |
77 | try:
78 | results = {
79 | "create_material": test_material_creation(),
80 | "get_material_info": test_material_info(),
81 | "modify_material": test_material_modification()
82 | }
83 |
84 | print("\nTest Results:")
85 | print("-" * 40)
86 | for test_name, success in results.items():
87 | status = "✓ PASS" if success else "✗ FAIL"
88 | print(f"{status} - {test_name}")
89 | print("-" * 40)
90 |
91 | if all(results.values()):
92 | print("\nAll material tests passed successfully!")
93 | else:
94 | print("\nSome tests failed. Check the output above for details.")
95 | sys.exit(1)
96 |
97 | except Exception as e:
98 | print(f"\nError during testing: {e}")
99 | sys.exit(1)
100 |
101 | if __name__ == "__main__":
102 | main()
```
--------------------------------------------------------------------------------
/MCP/Commands/commands_scene.py:
--------------------------------------------------------------------------------
```python
1 | """Scene-related commands for Unreal Engine.
2 |
3 | This module contains all scene-related commands for the UnrealMCP bridge,
4 | including getting scene information, creating, modifying, and deleting objects.
5 | """
6 |
7 | import sys
8 | import os
9 | from mcp.server.fastmcp import Context
10 |
11 | # Import send_command from the parent module
12 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13 | from unreal_mcp_bridge import send_command
14 |
15 | def register_all(mcp):
16 | """Register all scene-related commands with the MCP server."""
17 |
18 | @mcp.tool()
19 | def get_scene_info(ctx: Context) -> str:
20 | """Get detailed information about the current Unreal scene."""
21 | try:
22 | response = send_command("get_scene_info")
23 | if response["status"] == "success":
24 | return json.dumps(response["result"], indent=2)
25 | else:
26 | return f"Error: {response['message']}"
27 | except Exception as e:
28 | return f"Error getting scene info: {str(e)}"
29 |
30 | @mcp.tool()
31 | def create_object(ctx: Context, type: str, location: list = None, label: str = None) -> str:
32 | """Create a new object in the Unreal scene.
33 |
34 | Args:
35 | type: The type of object to create (e.g., 'StaticMeshActor', 'PointLight', etc.)
36 | location: Optional 3D location as [x, y, z]
37 | label: Optional label for the object
38 | """
39 | try:
40 | params = {"type": type}
41 | if location:
42 | params["location"] = location
43 | if label:
44 | params["label"] = label
45 | response = send_command("create_object", params)
46 | if response["status"] == "success":
47 | return f"Created object: {response['result']['name']} with label: {response['result']['label']}"
48 | else:
49 | return f"Error: {response['message']}"
50 | except Exception as e:
51 | return f"Error creating object: {str(e)}"
52 |
53 | @mcp.tool()
54 | def modify_object(ctx: Context, name: str, location: list = None, rotation: list = None, scale: list = None) -> str:
55 | """Modify an existing object in the Unreal scene.
56 |
57 | Args:
58 | name: The name of the object to modify
59 | location: Optional 3D location as [x, y, z]
60 | rotation: Optional rotation as [pitch, yaw, roll]
61 | scale: Optional scale as [x, y, z]
62 | """
63 | try:
64 | params = {"name": name}
65 | if location:
66 | params["location"] = location
67 | if rotation:
68 | params["rotation"] = rotation
69 | if scale:
70 | params["scale"] = scale
71 | response = send_command("modify_object", params)
72 | if response["status"] == "success":
73 | return f"Modified object: {response['result']['name']}"
74 | else:
75 | return f"Error: {response['message']}"
76 | except Exception as e:
77 | return f"Error modifying object: {str(e)}"
78 |
79 | @mcp.tool()
80 | def delete_object(ctx: Context, name: str) -> str:
81 | """Delete an object from the Unreal scene.
82 |
83 | Args:
84 | name: The name of the object to delete
85 | """
86 | try:
87 | response = send_command("delete_object", {"name": name})
88 | if response["status"] == "success":
89 | return f"Deleted object: {name}"
90 | else:
91 | return f"Error: {response['message']}"
92 | except Exception as e:
93 | return f"Error deleting object: {str(e)}"
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Public/MCPCommandHandlers_Blueprints.h:
--------------------------------------------------------------------------------
```
1 | #pragma once
2 |
3 | #include "CoreMinimal.h"
4 | #include "MCPCommandHandlers.h"
5 | #include "Engine/Blueprint.h"
6 | #include "Engine/BlueprintGeneratedClass.h"
7 | #include "Kismet/GameplayStatics.h"
8 | #include "Kismet/KismetSystemLibrary.h"
9 | #include "EdGraph/EdGraph.h"
10 | #include "K2Node_Event.h"
11 | #include "K2Node_CallFunction.h"
12 | #include "EdGraphSchema_K2.h"
13 | #include "AssetRegistry/AssetRegistryModule.h"
14 |
15 | /**
16 | * Common utilities for blueprint operations
17 | */
18 | class FMCPBlueprintUtils
19 | {
20 | public:
21 | /**
22 | * Create a new blueprint asset
23 | * @param PackagePath - Path where the blueprint should be created
24 | * @param BlueprintName - Name of the blueprint
25 | * @param ParentClass - Parent class for the blueprint
26 | * @return The created blueprint and success flag
27 | */
28 | static TPair<UBlueprint*, bool> CreateBlueprintAsset(
29 | const FString& PackagePath,
30 | const FString& BlueprintName,
31 | UClass* ParentClass);
32 |
33 | /**
34 | * Add event node to blueprint
35 | * @param Blueprint - Target blueprint
36 | * @param EventName - Name of the event to create
37 | * @param ParentClass - Parent class containing the event
38 | * @return The created event node and success flag
39 | */
40 | static TPair<UK2Node_Event*, bool> AddEventNode(
41 | UBlueprint* Blueprint,
42 | const FString& EventName,
43 | UClass* ParentClass);
44 |
45 | /**
46 | * Add print string node to blueprint
47 | * @param Graph - Target graph
48 | * @param Message - Message to print
49 | * @return The created print node and success flag
50 | */
51 | static TPair<UK2Node_CallFunction*, bool> AddPrintStringNode(
52 | UEdGraph* Graph,
53 | const FString& Message);
54 | };
55 |
56 | /**
57 | * Handler for creating blueprints
58 | */
59 | class FMCPCreateBlueprintHandler : public FMCPCommandHandlerBase
60 | {
61 | public:
62 | FMCPCreateBlueprintHandler() : FMCPCommandHandlerBase(TEXT("create_blueprint")) {}
63 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
64 |
65 | private:
66 | TPair<UBlueprint*, bool> CreateBlueprint(const FString& PackagePath, const FString& BlueprintName, const TSharedPtr<FJsonObject>& Properties);
67 | };
68 |
69 | /**
70 | * Handler for modifying blueprints
71 | */
72 | class FMCPModifyBlueprintHandler : public FMCPCommandHandlerBase
73 | {
74 | public:
75 | FMCPModifyBlueprintHandler() : FMCPCommandHandlerBase(TEXT("modify_blueprint")) {}
76 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
77 |
78 | private:
79 | bool ModifyBlueprint(UBlueprint* Blueprint, const TSharedPtr<FJsonObject>& Properties);
80 | };
81 |
82 | /**
83 | * Handler for getting blueprint info
84 | */
85 | class FMCPGetBlueprintInfoHandler : public FMCPCommandHandlerBase
86 | {
87 | public:
88 | FMCPGetBlueprintInfoHandler() : FMCPCommandHandlerBase(TEXT("get_blueprint_info")) {}
89 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
90 |
91 | private:
92 | TSharedPtr<FJsonObject> GetBlueprintInfo(UBlueprint* Blueprint);
93 | };
94 |
95 | /**
96 | * Handler for creating blueprint events
97 | */
98 | class FMCPCreateBlueprintEventHandler : public FMCPCommandHandlerBase
99 | {
100 | public:
101 | FMCPCreateBlueprintEventHandler() : FMCPCommandHandlerBase(TEXT("create_blueprint_event")) {}
102 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
103 |
104 | private:
105 | TPair<bool, TSharedPtr<FJsonObject>> CreateBlueprintEvent(
106 | UWorld* World,
107 | const FString& EventName,
108 | const FString& BlueprintPath,
109 | const TSharedPtr<FJsonObject>& EventParameters);
110 | };
```
--------------------------------------------------------------------------------
/MCP/TestScripts/2_python_execution.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Python Execution Test for MCP Server
4 |
5 | This script tests executing Python code through the MCP Server.
6 | It connects to the server, sends a Python code snippet, and verifies the execution.
7 | """
8 |
9 | import socket
10 | import json
11 | import sys
12 |
13 | def main():
14 | """Connect to the MCP Server and execute Python code."""
15 | try:
16 | # Create socket
17 | print("Connecting to MCP Server on localhost:13377...")
18 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
19 | s.settimeout(5) # 5 second timeout
20 |
21 | # Connect to server
22 | s.connect(("localhost", 13377))
23 | print("✓ Connected successfully")
24 |
25 | # Python code to execute
26 | code = """
27 | import unreal
28 |
29 | # Get the current level
30 | level = unreal.EditorLevelLibrary.get_editor_world()
31 | level_name = level.get_name()
32 |
33 | # Get all actors in the level
34 | actors = unreal.EditorLevelLibrary.get_all_level_actors()
35 | actor_count = len(actors)
36 |
37 | # Log some information
38 | unreal.log(f"Current level: {level_name}")
39 | unreal.log(f"Actor count: {actor_count}")
40 |
41 | # Return a result
42 | return {
43 | "level_name": level_name,
44 | "actor_count": actor_count
45 | }
46 | """
47 |
48 | # Create command
49 | command = {
50 | "type": "execute_python",
51 | "code": code
52 | }
53 |
54 | # Send command
55 | print("Sending execute_python command...")
56 | command_str = json.dumps(command) + "\n" # Add newline
57 | s.sendall(command_str.encode('utf-8'))
58 |
59 | # Receive response
60 | print("Waiting for response...")
61 | response = b""
62 | while True:
63 | data = s.recv(4096)
64 | if not data:
65 | break
66 | response += data
67 | if b"\n" in data: # Check for newline which indicates end of response
68 | break
69 |
70 | # Close connection
71 | s.close()
72 | print("✓ Connection closed properly")
73 |
74 | # Process response
75 | if response:
76 | response_str = response.decode('utf-8').strip()
77 |
78 | try:
79 | response_json = json.loads(response_str)
80 | print("\n=== RESPONSE ===")
81 | print(f"Status: {response_json.get('status', 'unknown')}")
82 |
83 | if response_json.get('status') == 'success':
84 | print("✓ Python execution successful")
85 | result = response_json.get('result', {})
86 | if isinstance(result, dict) and 'output' in result:
87 | print(f"Output: {result['output']}")
88 | return True
89 | else:
90 | print("✗ Python execution failed")
91 | print(f"Error: {response_json.get('message', 'Unknown error')}")
92 | return False
93 | except json.JSONDecodeError as e:
94 | print(f"✗ Error parsing JSON response: {e}")
95 | print(f"Raw response: {response_str}")
96 | return False
97 | else:
98 | print("✗ No response received from server")
99 | return False
100 |
101 | except ConnectionRefusedError:
102 | print("✗ Connection refused. Is the MCP Server running?")
103 | return False
104 | except socket.timeout:
105 | print("✗ Connection timed out. Is the MCP Server running?")
106 | return False
107 | except Exception as e:
108 | print(f"✗ Error: {e}")
109 | return False
110 |
111 | if __name__ == "__main__":
112 | print("=== MCP Server Python Execution Test ===")
113 | success = main()
114 | print("\n=== TEST RESULT ===")
115 | if success:
116 | print("✓ Python execution test PASSED")
117 | sys.exit(0)
118 | else:
119 | print("✗ Python execution test FAILED")
120 | sys.exit(1)
```
--------------------------------------------------------------------------------
/MCP/Commands/commands_python.py:
--------------------------------------------------------------------------------
```python
1 | """Python execution commands for Unreal Engine.
2 |
3 | This module contains commands for executing Python code in Unreal Engine.
4 | """
5 |
6 | import sys
7 | import os
8 | from mcp.server.fastmcp import Context
9 |
10 | # Import send_command from the parent module
11 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12 | from unreal_mcp_bridge import send_command
13 |
14 | def register_all(mcp):
15 | """Register all Python execution commands with the MCP server."""
16 |
17 | @mcp.tool()
18 | def execute_python(ctx: Context, code: str = None, file: str = None) -> str:
19 | """Execute Python code or a Python script file in Unreal Engine.
20 |
21 | This function allows you to execute arbitrary Python code directly in the Unreal Engine
22 | environment. You can either provide Python code as a string or specify a path to a Python
23 | script file to execute.
24 |
25 | The Python code will have access to the full Unreal Engine Python API, including the 'unreal'
26 | module, allowing you to interact with and manipulate the Unreal Engine editor and its assets.
27 |
28 | Args:
29 | code: Python code to execute as a string. Can be multiple lines.
30 | file: Path to a Python script file to execute.
31 |
32 | Note:
33 | - You must provide either code or file, but not both.
34 | - The output of the Python code will be visible in the Unreal Engine log.
35 | - The Python code runs in the Unreal Engine process, so it has full access to the engine.
36 | - Be careful with destructive operations as they can affect your project.
37 |
38 | Examples:
39 | # Execute simple Python code
40 | execute_python(code="print('Hello from Unreal Engine!')")
41 |
42 | # Get information about the current level
43 | execute_python(code='''
44 | import unreal
45 | level = unreal.EditorLevelLibrary.get_editor_world()
46 | print(f"Current level: {level.get_name()}")
47 | actors = unreal.EditorLevelLibrary.get_all_level_actors()
48 | print(f"Number of actors: {len(actors)}")
49 | ''')
50 |
51 | # Execute a Python script file
52 | execute_python(file="D:/my_scripts/create_assets.py")
53 | """
54 | try:
55 | if not code and not file:
56 | return "Error: You must provide either 'code' or 'file' parameter"
57 |
58 | if code and file:
59 | return "Error: You can only provide either 'code' or 'file', not both"
60 |
61 | params = {}
62 | if code:
63 | params["code"] = code
64 | if file:
65 | params["file"] = file
66 |
67 | response = send_command("execute_python", params)
68 |
69 | # Handle the response
70 | if response["status"] == "success":
71 | return f"Python execution successful:\n{response['result']['output']}"
72 | elif response["status"] == "error":
73 | # New format with detailed error information
74 | result = response.get("result", {})
75 | output = result.get("output", "")
76 | error = result.get("error", "")
77 |
78 | # Format the response with both output and error information
79 | response_text = "Python execution failed with errors:\n\n"
80 |
81 | if output:
82 | response_text += f"--- Output ---\n{output}\n\n"
83 |
84 | if error:
85 | response_text += f"--- Error ---\n{error}"
86 |
87 | return response_text
88 | else:
89 | return f"Error: {response['message']}"
90 | except Exception as e:
91 | return f"Error executing Python: {str(e)}"
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Public/MCPExtensionHandler.h:
--------------------------------------------------------------------------------
```
1 | #pragma once
2 |
3 | #include "CoreMinimal.h"
4 | #include "MCPTCPServer.h"
5 | #include "Delegates/Delegate.h"
6 | #include "Json.h"
7 |
8 | /**
9 | * Delegate for handling MCP command execution
10 | * Used by the extension system to allow easy registration of custom command handlers
11 | */
12 | DECLARE_DELEGATE_RetVal_TwoParams(
13 | TSharedPtr<FJsonObject>, // Return type: JSON response
14 | FMCPCommandExecuteDelegate, // Delegate name
15 | const TSharedPtr<FJsonObject>&, // Parameter 1: Command parameters
16 | FSocket* // Parameter 2: Client socket
17 | );
18 |
19 | /**
20 | * Helper class for creating external command handlers
21 | * Makes it easy for external code to register custom commands with the MCP server
22 | */
23 | class UNREALMCP_API FMCPExtensionHandler : public IMCPCommandHandler
24 | {
25 | public:
26 | /**
27 | * Constructor
28 | * @param InCommandName - The command name this handler responds to
29 | * @param InExecuteDelegate - The delegate to execute when this command is received
30 | */
31 | FMCPExtensionHandler(const FString& InCommandName, const FMCPCommandExecuteDelegate& InExecuteDelegate)
32 | : CommandName(InCommandName)
33 | , ExecuteDelegate(InExecuteDelegate)
34 | {
35 | }
36 |
37 |
38 |
39 | /**
40 | * Get the command name this handler responds to
41 | * @return The command name
42 | */
43 | virtual FString GetCommandName() const override
44 | {
45 | return CommandName;
46 | }
47 |
48 | /**
49 | * Handle the command by executing the delegate
50 | * @param Params - The command parameters
51 | * @param ClientSocket - The client socket
52 | * @return JSON response object
53 | */
54 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
55 | {
56 | // If the delegate is bound, execute it
57 | if (ExecuteDelegate.IsBound())
58 | {
59 | return ExecuteDelegate.Execute(Params, ClientSocket);
60 | }
61 |
62 | // If the delegate is not bound, return an error
63 | TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
64 | Response->SetStringField("status", "error");
65 | Response->SetStringField("message", FString::Printf(TEXT("Command handler for '%s' has no bound execution delegate"), *CommandName));
66 | return Response;
67 | }
68 |
69 | private:
70 | /** The command name this handler responds to */
71 | FString CommandName;
72 |
73 | /** The delegate to execute when this command is received */
74 | FMCPCommandExecuteDelegate ExecuteDelegate;
75 | };
76 |
77 | /**
78 | * Helper utility for working with the MCP extension system
79 | */
80 | class UNREALMCP_API FMCPExtensionSystem
81 | {
82 | public:
83 | /**
84 | * Register a command handler with the server
85 | * @param Server - The MCP server
86 | * @param CommandName - The name of the command to register
87 | * @param ExecuteDelegate - The delegate to execute when the command is received
88 | * @return True if registration was successful
89 | */
90 | static bool RegisterCommand(FMCPTCPServer* Server, const FString& CommandName, const FMCPCommandExecuteDelegate& ExecuteDelegate)
91 | {
92 | if (!Server)
93 | {
94 | return false;
95 | }
96 |
97 | // Create a handler with the delegate
98 | TSharedPtr<FMCPExtensionHandler> Handler = MakeShared<FMCPExtensionHandler>(CommandName, ExecuteDelegate);
99 |
100 | // Register the handler with the server
101 | return Server->RegisterExternalCommandHandler(Handler);
102 | }
103 |
104 | /**
105 | * Unregister a command handler with the server
106 | * @param Server - The MCP server
107 | * @param CommandName - The name of the command to unregister
108 | * @return True if unregistration was successful
109 | */
110 | static bool UnregisterCommand(FMCPTCPServer* Server, const FString& CommandName)
111 | {
112 | if (!Server)
113 | {
114 | return false;
115 | }
116 |
117 | // Unregister the handler with the server
118 | return Server->UnregisterExternalCommandHandler(CommandName);
119 | }
120 | };
```
--------------------------------------------------------------------------------
/MCP/utils/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Utility functions for the UnrealMCP bridge."""
2 |
3 | import json
4 | import socket
5 | import sys
6 | import os
7 |
8 | # Try to get the port from MCPConstants
9 | DEFAULT_PORT = 13377
10 | DEFAULT_BUFFER_SIZE = 65536
11 | DEFAULT_TIMEOUT = 10 # 10 second timeout
12 |
13 | try:
14 | # Try to read the port from the C++ constants
15 | plugin_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), ".."))
16 | constants_path = os.path.join(plugin_dir, "Source", "UnrealMCP", "Public", "MCPConstants.h")
17 |
18 | if os.path.exists(constants_path):
19 | with open(constants_path, 'r') as f:
20 | constants_content = f.read()
21 |
22 | # Extract port from MCPConstants
23 | port_match = constants_content.find("DEFAULT_PORT = ")
24 | if port_match != -1:
25 | port_line = constants_content[port_match:].split(';')[0]
26 | DEFAULT_PORT = int(port_line.split('=')[1].strip())
27 |
28 | # Extract buffer size from MCPConstants
29 | buffer_match = constants_content.find("DEFAULT_RECEIVE_BUFFER_SIZE = ")
30 | if buffer_match != -1:
31 | buffer_line = constants_content[buffer_match:].split(';')[0]
32 | DEFAULT_BUFFER_SIZE = int(buffer_line.split('=')[1].strip())
33 | except Exception as e:
34 | # If anything goes wrong, use the defaults (which are already defined)
35 | print(f"Warning: Could not read constants from MCPConstants.h: {e}", file=sys.stderr)
36 |
37 | def send_command(command_type, params=None):
38 | """Send a command to the C++ MCP server and return the response."""
39 | try:
40 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
41 | s.settimeout(DEFAULT_TIMEOUT) # Set a timeout
42 | s.connect(("localhost", DEFAULT_PORT)) # Connect to Unreal C++ server
43 | command = {
44 | "type": command_type,
45 | "params": params or {}
46 | }
47 | s.sendall(json.dumps(command).encode('utf-8'))
48 |
49 | # Read response with a buffer
50 | chunks = []
51 | response_data = b''
52 |
53 | # Wait for data with timeout
54 | while True:
55 | try:
56 | chunk = s.recv(DEFAULT_BUFFER_SIZE)
57 | if not chunk: # Connection closed
58 | break
59 | chunks.append(chunk)
60 |
61 | # Try to parse what we have so far
62 | response_data = b''.join(chunks)
63 | try:
64 | # If we can parse it as JSON, we have a complete response
65 | json.loads(response_data.decode('utf-8'))
66 | break
67 | except json.JSONDecodeError:
68 | # Incomplete JSON, continue receiving
69 | continue
70 | except socket.timeout:
71 | # If we have some data but timed out, try to use what we have
72 | if response_data:
73 | break
74 | raise
75 |
76 | if not response_data:
77 | raise Exception("No data received from server")
78 |
79 | return json.loads(response_data.decode('utf-8'))
80 | except ConnectionRefusedError:
81 | print(f"Error: Could not connect to Unreal MCP server on localhost:{DEFAULT_PORT}.", file=sys.stderr)
82 | print("Make sure your Unreal Engine with MCP plugin is running.", file=sys.stderr)
83 | raise Exception("Failed to connect to Unreal MCP server: Connection refused")
84 | except socket.timeout:
85 | print("Error: Connection timed out while communicating with Unreal MCP server.", file=sys.stderr)
86 | raise Exception("Failed to communicate with Unreal MCP server: Connection timed out")
87 | except Exception as e:
88 | print(f"Error communicating with Unreal MCP server: {str(e)}", file=sys.stderr)
89 | raise Exception(f"Failed to communicate with Unreal MCP server: {str(e)}")
90 |
91 | __all__ = ['send_command']
```
--------------------------------------------------------------------------------
/MCP/TestScripts/simple_test_command.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Simple Test Command for MCP Server
4 |
5 | This script sends a very simple Python command to the MCP Server.
6 | """
7 |
8 | import socket
9 | import json
10 | import sys
11 |
12 | def main():
13 | """Send a simple Python command to the MCP Server."""
14 | try:
15 | # Create socket
16 | print("Connecting to MCP Server on localhost:13377...")
17 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
18 | s.settimeout(5) # 5 second timeout
19 |
20 | # Connect to server
21 | s.connect(("localhost", 13377))
22 | print("Connected successfully")
23 |
24 | # Very simple Python code
25 | code = 'import unreal\nreturn "Hello from Python!"'
26 |
27 | # Try different command formats
28 | formats = [
29 | {
30 | "name": "Format 1",
31 | "command": {
32 | "type": "execute_python",
33 | "code": code
34 | }
35 | },
36 | {
37 | "name": "Format 2",
38 | "command": {
39 | "command": "execute_python",
40 | "code": code
41 | }
42 | },
43 | {
44 | "name": "Format 3",
45 | "command": {
46 | "type": "execute_python",
47 | "data": {
48 | "code": code
49 | }
50 | }
51 | },
52 | {
53 | "name": "Format 4",
54 | "command": {
55 | "command": "execute_python",
56 | "type": "execute_python",
57 | "code": code
58 | }
59 | },
60 | {
61 | "name": "Format 5",
62 | "command": {
63 | "command": "execute_python",
64 | "type": "execute_python",
65 | "data": {
66 | "code": code
67 | }
68 | }
69 | }
70 | ]
71 |
72 | # Test each format
73 | for format_info in formats:
74 | format_name = format_info["name"]
75 | command = format_info["command"]
76 |
77 | print(f"\n=== Testing {format_name} ===")
78 | print(f"Command: {json.dumps(command)}")
79 |
80 | # Create a new socket for each test
81 | test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
82 | test_socket.settimeout(5)
83 | test_socket.connect(("localhost", 13377))
84 |
85 | # Send command with newline
86 | command_str = json.dumps(command) + "\n"
87 | test_socket.sendall(command_str.encode('utf-8'))
88 |
89 | # Receive response
90 | response = b""
91 | while True:
92 | data = test_socket.recv(4096)
93 | if not data:
94 | break
95 | response += data
96 | if b"\n" in data:
97 | break
98 |
99 | # Close socket
100 | test_socket.close()
101 |
102 | # Process response
103 | if response:
104 | response_str = response.decode('utf-8').strip()
105 | print(f"Response: {response_str}")
106 |
107 | try:
108 | response_json = json.loads(response_str)
109 | status = response_json.get('status', 'unknown')
110 |
111 | if status == 'success':
112 | print(f"✓ {format_name} SUCCEEDED")
113 | return True
114 | else:
115 | print(f"✗ {format_name} FAILED: {response_json.get('message', 'Unknown error')}")
116 | except json.JSONDecodeError as e:
117 | print(f"✗ Error parsing JSON response: {e}")
118 | else:
119 | print("✗ No response received")
120 |
121 | print("\nAll formats failed.")
122 | return False
123 |
124 | except Exception as e:
125 | print(f"Error: {e}")
126 | return False
127 |
128 | if __name__ == "__main__":
129 | print("=== Simple Test Command for MCP Server ===")
130 | success = main()
131 | sys.exit(0 if success else 1)
```
--------------------------------------------------------------------------------
/MCP/TestScripts/format_test.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python
2 | # format_test.py - Tests different command formats for MCP Server
3 |
4 | import socket
5 | import json
6 | import sys
7 | import time
8 |
9 | # Configuration
10 | HOST = '127.0.0.1'
11 | PORT = 13377
12 | TIMEOUT = 5 # seconds
13 |
14 | # Simple Python code to execute
15 | PYTHON_CODE = """
16 | import unreal
17 | return "Hello from Python!"
18 | """
19 |
20 | def send_command(command_dict):
21 | """Send a command to the MCP Server and return the response."""
22 | try:
23 | # Create a socket connection
24 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
25 | s.settimeout(TIMEOUT)
26 | s.connect((HOST, PORT))
27 |
28 | # Convert command to JSON and send
29 | command_json = json.dumps(command_dict)
30 | print(f"Sending command format: {command_json}")
31 | s.sendall(command_json.encode('utf-8'))
32 |
33 | # Receive response
34 | response = b""
35 | while True:
36 | chunk = s.recv(4096)
37 | if not chunk:
38 | break
39 | response += chunk
40 |
41 | # Parse and return response
42 | if response:
43 | try:
44 | return json.loads(response.decode('utf-8'))
45 | except json.JSONDecodeError:
46 | return {"status": "error", "message": "Invalid JSON response", "raw": response.decode('utf-8')}
47 | else:
48 | return {"status": "error", "message": "Empty response"}
49 | except socket.timeout:
50 | return {"status": "error", "message": "Connection timed out"}
51 | except ConnectionRefusedError:
52 | return {"status": "error", "message": "Connection refused. Is the MCP Server running?"}
53 | except Exception as e:
54 | return {"status": "error", "message": f"Error: {str(e)}"}
55 |
56 | def test_format(format_name, command_dict):
57 | """Test a specific command format and print the result."""
58 | print(f"\n=== Testing Format: {format_name} ===")
59 | response = send_command(command_dict)
60 | print(f"Response: {json.dumps(response, indent=2)}")
61 | if response.get("status") == "success":
62 | print(f"✅ SUCCESS: Format '{format_name}' works!")
63 | return True
64 | else:
65 | print(f"❌ FAILED: Format '{format_name}' does not work.")
66 | return False
67 |
68 | def main():
69 | print("=== MCP Server Command Format Test ===")
70 | print(f"Connecting to {HOST}:{PORT}")
71 |
72 | # Test different command formats
73 | formats = [
74 | ("Format 1: Basic", {
75 | "command": "execute_python",
76 | "code": PYTHON_CODE
77 | }),
78 |
79 | ("Format 2: Type field", {
80 | "type": "execute_python",
81 | "code": PYTHON_CODE
82 | }),
83 |
84 | ("Format 3: Command in data", {
85 | "command": "execute_python",
86 | "data": {
87 | "code": PYTHON_CODE
88 | }
89 | }),
90 |
91 | ("Format 4: Type in data", {
92 | "type": "execute_python",
93 | "data": {
94 | "code": PYTHON_CODE
95 | }
96 | }),
97 |
98 | ("Format 5: Command and params", {
99 | "command": "execute_python",
100 | "params": {
101 | "code": PYTHON_CODE
102 | }
103 | }),
104 |
105 | ("Format 6: Type and params", {
106 | "type": "execute_python",
107 | "params": {
108 | "code": PYTHON_CODE
109 | }
110 | }),
111 |
112 | ("Format 7: Command and type", {
113 | "command": "execute_python",
114 | "type": "python",
115 | "code": PYTHON_CODE
116 | }),
117 |
118 | ("Format 8: Command, type, and data", {
119 | "command": "execute_python",
120 | "type": "python",
121 | "data": {
122 | "code": PYTHON_CODE
123 | }
124 | })
125 | ]
126 |
127 | success_count = 0
128 | for format_name, command_dict in formats:
129 | if test_format(format_name, command_dict):
130 | success_count += 1
131 | time.sleep(1) # Brief pause between tests
132 |
133 | print(f"\n=== Test Summary ===")
134 | print(f"Tested {len(formats)} command formats")
135 | print(f"Successful formats: {success_count}")
136 | print(f"Failed formats: {len(formats) - success_count}")
137 |
138 | if __name__ == "__main__":
139 | main()
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Private/MCPExtensionExample.cpp:
--------------------------------------------------------------------------------
```cpp
1 | #include "MCPExtensionHandler.h"
2 | #include "MCPFileLogger.h"
3 |
4 | /**
5 | * Example showing how to use the MCP extension system
6 | *
7 | * This code demonstrates how external modules can extend the MCP server
8 | * with custom commands without modifying the MCP plugin code.
9 | */
10 | class FMCPExtensionExample
11 | {
12 | public:
13 | static void RegisterCustomCommands(FMCPTCPServer* Server)
14 | {
15 | if (!Server || !Server->IsRunning())
16 | {
17 | return;
18 | }
19 |
20 | // Register a custom "hello_world" command
21 | FMCPExtensionSystem::RegisterCommand(
22 | Server,
23 | "hello_world",
24 | FMCPCommandExecuteDelegate::CreateStatic(&FMCPExtensionExample::HandleHelloWorldCommand)
25 | );
26 |
27 | // Register a custom "echo" command
28 | FMCPExtensionSystem::RegisterCommand(
29 | Server,
30 | "echo",
31 | FMCPCommandExecuteDelegate::CreateStatic(&FMCPExtensionExample::HandleEchoCommand)
32 | );
33 | }
34 |
35 | static void UnregisterCustomCommands(FMCPTCPServer* Server)
36 | {
37 | if (!Server)
38 | {
39 | return;
40 | }
41 |
42 | // Unregister the custom commands
43 | FMCPExtensionSystem::UnregisterCommand(Server, "hello_world");
44 | FMCPExtensionSystem::UnregisterCommand(Server, "echo");
45 | }
46 |
47 | private:
48 | /**
49 | * Handle the "hello_world" command
50 | * @param Params - The command parameters
51 | * @param ClientSocket - The client socket
52 | * @return JSON response
53 | */
54 | static TSharedPtr<FJsonObject> HandleHelloWorldCommand(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
55 | {
56 | // Log that we received this command
57 | UE_LOG(LogMCP, Display, TEXT("Received hello_world command"));
58 |
59 | // Get the name parameter if provided
60 | FString Name = "World";
61 | Params->TryGetStringField(FStringView(TEXT("name")), Name);
62 |
63 | // Create the response
64 | TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
65 | Result->SetStringField("message", FString::Printf(TEXT("Hello, %s!"), *Name));
66 |
67 | // Create the success response with the result
68 | TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
69 | Response->SetStringField("status", "success");
70 | Response->SetObjectField("result", Result);
71 |
72 | return Response;
73 | }
74 |
75 | /**
76 | * Handle the "echo" command
77 | * @param Params - The command parameters
78 | * @param ClientSocket - The client socket
79 | * @return JSON response
80 | */
81 | static TSharedPtr<FJsonObject> HandleEchoCommand(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket)
82 | {
83 | // Log that we received this command
84 | UE_LOG(LogMCP, Display, TEXT("Received echo command"));
85 |
86 | // Create the response
87 | TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
88 | Response->SetStringField("status", "success");
89 |
90 | // Echo back all parameters as the result
91 | Response->SetObjectField("result", Params);
92 |
93 | return Response;
94 | }
95 | };
96 |
97 | // The following code shows how you might register these handlers in your own module
98 | // Uncomment this code and modify as needed for your project
99 |
100 | /*
101 | void YourGameModule::StartupModule()
102 | {
103 | // ... your existing code ...
104 |
105 | // Get a reference to the MCP server
106 | FUnrealMCPModule& MCPModule = FModuleManager::LoadModuleChecked<FUnrealMCPModule>("UnrealMCP");
107 | FMCPTCPServer* MCPServer = MCPModule.GetServer();
108 |
109 | if (MCPServer && MCPServer->IsRunning())
110 | {
111 | // Register custom commands
112 | FMCPExtensionExample::RegisterCustomCommands(MCPServer);
113 | }
114 | else
115 | {
116 | // The MCP server isn't running yet
117 | // You might want to set up a delegate to register when it starts
118 | // or expose a function in the MCP module that lets you register commands
119 | // that will be applied when the server starts
120 | }
121 | }
122 |
123 | void YourGameModule::ShutdownModule()
124 | {
125 | // Get a reference to the MCP server
126 | FUnrealMCPModule& MCPModule = FModuleManager::GetModulePtr<FUnrealMCPModule>("UnrealMCP");
127 | if (MCPModule)
128 | {
129 | FMCPTCPServer* MCPServer = MCPModule.GetServer();
130 | if (MCPServer)
131 | {
132 | // Unregister custom commands
133 | FMCPExtensionExample::UnregisterCustomCommands(MCPServer);
134 | }
135 | }
136 |
137 | // ... your existing code ...
138 | }
139 | */
```
--------------------------------------------------------------------------------
/MCP/Commands/commands_materials.py:
--------------------------------------------------------------------------------
```python
1 | """Material-related commands for Unreal Engine.
2 |
3 | This module contains all material-related commands for the UnrealMCP bridge,
4 | including creation, modification, and querying of materials.
5 | """
6 |
7 | import sys
8 | import os
9 | import importlib.util
10 | import importlib
11 | from mcp.server.fastmcp import Context
12 |
13 | # Import send_command from the parent module
14 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15 | from unreal_mcp_bridge import send_command
16 |
17 | def register_all(mcp):
18 | """Register all material-related commands with the MCP server."""
19 |
20 | # Create material command
21 | @mcp.tool()
22 | def create_material(ctx: Context, package_path: str, name: str, properties: dict = None) -> str:
23 | """Create a new material in the Unreal project.
24 |
25 | Args:
26 | package_path: The path where the material should be created (e.g., '/Game/Materials')
27 | name: The name of the material
28 | properties: Optional dictionary of material properties to set. Can include:
29 | - shading_model: str (e.g., "DefaultLit", "Unlit", "Subsurface", etc.)
30 | - blend_mode: str (e.g., "Opaque", "Masked", "Translucent", etc.)
31 | - two_sided: bool
32 | - dithered_lod_transition: bool
33 | - cast_contact_shadow: bool
34 | - base_color: list[float] (RGBA values 0-1)
35 | - metallic: float (0-1)
36 | - roughness: float (0-1)
37 | """
38 | try:
39 | params = {
40 | "package_path": package_path,
41 | "name": name
42 | }
43 | if properties:
44 | params["properties"] = properties
45 | response = send_command("create_material", params)
46 | if response["status"] == "success":
47 | return f"Created material: {response['result']['name']} at path: {response['result']['path']}"
48 | else:
49 | return f"Error: {response['message']}"
50 | except Exception as e:
51 | return f"Error creating material: {str(e)}"
52 |
53 | # Modify material command
54 | @mcp.tool()
55 | def modify_material(ctx: Context, path: str, properties: dict) -> str:
56 | """Modify an existing material's properties.
57 |
58 | Args:
59 | path: The full path to the material (e.g., '/Game/Materials/MyMaterial')
60 | properties: Dictionary of material properties to set. Can include:
61 | - shading_model: str (e.g., "DefaultLit", "Unlit", "Subsurface", etc.)
62 | - blend_mode: str (e.g., "Opaque", "Masked", "Translucent", etc.)
63 | - two_sided: bool
64 | - dithered_lod_transition: bool
65 | - cast_contact_shadow: bool
66 | - base_color: list[float] (RGBA values 0-1)
67 | - metallic: float (0-1)
68 | - roughness: float (0-1)
69 | """
70 | try:
71 | params = {
72 | "path": path,
73 | "properties": properties
74 | }
75 | response = send_command("modify_material", params)
76 | if response["status"] == "success":
77 | return f"Modified material: {response['result']['name']} at path: {response['result']['path']}"
78 | else:
79 | return f"Error: {response['message']}"
80 | except Exception as e:
81 | return f"Error modifying material: {str(e)}"
82 |
83 | # Get material info command
84 | @mcp.tool()
85 | def get_material_info(ctx: Context, path: str) -> dict:
86 | """Get information about a material.
87 |
88 | Args:
89 | path: The full path to the material (e.g., '/Game/Materials/MyMaterial')
90 |
91 | Returns:
92 | Dictionary containing material information including:
93 | - name: str
94 | - path: str
95 | - shading_model: str
96 | - blend_mode: str
97 | - two_sided: bool
98 | - dithered_lod_transition: bool
99 | - cast_contact_shadow: bool
100 | - base_color: list[float]
101 | - metallic: float
102 | - roughness: float
103 | """
104 | try:
105 | params = {"path": path}
106 | response = send_command("get_material_info", params)
107 | if response["status"] == "success":
108 | return response["result"]
109 | else:
110 | return {"error": response["message"]}
111 | except Exception as e:
112 | return {"error": str(e)}
```
--------------------------------------------------------------------------------
/MCP/temp_update_config.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Script to configure Claude Desktop MCP integration.
3 |
4 | This script will update or create the necessary configuration for Claude Desktop
5 | to use the Unreal MCP bridge.
6 | """
7 |
8 | import json
9 | import os
10 | import sys
11 | import shutil
12 | from pathlib import Path
13 |
14 | def check_claude_installed():
15 | """Check if Claude Desktop is installed by looking for common installation paths."""
16 | claude_paths = []
17 |
18 | if os.name == 'nt': # Windows
19 | claude_paths = [
20 | os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Claude Desktop', 'Claude Desktop.exe'),
21 | os.path.join(os.environ.get('PROGRAMFILES', ''), 'Claude Desktop', 'Claude Desktop.exe'),
22 | os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'Claude Desktop', 'Claude Desktop.exe')
23 | ]
24 | elif os.name == 'darwin': # macOS
25 | claude_paths = [
26 | '/Applications/Claude Desktop.app',
27 | os.path.expanduser('~/Applications/Claude Desktop.app')
28 | ]
29 |
30 | # Check if any of the paths exist
31 | for path in claude_paths:
32 | if os.path.exists(path):
33 | return True
34 |
35 | # Check if config directory exists as a fallback
36 | claude_config_dir = os.environ.get('APPDATA', '')
37 | if os.name == 'nt': # Windows
38 | claude_config_dir = os.path.join(claude_config_dir, 'Claude')
39 | elif os.name == 'darwin': # macOS
40 | claude_config_dir = os.path.expanduser('~/Library/Application Support/Claude')
41 |
42 | return os.path.exists(claude_config_dir)
43 |
44 | def update_claude_config(config_file, run_script):
45 | """Update the Claude Desktop configuration file."""
46 | # Check if Claude is installed
47 | if not check_claude_installed():
48 | print(f"Claude Desktop doesn't appear to be installed on this system.")
49 | print("You can download Claude Desktop from: https://claude.ai/download")
50 | print("After installing Claude Desktop, run this script again.")
51 | return False
52 |
53 | # Make sure the config directory exists
54 | config_dir = os.path.dirname(config_file)
55 | if not os.path.exists(config_dir):
56 | try:
57 | print(f"Creating Claude configuration directory: {config_dir}")
58 | os.makedirs(config_dir, exist_ok=True)
59 | except Exception as e:
60 | print(f"Error creating Claude configuration directory: {str(e)}")
61 | return False
62 |
63 | # Load existing config or create new one
64 | config = {}
65 | try:
66 | if os.path.exists(config_file):
67 | with open(config_file, 'r') as f:
68 | config = json.load(f)
69 | except (FileNotFoundError, json.JSONDecodeError):
70 | print(f"Creating new Claude Desktop configuration file.")
71 |
72 | # Create backup of the original file if it exists
73 | if os.path.exists(config_file):
74 | backup_path = config_file + '.bak'
75 | try:
76 | shutil.copy2(config_file, backup_path)
77 | print(f"Created backup of original configuration at: {backup_path}")
78 | except Exception as e:
79 | print(f"Warning: Couldn't create backup: {str(e)}")
80 |
81 | # Update the config
82 | config.setdefault('mcpServers', {})['unreal'] = {'command': run_script, 'args': []}
83 |
84 | # Save the updated configuration
85 | try:
86 | with open(config_file, 'w') as f:
87 | json.dump(config, f, indent=4)
88 |
89 | print(f"Successfully updated Claude Desktop configuration at: {config_file}")
90 | return True
91 | except Exception as e:
92 | print(f"Error saving Claude Desktop configuration: {str(e)}")
93 | return False
94 |
95 | def main():
96 | if len(sys.argv) < 3:
97 | print("Usage: python temp_update_config.py <config_file> <run_script>")
98 | return 1
99 |
100 | config_file = sys.argv[1]
101 | run_script = sys.argv[2]
102 |
103 | # Get absolute paths
104 | config_file = os.path.abspath(config_file)
105 | run_script = os.path.abspath(run_script)
106 |
107 | if not os.path.exists(run_script):
108 | print(f"Error: Run script not found at: {run_script}")
109 | return 1
110 |
111 | # Update the configuration
112 | if update_claude_config(config_file, run_script):
113 | print("\nClaude Desktop has been configured to use Unreal MCP!")
114 | print("\nTo use with Claude Desktop:")
115 | print("1. Make sure Unreal Engine with MCP plugin is running")
116 | print("2. Start Claude Desktop and it should automatically use the Unreal MCP tools")
117 | return 0
118 | else:
119 | print("\nFailed to configure Claude Desktop for Unreal MCP.")
120 | return 1
121 |
122 | if __name__ == "__main__":
123 | sys.exit(main())
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Private/MCPFileLogger.h:
--------------------------------------------------------------------------------
```
1 | #pragma once
2 |
3 | #include "CoreMinimal.h"
4 | #include "Misc/FileHelper.h"
5 | #include "HAL/PlatformFilemanager.h"
6 | #include "UnrealMCP.h"
7 |
8 | // Shorthand for logger
9 | #define MCP_LOG(Verbosity, Format, ...) FMCPFileLogger::Get().Log(ELogVerbosity::Verbosity, FString::Printf(TEXT(Format), ##__VA_ARGS__))
10 | #define MCP_LOG_INFO(Format, ...) FMCPFileLogger::Get().Info(FString::Printf(TEXT(Format), ##__VA_ARGS__))
11 | #define MCP_LOG_ERROR(Format, ...) FMCPFileLogger::Get().Error(FString::Printf(TEXT(Format), ##__VA_ARGS__))
12 | #define MCP_LOG_WARNING(Format, ...) FMCPFileLogger::Get().Warning(FString::Printf(TEXT(Format), ##__VA_ARGS__))
13 | #define MCP_LOG_VERBOSE(Format, ...) FMCPFileLogger::Get().Verbose(FString::Printf(TEXT(Format), ##__VA_ARGS__))
14 |
15 | /**
16 | * Simple file logger for MCP operations
17 | * Writes logs to a file in the plugin directory
18 | */
19 | class FMCPFileLogger
20 | {
21 | public:
22 | static FMCPFileLogger& Get()
23 | {
24 | static FMCPFileLogger Instance;
25 | return Instance;
26 | }
27 |
28 | void Initialize(const FString& InLogFilePath)
29 | {
30 | LogFilePath = InLogFilePath;
31 |
32 | // Create or clear the log file
33 | FString LogDirectory = FPaths::GetPath(LogFilePath);
34 | IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
35 |
36 | if (!PlatformFile.DirectoryExists(*LogDirectory))
37 | {
38 | PlatformFile.CreateDirectoryTree(*LogDirectory);
39 | }
40 |
41 | // Clear the file and write a header
42 | FString Header = FString::Printf(TEXT("MCP Server Log - Started at %s\n"), *FDateTime::Now().ToString());
43 | FFileHelper::SaveStringToFile(Header, *LogFilePath);
44 |
45 | bInitialized = true;
46 | UE_LOG(LogMCP, Log, TEXT("MCP File Logger initialized at %s"), *LogFilePath);
47 | }
48 |
49 | // Log with verbosity level
50 | void Log(ELogVerbosity::Type Verbosity, const FString& Message)
51 | {
52 | if (!bInitialized) return;
53 |
54 | // Log to Unreal's logging system - need to handle each verbosity level separately
55 | switch (Verbosity)
56 | {
57 | case ELogVerbosity::Fatal:
58 | UE_LOG(LogMCP, Fatal, TEXT("%s"), *Message);
59 | break;
60 | case ELogVerbosity::Error:
61 | UE_LOG(LogMCP, Error, TEXT("%s"), *Message);
62 | break;
63 | case ELogVerbosity::Warning:
64 | UE_LOG(LogMCP, Warning, TEXT("%s"), *Message);
65 | break;
66 | case ELogVerbosity::Display:
67 | UE_LOG(LogMCP, Display, TEXT("%s"), *Message);
68 | break;
69 | case ELogVerbosity::Log:
70 | UE_LOG(LogMCP, Log, TEXT("%s"), *Message);
71 | break;
72 | case ELogVerbosity::Verbose:
73 | UE_LOG(LogMCP, Verbose, TEXT("%s"), *Message);
74 | break;
75 | case ELogVerbosity::VeryVerbose:
76 | UE_LOG(LogMCP, VeryVerbose, TEXT("%s"), *Message);
77 | break;
78 | default:
79 | UE_LOG(LogMCP, Log, TEXT("%s"), *Message);
80 | break;
81 | }
82 |
83 | // Also log to file
84 | FString TimeStamp = FDateTime::Now().ToString();
85 | FString VerbosityStr;
86 |
87 | switch (Verbosity)
88 | {
89 | case ELogVerbosity::Fatal: VerbosityStr = TEXT("Fatal"); break;
90 | case ELogVerbosity::Error: VerbosityStr = TEXT("Error"); break;
91 | case ELogVerbosity::Warning: VerbosityStr = TEXT("Warning"); break;
92 | case ELogVerbosity::Display: VerbosityStr = TEXT("Display"); break;
93 | case ELogVerbosity::Log: VerbosityStr = TEXT("Log"); break;
94 | case ELogVerbosity::Verbose: VerbosityStr = TEXT("Verbose"); break;
95 | case ELogVerbosity::VeryVerbose: VerbosityStr = TEXT("VeryVerbose"); break;
96 | default: VerbosityStr = TEXT("Unknown"); break;
97 | }
98 |
99 | FString LogEntry = FString::Printf(TEXT("[%s][%s] %s\n"), *TimeStamp, *VerbosityStr, *Message);
100 | FFileHelper::SaveStringToFile(LogEntry, *LogFilePath, FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), EFileWrite::FILEWRITE_Append);
101 | }
102 |
103 | // Convenience methods for different verbosity levels
104 | void Error(const FString& Message) { Log(ELogVerbosity::Error, Message); }
105 | void Warning(const FString& Message) { Log(ELogVerbosity::Warning, Message); }
106 | void Info(const FString& Message) { Log(ELogVerbosity::Log, Message); }
107 | void Verbose(const FString& Message) { Log(ELogVerbosity::Verbose, Message); }
108 |
109 | // For backward compatibility
110 | void Log(const FString& Message) { Info(Message); }
111 |
112 | private:
113 | FMCPFileLogger() : bInitialized(false) {}
114 | ~FMCPFileLogger() {}
115 |
116 | // Make non-copyable
117 | FMCPFileLogger(const FMCPFileLogger&) = delete;
118 | FMCPFileLogger& operator=(const FMCPFileLogger&) = delete;
119 |
120 | bool bInitialized;
121 | FString LogFilePath;
122 | };
```
--------------------------------------------------------------------------------
/MCP/cursor_setup.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Script to configure Cursor MCP integration.
3 |
4 | This script will update or create the necessary configuration for Cursor
5 | to use the Unreal MCP bridge.
6 | """
7 |
8 | import json
9 | import os
10 | import sys
11 | import shutil
12 | import argparse
13 | from pathlib import Path
14 |
15 | def get_cursor_config_dir():
16 | """Get the Cursor configuration directory based on OS."""
17 | if os.name == 'nt': # Windows
18 | appdata = os.environ.get('APPDATA', '')
19 | return os.path.join(appdata, 'Cursor', 'User')
20 | elif os.name == 'darwin': # macOS
21 | return os.path.expanduser('~/Library/Application Support/Cursor/User')
22 | else: # Linux
23 | return os.path.expanduser('~/.config/Cursor/User')
24 |
25 | def check_cursor_installed():
26 | """Check if Cursor is installed by looking for common installation paths."""
27 | cursor_paths = []
28 |
29 | if os.name == 'nt': # Windows
30 | cursor_paths = [
31 | os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Cursor', 'Cursor.exe'),
32 | os.path.join(os.environ.get('PROGRAMFILES', ''), 'Cursor', 'Cursor.exe'),
33 | os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'Cursor', 'Cursor.exe')
34 | ]
35 | elif os.name == 'darwin': # macOS
36 | cursor_paths = [
37 | '/Applications/Cursor.app',
38 | os.path.expanduser('~/Applications/Cursor.app')
39 | ]
40 | else: # Linux
41 | cursor_paths = [
42 | '/usr/bin/cursor',
43 | '/usr/local/bin/cursor',
44 | os.path.expanduser('~/.local/bin/cursor')
45 | ]
46 |
47 | # Check if any of the paths exist
48 | for path in cursor_paths:
49 | if os.path.exists(path):
50 | return True
51 |
52 | # Check if config directory exists as a fallback
53 | return os.path.exists(get_cursor_config_dir())
54 |
55 | def configure_cursor_mcp(run_script_path):
56 | """Configure Cursor to use the Unreal MCP bridge."""
57 | # First check if Cursor is installed
58 | if not check_cursor_installed():
59 | print(f"Cursor doesn't appear to be installed on this system.")
60 | print("You can download Cursor from: https://cursor.sh/")
61 | print("After installing Cursor, run this script again.")
62 | return False
63 |
64 | cursor_config_dir = get_cursor_config_dir()
65 |
66 | if not os.path.exists(cursor_config_dir):
67 | try:
68 | print(f"Creating Cursor configuration directory: {cursor_config_dir}")
69 | os.makedirs(cursor_config_dir, exist_ok=True)
70 | except Exception as e:
71 | print(f"Error creating Cursor configuration directory: {str(e)}")
72 | return False
73 |
74 | # Create settings.json path
75 | settings_path = os.path.join(cursor_config_dir, 'settings.json')
76 |
77 | # Load existing settings or create new ones
78 | settings = {}
79 | if os.path.exists(settings_path):
80 | try:
81 | with open(settings_path, 'r') as f:
82 | settings = json.load(f)
83 | except (json.JSONDecodeError, FileNotFoundError):
84 | print(f"Could not read existing settings file, creating new one.")
85 |
86 | # Ensure the settings structure exists
87 | settings.setdefault('mcp', {})
88 |
89 | # Add UnrealMCP to the MCP servers list
90 | settings['mcp'].setdefault('servers', {})
91 |
92 | # Configure the Unreal MCP server
93 | settings['mcp']['servers']['unreal'] = {
94 | 'command': run_script_path,
95 | 'args': []
96 | }
97 |
98 | # Enable MCP in Cursor
99 | settings['mcp']['enabled'] = True
100 |
101 | # Save the updated settings file
102 | try:
103 | # Create backup of the original file if it exists
104 | if os.path.exists(settings_path):
105 | backup_path = settings_path + '.bak'
106 | shutil.copy2(settings_path, backup_path)
107 | print(f"Created backup of original settings at: {backup_path}")
108 |
109 | # Write the new settings
110 | with open(settings_path, 'w') as f:
111 | json.dump(settings, f, indent=4)
112 |
113 | print(f"Successfully updated Cursor settings at: {settings_path}")
114 | print("Please restart Cursor for the changes to take effect.")
115 | return True
116 | except Exception as e:
117 | print(f"Error saving Cursor settings: {str(e)}")
118 | return False
119 |
120 | def main():
121 | parser = argparse.ArgumentParser(description='Configure Cursor for Unreal MCP')
122 | parser.add_argument('--script', help='Path to the run_unreal_mcp.bat script',
123 | default=os.path.abspath(os.path.join(os.path.dirname(__file__), 'run_unreal_mcp.bat')))
124 |
125 | args = parser.parse_args()
126 |
127 | # Get absolute path to the run script
128 | run_script_path = os.path.abspath(args.script)
129 |
130 | if not os.path.exists(run_script_path):
131 | print(f"Error: Run script not found at: {run_script_path}")
132 | return 1
133 |
134 | # Configure Cursor
135 | if configure_cursor_mcp(run_script_path):
136 | print("\nCursor has been configured to use Unreal MCP!")
137 | print("\nTo use with Cursor:")
138 | print("1. Make sure Unreal Engine with MCP plugin is running")
139 | print("2. Start Cursor and it should automatically use the Unreal MCP tools")
140 | return 0
141 | else:
142 | print("\nFailed to configure Cursor for Unreal MCP.")
143 | return 1
144 |
145 | if __name__ == "__main__":
146 | sys.exit(main())
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Public/MCPCommandHandlers.h:
--------------------------------------------------------------------------------
```
1 | #pragma once
2 |
3 | #include "CoreMinimal.h"
4 | #include "MCPTCPServer.h"
5 | #include "Engine/World.h"
6 | #include "Engine/StaticMeshActor.h"
7 | #include "Components/StaticMeshComponent.h"
8 |
9 | /**
10 | * Base class for MCP command handlers
11 | */
12 | class FMCPCommandHandlerBase : public IMCPCommandHandler
13 | {
14 | public:
15 | /**
16 | * Constructor
17 | * @param InCommandName - The command name this handler responds to
18 | */
19 | explicit FMCPCommandHandlerBase(const FString& InCommandName)
20 | : CommandName(InCommandName)
21 | {
22 | }
23 |
24 | /**
25 | * Get the command name this handler responds to
26 | * @return The command name
27 | */
28 | virtual FString GetCommandName() const override
29 | {
30 | return CommandName;
31 | }
32 |
33 | protected:
34 | /**
35 | * Create an error response
36 | * @param Message - The error message
37 | * @return JSON response object
38 | */
39 | TSharedPtr<FJsonObject> CreateErrorResponse(const FString& Message)
40 | {
41 | TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
42 | Response->SetStringField("status", "error");
43 | Response->SetStringField("message", Message);
44 | return Response;
45 | }
46 |
47 | /**
48 | * Create a success response
49 | * @param Result - Optional result object
50 | * @return JSON response object
51 | */
52 | TSharedPtr<FJsonObject> CreateSuccessResponse(TSharedPtr<FJsonObject> Result = nullptr)
53 | {
54 | TSharedPtr<FJsonObject> Response = MakeShared<FJsonObject>();
55 | Response->SetStringField("status", "success");
56 | if (Result.IsValid())
57 | {
58 | Response->SetObjectField("result", Result);
59 | }
60 | return Response;
61 | }
62 |
63 | /** The command name this handler responds to */
64 | FString CommandName;
65 | };
66 |
67 | /**
68 | * Handler for the get_scene_info command
69 | */
70 | class FMCPGetSceneInfoHandler : public FMCPCommandHandlerBase
71 | {
72 | public:
73 | FMCPGetSceneInfoHandler()
74 | : FMCPCommandHandlerBase("get_scene_info")
75 | {
76 | }
77 |
78 | /**
79 | * Execute the get_scene_info command
80 | * @param Params - The command parameters
81 | * @param ClientSocket - The client socket
82 | * @return JSON response object
83 | */
84 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
85 | };
86 |
87 | /**
88 | * Handler for the create_object command
89 | */
90 | class FMCPCreateObjectHandler : public FMCPCommandHandlerBase
91 | {
92 | public:
93 | FMCPCreateObjectHandler()
94 | : FMCPCommandHandlerBase("create_object")
95 | {
96 | }
97 |
98 | /**
99 | * Execute the create_object command
100 | * @param Params - The command parameters
101 | * @param ClientSocket - The client socket
102 | * @return JSON response object
103 | */
104 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
105 |
106 | protected:
107 | /**
108 | * Create a static mesh actor
109 | * @param World - The world to create the actor in
110 | * @param Location - The location to create the actor at
111 | * @param MeshPath - Optional path to the mesh to use
112 | * @param Label - Optional custom label for the actor in the outliner
113 | * @return The created actor and a success flag
114 | */
115 | TPair<AStaticMeshActor*, bool> CreateStaticMeshActor(UWorld* World, const FVector& Location, const FString& MeshPath = "", const FString& Label = "");
116 |
117 | /**
118 | * Create a cube actor
119 | * @param World - The world to create the actor in
120 | * @param Location - The location to create the actor at
121 | * @param Label - Optional custom label for the actor in the outliner
122 | * @return The created actor and a success flag
123 | */
124 | TPair<AStaticMeshActor*, bool> CreateCubeActor(UWorld* World, const FVector& Location, const FString& Label = "");
125 | };
126 |
127 | /**
128 | * Handler for the modify_object command
129 | */
130 | class FMCPModifyObjectHandler : public FMCPCommandHandlerBase
131 | {
132 | public:
133 | FMCPModifyObjectHandler()
134 | : FMCPCommandHandlerBase("modify_object")
135 | {
136 | }
137 |
138 | /**
139 | * Execute the modify_object command
140 | * @param Params - The command parameters
141 | * @param ClientSocket - The client socket
142 | * @return JSON response object
143 | */
144 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
145 | };
146 |
147 | /**
148 | * Handler for the delete_object command
149 | */
150 | class FMCPDeleteObjectHandler : public FMCPCommandHandlerBase
151 | {
152 | public:
153 | FMCPDeleteObjectHandler()
154 | : FMCPCommandHandlerBase("delete_object")
155 | {
156 | }
157 |
158 | /**
159 | * Execute the delete_object command
160 | * @param Params - The command parameters
161 | * @param ClientSocket - The client socket
162 | * @return JSON response object
163 | */
164 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
165 | };
166 |
167 | /**
168 | * Handler for the execute_python command
169 | */
170 | class FMCPExecutePythonHandler : public FMCPCommandHandlerBase
171 | {
172 | public:
173 | FMCPExecutePythonHandler()
174 | : FMCPCommandHandlerBase("execute_python")
175 | {
176 | }
177 |
178 | /**
179 | * Execute the execute_python command
180 | * @param Params - The command parameters
181 | * @param ClientSocket - The client socket
182 | * @return JSON response object
183 | */
184 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) override;
185 | };
```
--------------------------------------------------------------------------------
/MCP/TestScripts/3_string_test.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | String Handling Test for MCP Server
4 |
5 | This script tests string handling in the MCP Server.
6 | It connects to the server, sends Python code with various string formats,
7 | and verifies they are handled correctly.
8 | """
9 |
10 | import socket
11 | import json
12 | import sys
13 |
14 | def main():
15 | """Connect to the MCP Server and test string handling."""
16 | try:
17 | # Create socket
18 | print("Connecting to MCP Server on localhost:13377...")
19 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
20 | s.settimeout(10) # 10 second timeout
21 |
22 | # Connect to server
23 | s.connect(("localhost", 13377))
24 | print("✓ Connected successfully")
25 |
26 | # Python code with various string formats
27 | code = """
28 | import unreal
29 | import json
30 |
31 | # Test 1: Basic string types
32 | test_string1 = "This is a simple string with double quotes"
33 | test_string2 = 'This is a simple string with single quotes'
34 | test_string3 = \"\"\"This is a
35 | multiline string\"\"\"
36 | test_string4 = f"This is an f-string with {'interpolation'}"
37 | test_string5 = "This string has escape sequences: \\n \\t \\\\ \\' \\""
38 | test_string6 = r"This is a raw string with no escape processing: \n \t \\"
39 |
40 | # Test 2: Print statements
41 | print("Print statement 1: Simple string")
42 | print(f"Print statement 2: F-string with {test_string2}")
43 | print("Print statement 3: Multiple", "arguments", 123, test_string3)
44 | print("Print statement 4: With escape sequences: \\n \\t")
45 |
46 | # Test 3: Potentially problematic strings
47 | test_string7 = "String with quotes: 'single' and \\"double\\""
48 | test_string8 = 'String with quotes: "double" and \\'single\\''
49 | test_string9 = "String with backslashes: \\ \\\\ \\n"
50 | test_string10 = "String with special characters: 🐍 😊 🚀"
51 |
52 | # Test 4: Unterminated strings (these are properly terminated but might be misinterpreted)
53 | test_string11 = "String with a quote at the end: '"
54 | test_string12 = 'String with a quote at the end: "'
55 | test_string13 = "String with a backslash at the end: \\"
56 | test_string14 = "String with multiple backslashes at the end: \\\\"
57 |
58 | # Test 5: String concatenation
59 | test_string15 = "Part 1 " + "Part 2"
60 | test_string16 = "Multiple " + "parts " + "concatenated"
61 | test_string17 = "Mixed " + 'quote' + " types"
62 |
63 | # Collect results in a dictionary
64 | results = {
65 | "test1": test_string1,
66 | "test2": test_string2,
67 | "test3": test_string3,
68 | "test4": test_string4,
69 | "test5": test_string5,
70 | "test6": test_string6,
71 | "test7": test_string7,
72 | "test8": test_string8,
73 | "test9": test_string9,
74 | "test10": test_string10,
75 | "test11": test_string11,
76 | "test12": test_string12,
77 | "test13": test_string13,
78 | "test14": test_string14,
79 | "test15": test_string15,
80 | "test16": test_string16,
81 | "test17": test_string17
82 | }
83 |
84 | # Log the results
85 | unreal.log("===== STRING TEST RESULTS =====")
86 | for key, value in results.items():
87 | unreal.log(f"{key}: {value}")
88 |
89 | # Return the results as JSON
90 | return json.dumps(results, indent=2)
91 | """
92 |
93 | # Create command with multiple formats to try to make it work
94 | command = {
95 | "command": "execute_python",
96 | "type": "execute_python",
97 | "code": code,
98 | "data": {
99 | "code": code
100 | }
101 | }
102 |
103 | # Send command
104 | print("Sending execute_python command...")
105 | command_str = json.dumps(command) + "\n" # Add newline
106 | s.sendall(command_str.encode('utf-8'))
107 |
108 | # Receive response
109 | print("Waiting for response...")
110 | response = b""
111 | while True:
112 | data = s.recv(4096)
113 | if not data:
114 | break
115 | response += data
116 | if b"\n" in data: # Check for newline which indicates end of response
117 | break
118 |
119 | # Close connection
120 | s.close()
121 | print("✓ Connection closed properly")
122 |
123 | # Process response
124 | if response:
125 | response_str = response.decode('utf-8').strip()
126 |
127 | try:
128 | response_json = json.loads(response_str)
129 | print("\n=== RESPONSE ===")
130 | print(f"Status: {response_json.get('status', 'unknown')}")
131 |
132 | if response_json.get('status') == 'success':
133 | print("✓ String test successful")
134 | result = response_json.get('result', {})
135 | if isinstance(result, dict) and 'output' in result:
136 | output = result['output']
137 | print(f"Output length: {len(output)} characters")
138 | print("First 200 characters of output:")
139 | print(output[:200] + "..." if len(output) > 200 else output)
140 | return True
141 | else:
142 | print("✗ String test failed")
143 | print(f"Error: {response_json.get('message', 'Unknown error')}")
144 | return False
145 | except json.JSONDecodeError as e:
146 | print(f"✗ Error parsing JSON response: {e}")
147 | print(f"Raw response: {response_str}")
148 | return False
149 | else:
150 | print("✗ No response received from server")
151 | return False
152 |
153 | except ConnectionRefusedError:
154 | print("✗ Connection refused. Is the MCP Server running?")
155 | return False
156 | except socket.timeout:
157 | print("✗ Connection timed out. Is the MCP Server running?")
158 | return False
159 | except Exception as e:
160 | print(f"✗ Error: {e}")
161 | return False
162 |
163 | if __name__ == "__main__":
164 | print("=== MCP Server String Handling Test ===")
165 | success = main()
166 | print("\n=== TEST RESULT ===")
167 | if success:
168 | print("✓ String handling test PASSED")
169 | sys.exit(0)
170 | else:
171 | print("✗ String handling test FAILED")
172 | sys.exit(1)
```
--------------------------------------------------------------------------------
/MCP/README_MCP_SETUP.md:
--------------------------------------------------------------------------------
```markdown
1 | Here's the README in Markdown format (already used in the previous response, but I'll ensure it's clean and ready for copy-pasting). You can directly copy this into a `README.md` file for your GitHub repository:
2 |
3 | ```markdown
4 | # Unreal Engine MCP Interface
5 |
6 | This project provides a Model Context Protocol (MCP) interface for Unreal Engine, enabling seamless integration with Claude Desktop and Cursor. With this interface, users can interact with Unreal Engine using natural language commands through their preferred AI assistant, simplifying scene management and object manipulation.
7 |
8 | ## Table of Contents
9 |
10 | - [Prerequisites](#prerequisites)
11 | - [Quick Setup](#quick-setup)
12 | - [Manual Configuration](#manual-configuration)
13 | - [Troubleshooting](#troubleshooting)
14 | - [Usage](#usage)
15 | - [Available Commands](#available-commands)
16 | - [Testing the MCP Server Directly](#testing-the-mcp-server-directly)
17 |
18 | ## Prerequisites
19 |
20 | To set up the MCP interface, ensure you have the following:
21 |
22 | - **Python 3.7 or newer** installed on your system
23 | - **Claude Desktop** application or **Cursor** application
24 | - **Unreal Engine** with the UnrealMCP plugin enabled
25 |
26 | ## Quick Setup
27 |
28 | The setup process is streamlined with a single script that handles all installation scenarios:
29 |
30 | 1. Navigate to the `Plugins\UnrealMCP\MCP\` directory.
31 | 2. Run the following script:
32 | ```
33 | Plugins\UnrealMCP\MCP\setup_unreal_mcp.bat
34 | ```
35 |
36 | This script will:
37 |
38 | - Detect available Python environments (System Python, Miniconda/Anaconda, Claude Desktop environment, Cursor environment)
39 | - Prompt you to choose a Python environment
40 | - Install the required `mcp` package in the selected environment
41 | - Generate a `run_unreal_mcp.bat` script tailored to the chosen Python environment
42 | - Prompt you to configure Claude Desktop, Cursor, both, or skip configuration
43 | - Create or update the configuration files for the selected AI assistants
44 |
45 | ### Command Line Options
46 |
47 | The setup script supports the following command line options:
48 |
49 | ```
50 | setup_unreal_mcp.bat [OPTIONS]
51 |
52 | Options:
53 | --help Show help message
54 | --configure-claude Configure Claude Desktop (default)
55 | --configure-cursor Configure Cursor
56 | --configure-both Configure both Claude and Cursor
57 | --skip-config Skip configuration
58 | ```
59 |
60 | ### Python Environment Options
61 |
62 | The setup script supports multiple Python environment options:
63 |
64 | 1. **System Python**: Uses the Python installation in your system PATH.
65 | 2. **Miniconda/Anaconda**: Uses a Python environment from Miniconda/Anaconda (recommended for users integrating with Blender via Claude Desktop).
66 | 3. **Claude Desktop Environment**: Uses the Python environment bundled with Claude Desktop (if available).
67 | 4. **Cursor Environment**: Uses the Python environment bundled with Cursor (if available).
68 |
69 | ## Manual Configuration
70 |
71 | For manual setup, follow these steps:
72 |
73 | ### 1. Install Required Python Package
74 |
75 | Install the `mcp` package with the following command:
76 |
77 | ```bash
78 | python -m pip install mcp>=0.1.0
79 | ```
80 |
81 | ### 2. Create a Run Script
82 |
83 | Create a batch file named `run_unreal_mcp.bat` with this content:
84 |
85 | ```batch
86 | @echo off
87 | setlocal
88 | cd /d "%~dp0"
89 | python "%~dp0unreal_mcp_server.py"
90 | ```
91 |
92 | Save it in the `Plugins\UnrealMCP\MCP\` directory.
93 |
94 | ### 3. Configure Claude Desktop
95 |
96 | Locate or create the Claude Desktop configuration file at:
97 |
98 | ```
99 | %APPDATA%\Claude\claude_desktop_config.json
100 | ```
101 |
102 | Add or update it with the following content, replacing the path with the actual location of your `run_unreal_mcp.bat`:
103 |
104 | ```json
105 | {
106 | "mcpServers": {
107 | "unreal": {
108 | "command": "C:\\Path\\To\\Your\\Plugins\\UnrealMCP\\MCP\\run_unreal_mcp.bat",
109 | "args": []
110 | }
111 | }
112 | }
113 | ```
114 |
115 | ### 4. Configure Cursor
116 |
117 | Locate or create the Cursor settings file at:
118 |
119 | ```
120 | %APPDATA%\Cursor\User\settings.json
121 | ```
122 |
123 | Add or update it with the following MCP configuration, replacing the path with the actual location of your `run_unreal_mcp.bat`:
124 |
125 | ```json
126 | {
127 | "mcp": {
128 | "enabled": true,
129 | "servers": {
130 | "unreal": {
131 | "command": "C:\\Path\\To\\Your\\Plugins\\UnrealMCP\\MCP\\run_unreal_mcp.bat",
132 | "args": []
133 | }
134 | }
135 | }
136 | }
137 | ```
138 |
139 | ## Troubleshooting
140 |
141 | ### Common Issues
142 |
143 | 1. **"No module named 'mcp'"**
144 | - **Cause**: The `mcp` package isn't installed in the Python environment used by Claude Desktop or Cursor.
145 | - **Solution**: Rerun the `setup_unreal_mcp.bat` script and select the correct Python environment.
146 |
147 | 2. **Connection refused errors**
148 | - **Cause**: The MCP server isn't running or isn't listening on port 13377.
149 | - **Solution**:
150 | - Ensure Unreal Engine is running with the MCP plugin enabled.
151 | - Confirm the MCP plugin's port setting matches the default (13377).
152 |
153 | 3. **Claude Desktop or Cursor can't start the MCP server**
154 | - **Cause**: Configuration or file path issues.
155 | - **Solution**:
156 | - For Claude: Check the logs at: `%APPDATA%\Claude\logs\mcp-server-unreal.log`
157 | - Verify the path in the configuration file is correct.
158 | - Ensure `run_unreal_mcp.bat` exists and references the correct Python interpreter.
159 |
160 | ### Checking Logs
161 |
162 | Claude Desktop logs MCP server output to:
163 |
164 | ```
165 | %APPDATA%\Claude\logs\mcp-server-unreal.log
166 | ```
167 |
168 | Review this file for detailed error messages.
169 |
170 | ## Usage
171 |
172 | To use the MCP interface:
173 |
174 | 1. Launch your Unreal Engine project with the MCP plugin enabled.
175 | 2. Open Claude Desktop or Cursor.
176 | 3. Use natural language commands in your AI assistant, such as:
177 | - "Show me what's in the current Unreal scene"
178 | - "Create a cube at position [0, 0, 100]"
179 | - "Modify the object named 'Cube_1' to have scale [2, 2, 2]"
180 | - "Delete the object named 'Cube_1'"
181 |
182 | ## Available Commands
183 |
184 | The MCP interface supports these commands:
185 |
186 | - **`get_scene_info`**: Retrieves details about the current scene.
187 | - **`create_object`**: Spawns a new object in the scene.
188 | - **`modify_object`**: Updates properties of an existing object.
189 | - **`delete_object`**: Removes an object from the scene.
190 |
191 | ## Testing the MCP Server Directly
192 |
193 | To test the MCP server independently of Claude Desktop or Cursor:
194 |
195 | 1. Run the following script:
196 | ```
197 | Plugins\UnrealMCP\MCP\run_unreal_mcp.bat
198 | ```
199 |
200 | This starts the MCP server using the configured Python interpreter, allowing it to listen for connections.
201 | ```
```
--------------------------------------------------------------------------------
/Source/UnrealMCP/Public/MCPTCPServer.h:
--------------------------------------------------------------------------------
```
1 | #pragma once
2 | #include "CoreMinimal.h"
3 | #include "Containers/Ticker.h"
4 | #include "Json.h"
5 | #include "Networking.h"
6 | #include "Common/TcpListener.h"
7 | #include "Sockets.h"
8 | #include "SocketSubsystem.h"
9 | #include "MCPConstants.h"
10 |
11 | /**
12 | * Configuration struct for the TCP server
13 | * Allows for easy customization of server parameters
14 | */
15 | struct FMCPTCPServerConfig
16 | {
17 | /** Port to listen on */
18 | int32 Port = MCPConstants::DEFAULT_PORT;
19 |
20 | /** Client timeout in seconds */
21 | float ClientTimeoutSeconds = MCPConstants::DEFAULT_CLIENT_TIMEOUT_SECONDS;
22 |
23 | /** Size of the receive buffer in bytes */
24 | int32 ReceiveBufferSize = MCPConstants::DEFAULT_RECEIVE_BUFFER_SIZE;
25 |
26 | /** Tick interval in seconds */
27 | float TickIntervalSeconds = MCPConstants::DEFAULT_TICK_INTERVAL_SECONDS;
28 |
29 | /** Whether to log verbose messages */
30 | bool bEnableVerboseLogging = MCPConstants::DEFAULT_VERBOSE_LOGGING;
31 | };
32 |
33 | /**
34 | * Structure to track client connection information
35 | */
36 | struct FMCPClientConnection
37 | {
38 | /** Socket for this client */
39 | FSocket* Socket;
40 |
41 | /** Endpoint information */
42 | FIPv4Endpoint Endpoint;
43 |
44 | /** Time since last activity for timeout tracking */
45 | float TimeSinceLastActivity;
46 |
47 | /** Buffer for receiving data */
48 | TArray<uint8> ReceiveBuffer;
49 |
50 | /**
51 | * Constructor
52 | * @param InSocket - The client socket
53 | * @param InEndpoint - The client endpoint
54 | * @param BufferSize - Size of the receive buffer
55 | */
56 | FMCPClientConnection(FSocket* InSocket, const FIPv4Endpoint& InEndpoint, int32 BufferSize = MCPConstants::DEFAULT_RECEIVE_BUFFER_SIZE)
57 | : Socket(InSocket)
58 | , Endpoint(InEndpoint)
59 | , TimeSinceLastActivity(0.0f)
60 | {
61 | ReceiveBuffer.SetNumUninitialized(BufferSize);
62 | }
63 | };
64 |
65 | /**
66 | * Interface for command handlers
67 | * Allows for easy addition of new commands without modifying the server
68 | */
69 | class IMCPCommandHandler
70 | {
71 | public:
72 | virtual ~IMCPCommandHandler() {}
73 |
74 | /**
75 | * Get the command name this handler responds to
76 | * @return The command name
77 | */
78 | virtual FString GetCommandName() const = 0;
79 |
80 | /**
81 | * Handle the command
82 | * @param Params - The command parameters
83 | * @param ClientSocket - The client socket
84 | * @return JSON response object
85 | */
86 | virtual TSharedPtr<FJsonObject> Execute(const TSharedPtr<FJsonObject>& Params, FSocket* ClientSocket) = 0;
87 | };
88 |
89 | /**
90 | * MCP TCP Server
91 | * Manages connections and command routing
92 | */
93 | class UNREALMCP_API FMCPTCPServer
94 | {
95 | public:
96 | /**
97 | * Constructor
98 | * @param InConfig - Configuration for the server
99 | */
100 | FMCPTCPServer(const FMCPTCPServerConfig& InConfig);
101 |
102 | /**
103 | * Destructor
104 | */
105 | virtual ~FMCPTCPServer();
106 |
107 | /**
108 | * Start the server
109 | * @return True if started successfully
110 | */
111 | bool Start();
112 |
113 | /**
114 | * Stop the server
115 | */
116 | void Stop();
117 |
118 | /**
119 | * Check if the server is running
120 | * @return True if running
121 | */
122 | bool IsRunning() const { return bRunning; }
123 |
124 | /**
125 | * Register a command handler
126 | * @param Handler - The handler to register
127 | */
128 | void RegisterCommandHandler(TSharedPtr<IMCPCommandHandler> Handler);
129 |
130 | /**
131 | * Unregister a command handler
132 | * @param CommandName - The command name to unregister
133 | */
134 | void UnregisterCommandHandler(const FString& CommandName);
135 |
136 | /**
137 | * Register an external command handler
138 | * This is a public API that allows external code to extend the MCP plugin with custom functionality
139 | * @param Handler - The handler to register
140 | * @return True if registration was successful
141 | */
142 | bool RegisterExternalCommandHandler(TSharedPtr<IMCPCommandHandler> Handler);
143 |
144 | /**
145 | * Unregister an external command handler
146 | * @param CommandName - The command name to unregister
147 | * @return True if unregistration was successful
148 | */
149 | bool UnregisterExternalCommandHandler(const FString& CommandName);
150 |
151 | /**
152 | * Send a response to a client
153 | * @param Client - The client socket
154 | * @param Response - The response to send
155 | */
156 | void SendResponse(FSocket* Client, const TSharedPtr<FJsonObject>& Response);
157 |
158 | /**
159 | * Get the command handlers map (for testing purposes)
160 | * @return The map of command handlers
161 | */
162 | const TMap<FString, TSharedPtr<IMCPCommandHandler>>& GetCommandHandlers() const { return CommandHandlers; }
163 |
164 | protected:
165 | /**
166 | * Tick function called by the ticker
167 | * @param DeltaTime - Time since last tick
168 | * @return True to continue ticking
169 | */
170 | bool Tick(float DeltaTime);
171 |
172 | /**
173 | * Process pending connections
174 | */
175 | virtual void ProcessPendingConnections();
176 |
177 | /**
178 | * Process client data
179 | */
180 | virtual void ProcessClientData();
181 |
182 | /**
183 | * Process a command
184 | * @param CommandJson - The command JSON
185 | * @param ClientSocket - The client socket
186 | */
187 | virtual void ProcessCommand(const FString& CommandJson, FSocket* ClientSocket);
188 |
189 | /**
190 | * Check for client timeouts
191 | * @param DeltaTime - Time since last tick
192 | */
193 | virtual void CheckClientTimeouts(float DeltaTime);
194 |
195 | /**
196 | * Clean up a client connection
197 | * @param ClientConnection - The client connection to clean up
198 | */
199 | virtual void CleanupClientConnection(FMCPClientConnection& ClientConnection);
200 |
201 | /**
202 | * Clean up a client connection by socket
203 | * @param ClientSocket - The client socket to clean up
204 | */
205 | virtual void CleanupClientConnection(FSocket* ClientSocket);
206 |
207 | /**
208 | * Clean up all client connections
209 | */
210 | virtual void CleanupAllClientConnections();
211 |
212 | /**
213 | * Get a safe description of a socket
214 | * @param Socket - The socket
215 | * @return A safe description string
216 | */
217 | FString GetSafeSocketDescription(FSocket* Socket);
218 |
219 | /**
220 | * Connection handler
221 | * @param InSocket - The new client socket
222 | * @param Endpoint - The client endpoint
223 | * @return True if connection accepted
224 | */
225 | virtual bool HandleConnectionAccepted(FSocket* InSocket, const FIPv4Endpoint& Endpoint);
226 |
227 | /** Server configuration */
228 | FMCPTCPServerConfig Config;
229 |
230 | /** TCP listener */
231 | FTcpListener* Listener;
232 |
233 | /** Client connections */
234 | TArray<FMCPClientConnection> ClientConnections;
235 |
236 | /** Running flag */
237 | bool bRunning;
238 |
239 | /** Ticker handle */
240 | FTSTicker::FDelegateHandle TickerHandle;
241 |
242 | /** Command handlers map */
243 | TMap<FString, TSharedPtr<IMCPCommandHandler>> CommandHandlers;
244 |
245 | private:
246 | // Disable copy and assignment
247 | FMCPTCPServer(const FMCPTCPServer&) = delete;
248 | FMCPTCPServer& operator=(const FMCPTCPServer&) = delete;
249 | };
```
--------------------------------------------------------------------------------
/MCP/setup_unreal_mcp.bat:
--------------------------------------------------------------------------------
```
1 | @echo off
2 | setlocal EnableDelayedExpansion
3 |
4 | echo ========================================================
5 | echo Unreal MCP - Python Environment Setup
6 | echo ========================================================
7 | echo.
8 |
9 | REM Get the directory where this script is located
10 | set "SCRIPT_DIR=%~dp0"
11 | set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
12 |
13 | REM Set paths for local environment
14 | set "ENV_DIR=%SCRIPT_DIR%\python_env"
15 | set "MODULES_DIR=%SCRIPT_DIR%\python_modules"
16 |
17 | REM Parse command line arguments
18 | set "CONFIGURE_CLAUDE=0"
19 | set "CONFIGURE_CURSOR=0"
20 |
21 | :parse_args
22 | if "%1"=="" goto :done_parsing
23 | if /i "%1"=="--help" (
24 | echo Usage: setup_unreal_mcp.bat [OPTIONS]
25 | echo.
26 | echo Options:
27 | echo --help Show this help message
28 | echo --configure-claude Configure Claude Desktop (default)
29 | echo --configure-cursor Configure Cursor
30 | echo --configure-both Configure both Claude and Cursor
31 | echo --skip-config Skip configuration
32 | echo.
33 | goto :end
34 | )
35 | if /i "%1"=="--configure-claude" set "CONFIGURE_CLAUDE=1" & set "CONFIGURE_CURSOR=0" & shift & goto :parse_args
36 | if /i "%1"=="--configure-cursor" set "CONFIGURE_CLAUDE=0" & set "CONFIGURE_CURSOR=1" & shift & goto :parse_args
37 | if /i "%1"=="--configure-both" set "CONFIGURE_CLAUDE=1" & set "CONFIGURE_CURSOR=1" & shift & goto :parse_args
38 | if /i "%1"=="--skip-config" set "CONFIGURE_CLAUDE=0" & set "CONFIGURE_CURSOR=0" & shift & goto :parse_args
39 | shift
40 | goto :parse_args
41 | :done_parsing
42 |
43 | REM If no config option was specified, show the assistant choice menu first
44 | if "%CONFIGURE_CLAUDE%"=="0" if "%CONFIGURE_CURSOR%"=="0" (
45 | echo Which AI assistant would you like to configure?
46 | echo.
47 | echo 1. Claude Desktop
48 | echo 2. Cursor
49 | echo 3. Both Claude Desktop and Cursor
50 | echo 4. Skip AI assistant configuration
51 | echo.
52 |
53 | set /p AI_CHOICE="Enter choice (1-4): "
54 | echo.
55 |
56 | if "!AI_CHOICE!"=="1" set "CONFIGURE_CLAUDE=1" & set "CONFIGURE_CURSOR=0"
57 | if "!AI_CHOICE!"=="2" set "CONFIGURE_CLAUDE=0" & set "CONFIGURE_CURSOR=1"
58 | if "!AI_CHOICE!"=="3" set "CONFIGURE_CLAUDE=1" & set "CONFIGURE_CURSOR=1"
59 | if "!AI_CHOICE!"=="4" set "CONFIGURE_CLAUDE=0" & set "CONFIGURE_CURSOR=0"
60 | )
61 |
62 | echo Setting up Python environment in: %ENV_DIR%
63 | echo.
64 |
65 | REM Check if Python is installed
66 | where python >nul 2>&1
67 | if %ERRORLEVEL% neq 0 (
68 | echo ERROR: Python is not installed or not in your PATH.
69 | echo Please install Python and try again.
70 | goto :end
71 | )
72 |
73 | REM Get Python version and path
74 | for /f "tokens=*" %%i in ('python --version 2^>^&1') do set PYTHON_VERSION=%%i
75 | for /f "tokens=*" %%i in ('where python') do set SYSTEM_PYTHON=%%i
76 | echo Detected %PYTHON_VERSION% at %SYSTEM_PYTHON%
77 | echo.
78 |
79 | REM Create directories if they don't exist
80 | if not exist "%ENV_DIR%" (
81 | echo Creating Python environment directory...
82 | mkdir "%ENV_DIR%"
83 | )
84 |
85 | if not exist "%MODULES_DIR%" (
86 | echo Creating Python modules directory...
87 | mkdir "%MODULES_DIR%"
88 | )
89 |
90 | REM Check if virtualenv is installed
91 | python -c "import virtualenv" >nul 2>&1
92 | if %ERRORLEVEL% neq 0 (
93 | echo Installing virtualenv...
94 | python -m pip install virtualenv
95 | )
96 |
97 | REM Create virtual environment if it doesn't exist
98 | if not exist "%ENV_DIR%\Scripts\python.exe" (
99 | echo Creating virtual environment...
100 | python -m virtualenv "%ENV_DIR%"
101 | ) else (
102 | echo Virtual environment already exists.
103 | )
104 |
105 | REM Activate the virtual environment and install packages
106 | echo.
107 | echo Activating virtual environment and installing packages...
108 | call "%ENV_DIR%\Scripts\activate.bat"
109 |
110 | REM Check if activation was successful
111 | if %ERRORLEVEL% neq 0 (
112 | echo ERROR: Failed to activate virtual environment.
113 | goto :end
114 | )
115 |
116 | REM Install MCP package in the virtual environment
117 | echo Installing MCP package...
118 | python -m pip install mcp>=0.1.0
119 |
120 | REM Also install to modules directory as a backup
121 | echo Installing MCP package to modules directory as backup...
122 | python -m pip install mcp>=0.1.0 -t "%MODULES_DIR%"
123 |
124 | REM Verify installation
125 | echo.
126 | echo Verifying MCP installation...
127 | python -c "import mcp; print(f'MCP package installed successfully. Version: {getattr(mcp, \"__version__\", \"unknown\")}')"
128 |
129 | REM Create the run script
130 | echo.
131 | echo Creating run script...
132 | (
133 | echo @echo off
134 | echo setlocal
135 | echo.
136 | echo REM Get the directory where this script is located
137 | echo set "SCRIPT_DIR=%%~dp0"
138 | echo set "SCRIPT_DIR=%%SCRIPT_DIR:~0,-1%%"
139 | echo.
140 | echo REM Set paths for local environment
141 | echo set "ENV_DIR=%%SCRIPT_DIR%%\python_env"
142 | echo set "PYTHON_PATH=%%ENV_DIR%%\Scripts\python.exe"
143 | echo.
144 | echo REM Check if Python environment exists
145 | echo if not exist "%%PYTHON_PATH%%" (
146 | echo echo ERROR: Python environment not found. Please run setup_unreal_mcp.bat first. ^>^&2
147 | echo goto :end
148 | echo )
149 | echo.
150 | echo REM Activate the virtual environment silently
151 | echo call "%%ENV_DIR%%\Scripts\activate.bat" ^>nul 2^>^&1
152 | echo.
153 | echo REM Log start message to stderr
154 | echo echo Starting Unreal MCP bridge... ^>^&2
155 | echo.
156 | echo REM Run the Python bridge script
157 | echo python "%%SCRIPT_DIR%%\unreal_mcp_bridge.py" %%*
158 | echo.
159 | echo :end
160 | ) > "%SCRIPT_DIR%\run_unreal_mcp.bat"
161 |
162 | REM Configure Claude Desktop if requested
163 | if "%CONFIGURE_CLAUDE%"=="1" (
164 | set "CLAUDE_CONFIG_DIR=%APPDATA%\Claude"
165 | set "CLAUDE_CONFIG_FILE=%CLAUDE_CONFIG_DIR%\claude_desktop_config.json"
166 |
167 | REM Check if Claude Desktop is installed
168 | if not exist "%CLAUDE_CONFIG_DIR%" (
169 | echo Creating Claude configuration directory...
170 | mkdir "%CLAUDE_CONFIG_DIR%"
171 | )
172 |
173 | REM Update Claude Desktop configuration using Python
174 | echo.
175 | echo Updating Claude Desktop configuration...
176 | python "%SCRIPT_DIR%\temp_update_config.py" "%CLAUDE_CONFIG_FILE%" "%SCRIPT_DIR%\run_unreal_mcp.bat"
177 | if %ERRORLEVEL% neq 0 (
178 | echo WARNING: Failed to update Claude Desktop configuration. Claude Desktop may not be installed.
179 | ) else (
180 | echo Claude Desktop configuration updated at: %CLAUDE_CONFIG_FILE%
181 | )
182 | )
183 |
184 | REM Configure Cursor if requested
185 | if "%CONFIGURE_CURSOR%"=="1" (
186 | echo.
187 | echo Updating Cursor configuration...
188 | python "%SCRIPT_DIR%\cursor_setup.py" --script "%SCRIPT_DIR%\run_unreal_mcp.bat"
189 | if %ERRORLEVEL% neq 0 (
190 | echo WARNING: Failed to update Cursor configuration. Cursor may not be installed.
191 | ) else (
192 | echo Cursor configuration updated successfully!
193 | )
194 | )
195 |
196 | echo.
197 | echo ========================================================
198 | echo Setup complete!
199 | echo.
200 |
201 | if "%CONFIGURE_CLAUDE%"=="1" if "%CONFIGURE_CURSOR%"=="0" (
202 | echo To use with Claude Desktop:
203 | echo 1. Run run_unreal_mcp.bat to start the MCP bridge
204 | echo 2. Open Claude Desktop and it should automatically use the correct configuration
205 | ) else if "%CONFIGURE_CLAUDE%"=="0" if "%CONFIGURE_CURSOR%"=="1" (
206 | echo To use with Cursor:
207 | echo 1. Run run_unreal_mcp.bat to start the MCP bridge
208 | echo 2. Open Cursor and it should automatically use the UnrealMCP tools
209 | ) else if "%CONFIGURE_CLAUDE%"=="1" if "%CONFIGURE_CURSOR%"=="1" (
210 | echo To use with Claude Desktop:
211 | echo 1. Run run_unreal_mcp.bat to start the MCP bridge
212 | echo 2. Open Claude Desktop and it should automatically use the correct configuration
213 | echo.
214 | echo To use with Cursor:
215 | echo 1. Run run_unreal_mcp.bat to start the MCP bridge
216 | echo 2. Open Cursor and it should automatically use the UnrealMCP tools
217 | ) else (
218 | echo No AI assistant configurations were applied.
219 | echo To configure an assistant, run this script again with one of these options:
220 | echo --configure-claude Configure Claude Desktop
221 | echo --configure-cursor Configure Cursor
222 | echo --configure-both Configure both Claude and Cursor
223 | )
224 |
225 | echo ========================================================
226 | echo.
227 | echo Press any key to exit...
228 | pause >nul
229 |
230 | :end
```
--------------------------------------------------------------------------------
/MCP/unreal_mcp_bridge.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Bridge module connecting Unreal Engine to MCP (Model Context Protocol).
3 |
4 | This module serves as a bridge between the Unreal Engine MCP plugin and
5 | the MCP server provided by the 'mcp' Python package. It handles the communication
6 | between Claude for Desktop and Unreal Engine through the MCP protocol.
7 |
8 | Requirements:
9 | - Python 3.7+
10 | - MCP package (pip install mcp>=0.1.0)
11 | - Running Unreal Engine with the UnrealMCP plugin enabled
12 |
13 | The bridge connects to the Unreal Engine plugin (which acts as the actual MCP server)
14 | and exposes MCP functionality to Claude for Desktop. This allows Claude to interact
15 | with Unreal Engine through natural language commands.
16 | """
17 |
18 | import json
19 | import socket
20 | import sys
21 | import os
22 | import importlib.util
23 | import importlib
24 |
25 | # Try to get the port from MCPConstants
26 | DEFAULT_PORT = 13377
27 | DEFAULT_BUFFER_SIZE = 65536
28 | DEFAULT_TIMEOUT = 10 # 10 second timeout
29 |
30 | try:
31 | # Try to read the port from the C++ constants
32 | plugin_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
33 | constants_path = os.path.join(plugin_dir, "Source", "UnrealMCP", "Public", "MCPConstants.h")
34 |
35 | if os.path.exists(constants_path):
36 | with open(constants_path, 'r') as f:
37 | constants_content = f.read()
38 |
39 | # Extract port from MCPConstants
40 | port_match = constants_content.find("DEFAULT_PORT = ")
41 | if port_match != -1:
42 | port_line = constants_content[port_match:].split(';')[0]
43 | DEFAULT_PORT = int(port_line.split('=')[1].strip())
44 |
45 | # Extract buffer size from MCPConstants
46 | buffer_match = constants_content.find("DEFAULT_RECEIVE_BUFFER_SIZE = ")
47 | if buffer_match != -1:
48 | buffer_line = constants_content[buffer_match:].split(';')[0]
49 | DEFAULT_BUFFER_SIZE = int(buffer_line.split('=')[1].strip())
50 | except Exception as e:
51 | # If anything goes wrong, use the defaults (which are already defined)
52 | print(f"Warning: Could not read constants from MCPConstants.h: {e}", file=sys.stderr)
53 | # No need to redefine DEFAULT_PORT and DEFAULT_BUFFER_SIZE here
54 |
55 |
56 | print(f"Using port: {DEFAULT_PORT}", file=sys.stderr)
57 | print(f"Using buffer size: {DEFAULT_BUFFER_SIZE}", file=sys.stderr)
58 |
59 | # Check for local python_modules directory first
60 | local_modules_path = os.path.join(os.path.dirname(__file__), "python_modules")
61 | if os.path.exists(local_modules_path):
62 | print(f"Found local python_modules directory: {local_modules_path}", file=sys.stderr)
63 | sys.path.insert(0, local_modules_path)
64 | print(f"Added local python_modules to sys.path", file=sys.stderr)
65 |
66 | # Try to import MCP
67 | mcp_spec = importlib.util.find_spec("mcp")
68 | if mcp_spec is None:
69 | print("Error: The 'mcp' package is not installed.", file=sys.stderr)
70 | print("Please install it using one of the following methods:", file=sys.stderr)
71 | print("1. Run setup_unreal_mcp.bat to install it globally", file=sys.stderr)
72 | print("2. Run: pip install mcp", file=sys.stderr)
73 | print("3. Run: pip install mcp -t ./python_modules", file=sys.stderr)
74 | sys.exit(1)
75 |
76 | try:
77 | from mcp.server.fastmcp import FastMCP, Context
78 | except ImportError as e:
79 | print(f"Error importing from mcp package: {e}", file=sys.stderr)
80 | print("The mcp package is installed but there was an error importing from it.", file=sys.stderr)
81 | print("This could be due to a version mismatch or incomplete installation.", file=sys.stderr)
82 | print("Please try reinstalling the package using: pip install --upgrade mcp", file=sys.stderr)
83 | sys.exit(1)
84 |
85 | # Initialize the MCP server
86 | mcp = FastMCP(
87 | "UnrealMCP",
88 | description="Unreal Engine integration through the Model Context Protocol"
89 | )
90 |
91 | def send_command(command_type, params=None, timeout=DEFAULT_TIMEOUT):
92 | """Send a command to the C++ MCP server and return the response.
93 |
94 | Args:
95 | command_type: The type of command to send
96 | params: Optional parameters for the command
97 | timeout: Timeout in seconds (default: DEFAULT_TIMEOUT)
98 |
99 | Returns:
100 | The JSON response from the server
101 | """
102 | try:
103 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
104 | s.settimeout(timeout) # Set a timeout
105 | s.connect(("localhost", DEFAULT_PORT)) # Connect to Unreal C++ server
106 | command = {
107 | "type": command_type,
108 | "params": params or {}
109 | }
110 | s.sendall(json.dumps(command).encode('utf-8'))
111 |
112 | # Read response with a buffer
113 | chunks = []
114 | response_data = b''
115 |
116 | # Wait for data with timeout
117 | while True:
118 | try:
119 | chunk = s.recv(DEFAULT_BUFFER_SIZE)
120 | if not chunk: # Connection closed
121 | break
122 | chunks.append(chunk)
123 |
124 | # Try to parse what we have so far
125 | response_data = b''.join(chunks)
126 | try:
127 | # If we can parse it as JSON, we have a complete response
128 | json.loads(response_data.decode('utf-8'))
129 | break
130 | except json.JSONDecodeError:
131 | # Incomplete JSON, continue receiving
132 | continue
133 | except socket.timeout:
134 | # If we have some data but timed out, try to use what we have
135 | if response_data:
136 | break
137 | raise
138 |
139 | if not response_data:
140 | raise Exception("No data received from server")
141 |
142 | return json.loads(response_data.decode('utf-8'))
143 | except ConnectionRefusedError:
144 | print(f"Error: Could not connect to Unreal MCP server on localhost:{DEFAULT_PORT}.", file=sys.stderr)
145 | print("Make sure your Unreal Engine with MCP plugin is running.", file=sys.stderr)
146 | raise Exception("Failed to connect to Unreal MCP server: Connection refused")
147 | except socket.timeout:
148 | print("Error: Connection timed out while communicating with Unreal MCP server.", file=sys.stderr)
149 | raise Exception("Failed to communicate with Unreal MCP server: Connection timed out")
150 | except Exception as e:
151 | print(f"Error communicating with Unreal MCP server: {str(e)}", file=sys.stderr)
152 | raise Exception(f"Failed to communicate with Unreal MCP server: {str(e)}")
153 |
154 | # All commands have been moved to separate modules in the Commands directory
155 |
156 | def load_commands():
157 | """Load all commands from the Commands directory structure."""
158 | commands_dir = os.path.join(os.path.dirname(__file__), 'Commands')
159 | if not os.path.exists(commands_dir):
160 | print(f"Commands directory not found at: {commands_dir}", file=sys.stderr)
161 | return
162 |
163 | # First, load Python files directly in the Commands directory
164 | for filename in os.listdir(commands_dir):
165 | if filename.endswith('.py') and not filename.startswith('__'):
166 | try:
167 | module_name = f"Commands.{filename[:-3]}" # Remove .py extension
168 | module = importlib.import_module(module_name)
169 | if hasattr(module, 'register_all'):
170 | module.register_all(mcp)
171 | print(f"Registered commands from module: {filename}", file=sys.stderr)
172 | else:
173 | print(f"Warning: {filename} has no register_all function", file=sys.stderr)
174 | except Exception as e:
175 | print(f"Error loading module {filename}: {e}", file=sys.stderr)
176 |
177 | # Then, load command categories from subdirectories
178 | for category in os.listdir(commands_dir):
179 | category_path = os.path.join(commands_dir, category)
180 | if os.path.isdir(category_path) and not category.startswith('__'):
181 | try:
182 | # Try to load the category's __init__.py which should have register_all
183 | module_name = f"Commands.{category}"
184 | module = importlib.import_module(module_name)
185 | if hasattr(module, 'register_all'):
186 | module.register_all(mcp)
187 | print(f"Registered commands from category: {category}", file=sys.stderr)
188 | else:
189 | print(f"Warning: {category} has no register_all function", file=sys.stderr)
190 | except Exception as e:
191 | print(f"Error loading category {category}: {e}", file=sys.stderr)
192 |
193 | def load_user_tools():
194 | """Load user-defined tools from the UserTools directory."""
195 | user_tools_dir = os.path.join(os.path.dirname(__file__), 'UserTools')
196 | if not os.path.exists(user_tools_dir):
197 | print(f"User tools directory not found at: {user_tools_dir}", file=sys.stderr)
198 | return
199 |
200 | for filename in os.listdir(user_tools_dir):
201 | if filename.endswith('.py') and filename != '__init__.py':
202 | module_name = filename[:-3]
203 | try:
204 | spec = importlib.util.spec_from_file_location(module_name, os.path.join(user_tools_dir, filename))
205 | module = importlib.util.module_from_spec(spec)
206 | spec.loader.exec_module(module)
207 | if hasattr(module, 'register_tools'):
208 | from utils import send_command
209 | module.register_tools(mcp, {'send_command': send_command})
210 | print(f"Loaded user tool: {module_name}", file=sys.stderr)
211 | else:
212 | print(f"Warning: {filename} has no register_tools function", file=sys.stderr)
213 | except Exception as e:
214 | print(f"Error loading user tool {filename}: {str(e)}", file=sys.stderr)
215 |
216 | def main():
217 | """Main entry point for the Unreal MCP bridge."""
218 | print("Starting Unreal MCP bridge...", file=sys.stderr)
219 | try:
220 | load_commands() # Load built-in commands
221 | load_user_tools() # Load user-defined tools
222 | mcp.run() # Start the MCP bridge
223 | except Exception as e:
224 | print(f"Error starting MCP bridge: {str(e)}", file=sys.stderr)
225 | sys.exit(1)
226 |
227 | if __name__ == "__main__":
228 | main()
```
--------------------------------------------------------------------------------
/MCP/TestScripts/1_basic_connection.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Basic Connection Test for MCP Server
4 |
5 | This script tests the basic connection to the MCP Server.
6 | It connects to the server, sends a simple ping command, and verifies the response.
7 | """
8 |
9 | import socket
10 | import json
11 | import sys
12 | import os
13 | import platform
14 | import subprocess
15 | import time
16 | import traceback
17 | from datetime import datetime
18 |
19 | def check_port_in_use(host, port):
20 | """Check if the specified port is already in use."""
21 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
22 | try:
23 | s.bind((host, port))
24 | return False # Port is available
25 | except socket.error:
26 | return True # Port is in use
27 |
28 | def check_service_running(port):
29 | """Check if any service is running on the specified port using system commands."""
30 | try:
31 | if platform.system() == "Windows":
32 | output = subprocess.check_output(f"netstat -ano | findstr :{port}", shell=True).decode()
33 | if output:
34 | return True, output.strip()
35 | else:
36 | output = subprocess.check_output(f"lsof -i :{port}", shell=True).decode()
37 | if output:
38 | return True, output.strip()
39 | except subprocess.CalledProcessError:
40 | pass # Command returned error or no output
41 | except Exception as e:
42 | print(f"Error checking service: {e}")
43 |
44 | return False, "No service detected"
45 |
46 | def log_system_info():
47 | """Log system information to help with debugging."""
48 | print("\n=== SYSTEM INFORMATION ===")
49 | print(f"Operating System: {platform.system()} {platform.version()}")
50 | print(f"Python Version: {platform.python_version()}")
51 | print(f"Machine: {platform.machine()}")
52 | print(f"Node: {platform.node()}")
53 | print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
54 |
55 | try:
56 | # Check firewall status on Windows
57 | if platform.system() == "Windows":
58 | firewall_output = subprocess.check_output("netsh advfirewall show currentprofile", shell=True).decode()
59 | if "State ON" in firewall_output:
60 | print("Windows Firewall: ENABLED")
61 | else:
62 | print("Windows Firewall: DISABLED")
63 | except Exception as e:
64 | print(f"Error checking firewall: {e}")
65 |
66 | def ping_host(host, timeout=2):
67 | """Ping the host to check basic connectivity."""
68 | try:
69 | if platform.system() == "Windows":
70 | ping_cmd = f"ping -n 1 -w {int(timeout*1000)} {host}"
71 | else:
72 | ping_cmd = f"ping -c 1 -W {int(timeout)} {host}"
73 |
74 | result = subprocess.run(ping_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
75 | if result.returncode == 0:
76 | return True, "Host is reachable"
77 | else:
78 | return False, f"Host unreachable: {result.stdout.decode().strip()}"
79 | except Exception as e:
80 | return False, f"Error pinging host: {str(e)}"
81 |
82 | def trace_socket_errors(error):
83 | """Get detailed information about a socket error."""
84 | error_info = {
85 | 10035: "WSAEWOULDBLOCK: Resource temporarily unavailable, operation would block",
86 | 10036: "WSAEINPROGRESS: Operation now in progress",
87 | 10037: "WSAEALREADY: Operation already in progress",
88 | 10038: "WSAENOTSOCK: Socket operation on non-socket",
89 | 10039: "WSAEDESTADDRREQ: Destination address required",
90 | 10040: "WSAEMSGSIZE: Message too long",
91 | 10041: "WSAEPROTOTYPE: Protocol wrong type for socket",
92 | 10042: "WSAENOPROTOOPT: Bad protocol option",
93 | 10043: "WSAEPROTONOSUPPORT: Protocol not supported",
94 | 10044: "WSAESOCKTNOSUPPORT: Socket type not supported",
95 | 10045: "WSAEOPNOTSUPP: Operation not supported",
96 | 10046: "WSAEPFNOSUPPORT: Protocol family not supported",
97 | 10047: "WSAEAFNOSUPPORT: Address family not supported by protocol",
98 | 10048: "WSAEADDRINUSE: Address already in use",
99 | 10049: "WSAEADDRNOTAVAIL: Cannot assign requested address",
100 | 10050: "WSAENETDOWN: Network is down",
101 | 10051: "WSAENETUNREACH: Network is unreachable",
102 | 10052: "WSAENETRESET: Network dropped connection on reset",
103 | 10053: "WSAECONNABORTED: Software caused connection abort",
104 | 10054: "WSAECONNRESET: Connection reset by peer",
105 | 10055: "WSAENOBUFS: No buffer space available",
106 | 10056: "WSAEISCONN: Socket is already connected",
107 | 10057: "WSAENOTCONN: Socket is not connected",
108 | 10058: "WSAESHUTDOWN: Cannot send after socket shutdown",
109 | 10059: "WSAETOOMANYREFS: Too many references",
110 | 10060: "WSAETIMEDOUT: Connection timed out",
111 | 10061: "WSAECONNREFUSED: Connection refused",
112 | 10062: "WSAELOOP: Cannot translate name",
113 | 10063: "WSAENAMETOOLONG: Name too long",
114 | 10064: "WSAEHOSTDOWN: Host is down",
115 | 10065: "WSAEHOSTUNREACH: No route to host",
116 | }
117 |
118 | if hasattr(error, 'errno'):
119 | errno = error.errno
120 | description = error_info.get(errno, f"Unknown error code: {errno}")
121 | return f"Socket error {errno}: {description}"
122 | return f"Unknown socket error: {str(error)}"
123 |
124 | def main():
125 | """Connect to the MCP Server and verify the connection works."""
126 | host = "localhost"
127 | port = 13377
128 | timeout = 5 # 5 second timeout
129 |
130 | print("\n=== NETWORK DIAGNOSTICS ===")
131 | # Check if we can ping the host
132 | ping_success, ping_msg = ping_host(host)
133 | print(f"Ping test: {ping_msg}")
134 |
135 | # Check if port is already in use locally
136 | port_in_use = check_port_in_use("127.0.0.1", port)
137 | if port_in_use:
138 | print(f"Warning: Port {port} is already in use on this machine")
139 | else:
140 | print(f"Port {port} is not in use on this machine")
141 |
142 | # Check if any service is running on the target port
143 | service_running, service_details = check_service_running(port)
144 | if service_running:
145 | print(f"A service is running on port {port}:")
146 | print(service_details)
147 | else:
148 | print(f"No service detected on port {port}")
149 |
150 | try:
151 | print("\n=== CONNECTION TEST ===")
152 | print(f"Creating socket to connect to {host}:{port}...")
153 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
154 | s.settimeout(timeout)
155 |
156 | # Try to connect
157 | print(f"Attempting connection to {host}:{port}...")
158 | connect_start = time.time()
159 | s.connect((host, port))
160 | connect_time = time.time() - connect_start
161 | print(f"✓ Connected successfully in {connect_time:.2f} seconds")
162 |
163 | # Create a simple get_scene_info command
164 | command = {
165 | "type": "get_scene_info"
166 | }
167 |
168 | # Send command
169 | print("Sending get_scene_info command...")
170 | command_str = json.dumps(command) + "\n" # Add newline
171 | s.sendall(command_str.encode('utf-8'))
172 |
173 | # Receive response with timeout tracking
174 | print("Waiting for response...")
175 | response_start = time.time()
176 | response = b""
177 | while True:
178 | try:
179 | data = s.recv(4096)
180 | if not data:
181 | print("Connection closed by server")
182 | break
183 |
184 | response += data
185 | print(f"Received {len(data)} bytes")
186 |
187 | if b"\n" in data: # Check for newline which indicates end of response
188 | print("Received newline character, end of response")
189 | break
190 |
191 | # Check if we've been waiting too long
192 | if time.time() - response_start > timeout:
193 | print(f"Response timeout after {timeout} seconds")
194 | break
195 | except socket.timeout:
196 | print(f"Socket timeout after {timeout} seconds")
197 | break
198 |
199 | response_time = time.time() - response_start
200 | print(f"Response received in {response_time:.2f} seconds")
201 |
202 | # Close connection
203 | try:
204 | s.shutdown(socket.SHUT_RDWR)
205 | except Exception as e:
206 | print(f"Warning during socket shutdown: {str(e)}")
207 |
208 | s.close()
209 | print("✓ Connection closed properly")
210 |
211 | # Process response
212 | if response:
213 | response_str = response.decode('utf-8').strip()
214 | print(f"Raw response ({len(response_str)} bytes): {response_str[:100]}...")
215 |
216 | try:
217 | response_json = json.loads(response_str)
218 | print("\n=== RESPONSE ===")
219 | print(f"Status: {response_json.get('status', 'unknown')}")
220 |
221 | if response_json.get('status') == 'success':
222 | print("✓ Server responded successfully")
223 | print(f"Level: {response_json.get('result', {}).get('level', 'unknown')}")
224 | print(f"Actor count: {response_json.get('result', {}).get('actor_count', 0)}")
225 | return True
226 | else:
227 | print("✗ Server responded with an error")
228 | print(f"Error: {response_json.get('message', 'Unknown error')}")
229 | return False
230 | except json.JSONDecodeError as e:
231 | print(f"✗ Error parsing JSON response: {e}")
232 | print(f"Raw response: {response_str}")
233 | return False
234 | else:
235 | print("✗ No response received from server")
236 | return False
237 |
238 | except ConnectionRefusedError as e:
239 | print(f"✗ Connection refused: {trace_socket_errors(e)}")
240 | print("This typically means:")
241 | print(" 1. The MCP Server is not running")
242 | print(" 2. The server is running on a different port")
243 | print(" 3. A firewall is blocking the connection")
244 | return False
245 | except socket.timeout as e:
246 | print(f"✗ Connection timed out: {trace_socket_errors(e)}")
247 | print("This typically means:")
248 | print(" 1. The MCP Server is running but not responding")
249 | print(" 2. A firewall or security software is intercepting but not blocking the connection")
250 | print(" 3. The network configuration is preventing the connection")
251 | return False
252 | except Exception as e:
253 | print(f"✗ Error: {trace_socket_errors(e)}")
254 | print("Detailed error information:")
255 | traceback.print_exc()
256 | return False
257 |
258 | if __name__ == "__main__":
259 | print("=== MCP Server Basic Connection Test ===")
260 | print(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
261 |
262 | # Log system information
263 | log_system_info()
264 |
265 | # Run the main test
266 | success = main()
267 |
268 | print("\n=== TEST RESULT ===")
269 | if success:
270 | print("✓ Connection test PASSED")
271 | sys.exit(0)
272 | else:
273 | print("✗ Connection test FAILED")
274 | sys.exit(1)
```
--------------------------------------------------------------------------------
/MCP/TestScripts/test_commands_blueprint.py:
--------------------------------------------------------------------------------
```python
1 | """Test script for UnrealMCP blueprint commands.
2 |
3 | This script tests the blueprint-related commands available in the UnrealMCP bridge.
4 | Make sure Unreal Engine is running with the UnrealMCP plugin enabled before running this script.
5 | """
6 |
7 | import sys
8 | import os
9 | import json
10 | import time
11 | from mcp.server.fastmcp import FastMCP, Context
12 |
13 | # Add the MCP directory to sys.path so we can import unreal_mcp_bridge
14 | mcp_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15 | if mcp_dir not in sys.path:
16 | sys.path.insert(0, mcp_dir)
17 |
18 | from unreal_mcp_bridge import send_command
19 |
20 | # Global variables to store paths
21 | blueprint_path = ""
22 | # Longer timeout for Unreal Engine operations
23 | TIMEOUT = 30
24 |
25 | def test_create_blueprint():
26 | """Test blueprint creation command."""
27 | global blueprint_path
28 | print("\n1. Testing create_blueprint...")
29 | try:
30 | # Define the package path and blueprint name
31 | # Use a subdirectory to ensure proper directory structure
32 | package_path = "/Game/Blueprints/TestDir"
33 | blueprint_name = "TestBlueprint"
34 |
35 | # Print the expected file path (this is just an approximation)
36 | project_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
37 | # Go up one more level to get to the actual project directory
38 | project_dir = os.path.dirname(project_dir)
39 | project_content_dir = os.path.join(project_dir, "Content")
40 |
41 | # Check both possible locations based on how the path is interpreted
42 | expected_path_in_dir = os.path.join(project_content_dir, "Blueprints", "TestDir", f"{blueprint_name}.uasset")
43 | expected_path_as_asset = os.path.join(project_content_dir, "Blueprints", "TestDir.uasset")
44 |
45 | print(f"Project directory: {project_dir}")
46 | print(f"Project Content directory: {project_content_dir}")
47 | print(f"Expected file path (in directory): {expected_path_in_dir}")
48 | print(f"Expected file path (as asset): {expected_path_as_asset}")
49 |
50 | # Print debug info about the current working directory
51 | print(f"Current working directory: {os.getcwd()}")
52 |
53 | # The package_path should be the directory, and name should be the asset name
54 | params = {
55 | "package_path": package_path, # Directory path
56 | "name": blueprint_name, # Asset name
57 | "properties": {
58 | "parent_class": "Actor" # Default parent class
59 | }
60 | }
61 |
62 | print(f"Sending create_blueprint command with package_path={params['package_path']} and name={params['name']}")
63 | response = send_command("create_blueprint", params, timeout=TIMEOUT)
64 | print(f"Create Blueprint Response: {json.dumps(response, indent=2)}")
65 |
66 | # Store the actual path from the response for later tests
67 | if response["status"] == "success":
68 | blueprint_path = response["result"]["path"]
69 | print(f"Blueprint created at: {blueprint_path}")
70 |
71 | # Check if the blueprint file exists in either expected location
72 | if os.path.exists(expected_path_in_dir):
73 | print(f"✓ Blueprint file found at: {expected_path_in_dir}")
74 | elif os.path.exists(expected_path_as_asset):
75 | print(f"✓ Blueprint file found at: {expected_path_as_asset}")
76 | else:
77 | print(f"✗ Blueprint file NOT found at expected locations")
78 |
79 | # Try to find the file in other possible locations
80 | possible_locations = [
81 | os.path.join(project_dir, "Saved", "Blueprints"),
82 | os.path.join(project_dir, "Saved", "Autosaves", "Game", "Blueprints"),
83 | os.path.join(project_dir, "Plugins", "UnrealMCP", "Content", "Blueprints")
84 | ]
85 |
86 | for location in possible_locations:
87 | potential_path = os.path.join(location, f"{blueprint_name}.uasset")
88 | if os.path.exists(potential_path):
89 | print(f"✓ Blueprint file found at alternative location: {potential_path}")
90 | break
91 | else:
92 | print("✗ Blueprint file not found in any expected location")
93 |
94 | # Try to find the file using a more extensive search
95 | print("Searching for the blueprint file in the project directory...")
96 | for root, dirs, files in os.walk(project_dir):
97 | for file in files:
98 | if "Blueprint" in file and file.endswith(".uasset"):
99 | found_path = os.path.join(root, file)
100 | print(f"✓ Blueprint file found at: {found_path}")
101 | break
102 | else:
103 | continue
104 | break
105 |
106 | return response["status"] == "success"
107 | except Exception as e:
108 | print(f"Error testing create_blueprint: {e}")
109 | return False
110 |
111 | def test_get_blueprint_info():
112 | """Test getting blueprint information."""
113 | global blueprint_path
114 | print("\n2. Testing get_blueprint_info...")
115 | try:
116 | # Use the path from the create_blueprint response
117 | params = {
118 | "blueprint_path": blueprint_path
119 | }
120 | response = send_command("get_blueprint_info", params, timeout=TIMEOUT)
121 | print(f"Get Blueprint Info Response: {json.dumps(response, indent=2)}")
122 | return response["status"] == "success"
123 | except Exception as e:
124 | print(f"Error testing get_blueprint_info: {e}")
125 | return False
126 |
127 | def test_create_blueprint_event():
128 | """Test creating a blueprint event."""
129 | global blueprint_path
130 | print("\n3. Testing create_blueprint_event...")
131 | try:
132 | # Use the path from the create_blueprint response
133 | params = {
134 | "event_name": "TestEvent",
135 | "blueprint_path": blueprint_path
136 | }
137 |
138 | # Set a longer timeout for this operation
139 | print("This operation may take some time...")
140 | response = send_command("create_blueprint_event", params, timeout=TIMEOUT)
141 | print(f"Create Blueprint Event Response: {json.dumps(response, indent=2)}")
142 | return response["status"] == "success"
143 | except Exception as e:
144 | print(f"Error testing create_blueprint_event: {e}")
145 | return False
146 |
147 | def test_modify_blueprint():
148 | """Test modifying a blueprint."""
149 | global blueprint_path
150 | print("\n4. Testing modify_blueprint...")
151 | try:
152 | # Use the path from the create_blueprint response
153 | params = {
154 | "blueprint_path": blueprint_path,
155 | "properties": {
156 | "description": "A test blueprint created by MCP",
157 | "category": "Tests",
158 | "options": {
159 | "hide_categories": ["Variables", "Transformation"],
160 | "namespace": "MCP",
161 | "display_name": "MCP Test Blueprint",
162 | "compile_mode": "Development",
163 | "abstract_class": False,
164 | "const_class": False,
165 | "deprecate": False
166 | }
167 | }
168 | }
169 | response = send_command("modify_blueprint", params, timeout=TIMEOUT)
170 | print(f"Modify Blueprint Response: {json.dumps(response, indent=2)}")
171 |
172 | # Verify the changes by getting the blueprint info again
173 | if response["status"] == "success":
174 | print("\nVerifying blueprint modifications...")
175 | verify_params = {
176 | "blueprint_path": blueprint_path
177 | }
178 | verify_response = send_command("get_blueprint_info", verify_params, timeout=TIMEOUT)
179 | print(f"Updated Blueprint Info: {json.dumps(verify_response, indent=2)}")
180 |
181 | # Check if the events were updated
182 | if verify_response["status"] == "success":
183 | result = verify_response["result"]
184 |
185 | # Check for events
186 | if "events" in result and len(result["events"]) > 0:
187 | print(f"✓ Blueprint has {len(result['events'])} events")
188 |
189 | # Look for our TestEvent
190 | test_event_found = False
191 | for event in result["events"]:
192 | if "name" in event and "TestEvent" in event["name"]:
193 | test_event_found = True
194 | print(f"✓ Found TestEvent: {event['name']}")
195 | break
196 |
197 | if not test_event_found:
198 | print("✗ TestEvent not found in events")
199 | else:
200 | print("✗ No events found in blueprint")
201 |
202 | return response["status"] == "success"
203 | except Exception as e:
204 | print(f"Error testing modify_blueprint: {e}")
205 | return False
206 |
207 | def main():
208 | """Run all blueprint-related tests."""
209 | print("Starting UnrealMCP blueprint command tests...")
210 | print("Make sure Unreal Engine is running with the UnrealMCP plugin enabled!")
211 |
212 | try:
213 | # Run tests in sequence, with each test depending on the previous one
214 | create_result = test_create_blueprint()
215 |
216 | # Only run subsequent tests if the blueprint was created successfully
217 | if create_result:
218 | # Wait a moment for the blueprint to be fully created
219 | time.sleep(1)
220 | get_info_result = test_get_blueprint_info()
221 |
222 | # Only run event creation if get_info succeeded
223 | if get_info_result:
224 | create_event_result = test_create_blueprint_event()
225 | else:
226 | create_event_result = False
227 |
228 | # Only run modify if previous tests succeeded
229 | if create_event_result:
230 | modify_result = test_modify_blueprint()
231 | else:
232 | modify_result = False
233 | else:
234 | get_info_result = False
235 | create_event_result = False
236 | modify_result = False
237 |
238 | results = {
239 | "create_blueprint": create_result,
240 | "get_blueprint_info": get_info_result,
241 | "create_blueprint_event": create_event_result,
242 | "modify_blueprint": modify_result
243 | }
244 |
245 | print("\nTest Results:")
246 | print("-" * 40)
247 | for test_name, success in results.items():
248 | status = "✓ PASS" if success else "✗ FAIL"
249 | print(f"{status} - {test_name}")
250 | print("-" * 40)
251 |
252 | if all(results.values()):
253 | print("\nAll blueprint tests passed successfully!")
254 | else:
255 | print("\nSome tests failed. Check the output above for details.")
256 | sys.exit(1)
257 |
258 | except Exception as e:
259 | print(f"\nError during testing: {e}")
260 | sys.exit(1)
261 |
262 | if __name__ == "__main__":
263 | main()
```