├── 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 |
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)}"
--------------------------------------------------------------------------------