diff --git a/HOW_TO_ADD_A_TOOL.md b/HOW_TO_ADD_A_TOOL.md index 490c139..8426730 100644 --- a/HOW_TO_ADD_A_TOOL.md +++ b/HOW_TO_ADD_A_TOOL.md @@ -108,31 +108,38 @@ def asset_creation_strategy() -> str: ## Best Practices -1. **Error Handling**: +1. **Existence Checking**: + + - ALWAYS check if objects, scripts, assets, or materials exist before creating or updating them + - Use appropriate search tools (`find_objects_by_name`, `list_scripts`, `get_asset_list`) to verify existence + - Handle both cases: creation when it doesn't exist and updating when it does + - Implement proper error handling when an expected resource is not found + +2. **Error Handling**: - Always include try-catch blocks in Python tools - Validate parameters in C# handlers - Return meaningful error messages -2. **Documentation**: +3. **Documentation**: - Add XML documentation to C# handlers - Include detailed docstrings in Python tools - Update the prompt with clear usage instructions -3. **Parameter Validation**: +4. **Parameter Validation**: - Validate parameters on both Python and C# sides - Use appropriate types (str, int, float, List, etc.) - Provide default values when appropriate -4. **Testing**: +5. **Testing**: - Test the tool in both Unity Editor and Python environments - Verify error handling works as expected - Check that the tool integrates well with existing functionality -5. **Code Organization**: +6. **Code Organization**: - Group related tools in appropriate handler classes - Keep tools focused and single-purpose - Follow existing naming conventions @@ -150,14 +157,29 @@ public static class ExampleHandler { string prefabName = (string)@params["prefab_name"]; string template = (string)@params["template"]; + bool overwrite = @params["overwrite"] != null ? (bool)@params["overwrite"] : false; + + // Check if the prefab already exists + string prefabPath = $"Assets/Prefabs/{prefabName}.prefab"; + bool prefabExists = System.IO.File.Exists(prefabPath); + + if (prefabExists && !overwrite) + { + return new { + message = $"Prefab already exists: {prefabName}. Use overwrite=true to replace it.", + exists = true, + path = prefabPath + }; + } // Implementation GameObject prefab = new GameObject(prefabName); // ... setup prefab ... return new { - message = $"Created prefab: {prefabName}", - path = $"Assets/Prefabs/{prefabName}.prefab" + message = prefabExists ? $"Updated prefab: {prefabName}" : $"Created prefab: {prefabName}", + exists = prefabExists, + path = prefabPath }; } } @@ -170,33 +192,51 @@ public static class ExampleHandler def create_prefab( ctx: Context, prefab_name: str, - template: str = "default" + template: str = "default", + overwrite: bool = False ) -> str: - """Create a new prefab in the project. + """Create a new prefab in the project or update if it exists. Args: ctx: The MCP context prefab_name: Name for the new prefab template: Template to use (default: "default") + overwrite: Whether to overwrite an existing prefab (default: False) Returns: str: Success message or error details """ try: + # First check if the prefab already exists + assets = get_unity_connection().send_command("GET_ASSET_LIST", { + "type": "Prefab", + "search_pattern": prefab_name, + "folder": "Assets/Prefabs" + }).get("assets", []) + + prefab_exists = any(asset.get("name") == prefab_name for asset in assets) + + if prefab_exists and not overwrite: + return f"Prefab '{prefab_name}' already exists. Use overwrite=True to replace it." + + # Create or update the prefab response = get_unity_connection().send_command("CREATE_PREFAB", { "prefab_name": prefab_name, - "template": template + "template": template, + "overwrite": overwrite }) - return response.get("message", "Prefab created successfully") + + return response.get("message", "Prefab operation completed successfully") except Exception as e: - return f"Error creating prefab: {str(e)}" + return f"Error with prefab operation: {str(e)}" ``` 3. **Update Prompt**: ```python "1. **Prefab Management**:\n" -" - Create prefabs with `create_prefab(prefab_name, template)`\n" +" - ALWAYS check if a prefab exists before creating it\n" +" - Create or update prefabs with `create_prefab(prefab_name, template, overwrite=False)`\n" ``` ## Troubleshooting diff --git a/Python/server.py b/Python/server.py index 2ecd276..36e551d 100644 --- a/Python/server.py +++ b/Python/server.py @@ -56,6 +56,7 @@ def asset_creation_strategy() -> str: " - `open_scene(path)`, `save_scene(path)` - Open/save scenes\n" " - `new_scene(path)`, `change_scene(path, save_current)` - Create/switch scenes\n\n" "3. **Object Management**\n" + " - ALWAYS use `find_objects_by_name(name)` to check if an object exists before creating or modifying it\n" " - `create_object(name, type)` - Create objects (e.g. `CUBE`, `SPHERE`, `EMPTY`, `CAMERA`)\n" " - `delete_object(name)` - Remove objects\n" " - `set_object_transform(name, location, rotation, scale)` - Modify object position, rotation, and scale\n" @@ -65,11 +66,13 @@ def asset_creation_strategy() -> str: " - `find_objects_by_name(name)` - Find objects by name\n" " - `get_hierarchy()` - Get object hierarchy\n" "4. **Script Management**\n" + " - ALWAYS use `list_scripts(folder_path)` or `view_script(path)` to check if a script exists before creating or updating it\n" " - `create_script(name, type, namespace, template)` - Create scripts\n" " - `view_script(path)`, `update_script(path, content)` - View/modify scripts\n" " - `attach_script(object_name, script_name)` - Add scripts to objects\n" " - `list_scripts(folder_path)` - List scripts in folder\n\n" "5. **Asset Management**\n" + " - ALWAYS use `get_asset_list(type, search_pattern, folder)` to check if an asset exists before creating or importing it\n" " - `import_asset(source_path, target_path)` - Import external assets\n" " - `instantiate_prefab(path, pos_x, pos_y, pos_z, rot_x, rot_y, rot_z)` - Create prefab instances\n" " - `create_prefab(object_name, path)`, `apply_prefab(object_name, path)` - Manage prefabs\n" @@ -77,9 +80,11 @@ def asset_creation_strategy() -> str: " - Use relative paths for Unity assets (e.g., 'Assets/Models/MyModel.fbx')\n" " - Use absolute paths for external files\n\n" "6. **Material Management**\n" + " - ALWAYS check if a material exists before creating or modifying it\n" " - `set_material(object_name, material_name, color)` - Apply/create materials\n" " - Use RGB colors (0.0-1.0 range)\n\n" "7. **Best Practices**\n" + " - ALWAYS verify existence before creating or updating any objects, scripts, assets, or materials\n" " - Use meaningful names for objects and scripts\n" " - Keep scripts organized in folders with namespaces\n" " - Verify changes after modifications\n" diff --git a/Python/tools/asset_tools.py b/Python/tools/asset_tools.py index cb72c4e..1a5bdbf 100644 --- a/Python/tools/asset_tools.py +++ b/Python/tools/asset_tools.py @@ -9,7 +9,8 @@ def register_asset_tools(mcp: FastMCP): def import_asset( ctx: Context, source_path: str, - target_path: str + target_path: str, + overwrite: bool = False ) -> str: """Import an asset (e.g., 3D model, texture) into the Unity project. @@ -17,21 +18,45 @@ def register_asset_tools(mcp: FastMCP): ctx: The MCP context source_path: Path to the source file on disk target_path: Path where the asset should be imported in the Unity project (relative to Assets folder) + overwrite: Whether to overwrite if an asset already exists at the target path (default: False) Returns: str: Success message or error details """ try: + unity = get_unity_connection() + # Parameter validation if not source_path or not isinstance(source_path, str): return f"Error importing asset: source_path must be a valid string" if not target_path or not isinstance(target_path, str): return f"Error importing asset: target_path must be a valid string" + + # Check if the source file exists (on local disk) + import os + if not os.path.exists(source_path): + return f"Error importing asset: Source file '{source_path}' does not exist" + + # Extract the target directory and filename + target_dir = '/'.join(target_path.split('/')[:-1]) + target_filename = target_path.split('/')[-1] + + # Check if an asset already exists at the target path + existing_assets = unity.send_command("GET_ASSET_LIST", { + "search_pattern": target_filename, + "folder": target_dir or "Assets" + }).get("assets", []) + + # Check if any asset matches the exact path + asset_exists = any(asset.get("path") == target_path for asset in existing_assets) + if asset_exists and not overwrite: + return f"Asset already exists at '{target_path}'. Use overwrite=True to replace it." - response = get_unity_connection().send_command("IMPORT_ASSET", { + response = unity.send_command("IMPORT_ASSET", { "source_path": source_path, - "target_path": target_path + "target_path": target_path, + "overwrite": overwrite }) if not response.get("success", False): @@ -68,6 +93,8 @@ def register_asset_tools(mcp: FastMCP): str: Success message or error details """ try: + unity = get_unity_connection() + # Parameter validation if not prefab_path or not isinstance(prefab_path, str): return f"Error instantiating prefab: prefab_path must be a valid string" @@ -86,7 +113,26 @@ def register_asset_tools(mcp: FastMCP): if not isinstance(param_value, (int, float)): return f"Error instantiating prefab: {param_name} must be a number" - response = get_unity_connection().send_command("INSTANTIATE_PREFAB", { + # Check if the prefab exists + prefab_dir = '/'.join(prefab_path.split('/')[:-1]) or "Assets" + prefab_name = prefab_path.split('/')[-1] + + # Ensure prefab has .prefab extension for searching + if not prefab_name.lower().endswith('.prefab'): + prefab_name = f"{prefab_name}.prefab" + prefab_path = f"{prefab_path}.prefab" + + prefab_assets = unity.send_command("GET_ASSET_LIST", { + "type": "Prefab", + "search_pattern": prefab_name, + "folder": prefab_dir + }).get("assets", []) + + prefab_exists = any(asset.get("path") == prefab_path for asset in prefab_assets) + if not prefab_exists: + return f"Prefab '{prefab_path}' not found in the project." + + response = unity.send_command("INSTANTIATE_PREFAB", { "prefab_path": prefab_path, "position_x": position_x, "position_y": position_y, @@ -107,7 +153,8 @@ def register_asset_tools(mcp: FastMCP): def create_prefab( ctx: Context, object_name: str, - prefab_path: str + prefab_path: str, + overwrite: bool = False ) -> str: """Create a new prefab asset from a GameObject in the scene. @@ -115,25 +162,51 @@ def register_asset_tools(mcp: FastMCP): ctx: The MCP context object_name: Name of the GameObject in the scene to create prefab from prefab_path: Path where the prefab should be saved (relative to Assets folder) + overwrite: Whether to overwrite if a prefab already exists at the path (default: False) Returns: str: Success message or error details """ try: + unity = get_unity_connection() + # Parameter validation if not object_name or not isinstance(object_name, str): return f"Error creating prefab: object_name must be a valid string" if not prefab_path or not isinstance(prefab_path, str): return f"Error creating prefab: prefab_path must be a valid string" + + # Check if the GameObject exists + found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", { + "name": object_name + }).get("objects", []) + + if not found_objects: + return f"GameObject '{object_name}' not found in the scene." # Verify prefab path has proper extension if not prefab_path.lower().endswith('.prefab'): prefab_path = f"{prefab_path}.prefab" - response = get_unity_connection().send_command("CREATE_PREFAB", { + # Check if a prefab already exists at this path + prefab_dir = '/'.join(prefab_path.split('/')[:-1]) or "Assets" + prefab_name = prefab_path.split('/')[-1] + + prefab_assets = unity.send_command("GET_ASSET_LIST", { + "type": "Prefab", + "search_pattern": prefab_name, + "folder": prefab_dir + }).get("assets", []) + + prefab_exists = any(asset.get("path") == prefab_path for asset in prefab_assets) + if prefab_exists and not overwrite: + return f"Prefab already exists at '{prefab_path}'. Use overwrite=True to replace it." + + response = unity.send_command("CREATE_PREFAB", { "object_name": object_name, - "prefab_path": prefab_path + "prefab_path": prefab_path, + "overwrite": overwrite }) if not response.get("success", False): @@ -158,7 +231,27 @@ def register_asset_tools(mcp: FastMCP): str: Success message or error details """ try: - response = get_unity_connection().send_command("APPLY_PREFAB", { + unity = get_unity_connection() + + # Check if the GameObject exists + found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", { + "name": object_name + }).get("objects", []) + + if not found_objects: + return f"GameObject '{object_name}' not found in the scene." + + # Check if the object is a prefab instance + object_props = unity.send_command("GET_OBJECT_PROPERTIES", { + "name": object_name + }) + + # Try to extract prefab status from properties + is_prefab_instance = object_props.get("isPrefabInstance", False) + if not is_prefab_instance: + return f"GameObject '{object_name}' is not a prefab instance." + + response = unity.send_command("APPLY_PREFAB", { "object_name": object_name }) return response.get("message", "Prefab changes applied successfully") diff --git a/Python/tools/editor_tools.py b/Python/tools/editor_tools.py index f4e68e5..e5b2e42 100644 --- a/Python/tools/editor_tools.py +++ b/Python/tools/editor_tools.py @@ -92,6 +92,34 @@ def register_editor_tools(mcp: FastMCP): str: Success message or error details """ try: + # Validate platform + valid_platforms = ["windows", "mac", "linux", "android", "ios", "webgl"] + if platform.lower() not in valid_platforms: + return f"Error: '{platform}' is not a valid platform. Valid platforms are: {', '.join(valid_platforms)}" + + # Check if build_path exists and is writable + import os + + # Check if the directory exists + build_dir = os.path.dirname(build_path) + if not os.path.exists(build_dir): + return f"Error: Build directory '{build_dir}' does not exist. Please create it first." + + # Check if the directory is writable + if not os.access(build_dir, os.W_OK): + return f"Error: Build directory '{build_dir}' is not writable." + + # If the build path itself exists, check if it's a file or directory + if os.path.exists(build_path): + if os.path.isfile(build_path): + # If it's a file, check if it's writable + if not os.access(build_path, os.W_OK): + return f"Error: Existing build file '{build_path}' is not writable." + elif os.path.isdir(build_path): + # If it's a directory, check if it's writable + if not os.access(build_path, os.W_OK): + return f"Error: Existing build directory '{build_path}' is not writable." + response = get_unity_connection().send_command("EDITOR_CONTROL", { "command": "BUILD", "params": { @@ -104,17 +132,41 @@ def register_editor_tools(mcp: FastMCP): return f"Error building project: {str(e)}" @mcp.tool() - def execute_command(ctx: Context, command_name: str) -> str: + def execute_command(ctx: Context, command_name: str, validate_command: bool = True) -> str: """Execute a specific editor command or custom script within the Unity editor. Args: command_name: Name of the editor command to execute (e.g., "Edit/Preferences") + validate_command: Whether to validate the command existence before executing (default: True) Returns: str: Success message or error details """ try: - response = get_unity_connection().send_command("EDITOR_CONTROL", { + unity = get_unity_connection() + + # Optionally validate if the command exists + if validate_command: + # Get a list of available commands from Unity + available_commands = unity.send_command("EDITOR_CONTROL", { + "command": "GET_AVAILABLE_COMMANDS" + }).get("commands", []) + + # Check if the command exists in the list + if available_commands and command_name not in available_commands: + # If command doesn't exist, try to find similar commands as suggestions + similar_commands = [cmd for cmd in available_commands if command_name.lower() in cmd.lower()] + suggestion_msg = "" + if similar_commands: + suggestion_msg = f" Did you mean one of these: {', '.join(similar_commands[:5])}" + if len(similar_commands) > 5: + suggestion_msg += " or others?" + else: + suggestion_msg += "?" + + return f"Error: Command '{command_name}' not found.{suggestion_msg}" + + response = unity.send_command("EDITOR_CONTROL", { "command": "EXECUTE_COMMAND", "params": { "commandName": command_name diff --git a/Python/tools/material_tools.py b/Python/tools/material_tools.py index e0ee286..47cbf2d 100644 --- a/Python/tools/material_tools.py +++ b/Python/tools/material_tools.py @@ -10,7 +10,8 @@ def register_material_tools(mcp: FastMCP): ctx: Context, object_name: str, material_name: str = None, - color: List[float] = None + color: List[float] = None, + create_if_missing: bool = True ) -> str: """ Apply or create a material for a game object. @@ -19,14 +20,56 @@ def register_material_tools(mcp: FastMCP): object_name: Target game object. material_name: Optional material name. color: Optional [R, G, B] values (0.0-1.0). + create_if_missing: Whether to create the material if it doesn't exist (default: True). """ try: unity = get_unity_connection() + + # Check if the object exists + object_response = unity.send_command("FIND_OBJECTS_BY_NAME", { + "name": object_name + }) + + objects = object_response.get("objects", []) + if not objects: + return f"GameObject '{object_name}' not found in the scene." + + # If a material name is specified, check if it exists + if material_name: + material_assets = unity.send_command("GET_ASSET_LIST", { + "type": "Material", + "search_pattern": material_name, + "folder": "Assets/Materials" + }).get("assets", []) + + material_exists = any(asset.get("name") == material_name for asset in material_assets) + + if not material_exists and not create_if_missing: + return f"Material '{material_name}' not found. Use create_if_missing=True to create it." + + # Validate color values if provided + if color: + # Check if color has the right number of components (RGB or RGBA) + if not (len(color) == 3 or len(color) == 4): + return f"Error: Color must have 3 (RGB) or 4 (RGBA) components, but got {len(color)}." + + # Check if all color values are in the 0-1 range + for i, value in enumerate(color): + if not isinstance(value, (int, float)): + return f"Error: Color component at index {i} is not a number." + + if value < 0.0 or value > 1.0: + channel = "RGBA"[i] if i < 4 else f"component {i}" + return f"Error: Color {channel} value must be in the range 0.0-1.0, but got {value}." + + # Set up parameters for the command params = {"object_name": object_name} if material_name: params["material_name"] = material_name + params["create_if_missing"] = create_if_missing if color: params["color"] = color + result = unity.send_command("SET_MATERIAL", params) return f"Applied material to {object_name}: {result.get('material_name', 'unknown')}" except Exception as e: diff --git a/Python/tools/scene_tools.py b/Python/tools/scene_tools.py index 988b9e4..378fbd8 100644 --- a/Python/tools/scene_tools.py +++ b/Python/tools/scene_tools.py @@ -28,6 +28,19 @@ def register_scene_tools(mcp: FastMCP): """ try: unity = get_unity_connection() + + # Check if the scene exists in the project + scenes = unity.send_command("GET_ASSET_LIST", { + "type": "Scene", + "search_pattern": scene_path.split('/')[-1], + "folder": '/'.join(scene_path.split('/')[:-1]) or "Assets" + }).get("assets", []) + + # Check if any scene matches the exact path + scene_exists = any(scene.get("path") == scene_path for scene in scenes) + if not scene_exists: + return f"Scene at '{scene_path}' not found in the project." + result = unity.send_command("OPEN_SCENE", {"scene_path": scene_path}) return result.get("message", "Scene opened successfully") except Exception as e: @@ -48,19 +61,36 @@ def register_scene_tools(mcp: FastMCP): return f"Error saving scene: {str(e)}" @mcp.tool() - def new_scene(ctx: Context, scene_path: str) -> str: + def new_scene(ctx: Context, scene_path: str, overwrite: bool = False) -> str: """Create a new empty scene in the Unity editor. Args: scene_path: Full path where the new scene should be saved (e.g., "Assets/Scenes/NewScene.unity") + overwrite: Whether to overwrite if scene already exists (default: False) Returns: str: Success message or error details """ try: unity = get_unity_connection() + + # Check if a scene with this path already exists + scenes = unity.send_command("GET_ASSET_LIST", { + "type": "Scene", + "search_pattern": scene_path.split('/')[-1], + "folder": '/'.join(scene_path.split('/')[:-1]) or "Assets" + }).get("assets", []) + + # Check if any scene matches the exact path + scene_exists = any(scene.get("path") == scene_path for scene in scenes) + if scene_exists and not overwrite: + return f"Scene at '{scene_path}' already exists. Use overwrite=True to replace it." + # Create new scene - result = unity.send_command("NEW_SCENE", {"scene_path": scene_path}) + result = unity.send_command("NEW_SCENE", { + "scene_path": scene_path, + "overwrite": overwrite + }) # Save the scene to ensure it's properly created unity.send_command("SAVE_SCENE") @@ -115,7 +145,8 @@ def register_scene_tools(mcp: FastMCP): name: str = None, location: List[float] = None, rotation: List[float] = None, - scale: List[float] = None + scale: List[float] = None, + replace_if_exists: bool = False ) -> str: """ Create a game object in the Unity scene. @@ -126,12 +157,27 @@ def register_scene_tools(mcp: FastMCP): location: [x, y, z] position (defaults to [0, 0, 0]). rotation: [x, y, z] rotation in degrees (defaults to [0, 0, 0]). scale: [x, y, z] scale factors (defaults to [1, 1, 1]). + replace_if_exists: Whether to replace if an object with the same name exists (default: False) Returns: Confirmation message with the created object's name. """ try: unity = get_unity_connection() + + # Check if an object with the specified name already exists (if name is provided) + if name: + found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", { + "name": name + }).get("objects", []) + + if found_objects and not replace_if_exists: + return f"Object with name '{name}' already exists. Use replace_if_exists=True to replace it." + elif found_objects and replace_if_exists: + # Delete the existing object + unity.send_command("DELETE_OBJECT", {"name": name}) + + # Create the new object params = { "type": type.upper(), "location": location or [0, 0, 0], @@ -140,6 +186,7 @@ def register_scene_tools(mcp: FastMCP): } if name: params["name"] = name + result = unity.send_command("CREATE_OBJECT", params) return f"Created {type} game object: {result['name']}" except Exception as e: @@ -180,6 +227,48 @@ def register_scene_tools(mcp: FastMCP): """ try: unity = get_unity_connection() + + # Check if the object exists + found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", { + "name": name + }).get("objects", []) + + if not found_objects: + return f"Object with name '{name}' not found in the scene." + + # If set_parent is provided, check if parent object exists + if set_parent is not None: + parent_objects = unity.send_command("FIND_OBJECTS_BY_NAME", { + "name": set_parent + }).get("objects", []) + + if not parent_objects: + return f"Parent object '{set_parent}' not found in the scene." + + # If we're adding a component, we could also check if it's already attached + if add_component is not None: + object_props = unity.send_command("GET_OBJECT_PROPERTIES", { + "name": name + }) + + components = object_props.get("components", []) + component_exists = any(comp.get("type") == add_component for comp in components) + + if component_exists: + return f"Component '{add_component}' is already attached to '{name}'." + + # If we're removing a component, check if it exists + if remove_component is not None: + object_props = unity.send_command("GET_OBJECT_PROPERTIES", { + "name": name + }) + + components = object_props.get("components", []) + component_exists = any(comp.get("type") == remove_component for comp in components) + + if not component_exists: + return f"Component '{remove_component}' is not attached to '{name}'." + params = {"name": name} # Add basic transform properties @@ -212,15 +301,31 @@ def register_scene_tools(mcp: FastMCP): return f"Error modifying game object: {str(e)}" @mcp.tool() - def delete_object(ctx: Context, name: str) -> str: + def delete_object(ctx: Context, name: str, ignore_missing: bool = False) -> str: """ Remove a game object from the scene. Args: name: Name of the game object to delete. + ignore_missing: Whether to silently ignore if the object doesn't exist (default: False) + + Returns: + str: Success message or error details """ try: unity = get_unity_connection() + + # Check if the object exists + found_objects = unity.send_command("FIND_OBJECTS_BY_NAME", { + "name": name + }).get("objects", []) + + if not found_objects: + if ignore_missing: + return f"No object named '{name}' found to delete. Ignoring." + else: + return f"Error: Object '{name}' not found in the scene." + result = unity.send_command("DELETE_OBJECT", {"name": name}) return f"Deleted game object: {name}" except Exception as e: diff --git a/Python/tools/script_tools.py b/Python/tools/script_tools.py index be2df00..2914ea9 100644 --- a/Python/tools/script_tools.py +++ b/Python/tools/script_tools.py @@ -31,7 +31,8 @@ def register_script_tools(mcp: FastMCP): script_name: str, script_type: str = "MonoBehaviour", namespace: str = None, - template: str = None + template: str = None, + overwrite: bool = False ) -> str: """Create a new Unity script file. @@ -41,17 +42,32 @@ def register_script_tools(mcp: FastMCP): script_type: Type of script (e.g., MonoBehaviour, ScriptableObject) namespace: Optional namespace for the script template: Optional custom template to use + overwrite: Whether to overwrite if script already exists (default: False) Returns: str: Success message or error details """ try: + # First check if a script with this name already exists + unity = get_unity_connection() + script_path = f"Assets/Scripts/{script_name}.cs" + + # Try to view the script to check if it exists + existing_script_response = unity.send_command("VIEW_SCRIPT", { + "script_path": script_path + }) + + # If the script exists and overwrite is False, return a message + if "content" in existing_script_response and not overwrite: + return f"Script '{script_name}.cs' already exists. Use overwrite=True to replace it." + # Send command to Unity to create the script - response = get_unity_connection().send_command("CREATE_SCRIPT", { + response = unity.send_command("CREATE_SCRIPT", { "script_name": script_name, "script_type": script_type, "namespace": namespace, - "template": template + "template": template, + "overwrite": overwrite }) return response.get("message", "Script created successfully") except Exception as e: @@ -61,7 +77,9 @@ def register_script_tools(mcp: FastMCP): def update_script( ctx: Context, script_path: str, - content: str + content: str, + create_if_missing: bool = False, + create_folder_if_missing: bool = False ) -> str: """Update the contents of an existing Unity script. @@ -69,13 +87,66 @@ def register_script_tools(mcp: FastMCP): ctx: The MCP context script_path: Path to the script file relative to the Assets folder content: New content for the script + create_if_missing: Whether to create the script if it doesn't exist (default: False) + create_folder_if_missing: Whether to create the parent directory if it doesn't exist (default: False) Returns: str: Success message or error details """ try: + unity = get_unity_connection() + + # Check if the script exists first + existing_script_response = unity.send_command("VIEW_SCRIPT", { + "script_path": script_path + }) + + # If the script doesn't exist + if "content" not in existing_script_response: + if not create_if_missing: + return f"Script at '{script_path}' not found. Use create_if_missing=True to create it." + + # If we should create the missing script + script_name = script_path.split("/")[-1].replace(".cs", "") + script_folder = "/".join(script_path.split("/")[:-1]) + + # Check if the parent directory exists + if script_folder: + folder_exists = False + try: + # Try to list scripts in the folder to see if it exists + folder_response = unity.send_command("LIST_SCRIPTS", { + "folder_path": script_folder + }) + # If we didn't get an error, the folder exists + folder_exists = "error" not in folder_response + except: + folder_exists = False + + if not folder_exists: + if not create_folder_if_missing: + return f"Parent directory '{script_folder}' does not exist. Use create_folder_if_missing=True to create it." + + # Create the directory structure + try: + response = unity.send_command("CREATE_FOLDER", { + "folder_path": script_folder + }) + if "error" in response: + return f"Error creating directory '{script_folder}': {response.get('error')}" + except Exception as folder_error: + return f"Error creating directory '{script_folder}': {str(folder_error)}" + + # Create the script with the provided content + response = unity.send_command("CREATE_SCRIPT", { + "script_name": script_name, + "script_folder": script_folder, + "content": content + }) + return response.get("message", "Script created successfully") + # Send command to Unity to update the script - response = get_unity_connection().send_command("UPDATE_SCRIPT", { + response = unity.send_command("UPDATE_SCRIPT", { "script_path": script_path, "content": content }) @@ -110,7 +181,8 @@ def register_script_tools(mcp: FastMCP): def attach_script( ctx: Context, object_name: str, - script_name: str + script_name: str, + script_path: str = None ) -> str: """Attach a script component to a GameObject. @@ -118,15 +190,85 @@ def register_script_tools(mcp: FastMCP): ctx: The MCP context object_name: Name of the target GameObject in the scene script_name: Name of the script to attach (with or without .cs extension) + script_path: Optional full path to the script (if not in the default Scripts folder) Returns: str: Success message or error details """ try: + unity = get_unity_connection() + + # Check if the object exists + object_response = unity.send_command("FIND_OBJECTS_BY_NAME", { + "name": object_name + }) + + objects = object_response.get("objects", []) + if not objects: + return f"GameObject '{object_name}' not found in the scene." + + # Ensure script_name has .cs extension + if not script_name.lower().endswith(".cs"): + script_name = f"{script_name}.cs" + + # Determine the full script path + if script_path is None: + # Use default Scripts folder if no path provided + script_path = f"Assets/Scripts/{script_name}" + elif not script_path.endswith(script_name): + # If path is just a directory, append the script name + if script_path.endswith("/"): + script_path = f"{script_path}{script_name}" + else: + script_path = f"{script_path}/{script_name}" + + # Check if the script exists by trying to view it + existing_script_response = unity.send_command("VIEW_SCRIPT", { + "script_path": script_path + }) + + if "content" not in existing_script_response: + # If not found at the specific path, try to search for it in the project + script_found = False + try: + # Search in the entire Assets folder + script_assets = unity.send_command("LIST_SCRIPTS", { + "folder_path": "Assets" + }).get("scripts", []) + + # Look for matching script name in any folder + matching_scripts = [path for path in script_assets if path.endswith(f"/{script_name}") or path == script_name] + + if matching_scripts: + script_path = matching_scripts[0] + script_found = True + if len(matching_scripts) > 1: + return f"Multiple scripts named '{script_name}' found in the project. Please specify script_path parameter." + except: + pass + + if not script_found: + return f"Script '{script_name}' not found in the project." + + # Check if the script is already attached + object_props = unity.send_command("GET_OBJECT_PROPERTIES", { + "name": object_name + }) + + # Extract script name without .cs and without path + script_class_name = script_name.replace(".cs", "") + + # Check if component is already attached + components = object_props.get("components", []) + for component in components: + if component.get("type") == script_class_name: + return f"Script '{script_class_name}' is already attached to '{object_name}'." + # Send command to Unity to attach the script - response = get_unity_connection().send_command("ATTACH_SCRIPT", { + response = unity.send_command("ATTACH_SCRIPT", { "object_name": object_name, - "script_name": script_name + "script_name": script_name, + "script_path": script_path }) return response.get("message", "Script attached successfully") except Exception as e: