From afff1caa4fc91c6dc88c1dce365a5a17e35d432c Mon Sep 17 00:00:00 2001 From: Justin Barnett Date: Tue, 18 Mar 2025 10:41:01 -0400 Subject: [PATCH] fix script adding issues --- Editor/Commands/ScriptCommandHandler.cs | 401 +++++++++++++++++------- Python/tools/script_tools.py | 159 ++++++---- 2 files changed, 379 insertions(+), 181 deletions(-) diff --git a/Editor/Commands/ScriptCommandHandler.cs b/Editor/Commands/ScriptCommandHandler.cs index 2cbc261..18055f7 100644 --- a/Editor/Commands/ScriptCommandHandler.cs +++ b/Editor/Commands/ScriptCommandHandler.cs @@ -20,12 +20,40 @@ namespace MCPServer.Editor.Commands public static object ViewScript(JObject @params) { string scriptPath = (string)@params["script_path"] ?? throw new System.Exception("Parameter 'script_path' is required."); - string fullPath = Path.Combine(Application.dataPath, scriptPath); + bool requireExists = (bool?)@params["require_exists"] ?? true; + + // Debug to help diagnose issues + Debug.Log($"ViewScript - Original script path: {scriptPath}"); + + // Handle path correctly to avoid double "Assets" folder issue + string relativePath; + if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + // If path already starts with Assets/, remove it for local path operations + relativePath = scriptPath.Substring(7); + } + else + { + relativePath = scriptPath; + } + + string fullPath = Path.Combine(Application.dataPath, relativePath); + Debug.Log($"ViewScript - Relative path: {relativePath}"); + Debug.Log($"ViewScript - Full path: {fullPath}"); if (!File.Exists(fullPath)) - throw new System.Exception($"Script file not found: {scriptPath}"); + { + if (requireExists) + { + throw new System.Exception($"Script file not found: {scriptPath}"); + } + else + { + return new { exists = false, message = $"Script file not found: {scriptPath}" }; + } + } - return new { content = File.ReadAllText(fullPath) }; + return new { exists = true, content = File.ReadAllText(fullPath) }; } /// @@ -33,6 +61,8 @@ namespace MCPServer.Editor.Commands /// private static void EnsureScriptsFolderExists() { + // Never create an "Assets" folder as it's the project root + // Instead create "Scripts" within the existing Assets folder string scriptsFolderPath = Path.Combine(Application.dataPath, "Scripts"); if (!Directory.Exists(scriptsFolderPath)) { @@ -42,7 +72,7 @@ namespace MCPServer.Editor.Commands } /// - /// Creates a new Unity script file in the Scripts folder + /// Creates a new Unity script file in the specified folder /// public static object CreateScript(JObject @params) { @@ -52,114 +82,147 @@ namespace MCPServer.Editor.Commands string template = (string)@params["template"]; string scriptFolder = (string)@params["script_folder"]; string content = (string)@params["content"]; + bool overwrite = (bool?)@params["overwrite"] ?? false; // Ensure script name ends with .cs - if (!scriptName.EndsWith(".cs")) + if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) scriptName += ".cs"; + // Make sure scriptName doesn't contain path separators - extract base name + scriptName = Path.GetFileName(scriptName); + + // Determine the script path string scriptPath; - - // If content is provided, use it directly - if (!string.IsNullOrEmpty(content)) + + // Handle the script folder parameter + if (string.IsNullOrEmpty(scriptFolder)) { - // Use specified folder or default to Scripts - scriptPath = string.IsNullOrEmpty(scriptFolder) ? "Scripts" : scriptFolder; + // Default to Scripts folder within Assets + scriptPath = "Scripts"; + EnsureScriptsFolderExists(); + } + else + { + // Use provided folder path + scriptPath = scriptFolder; + + // If scriptFolder starts with "Assets/", remove it for local path operations + if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + scriptPath = scriptPath.Substring(7); + } + } - // Ensure folder exists - string folderPath = Path.Combine(Application.dataPath, scriptPath); - if (!Directory.Exists(folderPath)) + // Create the full directory path, avoiding Assets/Assets issue + string folderPath = Path.Combine(Application.dataPath, scriptPath); + Debug.Log($"CreateScript - Script name: {scriptName}"); + Debug.Log($"CreateScript - Script path: {scriptPath}"); + Debug.Log($"CreateScript - Creating script in folder path: {folderPath}"); + + // Create directory if it doesn't exist + if (!Directory.Exists(folderPath)) + { + try { Directory.CreateDirectory(folderPath); AssetDatabase.Refresh(); } + catch (Exception ex) + { + throw new System.Exception($"Failed to create directory '{scriptPath}': {ex.Message}"); + } + } - // Create the script file with provided content - string fullPath = Path.Combine(Application.dataPath, scriptPath, scriptName); - File.WriteAllText(fullPath, content); + // Check if script already exists + string fullFilePath = Path.Combine(folderPath, scriptName); + if (File.Exists(fullFilePath) && !overwrite) + { + throw new System.Exception($"Script file '{scriptName}' already exists in '{scriptPath}' and overwrite is not enabled."); + } - // Refresh the AssetDatabase + try + { + // If content is provided, use it directly + if (!string.IsNullOrEmpty(content)) + { + // Create the script file with provided content + File.WriteAllText(fullFilePath, content); + } + else + { + // Otherwise generate content based on template and parameters + StringBuilder contentBuilder = new StringBuilder(); + + // Add using directives + contentBuilder.AppendLine("using UnityEngine;"); + contentBuilder.AppendLine(); + + // Add namespace if specified + if (!string.IsNullOrEmpty(namespaceName)) + { + contentBuilder.AppendLine($"namespace {namespaceName}"); + contentBuilder.AppendLine("{"); + } + + // Add class definition with indent based on namespace + string indent = string.IsNullOrEmpty(namespaceName) ? "" : " "; + contentBuilder.AppendLine($"{indent}public class {Path.GetFileNameWithoutExtension(scriptName)} : {scriptType}"); + contentBuilder.AppendLine($"{indent}{{"); + + // Add default Unity methods based on script type + if (scriptType == "MonoBehaviour") + { + contentBuilder.AppendLine($"{indent} private void Start()"); + contentBuilder.AppendLine($"{indent} {{"); + contentBuilder.AppendLine($"{indent} // Initialize your component here"); + contentBuilder.AppendLine($"{indent} }}"); + contentBuilder.AppendLine(); + contentBuilder.AppendLine($"{indent} private void Update()"); + contentBuilder.AppendLine($"{indent} {{"); + contentBuilder.AppendLine($"{indent} // Update your component here"); + contentBuilder.AppendLine($"{indent} }}"); + } + else if (scriptType == "ScriptableObject") + { + contentBuilder.AppendLine($"{indent} private void OnEnable()"); + contentBuilder.AppendLine($"{indent} {{"); + contentBuilder.AppendLine($"{indent} // Initialize your ScriptableObject here"); + contentBuilder.AppendLine($"{indent} }}"); + } + + // Close class + contentBuilder.AppendLine($"{indent}}}"); + + // Close namespace if specified + if (!string.IsNullOrEmpty(namespaceName)) + { + contentBuilder.AppendLine("}"); + } + + // Write the generated content to file + File.WriteAllText(fullFilePath, contentBuilder.ToString()); + } + + // Refresh the AssetDatabase to recognize the new script AssetDatabase.Refresh(); - return new { message = $"Created script: {Path.Combine(scriptPath, scriptName)}" }; - } - - // Otherwise generate content based on template and parameters - - // Ensure Scripts folder exists - EnsureScriptsFolderExists(); - - // Create namespace-based folder structure if namespace is specified - scriptPath = string.IsNullOrEmpty(scriptFolder) ? "Scripts" : scriptFolder; - if (!string.IsNullOrEmpty(namespaceName)) - { - if (scriptPath == "Scripts") // Only modify path if we're using the default + // Return the relative path for easier reference + string relativePath = scriptPath.Replace('\\', '/'); + if (!relativePath.StartsWith("Assets/")) { - scriptPath = Path.Combine(scriptPath, namespaceName.Replace('.', '/')); - } - string namespaceFolderPath = Path.Combine(Application.dataPath, scriptPath); - if (!Directory.Exists(namespaceFolderPath)) - { - Directory.CreateDirectory(namespaceFolderPath); - AssetDatabase.Refresh(); + relativePath = $"Assets/{relativePath}"; } + + return new { + message = $"Created script: {Path.Combine(relativePath, scriptName).Replace('\\', '/')}", + script_path = Path.Combine(relativePath, scriptName).Replace('\\', '/') + }; } - - // Create the script content - StringBuilder contentBuilder = new StringBuilder(); - - // Add using directives - contentBuilder.AppendLine("using UnityEngine;"); - contentBuilder.AppendLine(); - - // Add namespace if specified - if (!string.IsNullOrEmpty(namespaceName)) + catch (Exception ex) { - contentBuilder.AppendLine($"namespace {namespaceName}"); - contentBuilder.AppendLine("{"); + Debug.LogError($"Failed to create script: {ex.Message}\n{ex.StackTrace}"); + throw new System.Exception($"Failed to create script '{scriptName}': {ex.Message}"); } - - // Add class definition - contentBuilder.AppendLine($" public class {Path.GetFileNameWithoutExtension(scriptName)} : {scriptType}"); - contentBuilder.AppendLine(" {"); - - // Add default Unity methods based on script type - if (scriptType == "MonoBehaviour") - { - contentBuilder.AppendLine(" private void Start()"); - contentBuilder.AppendLine(" {"); - contentBuilder.AppendLine(" // Initialize your component here"); - contentBuilder.AppendLine(" }"); - contentBuilder.AppendLine(); - contentBuilder.AppendLine(" private void Update()"); - contentBuilder.AppendLine(" {"); - contentBuilder.AppendLine(" // Update your component here"); - contentBuilder.AppendLine(" }"); - } - else if (scriptType == "ScriptableObject") - { - contentBuilder.AppendLine(" private void OnEnable()"); - contentBuilder.AppendLine(" {"); - contentBuilder.AppendLine(" // Initialize your ScriptableObject here"); - contentBuilder.AppendLine(" }"); - } - - // Close class - contentBuilder.AppendLine(" }"); - - // Close namespace if specified - if (!string.IsNullOrEmpty(namespaceName)) - { - contentBuilder.AppendLine("}"); - } - - // Create the script file in the Scripts folder - string fullFilePath = Path.Combine(Application.dataPath, scriptPath, scriptName); - File.WriteAllText(fullFilePath, contentBuilder.ToString()); - - // Refresh the AssetDatabase - AssetDatabase.Refresh(); - - return new { message = $"Created script: {Path.Combine(scriptPath, scriptName)}" }; } /// @@ -172,8 +235,25 @@ namespace MCPServer.Editor.Commands bool createIfMissing = (bool?)@params["create_if_missing"] ?? false; bool createFolderIfMissing = (bool?)@params["create_folder_if_missing"] ?? false; - string fullPath = Path.Combine(Application.dataPath, scriptPath); + // Handle path correctly to avoid double "Assets" folder + string relativePath; + if (scriptPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + // If path already starts with Assets/, remove it for local path operations + relativePath = scriptPath.Substring(7); + } + else + { + relativePath = scriptPath; + } + + string fullPath = Path.Combine(Application.dataPath, relativePath); string directory = Path.GetDirectoryName(fullPath); + + // Debug the paths to help diagnose issues + Debug.Log($"UpdateScript - Original script path: {scriptPath}"); + Debug.Log($"UpdateScript - Relative path: {relativePath}"); + Debug.Log($"UpdateScript - Full path: {fullPath}"); // Check if file exists, create if requested if (!File.Exists(fullPath)) @@ -252,6 +332,7 @@ namespace MCPServer.Editor.Commands { string objectName = (string)@params["object_name"] ?? throw new System.Exception("Parameter 'object_name' is required."); string scriptName = (string)@params["script_name"] ?? throw new System.Exception("Parameter 'script_name' is required."); + string scriptPath = (string)@params["script_path"]; // Optional // Find the target object GameObject targetObject = GameObject.Find(objectName); @@ -259,35 +340,119 @@ namespace MCPServer.Editor.Commands throw new System.Exception($"Object '{objectName}' not found in scene."); // Ensure script name ends with .cs - if (!scriptName.EndsWith(".cs")) + if (!scriptName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) scriptName += ".cs"; + // Remove the path from the scriptName if it contains path separators + string scriptFileName = Path.GetFileName(scriptName); + string scriptNameWithoutExtension = Path.GetFileNameWithoutExtension(scriptFileName); + // Find the script asset - string[] guids = AssetDatabase.FindAssets(Path.GetFileNameWithoutExtension(scriptName)); - if (guids.Length == 0) - throw new System.Exception($"Script '{scriptName}' not found in project."); - - // Get the script asset - string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]); - MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath(scriptPath); - if (scriptAsset == null) - throw new System.Exception($"Failed to load script asset: {scriptName}"); - - // Get the script type - System.Type scriptType = scriptAsset.GetClass(); - if (scriptType == null) - throw new System.Exception($"Script '{scriptName}' does not contain a valid MonoBehaviour class."); - - // Add the component - Component component = targetObject.AddComponent(scriptType); - if (component == null) - throw new System.Exception($"Failed to add component of type {scriptType.Name} to object '{objectName}'."); - - return new + string[] guids; + + if (!string.IsNullOrEmpty(scriptPath)) { - message = $"Successfully attached script '{scriptName}' to object '{objectName}'", - component_type = scriptType.Name - }; + // If a specific path is provided, try that first + if (File.Exists(Path.Combine(Application.dataPath, scriptPath.Replace("Assets/", "")))) + { + // Use the direct path if it exists + MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath(scriptPath); + if (scriptAsset != null) + { + System.Type scriptType = scriptAsset.GetClass(); + if (scriptType != null) + { + try + { + // Try to add the component + Component component = targetObject.AddComponent(scriptType); + if (component != null) + { + return new + { + message = $"Successfully attached script '{scriptFileName}' to object '{objectName}'", + component_type = scriptType.Name + }; + } + } + catch (Exception ex) + { + Debug.LogError($"Error attaching script component: {ex.Message}"); + throw new System.Exception($"Failed to add component: {ex.Message}"); + } + } + } + } + } + + // Use the file name for searching if direct path didn't work + guids = AssetDatabase.FindAssets(scriptNameWithoutExtension + " t:script"); + + if (guids.Length == 0) + { + // Try a broader search if exact match fails + guids = AssetDatabase.FindAssets(scriptNameWithoutExtension); + + if (guids.Length == 0) + throw new System.Exception($"Script '{scriptFileName}' not found in project."); + } + + // Check each potential script until we find one that can be attached + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + + // Filter to only consider .cs files + if (!path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) + continue; + + // Double check the file name to avoid false matches + string foundFileName = Path.GetFileName(path); + if (!string.Equals(foundFileName, scriptFileName, StringComparison.OrdinalIgnoreCase) && + !string.Equals(Path.GetFileNameWithoutExtension(foundFileName), scriptNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) + continue; + + MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath(path); + if (scriptAsset == null) + continue; + + System.Type scriptType = scriptAsset.GetClass(); + if (scriptType == null || !typeof(MonoBehaviour).IsAssignableFrom(scriptType)) + continue; + + try + { + // Check if component is already attached + if (targetObject.GetComponent(scriptType) != null) + { + return new + { + message = $"Script '{scriptNameWithoutExtension}' is already attached to object '{objectName}'", + component_type = scriptType.Name + }; + } + + // Add the component + Component component = targetObject.AddComponent(scriptType); + if (component != null) + { + return new + { + message = $"Successfully attached script '{scriptFileName}' to object '{objectName}'", + component_type = scriptType.Name, + script_path = path + }; + } + } + catch (Exception ex) + { + Debug.LogError($"Error attaching script '{path}': {ex.Message}"); + // Continue trying other matches instead of failing immediately + } + } + + // If we've tried all possibilities and nothing worked + throw new System.Exception($"Could not attach script '{scriptFileName}' to object '{objectName}'. No valid script found or component creation failed."); } } } \ No newline at end of file diff --git a/Python/tools/script_tools.py b/Python/tools/script_tools.py index 933a0d2..f67a963 100644 --- a/Python/tools/script_tools.py +++ b/Python/tools/script_tools.py @@ -6,22 +6,35 @@ def register_script_tools(mcp: FastMCP): """Register all script-related tools with the MCP server.""" @mcp.tool() - def view_script(ctx: Context, script_path: str) -> str: + def view_script(ctx: Context, script_path: str, require_exists: bool = True) -> str: """View the contents of a Unity script file. Args: ctx: The MCP context script_path: Path to the script file relative to the Assets folder + require_exists: Whether to raise an error if the file doesn't exist (default: True) Returns: str: The contents of the script file or error message """ try: + # Normalize script path to ensure it has the correct format + if not script_path.startswith("Assets/"): + script_path = f"Assets/{script_path}" + + # Debug to help diagnose issues + print(f"ViewScript - Using normalized script path: {script_path}") + # Send command to Unity to read the script file response = get_unity_connection().send_command("VIEW_SCRIPT", { - "script_path": script_path + "script_path": script_path, + "require_exists": require_exists }) - return response.get("content", "Script not found") + + if response.get("exists", True): + return response.get("content", "Script contents not available") + else: + return response.get("message", "Script not found") except Exception as e: return f"Error viewing script: {str(e)}" @@ -32,7 +45,9 @@ def register_script_tools(mcp: FastMCP): script_type: str = "MonoBehaviour", namespace: str = None, template: str = None, - overwrite: bool = False + script_folder: str = None, + overwrite: bool = False, + content: str = None ) -> str: """Create a new Unity script file. @@ -42,33 +57,59 @@ 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 + script_folder: Optional folder path within Assets to create the script overwrite: Whether to overwrite if script already exists (default: False) + content: Optional custom content for the script 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 - }) + # Determine script path based on script_folder parameter + if script_folder: + # Use provided folder path + # Normalize the folder path first + if script_folder.startswith("Assets/"): + normalized_folder = script_folder + else: + normalized_folder = f"Assets/{script_folder}" + + # Create the full path + if normalized_folder.endswith("/"): + script_path = f"{normalized_folder}{script_name}.cs" + else: + script_path = f"{normalized_folder}/{script_name}.cs" + + # Debug to help diagnose issues + print(f"CreateScript - Folder: {script_folder}") + print(f"CreateScript - Normalized folder: {normalized_folder}") + print(f"CreateScript - Script path: {script_path}") + else: + # Default to Scripts folder when no folder is provided + script_path = f"Assets/Scripts/{script_name}.cs" + print(f"CreateScript - Using default 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 = unity.send_command("CREATE_SCRIPT", { + # Send command to Unity to create the script directly + # The C# handler will handle the file existence check + params = { "script_name": script_name, "script_type": script_type, "namespace": namespace, "template": template, "overwrite": overwrite - }) + } + + # Add script_folder if provided + if script_folder: + params["script_folder"] = script_folder + + # Add content if provided + if content: + params["content"] = content + + response = unity.send_command("CREATE_SCRIPT", params) return response.get("message", "Script created successfully") except Exception as e: return f"Error creating script: {str(e)}" @@ -96,9 +137,19 @@ def register_script_tools(mcp: FastMCP): try: unity = get_unity_connection() + # Normalize script path to ensure it has the correct format + # Make sure the path starts with Assets/ but not Assets/Assets/ + if not script_path.startswith("Assets/"): + script_path = f"Assets/{script_path}" + + # Debug to help diagnose issues + print(f"UpdateScript - Original path: {script_path}") + # Parse script path (for potential creation) - script_name = script_path.split("/")[-1].replace(".cs", "") - script_folder = "/".join(script_path.split("/")[:-1]) + script_name = script_path.split("/")[-1] + if not script_name.endswith(".cs"): + script_name += ".cs" + script_path = f"{script_path}.cs" if create_if_missing: # When create_if_missing is true, we'll just try to update directly, @@ -179,56 +230,33 @@ def register_script_tools(mcp: FastMCP): if not objects: return f"GameObject '{object_name}' not found in the scene." - # Ensure script_name has .cs extension + # 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): + # Remove any path information from script_name if it contains slashes + script_basename = script_name.split('/')[-1] + + # Determine the full script path if provided + if script_path is not None: + # Ensure script_path starts with Assets/ + if not script_path.startswith("Assets/"): + script_path = f"Assets/{script_path}" + # 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." + if not script_path.endswith(script_basename): + if script_path.endswith("/"): + script_path = f"{script_path}{script_basename}" + else: + script_path = f"{script_path}/{script_basename}" # 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", "") + # Extract script name without .cs and without path for component type checking + script_class_name = script_basename.replace(".cs", "") # Check if component is already attached components = object_props.get("components", []) @@ -237,11 +265,16 @@ def register_script_tools(mcp: FastMCP): return f"Script '{script_class_name}' is already attached to '{object_name}'." # Send command to Unity to attach the script - response = unity.send_command("ATTACH_SCRIPT", { + params = { "object_name": object_name, - "script_name": script_name, - "script_path": script_path - }) + "script_name": script_basename + } + + # Add script_path if provided + if script_path: + params["script_path"] = script_path + + response = unity.send_command("ATTACH_SCRIPT", params) return response.get("message", "Script attached successfully") except Exception as e: return f"Error attaching script: {str(e)}" \ No newline at end of file