diff --git a/Editor/Commands/ScriptCommandHandler.cs b/Editor/Commands/ScriptCommandHandler.cs index 6b6af57..666976c 100644 --- a/Editor/Commands/ScriptCommandHandler.cs +++ b/Editor/Commands/ScriptCommandHandler.cs @@ -1,10 +1,10 @@ -using UnityEngine; -using UnityEditor; using System; using System.IO; -using System.Text; using System.Linq; +using System.Text; using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; namespace UnityMCP.Editor.Commands { @@ -18,7 +18,9 @@ namespace UnityMCP.Editor.Commands /// public static object ViewScript(JObject @params) { - string scriptPath = (string)@params["script_path"] ?? throw new Exception("Parameter 'script_path' is required."); + string scriptPath = + (string)@params["script_path"] + ?? throw new Exception("Parameter 'script_path' is required."); bool requireExists = (bool?)@params["require_exists"] ?? true; // Handle path correctly to avoid double "Assets" folder issue @@ -47,7 +49,16 @@ namespace UnityMCP.Editor.Commands } } - return new { exists = true, content = File.ReadAllText(fullPath) }; + string content = File.ReadAllText(fullPath); + byte[] contentBytes = System.Text.Encoding.UTF8.GetBytes(content); + string base64Content = Convert.ToBase64String(contentBytes); + + return new + { + exists = true, + content = base64Content, + encoding = "base64" + }; } /// @@ -70,7 +81,9 @@ namespace UnityMCP.Editor.Commands /// public static object CreateScript(JObject @params) { - string scriptName = (string)@params["script_name"] ?? throw new Exception("Parameter 'script_name' is required."); + string scriptName = + (string)@params["script_name"] + ?? throw new Exception("Parameter 'script_name' is required."); string scriptType = (string)@params["script_type"] ?? "MonoBehaviour"; string namespaceName = (string)@params["namespace"]; string template = (string)@params["template"]; @@ -128,7 +141,9 @@ namespace UnityMCP.Editor.Commands string fullFilePath = Path.Combine(folderPath, scriptName); if (File.Exists(fullFilePath) && !overwrite) { - throw new Exception($"Script file '{scriptName}' already exists in '{scriptPath}' and overwrite is not enabled."); + throw new Exception( + $"Script file '{scriptName}' already exists in '{scriptPath}' and overwrite is not enabled." + ); } try @@ -157,7 +172,9 @@ namespace UnityMCP.Editor.Commands // 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}public class {Path.GetFileNameWithoutExtension(scriptName)} : {scriptType}" + ); contentBuilder.AppendLine($"{indent}{{"); // Add default Unity methods based on script type @@ -165,7 +182,9 @@ namespace UnityMCP.Editor.Commands { contentBuilder.AppendLine($"{indent} private void Start()"); contentBuilder.AppendLine($"{indent} {{"); - contentBuilder.AppendLine($"{indent} // Initialize your component here"); + contentBuilder.AppendLine( + $"{indent} // Initialize your component here" + ); contentBuilder.AppendLine($"{indent} }}"); contentBuilder.AppendLine(); contentBuilder.AppendLine($"{indent} private void Update()"); @@ -177,7 +196,9 @@ namespace UnityMCP.Editor.Commands { contentBuilder.AppendLine($"{indent} private void OnEnable()"); contentBuilder.AppendLine($"{indent} {{"); - contentBuilder.AppendLine($"{indent} // Initialize your ScriptableObject here"); + contentBuilder.AppendLine( + $"{indent} // Initialize your ScriptableObject here" + ); contentBuilder.AppendLine($"{indent} }}"); } @@ -222,8 +243,12 @@ namespace UnityMCP.Editor.Commands /// public static object UpdateScript(JObject @params) { - string scriptPath = (string)@params["script_path"] ?? throw new Exception("Parameter 'script_path' is required."); - string content = (string)@params["content"] ?? throw new Exception("Parameter 'content' is required."); + string scriptPath = + (string)@params["script_path"] + ?? throw new Exception("Parameter 'script_path' is required."); + string content = + (string)@params["content"] + ?? throw new Exception("Parameter 'content' is required."); bool createIfMissing = (bool?)@params["create_if_missing"] ?? false; bool createFolderIfMissing = (bool?)@params["create_folder_if_missing"] ?? false; @@ -257,7 +282,9 @@ namespace UnityMCP.Editor.Commands } else if (!Directory.Exists(directory)) { - throw new Exception($"Directory does not exist: {Path.GetDirectoryName(scriptPath)}"); + throw new Exception( + $"Directory does not exist: {Path.GetDirectoryName(scriptPath)}" + ); } // Create the file with content @@ -308,7 +335,8 @@ namespace UnityMCP.Editor.Commands if (!Directory.Exists(fullPath)) throw new Exception($"Folder not found: {folderPath}"); - string[] scripts = Directory.GetFiles(fullPath, "*.cs", SearchOption.AllDirectories) + string[] scripts = Directory + .GetFiles(fullPath, "*.cs", SearchOption.AllDirectories) .Select(path => path.Replace(Application.dataPath, "Assets")) .ToArray(); @@ -320,8 +348,12 @@ namespace UnityMCP.Editor.Commands /// public static object AttachScript(JObject @params) { - string objectName = (string)@params["object_name"] ?? throw new Exception("Parameter 'object_name' is required."); - string scriptName = (string)@params["script_name"] ?? throw new Exception("Parameter 'script_name' is required."); + string objectName = + (string)@params["object_name"] + ?? throw new Exception("Parameter 'object_name' is required."); + string scriptName = + (string)@params["script_name"] + ?? throw new Exception("Parameter 'script_name' is required."); string scriptPath = (string)@params["script_path"]; // Optional // Find the target object @@ -343,7 +375,11 @@ namespace UnityMCP.Editor.Commands if (!string.IsNullOrEmpty(scriptPath)) { // If a specific path is provided, try that first - if (File.Exists(Path.Combine(Application.dataPath, scriptPath.Replace("Assets/", "")))) + if ( + File.Exists( + Path.Combine(Application.dataPath, scriptPath.Replace("Assets/", "")) + ) + ) { // Use the direct path if it exists MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath(scriptPath); @@ -398,8 +434,18 @@ namespace UnityMCP.Editor.Commands // 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)) + if ( + !string.Equals( + foundFileName, + scriptFileName, + StringComparison.OrdinalIgnoreCase + ) + && !string.Equals( + Path.GetFileNameWithoutExtension(foundFileName), + scriptNameWithoutExtension, + StringComparison.OrdinalIgnoreCase + ) + ) continue; MonoScript scriptAsset = AssetDatabase.LoadAssetAtPath(path); @@ -442,7 +488,9 @@ namespace UnityMCP.Editor.Commands } // If we've tried all possibilities and nothing worked - throw new Exception($"Could not attach script '{scriptFileName}' to object '{objectName}'. No valid script found or component creation failed."); + throw new 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 f67a963..b7dee85 100644 --- a/Python/tools/script_tools.py +++ b/Python/tools/script_tools.py @@ -1,19 +1,21 @@ from mcp.server.fastmcp import FastMCP, Context from typing import List from unity_connection import get_unity_connection +import base64 + 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, 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 """ @@ -21,17 +23,22 @@ def register_script_tools(mcp: FastMCP): # 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, - "require_exists": require_exists - }) - + response = get_unity_connection().send_command( + "VIEW_SCRIPT", + {"script_path": script_path, "require_exists": require_exists}, + ) + if response.get("exists", True): + if response.get("encoding", "base64") == "base64": + decoded_content = base64.b64decode(response.get("content")).decode( + "utf-8" + ) + return decoded_content return response.get("content", "Script contents not available") else: return response.get("message", "Script not found") @@ -47,10 +54,10 @@ def register_script_tools(mcp: FastMCP): template: str = None, script_folder: str = None, overwrite: bool = False, - content: str = None + content: str = None, ) -> str: """Create a new Unity script file. - + Args: ctx: The MCP context script_name: Name of the script (without .cs extension) @@ -60,13 +67,13 @@ def register_script_tools(mcp: FastMCP): 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: unity = get_unity_connection() - + # Determine script path based on script_folder parameter if script_folder: # Use provided folder path @@ -75,13 +82,13 @@ def register_script_tools(mcp: FastMCP): 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}") @@ -90,7 +97,7 @@ def register_script_tools(mcp: FastMCP): # 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}") - + # Send command to Unity to create the script directly # The C# handler will handle the file existence check params = { @@ -98,17 +105,17 @@ def register_script_tools(mcp: FastMCP): "script_type": script_type, "namespace": namespace, "template": template, - "overwrite": overwrite + "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: @@ -120,59 +127,58 @@ def register_script_tools(mcp: FastMCP): script_path: str, content: str, create_if_missing: bool = False, - create_folder_if_missing: bool = False + create_folder_if_missing: bool = False, ) -> str: """Update the contents of an existing Unity script. - + Args: 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() - + # 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] 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, # and let Unity handle the creation if needed params = { "script_path": script_path, "content": content, - "create_if_missing": True + "create_if_missing": True, } - + # Add folder creation flag if requested if create_folder_if_missing: params["create_folder_if_missing"] = True - + # Send command to Unity to update/create the script response = unity.send_command("UPDATE_SCRIPT", params) return response.get("message", "Script updated successfully") else: # Standard update without creation flags - response = unity.send_command("UPDATE_SCRIPT", { - "script_path": script_path, - "content": content - }) + response = unity.send_command( + "UPDATE_SCRIPT", {"script_path": script_path, "content": content} + ) return response.get("message", "Script updated successfully") except Exception as e: return f"Error updating script: {str(e)}" @@ -180,19 +186,19 @@ def register_script_tools(mcp: FastMCP): @mcp.tool() def list_scripts(ctx: Context, folder_path: str = "Assets") -> str: """List all script files in a specified folder. - + Args: ctx: The MCP context folder_path: Path to the folder to search (default: Assets) - + Returns: str: List of script files or error message """ try: # Send command to Unity to list scripts - response = get_unity_connection().send_command("LIST_SCRIPTS", { - "folder_path": folder_path - }) + response = get_unity_connection().send_command( + "LIST_SCRIPTS", {"folder_path": folder_path} + ) scripts = response.get("scripts", []) if not scripts: return "No scripts found in the specified folder" @@ -202,79 +208,73 @@ def register_script_tools(mcp: FastMCP): @mcp.tool() def attach_script( - ctx: Context, - object_name: str, - script_name: str, - script_path: str = None + ctx: Context, object_name: str, script_name: str, script_path: str = None ) -> str: """Attach a script component to a GameObject. - + Args: 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 - }) - + 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 + + # Ensure script_name has .cs extension if not script_name.lower().endswith(".cs"): script_name = f"{script_name}.cs" - + # Remove any path information from script_name if it contains slashes - script_basename = script_name.split('/')[-1] - + 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 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 - }) - + object_props = unity.send_command( + "GET_OBJECT_PROPERTIES", {"name": object_name} + ) + # 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", []) 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 - params = { - "object_name": object_name, - "script_name": script_basename - } - + params = {"object_name": object_name, "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 + return f"Error attaching script: {str(e)}"