├── addons ├── godot_mcp │ ├── plugin.gd.uid │ ├── command_handler.gd.uid │ ├── ui │ │ ├── mcp_panel.gd.uid │ │ ├── mcp_panel.tscn │ │ └── mcp_panel.gd │ ├── .DS_Store │ ├── plugin.cfg │ └── plugin.gd └── .DS_Store ├── .DS_Store ├── python ├── .DS_Store ├── tools │ ├── .DS_Store │ ├── __pycache__ │ │ ├── __init__.cpython-313.pyc │ │ ├── asset_tools.cpython-313.pyc │ │ ├── scene_tools.cpython-313.pyc │ │ ├── editor_tools.cpython-313.pyc │ │ ├── material_tools.cpython-313.pyc │ │ ├── object_tools.cpython-313.pyc │ │ └── script_tools.cpython-313.pyc │ ├── __init__.py │ ├── material_tools.py │ ├── editor_tools.py │ ├── script_tools.py │ ├── scene_tools.py │ ├── asset_tools.py │ ├── object_tools.py │ └── meshy_tools.py ├── __pycache__ │ ├── config.cpython-313.pyc │ └── godot_connection.cpython-313.pyc ├── .gitignore ├── config.py ├── README.md ├── godot_connection.py └── server.py ├── requirements.txt ├── LISCENCE └── README.md /addons/godot_mcp/plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ddfhkiyge45tn 2 | -------------------------------------------------------------------------------- /addons/godot_mcp/command_handler.gd.uid: -------------------------------------------------------------------------------- 1 | uid://mcipi7y664aq 2 | -------------------------------------------------------------------------------- /addons/godot_mcp/ui/mcp_panel.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bqabyd7ce60u0 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/.DS_Store -------------------------------------------------------------------------------- /addons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/addons/.DS_Store -------------------------------------------------------------------------------- /python/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/.DS_Store -------------------------------------------------------------------------------- /python/tools/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/tools/.DS_Store -------------------------------------------------------------------------------- /addons/godot_mcp/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/addons/godot_mcp/.DS_Store -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp>=0.1.0 2 | typing-extensions>=4.0.0 3 | dataclasses>=0.6 4 | requests>=2.31.0 5 | python-dotenv>=1.0.0 -------------------------------------------------------------------------------- /python/__pycache__/config.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/__pycache__/config.cpython-313.pyc -------------------------------------------------------------------------------- /python/tools/__pycache__/__init__.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/tools/__pycache__/__init__.cpython-313.pyc -------------------------------------------------------------------------------- /python/__pycache__/godot_connection.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/__pycache__/godot_connection.cpython-313.pyc -------------------------------------------------------------------------------- /python/tools/__pycache__/asset_tools.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/tools/__pycache__/asset_tools.cpython-313.pyc -------------------------------------------------------------------------------- /python/tools/__pycache__/scene_tools.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/tools/__pycache__/scene_tools.cpython-313.pyc -------------------------------------------------------------------------------- /python/tools/__pycache__/editor_tools.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/tools/__pycache__/editor_tools.cpython-313.pyc -------------------------------------------------------------------------------- /python/tools/__pycache__/material_tools.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/tools/__pycache__/material_tools.cpython-313.pyc -------------------------------------------------------------------------------- /python/tools/__pycache__/object_tools.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/tools/__pycache__/object_tools.cpython-313.pyc -------------------------------------------------------------------------------- /python/tools/__pycache__/script_tools.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dokujaa/Godot-MCP/HEAD/python/tools/__pycache__/script_tools.cpython-313.pyc -------------------------------------------------------------------------------- /addons/godot_mcp/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Godot MCP" 4 | description="A plugin to enable communication between Godot Editor and Model Context Protocol (MCP) clients." 5 | author="Your Name" 6 | version="0.1.0" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /python/.gitignore: -------------------------------------------------------------------------------- 1 | # Environment variables 2 | .env 3 | 4 | # Python 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | *.so 9 | .Python 10 | build/ 11 | develop-eggs/ 12 | dist/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | pip-wheel-metadata/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # Virtual environments 30 | venv/ 31 | ENV/ 32 | env/ 33 | .venv/ 34 | 35 | # IDE 36 | .vscode/ 37 | .idea/ 38 | *.swp 39 | *.swo 40 | 41 | # OS 42 | .DS_Store 43 | Thumbs.db -------------------------------------------------------------------------------- /python/tools/__init__.py: -------------------------------------------------------------------------------- 1 | # tools/__init__.py 2 | from .scene_tools import register_scene_tools 3 | from .script_tools import register_script_tools 4 | from .object_tools import register_object_tools 5 | from .asset_tools import register_asset_tools 6 | from .material_tools import register_material_tools 7 | from .editor_tools import register_editor_tools 8 | from .meshy_tools import register_meshy_tools 9 | 10 | def register_all_tools(mcp): 11 | """Register all tools with the MCP server.""" 12 | register_scene_tools(mcp) 13 | register_script_tools(mcp) 14 | register_object_tools(mcp) 15 | register_asset_tools(mcp) 16 | register_material_tools(mcp) 17 | register_editor_tools(mcp) 18 | register_meshy_tools(mcp) -------------------------------------------------------------------------------- /LISCENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Srikar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /python/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | """ 3 | Configuration settings for the Godot MCP Server. 4 | This file contains all configurable parameters for the server. 5 | """ 6 | 7 | from dataclasses import dataclass 8 | import os 9 | from pathlib import Path 10 | 11 | # Load .env file if it exists 12 | try: 13 | from dotenv import load_dotenv 14 | # Look for .env file in the python directory 15 | env_path = Path(__file__).parent / '.env' 16 | load_dotenv(env_path) 17 | except ImportError: 18 | # python-dotenv not installed, just use system env vars 19 | pass 20 | 21 | @dataclass 22 | class ServerConfig: 23 | """Main configuration class for the MCP server.""" 24 | 25 | # Network settings 26 | godot_host: str = "localhost" 27 | godot_port: int = 6400 28 | mcp_port: int = 6500 29 | 30 | # Connection settings 31 | connection_timeout: float = 300.0 # 5 minutes timeout 32 | buffer_size: int = 1024 * 1024 # 1MB buffer for localhost 33 | 34 | # Logging settings 35 | log_level: str = "INFO" 36 | log_format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 37 | 38 | # Server settings 39 | max_retries: int = 3 40 | retry_delay: float = 1.0 41 | 42 | # Meshy API settings 43 | # API key loaded from environment variable 44 | meshy_api_key: str = os.getenv("MESHY_API_KEY") 45 | meshy_base_url: str = "https://api.meshy.ai/openapi" # Official API base URL 46 | meshy_timeout: int = 300 # 5 minutes for mesh generation 47 | meshy_download_timeout: int = 60 # 1 minute for downloading 48 | 49 | # Asset import settings 50 | asset_import_path: str = "res://assets/generated_meshes/" 51 | 52 | # Create a global config instance 53 | config = ServerConfig() -------------------------------------------------------------------------------- /addons/godot_mcp/ui/mcp_panel.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://dxvt86ck6b2a4"] 2 | 3 | [ext_resource type="Script" path="res://addons/godot_mcp/ui/mcp_panel.gd" id="1_4g23r"] 4 | 5 | [node name="MCPPanel" type="Control"] 6 | layout_mode = 3 7 | anchors_preset = 15 8 | anchor_right = 1.0 9 | anchor_bottom = 1.0 10 | grow_horizontal = 2 11 | grow_vertical = 2 12 | script = ExtResource("1_4g23r") 13 | 14 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 15 | layout_mode = 1 16 | anchors_preset = 15 17 | anchor_right = 1.0 18 | anchor_bottom = 1.0 19 | grow_horizontal = 2 20 | grow_vertical = 2 21 | 22 | [node name="StatusPanel" type="PanelContainer" parent="VBoxContainer"] 23 | layout_mode = 2 24 | 25 | [node name="StatusLabel" type="Label" parent="VBoxContainer/StatusPanel"] 26 | layout_mode = 2 27 | text = "Status: Not running" 28 | 29 | [node name="ConfigPanel" type="PanelContainer" parent="VBoxContainer"] 30 | layout_mode = 2 31 | 32 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ConfigPanel"] 33 | layout_mode = 2 34 | 35 | [node name="PortLabel" type="Label" parent="VBoxContainer/ConfigPanel/HBoxContainer"] 36 | layout_mode = 2 37 | text = "Port:" 38 | 39 | [node name="PortField" type="SpinBox" parent="VBoxContainer/ConfigPanel"] 40 | layout_mode = 2 41 | min_value = 1024.0 42 | max_value = 65535.0 43 | value = 6400.0 44 | alignment = 1 45 | 46 | [node name="ButtonPanel" type="PanelContainer" parent="VBoxContainer"] 47 | layout_mode = 2 48 | 49 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ButtonPanel"] 50 | layout_mode = 2 51 | alignment = 1 52 | 53 | [node name="StartButton" type="Button" parent="VBoxContainer/ButtonPanel"] 54 | layout_mode = 2 55 | text = "Start Server" 56 | 57 | [node name="StopButton" type="Button" parent="VBoxContainer/ButtonPanel"] 58 | layout_mode = 2 59 | disabled = true 60 | text = "Stop Server" 61 | 62 | [node name="LogPanel" type="PanelContainer" parent="VBoxContainer"] 63 | layout_mode = 2 64 | size_flags_vertical = 3 65 | 66 | [node name="LogDisplay" type="TextEdit" parent="VBoxContainer/LogPanel"] 67 | layout_mode = 2 68 | editable = false 69 | wrap_mode = 1 -------------------------------------------------------------------------------- /addons/godot_mcp/ui/mcp_panel.gd: -------------------------------------------------------------------------------- 1 | # addons/godot_mcp/ui/mcp_panel.gd 2 | @tool 3 | extends Control 4 | 5 | var status_label: Label 6 | var port_field: SpinBox 7 | var start_button: Button 8 | var stop_button: Button 9 | var log_display: TextEdit 10 | 11 | func _ready(): 12 | # Set up references to UI elements 13 | status_label = $VBoxContainer/StatusPanel/StatusLabel 14 | port_field = $VBoxContainer/ConfigPanel/PortField 15 | start_button = $VBoxContainer/ButtonPanel/StartButton 16 | stop_button = $VBoxContainer/ButtonPanel/StopButton 17 | log_display = $VBoxContainer/LogPanel/LogDisplay 18 | 19 | # Initialize UI 20 | port_field.value = 6400 # Default port 21 | start_button.disabled = false 22 | stop_button.disabled = true 23 | 24 | # Connect signals 25 | start_button.pressed.connect(_on_start_button_pressed) 26 | stop_button.pressed.connect(_on_stop_button_pressed) 27 | 28 | # Set initial status 29 | update_status("Not running") 30 | add_log_message("Godot MCP Plugin initialized") 31 | 32 | func update_status(status_text: String, is_error: bool = false): 33 | status_label.text = "Status: " + status_text 34 | if is_error: 35 | status_label.add_theme_color_override("font_color", Color(1, 0.3, 0.3)) 36 | else: 37 | status_label.remove_theme_color_override("font_color") 38 | 39 | func add_log_message(message: String): 40 | var timestamp = Time.get_datetime_string_from_system() 41 | log_display.text += "[" + timestamp + "] " + message + "\n" 42 | log_display.scroll_vertical = log_display.get_line_count() 43 | 44 | func _on_start_button_pressed(): 45 | # This function will be called from the plugin.gd script 46 | # when the server is actually started 47 | update_status("Running on port " + str(port_field.value)) 48 | start_button.disabled = true 49 | stop_button.disabled = false 50 | add_log_message("MCP Server started on port " + str(port_field.value)) 51 | 52 | func _on_stop_button_pressed(): 53 | # This function will be called from the plugin.gd script 54 | # when the server is actually stopped 55 | update_status("Stopped") 56 | start_button.disabled = false 57 | stop_button.disabled = true 58 | add_log_message("MCP Server stopped") 59 | 60 | # Function to be called from plugin.gd when a client connects 61 | func on_client_connected(): 62 | add_log_message("Client connected") 63 | update_status("Client connected") 64 | 65 | # Function to be called from plugin.gd when a client disconnects 66 | func on_client_disconnected(): 67 | add_log_message("Client disconnected") 68 | update_status("Running (no clients)") 69 | 70 | # Function to be called from plugin.gd when a command is received 71 | func on_command_received(command_type, params): 72 | add_log_message("Command received: " + command_type) 73 | 74 | # Function to be called from plugin.gd when a response is sent 75 | func on_response_sent(command_type, success): 76 | var status = "Success" if success else "Failed" 77 | add_log_message("Response sent for " + command_type + ": " + status) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot MCP Server 2 | 3 | A Model Context Protocol (MCP) server that enables Claude Desktop to control and interact with the Godot Engine editor. 4 | 5 | 6 | # DEMO VIDEO 7 | 8 | 9 | 10 | https://github.com/user-attachments/assets/07424399-31b5-47ee-a20d-808b2e789731 11 | 12 | 13 | # NEW UPDATE!!!! ADDED MESHY API INTEGRATION 14 | 15 | Screenshot 2025-07-14 at 9 07 13 PM 16 | 17 | GENERATE DYNAMIC SCENES BY CALLING THE MESHY API, DIRECTLY IMPORTED INTO GODOT 18 | 19 | 20 | 21 | 22 | 23 | 24 | ## Setup Instructions 25 | 26 | ### Prerequisites 27 | 28 | - Godot Engine (4.x or later) 29 | - Python 3.8+ 30 | - Claude Desktop app 31 | - Meshy API account (optional, for AI-generated meshes) 32 | 33 | 34 | ### STEP 0: Clone the repo and navigate to the directory 35 | 36 | ```bash 37 | git clone https://github.com/Dokujaa/Godot-MCP.git 38 | 39 | ``` 40 | 41 | 42 | 43 | 44 | ### Step 1: Install Godot Plugin 45 | 46 | 1. Copy the `addons/godot_mcp/` folder to your Godot project's `addons/` directory 47 | 2. Open your Godot project 48 | 3. Go to `Project → Project Settings → Plugins` 49 | 4. Enable the "Godot MCP" plugin 50 | 5. You should see an "MCP" panel appear at the bottom of the editor 51 | 6. The plugin automatically starts listening on a port 52 | 53 | ### Step 2: Set up Python Environment 54 | 55 | 1. Navigate to the `python/` directory: 56 | ```bash 57 | cd python 58 | ``` 59 | 60 | 2. Create and activate a virtual environment: 61 | ```bash 62 | python3 -m venv venv 63 | source venv/bin/activate # On Windows: venv\Scripts\activate 64 | ``` 65 | 66 | 3. Install dependencies: 67 | ```bash 68 | pip install -r ../requirements.txt 69 | ``` 70 | 71 | ### Step 3: Configure Claude Desktop 72 | 73 | 1. Locate your Claude Desktop configuration file: 74 | - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 75 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 76 | 77 | 2. Add the Godot MCP server configuration: 78 | ```json 79 | { 80 | "mcpServers": { 81 | "godot": { 82 | "command": "/path/to/your/godot-mcp/python/venv/bin/python", 83 | "args": ["/path/to/your/godot-mcp/python/server.py"], 84 | "env": {} 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | Replace `/path/to/your/godot-mcp/python/server.py` with the actual path to your server.py file. 91 | 92 | 3. Restart Claude Desktop and happy prompting! 93 | 94 | ### OPTIONAL: Set up Meshy API 95 | 96 | 1. Sign up for a Meshy API account at [https://www.meshy.ai/](https://www.meshy.ai/) 97 | 2. Get your API key from the dashboard (format: `msy-`) 98 | 3. Set up your API key using one of these methods: 99 | 100 | **Option A: Using .env file (Recommended)** 101 | ```bash 102 | # Copy the example file 103 | cp python/.env.example python/.env 104 | 105 | # Edit the .env file and add your API key 106 | nano python/.env # or use your preferred editor 107 | ``` 108 | 109 | Then add your key to the `.env` file: 110 | ``` 111 | MESHY_API_KEY=your_actual_api_key_here 112 | ``` 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /python/tools/material_tools.py: -------------------------------------------------------------------------------- 1 | # tools/material_tools.py 2 | from mcp.server.fastmcp import FastMCP, Context 3 | from typing import List, Optional 4 | from godot_connection import get_godot_connection 5 | 6 | def register_material_tools(mcp: FastMCP): 7 | """Register all material-related tools with the MCP server.""" 8 | 9 | @mcp.tool() 10 | def set_material( 11 | ctx: Context, 12 | object_name: str, 13 | material_name: Optional[str] = None, 14 | color: Optional[List[float]] = None, 15 | create_if_missing: bool = True 16 | ) -> str: 17 | """Apply or create a material for an object. 18 | 19 | Args: 20 | ctx: The MCP context 21 | object_name: Name of the target object in the scene 22 | material_name: Optional name for a shared material. If provided, creates/uses a shared material asset 23 | color: Optional RGBA color values [r, g, b] or [r, g, b, a] in range 0.0-1.0 24 | create_if_missing: Whether to create the material if it doesn't exist 25 | 26 | Returns: 27 | str: Success message or error details 28 | """ 29 | try: 30 | params = { 31 | "object_name": object_name, 32 | "create_if_missing": create_if_missing 33 | } 34 | 35 | if material_name: 36 | params["material_name"] = material_name 37 | 38 | if color: 39 | # Validate color array 40 | if len(color) < 3 or len(color) > 4: 41 | return "Error: Color must be [r, g, b] or [r, g, b, a]" 42 | 43 | # Ensure all values are in 0.0-1.0 range 44 | for value in color: 45 | if value < 0.0 or value > 1.0: 46 | return "Error: Color values must be in range 0.0-1.0" 47 | 48 | params["color"] = color 49 | 50 | response = get_godot_connection().send_command("SET_MATERIAL", params) 51 | return response.get("message", "Material applied successfully") 52 | except Exception as e: 53 | return f"Error setting material: {str(e)}" 54 | 55 | @mcp.tool() 56 | def list_materials(ctx: Context, folder_path: str = "res://materials") -> str: 57 | """List all material files in a specified folder. 58 | 59 | Args: 60 | ctx: The MCP context 61 | folder_path: Path to the folder to search (default: "res://materials") 62 | 63 | Returns: 64 | str: List of material files or error message 65 | """ 66 | try: 67 | # Use asset list command with material type filter 68 | response = get_godot_connection().send_command("GET_ASSET_LIST", { 69 | "type": "material", 70 | "folder": folder_path 71 | }) 72 | 73 | materials = response.get("assets", []) 74 | if not materials: 75 | return f"No materials found in {folder_path}" 76 | 77 | result = "Available materials:\n" 78 | for mat in materials: 79 | result += f"- {mat.get('name')} ({mat.get('path')})\n" 80 | 81 | return result 82 | except Exception as e: 83 | return f"Error listing materials: {str(e)}" -------------------------------------------------------------------------------- /python/tools/editor_tools.py: -------------------------------------------------------------------------------- 1 | # tools/editor_tools.py 2 | from mcp.server.fastmcp import FastMCP, Context 3 | from typing import Optional 4 | from godot_connection import get_godot_connection 5 | 6 | def register_editor_tools(mcp: FastMCP): 7 | """Register all editor control tools with the MCP server.""" 8 | 9 | @mcp.tool() 10 | def editor_action(ctx: Context, command: str) -> str: 11 | """Execute an editor command like play, stop, or save. 12 | 13 | Args: 14 | ctx: The MCP context 15 | command: The command to execute (PLAY, STOP, SAVE) 16 | 17 | Returns: 18 | str: Success message or error details 19 | """ 20 | try: 21 | # Validate command 22 | valid_commands = ["PLAY", "STOP", "SAVE"] 23 | if command.upper() not in valid_commands: 24 | return f"Error: Invalid command '{command}'. Valid commands are {', '.join(valid_commands)}" 25 | 26 | response = get_godot_connection().send_command("EDITOR_CONTROL", { 27 | "command": command.upper() 28 | }) 29 | 30 | return response.get("message", f"Editor command '{command}' executed") 31 | except Exception as e: 32 | return f"Error executing editor command: {str(e)}" 33 | 34 | @mcp.tool() 35 | def show_message( 36 | ctx: Context, 37 | title: str, 38 | message: str, 39 | type: str = "INFO" 40 | ) -> str: 41 | """Show a message in the Godot editor. 42 | 43 | Args: 44 | ctx: The MCP context 45 | title: Title of the message 46 | message: Content of the message 47 | type: Message type (INFO, WARNING, ERROR) 48 | 49 | Returns: 50 | str: Success message or error details 51 | """ 52 | try: 53 | # Validate message type 54 | valid_types = ["INFO", "WARNING", "ERROR"] 55 | if type.upper() not in valid_types: 56 | return f"Error: Invalid message type '{type}'. Valid types are {', '.join(valid_types)}" 57 | 58 | response = get_godot_connection().send_command("EDITOR_CONTROL", { 59 | "command": "SHOW_MESSAGE", 60 | "params": { 61 | "title": title, 62 | "message": message, 63 | "type": type.upper() 64 | } 65 | }) 66 | 67 | return response.get("message", "Message shown in editor") 68 | except Exception as e: 69 | return f"Error showing message: {str(e)}" 70 | 71 | @mcp.tool() 72 | def play_scene(ctx: Context) -> str: 73 | """Start playing the current scene in the editor. 74 | 75 | Args: 76 | ctx: The MCP context 77 | 78 | Returns: 79 | str: Success message or error details 80 | """ 81 | return editor_action(ctx, "PLAY") 82 | 83 | @mcp.tool() 84 | def stop_scene(ctx: Context) -> str: 85 | """Stop playing the current scene in the editor. 86 | 87 | Args: 88 | ctx: The MCP context 89 | 90 | Returns: 91 | str: Success message or error details 92 | """ 93 | return editor_action(ctx, "STOP") 94 | 95 | @mcp.tool() 96 | def save_all(ctx: Context) -> str: 97 | """Save all open resources in the editor. 98 | 99 | Args: 100 | ctx: The MCP context 101 | 102 | Returns: 103 | str: Success message or error details 104 | """ 105 | return editor_action(ctx, "SAVE") -------------------------------------------------------------------------------- /addons/godot_mcp/plugin.gd: -------------------------------------------------------------------------------- 1 | # Structure for addons/godot_mcp/plugin.gd 2 | @tool 3 | extends EditorPlugin 4 | 5 | const SERVER_PORT = 6400 6 | var server: TCPServer = null 7 | var active_connections = [] 8 | var command_handler 9 | 10 | func _enter_tree(): 11 | # Initialize the plugin 12 | print("Godot MCP Plugin activated") 13 | 14 | # Create command handler 15 | command_handler = preload("res://addons/godot_mcp/command_handler.gd").new() 16 | command_handler.set_editor_plugin(self) 17 | 18 | # Start the TCP server 19 | server = TCPServer.new() 20 | var error = server.listen(SERVER_PORT) 21 | if error != OK: 22 | push_error("Failed to start Godot MCP Server on port %d: %s" % [SERVER_PORT, error]) 23 | return 24 | 25 | print("Godot MCP Server listening on port %d" % SERVER_PORT) 26 | 27 | # Add UI 28 | add_control_to_bottom_panel( 29 | preload("res://addons/godot_mcp/ui/mcp_panel.tscn").instantiate(), 30 | "MCP" 31 | ) 32 | 33 | func _exit_tree(): 34 | # Clean up the plugin when disabled 35 | if server: 36 | server.stop() 37 | server = null 38 | 39 | for connection in active_connections: 40 | if connection.get_status() == StreamPeerTCP.STATUS_CONNECTED: 41 | connection.disconnect_from_host() 42 | 43 | active_connections.clear() 44 | 45 | # Remove UI 46 | remove_control_from_bottom_panel(get_editor_interface().get_base_control().get_node("MCPPanel")) 47 | print("Godot MCP Plugin deactivated") 48 | 49 | func _process(delta): 50 | # Check for new connections 51 | if server and server.is_connection_available(): 52 | var connection = server.take_connection() 53 | if connection: 54 | active_connections.append(connection) 55 | print("New MCP connection established") 56 | 57 | # Process existing connections 58 | var i = 0 59 | while i < active_connections.size(): 60 | var connection = active_connections[i] 61 | 62 | # Check connection status 63 | if connection.get_status() != StreamPeerTCP.STATUS_CONNECTED: 64 | active_connections.remove_at(i) 65 | print("MCP connection closed") 66 | continue 67 | 68 | # Check for incoming messages 69 | if connection.get_available_bytes() > 0: 70 | var data = _read_message(connection) 71 | if data.size() > 0: 72 | # Process the command 73 | var response = _process_command(data) 74 | 75 | # Send the response 76 | _send_message(connection, response) 77 | 78 | i += 1 79 | 80 | func _read_message(connection): 81 | # Read data from the connection 82 | var data = PackedByteArray() 83 | var bytes_available = connection.get_available_bytes() 84 | 85 | if bytes_available > 0: 86 | data = connection.get_data(bytes_available)[1] 87 | 88 | # Attempt to parse as JSON 89 | var json_string = data.get_string_from_utf8() 90 | var json = JSON.new() 91 | var error = json.parse(json_string) 92 | 93 | if error == OK: 94 | return json.get_data() 95 | else: 96 | print("Failed to parse JSON: ", json.get_error_message()) 97 | 98 | return {} 99 | 100 | func _send_message(connection, data): 101 | # Convert to JSON and send 102 | var json_string = JSON.stringify(data) 103 | connection.put_data(json_string.to_utf8_buffer()) 104 | 105 | 106 | 107 | func _process_command(data): 108 | # Process the command and return a response 109 | if data == null or typeof(data) != TYPE_DICTIONARY: 110 | return { 111 | "status": "error", 112 | "error": "Invalid command format. Expected a dictionary." 113 | } 114 | 115 | if not data.has("type") or not data.has("params"): 116 | return { 117 | "status": "error", 118 | "error": "Invalid command format. Expected 'type' and 'params' fields." 119 | } 120 | 121 | var command_type = data["type"] 122 | var params = data["params"] 123 | 124 | if command_type == "ping": 125 | return {"status": "success", "result": {"message": "pong"}} 126 | 127 | # Forward to command handler 128 | var result = command_handler.handle_command(command_type, params) 129 | 130 | # Check if result is valid 131 | if result == null: 132 | return { 133 | "status": "error", 134 | "error": "Command handler returned null result" 135 | } 136 | 137 | if result.has("error"): 138 | return {"status": "error", "error": result.error} 139 | else: 140 | return {"status": "success", "result": result} 141 | -------------------------------------------------------------------------------- /python/tools/script_tools.py: -------------------------------------------------------------------------------- 1 | # tools/script_tools.py 2 | from mcp.server.fastmcp import FastMCP, Context 3 | from typing import List 4 | from godot_connection import get_godot_connection 5 | 6 | def register_script_tools(mcp: FastMCP): 7 | """Register all script-related tools with the MCP server.""" 8 | 9 | @mcp.tool() 10 | def view_script(ctx: Context, script_path: str, require_exists: bool = True) -> str: 11 | """View the contents of a Godot script file. 12 | 13 | Args: 14 | ctx: The MCP context 15 | script_path: Path to the script file (e.g., "res://scripts/player.gd") 16 | require_exists: Whether to raise an error if the file doesn't exist 17 | 18 | Returns: 19 | str: The contents of the script file or error message 20 | """ 21 | try: 22 | # Ensure path starts with res:// 23 | if not script_path.startswith("res://"): 24 | script_path = "res://" + script_path 25 | 26 | # Ensure it has .gd extension if no extension is provided 27 | if "." not in script_path.split("/")[-1]: 28 | script_path += ".gd" 29 | 30 | response = get_godot_connection().send_command("VIEW_SCRIPT", { 31 | "script_path": script_path, 32 | "require_exists": require_exists 33 | }) 34 | 35 | if response.get("exists", True): 36 | return response.get("content", "Script contents not available") 37 | else: 38 | return response.get("message", "Script not found") 39 | except Exception as e: 40 | return f"Error viewing script: {str(e)}" 41 | 42 | @mcp.tool() 43 | def create_script( 44 | ctx: Context, 45 | script_name: str, 46 | script_type: str = "Node", 47 | namespace: str = None, 48 | script_folder: str = "res://scripts", 49 | overwrite: bool = False, 50 | content: str = None 51 | ) -> str: 52 | """Create a new Godot script file. 53 | 54 | Args: 55 | ctx: The MCP context 56 | script_name: Name of the script (with or without .gd extension) 57 | script_type: Base class to extend (e.g., "Node", "Node3D", "Control") 58 | namespace: Optional class_name for the script 59 | script_folder: Folder path within the project to create the script 60 | overwrite: Whether to overwrite if script already exists 61 | content: Optional custom content for the script 62 | 63 | Returns: 64 | str: Success message or error details 65 | """ 66 | try: 67 | # Ensure script_name has .gd extension 68 | if not script_name.endswith(".gd"): 69 | script_name += ".gd" 70 | 71 | # Ensure script_folder starts with res:// 72 | if not script_folder.startswith("res://"): 73 | script_folder = "res://" + script_folder 74 | 75 | params = { 76 | "script_name": script_name, 77 | "script_type": script_type, 78 | "script_folder": script_folder, 79 | "overwrite": overwrite 80 | } 81 | 82 | if namespace: 83 | params["namespace"] = namespace 84 | 85 | if content: 86 | params["content"] = content 87 | 88 | response = get_godot_connection().send_command("CREATE_SCRIPT", params) 89 | return response.get("message", "Script created successfully") 90 | except Exception as e: 91 | return f"Error creating script: {str(e)}" 92 | 93 | @mcp.tool() 94 | def update_script( 95 | ctx: Context, 96 | script_path: str, 97 | content: str, 98 | create_if_missing: bool = False, 99 | create_folder_if_missing: bool = False 100 | ) -> str: 101 | """Update the contents of an existing Godot script. 102 | 103 | Args: 104 | ctx: The MCP context 105 | script_path: Path to the script file (e.g., "res://scripts/player.gd") 106 | content: New content for the script 107 | create_if_missing: Whether to create the script if it doesn't exist 108 | create_folder_if_missing: Whether to create the parent directory if needed 109 | 110 | Returns: 111 | str: Success message or error details 112 | """ 113 | try: 114 | # Ensure path starts with res:// 115 | if not script_path.startswith("res://"): 116 | script_path = "res://" + script_path 117 | 118 | # Ensure it has .gd extension if no extension is provided 119 | if "." not in script_path.split("/")[-1]: 120 | script_path += ".gd" 121 | 122 | response = get_godot_connection().send_command("UPDATE_SCRIPT", { 123 | "script_path": script_path, 124 | "content": content, 125 | "create_if_missing": create_if_missing, 126 | "create_folder_if_missing": create_folder_if_missing 127 | }) 128 | 129 | return response.get("message", "Script updated successfully") 130 | except Exception as e: 131 | return f"Error updating script: {str(e)}" 132 | 133 | @mcp.tool() 134 | def list_scripts(ctx: Context, folder_path: str = "res://") -> str: 135 | """List all script files in a specified folder. 136 | 137 | Args: 138 | ctx: The MCP context 139 | folder_path: Path to the folder to search (default: "res://") 140 | 141 | Returns: 142 | str: List of script files or error message 143 | """ 144 | try: 145 | # Ensure path starts with res:// 146 | if not folder_path.startswith("res://"): 147 | folder_path = "res://" + folder_path 148 | 149 | response = get_godot_connection().send_command("LIST_SCRIPTS", { 150 | "folder_path": folder_path 151 | }) 152 | 153 | scripts = response.get("scripts", []) 154 | if not scripts: 155 | return "No scripts found in the specified folder" 156 | 157 | return "\n".join(scripts) 158 | except Exception as e: 159 | return f"Error listing scripts: {str(e)}" -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # Godot MCP Server 2 | 3 | A Model Context Protocol (MCP) server that enables Claude Desktop to control and interact with the Godot Engine editor. This allows you to use Claude to create scenes, manipulate objects, write scripts, and perform various editor operations directly through conversation. 4 | 5 | ## Architecture 6 | 7 | This project consists of two components: 8 | 9 | 1. **Godot Plugin** (`addons/godot_mcp/`) - Runs inside Godot Editor and listens for commands on port 6400 10 | 2. **Python MCP Server** (`python/`) - Acts as a bridge between Claude Desktop and the Godot plugin 11 | 12 | ## Setup Instructions 13 | 14 | ### Prerequisites 15 | 16 | - Godot Engine (4.x or later) 17 | - Python 3.8+ 18 | - Claude Desktop app 19 | - Meshy API account (optional, for AI-generated meshes) 20 | 21 | ### Step 1: Install Godot Plugin 22 | 23 | 1. Copy the `addons/godot_mcp/` folder to your Godot project's `addons/` directory 24 | 2. Open your Godot project 25 | 3. Go to `Project → Project Settings → Plugins` 26 | 4. Enable the "Godot MCP" plugin 27 | 5. You should see an "MCP" panel appear at the bottom of the editor 28 | 6. The plugin automatically starts listening on port 6400 29 | 30 | ### Step 2: Set up Python Environment 31 | 32 | 1. Navigate to the `python/` directory: 33 | ```bash 34 | cd python 35 | ``` 36 | 37 | 2. Create and activate a virtual environment: 38 | ```bash 39 | python3 -m venv venv 40 | source venv/bin/activate # On Windows: venv\Scripts\activate 41 | ``` 42 | 43 | 3. Install dependencies: 44 | ```bash 45 | pip install -r ../requirements.txt 46 | ``` 47 | 48 | ### Step 3: Configure Claude Desktop 49 | 50 | 1. Locate your Claude Desktop configuration file: 51 | - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` 52 | - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` 53 | 54 | 2. Add the Godot MCP server configuration: 55 | ```json 56 | { 57 | "mcpServers": { 58 | "godot": { 59 | "command": "python", 60 | "args": ["/path/to/your/godot-mcp/python/server.py"], 61 | "env": {} 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | Replace `/path/to/your/godot-mcp/python/server.py` with the actual path to your server.py file. 68 | 69 | 3. Restart Claude Desktop 70 | 71 | ### Step 4: Set up Meshy API (Optional) 72 | 73 | If you want to use AI-generated mesh features: 74 | 75 | **For Testing (Free - No Credits Consumed):** 76 | ```bash 77 | export MESHY_API_KEY="msy_dummy_api_key_for_test_mode_12345678" 78 | ``` 79 | 80 | **For Production:** 81 | 1. Sign up for a Meshy API account at [https://www.meshy.ai/](https://www.meshy.ai/) 82 | 2. Get your API key from the dashboard (format: `msy-`) 83 | 3. Set up your API key using one of these methods: 84 | 85 | **Option A: Using .env file (Recommended)** 86 | ```bash 87 | # Copy the example file 88 | cp python/.env.example python/.env 89 | 90 | # Edit the .env file and add your API key 91 | nano python/.env # or use your preferred editor 92 | ``` 93 | 94 | Then add your key to the `.env` file: 95 | ``` 96 | MESHY_API_KEY=your_actual_api_key_here 97 | ``` 98 | 99 | **Option B: Using system environment variables** 100 | ```bash 101 | export MESHY_API_KEY="your_api_key_here" 102 | ``` 103 | 104 | Or add it to your shell profile (`.zshrc`, `.bashrc`, etc.): 105 | ```bash 106 | echo 'export MESHY_API_KEY="your_api_key_here"' >> ~/.zshrc 107 | source ~/.zshrc 108 | ``` 109 | 110 | **Note**: The test API key (`msy_dummy_api_key_for_test_mode_12345678`) returns sample results without consuming credits, perfect for testing your integration before using your real API key. 111 | 112 | ### Step 5: Test the Setup 113 | 114 | 1. Make sure Godot is running with your project open and the MCP plugin enabled 115 | 2. Open Claude Desktop 116 | 3. Start a new conversation and try asking Claude to: 117 | - Get scene information: "Can you show me information about the current scene?" 118 | - Create objects: "Create a cube named 'TestCube' at position [2, 1, 0]" 119 | - Manipulate objects: "Set the material color of TestCube to red" 120 | - Generate AI meshes: "Generate a realistic medieval sword and place it at [0, 1, 0]" 121 | 122 | ## Usage 123 | 124 | Once set up, you can ask Claude to perform various Godot operations: 125 | 126 | ### Scene Management 127 | - Get current scene info 128 | - Open/save scenes 129 | - Create new scenes 130 | 131 | ### Object Operations 132 | - Create 3D/2D objects and UI elements 133 | - Set object transforms (position, rotation, scale) 134 | - Create parent-child relationships 135 | - Set object properties 136 | 137 | ### Script Management 138 | - Create GDScript files 139 | - View and edit existing scripts 140 | - List scripts in the project 141 | 142 | ### Material and Asset Management 143 | - Set material properties and colors 144 | - Import external assets 145 | - Create and instantiate packed scenes (prefabs) 146 | 147 | ### Environment Setup 148 | - Configure WorldEnvironment settings 149 | - Set up lighting and sky materials 150 | - Adjust fog and atmospheric effects 151 | 152 | ### AI-Generated Meshes (with Meshy API) 153 | - Generate 3D models from text descriptions 154 | - Convert images to 3D meshes 155 | - Refine generated meshes to higher quality 156 | - Automatic import and placement in scenes 157 | 158 | ## Configuration 159 | 160 | The server configuration can be modified in `python/config.py`: 161 | 162 | - `godot_host`: Godot connection host (default: "localhost") 163 | - `godot_port`: Godot plugin port (default: 6400) 164 | - `connection_timeout`: Connection timeout in seconds (default: 300) 165 | - `log_level`: Logging level (default: "INFO") 166 | 167 | ## Troubleshooting 168 | 169 | ### Connection Issues 170 | 171 | 1. **"Could not connect to Godot"**: 172 | - Ensure Godot is running with your project open 173 | - Check that the MCP plugin is enabled 174 | - Verify the plugin is listening on port 6400 (check the MCP panel in Godot) 175 | 176 | 2. **"MCP server not responding"**: 177 | - Check the Claude Desktop configuration path is correct 178 | - Ensure Python virtual environment is activated 179 | - Check the server.py path in Claude Desktop config 180 | 181 | 3. **Permission Errors**: 182 | - Ensure the Python script has execute permissions 183 | - Check that the virtual environment is properly activated 184 | 185 | ### Debug Mode 186 | 187 | To enable debug logging, modify `config.py`: 188 | ```python 189 | log_level: str = "DEBUG" 190 | ``` 191 | 192 | This will provide detailed logs of all commands and responses between Claude and Godot. 193 | 194 | ## Available Commands 195 | 196 | The MCP server provides these tools to Claude: 197 | 198 | - **Scene Tools**: `get_scene_info`, `open_scene`, `save_scene`, `new_scene` 199 | - **Object Tools**: `create_object`, `create_child_object`, `delete_object`, `find_objects_by_name` 200 | - **Transform Tools**: `set_object_transform`, `set_parent` 201 | - **Property Tools**: `set_property`, `set_nested_property`, `get_object_properties` 202 | - **Material Tools**: `set_material`, `set_mesh`, `set_collision_shape` 203 | - **Script Tools**: `create_script`, `view_script`, `update_script`, `list_scripts` 204 | - **Asset Tools**: `import_asset`, `get_asset_list`, `create_prefab`, `instantiate_prefab` 205 | - **Editor Tools**: `editor_action`, `play_scene`, `stop_scene`, `save_all` 206 | - **AI Mesh Tools**: `generate_mesh_from_text`, `generate_mesh_from_image`, `refine_generated_mesh` 207 | 208 | ## Examples 209 | 210 | Here are some example interactions you can have with Claude: 211 | 212 | ``` 213 | "Create a simple 3D scene with a ground plane, a player character, and some lighting" 214 | 215 | "Set up a basic character controller with a CharacterBody3D, mesh, and collision shape" 216 | 217 | "Create a simple inventory system script for an RPG game" 218 | 219 | "Set up a beautiful sky environment with clouds and atmospheric lighting" 220 | 221 | "Generate a realistic dragon model and place it in the scene" 222 | 223 | "Create a low-poly fantasy village with houses, trees, and a well from text descriptions" 224 | ``` 225 | 226 | ## License 227 | 228 | [Your License Here] 229 | -------------------------------------------------------------------------------- /python/godot_connection.py: -------------------------------------------------------------------------------- 1 | # godot_connection.py 2 | import socket 3 | import json 4 | import logging 5 | from dataclasses import dataclass 6 | from typing import Dict, Any 7 | from config import config 8 | 9 | # Configure logging using settings from config 10 | logging.basicConfig( 11 | level=getattr(logging, config.log_level), 12 | format=config.log_format 13 | ) 14 | logger = logging.getLogger("GodotMCP") 15 | 16 | @dataclass 17 | class GodotConnection: 18 | """Manages the socket connection to the Godot Editor.""" 19 | host: str = config.godot_host 20 | port: int = config.godot_port 21 | sock: socket.socket = None # Socket for Godot communication 22 | 23 | def connect(self) -> bool: 24 | """Establish a connection to the Godot Editor.""" 25 | if self.sock: 26 | return True 27 | try: 28 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 29 | self.sock.connect((self.host, self.port)) 30 | logger.info(f"Connected to Godot at {self.host}:{self.port}") 31 | return True 32 | except Exception as e: 33 | logger.error(f"Failed to connect to Godot: {str(e)}") 34 | self.sock = None 35 | return False 36 | 37 | def disconnect(self): 38 | """Close the connection to the Godot Editor.""" 39 | if self.sock: 40 | try: 41 | self.sock.close() 42 | except Exception as e: 43 | logger.error(f"Error disconnecting from Godot: {str(e)}") 44 | finally: 45 | self.sock = None 46 | 47 | def receive_full_response(self, sock, buffer_size=config.buffer_size) -> bytes: 48 | """Receive a complete response from Godot, handling chunked data.""" 49 | chunks = [] 50 | sock.settimeout(config.connection_timeout) # Use timeout from config 51 | try: 52 | while True: 53 | chunk = sock.recv(buffer_size) 54 | if not chunk: 55 | if not chunks: 56 | raise Exception("Connection closed before receiving data") 57 | break 58 | chunks.append(chunk) 59 | 60 | # Process the data received so far 61 | data = b''.join(chunks) 62 | decoded_data = data.decode('utf-8') 63 | 64 | # Check if we've received a complete response 65 | try: 66 | # Special case for ping-pong 67 | if decoded_data.strip().startswith('{"status":"success","result":{"message":"pong"'): 68 | logger.debug("Received ping response") 69 | return data 70 | 71 | # Validate JSON format 72 | json.loads(decoded_data) 73 | 74 | # If we get here, we have valid JSON 75 | logger.info(f"Received complete response ({len(data)} bytes)") 76 | return data 77 | except json.JSONDecodeError: 78 | # We haven't received a complete valid JSON response yet 79 | continue 80 | except Exception as e: 81 | logger.warning(f"Error processing response chunk: {str(e)}") 82 | # Continue reading more chunks as this might not be the complete response 83 | continue 84 | except socket.timeout: 85 | logger.warning("Socket timeout during receive") 86 | raise Exception("Timeout receiving Godot response") 87 | except Exception as e: 88 | logger.error(f"Error during receive: {str(e)}") 89 | raise 90 | 91 | 92 | def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict[str, Any]: 93 | """Send a command to Godot and return its response.""" 94 | if not self.sock and not self.connect(): 95 | raise ConnectionError("Not connected to Godot") 96 | 97 | # Special handling for ping command 98 | if command_type == "ping": 99 | try: 100 | logger.debug("Sending ping to verify connection") 101 | ping_command = json.dumps({"type": "ping", "params": {}}).encode('utf-8') 102 | self.sock.sendall(ping_command) 103 | response_data = self.receive_full_response(self.sock) 104 | 105 | if not response_data: 106 | logger.warning("Received empty ping response") 107 | self.sock = None 108 | raise ConnectionError("Empty ping response") 109 | 110 | response = json.loads(response_data.decode('utf-8')) 111 | 112 | if not response or response.get("status") != "success": 113 | logger.warning(f"Ping response was not successful: {response}") 114 | self.sock = None 115 | raise ConnectionError("Connection verification failed") 116 | 117 | return {"message": "pong"} 118 | except Exception as e: 119 | logger.error(f"Ping error: {str(e)}") 120 | self.sock = None 121 | raise ConnectionError(f"Connection verification failed: {str(e)}") 122 | 123 | # Normal command handling 124 | command = {"type": command_type, "params": params or {}} 125 | try: 126 | logger.info(f"Sending command: {command_type} with params: {params}") 127 | self.sock.sendall(json.dumps(command).encode('utf-8')) 128 | response_data = self.receive_full_response(self.sock) 129 | 130 | if not response_data: 131 | logger.warning(f"Received empty response for command: {command_type}") 132 | raise Exception("Empty response from Godot") 133 | 134 | response = json.loads(response_data.decode('utf-8')) 135 | 136 | if not response: 137 | logger.warning(f"Failed to parse response JSON for command: {command_type}") 138 | raise Exception("Invalid JSON response from Godot") 139 | 140 | if response.get("status") == "error": 141 | error_message = response.get("error") or response.get("message", "Unknown Godot error") 142 | logger.error(f"Godot error: {error_message}") 143 | raise Exception(error_message) 144 | 145 | result = response.get("result") 146 | if result is None: 147 | logger.warning(f"Response missing 'result' field for command: {command_type}") 148 | return {} 149 | 150 | return result 151 | except Exception as e: 152 | logger.error(f"Communication error with Godot: {str(e)}") 153 | self.sock = None 154 | raise Exception(f"Failed to communicate with Godot: {str(e)}") 155 | 156 | # Global Godot connection 157 | _godot_connection = None 158 | 159 | def get_godot_connection() -> GodotConnection: 160 | """Retrieve or establish a persistent Godot connection.""" 161 | global _godot_connection 162 | if _godot_connection is not None: 163 | try: 164 | # Try to ping with a short timeout to verify connection 165 | result = _godot_connection.send_command("ping") 166 | # If we get here, the connection is still valid 167 | logger.debug("Reusing existing Godot connection") 168 | return _godot_connection 169 | except Exception as e: 170 | logger.warning(f"Existing connection failed: {str(e)}") 171 | try: 172 | _godot_connection.disconnect() 173 | except: 174 | pass 175 | _godot_connection = None 176 | 177 | # Create a new connection 178 | logger.info("Creating new Godot connection") 179 | _godot_connection = GodotConnection() 180 | if not _godot_connection.connect(): 181 | _godot_connection = None 182 | raise ConnectionError("Could not connect to Godot. Ensure the Godot Editor and MCP Bridge are running.") 183 | 184 | try: 185 | # Verify the new connection works 186 | _godot_connection.send_command("ping") 187 | logger.info("Successfully established new Godot connection") 188 | return _godot_connection 189 | except Exception as e: 190 | logger.error(f"Could not verify new connection: {str(e)}") 191 | try: 192 | _godot_connection.disconnect() 193 | except: 194 | pass 195 | _godot_connection = None 196 | raise ConnectionError(f"Could not establish valid Godot connection: {str(e)}") -------------------------------------------------------------------------------- /python/tools/scene_tools.py: -------------------------------------------------------------------------------- 1 | # tools/scene_tools.py 2 | from mcp.server.fastmcp import FastMCP, Context 3 | from typing import Dict, Any, List 4 | import json 5 | from godot_connection import get_godot_connection 6 | from mcp.server.fastmcp import FastMCP, Context 7 | from typing import Dict, Any 8 | import json 9 | from godot_connection import get_godot_connection 10 | 11 | def register_scene_tools(mcp: FastMCP): 12 | """Register all scene-related tools with the MCP server.""" 13 | 14 | @mcp.tool() 15 | def get_scene_info(ctx: Context) -> str: 16 | """Get information about the current scene. 17 | 18 | Returns: 19 | str: JSON string containing scene information 20 | """ 21 | try: 22 | godot = get_godot_connection() 23 | result = godot.send_command("GET_SCENE_INFO") 24 | return json.dumps(result, indent=2) 25 | except Exception as e: 26 | return f"Error getting scene info: {str(e)}" 27 | 28 | @mcp.tool() 29 | def open_scene(ctx: Context, scene_path: str, save_current: bool = False) -> str: 30 | """Open a scene from the project. 31 | 32 | Args: 33 | ctx: The MCP context 34 | scene_path: Path to the scene file (e.g., "res://scenes/Main.tscn") 35 | save_current: Whether to save the current scene before opening the new one 36 | 37 | Returns: 38 | str: Success message or error details 39 | """ 40 | try: 41 | # Ensure path starts with res:// 42 | if not scene_path.startswith("res://"): 43 | scene_path = "res://" + scene_path 44 | 45 | # Ensure it has .tscn extension 46 | if not scene_path.endswith(".tscn") and not scene_path.endswith(".scn"): 47 | scene_path += ".tscn" 48 | 49 | response = get_godot_connection().send_command("OPEN_SCENE", { 50 | "scene_path": scene_path, 51 | "save_current": save_current 52 | }) 53 | return response.get("message", "Scene opened successfully") 54 | except Exception as e: 55 | return f"Error opening scene: {str(e)}" 56 | 57 | @mcp.tool() 58 | def save_scene(ctx: Context) -> str: 59 | """Save the current scene. 60 | 61 | Args: 62 | ctx: The MCP context 63 | 64 | Returns: 65 | str: Success message or error details 66 | """ 67 | try: 68 | response = get_godot_connection().send_command("SAVE_SCENE") 69 | return response.get("message", "Scene saved successfully") 70 | except Exception as e: 71 | return f"Error saving scene: {str(e)}" 72 | 73 | @mcp.tool() 74 | def new_scene(ctx: Context, scene_path: str, overwrite: bool = False) -> str: 75 | """Create a new empty scene. 76 | 77 | Args: 78 | ctx: The MCP context 79 | scene_path: Path where the new scene should be saved (e.g., "res://scenes/New.tscn") 80 | overwrite: Whether to overwrite if the scene already exists 81 | 82 | Returns: 83 | str: Success message or error details 84 | """ 85 | try: 86 | # Ensure path starts with res:// 87 | if not scene_path.startswith("res://"): 88 | scene_path = "res://" + scene_path 89 | 90 | # Ensure it has .tscn extension 91 | if not scene_path.endswith(".tscn"): 92 | scene_path += ".tscn" 93 | 94 | response = get_godot_connection().send_command("NEW_SCENE", { 95 | "scene_path": scene_path, 96 | "overwrite": overwrite 97 | }) 98 | return response.get("message", "New scene created successfully") 99 | except Exception as e: 100 | return f"Error creating new scene: {str(e)}" 101 | 102 | @mcp.tool() 103 | def create_object( 104 | ctx: Context, 105 | type: str = "EMPTY", 106 | name: str = None, 107 | location: list = None, 108 | rotation: list = None, 109 | scale: list = None, 110 | replace_if_exists: bool = False 111 | ) -> str: 112 | """Create a new object (node) in the current scene. 113 | 114 | Args: 115 | ctx: The MCP context 116 | type: Type of object to create. Common types include: 117 | - 3D Nodes: "Node3D", "MeshInstance3D", "Camera3D", "DirectionalLight3D", etc. 118 | - 3D Primitives: "CUBE", "SPHERE", "CYLINDER", "PLANE" 119 | - 2D Nodes: "Node2D", "Sprite2D", "Camera2D", etc. 120 | - UI Nodes: "Control", "Panel", "Button", "Label", etc. 121 | name: Optional name for the new object 122 | location: Optional [x, y, z] position (for 2D nodes, only x,y are used) 123 | rotation: Optional [x, y, z] rotation in degrees (for 2D nodes, only x is used) 124 | scale: Optional [x, y, z] scale factors (for 2D nodes, only x,y are used) 125 | replace_if_exists: Whether to replace if an object with the same name exists 126 | 127 | Returns: 128 | str: Success message or error details 129 | """ 130 | try: 131 | params = {"type": type} 132 | if name: 133 | params["name"] = name 134 | if location: 135 | params["location"] = location 136 | if rotation: 137 | params["rotation"] = rotation 138 | if scale: 139 | params["scale"] = scale 140 | params["replace_if_exists"] = replace_if_exists 141 | 142 | response = get_godot_connection().send_command("CREATE_OBJECT", params) 143 | 144 | if isinstance(response, dict) and "name" in response: 145 | node_type = response.get("type", type) 146 | return f"Created {node_type} object: {response['name']}" 147 | else: 148 | return f"Created {type} object" 149 | except Exception as e: 150 | return f"Error creating object: {str(e)}" 151 | 152 | @mcp.tool() 153 | def delete_object(ctx: Context, name: str) -> str: 154 | """Delete an object (node) from the current scene. 155 | 156 | Args: 157 | ctx: The MCP context 158 | name: Name of the object to delete 159 | 160 | Returns: 161 | str: Success message or error details 162 | """ 163 | try: 164 | response = get_godot_connection().send_command("DELETE_OBJECT", { 165 | "name": name 166 | }) 167 | return response.get("message", f"Object deleted: {name}") 168 | except Exception as e: 169 | return f"Error deleting object: {str(e)}" 170 | 171 | @mcp.tool() 172 | def find_objects_by_name(ctx: Context, name: str) -> str: 173 | """Find objects in the scene by name (partial matches supported). 174 | 175 | Args: 176 | ctx: The MCP context 177 | name: Name to search for 178 | 179 | Returns: 180 | str: JSON string with list of found objects or error details 181 | """ 182 | try: 183 | response = get_godot_connection().send_command("FIND_OBJECTS_BY_NAME", { 184 | "name": name 185 | }) 186 | 187 | objects = response.get("objects", []) 188 | if not objects: 189 | return f"No objects found with name containing '{name}'" 190 | 191 | return json.dumps(objects, indent=2) 192 | except Exception as e: 193 | return f"Error finding objects: {str(e)}" 194 | 195 | @mcp.tool() 196 | def set_object_transform( 197 | ctx: Context, 198 | name: str, 199 | location: List[float] = None, 200 | rotation: List[float] = None, 201 | scale: List[float] = None 202 | ) -> str: 203 | """Set the transform (position, rotation, scale) of an object. 204 | 205 | Args: 206 | ctx: The MCP context 207 | name: Name of the object to modify 208 | location: Optional [x, y, z] position 209 | rotation: Optional [x, y, z] rotation in degrees 210 | scale: Optional [x, y, z] scale factors 211 | 212 | Returns: 213 | str: Success message or error details 214 | """ 215 | try: 216 | params = {"name": name} 217 | if location: 218 | params["location"] = location 219 | if rotation: 220 | params["rotation"] = rotation 221 | if scale: 222 | params["scale"] = scale 223 | 224 | response = get_godot_connection().send_command("SET_OBJECT_TRANSFORM", params) 225 | return response.get("message", f"Transform updated for {name}") 226 | except Exception as e: 227 | return f"Error setting transform: {str(e)}" 228 | 229 | @mcp.tool() 230 | def get_object_properties(ctx: Context, name: str) -> str: 231 | """Get all properties of an object. 232 | 233 | Args: 234 | ctx: The MCP context 235 | name: Name of the object to inspect 236 | 237 | Returns: 238 | str: JSON string with object properties or error details 239 | """ 240 | try: 241 | response = get_godot_connection().send_command("GET_OBJECT_PROPERTIES", { 242 | "name": name 243 | }) 244 | 245 | if "error" in response: 246 | return f"Error: {response['error']}" 247 | 248 | return json.dumps(response, indent=2) 249 | except Exception as e: 250 | return f"Error getting object properties: {str(e)}" -------------------------------------------------------------------------------- /python/tools/asset_tools.py: -------------------------------------------------------------------------------- 1 | # tools/asset_tools.py 2 | from mcp.server.fastmcp import FastMCP, Context 3 | from typing import Optional, List 4 | from godot_connection import get_godot_connection 5 | import json 6 | 7 | def register_asset_tools(mcp: FastMCP): 8 | """Register all asset management tools with the MCP server.""" 9 | 10 | @mcp.tool() 11 | def get_asset_list( 12 | ctx: Context, 13 | type: Optional[str] = None, 14 | search_pattern: str = "*", 15 | folder: str = "res://" 16 | ) -> str: 17 | """List assets in the project. 18 | 19 | Args: 20 | ctx: The MCP context 21 | type: Optional asset type to filter by (e.g., "scene", "script", "texture") 22 | search_pattern: Pattern to match in asset names 23 | folder: Folder path to search in 24 | 25 | Returns: 26 | str: JSON string with list of found assets or error details 27 | """ 28 | try: 29 | # Ensure folder starts with res:// 30 | if not folder.startswith("res://"): 31 | folder = "res://" + folder 32 | 33 | params = { 34 | "search_pattern": search_pattern, 35 | "folder": folder 36 | } 37 | 38 | if type: 39 | params["type"] = type 40 | 41 | response = get_godot_connection().send_command("GET_ASSET_LIST", params) 42 | assets = response.get("assets", []) 43 | 44 | if not assets: 45 | if type: 46 | return f"No {type} assets found in {folder} matching '{search_pattern}'" 47 | else: 48 | return f"No assets found in {folder} matching '{search_pattern}'" 49 | 50 | return json.dumps(assets, indent=2) 51 | except Exception as e: 52 | return f"Error listing assets: {str(e)}" 53 | 54 | @mcp.tool() 55 | def import_asset( 56 | ctx: Context, 57 | source_path: str, 58 | target_path: str, 59 | overwrite: bool = False 60 | ) -> str: 61 | """Import an external asset into the project. 62 | 63 | Args: 64 | ctx: The MCP context 65 | source_path: Path to the source file on disk 66 | target_path: Path where the asset should be imported in the project 67 | overwrite: Whether to overwrite if an asset already exists at target path 68 | 69 | Returns: 70 | str: Success message or error details 71 | """ 72 | try: 73 | # Ensure target_path starts with res:// 74 | if not target_path.startswith("res://"): 75 | target_path = "res://" + target_path 76 | 77 | response = get_godot_connection().send_command("IMPORT_ASSET", { 78 | "source_path": source_path, 79 | "target_path": target_path, 80 | "overwrite": overwrite 81 | }) 82 | 83 | return response.get("message", "Asset imported successfully") 84 | except Exception as e: 85 | return f"Error importing asset: {str(e)}" 86 | 87 | @mcp.tool() 88 | def create_prefab( 89 | ctx: Context, 90 | object_name: str, 91 | prefab_path: str, 92 | overwrite: bool = False 93 | ) -> str: 94 | """Create a packed scene (prefab) from an object in the scene. 95 | 96 | Args: 97 | ctx: The MCP context 98 | object_name: Name of the object to create a packed scene from 99 | prefab_path: Path where the packed scene should be saved 100 | overwrite: Whether to overwrite if a file already exists at the path 101 | 102 | Returns: 103 | str: Success message or error details 104 | """ 105 | try: 106 | # Ensure prefab_path starts with res:// 107 | if not prefab_path.startswith("res://"): 108 | prefab_path = "res://" + prefab_path 109 | 110 | # Ensure it has .tscn extension 111 | if not prefab_path.endswith(".tscn"): 112 | prefab_path += ".tscn" 113 | 114 | response = get_godot_connection().send_command("CREATE_PREFAB", { 115 | "object_name": object_name, 116 | "prefab_path": prefab_path, 117 | "overwrite": overwrite 118 | }) 119 | 120 | if response.get("success", False): 121 | return f"Packed scene created successfully at {response.get('path', prefab_path)}" 122 | else: 123 | return f"Error creating packed scene: {response.get('error', 'Unknown error')}" 124 | except Exception as e: 125 | return f"Error creating packed scene: {str(e)}" 126 | 127 | @mcp.tool() 128 | def instantiate_prefab( 129 | ctx: Context, 130 | prefab_path: str, 131 | position_x: float = 0.0, 132 | position_y: float = 0.0, 133 | position_z: float = 0.0, 134 | rotation_x: float = 0.0, 135 | rotation_y: float = 0.0, 136 | rotation_z: float = 0.0 137 | ) -> str: 138 | """Instantiate a packed scene (prefab) into the current scene. 139 | 140 | Args: 141 | ctx: The MCP context 142 | prefab_path: Path to the packed scene file 143 | position_x: X position in 3D space 144 | position_y: Y position in 3D space 145 | position_z: Z position in 3D space 146 | rotation_x: X rotation in degrees 147 | rotation_y: Y rotation in degrees 148 | rotation_z: Z rotation in degrees 149 | 150 | Returns: 151 | str: Success message or error details 152 | """ 153 | try: 154 | # Ensure prefab_path starts with res:// 155 | if not prefab_path.startswith("res://"): 156 | prefab_path = "res://" + prefab_path 157 | 158 | # Ensure it has .tscn extension 159 | if not prefab_path.endswith(".tscn") and not prefab_path.endswith(".scn"): 160 | prefab_path += ".tscn" 161 | 162 | response = get_godot_connection().send_command("INSTANTIATE_PREFAB", { 163 | "prefab_path": prefab_path, 164 | "position_x": position_x, 165 | "position_y": position_y, 166 | "position_z": position_z, 167 | "rotation_x": rotation_x, 168 | "rotation_y": rotation_y, 169 | "rotation_z": rotation_z 170 | }) 171 | 172 | if response.get("success", False): 173 | return f"Packed scene instantiated as {response.get('instance_name', 'unknown')}" 174 | else: 175 | return f"Error instantiating packed scene: {response.get('error', 'Unknown error')}" 176 | except Exception as e: 177 | return f"Error instantiating packed scene: {str(e)}" 178 | 179 | @mcp.tool() 180 | def import_3d_model( 181 | ctx: Context, 182 | model_path: str, 183 | name: str = None, 184 | position_x: float = 0.0, 185 | position_y: float = 0.0, 186 | position_z: float = 0.0, 187 | rotation_x: float = 0.0, 188 | rotation_y: float = 0.0, 189 | rotation_z: float = 0.0, 190 | scale_x: float = 1.0, 191 | scale_y: float = 1.0, 192 | scale_z: float = 1.0 193 | ) -> str: 194 | """Import a 3D model file (GLB, FBX, OBJ) into the current scene as a MeshInstance3D. 195 | 196 | This is different from instantiate_prefab which is for .tscn packed scenes. 197 | Use this for 3D model files like those generated by Meshy API. 198 | 199 | Args: 200 | ctx: The MCP context 201 | model_path: Path to the 3D model file (e.g., res://assets/generated_meshes/House.glb) 202 | name: Optional name for the imported model node 203 | position_x: X position in 3D space 204 | position_y: Y position in 3D space 205 | position_z: Z position in 3D space 206 | rotation_x: X rotation in degrees 207 | rotation_y: Y rotation in degrees 208 | rotation_z: Z rotation in degrees 209 | scale_x: X scale factor 210 | scale_y: Y scale factor 211 | scale_z: Z scale factor 212 | 213 | Returns: 214 | str: Success message with node details or error 215 | """ 216 | try: 217 | # Ensure model_path starts with res:// 218 | if not model_path.startswith("res://"): 219 | model_path = "res://" + model_path 220 | 221 | # Determine the name from the file if not provided 222 | if not name: 223 | # Extract filename without extension 224 | filename = model_path.split('/')[-1] 225 | name = filename.rsplit('.', 1)[0] if '.' in filename else filename 226 | 227 | # Check file extension 228 | extension = model_path.split('.')[-1].lower() if '.' in model_path else "" 229 | 230 | if extension == "glb" or extension == "gltf": 231 | # Use the specialized GLB import handler 232 | glb_response = get_godot_connection().send_command("IMPORT_GLB_SCENE", { 233 | "glb_path": model_path, 234 | "name": name, 235 | "position": [position_x, position_y, position_z], 236 | "rotation": [rotation_x, rotation_y, rotation_z], 237 | "scale": [scale_x, scale_y, scale_z] 238 | }) 239 | 240 | if glb_response.get("success", False): 241 | instance_name = glb_response.get("instance_name", name) 242 | return f"Successfully imported GLB model: {instance_name} at position ({position_x}, {position_y}, {position_z})" 243 | elif "error" in glb_response: 244 | # If GLB import fails, try regular mesh approach 245 | print(f"GLB import failed: {glb_response['error']}, trying mesh approach...") 246 | else: 247 | # Continue to mesh approach 248 | pass 249 | 250 | # Create a MeshInstance3D node 251 | create_response = get_godot_connection().send_command("CREATE_OBJECT", { 252 | "type": "MeshInstance3D", 253 | "name": name, 254 | "location": [position_x, position_y, position_z], 255 | "rotation": [rotation_x, rotation_y, rotation_z], 256 | "scale": [scale_x, scale_y, scale_z] 257 | }) 258 | 259 | if "error" in create_response: 260 | return f"Failed to create MeshInstance3D: {create_response['error']}" 261 | 262 | # Try to set the mesh resource 263 | set_mesh_response = get_godot_connection().send_command("SET_PROPERTY", { 264 | "node_name": name, 265 | "property_name": "mesh", 266 | "value": model_path 267 | }) 268 | 269 | if "error" in set_mesh_response: 270 | # If setting mesh fails, the node is still created 271 | return f"Created MeshInstance3D '{name}' but couldn't load mesh. You may need to manually assign the mesh from: {model_path}" 272 | 273 | return f"Successfully imported 3D model '{name}' from {model_path} at position ({position_x}, {position_y}, {position_z})" 274 | 275 | except Exception as e: 276 | return f"Error importing 3D model: {str(e)}" 277 | 278 | @mcp.tool() 279 | def list_generated_meshes(ctx: Context) -> str: 280 | """List all generated mesh files in the res://assets/generated_meshes/ folder. 281 | 282 | This is a convenience tool specifically for listing Meshy-generated models. 283 | 284 | Args: 285 | ctx: The MCP context 286 | 287 | Returns: 288 | str: List of generated mesh files or error message 289 | """ 290 | try: 291 | response = get_godot_connection().send_command("GET_ASSET_LIST", { 292 | "search_pattern": ".glb", 293 | "folder": "res://assets/generated_meshes/" 294 | }) 295 | 296 | assets = response.get("assets", []) 297 | 298 | if not assets: 299 | return "No generated meshes found. Generate some meshes using generate_mesh_from_text first!" 300 | 301 | # Format the output nicely 302 | result = "**Generated Meshes Available:**\n\n" 303 | glb_files = [asset for asset in assets if asset['name'].endswith('.glb')] 304 | 305 | for asset in glb_files: 306 | name = asset['name'].replace('.glb', '') 307 | result += f"• **{name}** - `{asset['path']}`\n" 308 | 309 | result += f"\n**Total:** {len(glb_files)} mesh(es)\n" 310 | result += "\nUse `import_3d_model` to add any of these to your scene!" 311 | 312 | return result 313 | 314 | except Exception as e: 315 | return f"Error listing generated meshes: {str(e)}" -------------------------------------------------------------------------------- /python/tools/object_tools.py: -------------------------------------------------------------------------------- 1 | # tools/object_tools.py 2 | from mcp.server.fastmcp import FastMCP, Context 3 | from typing import Dict, Any, List, Optional 4 | from godot_connection import get_godot_connection 5 | import json 6 | 7 | def register_object_tools(mcp: FastMCP): 8 | """Register all object inspection and manipulation tools with the MCP server.""" 9 | 10 | @mcp.tool() 11 | def get_object_properties(ctx: Context, name: str) -> Dict[str, Any]: 12 | """Get all properties of a specified object (node). 13 | 14 | Args: 15 | ctx: The MCP context 16 | name: Name of the object to inspect 17 | 18 | Returns: 19 | Dict: Object containing the node's properties, components, and their values 20 | """ 21 | try: 22 | response = get_godot_connection().send_command("GET_OBJECT_PROPERTIES", { 23 | "name": name 24 | }) 25 | 26 | if "error" in response: 27 | return {"error": response["error"]} 28 | 29 | return response 30 | except Exception as e: 31 | return {"error": f"Failed to get object properties: {str(e)}"} 32 | 33 | @mcp.tool() 34 | def get_hierarchy(ctx: Context) -> str: 35 | """Get the detailed hierarchy of objects in the current scene. 36 | 37 | Args: 38 | ctx: The MCP context 39 | 40 | Returns: 41 | str: JSON string containing the complete scene hierarchy 42 | """ 43 | try: 44 | scene_info = get_godot_connection().send_command("GET_SCENE_INFO") 45 | 46 | if "error" in scene_info: 47 | return f"Error: {scene_info['error']}" 48 | 49 | # Format the hierarchy for better readability 50 | formatted_result = { 51 | "scene_name": scene_info.get("name", "Unknown"), 52 | "scene_path": scene_info.get("path", "Unknown"), 53 | } 54 | 55 | # Include the full hierarchy if available 56 | if "hierarchy" in scene_info: 57 | formatted_result["hierarchy"] = scene_info["hierarchy"] 58 | 59 | # Print a more readable tree representation 60 | tree_view = _format_node_tree(scene_info["hierarchy"], indent="") 61 | return f"Scene: {formatted_result['scene_name']} ({formatted_result['scene_path']})\n\n{tree_view}" 62 | else: 63 | # Fall back to the simple root_objects list if full hierarchy isn't available 64 | formatted_result["root_objects"] = scene_info.get("root_objects", []) 65 | return json.dumps(formatted_result, indent=2) 66 | 67 | except Exception as e: 68 | return f"Error getting hierarchy: {str(e)}" 69 | 70 | def _format_node_tree(node_data, indent=""): 71 | """Helper function to format node hierarchy as a tree view.""" 72 | result = f"{indent}└─ {node_data['name']} ({node_data['type']})" 73 | 74 | # Add script info if available 75 | if "script" in node_data: 76 | result += f" [Script: {node_data['script']}]" 77 | 78 | # Add transform info for 3D nodes 79 | if "transform" in node_data: 80 | pos = node_data["transform"].get("position", [0, 0, 0]) 81 | pos_str = f"({pos[0]:.1f}, {pos[1]:.1f}, {pos[2]:.1f})" if len(pos) >= 3 else f"({pos[0]:.1f}, {pos[1]:.1f})" 82 | result += f" at {pos_str}" 83 | 84 | # Process children with increased indentation 85 | if "children" in node_data and node_data["children"]: 86 | result += "\n" 87 | for i, child in enumerate(node_data["children"]): 88 | # Use different indentation for last child 89 | is_last = i == len(node_data["children"]) - 1 90 | child_indent = indent + (" " if is_last else "│ ") 91 | result += _format_node_tree(child, child_indent) + "\n" 92 | 93 | return result.rstrip() 94 | 95 | 96 | @mcp.tool() 97 | def rename_node( 98 | ctx: Context, 99 | old_name: str, 100 | new_name: str 101 | ) -> str: 102 | """Rename a node in the scene. 103 | 104 | Args: 105 | ctx: The MCP context 106 | old_name: Current name of the node 107 | new_name: New name for the node 108 | 109 | Returns: 110 | str: Success message or error details 111 | """ 112 | try: 113 | # Check if node with old_name exists 114 | old_response = get_godot_connection().send_command("FIND_OBJECTS_BY_NAME", { 115 | "name": old_name 116 | }) 117 | 118 | node_exists = len(old_response.get("objects", [])) > 0 119 | if not node_exists: 120 | return f"Error: Node '{old_name}' not found" 121 | 122 | # Check if new_name is already taken 123 | new_response = get_godot_connection().send_command("FIND_OBJECTS_BY_NAME", { 124 | "name": new_name 125 | }) 126 | 127 | name_taken = len(new_response.get("objects", [])) > 0 128 | if name_taken: 129 | return f"Error: Name '{new_name}' is already taken by another node" 130 | 131 | # Rename the node 132 | rename_response = get_godot_connection().send_command("RENAME_NODE", { 133 | "old_name": old_name, 134 | "new_name": new_name 135 | }) 136 | 137 | if "error" in rename_response: 138 | return f"Error renaming node: {rename_response['error']}" 139 | 140 | return f"Renamed node from '{old_name}' to '{new_name}'" 141 | except Exception as e: 142 | return f"Error renaming node: {str(e)}" 143 | 144 | @mcp.tool() 145 | def set_property( 146 | ctx: Context, 147 | node_name: str, 148 | property_name: str, 149 | value: Any 150 | ) -> str: 151 | """Set a property value on a node. 152 | 153 | Args: 154 | ctx: The MCP context 155 | node_name: Name of the target node 156 | property_name: Name of the property to set 157 | value: New value for the property 158 | 159 | Returns: 160 | str: Success message or error details 161 | """ 162 | try: 163 | # Check input parameters 164 | if not node_name or not isinstance(node_name, str): 165 | return "Error: Invalid node_name parameter" 166 | 167 | if not property_name or not isinstance(property_name, str): 168 | return "Error: Invalid property_name parameter" 169 | 170 | # Handle special cases 171 | if property_name == "script" and isinstance(value, str): 172 | # Ensure script path starts with res:// 173 | if not value.startswith("res://"): 174 | value = "res://" + value 175 | 176 | # Ensure script has .gd extension if no extension provided 177 | if "." not in value.split("/")[-1]: 178 | value += ".gd" 179 | 180 | # Send the command 181 | response = get_godot_connection().send_command("SET_PROPERTY", { 182 | "node_name": node_name, 183 | "property_name": property_name, 184 | "value": value 185 | }) 186 | 187 | return response.get("message", f"Set property '{property_name}' on node '{node_name}' to {value}") 188 | except Exception as e: 189 | return f"Error setting property: {str(e)}" 190 | 191 | @mcp.tool() 192 | def create_child_object( 193 | ctx: Context, 194 | parent_name: str, 195 | type: str = "EMPTY", 196 | name: str = None, 197 | location: List[float] = None, 198 | rotation: List[float] = None, 199 | scale: List[float] = None, 200 | replace_if_exists: bool = False 201 | ) -> str: 202 | """Create a new object as a child of an existing node. 203 | 204 | Args: 205 | ctx: The MCP context 206 | parent_name: Name of the parent node to attach this object to 207 | type: Type of object to create (e.g., "Node3D", "MeshInstance3D", "CUBE") 208 | name: Optional name for the new object 209 | location: Optional [x, y, z] position 210 | rotation: Optional [x, y, z] rotation in degrees 211 | scale: Optional [x, y, z] scale factors 212 | replace_if_exists: Whether to replace if an object with the same name exists 213 | 214 | Returns: 215 | str: Success message or error details 216 | """ 217 | try: 218 | params = { 219 | "parent_name": parent_name, 220 | "type": type, 221 | "replace_if_exists": replace_if_exists 222 | } 223 | 224 | if name: 225 | params["name"] = name 226 | if location: 227 | params["location"] = location 228 | if rotation: 229 | params["rotation"] = rotation 230 | if scale: 231 | params["scale"] = scale 232 | 233 | response = get_godot_connection().send_command("CREATE_CHILD_OBJECT", params) 234 | 235 | if isinstance(response, dict) and "name" in response: 236 | node_type = response.get("type", type) 237 | return f"Created {node_type} object: {response['name']} as child of {parent_name}" 238 | else: 239 | return f"Created {type} object as child of {parent_name}" 240 | except Exception as e: 241 | return f"Error creating child object: {str(e)}" 242 | 243 | 244 | @mcp.tool() 245 | def set_mesh( 246 | ctx: Context, 247 | node_name: str, 248 | mesh_type: str, 249 | radius: float = None, 250 | height: float = None, 251 | size: List[float] = None 252 | ) -> str: 253 | """Set a mesh on a MeshInstance3D node. 254 | 255 | Args: 256 | ctx: The MCP context 257 | node_name: Name of the target MeshInstance3D node 258 | mesh_type: Type of mesh to create (CapsuleMesh, BoxMesh, SphereMesh, CylinderMesh, PlaneMesh) 259 | radius: Radius for CapsuleMesh, SphereMesh, or CylinderMesh 260 | height: Height for CapsuleMesh or CylinderMesh 261 | size: Size for BoxMesh [x, y, z] or PlaneMesh [x, y] 262 | 263 | Returns: 264 | str: Success message or error details 265 | """ 266 | try: 267 | params = { 268 | "node_name": node_name, 269 | "mesh_type": mesh_type 270 | } 271 | 272 | # Add optional parameters if provided 273 | mesh_params = {} 274 | if radius is not None: 275 | mesh_params["radius"] = radius 276 | if height is not None: 277 | mesh_params["height"] = height 278 | if size is not None: 279 | mesh_params["size"] = size 280 | 281 | if mesh_params: 282 | params["mesh_params"] = mesh_params 283 | 284 | response = get_godot_connection().send_command("SET_MESH", params) 285 | return response.get("message", f"Set {mesh_type} on node '{node_name}'") 286 | except Exception as e: 287 | return f"Error setting mesh: {str(e)}" 288 | 289 | 290 | @mcp.tool() 291 | def set_collision_shape( 292 | ctx: Context, 293 | node_name: str, 294 | shape_type: str, 295 | radius: float = None, 296 | height: float = None, 297 | size: List[float] = None 298 | ) -> str: 299 | """Set a collision shape on a CollisionShape3D or CollisionShape2D node. 300 | 301 | Args: 302 | ctx: The MCP context 303 | node_name: Name or path of the target CollisionShape node 304 | shape_type: Type of shape to create (CapsuleShape3D, BoxShape3D, SphereShape3D, etc.) 305 | radius: Radius for CapsuleShape3D, SphereShape3D, etc. 306 | height: Height for CapsuleShape3D or CylinderShape3D 307 | size: Size for BoxShape3D [x, y, z] or RectangleShape2D [x, y] 308 | 309 | Returns: 310 | str: Success message or error details 311 | """ 312 | try: 313 | params = { 314 | "node_name": node_name, 315 | "shape_type": shape_type 316 | } 317 | 318 | # Add optional parameters if provided 319 | shape_params = {} 320 | if radius is not None: 321 | shape_params["radius"] = radius 322 | if height is not None: 323 | shape_params["height"] = height 324 | if size is not None: 325 | shape_params["size"] = size 326 | 327 | if shape_params: 328 | params["shape_params"] = shape_params 329 | 330 | response = get_godot_connection().send_command("SET_COLLISION_SHAPE", params) 331 | return response.get("message", f"Set {shape_type} on node '{node_name}'") 332 | except Exception as e: 333 | return f"Error setting collision shape: {str(e)}" 334 | 335 | @mcp.tool() 336 | def set_nested_property( 337 | ctx: Context, 338 | node_name: str, 339 | property_name: str, 340 | value: Any, 341 | value_type: str = None 342 | ) -> str: 343 | """Set a nested property on a node (like environment/sky/sky_material). 344 | 345 | Args: 346 | ctx: The MCP context 347 | node_name: Name of the target node 348 | property_name: Path to the nested property using slashes (e.g., "environment/sky/sky_material") 349 | value: Value to set 350 | value_type: Optional type hint for the value 351 | 352 | Returns: 353 | str: Success message or error details 354 | """ 355 | try: 356 | params = { 357 | "node_name": node_name, 358 | "property_name": property_name, 359 | "value": value 360 | } 361 | 362 | if value_type: 363 | params["value_type"] = value_type 364 | 365 | response = get_godot_connection().send_command("SET_NESTED_PROPERTY", params) 366 | return response.get("message", f"Set nested property {property_name} on {node_name}") 367 | except Exception as e: 368 | return f"Error setting nested property: {str(e)}" -------------------------------------------------------------------------------- /python/server.py: -------------------------------------------------------------------------------- 1 | # server.py 2 | from mcp.server.fastmcp import FastMCP, Context, Image 3 | import logging 4 | from dataclasses import dataclass 5 | from contextlib import asynccontextmanager 6 | from typing import AsyncIterator, Dict, Any, List 7 | import json 8 | import socket 9 | import os 10 | from config import config 11 | from tools import register_all_tools 12 | from godot_connection import get_godot_connection, GodotConnection 13 | 14 | # Configure logging using settings from config 15 | logging.basicConfig( 16 | level=getattr(logging, config.log_level), 17 | format=config.log_format 18 | ) 19 | logger = logging.getLogger("GodotMCP") 20 | 21 | # Global connection state 22 | _godot_connection: GodotConnection = None 23 | 24 | @asynccontextmanager 25 | async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: 26 | """Handle server startup and shutdown.""" 27 | global _godot_connection 28 | logger.info("GodotMCP server starting up") 29 | try: 30 | _godot_connection = get_godot_connection() 31 | logger.info("Connected to Godot on startup") 32 | except Exception as e: 33 | logger.warning(f"Could not connect to Godot on startup: {str(e)}") 34 | _godot_connection = None 35 | try: 36 | yield {} 37 | finally: 38 | if _godot_connection: 39 | _godot_connection.disconnect() 40 | _godot_connection = None 41 | logger.info("GodotMCP server shut down") 42 | 43 | # Initialize MCP server 44 | mcp = FastMCP( 45 | "GodotMCP", 46 | description="Godot Editor integration via Model Context Protocol", 47 | lifespan=server_lifespan 48 | ) 49 | 50 | # Register all tools 51 | register_all_tools(mcp) 52 | 53 | # Editor Strategies and Best Practices Prompt 54 | @mcp.prompt() 55 | def godot_editor_strategy() -> str: 56 | """Guide for working with the Godot Engine editor through MCP.""" 57 | return ( 58 | "Godot MCP Server Tools and Best Practices:\n\n" 59 | "1. **Editor Control**\n" 60 | " - `editor_control(command)` - Performs editor-wide actions such as `PLAY`, `STOP`, `SAVE`\n" 61 | " - Commands available: PLAY, STOP, SAVE\n\n" 62 | 63 | "2. **Scene Management**\n" 64 | " - `get_scene_info()` - Get current scene details\n" 65 | " - `open_scene(scene_path)`, `save_scene()` - Open/save scenes\n" 66 | " - `new_scene(scene_path, overwrite=False)` - Create new scenes\n\n" 67 | 68 | "3. **Object Management**\n" 69 | " - ALWAYS use `find_objects_by_name(name)` to check if an object exists before creating or modifying it\n" 70 | " - `create_object(type, name=None, location=None, rotation=None, scale=None)` - Create objects (e.g. `CUBE`, `SPHERE`, `EMPTY`, `CAMERA`)\n" 71 | " - For adding children to existing objects, use: `create_child_object(parent_name, type, name=None, location=None, rotation=None, scale=None)` instead of creating and then parenting\n" 72 | " - `delete_object(name)` - Remove objects\n" 73 | " - `set_object_transform(name, location=None, rotation=None, scale=None)` - Modify object position, rotation, and scale\n" 74 | " - `get_object_properties(name)` - Get object properties\n" 75 | " - `find_objects_by_name(name)` - Find objects by name\n" 76 | " - `set_parent(child_name, parent_name, keep_global_transform=True)` - Change a node's parent while maintaining proper ownership\n\n" 77 | 78 | "4. **Property Management**\n" 79 | " - `set_property(node_name, property_name, value, force_type=None)` - Set properties on nodes\n" 80 | " - Use dot notation for component properties: `set_property(node_name, \"position:y\", 5.0)` to set just the y component\n" 81 | " - For type-specific values, use force_type parameter: `set_property(node_name, \"mass\", \"10.5\", force_type=\"float\")`\n" 82 | " - For meshes, use the dedicated mesh function: `set_mesh(node_name, mesh_type, radius=None, height=None, size=None)`\n" 83 | " - For example: `set_mesh(\"PlayerMesh\", \"CapsuleMesh\", radius=0.5, height=2.0)` or `set_mesh(\"Floor\", \"BoxMesh\", size=[10, 0.1, 10])`\n" 84 | " - For collision shapes, use: `set_collision_shape(node_name, shape_type, radius=None, height=None, size=None)`\n" 85 | " - For example: `set_collision_shape(\"Player/PlayerCollision\", \"CapsuleShape3D\", radius=0.4, height=1.8)`\n\n" 86 | 87 | " **Advanced Property Handling:**\n" 88 | " - For simple properties: `set_property(node_name, property_name, value)`\n" 89 | " - For component-wise properties: `set_property(node_name, \"position:x\", 5.0)`\n" 90 | " - For nested properties like environment settings:\n" 91 | " `set_nested_property(node_name, nested_property_path, value)`\n" 92 | " - Example: `set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material\", \"ProceduralSkyMaterial\")`\n\n" 93 | 94 | " **Environment Properties Guide:**\n" 95 | " - Basic environment properties:\n" 96 | " • `environment/background_mode` - Background mode (0-6), use integers\n" 97 | " • `environment/background_color` - Background color [r, g, b]\n" 98 | " • `environment/ambient_light_color` - Ambient light color [r, g, b]\n" 99 | " • `environment/fog_enabled` - Enable/disable fog (true/false)\n" 100 | " • `environment/fog_density` - Fog density (float)\n" 101 | " • `environment/fog_color` - Fog color [r, g, b]\n" 102 | " • `environment/glow_enabled` - Enable/disable glow (true/false)\n" 103 | " • `environment/glow_intensity` - Glow intensity (float)\n" 104 | " - Sky material properties:\n" 105 | " • `environment/sky/sky_material` - Material type (\"ProceduralSkyMaterial\", \"PanoramaSkyMaterial\")\n" 106 | " • `environment/sky/sky_material/sky_top_color` - Color at the top of the sky [r, g, b]\n" 107 | " • `environment/sky/sky_material/sky_horizon_color` - Color at the horizon [r, g, b]\n" 108 | " • `environment/sky/sky_material/ground_bottom_color` - Color of the ground [r, g, b]\n" 109 | " • `environment/sky/sky_material/ground_horizon_color` - Color of the ground at horizon [r, g, b]\n" 110 | " • `environment/sky/sky_material/sun_angle_max` - Maximum sun angle (degrees)\n" 111 | " • `environment/sky/sky_material/sky_curve` - Sky gradient curve (0.0-1.0)\n\n" 112 | " - Setting up complete environment example:\n" 113 | " ```\n" 114 | " # Create environment\n" 115 | " create_object(\"WorldEnvironment\", name=\"WorldEnvironment\")\n" 116 | " \n" 117 | " # Set sky material\n" 118 | " set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material\", \"ProceduralSkyMaterial\")\n" 119 | " \n" 120 | " # Set sky colors\n" 121 | " set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material/sky_top_color\", [0.1, 0.3, 0.8])\n" 122 | " set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material/sky_horizon_color\", [0.6, 0.7, 0.9])\n" 123 | " set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material/ground_horizon_color\", [0.5, 0.35, 0.15])\n" 124 | " set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material/ground_bottom_color\", [0.3, 0.2, 0.1])\n" 125 | " \n" 126 | " # Set ambient lighting\n" 127 | " set_nested_property(\"WorldEnvironment\", \"environment/ambient_light_color\", [0.2, 0.3, 0.4])\n" 128 | " \n" 129 | " # Enable fog\n" 130 | " set_nested_property(\"WorldEnvironment\", \"environment/fog_enabled\", true)\n" 131 | " set_nested_property(\"WorldEnvironment\", \"environment/fog_density\", 0.02)\n" 132 | " set_nested_property(\"WorldEnvironment\", \"environment/fog_color\", [0.5, 0.6, 0.7])\n" 133 | " ```\n\n" 134 | 135 | "5. **Script Management**\n" 136 | " - ALWAYS use `list_scripts(folder_path)` or `view_script(path)` to check if a script exists before creating or updating it\n" 137 | " - `create_script(script_name, script_type=\"Node\", namespace=None, script_folder=\"res://scripts\", overwrite=False, content=None)` - Create scripts\n" 138 | " - `view_script(script_path)`, `update_script(script_path, content, create_if_missing=False)` - View/modify scripts\n" 139 | " - `list_scripts(folder_path=\"res://\")` - List scripts in folder\n\n" 140 | 141 | "6. **Asset Management**\n" 142 | " - ALWAYS use `get_asset_list(type, search_pattern, folder)` to check if an asset exists before creating or importing it\n" 143 | " - `import_asset(source_path, target_path, overwrite=False)` - Import external assets\n" 144 | " - `import_3d_model(model_path, name=None, position_x=0, position_y=0, position_z=0)` - Import 3D models (GLB, FBX, OBJ) into the scene\n" 145 | " - `instantiate_prefab(prefab_path, position_x=0, position_y=0, position_z=0, rotation_x=0, rotation_y=0, rotation_z=0)` - Instantiate packed scenes (.tscn files)\n" 146 | " - `create_prefab(object_name, prefab_path, overwrite=False)` - Create packed scenes (Godot's equivalent to Unity prefabs)\n" 147 | " - `get_asset_list(type=None, search_pattern=\"*\", folder=\"res://\")` - List project assets\n" 148 | " - Use relative paths for Godot assets starting with \"res://\" (e.g., 'res://scenes/MyScene.tscn')\n" 149 | " - Use absolute paths for external files\n" 150 | " - **Important:** Use `import_3d_model` for GLB/FBX files, NOT `instantiate_prefab` which is only for .tscn files\n\n" 151 | 152 | "7. **Material Management**\n" 153 | " - ALWAYS check if a material exists before creating or modifying it\n" 154 | " - `set_material(object_name, material_name=None, color=None, create_if_missing=True)` - Apply/create materials\n" 155 | " - Use RGB or RGBA colors (0.0-1.0 range)\n\n" 156 | 157 | "8. **Common Workflows**\n" 158 | " - For hierarchical objects (like a character with collision): Use `create_object` for the parent, then `create_child_object` for the children\n" 159 | " - For setting node properties: First check the property exists with `get_object_properties`, then use `set_property`\n" 160 | " - For component-wise property changes: Use colon notation like `position:x` or `rotation:y`\n\n" 161 | 162 | "9. **Best Practices**\n" 163 | " - ALWAYS verify existence before creating or updating any objects, scripts, assets, or materials\n" 164 | " - Use `create_child_object` instead of `create_object` followed by `set_parent` for better hierarchy management\n" 165 | " - Use meaningful names for nodes and scripts\n" 166 | " - Keep scripts organized in dedicated folders\n" 167 | " - Use correct node types (Node3D, MeshInstance3D, etc.)\n" 168 | " - In Godot, Unity's prefabs are called 'packed scenes' (.tscn files)\n" 169 | " - Godot uses different terminology than Unity: GameObjects are Nodes, Components are either built-in properties or scripts\n" 170 | " - Paths in Godot start with 'res://' for project resources\n" 171 | " - Use consistent capitalization; Godot node types are CamelCase (Node3D, MeshInstance3D)\n" 172 | " - Verify transforms are applied to 3D nodes only (inheriting from Node3D)\n" 173 | " - Keep scene hierarchies clean and logical\n" 174 | " - Use Vector3 notation for position, rotation, and scale [x, y, z]\n\n" 175 | 176 | "10. **Godot MCP Best Practices**\n" 177 | " **Node Creation and Hierarchy:**\n" 178 | " - Create parent nodes first, then create children using `create_child_object`\n" 179 | " - For character controllers: Create a CharacterBody3D parent → Then add MeshInstance3D and CollisionShape3D as children\n" 180 | " - Example hierarchy pattern: `Player (CharacterBody3D) → [PlayerMesh (MeshInstance3D), PlayerCollision (CollisionShape3D)]`\n" 181 | " - Use simple, clear hierarchies - avoid deeply nested objects when possible\n" 182 | " - Put related objects (like a character and its components) under a common parent\n\n" 183 | 184 | " **Working with Properties:**\n" 185 | " - Always use specialized functions for complex objects instead of generic `set_property`:\n" 186 | " • For meshes: `set_mesh(\"PlayerMesh\", \"CapsuleMesh\", radius=0.5, height=2.0)`\n" 187 | " • For collision shapes: `set_collision_shape(\"PlayerCollision\", \"CapsuleShape3D\", radius=0.4, height=1.8)`\n" 188 | " • For materials: `set_material(\"PlayerMesh\", color=[0.8, 0.2, 0.2])`\n" 189 | " • For environment: `set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material\", \"ProceduralSkyMaterial\")`\n" 190 | " - For node paths (Parent/Child), use node references, not string concatenation\n" 191 | " - For vector components: Use `set_property(node_name, \"position:y\", 10.0)` syntax\n" 192 | " - When getting properties, use `get_object_properties` first to understand what's available\n" 193 | " - Verify property types before setting them; pass correct data types (arrays for vectors, etc.)\n\n" 194 | 195 | " **Resource Handling:**\n" 196 | " - Resource creation pattern: 1) Create parent node 2) Create child node 3) Set resource on child node\n" 197 | " - Example for character: ```\n" 198 | " create_object(\"CharacterBody3D\", name=\"Player\")\n" 199 | " create_child_object(\"Player\", \"MeshInstance3D\", name=\"PlayerMesh\")\n" 200 | " create_child_object(\"Player\", \"CollisionShape3D\", name=\"PlayerCollision\")\n" 201 | " set_mesh(\"PlayerMesh\", \"CapsuleMesh\", radius=0.5, height=2.0)\n" 202 | " set_collision_shape(\"PlayerCollision\", \"CapsuleShape3D\", radius=0.4, height=1.8)\n" 203 | " ```\n" 204 | " - For asset references, ensure they exist (use `get_asset_list`) before trying to use them\n" 205 | " - Create texture resources with proper paths before assigning to materials\n\n" 206 | 207 | "11. **AI-Generated Mesh Integration (Meshy API)**\n" 208 | " - `generate_mesh_from_text(prompt, name=None, art_style=\"realistic\", import_to_godot=True, position=None)` - Generate 3D meshes from text descriptions\n" 209 | " - `generate_mesh_from_image(image_url, name=None, import_to_godot=True, position=None)` - Generate 3D meshes from images\n" 210 | " - `refine_generated_mesh(task_id, name=None, import_to_godot=True, position=None)` - Refine previously generated meshes to higher quality\n" 211 | " - Art styles available: \"realistic\", \"cartoon\", \"low-poly\", \"sculpture\"\n" 212 | " - Requires MESHY_API_KEY environment variable to be set\n" 213 | " - Generated meshes are automatically imported to `res://assets/generated_meshes/`\n\n" 214 | " **⚠️ CRITICAL: API CREDIT USAGE WARNINGS**\n" 215 | " - **NEVER automatically refine meshes when something goes wrong!**\n" 216 | " - **Preview generation is FREE with test API key, refinement costs real credits**\n" 217 | " - **Only use `refine_generated_mesh()` when explicitly requested by the user**\n" 218 | " - **If a mesh generation fails or has issues, do NOT attempt refinement as a fix**\n" 219 | " - **Always ask the user before refining a mesh to avoid wasting API credits**\n" 220 | " - Use the test key `msy_dummy_api_key_for_test_mode_12345678` for testing without cost\n\n" 221 | " - Example usage:\n" 222 | " • `generate_mesh_from_text(\"a medieval sword with ornate handle\", name=\"MedievalSword\", art_style=\"realistic\")`\n" 223 | " • `generate_mesh_from_text(\"cute cartoon mushroom house\", art_style=\"cartoon\", position=[5, 0, 3])`\n" 224 | " • `generate_mesh_from_image(\"https://example.com/chair.jpg\", name=\"GeneratedChair\")`\n\n" 225 | " - **Workflow for AI-Generated Content:**\n" 226 | " 1. Start with preview generation (faster, lower quality, often free)\n" 227 | " 2. **ONLY IF the user is satisfied AND explicitly requests it**: use `refine_generated_mesh()` for higher quality\n" 228 | " 3. **Never refine as an error recovery mechanism - this wastes API credits**\n" 229 | " 4. Meshes are automatically imported as MeshInstance3D objects\n" 230 | " 5. Generated meshes support PBR materials when available\n\n" 231 | 232 | " **Scene Management:**\n" 233 | " - Save current scene before opening a different one\n" 234 | " - Use `get_scene_info` at the start of major operations to understand scene structure\n" 235 | " - Create prefabs (packed scenes) for reusable objects\n" 236 | " - Prefer scene instancing over duplicating node creation code\n\n" 237 | 238 | " **Script Handling:**\n" 239 | " - Create scripts in dedicated folders grouped by functionality\n" 240 | " - Attach scripts to the highest logical node in a functional group\n" 241 | " - Use `view_script` to understand existing scripts before modifying\n" 242 | " - Follow Godot naming conventions: lowercase with underscores for filenames\n\n" 243 | 244 | " **Common Pitfalls to Avoid:**\n" 245 | " - DON'T set mesh/shape resources directly with `set_property` - use the specialized functions\n" 246 | " - DON'T use forward slashes in node names (they're interpreted as paths)\n" 247 | " - DON'T create nodes without properly setting their owners (use `create_child_object`)\n" 248 | " - DON'T access paths like 'Parent/Child' with string operations - use proper node paths\n" 249 | " - DON'T try to set read-only properties; check documentation if uncertain\n\n" 250 | 251 | " **Type Conversion Guide:**\n" 252 | " - Boolean: `true`/`false` or `1`/`0`\n" 253 | " - Vector3: `[x, y, z]` as array of floats\n" 254 | " - Vector2: `[x, y]` as array of floats\n" 255 | " - Color: `[r, g, b]` or `[r, g, b, a]` as array of floats (0.0-1.0)\n" 256 | " - Transform3D: Use individual position, rotation, scale properties instead\n\n" 257 | 258 | " **Workflow Examples:**\n" 259 | " - Creating a simple platform: ```\n" 260 | " create_object(\"StaticBody3D\", name=\"Platform\", location=[0, -1, 0], scale=[10, 0.5, 10])\n" 261 | " create_child_object(\"Platform\", \"MeshInstance3D\", name=\"PlatformMesh\")\n" 262 | " create_child_object(\"Platform\", \"CollisionShape3D\", name=\"PlatformCollision\")\n" 263 | " set_mesh(\"PlatformMesh\", \"BoxMesh\", size=[10, 0.5, 10])\n" 264 | " set_collision_shape(\"PlatformCollision\", \"BoxShape3D\", size=[10, 0.5, 10])\n" 265 | " ```\n" 266 | " - Creating a light: ```\n" 267 | " create_object(\"DirectionalLight3D\", name=\"Sun\", location=[10, 15, 5])\n" 268 | " set_property(\"Sun\", \"light_energy\", 1.5)\n" 269 | " set_property(\"Sun\", \"shadow_enabled\", true)\n" 270 | " ```\n" 271 | " - Creating a camera: ```\n" 272 | " create_object(\"Camera3D\", name=\"MainCamera\", location=[0, 5, 10])\n" 273 | " set_property(\"MainCamera\", \"rotation:x\", -30.0 * 0.0174533) # Convert degrees to radians\n" 274 | " set_property(\"MainCamera\", \"current\", true)\n" 275 | " ```\n" 276 | " - Setting up environment: ```\n" 277 | " create_object(\"WorldEnvironment\", name=\"WorldEnvironment\")\n" 278 | " set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material\", \"ProceduralSkyMaterial\")\n" 279 | " set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material/sky_top_color\", [0.1, 0.3, 0.8])\n" 280 | " set_nested_property(\"WorldEnvironment\", \"environment/sky/sky_material/sky_horizon_color\", [0.6, 0.7, 0.9])\n" 281 | " set_nested_property(\"WorldEnvironment\", \"environment/ambient_light_color\", [0.2, 0.3, 0.4])\n" 282 | " set_nested_property(\"WorldEnvironment\", \"environment/fog_enabled\", true)\n" 283 | " ```\n\n" 284 | 285 | " **Debugging Tips:**\n" 286 | " - If operations fail, check if the node exists with `find_objects_by_name`\n" 287 | " - For complex operations, break them down into individual commands\n" 288 | " - After creating nodes, verify their properties with `get_object_properties`\n" 289 | " - When a command fails, try similar operations with simpler parameters first\n" 290 | " - Use `show_message` to display debugging information in the Godot editor\n" 291 | ) 292 | # Run the server 293 | if __name__ == "__main__": 294 | mcp.run(transport='stdio') -------------------------------------------------------------------------------- /python/tools/meshy_tools.py: -------------------------------------------------------------------------------- 1 | # tools/meshy_tools.py 2 | from mcp.server.fastmcp import FastMCP, Context 3 | from typing import Optional, Dict, Any 4 | import requests 5 | import json 6 | import time 7 | import os 8 | import urllib.request 9 | import logging 10 | from config import config 11 | from godot_connection import get_godot_connection 12 | 13 | logger = logging.getLogger("GodotMCP") 14 | 15 | def register_meshy_tools(mcp: FastMCP): 16 | """Register Meshy API tools with the MCP server.""" 17 | 18 | @mcp.tool() 19 | def generate_mesh_from_text( 20 | ctx: Context, 21 | prompt: str, 22 | name: str = None, 23 | art_style: str = "realistic", 24 | negative_prompt: str = "", 25 | should_remesh: bool = True, 26 | import_to_godot: bool = True, 27 | position: list = None 28 | ) -> str: 29 | """Generate a 3D mesh from text description using Meshy API and optionally import it into Godot. 30 | 31 | ⚠️ IMPORTANT: This generates PREVIEW quality meshes (lower quality, faster, often free). 32 | ⚠️ DO NOT automatically refine unless explicitly requested by the user! 33 | ⚠️ Refinement costs real API credits and should only be used when the user is satisfied with the preview. 34 | 35 | Uses the official Meshy API v2 text-to-3d endpoint to generate 3D models. 36 | This creates a preview mesh first, which can later be refined for higher quality using refine_generated_mesh(). 37 | 38 | For testing without consuming credits, you can set MESHY_API_KEY to: msy_dummy_api_key_for_test_mode_12345678 39 | 40 | Args: 41 | ctx: The MCP context 42 | prompt: Text description of the 3D model to generate (e.g., "a medieval sword with ornate handle") 43 | name: Optional name for the generated mesh object in Godot 44 | art_style: Art style for generation ("realistic", "cartoon", "low-poly", "sculpture") 45 | negative_prompt: What to avoid in the generation (e.g., "low quality, low resolution, low poly, ugly") 46 | should_remesh: Whether to apply remeshing for better topology (recommended: True) 47 | import_to_godot: Whether to automatically import the mesh into Godot scene 48 | position: Optional [x, y, z] position to place the object 49 | 50 | Returns: 51 | str: Success message with details or error information 52 | """ 53 | try: 54 | if not config.meshy_api_key: 55 | return "Error: MESHY_API_KEY environment variable not set. Please set your Meshy API key or use the test key: msy_dummy_api_key_for_test_mode_12345678" 56 | 57 | # Step 1: Create text-to-3D task 58 | logger.info(f"Starting mesh generation for prompt: {prompt}") 59 | 60 | headers = { 61 | "Authorization": f"Bearer {config.meshy_api_key}", 62 | "Content-Type": "application/json" 63 | } 64 | 65 | # Prepare the generation request - Matches official Meshy API v2 format exactly 66 | generation_data = { 67 | "mode": "preview", # Start with preview mode 68 | "prompt": prompt, 69 | "art_style": art_style, 70 | "should_remesh": should_remesh 71 | } 72 | 73 | # Add negative_prompt only if provided (optional parameter) 74 | if negative_prompt.strip(): 75 | generation_data["negative_prompt"] = negative_prompt 76 | 77 | # Create the generation task - Official v2 endpoint 78 | response = requests.post( 79 | f"{config.meshy_base_url}/v2/text-to-3d", 80 | headers=headers, 81 | json=generation_data, 82 | timeout=30 83 | ) 84 | 85 | # Handle response according to official documentation 86 | response.raise_for_status() 87 | 88 | task_data = response.json() 89 | task_id = task_data.get("result") 90 | 91 | if not task_id: 92 | return f"Error: No task ID returned from Meshy API. Response: {task_data}" 93 | 94 | logger.info(f"Preview task created. Task ID: {task_id}") 95 | 96 | # Step 2: Poll for completion - matches official polling pattern 97 | logger.info("Waiting for mesh generation to complete...") 98 | 99 | max_wait_time = config.meshy_timeout 100 | check_interval = 5 # Check every 5 seconds (matches documentation example) 101 | elapsed_time = 0 102 | 103 | while elapsed_time < max_wait_time: 104 | # Check task status - Official v2 endpoint 105 | status_response = requests.get( 106 | f"{config.meshy_base_url}/v2/text-to-3d/{task_id}", 107 | headers=headers, 108 | timeout=30 109 | ) 110 | 111 | status_response.raise_for_status() 112 | 113 | status_data = status_response.json() 114 | status = status_data.get("status") 115 | progress = status_data.get("progress", 0) 116 | 117 | logger.info(f"Preview task status: {status} | Progress: {progress}%") 118 | 119 | if status == "SUCCEEDED": 120 | logger.info("Preview task finished.") 121 | 122 | # Generation completed successfully 123 | model_urls = status_data.get("model_urls", {}) 124 | 125 | if not model_urls: 126 | return "Error: No model URLs in completed task" 127 | 128 | # Prefer GLB format for Godot (matches documentation example) 129 | download_url = model_urls.get("glb") or model_urls.get("fbx") or model_urls.get("obj") 130 | 131 | if not download_url: 132 | return f"Error: No supported model format found. Available formats: {list(model_urls.keys())}" 133 | 134 | logger.info(f"Preview model completed! Download URL: {download_url}") 135 | 136 | # Store task_id for potential refinement 137 | result_message = f"Preview mesh generated successfully! Task ID: {task_id}\n" 138 | result_message += f"Download URL: {download_url}\n" 139 | result_message += f"Use refine_generated_mesh('{task_id}') to create a high-quality textured version." 140 | 141 | # Step 3: Download the mesh file 142 | if import_to_godot: 143 | download_result = _download_mesh_to_project(download_url, name) 144 | if "Error" in download_result: 145 | return f"{result_message}\n\n{download_result}" 146 | else: 147 | return f"{result_message}\n\n{download_result}" 148 | else: 149 | return result_message 150 | 151 | elif status == "FAILED": 152 | error_msg = status_data.get("task_error", {}).get("message", "Unknown error") 153 | return f"Mesh generation failed: {error_msg}" 154 | 155 | elif status in ["PENDING", "IN_PROGRESS"]: 156 | # Still processing, wait and check again (matches documentation pattern) 157 | logger.info(f"Preview task status: {status} | Progress: {progress} | Retrying in {check_interval} seconds...") 158 | time.sleep(check_interval) 159 | elapsed_time += check_interval 160 | else: 161 | return f"Unknown task status: {status}" 162 | 163 | return f"Mesh generation timeout after {max_wait_time} seconds" 164 | 165 | except requests.exceptions.RequestException as e: 166 | return f"Network error communicating with Meshy API: {str(e)}" 167 | except Exception as e: 168 | return f"Error generating mesh: {str(e)}" 169 | 170 | @mcp.tool() 171 | def generate_mesh_from_image( 172 | ctx: Context, 173 | image_url: str, 174 | name: str = None, 175 | import_to_godot: bool = True, 176 | position: list = None 177 | ) -> str: 178 | """Generate a 3D mesh from an image using Meshy API and optionally import it into Godot. 179 | 180 | ⚠️ IMPORTANT: This generates standard quality meshes and may consume API credits. 181 | ⚠️ Do NOT automatically refine the result unless explicitly requested by the user! 182 | 183 | Args: 184 | ctx: The MCP context 185 | image_url: URL or path to the image to convert to 3D 186 | name: Optional name for the generated mesh object in Godot 187 | import_to_godot: Whether to automatically import the mesh into Godot scene 188 | position: Optional [x, y, z] position to place the object 189 | 190 | Returns: 191 | str: Success message with details or error information 192 | """ 193 | try: 194 | if not config.meshy_api_key: 195 | return "Error: MESHY_API_KEY environment variable not set. Please set your Meshy API key." 196 | 197 | logger.info(f"Starting mesh generation from image: {image_url}") 198 | 199 | headers = { 200 | "Authorization": f"Bearer {config.meshy_api_key}", 201 | "Content-Type": "application/json" 202 | } 203 | 204 | # Prepare the generation request 205 | generation_data = { 206 | "image_url": image_url, 207 | "enable_pbr": True # Enable PBR materials 208 | } 209 | 210 | # Create the generation task - Updated endpoint 211 | response = requests.post( 212 | f"{config.meshy_base_url}/v2/image-to-3d", 213 | headers=headers, 214 | json=generation_data, 215 | timeout=30 216 | ) 217 | 218 | if response.status_code not in [200, 202]: 219 | return f"Error creating image-to-3D task: {response.status_code} - {response.text}" 220 | 221 | task_data = response.json() 222 | task_id = task_data.get("result") 223 | 224 | if not task_id: 225 | return f"Error: No task ID returned from Meshy API. Response: {task_data}" 226 | 227 | logger.info(f"Image-to-3D task created with ID: {task_id}") 228 | 229 | # Poll for completion (similar to text-to-3D) 230 | max_wait_time = config.meshy_timeout 231 | check_interval = 15 # Image-to-3D might take longer 232 | elapsed_time = 0 233 | 234 | while elapsed_time < max_wait_time: 235 | status_response = requests.get( 236 | f"{config.meshy_base_url}/v2/image-to-3d/{task_id}", 237 | headers=headers, 238 | timeout=30 239 | ) 240 | 241 | if status_response.status_code != 200: 242 | return f"Error checking task status: {status_response.status_code} - {status_response.text}" 243 | 244 | status_data = status_response.json() 245 | status = status_data.get("status") 246 | 247 | logger.info(f"Task status: {status}") 248 | 249 | if status == "SUCCEEDED": 250 | model_urls = status_data.get("model_urls", {}) 251 | 252 | if not model_urls: 253 | return "Error: No model URLs in completed task" 254 | 255 | download_url = model_urls.get("glb") or model_urls.get("fbx") or model_urls.get("obj") 256 | 257 | if not download_url: 258 | return f"Error: No supported model format found. Available formats: {list(model_urls.keys())}" 259 | 260 | logger.info(f"Image-to-3D generation completed! Download URL: {download_url}") 261 | 262 | result_message = f"Mesh generated successfully from image! Download URL: {download_url}" 263 | 264 | if import_to_godot: 265 | download_result = _download_mesh_to_project(download_url, name) 266 | return f"{result_message}\n\n{download_result}" 267 | else: 268 | return result_message 269 | 270 | elif status == "FAILED": 271 | error_msg = status_data.get("task_error", {}).get("message", "Unknown error") 272 | return f"Image-to-3D generation failed: {error_msg}" 273 | 274 | elif status in ["PENDING", "IN_PROGRESS"]: 275 | time.sleep(check_interval) 276 | elapsed_time += check_interval 277 | else: 278 | return f"Unknown task status: {status}" 279 | 280 | return f"Image-to-3D generation timeout after {max_wait_time} seconds" 281 | 282 | except Exception as e: 283 | return f"Error generating mesh from image: {str(e)}" 284 | 285 | @mcp.tool() 286 | def check_mesh_generation_progress( 287 | ctx: Context, 288 | task_id: str 289 | ) -> str: 290 | """Check the progress of a mesh generation task using its task ID. 291 | 292 | Args: 293 | ctx: The MCP context 294 | task_id: The task ID from a previous mesh generation request 295 | 296 | Returns: 297 | str: Current status and progress information 298 | """ 299 | try: 300 | if not config.meshy_api_key: 301 | return "Error: MESHY_API_KEY environment variable not set. Please set your Meshy API key." 302 | 303 | headers = { 304 | "Authorization": f"Bearer {config.meshy_api_key}", 305 | "Content-Type": "application/json" 306 | } 307 | 308 | # Check task status 309 | status_response = requests.get( 310 | f"{config.meshy_base_url}/v2/text-to-3d/{task_id}", 311 | headers=headers, 312 | timeout=30 313 | ) 314 | 315 | if status_response.status_code != 200: 316 | return f"Error checking task status: {status_response.status_code} - {status_response.text}" 317 | 318 | status_data = status_response.json() 319 | status = status_data.get("status") 320 | 321 | if status == "SUCCEEDED": 322 | model_urls = status_data.get("model_urls", {}) 323 | available_formats = list(model_urls.keys()) 324 | 325 | return f"✅ Mesh generation completed successfully!\n" \ 326 | f"Task ID: {task_id}\n" \ 327 | f"Available formats: {', '.join(available_formats)}\n" \ 328 | f"Use refine_generated_mesh() if you want higher quality, or the mesh is ready for download." 329 | 330 | elif status == "FAILED": 331 | error_msg = status_data.get("task_error", {}).get("message", "Unknown error") 332 | return f"❌ Mesh generation failed for task {task_id}\n" \ 333 | f"Error: {error_msg}" 334 | 335 | elif status == "PENDING": 336 | return f"⏳ Mesh generation is queued for processing\n" \ 337 | f"Task ID: {task_id}\n" \ 338 | f"Status: Waiting to start..." 339 | 340 | elif status == "IN_PROGRESS": 341 | # Try to get progress percentage if available 342 | progress = status_data.get("progress", 0) 343 | return f"🔄 Mesh generation in progress\n" \ 344 | f"Task ID: {task_id}\n" \ 345 | f"Progress: {progress}%\n" \ 346 | f"Estimated time remaining: 2-5 minutes" 347 | 348 | else: 349 | return f"❓ Unknown status for task {task_id}: {status}\n" \ 350 | f"Full response: {status_data}" 351 | 352 | except requests.exceptions.RequestException as e: 353 | return f"Network error checking task progress: {str(e)}" 354 | except Exception as e: 355 | return f"Error checking mesh generation progress: {str(e)}" 356 | 357 | @mcp.tool() 358 | def refine_generated_mesh( 359 | ctx: Context, 360 | task_id: str, 361 | name: str = None, 362 | import_to_godot: bool = True, 363 | position: list = None 364 | ) -> str: 365 | """Refine a previously generated mesh to higher quality using Meshy API. 366 | 367 | 🚨 WARNING: This function consumes SIGNIFICANT API credits! 🚨 368 | ⚠️ Only use when the user explicitly requests mesh refinement 369 | ⚠️ Takes 10-20 minutes to complete and costs real money 370 | ⚠️ Do NOT use as an error recovery mechanism 371 | ⚠️ Always ask user permission before calling this function 372 | 373 | Args: 374 | ctx: The MCP context 375 | task_id: The task ID from a previous generation 376 | name: Optional name for the refined mesh object in Godot 377 | import_to_godot: Whether to automatically import the mesh into Godot scene 378 | position: Optional [x, y, z] position to place the object 379 | 380 | Returns: 381 | str: Success message with details or error information 382 | """ 383 | try: 384 | if not config.meshy_api_key: 385 | return "Error: MESHY_API_KEY environment variable not set. Please set your Meshy API key." 386 | 387 | logger.info(f"Starting mesh refinement for task: {task_id}") 388 | 389 | headers = { 390 | "Authorization": f"Bearer {config.meshy_api_key}", 391 | "Content-Type": "application/json" 392 | } 393 | 394 | # Create refinement task 395 | refinement_data = { 396 | "mode": "refine", 397 | "preview_task_id": task_id 398 | } 399 | 400 | response = requests.post( 401 | f"{config.meshy_base_url}/v2/text-to-3d", 402 | headers=headers, 403 | json=refinement_data, 404 | timeout=30 405 | ) 406 | 407 | if response.status_code not in [200, 202]: 408 | return f"Error creating refinement task: {response.status_code} - {response.text}" 409 | 410 | task_data = response.json() 411 | refine_task_id = task_data.get("result") 412 | 413 | logger.info(f"Refinement task created with ID: {refine_task_id}") 414 | 415 | # Poll for completion (refinement takes longer) 416 | max_wait_time = config.meshy_timeout * 2 # Double timeout for refinement 417 | check_interval = 20 418 | elapsed_time = 0 419 | 420 | while elapsed_time < max_wait_time: 421 | status_response = requests.get( 422 | f"{config.meshy_base_url}/v2/text-to-3d/{refine_task_id}", 423 | headers=headers, 424 | timeout=30 425 | ) 426 | 427 | if status_response.status_code != 200: 428 | return f"Error checking refinement status: {status_response.status_code} - {status_response.text}" 429 | 430 | status_data = status_response.json() 431 | status = status_data.get("status") 432 | 433 | logger.info(f"Refinement status: {status}") 434 | 435 | if status == "SUCCEEDED": 436 | model_urls = status_data.get("model_urls", {}) 437 | download_url = model_urls.get("glb") or model_urls.get("fbx") or model_urls.get("obj") 438 | 439 | if not download_url: 440 | return f"No supported format in refined mesh. Available: {list(model_urls.keys())}" 441 | 442 | logger.info(f"Mesh refinement completed! Download URL: {download_url}") 443 | 444 | result_message = f"Mesh refined successfully! Download URL: {download_url}" 445 | 446 | if import_to_godot: 447 | download_result = _download_mesh_to_project(download_url, name) 448 | return f"{result_message}\n\n{download_result}" 449 | else: 450 | result = f"Mesh refined successfully! Download URL: {download_url}\n\n" 451 | result += f"**Refinement Task ID:** {refine_task_id}\n" 452 | result += f"**Note:** The refined mesh was NOT imported to Godot.\n\n" 453 | result += f"To import it, use one of these options:\n" 454 | result += f"1. Run: `import_asset` with source URL and target path\n" 455 | result += f"2. Download manually and import\n" 456 | result += f"3. Re-run refinement with `import_to_godot: true`" 457 | return result 458 | 459 | elif status == "FAILED": 460 | error_msg = status_data.get("task_error", {}).get("message", "Unknown error") 461 | return f"Mesh refinement failed: {error_msg}" 462 | 463 | elif status in ["PENDING", "IN_PROGRESS"]: 464 | time.sleep(check_interval) 465 | elapsed_time += check_interval 466 | else: 467 | return f"Unknown refinement status: {status}" 468 | 469 | return f"Mesh refinement timeout after {max_wait_time} seconds" 470 | 471 | except Exception as e: 472 | return f"Error refining mesh: {str(e)}" 473 | 474 | @mcp.tool() 475 | def download_and_import_mesh( 476 | ctx: Context, 477 | download_url: str, 478 | name: str, 479 | position: list = None 480 | ) -> str: 481 | """Download a mesh from a URL (e.g., from Meshy API) and import it into Godot. 482 | 483 | Use this when you have a mesh URL but haven't imported it to Godot yet. 484 | 485 | Args: 486 | ctx: The MCP context 487 | download_url: The URL to download the mesh from 488 | name: Name for the mesh in Godot 489 | position: Optional [x, y, z] position to place the object 490 | 491 | Returns: 492 | str: Success message or error information 493 | """ 494 | try: 495 | logger.info(f"Downloading mesh from URL: {name}") 496 | # First download the mesh 497 | download_result = _download_mesh_to_project(download_url, name) 498 | 499 | if "Error" in download_result: 500 | return download_result 501 | 502 | # Extract the file path from the download result 503 | import re 504 | match = re.search(r'`(res://[^`]+)`', download_result) 505 | if match: 506 | file_path = match.group(1) 507 | # Now import it using import_3d_model 508 | from .asset_tools import import_3d_model 509 | import_result = import_3d_model( 510 | ctx=ctx, 511 | model_path=file_path, 512 | name=name, 513 | position_x=position[0] if position else 0, 514 | position_y=position[1] if position else 0, 515 | position_z=position[2] if position else 0 516 | ) 517 | return f"{download_result}\n\n{import_result}" 518 | else: 519 | return f"{download_result}\n\nNote: Could not automatically import. Use import_3d_model manually." 520 | except Exception as e: 521 | return f"Error downloading and importing mesh: {str(e)}" 522 | 523 | def _download_mesh_to_project(download_url: str, name: str = None) -> str: 524 | """Helper function to download a mesh file to the Godot project.""" 525 | try: 526 | # Generate a filename 527 | if not name: 528 | name = f"GeneratedMesh_{int(time.time())}" 529 | 530 | # Clean the name for filename use 531 | safe_name = "".join(c for c in name if c.isalnum() or c in (' ', '-', '_')).rstrip() 532 | safe_name = safe_name.replace(' ', '_') 533 | 534 | # Determine file extension from URL 535 | url_lower = download_url.lower() 536 | if '.glb' in url_lower: 537 | extension = '.glb' 538 | elif '.fbx' in url_lower: 539 | extension = '.fbx' 540 | elif '.obj' in url_lower: 541 | extension = '.obj' 542 | else: 543 | extension = '.glb' # Default 544 | 545 | filename = f"{safe_name}{extension}" 546 | local_path = f"/tmp/{filename}" 547 | 548 | logger.info(f"Downloading mesh to: {local_path}") 549 | 550 | # Download the file 551 | urllib.request.urlretrieve(download_url, local_path) 552 | 553 | # Import to Godot 554 | target_path = f"{config.asset_import_path}{filename}" 555 | 556 | godot = get_godot_connection() 557 | 558 | # First, ensure the target directory exists by creating a dummy file 559 | # This is necessary because Godot needs the directory to exist before importing 560 | logger.info(f"Ensuring directory exists: {config.asset_import_path}") 561 | 562 | # Check if the asset directory exists, create it if not 563 | check_dir_result = godot.send_command("GET_ASSET_LIST", { 564 | "folder": config.asset_import_path 565 | }) 566 | 567 | if "error" in check_dir_result and "Unable to access directory" in check_dir_result.get("error", ""): 568 | # Directory doesn't exist, we need to create it 569 | # Create a dummy file to force directory creation 570 | dummy_path = f"{config.asset_import_path}.gdignore" 571 | dummy_result = godot.send_command("CREATE_SCRIPT", { 572 | "script_name": ".gdignore", 573 | "script_folder": config.asset_import_path.rstrip("/"), 574 | "content": "# This file tells Godot to ignore this directory for scanning", 575 | "overwrite": True 576 | }) 577 | logger.info(f"Created directory with .gdignore: {dummy_result}") 578 | 579 | # Now import the asset 580 | import_result = godot.send_command("IMPORT_ASSET", { 581 | "source_path": local_path, 582 | "target_path": target_path, 583 | "overwrite": True 584 | }) 585 | 586 | if "error" in import_result: 587 | return f"Error: Failed to copy file to Godot project: {import_result['error']}" 588 | 589 | # Clean up temporary file 590 | try: 591 | os.remove(local_path) 592 | except: 593 | pass 594 | 595 | success_msg = f"✅ **Mesh Downloaded Successfully!**\n\n" 596 | success_msg += f"**File:** `{target_path}`\n" 597 | success_msg += f"**Name:** {name}\n\n" 598 | success_msg += f"The mesh has been downloaded to your project.\n" 599 | success_msg += f"Use `import_3d_model` to add it to your scene." 600 | 601 | return success_msg 602 | 603 | except Exception as e: 604 | return f"Error importing mesh to Godot: {str(e)}" --------------------------------------------------------------------------------