From 47bb2c63633eb33cc43404c66615836ba1aae43a Mon Sep 17 00:00:00 2001 From: David Sarno Date: Sun, 13 Apr 2025 10:38:53 -0700 Subject: [PATCH 01/32] docs: Add documentation for get_components action in manage_gameobject - Add get_components to the list of available actions in the docstring - Document required and optional parameters for get_components action - Clarify the return value structure for get_components - Improve overall documentation clarity for component data retrieval --- UnityMcpServer/src/tools/manage_gameobject.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/UnityMcpServer/src/tools/manage_gameobject.py b/UnityMcpServer/src/tools/manage_gameobject.py index 0f4c9bf..83ab9c7 100644 --- a/UnityMcpServer/src/tools/manage_gameobject.py +++ b/UnityMcpServer/src/tools/manage_gameobject.py @@ -40,7 +40,7 @@ def register_manage_gameobject_tools(mcp: FastMCP): """Manages GameObjects: create, modify, delete, find, and component operations. Args: - action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property'). + action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property', 'get_components'). target: GameObject identifier (name or path string) for modify/delete/component actions. search_method: How to find objects ('by_name', 'by_id', 'by_path', etc.). Used with 'find' and some 'target' lookups. name: GameObject name - used for both 'create' (initial name) and 'modify' (rename). @@ -62,8 +62,16 @@ def register_manage_gameobject_tools(mcp: FastMCP): search_term, find_all for 'find'). includeNonPublicSerialized: If True, includes private fields marked [SerializeField] in component data. + Action-specific details: + - For 'get_components': + Required: target, search_method + Optional: includeNonPublicSerialized (defaults to True) + Returns all components on the target GameObject with their serialized data. + The search_method parameter determines how to find the target ('by_name', 'by_id', 'by_path'). + Returns: Dictionary with operation results ('success', 'message', 'data'). + For 'get_components', the 'data' field contains a dictionary of component names and their serialized properties. """ try: # --- Early check for attempting to modify a prefab asset --- From 649599fcfe3fdc9c44c63996a03299d352c047d3 Mon Sep 17 00:00:00 2001 From: "toma.tanigawa" Date: Fri, 18 Apr 2025 20:23:41 +0900 Subject: [PATCH 02/32] Add: Shader management tool --- UnityMcpServer/src/server.py | 3 +- UnityMcpServer/src/tools/__init__.py | 2 + UnityMcpServer/src/tools/manage_shader.py | 67 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 UnityMcpServer/src/tools/manage_shader.py diff --git a/UnityMcpServer/src/server.py b/UnityMcpServer/src/server.py index 90d0c72..55360b5 100644 --- a/UnityMcpServer/src/server.py +++ b/UnityMcpServer/src/server.py @@ -61,7 +61,8 @@ def asset_creation_strategy() -> str: "- `manage_scene`: Manages scenes.\\n" "- `manage_gameobject`: Manages GameObjects in the scene.\\n" "- `manage_script`: Manages C# script files.\\n" - "- `manage_asset`: Manages prefabs and assets.\\n\\n" + "- `manage_asset`: Manages prefabs and assets.\\n" + "- `manage_shader`: Manages shaders.\\n\\n" "Tips:\\n" "- Create prefabs for reusable GameObjects.\\n" "- Always include a camera and main light in your scenes.\\n" diff --git a/UnityMcpServer/src/tools/__init__.py b/UnityMcpServer/src/tools/__init__.py index 8cfc38e..4d8d63c 100644 --- a/UnityMcpServer/src/tools/__init__.py +++ b/UnityMcpServer/src/tools/__init__.py @@ -3,6 +3,7 @@ from .manage_scene import register_manage_scene_tools from .manage_editor import register_manage_editor_tools from .manage_gameobject import register_manage_gameobject_tools from .manage_asset import register_manage_asset_tools +from .manage_shader import register_manage_shader_tools from .read_console import register_read_console_tools from .execute_menu_item import register_execute_menu_item_tools @@ -14,6 +15,7 @@ def register_all_tools(mcp): register_manage_editor_tools(mcp) register_manage_gameobject_tools(mcp) register_manage_asset_tools(mcp) + register_manage_shader_tools(mcp) register_read_console_tools(mcp) register_execute_menu_item_tools(mcp) print("Unity MCP Server tool registration complete.") diff --git a/UnityMcpServer/src/tools/manage_shader.py b/UnityMcpServer/src/tools/manage_shader.py new file mode 100644 index 0000000..c447a3a --- /dev/null +++ b/UnityMcpServer/src/tools/manage_shader.py @@ -0,0 +1,67 @@ +from mcp.server.fastmcp import FastMCP, Context +from typing import Dict, Any +from unity_connection import get_unity_connection +import os +import base64 + +def register_manage_shader_tools(mcp: FastMCP): + """Register all shader script management tools with the MCP server.""" + + @mcp.tool() + def manage_shader( + ctx: Context, + action: str, + name: str, + path: str, + contents: str, + ) -> Dict[str, Any]: + """Manages shader scripts in Unity (create, read, update, delete). + + Args: + action: Operation ('create', 'read', 'update', 'delete'). + name: Shader name (no .cs extension). + path: Asset path (default: "Assets/"). + contents: Shader code for 'create'/'update'. + + Returns: + Dictionary with results ('success', 'message', 'data'). + """ + try: + # Prepare parameters for Unity + params = { + "action": action, + "name": name, + "path": path, + } + + # Base64 encode the contents if they exist to avoid JSON escaping issues + if contents is not None: + if action in ['create', 'update']: + # Encode content for safer transmission + params["encodedContents"] = base64.b64encode(contents.encode('utf-8')).decode('utf-8') + params["contentsEncoded"] = True + else: + params["contents"] = contents + + # Remove None values so they don't get sent as null + params = {k: v for k, v in params.items() if v is not None} + + # Send command to Unity + response = get_unity_connection().send_command("manage_shader", params) + + # Process response from Unity + if response.get("success"): + # If the response contains base64 encoded content, decode it + if response.get("data", {}).get("contentsEncoded"): + decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8') + response["data"]["contents"] = decoded_contents + del response["data"]["encodedContents"] + del response["data"]["contentsEncoded"] + + return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} + else: + return {"success": False, "message": response.get("error", "An unknown error occurred.")} + + except Exception as e: + # Handle Python-side errors (e.g., connection issues) + return {"success": False, "message": f"Python error managing shader: {str(e)}"} \ No newline at end of file From d4bd504e3167e71f4c067e8c6fb6845674ba4b3c Mon Sep 17 00:00:00 2001 From: "toma.tanigawa" Date: Fri, 18 Apr 2025 20:24:46 +0900 Subject: [PATCH 03/32] Add: MCP Bridge part of shader management tool --- UnityMcpBridge/Editor/Tools/ManageShader.cs | 325 ++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 UnityMcpBridge/Editor/Tools/ManageShader.cs diff --git a/UnityMcpBridge/Editor/Tools/ManageShader.cs b/UnityMcpBridge/Editor/Tools/ManageShader.cs new file mode 100644 index 0000000..49bbf15 --- /dev/null +++ b/UnityMcpBridge/Editor/Tools/ManageShader.cs @@ -0,0 +1,325 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using UnityMcpBridge.Editor.Helpers; + +namespace UnityMcpBridge.Editor.Tools +{ + /// + /// Handles CRUD operations for shader files within the Unity project. + /// + public static class ManageShader + { + /// + /// Main handler for shader management actions. + /// + public static object HandleCommand(JObject @params) + { + // Extract parameters + string action = @params["action"]?.ToString().ToLower(); + string name = @params["name"]?.ToString(); + string path = @params["path"]?.ToString(); // Relative to Assets/ + string contents = null; + + // Check if we have base64 encoded contents + bool contentsEncoded = @params["contentsEncoded"]?.ToObject() ?? false; + if (contentsEncoded && @params["encodedContents"] != null) + { + try + { + contents = DecodeBase64(@params["encodedContents"].ToString()); + } + catch (Exception e) + { + return Response.Error($"Failed to decode shader contents: {e.Message}"); + } + } + else + { + contents = @params["contents"]?.ToString(); + } + + // Validate required parameters + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required."); + } + if (string.IsNullOrEmpty(name)) + { + return Response.Error("Name parameter is required."); + } + // Basic name validation (alphanumeric, underscores, cannot start with number) + if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$")) + { + return Response.Error( + $"Invalid shader name: '{name}'. Use only letters, numbers, underscores, and don't start with a number." + ); + } + + // Ensure path is relative to Assets/, removing any leading "Assets/" + // Set default directory to "Shaders" if path is not provided + string relativeDir = path ?? "Shaders"; // Default to "Shaders" if path is null + if (!string.IsNullOrEmpty(relativeDir)) + { + relativeDir = relativeDir.Replace('\\', '/').Trim('/'); + if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/'); + } + } + // Handle empty string case explicitly after processing + if (string.IsNullOrEmpty(relativeDir)) + { + relativeDir = "Shaders"; // Ensure default if path was provided as "" or only "/" or "Assets/" + } + + // Construct paths + string shaderFileName = $"{name}.shader"; + string fullPathDir = Path.Combine(Application.dataPath, relativeDir); + string fullPath = Path.Combine(fullPathDir, shaderFileName); + string relativePath = Path.Combine("Assets", relativeDir, shaderFileName) + .Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes + + // Ensure the target directory exists for create/update + if (action == "create" || action == "update") + { + try + { + Directory.CreateDirectory(fullPathDir); + } + catch (Exception e) + { + return Response.Error( + $"Could not create directory '{fullPathDir}': {e.Message}" + ); + } + } + + // Route to specific action handlers + switch (action) + { + case "create": + return CreateShader(fullPath, relativePath, name, contents); + case "read": + return ReadShader(fullPath, relativePath); + case "update": + return UpdateShader(fullPath, relativePath, name, contents); + case "delete": + return DeleteShader(fullPath, relativePath); + default: + return Response.Error( + $"Unknown action: '{action}'. Valid actions are: create, read, update, delete." + ); + } + } + + /// + /// Decode base64 string to normal text + /// + private static string DecodeBase64(string encoded) + { + byte[] data = Convert.FromBase64String(encoded); + return System.Text.Encoding.UTF8.GetString(data); + } + + /// + /// Encode text to base64 string + /// + private static string EncodeBase64(string text) + { + byte[] data = System.Text.Encoding.UTF8.GetBytes(text); + return Convert.ToBase64String(data); + } + + private static object CreateShader( + string fullPath, + string relativePath, + string name, + string contents + ) + { + // Check if shader already exists + if (File.Exists(fullPath)) + { + return Response.Error( + $"Shader already exists at '{relativePath}'. Use 'update' action to modify." + ); + } + + // Generate default content if none provided + if (string.IsNullOrEmpty(contents)) + { + contents = GenerateDefaultShaderContent(name); + } + + try + { + File.WriteAllText(fullPath, contents); + AssetDatabase.ImportAsset(relativePath); + AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader + return Response.Success( + $"Shader '{name}.shader' created successfully at '{relativePath}'.", + new { path = relativePath } + ); + } + catch (Exception e) + { + return Response.Error($"Failed to create shader '{relativePath}': {e.Message}"); + } + } + + private static object ReadShader(string fullPath, string relativePath) + { + if (!File.Exists(fullPath)) + { + return Response.Error($"Shader not found at '{relativePath}'."); + } + + try + { + string contents = File.ReadAllText(fullPath); + + // Return both normal and encoded contents for larger files + bool isLarge = contents.Length > 10000; // If content is large, include encoded version + var responseData = new + { + path = relativePath, + contents = contents, + // For large files, also include base64-encoded version + encodedContents = isLarge ? EncodeBase64(contents) : null, + contentsEncoded = isLarge, + }; + + return Response.Success( + $"Shader '{Path.GetFileName(relativePath)}' read successfully.", + responseData + ); + } + catch (Exception e) + { + return Response.Error($"Failed to read shader '{relativePath}': {e.Message}"); + } + } + + private static object UpdateShader( + string fullPath, + string relativePath, + string name, + string contents + ) + { + if (!File.Exists(fullPath)) + { + return Response.Error( + $"Shader not found at '{relativePath}'. Use 'create' action to add a new shader." + ); + } + if (string.IsNullOrEmpty(contents)) + { + return Response.Error("Content is required for the 'update' action."); + } + + try + { + File.WriteAllText(fullPath, contents); + AssetDatabase.ImportAsset(relativePath); + return Response.Success( + $"Shader '{Path.GetFileName(relativePath)}' updated successfully.", + new { path = relativePath } + ); + } + catch (Exception e) + { + return Response.Error($"Failed to update shader '{relativePath}': {e.Message}"); + } + } + + private static object DeleteShader(string fullPath, string relativePath) + { + if (!File.Exists(fullPath)) + { + return Response.Error($"Shader not found at '{relativePath}'."); + } + + try + { + // Delete the asset through Unity's AssetDatabase first + bool success = AssetDatabase.DeleteAsset(relativePath); + if (!success) + { + return Response.Error($"Failed to delete shader through Unity's AssetDatabase: '{relativePath}'"); + } + + // If the file still exists (rare case), try direct deletion + if (File.Exists(fullPath)) + { + File.Delete(fullPath); + } + + return Response.Success($"Shader '{Path.GetFileName(relativePath)}' deleted successfully."); + } + catch (Exception e) + { + return Response.Error($"Failed to delete shader '{relativePath}': {e.Message}"); + } + } + + private static string GenerateDefaultShaderContent(string name) + { + return @"Shader """ + name + @""" +{ + Properties + { + _MainTex (""Texture"", 2D) = ""white"" {} + } + SubShader + { + Tags { ""RenderType""=""Opaque"" } + LOD 100 + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #include ""UnityCG.cginc"" + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + }; + + struct v2f + { + float2 uv : TEXCOORD0; + float4 vertex : SV_POSITION; + }; + + sampler2D _MainTex; + float4 _MainTex_ST; + + v2f vert (appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = TRANSFORM_TEX(v.uv, _MainTex); + return o; + } + + fixed4 frag (v2f i) : SV_Target + { + fixed4 col = tex2D(_MainTex, i.uv); + return col; + } + ENDCG + } + } +}"; + } + } +} \ No newline at end of file From 738c3a2a90160d7d98799f88df666fd3ee346da8 Mon Sep 17 00:00:00 2001 From: Michael Nugent Date: Fri, 18 Apr 2025 20:47:17 -0400 Subject: [PATCH 04/32] dockerfile --- UnityMcpServer/src/Dockerfile | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 UnityMcpServer/src/Dockerfile diff --git a/UnityMcpServer/src/Dockerfile b/UnityMcpServer/src/Dockerfile new file mode 100644 index 0000000..3f884f3 --- /dev/null +++ b/UnityMcpServer/src/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.12-slim + +# Install required system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Install uv package manager +RUN pip install uv + +# Copy required files +COPY config.py /app/ +COPY server.py /app/ +COPY unity_connection.py /app/ +COPY pyproject.toml /app/ +COPY __init__.py /app/ +COPY tools/ /app/tools/ + +# Install dependencies using uv +RUN uv pip install --system -e . + + +# Command to run the server +CMD ["uv", "run", "server.py"] \ No newline at end of file From 619172ad9937fccd65c8346b52f3de66c6bdc27f Mon Sep 17 00:00:00 2001 From: xsodus Date: Mon, 12 May 2025 08:25:21 +0700 Subject: [PATCH 05/32] Add VSCode GitHub Copilot support and configuration window - Updated McpTypes to include VSCode. - Enhanced McpClients to handle VSCode configuration paths. - Implemented VSCodeManualSetupWindow for user instructions and JSON configuration. - Modified UnityMcpEditorWindow to support VSCode-specific setup and configuration. - Updated .gitignore to include .DS_Store files. --- .gitignore | 1 + UnityMcpBridge/Editor/Data/McpClients.cs | 20 ++ UnityMcpBridge/Editor/Models/McpTypes.cs | 1 + .../Editor/Windows/UnityMcpEditorWindow.cs | 237 ++++++++++---- .../Editor/Windows/VSCodeManualSetupWindow.cs | 297 ++++++++++++++++++ 5 files changed, 501 insertions(+), 55 deletions(-) create mode 100644 UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs diff --git a/.gitignore b/.gitignore index 75612dc..f1d2443 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ CONTRIBUTING.md.meta .idea/ .vscode/ .aider* +.DS_Store* \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Data/McpClients.cs b/UnityMcpBridge/Editor/Data/McpClients.cs index d370df8..2360ab1 100644 --- a/UnityMcpBridge/Editor/Data/McpClients.cs +++ b/UnityMcpBridge/Editor/Data/McpClients.cs @@ -43,6 +43,26 @@ namespace UnityMcpBridge.Editor.Data mcpType = McpTypes.Cursor, configStatus = "Not Configured", }, + new() + { + name = "VSCode GitHub Copilot", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Code", + "User", + "settings.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", + "Application Support", + "Code", + "User", + "settings.json" + ), + mcpType = McpTypes.VSCode, + configStatus = "Not Configured", + }, }; // Initialize status enums after construction diff --git a/UnityMcpBridge/Editor/Models/McpTypes.cs b/UnityMcpBridge/Editor/Models/McpTypes.cs index 913ed47..0259b9c 100644 --- a/UnityMcpBridge/Editor/Models/McpTypes.cs +++ b/UnityMcpBridge/Editor/Models/McpTypes.cs @@ -4,6 +4,7 @@ namespace UnityMcpBridge.Editor.Models { ClaudeDesktop, Cursor, + VSCode, } } diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index c794fb7..1971bc1 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -137,24 +137,74 @@ namespace UnityMcpBridge.Editor.Windows // Create muted button style for Manual Setup GUIStyle mutedButtonStyle = new(buttonStyle); - if ( - GUILayout.Button( - $"Auto Configure {mcpClient.name}", - buttonStyle, - GUILayout.Height(28) + if (mcpClient.mcpType == McpTypes.VSCode) + { + // Special handling for VSCode GitHub Copilot + if ( + GUILayout.Button( + "Auto Configure VSCode with GitHub Copilot", + buttonStyle, + GUILayout.Height(28) + ) ) - ) - { - ConfigureMcpClient(mcpClient); - } + { + ConfigureMcpClient(mcpClient); + } - if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28))) + if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28))) + { + // Show VSCode specific manual setup window + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? mcpClient.windowsConfigPath + : mcpClient.linuxConfigPath; + + // Get the Python directory path + string pythonDir = FindPackagePythonDirectory(); + + // Create VSCode-specific configuration + var vscodeConfig = new + { + mcp = new + { + servers = new + { + UnityMCP = new + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" } + } + } + } + }; + + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + + VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); + } + } + else { - // Get the appropriate config path based on OS - string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? mcpClient.windowsConfigPath - : mcpClient.linuxConfigPath; - ShowManualInstructionsWindow(configPath, mcpClient); + // Standard client buttons + if ( + GUILayout.Button( + $"Auto Configure {mcpClient.name}", + buttonStyle, + GUILayout.Height(28) + ) + ) + { + ConfigureMcpClient(mcpClient); + } + + if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28))) + { + // Get the appropriate config path based on OS + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? mcpClient.windowsConfigPath + : mcpClient.linuxConfigPath; + ShowManualInstructionsWindow(configPath, mcpClient); + } } EditorGUILayout.Space(5); @@ -274,7 +324,7 @@ namespace UnityMcpBridge.Editor.Windows isUnityBridgeRunning = !isUnityBridgeRunning; } - private string WriteToConfig(string pythonDir, string configPath) + private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null) { // Create configuration object for unityMCP McpConfigServer unityMCPConfig = new() @@ -303,14 +353,36 @@ namespace UnityMcpBridge.Editor.Windows dynamic existingConfig = JsonConvert.DeserializeObject(existingJson); existingConfig ??= new Newtonsoft.Json.Linq.JObject(); - // Ensure mcpServers object exists - if (existingConfig.mcpServers == null) + // Handle different client types with a switch statement + switch (mcpClient?.mcpType) { - existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject(); + case McpTypes.VSCode: + // VSCode specific configuration + // Ensure mcp object exists + if (existingConfig.mcp == null) + { + existingConfig.mcp = new Newtonsoft.Json.Linq.JObject(); + } + + // Ensure mcp.servers object exists + if (existingConfig.mcp.servers == null) + { + existingConfig.mcp.servers = new Newtonsoft.Json.Linq.JObject(); + } + break; + + default: + // Standard MCP configuration (Claude Desktop, Cursor, etc.) + // Ensure mcpServers object exists + if (existingConfig.mcpServers == null) + { + existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject(); + } + break; } - // Add/update unityMCP while preserving other servers - existingConfig.mcpServers.unityMCP = + // Add/update UnityMCP server in VSCode settings + existingConfig.mcp.servers.unityMCP = JsonConvert.DeserializeObject( JsonConvert.SerializeObject(unityMCPConfig) ); @@ -334,22 +406,47 @@ namespace UnityMcpBridge.Editor.Windows { // Get the Python directory path using Package Manager API string pythonDir = FindPackagePythonDirectory(); - - // Create the manual configuration message - McpConfig jsonConfig = new() + string manualConfigJson; + + if (mcpClient.mcpType == McpTypes.VSCode) { - mcpServers = new McpConfigServers + // Create VSCode-specific configuration with proper format + var vscodeConfig = new { - unityMCP = new McpConfigServer + mcp = new { - command = "uv", - args = new[] { "--directory", pythonDir, "run", "server.py" }, + servers = new + { + unityMCP = new + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" } + } + } + } + }; + + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + } + else + { + // Create standard MCP configuration for other clients + McpConfig jsonConfig = new() + { + mcpServers = new McpConfigServers + { + unityMCP = new McpConfigServer + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" }, + }, }, - }, - }; + }; - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); + } ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); } @@ -450,7 +547,7 @@ namespace UnityMcpBridge.Editor.Windows return "Manual Configuration Required"; } - string result = WriteToConfig(pythonDir, configPath); + string result = WriteToConfig(pythonDir, configPath, mcpClient); // Update the client status after successful configuration if (result == "Configured successfully") @@ -542,29 +639,59 @@ namespace UnityMcpBridge.Editor.Windows } string configJson = File.ReadAllText(configPath); - McpConfig config = JsonConvert.DeserializeObject(configJson); + string pythonDir = ServerInstaller.GetServerPath(); + + // Use switch statement to handle different client types + switch (mcpClient.mcpType) + { + case McpTypes.VSCode: + dynamic config = JsonConvert.DeserializeObject(configJson); + + if (config?.mcp?.servers?.unityMCP != null) + { + // Extract args from VSCode config format + var args = config.mcp.servers.unityMCP.args.ToObject(); + + if (pythonDir != null && + Array.Exists(args, arg => arg.Contains(pythonDir, StringComparison.Ordinal))) + { + mcpClient.SetStatus(McpStatus.Configured); + } + else + { + mcpClient.SetStatus(McpStatus.IncorrectPath); + } + } + else + { + mcpClient.SetStatus(McpStatus.MissingConfig); + } + break; + + default: + // Standard MCP configuration check for Claude Desktop, Cursor, etc. + McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); - if (config?.mcpServers?.unityMCP != null) - { - string pythonDir = ServerInstaller.GetServerPath(); - if ( - pythonDir != null - && Array.Exists( - config.mcpServers.unityMCP.args, - arg => arg.Contains(pythonDir, StringComparison.Ordinal) - ) - ) - { - mcpClient.SetStatus(McpStatus.Configured); - } - else - { - mcpClient.SetStatus(McpStatus.IncorrectPath); - } - } - else - { - mcpClient.SetStatus(McpStatus.MissingConfig); + if (standardConfig?.mcpServers?.unityMCP != null) + { + if (pythonDir != null + && Array.Exists( + standardConfig.mcpServers.unityMCP.args, + arg => arg.Contains(pythonDir, StringComparison.Ordinal) + )) + { + mcpClient.SetStatus(McpStatus.Configured); + } + else + { + mcpClient.SetStatus(McpStatus.IncorrectPath); + } + } + else + { + mcpClient.SetStatus(McpStatus.MissingConfig); + } + break; } } catch (Exception e) diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs new file mode 100644 index 0000000..bfdbe92 --- /dev/null +++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs @@ -0,0 +1,297 @@ +using System.Runtime.InteropServices; +using UnityEditor; +using UnityEngine; +using UnityMcpBridge.Editor.Models; + +namespace UnityMcpBridge.Editor.Windows +{ + public class VSCodeManualSetupWindow : EditorWindow + { + private string configPath; + private string configJson; + private Vector2 scrollPos; + private bool pathCopied = false; + private bool jsonCopied = false; + private float copyFeedbackTimer = 0; + + public static void ShowWindow(string configPath, string configJson) + { + VSCodeManualSetupWindow window = GetWindow("VSCode GitHub Copilot Setup"); + window.configPath = configPath; + window.configJson = configJson; + window.minSize = new Vector2(550, 500); + window.Show(); + } + + private void OnGUI() + { + scrollPos = EditorGUILayout.BeginScrollView(scrollPos); + + // Header with improved styling + EditorGUILayout.Space(10); + Rect titleRect = EditorGUILayout.GetControlRect(false, 30); + EditorGUI.DrawRect( + new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), + new Color(0.2f, 0.2f, 0.2f, 0.1f) + ); + GUI.Label( + new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height), + "VSCode GitHub Copilot MCP Setup", + EditorStyles.boldLabel + ); + EditorGUILayout.Space(10); + + // Instructions with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + Rect headerRect = EditorGUILayout.GetControlRect(false, 24); + EditorGUI.DrawRect( + new Rect(headerRect.x, headerRect.y, headerRect.width, headerRect.height), + new Color(0.1f, 0.1f, 0.1f, 0.2f) + ); + GUI.Label( + new Rect( + headerRect.x + 8, + headerRect.y + 4, + headerRect.width - 16, + headerRect.height + ), + "Setting up GitHub Copilot in VSCode with Unity MCP", + EditorStyles.boldLabel + ); + EditorGUILayout.Space(10); + + GUIStyle instructionStyle = new(EditorStyles.wordWrappedLabel) + { + margin = new RectOffset(10, 10, 5, 5), + }; + + EditorGUILayout.LabelField( + "1. Prerequisites", + EditorStyles.boldLabel + ); + EditorGUILayout.LabelField( + "• Ensure you have VSCode installed", + instructionStyle + ); + EditorGUILayout.LabelField( + "• Ensure you have GitHub Copilot extension installed in VSCode", + instructionStyle + ); + EditorGUILayout.LabelField( + "• Ensure you have a valid GitHub Copilot subscription", + instructionStyle + ); + EditorGUILayout.Space(5); + + EditorGUILayout.LabelField( + "2. Steps to Configure", + EditorStyles.boldLabel + ); + EditorGUILayout.LabelField( + "a) Open VSCode Settings (File > Preferences > Settings)", + instructionStyle + ); + EditorGUILayout.LabelField( + "b) Click on the 'Open Settings (JSON)' button in the top right", + instructionStyle + ); + EditorGUILayout.LabelField( + "c) Add the MCP configuration shown below to your settings.json file", + instructionStyle + ); + EditorGUILayout.LabelField( + "d) Save the file and restart VSCode", + instructionStyle + ); + EditorGUILayout.Space(5); + + EditorGUILayout.LabelField( + "3. VSCode settings.json location:", + EditorStyles.boldLabel + ); + + // Path section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + string displayPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + displayPath = System.IO.Path.Combine( + System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData), + "Code", + "User", + "settings.json" + ); + } + else + { + displayPath = System.IO.Path.Combine( + System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), + "Library", + "Application Support", + "Code", + "User", + "settings.json" + ); + } + + // Prevent text overflow by allowing the text field to wrap + GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true }; + + EditorGUILayout.TextField( + displayPath, + pathStyle, + GUILayout.Height(EditorGUIUtility.singleLineHeight) + ); + + // Copy button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUIStyle copyButtonStyle = new(GUI.skin.button) + { + padding = new RectOffset(15, 15, 5, 5), + margin = new RectOffset(10, 10, 5, 5), + }; + + if ( + GUILayout.Button( + "Copy Path", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + EditorGUIUtility.systemCopyBuffer = displayPath; + pathCopied = true; + copyFeedbackTimer = 2f; + } + + if ( + GUILayout.Button( + "Open File", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + // Open the file using the system's default application + System.Diagnostics.Process.Start( + new System.Diagnostics.ProcessStartInfo + { + FileName = displayPath, + UseShellExecute = true, + } + ); + } + + if (pathCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); + + EditorGUILayout.LabelField( + "4. Add this configuration to your settings.json:", + EditorStyles.boldLabel + ); + + // JSON section with improved styling + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Improved text area for JSON with syntax highlighting colors + GUIStyle jsonStyle = new(EditorStyles.textArea) + { + font = EditorStyles.boldFont, + wordWrap = true, + }; + jsonStyle.normal.textColor = new Color(0.3f, 0.6f, 0.9f); // Syntax highlighting blue + + // Draw the JSON in a text area with a taller height for better readability + EditorGUILayout.TextArea(configJson, jsonStyle, GUILayout.Height(200)); + + // Copy JSON button with improved styling + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + if ( + GUILayout.Button( + "Copy JSON", + copyButtonStyle, + GUILayout.Height(25), + GUILayout.Width(100) + ) + ) + { + EditorGUIUtility.systemCopyBuffer = configJson; + jsonCopied = true; + copyFeedbackTimer = 2f; + } + + if (jsonCopied) + { + GUIStyle feedbackStyle = new(EditorStyles.label); + feedbackStyle.normal.textColor = Color.green; + EditorGUILayout.LabelField("Copied!", feedbackStyle, GUILayout.Width(60)); + } + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField( + "5. After configuration:", + EditorStyles.boldLabel + ); + EditorGUILayout.LabelField( + "• Restart VSCode", + instructionStyle + ); + EditorGUILayout.LabelField( + "• GitHub Copilot will now be able to interact with your Unity project through the MCP protocol", + instructionStyle + ); + EditorGUILayout.LabelField( + "• Remember to have the Unity MCP Bridge running in Unity Editor", + instructionStyle + ); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(10); + + // Close button at the bottom + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Close", GUILayout.Height(30), GUILayout.Width(100))) + { + Close(); + } + GUILayout.FlexibleSpace(); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.EndScrollView(); + } + + private void Update() + { + // Handle the feedback message timer + if (copyFeedbackTimer > 0) + { + copyFeedbackTimer -= Time.deltaTime; + if (copyFeedbackTimer <= 0) + { + pathCopied = false; + jsonCopied = false; + Repaint(); + } + } + } + } +} From b4910be508b81139cee89399b478977659199bee Mon Sep 17 00:00:00 2001 From: xsodus Date: Mon, 12 May 2025 09:24:10 +0700 Subject: [PATCH 06/32] Refactor VSCode configuration to use lowercase 'unityMCP' and improve argument checking --- UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 1971bc1..45394f9 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -168,7 +168,7 @@ namespace UnityMcpBridge.Editor.Windows { servers = new { - UnityMCP = new + unityMCP = new { command = "uv", args = new[] { "--directory", pythonDir, "run", "server.py" } @@ -180,6 +180,7 @@ namespace UnityMcpBridge.Editor.Windows JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + // Use the VSCodeManualSetupWindow directly since we're in the same namespace VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); } } @@ -653,7 +654,7 @@ namespace UnityMcpBridge.Editor.Windows var args = config.mcp.servers.unityMCP.args.ToObject(); if (pythonDir != null && - Array.Exists(args, arg => arg.Contains(pythonDir, StringComparison.Ordinal))) + Array.Exists(args, new Predicate(arg => arg.Contains(pythonDir, StringComparison.Ordinal)))) { mcpClient.SetStatus(McpStatus.Configured); } From d63eb7d4e4cee485e65bc379659ad08273f1acc3 Mon Sep 17 00:00:00 2001 From: xsodus Date: Mon, 12 May 2025 09:25:01 +0700 Subject: [PATCH 07/32] Add metadata file for VSCodeManualSetupWindow with file format version and GUID --- UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta new file mode 100644 index 0000000..437ccab --- /dev/null +++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 377fe73d52cf0435fabead5f50a0d204 \ No newline at end of file From 861e6bc62cc387428ef7c3c78ca988f539a275fa Mon Sep 17 00:00:00 2001 From: xsodus Date: Mon, 12 May 2025 09:34:04 +0700 Subject: [PATCH 08/32] Refactor VSCodeManualSetupWindow to inherit from ManualConfigEditorWindow and override methods for improved functionality --- .../Windows/ManualConfigEditorWindow.cs | 18 +++---- .../Editor/Windows/VSCodeManualSetupWindow.cs | 52 +++++++++++-------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs index 17c93e0..07b7da3 100644 --- a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs @@ -8,13 +8,13 @@ namespace UnityMcpBridge.Editor.Windows // Editor window to display manual configuration instructions public class ManualConfigEditorWindow : EditorWindow { - private string configPath; - private string configJson; - private Vector2 scrollPos; - private bool pathCopied = false; - private bool jsonCopied = false; - private float copyFeedbackTimer = 0; - private McpClient mcpClient; + protected string configPath; + protected string configJson; + protected Vector2 scrollPos; + protected bool pathCopied = false; + protected bool jsonCopied = false; + protected float copyFeedbackTimer = 0; + protected McpClient mcpClient; public static void ShowWindow(string configPath, string configJson, McpClient mcpClient) { @@ -26,7 +26,7 @@ namespace UnityMcpBridge.Editor.Windows window.Show(); } - private void OnGUI() + protected virtual void OnGUI() { scrollPos = EditorGUILayout.BeginScrollView(scrollPos); @@ -245,7 +245,7 @@ namespace UnityMcpBridge.Editor.Windows EditorGUILayout.EndScrollView(); } - private void Update() + protected virtual void Update() { // Handle the feedback message timer if (copyFeedbackTimer > 0) diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs index bfdbe92..4eb2eb1 100644 --- a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs +++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs @@ -5,25 +5,34 @@ using UnityMcpBridge.Editor.Models; namespace UnityMcpBridge.Editor.Windows { - public class VSCodeManualSetupWindow : EditorWindow + public class VSCodeManualSetupWindow : ManualConfigEditorWindow { - private string configPath; - private string configJson; - private Vector2 scrollPos; - private bool pathCopied = false; - private bool jsonCopied = false; - private float copyFeedbackTimer = 0; - - public static void ShowWindow(string configPath, string configJson) + // Not defining fields that are inherited from ManualConfigEditorWindow: + // protected string configPath; + // protected string configJson; + // protected Vector2 scrollPos; + // protected bool pathCopied; + // protected bool jsonCopied; + // protected float copyFeedbackTimer; + // protected McpClient mcpClient; + public static new void ShowWindow(string configPath, string configJson) { - VSCodeManualSetupWindow window = GetWindow("VSCode GitHub Copilot Setup"); + var window = GetWindow("VSCode GitHub Copilot Setup"); window.configPath = configPath; window.configJson = configJson; window.minSize = new Vector2(550, 500); + + // Create a McpClient for VSCode + window.mcpClient = new McpClient + { + name = "VSCode GitHub Copilot", + mcpType = McpTypes.VSCode + }; + window.Show(); } - private void OnGUI() + protected override void OnGUI() { scrollPos = EditorGUILayout.BeginScrollView(scrollPos); @@ -135,6 +144,12 @@ namespace UnityMcpBridge.Editor.Windows ); } + // Store the path in the base class config path + if (string.IsNullOrEmpty(configPath)) + { + configPath = displayPath; + } + // Prevent text overflow by allowing the text field to wrap GUIStyle pathStyle = new(EditorStyles.textField) { wordWrap = true }; @@ -279,19 +294,10 @@ namespace UnityMcpBridge.Editor.Windows EditorGUILayout.EndScrollView(); } - private void Update() + protected override void Update() { - // Handle the feedback message timer - if (copyFeedbackTimer > 0) - { - copyFeedbackTimer -= Time.deltaTime; - if (copyFeedbackTimer <= 0) - { - pathCopied = false; - jsonCopied = false; - Repaint(); - } - } + // Call the base implementation which handles the copy feedback timer + base.Update(); } } } From 48a865d3862d5f7f029f46ec2a5ba71a411ad54f Mon Sep 17 00:00:00 2001 From: xsodus Date: Mon, 12 May 2025 09:34:27 +0700 Subject: [PATCH 09/32] Remove redundant field definitions inherited from ManualConfigEditorWindow in VSCodeManualSetupWindow --- UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs index 4eb2eb1..a0b78e2 100644 --- a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs +++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs @@ -7,14 +7,6 @@ namespace UnityMcpBridge.Editor.Windows { public class VSCodeManualSetupWindow : ManualConfigEditorWindow { - // Not defining fields that are inherited from ManualConfigEditorWindow: - // protected string configPath; - // protected string configJson; - // protected Vector2 scrollPos; - // protected bool pathCopied; - // protected bool jsonCopied; - // protected float copyFeedbackTimer; - // protected McpClient mcpClient; public static new void ShowWindow(string configPath, string configJson) { var window = GetWindow("VSCode GitHub Copilot Setup"); From a880bf485b16512243d5e8b946030090dd49d251 Mon Sep 17 00:00:00 2001 From: xsodus Date: Mon, 12 May 2025 15:05:56 +0700 Subject: [PATCH 10/32] Refactor configuration handling in UnityMcpEditorWindow to streamline JSON serialization and improve client type management --- .../Editor/Windows/UnityMcpEditorWindow.cs | 120 +++++++++--------- 1 file changed, 58 insertions(+), 62 deletions(-) diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 45394f9..bf11f1f 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -409,44 +409,46 @@ namespace UnityMcpBridge.Editor.Windows string pythonDir = FindPackagePythonDirectory(); string manualConfigJson; - if (mcpClient.mcpType == McpTypes.VSCode) + // Create common JsonSerializerSettings + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + + // Use switch statement to handle different client types + switch (mcpClient.mcpType) { - // Create VSCode-specific configuration with proper format - var vscodeConfig = new - { - mcp = new + case McpTypes.VSCode: + // Create VSCode-specific configuration with proper format + var vscodeConfig = new { - servers = new + mcp = new { - unityMCP = new + servers = new { - command = "uv", - args = new[] { "--directory", pythonDir, "run", "server.py" } + unityMCP = new + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" } + } } } - } - }; - - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); - } - else - { - // Create standard MCP configuration for other clients - McpConfig jsonConfig = new() - { - mcpServers = new McpConfigServers + }; + manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + break; + + default: + // Create standard MCP configuration for other clients + McpConfig jsonConfig = new() { - unityMCP = new McpConfigServer + mcpServers = new McpConfigServers { - command = "uv", - args = new[] { "--directory", pythonDir, "run", "server.py" }, + unityMCP = new McpConfigServer + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" }, + }, }, - }, - }; - - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); + }; + manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); + break; } ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); @@ -642,7 +644,10 @@ namespace UnityMcpBridge.Editor.Windows string configJson = File.ReadAllText(configPath); string pythonDir = ServerInstaller.GetServerPath(); - // Use switch statement to handle different client types + // Use switch statement to handle different client types, extracting common logic + string[] args = null; + bool configExists = false; + switch (mcpClient.mcpType) { case McpTypes.VSCode: @@ -651,49 +656,40 @@ namespace UnityMcpBridge.Editor.Windows if (config?.mcp?.servers?.unityMCP != null) { // Extract args from VSCode config format - var args = config.mcp.servers.unityMCP.args.ToObject(); - - if (pythonDir != null && - Array.Exists(args, new Predicate(arg => arg.Contains(pythonDir, StringComparison.Ordinal)))) - { - mcpClient.SetStatus(McpStatus.Configured); - } - else - { - mcpClient.SetStatus(McpStatus.IncorrectPath); - } - } - else - { - mcpClient.SetStatus(McpStatus.MissingConfig); + args = config.mcp.servers.unityMCP.args.ToObject(); + configExists = true; } break; default: // Standard MCP configuration check for Claude Desktop, Cursor, etc. McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); - + if (standardConfig?.mcpServers?.unityMCP != null) { - if (pythonDir != null - && Array.Exists( - standardConfig.mcpServers.unityMCP.args, - arg => arg.Contains(pythonDir, StringComparison.Ordinal) - )) - { - mcpClient.SetStatus(McpStatus.Configured); - } - else - { - mcpClient.SetStatus(McpStatus.IncorrectPath); - } - } - else - { - mcpClient.SetStatus(McpStatus.MissingConfig); + args = standardConfig.mcpServers.unityMCP.args; + configExists = true; } break; } + + // Common logic for checking configuration status + if (configExists) + { + if (pythonDir != null && + Array.Exists(args, arg => arg.Contains(pythonDir, StringComparison.Ordinal))) + { + mcpClient.SetStatus(McpStatus.Configured); + } + else + { + mcpClient.SetStatus(McpStatus.IncorrectPath); + } + } + else + { + mcpClient.SetStatus(McpStatus.MissingConfig); + } } catch (Exception e) { From d1249cb28116be2f46042a53575d9efefea32112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=AD=E9=B9=A4?= Date: Thu, 22 May 2025 17:23:12 +0800 Subject: [PATCH 11/32] feat: add support for creating PhysicsMaterial assets --- UnityMcpBridge/Editor/Tools/ManageAsset.cs | 76 ++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index 7a0dad7..35df474 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -177,6 +177,14 @@ namespace UnityMcpBridge.Editor.Tools AssetDatabase.CreateAsset(mat, fullPath); newAsset = mat; } + else if (lowerAssetType == "physicsmaterial") + { + PhysicsMaterial pmat = new PhysicsMaterial(); + if (properties != null) + ApplyPhysicsMaterialProperties(pmat, properties); + AssetDatabase.CreateAsset(pmat, fullPath); + newAsset = pmat; + } else if (lowerAssetType == "scriptableobject") { string scriptClassName = properties?["scriptClass"]?.ToString(); @@ -948,6 +956,74 @@ namespace UnityMcpBridge.Editor.Tools return modified; } + private static bool ApplyPhysicsMaterialProperties(PhysicsMaterial pmat, JObject properties) + { + if (pmat == null || properties == null) + return false; + bool modified = false; + + // Example: Set dynamic friction + if (properties["DynamicFriction"]?.Type == JTokenType.Float) + { + float dynamicFriction = properties["DynamicFriction"].ToObject(); + pmat.dynamicFriction = dynamicFriction; + modified = true; + } + + // Example: Set static friction + if (properties["StaticFriction"]?.Type == JTokenType.Float) + { + float staticFriction = properties["StaticFriction"].ToObject(); + pmat.staticFriction = staticFriction; + modified = true; + } + + // Example: Set bounciness + if (properties["Bounciness"]?.Type == JTokenType.Float) + { + float bounciness = properties["Bounciness"].ToObject(); + pmat.bounciness = bounciness; + modified = true; + } + + List averageList = new List{"ave", "Ave", "average", "Average"}; + List multiplyList = new List{"mul", "Mul", "mult", "Mult", "multiply", "Multiply"}; + List minimumList = new List{"min", "Min", "minimum", "Minimum"}; + List maximumList = new List{"max", "Max", "maximum", "Maximum"}; + + // Example: Set friction combine + if (properties["FrictionCombine"]?.Type == JTokenType.String) + { + string frictionCombine = properties["FrictionCombine"].ToString(); + if (averageList.Contains(frictionCombine)) + pmat.frictionCombine = PhysicsMaterialCombine.Average; + else if (multiplyList.Contains(frictionCombine)) + pmat.frictionCombine = PhysicsMaterialCombine.Multiply; + else if (minimumList.Contains(frictionCombine)) + pmat.frictionCombine = PhysicsMaterialCombine.Minimum; + else if (maximumList.Contains(frictionCombine)) + pmat.frictionCombine = PhysicsMaterialCombine.Maximum; + modified = true; + } + + // Example: Set bounce combine + if (properties["BounceCombine"]?.Type == JTokenType.String) + { + string bounceCombine = properties["BounceCombine"].ToString(); + if (averageList.Contains(bounceCombine)) + pmat.bounceCombine = PhysicsMaterialCombine.Average; + else if (multiplyList.Contains(bounceCombine)) + pmat.bounceCombine = PhysicsMaterialCombine.Multiply; + else if (minimumList.Contains(bounceCombine)) + pmat.bounceCombine = PhysicsMaterialCombine.Minimum; + else if (maximumList.Contains(bounceCombine)) + pmat.bounceCombine = PhysicsMaterialCombine.Maximum; + modified = true; + } + + return modified; + } + /// /// Generic helper to set properties on any UnityEngine.Object using reflection. /// From 77ed43bac531a87dbaa76987acff3a767a7dc740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=AD=E9=B9=A4?= Date: Thu, 22 May 2025 19:48:16 +0800 Subject: [PATCH 12/32] docs: add summary for ApplyPhysicsMaterialProperties method --- UnityMcpBridge/Editor/Tools/ManageAsset.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index 35df474..aeeb08e 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -956,6 +956,9 @@ namespace UnityMcpBridge.Editor.Tools return modified; } + /// + /// Applies properties from JObject to a PhysicsMaterial. + /// private static bool ApplyPhysicsMaterialProperties(PhysicsMaterial pmat, JObject properties) { if (pmat == null || properties == null) From 85c947a34ecb3e1ea9510c5b2d41f8b67e900362 Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Sun, 6 Jul 2025 19:51:12 -0400 Subject: [PATCH 13/32] Minor Bug Fix 1. Solve the IDE/Unity miscommunication for the _Color and menuPath params 2. TODOs: Fix readme, look into more issues, bring back tool dev tutorial, view pull request and set a future roadmap --- .../Editor/Tools/ExecuteMenuItem.cs | 6 +++-- UnityMcpBridge/Editor/Tools/ManageAsset.cs | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs b/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs index b5de165..18b4f04 100644 --- a/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs +++ b/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs @@ -66,13 +66,15 @@ namespace UnityMcpBridge.Editor.Tools /// private static object ExecuteItem(JObject @params) { - string menuPath = @params["menu_path"]?.ToString(); + // Try both naming conventions: snake_case and camelCase + string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString(); + // string alias = @params["alias"]?.ToString(); // TODO: Implement alias mapping based on refactor plan requirements. // JObject parameters = @params["parameters"] as JObject; // TODO: Investigate parameter passing (often not directly supported by ExecuteMenuItem). if (string.IsNullOrWhiteSpace(menuPath)) { - return Response.Error("Required parameter 'menu_path' is missing or empty."); + return Response.Error("Required parameter 'menu_path' or 'menuPath' is missing or empty."); } // Validate against blacklist diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index 7a0dad7..84af347 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -891,6 +891,29 @@ namespace UnityMcpBridge.Editor.Tools ); } } + } else if (properties["_Color"] is JArray colorArr) //Current Prevention for systems that use _Color instead of color + { + try { + if (colorArr.Count >= 3) + { + Color newColor = new Color( + colorArr[0].ToObject(), + colorArr[1].ToObject(), + colorArr[2].ToObject(), + colorArr.Count > 3 ? colorArr[3].ToObject() : 1.0f + ); + if (mat.HasProperty(propName) && mat.GetColor(propName) != newColor) + { + mat.SetColor(propName, newColor); + modified = true; + } + } + } + catch (Exception ex) { + Debug.LogWarning( + $"Error parsing color property '{propName}': {ex.Message}" + ); + } } // Example: Set float property if (properties["float"] is JObject floatProps) From 1e8862aa4bb80a557fe0ed6ad7e89ce4c0fe4071 Mon Sep 17 00:00:00 2001 From: Shutong Wu <1300285021@qq.com> Date: Mon, 7 Jul 2025 15:43:34 -0400 Subject: [PATCH 14/32] Update ManageAsset.cs --- UnityMcpBridge/Editor/Tools/ManageAsset.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index 84af347..527ef88 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -893,6 +893,7 @@ namespace UnityMcpBridge.Editor.Tools } } else if (properties["_Color"] is JArray colorArr) //Current Prevention for systems that use _Color instead of color { + string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color try { if (colorArr.Count >= 3) { From 7b3b562c729e79d562da88f1ba40c7cd2a85f1bf Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Tue, 8 Jul 2025 02:37:08 -0400 Subject: [PATCH 15/32] Update ManageAsset.cs Silly mistake I made with a rush to fix the previous bug, will be more cautious and run through the test in the future. --- UnityMcpBridge/Editor/Tools/ManageAsset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index 527ef88..63d8f0b 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -893,7 +893,7 @@ namespace UnityMcpBridge.Editor.Tools } } else if (properties["_Color"] is JArray colorArr) //Current Prevention for systems that use _Color instead of color { - string propName = colorProps["name"]?.ToString() ?? "_Color"; // Default main color + string propName = "_Color"; // Default main color try { if (colorArr.Count >= 3) { From 21fbac60c21d376af9f788dff8d002aaed529aba Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Sun, 13 Jul 2025 14:33:06 -0400 Subject: [PATCH 16/32] Update for PhysicsMaterial 1. Fixing to ensure both version of PhysicMaterial works, editing PhysicsMaterial properties to camelCase 2. Add example output on server --- UnityMcpBridge/Editor/Tools/ManageAsset.cs | 40 +++++++++++++--------- UnityMcpServer/src/tools/manage_asset.py | 3 ++ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index aeeb08e..90657b2 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -8,6 +8,14 @@ using UnityEditor; using UnityEngine; using UnityMcpBridge.Editor.Helpers; // For Response class +#if UNITY_6000_0_OR_NEWER +using PhysicsMaterialType = UnityEngine.PhysicsMaterial; +using PhysicsMaterialCombine = UnityEngine.PhysicsMaterialCombine; +#else +using PhysicsMaterialType = UnityEngine.PhysicMaterial; +using PhysicsMaterialCombine = UnityEngine.PhysicMaterialCombine; +#endif + namespace UnityMcpBridge.Editor.Tools { /// @@ -179,7 +187,7 @@ namespace UnityMcpBridge.Editor.Tools } else if (lowerAssetType == "physicsmaterial") { - PhysicsMaterial pmat = new PhysicsMaterial(); + PhysicsMaterialType pmat = new PhysicsMaterialType(); if (properties != null) ApplyPhysicsMaterialProperties(pmat, properties); AssetDatabase.CreateAsset(pmat, fullPath); @@ -959,45 +967,45 @@ namespace UnityMcpBridge.Editor.Tools /// /// Applies properties from JObject to a PhysicsMaterial. /// - private static bool ApplyPhysicsMaterialProperties(PhysicsMaterial pmat, JObject properties) + private static bool ApplyPhysicsMaterialProperties(PhysicsMaterialType pmat, JObject properties) { if (pmat == null || properties == null) return false; bool modified = false; // Example: Set dynamic friction - if (properties["DynamicFriction"]?.Type == JTokenType.Float) + if (properties["dynamicFriction"]?.Type == JTokenType.Float) { - float dynamicFriction = properties["DynamicFriction"].ToObject(); + float dynamicFriction = properties["dynamicFriction"].ToObject(); pmat.dynamicFriction = dynamicFriction; modified = true; } // Example: Set static friction - if (properties["StaticFriction"]?.Type == JTokenType.Float) + if (properties["staticFriction"]?.Type == JTokenType.Float) { - float staticFriction = properties["StaticFriction"].ToObject(); + float staticFriction = properties["staticFriction"].ToObject(); pmat.staticFriction = staticFriction; modified = true; } // Example: Set bounciness - if (properties["Bounciness"]?.Type == JTokenType.Float) + if (properties["bounciness"]?.Type == JTokenType.Float) { - float bounciness = properties["Bounciness"].ToObject(); + float bounciness = properties["bounciness"].ToObject(); pmat.bounciness = bounciness; modified = true; } - List averageList = new List{"ave", "Ave", "average", "Average"}; - List multiplyList = new List{"mul", "Mul", "mult", "Mult", "multiply", "Multiply"}; - List minimumList = new List{"min", "Min", "minimum", "Minimum"}; - List maximumList = new List{"max", "Max", "maximum", "Maximum"}; + List averageList = new List { "ave", "Ave", "average", "Average" }; + List multiplyList = new List { "mul", "Mul", "mult", "Mult", "multiply", "Multiply" }; + List minimumList = new List { "min", "Min", "minimum", "Minimum" }; + List maximumList = new List { "max", "Max", "maximum", "Maximum" }; // Example: Set friction combine - if (properties["FrictionCombine"]?.Type == JTokenType.String) + if (properties["frictionCombine"]?.Type == JTokenType.String) { - string frictionCombine = properties["FrictionCombine"].ToString(); + string frictionCombine = properties["frictionCombine"].ToString(); if (averageList.Contains(frictionCombine)) pmat.frictionCombine = PhysicsMaterialCombine.Average; else if (multiplyList.Contains(frictionCombine)) @@ -1010,9 +1018,9 @@ namespace UnityMcpBridge.Editor.Tools } // Example: Set bounce combine - if (properties["BounceCombine"]?.Type == JTokenType.String) + if (properties["bounceCombine"]?.Type == JTokenType.String) { - string bounceCombine = properties["BounceCombine"].ToString(); + string bounceCombine = properties["bounceCombine"].ToString(); if (averageList.Contains(bounceCombine)) pmat.bounceCombine = PhysicsMaterialCombine.Average; else if (multiplyList.Contains(bounceCombine)) diff --git a/UnityMcpServer/src/tools/manage_asset.py b/UnityMcpServer/src/tools/manage_asset.py index 328b85a..dada66b 100644 --- a/UnityMcpServer/src/tools/manage_asset.py +++ b/UnityMcpServer/src/tools/manage_asset.py @@ -33,6 +33,9 @@ def register_manage_asset_tools(mcp: FastMCP): path: Asset path (e.g., "Materials/MyMaterial.mat") or search scope. asset_type: Asset type (e.g., 'Material', 'Folder') - required for 'create'. properties: Dictionary of properties for 'create'/'modify'. + example properties for Material: {"color": [1, 0, 0, 1], "shader": "Standard"}. + example properties for Texture: {"width": 1024, "height": 1024, "format": "RGBA32"}. + example properties for PhysicsMaterial: {"bounciness": 1.0, "staticFriction": 0.5, "dynamicFriction": 0.5}. destination: Target path for 'duplicate'/'move'. search_pattern: Search pattern (e.g., '*.prefab'). filter_*: Filters for search (type, date). From fb8be3b1c59bd967fdd8f36c09bfdddd5479183e Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Sun, 13 Jul 2025 15:14:08 -0400 Subject: [PATCH 17/32] Update ManageAsset.cs --- UnityMcpBridge/Editor/Tools/ManageAsset.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index a9eeae2..5a27de0 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -907,9 +907,9 @@ namespace UnityMcpBridge.Editor.Tools ); } } - } else if (properties["_Color"] is JArray colorArr) //Current Prevention for systems that use _Color instead of color + } else if (properties["color"] is JArray colorArr) //Current Prevention for systems that use _Color instead of color { - string propName = "_Color"; // Default main color + string propName = "_Color"; try { if (colorArr.Count >= 3) { From e5793a63472abc9e2bfb6ffa174656208d88c7cc Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Sun, 13 Jul 2025 16:06:32 -0400 Subject: [PATCH 18/32] Update on Readme and TODO --- README.md | 29 +++++++++++++++++++--- UnityMcpBridge/Editor/Tools/ManageAsset.cs | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e422540..64ce3e7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ Unity MCP acts as a bridge, allowing AI assistants (like Claude, Cursor) to inte --- -## UnityMCP Workflow ## Key Features 🚀 @@ -62,6 +61,7 @@ Unity MCP connects your tools using two components: * **An MCP Client:** * [Claude Desktop](https://claude.ai/download) * [Cursor](https://www.cursor.com/en/downloads) + * [Visual Studio Code Copilot](https://code.visualstudio.com/docs/copilot/overview) * *(Others may work with manual config)* @@ -84,7 +84,7 @@ Connect your MCP Client (Claude, Cursor, etc.) to the Python server you installe **Option A: Auto-Configure (Recommended for Claude/Cursor)** 1. In Unity, go to `Window > Unity MCP`. -2. Click `Auto Configure Claude` or `Auto Configure Cursor`. +2. Click `Auto Configure` on the IDE you uses. 3. Look for a green status indicator 🟢 and "Connected". *(This attempts to modify the MCP Client's config file automatically)*. **Option B: Manual Configuration** @@ -175,6 +175,28 @@ If Auto-Configure fails or you use a different client: Example Prompt: `Create a 3D player controller.` +--- + +## TODOs (Besides PR) 📝 +### High Priority +- [ ] **AssetsGeneration Improvements** - Better server request handling +- [ ] **CodeGeneration Improvements** - Better generated code handling +- [ ] **Error Handling Improvements** - Better error messages and recovery mechanisms +- [ ] **Remote Connection** - Enable remote connection between host and server +- [ ] **Documentation Expansion** - Add tutorials on how to add your own tools + +### Medium Priority +- [ ] **Custom Tool Creation** - GUI for users to create their own MCP tools +- [ ] **Logging System** - Comprehensive logging for debugging and monitoring + +### Low Priority +- [ ] **Mobile Platform Support** - Extended tools for mobile development workflows +- [ ] **Easier Setup** + +### Research & Exploration +- [ ] **AI-Powered Asset Generation** - Integration with AI tools for automatic asset creation +- [ ] **Real-time Collaboration** - Live editing sessions between multiple developers (Currently ongoing) +- [ ] **Analytics Dashboard** - Usage analytics and project insights --- @@ -239,7 +261,8 @@ MIT License. See [LICENSE](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithu ## Contact 👋 -- **X/Twitter:** [@justinpbarnett](https://www.google.com/url?sa=E&q=https%3A%2F%2Fx.com%2Fjustinpbarnett) +- **justinpbarnett:** [X/Twitter](https://www.google.com/url?sa=E&q=https%3A%2F%2Fx.com%2Fjustinpbarnett) +- **scriptwonder**: [Email](mailto:swu85@ur.rochester.edu), [LinkedIn](https://www.linkedin.com/in/shutong-wu-214043172/) --- diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index 5a27de0..432b234 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -907,7 +907,7 @@ namespace UnityMcpBridge.Editor.Tools ); } } - } else if (properties["color"] is JArray colorArr) //Current Prevention for systems that use _Color instead of color + } else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py { string propName = "_Color"; try { From 6665eac5151c7e4caf8e056222513dd534b89f29 Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Sun, 13 Jul 2025 16:29:58 -0400 Subject: [PATCH 19/32] Readme update with badges and more --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 64ce3e7..5977409 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,17 @@ # Unity MCP ✨ -**Connect your Unity Editor to LLMs using the Model Context Protocol.** + +[![](https://img.shields.io/badge/Unity-000000?style=flat&logo=unity&logoColor=blue 'Unity')](https://unity.com/releases/editor/archive) +[![python](https://img.shields.io/badge/Python-3.12-3776AB.svg?style=flat&logo=python&logoColor=white)](https://www.python.org) +[![](https://badge.mcpx.dev?status=on 'MCP Enabled')](https://modelcontextprotocol.io/introduction) +![GitHub commit activity](https://img.shields.io/github/commit-activity/w/justinpbarnett/unity-mcp) +![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/justinpbarnett/unity-mcp) +[![](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](https://opensource.org/licenses/MIT) + + + + +**Create your Unity apps with LLMs!** Unity MCP acts as a bridge, allowing AI assistants (like Claude, Cursor) to interact directly with your Unity Editor via a local **MCP (Model Context Protocol) Client**. Give your LLM tools to manage assets, control scenes, edit scripts, and automate tasks within Unity. @@ -47,8 +58,6 @@ Unity MCP connects your tools using two components: ### Prerequisites -
- Click to view required software... * **Git CLI:** For cloning the server code. [Download Git](https://git-scm.com/downloads) * **Python:** Version 3.12 or newer. [Download Python](https://www.python.org/downloads/) @@ -63,7 +72,6 @@ Unity MCP connects your tools using two components: * [Cursor](https://www.cursor.com/en/downloads) * [Visual Studio Code Copilot](https://code.visualstudio.com/docs/copilot/overview) * *(Others may work with manual config)* -
### Step 1: Install the Unity Package (Bridge) @@ -183,7 +191,7 @@ If Auto-Configure fails or you use a different client: - [ ] **CodeGeneration Improvements** - Better generated code handling - [ ] **Error Handling Improvements** - Better error messages and recovery mechanisms - [ ] **Remote Connection** - Enable remote connection between host and server -- [ ] **Documentation Expansion** - Add tutorials on how to add your own tools +- [ ] **Documentation Expansion** - Add tutorials on how to add your own tools, and documentation on existing tools and functions ### Medium Priority - [ ] **Custom Tool Creation** - GUI for users to create their own MCP tools @@ -249,7 +257,7 @@ Help make Unity MCP better! -Still stuck? [Open an Issue](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithub.com%2Fjustinpbarnett%2Funity-mcp%2Fissues). +Still stuck? [Open an Issue](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithub.com%2Fjustinpbarnett%2Funity-mcp%2Fissues) or [Join the Discord](https://discord.gg/vhTUxXaqYr)! --- @@ -270,3 +278,8 @@ MIT License. See [LICENSE](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithu ## Acknowledgments 🙏 Thanks to the contributors and the Unity team. + + +## Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=unity-mcp/unity-mcp,justinpbarnett/unity-mcp&type=Date)](https://www.star-history.com/#unity-mcp/unity-mcp&justinpbarnett/unity-mcp&Date) From 55f7c55389a3fd7afe773ffa521687c0e85cc7ca Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Mon, 14 Jul 2025 01:42:16 -0400 Subject: [PATCH 20/32] Update for ManageShader Bug fix and error handling check for the PR. Great work and I love what I built! --- README.md | 5 +- .../Editor/Tools/CommandRegistry.cs | 1 + UnityMcpBridge/Editor/Tools/ManageShader.cs | 107 ++++++++++-------- .../Editor/Tools/ManageShader.cs.meta | 11 ++ UnityMcpBridge/Editor/UnityMcpBridge.cs | 1 + 5 files changed, 79 insertions(+), 46 deletions(-) create mode 100644 UnityMcpBridge/Editor/Tools/ManageShader.cs.meta diff --git a/README.md b/README.md index e422540..d4035a9 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Unity MCP acts as a bridge, allowing AI assistants (like Claude, Cursor) to inte * `manage_editor`: Controls and queries the editor's state and settings. * `manage_scene`: Manages scenes (load, save, create, get hierarchy, etc.). * `manage_asset`: Performs asset operations (import, create, modify, delete, etc.). + * `manage_shader`: Performs shader CRUD operations (create, read, modify, delete). * `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations. * `execute_menu_item`: Executes a menu item via its path (e.g., "File/Save Project"). @@ -81,7 +82,9 @@ Unity MCP connects your tools using two components: Connect your MCP Client (Claude, Cursor, etc.) to the Python server you installed in Step 1. -**Option A: Auto-Configure (Recommended for Claude/Cursor)** +image + +**Option A: Auto-Configure (Recommended for Claude/Cursor/VSC Copilot)** 1. In Unity, go to `Window > Unity MCP`. 2. Click `Auto Configure Claude` or `Auto Configure Cursor`. diff --git a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs index 18e2de9..ce502b4 100644 --- a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs +++ b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs @@ -20,6 +20,7 @@ namespace UnityMcpBridge.Editor.Tools { "HandleManageAsset", ManageAsset.HandleCommand }, { "HandleReadConsole", ReadConsole.HandleCommand }, { "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand }, + { "HandleManageShader", ManageShader.HandleCommand}, }; /// diff --git a/UnityMcpBridge/Editor/Tools/ManageShader.cs b/UnityMcpBridge/Editor/Tools/ManageShader.cs index 49bbf15..03b28f1 100644 --- a/UnityMcpBridge/Editor/Tools/ManageShader.cs +++ b/UnityMcpBridge/Editor/Tools/ManageShader.cs @@ -89,7 +89,12 @@ namespace UnityMcpBridge.Editor.Tools { try { - Directory.CreateDirectory(fullPathDir); + if (!Directory.Exists(fullPathDir)) + { + Directory.CreateDirectory(fullPathDir); + // Refresh AssetDatabase to recognize new folders + AssetDatabase.Refresh(); + } } catch (Exception e) { @@ -150,6 +155,14 @@ namespace UnityMcpBridge.Editor.Tools ); } + // Add validation for shader name conflicts in Unity + if (Shader.Find(name) != null) + { + return Response.Error( + $"A shader with name '{name}' already exists in the project. Choose a different name." + ); + } + // Generate default content if none provided if (string.IsNullOrEmpty(contents)) { @@ -184,6 +197,7 @@ namespace UnityMcpBridge.Editor.Tools string contents = File.ReadAllText(fullPath); // Return both normal and encoded contents for larger files + //TODO: Consider a threshold for large files bool isLarge = contents.Length > 10000; // If content is large, include encoded version var responseData = new { @@ -227,6 +241,7 @@ namespace UnityMcpBridge.Editor.Tools { File.WriteAllText(fullPath, contents); AssetDatabase.ImportAsset(relativePath); + AssetDatabase.Refresh(); return Response.Success( $"Shader '{Path.GetFileName(relativePath)}' updated successfully.", new { path = relativePath } @@ -268,58 +283,60 @@ namespace UnityMcpBridge.Editor.Tools } } + //This is a CGProgram template + //TODO: making a HLSL template as well? private static string GenerateDefaultShaderContent(string name) { return @"Shader """ + name + @""" -{ - Properties - { - _MainTex (""Texture"", 2D) = ""white"" {} - } - SubShader - { - Tags { ""RenderType""=""Opaque"" } - LOD 100 - - Pass { - CGPROGRAM - #pragma vertex vert - #pragma fragment frag - #include ""UnityCG.cginc"" - - struct appdata + Properties { - float4 vertex : POSITION; - float2 uv : TEXCOORD0; - }; - - struct v2f - { - float2 uv : TEXCOORD0; - float4 vertex : SV_POSITION; - }; - - sampler2D _MainTex; - float4 _MainTex_ST; - - v2f vert (appdata v) - { - v2f o; - o.vertex = UnityObjectToClipPos(v.vertex); - o.uv = TRANSFORM_TEX(v.uv, _MainTex); - return o; + _MainTex (""Texture"", 2D) = ""white"" {} } - - fixed4 frag (v2f i) : SV_Target + SubShader { - fixed4 col = tex2D(_MainTex, i.uv); - return col; + Tags { ""RenderType""=""Opaque"" } + LOD 100 + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #include ""UnityCG.cginc"" + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + }; + + struct v2f + { + float2 uv : TEXCOORD0; + float4 vertex : SV_POSITION; + }; + + sampler2D _MainTex; + float4 _MainTex_ST; + + v2f vert (appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = TRANSFORM_TEX(v.uv, _MainTex); + return o; + } + + fixed4 frag (v2f i) : SV_Target + { + fixed4 col = tex2D(_MainTex, i.uv); + return col; + } + ENDCG + } } - ENDCG - } - } -}"; + }"; } } } \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Tools/ManageShader.cs.meta b/UnityMcpBridge/Editor/Tools/ManageShader.cs.meta new file mode 100644 index 0000000..89d10cd --- /dev/null +++ b/UnityMcpBridge/Editor/Tools/ManageShader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bcf4f1f3110494344b2af9324cf5c571 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.cs b/UnityMcpBridge/Editor/UnityMcpBridge.cs index 9276c05..e6b46db 100644 --- a/UnityMcpBridge/Editor/UnityMcpBridge.cs +++ b/UnityMcpBridge/Editor/UnityMcpBridge.cs @@ -378,6 +378,7 @@ namespace UnityMcpBridge.Editor "manage_editor" => ManageEditor.HandleCommand(paramsObject), "manage_gameobject" => ManageGameObject.HandleCommand(paramsObject), "manage_asset" => ManageAsset.HandleCommand(paramsObject), + "manage_shader" => ManageShader.HandleCommand(paramsObject), "read_console" => ReadConsole.HandleCommand(paramsObject), "execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject), _ => throw new ArgumentException( From b13661c8fba91f50bf4869acc29aed384eebccaa Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 14 Jul 2025 02:01:25 -0400 Subject: [PATCH 21/32] Update README.md --- README.md | 62 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 09168c8..60439a5 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ Unity MCP acts as a bridge, allowing AI assistants (like Claude, Cursor) to inte * **🤖 Automation:** Automate repetitive Unity workflows. * **🧩 Extensible:** Designed to work with various MCP Clients. -
- Expand for Available Tools... +
+ Available Tools Your LLM can use functions like: @@ -184,30 +184,40 @@ If Auto-Configure fails or you use a different client: 3. **Interact!** Unity tools should now be available in your MCP Client. - Example Prompt: `Create a 3D player controller.` + Example Prompt: `Create a 3D player controller`, `Create a yellow and bridge sun`, `Create a cool shader and apply it on a cube`. --- -## TODOs (Besides PR) 📝 -### High Priority -- [ ] **AssetsGeneration Improvements** - Better server request handling -- [ ] **CodeGeneration Improvements** - Better generated code handling -- [ ] **Error Handling Improvements** - Better error messages and recovery mechanisms -- [ ] **Remote Connection** - Enable remote connection between host and server -- [ ] **Documentation Expansion** - Add tutorials on how to add your own tools, and documentation on existing tools and functions +## Future Dev Plans (Besides PR) 📝 -### Medium Priority -- [ ] **Custom Tool Creation** - GUI for users to create their own MCP tools -- [ ] **Logging System** - Comprehensive logging for debugging and monitoring +### 🔴 High Priority +- [ ] **Asset Generation Improvements** - Enhanced server request handling and asset pipeline optimization +- [ ] **Code Generation Enhancements** - Improved generated code quality, validation, and error handling +- [ ] **Robust Error Handling** - Comprehensive error messages, recovery mechanisms, and graceful degradation +- [ ] **Remote Connection Support** - Enable seamless remote connection between Unity host and MCP server +- [ ] **Documentation Expansion** - Complete tutorials for custom tool creation and API reference -### Low Priority -- [ ] **Mobile Platform Support** - Extended tools for mobile development workflows -- [ ] **Easier Setup** +### 🟡 Medium Priority +- [ ] **Custom Tool Creation GUI** - Visual interface for users to create and configure their own MCP tools +- [ ] **Advanced Logging System** - Logging with filtering, export, and debugging capabilities -### Research & Exploration -- [ ] **AI-Powered Asset Generation** - Integration with AI tools for automatic asset creation -- [ ] **Real-time Collaboration** - Live editing sessions between multiple developers (Currently ongoing) -- [ ] **Analytics Dashboard** - Usage analytics and project insights +### 🟢 Low Priority +- [ ] **Mobile Platform Support** - Extended toolset for mobile development workflows and platform-specific features +- [ ] **Easier Tool Setup** +- [ ] **Plugin Marketplace** - Community-driven tool sharing and distribution platform + +
+ ✅ Completed Features + + - [x] **Shader Generation** - Generate shaders using CGProgram template +
+ +### 🔬 Research & Exploration +- [ ] **AI-Powered Asset Generation** - Integration with AI tools for automatic 3D models, textures, and animations +- [ ] **Real-time Collaboration** - Live editing sessions between multiple developers *(Currently in progress)* +- [ ] **Analytics Dashboard** - Usage analytics, project insights, and performance metrics +- [ ] **Voice Commands** - Voice-controlled Unity operations for accessibility +- [ ] **AR/VR Tool Integration** - Extended support for immersive development workflows --- @@ -264,18 +274,18 @@ Still stuck? [Open an Issue](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgit --- -## License 📜 - -MIT License. See [LICENSE](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithub.com%2Fjustinpbarnett%2Funity-mcp%2Fblob%2Fmaster%2FLICENSE) file. - ---- - ## Contact 👋 - **justinpbarnett:** [X/Twitter](https://www.google.com/url?sa=E&q=https%3A%2F%2Fx.com%2Fjustinpbarnett) - **scriptwonder**: [Email](mailto:swu85@ur.rochester.edu), [LinkedIn](https://www.linkedin.com/in/shutong-wu-214043172/) +--- + +## License 📜 + +MIT License. See [LICENSE](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgithub.com%2Fjustinpbarnett%2Funity-mcp%2Fblob%2Fmaster%2FLICENSE) file. + --- ## Acknowledgments 🙏 From 91011052124847e1bd773f8be5ddf3149be6c94b Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Wed, 23 Jul 2025 23:31:47 -0400 Subject: [PATCH 22/32] Code Validation Update 1. Update the code validation feature. With Roslyn installed(see guide), MCP clients will receive detailed error messages and making the code generation process more error-proof 2. Minor update to the EditorWindow, making more space for upcoming features 3. Readme update to include Code validation guides 4. Minor bug fixes including installation bugs from previous VSC PR --- README.md | 26 +- UnityMcpBridge/Editor/Tools/ManageScript.cs | 687 +++++++++++++++++- .../Editor/Windows/UnityMcpEditorWindow.cs | 500 ++++++++----- 3 files changed, 998 insertions(+), 215 deletions(-) diff --git a/README.md b/README.md index 60439a5..2d8f82e 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,27 @@ Unity MCP connects your tools using two components: * [Cursor](https://www.cursor.com/en/downloads) * [Visual Studio Code Copilot](https://code.visualstudio.com/docs/copilot/overview) * *(Others may work with manual config)* + *
[Optional] Roslyn for Advanced Script Validation + + For **Strict** validation level that catches undefined namespaces, types, and methods: + + **Method 1: NuGet for Unity (Recommended)** + 1. Install [NuGetForUnity](https://github.com/GlitchEnzo/NuGetForUnity) + 2. Go to `Window > NuGet Package Manager` + 3. Search for `Microsoft.CodeAnalysis.CSharp` and install the package + 5. Go to `Player Settings > Scripting Define Symbols` + 6. Add `USE_ROSLYN` + 7. Restart Unity + + **Method 2: Manual DLL Installation** + 1. Download Microsoft.CodeAnalysis.CSharp.dll and dependencies from [NuGet](https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp/) + 2. Place DLLs in `Assets/Plugins/` folder + 3. Ensure .NET compatibility settings are correct + 4. Add `USE_ROSLYN` to Scripting Define Symbols + 5. Restart Unity + + **Note:** Without Roslyn, script validation falls back to basic structural checks. Roslyn enables full C# compiler diagnostics with precise error reporting.
+ ### Step 1: Install the Unity Package (Bridge) @@ -192,7 +213,7 @@ If Auto-Configure fails or you use a different client: ### 🔴 High Priority - [ ] **Asset Generation Improvements** - Enhanced server request handling and asset pipeline optimization -- [ ] **Code Generation Enhancements** - Improved generated code quality, validation, and error handling +- [ ] **Code Generation Enhancements** - Improved generated code quality and error handling - [ ] **Robust Error Handling** - Comprehensive error messages, recovery mechanisms, and graceful degradation - [ ] **Remote Connection Support** - Enable seamless remote connection between Unity host and MCP server - [ ] **Documentation Expansion** - Complete tutorials for custom tool creation and API reference @@ -210,6 +231,7 @@ If Auto-Configure fails or you use a different client: ✅ Completed Features - [x] **Shader Generation** - Generate shaders using CGProgram template + - [x] **Advanced Script Validation** - Multi-level validation with semantic analysis, namespace/type checking, and Unity best practices (Will need Roslyn Installed, see [Prerequisite](#prerequisites)).
### 🔬 Research & Exploration @@ -295,4 +317,4 @@ Thanks to the contributors and the Unity team. ## Star History -[![Star History Chart](https://api.star-history.com/svg?repos=unity-mcp/unity-mcp,justinpbarnett/unity-mcp&type=Date)](https://www.star-history.com/#unity-mcp/unity-mcp&justinpbarnett/unity-mcp&Date) +[![Star History Chart](https://api.star-history.com/svg?repos=justinpbarnett/unity-mcp&type=Date)](https://www.star-history.com/#justinpbarnett/unity-mcp&Date) diff --git a/UnityMcpBridge/Editor/Tools/ManageScript.cs b/UnityMcpBridge/Editor/Tools/ManageScript.cs index 70c9f71..d79e17a 100644 --- a/UnityMcpBridge/Editor/Tools/ManageScript.cs +++ b/UnityMcpBridge/Editor/Tools/ManageScript.cs @@ -7,10 +7,43 @@ using UnityEditor; using UnityEngine; using UnityMcpBridge.Editor.Helpers; +#if USE_ROSLYN +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +#endif + +#if UNITY_EDITOR +using UnityEditor.Compilation; +#endif + + namespace UnityMcpBridge.Editor.Tools { /// /// Handles CRUD operations for C# scripts within the Unity project. + /// + /// ROSLYN INSTALLATION GUIDE: + /// To enable advanced syntax validation with Roslyn compiler services: + /// + /// 1. Install Microsoft.CodeAnalysis.CSharp NuGet package: + /// - Open Package Manager in Unity + /// - Follow the instruction on https://github.com/GlitchEnzo/NuGetForUnity + /// + /// 2. Open NuGet Package Manager and Install Microsoft.CodeAnalysis.CSharp: + /// + /// 3. Alternative: Manual DLL installation: + /// - Download Microsoft.CodeAnalysis.CSharp.dll and dependencies + /// - Place in Assets/Plugins/ folder + /// - Ensure .NET compatibility settings are correct + /// + /// 4. Define USE_ROSLYN symbol: + /// - Go to Player Settings > Scripting Define Symbols + /// - Add "USE_ROSLYN" to enable Roslyn-based validation + /// + /// 5. Restart Unity after installation + /// + /// Note: Without Roslyn, the system falls back to basic structural validation. + /// Roslyn provides full C# compiler diagnostics with line numbers and detailed error messages. /// public static class ManageScript { @@ -168,12 +201,18 @@ namespace UnityMcpBridge.Editor.Tools contents = GenerateDefaultScriptContent(name, scriptType, namespaceName); } - // Validate syntax (basic check) - if (!ValidateScriptSyntax(contents)) + // Validate syntax with detailed error reporting using GUI setting + ValidationLevel validationLevel = GetValidationLevelFromGUI(); + bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors); + if (!isValid) { - // Optionally return a specific error or warning about syntax - // return Response.Error("Provided script content has potential syntax errors."); - Debug.LogWarning($"Potential syntax error in script being created: {name}"); + string errorMessage = "Script validation failed:\n" + string.Join("\n", validationErrors); + return Response.Error(errorMessage); + } + else if (validationErrors != null && validationErrors.Length > 0) + { + // Log warnings but don't block creation + Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors)); } try @@ -243,11 +282,18 @@ namespace UnityMcpBridge.Editor.Tools return Response.Error("Content is required for the 'update' action."); } - // Validate syntax (basic check) - if (!ValidateScriptSyntax(contents)) + // Validate syntax with detailed error reporting using GUI setting + ValidationLevel validationLevel = GetValidationLevelFromGUI(); + bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors); + if (!isValid) { - Debug.LogWarning($"Potential syntax error in script being updated: {name}"); - // Consider if this should be a hard error or just a warning + string errorMessage = "Script validation failed:\n" + string.Join("\n", validationErrors); + return Response.Error(errorMessage); + } + else if (validationErrors != null && validationErrors.Length > 0) + { + // Log warnings but don't block update + Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors)); } try @@ -361,27 +407,624 @@ namespace UnityMcpBridge.Editor.Tools } /// - /// Performs a very basic syntax validation (checks for balanced braces). - /// TODO: Implement more robust syntax checking if possible. + /// Gets the validation level from the GUI settings + /// + private static ValidationLevel GetValidationLevelFromGUI() + { + string savedLevel = EditorPrefs.GetString("UnityMCP_ScriptValidationLevel", "standard"); + return savedLevel.ToLower() switch + { + "basic" => ValidationLevel.Basic, + "standard" => ValidationLevel.Standard, + "comprehensive" => ValidationLevel.Comprehensive, + "strict" => ValidationLevel.Strict, + _ => ValidationLevel.Standard // Default fallback + }; + } + + /// + /// Validates C# script syntax using multiple validation layers. /// private static bool ValidateScriptSyntax(string contents) { - if (string.IsNullOrEmpty(contents)) - return true; // Empty is technically valid? + return ValidateScriptSyntax(contents, ValidationLevel.Standard, out _); + } - int braceBalance = 0; - foreach (char c in contents) + /// + /// Advanced syntax validation with detailed diagnostics and configurable strictness. + /// + private static bool ValidateScriptSyntax(string contents, ValidationLevel level, out string[] errors) + { + var errorList = new System.Collections.Generic.List(); + errors = null; + + if (string.IsNullOrEmpty(contents)) { - if (c == '{') - braceBalance++; - else if (c == '}') - braceBalance--; + return true; // Empty content is valid } - return braceBalance == 0; - // This is extremely basic. A real C# parser/compiler check would be ideal - // but is complex to implement directly here. + // Basic structural validation + if (!ValidateBasicStructure(contents, errorList)) + { + errors = errorList.ToArray(); + return false; + } + +#if USE_ROSLYN + // Advanced Roslyn-based validation + if (!ValidateScriptSyntaxRoslyn(contents, level, errorList)) + { + errors = errorList.ToArray(); + return level != ValidationLevel.Standard; //TODO: Allow standard to run roslyn right now, might formalize it in the future + } +#endif + + // Unity-specific validation + if (level >= ValidationLevel.Standard) + { + ValidateScriptSyntaxUnity(contents, errorList); + } + + // Semantic analysis for common issues + if (level >= ValidationLevel.Comprehensive) + { + ValidateSemanticRules(contents, errorList); + } + +#if USE_ROSLYN + // Full semantic compilation validation for Strict level + if (level == ValidationLevel.Strict) + { + if (!ValidateScriptSemantics(contents, errorList)) + { + errors = errorList.ToArray(); + return false; // Strict level fails on any semantic errors + } + } +#endif + + errors = errorList.ToArray(); + return errorList.Count == 0 || (level != ValidationLevel.Strict && !errorList.Any(e => e.StartsWith("ERROR:"))); } + + /// + /// Validation strictness levels + /// + private enum ValidationLevel + { + Basic, // Only syntax errors + Standard, // Syntax + Unity best practices + Comprehensive, // All checks + semantic analysis + Strict // Treat all issues as errors + } + + /// + /// Validates basic code structure (braces, quotes, comments) + /// + private static bool ValidateBasicStructure(string contents, System.Collections.Generic.List errors) + { + bool isValid = true; + int braceBalance = 0; + int parenBalance = 0; + int bracketBalance = 0; + bool inStringLiteral = false; + bool inCharLiteral = false; + bool inSingleLineComment = false; + bool inMultiLineComment = false; + bool escaped = false; + + for (int i = 0; i < contents.Length; i++) + { + char c = contents[i]; + char next = i + 1 < contents.Length ? contents[i + 1] : '\0'; + + // Handle escape sequences + if (escaped) + { + escaped = false; + continue; + } + + if (c == '\\' && (inStringLiteral || inCharLiteral)) + { + escaped = true; + continue; + } + + // Handle comments + if (!inStringLiteral && !inCharLiteral) + { + if (c == '/' && next == '/' && !inMultiLineComment) + { + inSingleLineComment = true; + continue; + } + if (c == '/' && next == '*' && !inSingleLineComment) + { + inMultiLineComment = true; + i++; // Skip next character + continue; + } + if (c == '*' && next == '/' && inMultiLineComment) + { + inMultiLineComment = false; + i++; // Skip next character + continue; + } + } + + if (c == '\n') + { + inSingleLineComment = false; + continue; + } + + if (inSingleLineComment || inMultiLineComment) + continue; + + // Handle string and character literals + if (c == '"' && !inCharLiteral) + { + inStringLiteral = !inStringLiteral; + continue; + } + if (c == '\'' && !inStringLiteral) + { + inCharLiteral = !inCharLiteral; + continue; + } + + if (inStringLiteral || inCharLiteral) + continue; + + // Count brackets and braces + switch (c) + { + case '{': braceBalance++; break; + case '}': braceBalance--; break; + case '(': parenBalance++; break; + case ')': parenBalance--; break; + case '[': bracketBalance++; break; + case ']': bracketBalance--; break; + } + + // Check for negative balances (closing without opening) + if (braceBalance < 0) + { + errors.Add("ERROR: Unmatched closing brace '}'"); + isValid = false; + } + if (parenBalance < 0) + { + errors.Add("ERROR: Unmatched closing parenthesis ')'"); + isValid = false; + } + if (bracketBalance < 0) + { + errors.Add("ERROR: Unmatched closing bracket ']'"); + isValid = false; + } + } + + // Check final balances + if (braceBalance != 0) + { + errors.Add($"ERROR: Unbalanced braces (difference: {braceBalance})"); + isValid = false; + } + if (parenBalance != 0) + { + errors.Add($"ERROR: Unbalanced parentheses (difference: {parenBalance})"); + isValid = false; + } + if (bracketBalance != 0) + { + errors.Add($"ERROR: Unbalanced brackets (difference: {bracketBalance})"); + isValid = false; + } + if (inStringLiteral) + { + errors.Add("ERROR: Unterminated string literal"); + isValid = false; + } + if (inCharLiteral) + { + errors.Add("ERROR: Unterminated character literal"); + isValid = false; + } + if (inMultiLineComment) + { + errors.Add("WARNING: Unterminated multi-line comment"); + } + + return isValid; + } + +#if USE_ROSLYN + /// + /// Cached compilation references for performance + /// + private static System.Collections.Generic.List _cachedReferences = null; + private static DateTime _cacheTime = DateTime.MinValue; + private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(5); + + /// + /// Validates syntax using Roslyn compiler services + /// + private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List errors) + { + try + { + var syntaxTree = CSharpSyntaxTree.ParseText(contents); + var diagnostics = syntaxTree.GetDiagnostics(); + + bool hasErrors = false; + foreach (var diagnostic in diagnostics) + { + string severity = diagnostic.Severity.ToString().ToUpper(); + string message = $"{severity}: {diagnostic.GetMessage()}"; + + if (diagnostic.Severity == DiagnosticSeverity.Error) + { + hasErrors = true; + } + + // Include warnings in comprehensive mode + if (level >= ValidationLevel.Standard || diagnostic.Severity == DiagnosticSeverity.Error) //Also use Standard for now + { + var location = diagnostic.Location.GetLineSpan(); + if (location.IsValid) + { + message += $" (Line {location.StartLinePosition.Line + 1})"; + } + errors.Add(message); + } + } + + return !hasErrors; + } + catch (Exception ex) + { + errors.Add($"ERROR: Roslyn validation failed: {ex.Message}"); + return false; + } + } + + /// + /// Validates script semantics using full compilation context to catch namespace, type, and method resolution errors + /// + private static bool ValidateScriptSemantics(string contents, System.Collections.Generic.List errors) + { + try + { + // Get compilation references with caching + var references = GetCompilationReferences(); + if (references == null || references.Count == 0) + { + errors.Add("WARNING: Could not load compilation references for semantic validation"); + return true; // Don't fail if we can't get references + } + + // Create syntax tree + var syntaxTree = CSharpSyntaxTree.ParseText(contents); + + // Create compilation with full context + var compilation = CSharpCompilation.Create( + "TempValidation", + new[] { syntaxTree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + ); + + // Get semantic diagnostics - this catches all the issues you mentioned! + var diagnostics = compilation.GetDiagnostics(); + + bool hasErrors = false; + foreach (var diagnostic in diagnostics) + { + if (diagnostic.Severity == DiagnosticSeverity.Error) + { + hasErrors = true; + var location = diagnostic.Location.GetLineSpan(); + string locationInfo = location.IsValid ? + $" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : ""; + + // Include diagnostic ID for better error identification + string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : ""; + errors.Add($"ERROR: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}"); + } + else if (diagnostic.Severity == DiagnosticSeverity.Warning) + { + var location = diagnostic.Location.GetLineSpan(); + string locationInfo = location.IsValid ? + $" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : ""; + + string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : ""; + errors.Add($"WARNING: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}"); + } + } + + return !hasErrors; + } + catch (Exception ex) + { + errors.Add($"ERROR: Semantic validation failed: {ex.Message}"); + return false; + } + } + + /// + /// Gets compilation references with caching for performance + /// + private static System.Collections.Generic.List GetCompilationReferences() + { + // Check cache validity + if (_cachedReferences != null && DateTime.Now - _cacheTime < CacheExpiry) + { + return _cachedReferences; + } + + try + { + var references = new System.Collections.Generic.List(); + + // Core .NET assemblies + references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); // mscorlib/System.Private.CoreLib + references.Add(MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location)); // System.Linq + references.Add(MetadataReference.CreateFromFile(typeof(System.Collections.Generic.List<>).Assembly.Location)); // System.Collections + + // Unity assemblies + try + { + references.Add(MetadataReference.CreateFromFile(typeof(UnityEngine.Debug).Assembly.Location)); // UnityEngine + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load UnityEngine assembly: {ex.Message}"); + } + +#if UNITY_EDITOR + try + { + references.Add(MetadataReference.CreateFromFile(typeof(UnityEditor.Editor).Assembly.Location)); // UnityEditor + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load UnityEditor assembly: {ex.Message}"); + } + + // Get Unity project assemblies + try + { + var assemblies = CompilationPipeline.GetAssemblies(); + foreach (var assembly in assemblies) + { + if (File.Exists(assembly.outputPath)) + { + references.Add(MetadataReference.CreateFromFile(assembly.outputPath)); + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load Unity project assemblies: {ex.Message}"); + } +#endif + + // Cache the results + _cachedReferences = references; + _cacheTime = DateTime.Now; + + return references; + } + catch (Exception ex) + { + Debug.LogError($"Failed to get compilation references: {ex.Message}"); + return new System.Collections.Generic.List(); + } + } +#else + private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List errors) + { + // Fallback when Roslyn is not available + return true; + } +#endif + + /// + /// Validates Unity-specific coding rules and best practices + /// //TODO: Naive Unity Checks and not really yield any results, need to be improved + /// + private static void ValidateScriptSyntaxUnity(string contents, System.Collections.Generic.List errors) + { + // Check for common Unity anti-patterns + if (contents.Contains("FindObjectOfType") && contents.Contains("Update()")) + { + errors.Add("WARNING: FindObjectOfType in Update() can cause performance issues"); + } + + if (contents.Contains("GameObject.Find") && contents.Contains("Update()")) + { + errors.Add("WARNING: GameObject.Find in Update() can cause performance issues"); + } + + // Check for proper MonoBehaviour usage + if (contents.Contains(": MonoBehaviour") && !contents.Contains("using UnityEngine")) + { + errors.Add("WARNING: MonoBehaviour requires 'using UnityEngine;'"); + } + + // Check for SerializeField usage + if (contents.Contains("[SerializeField]") && !contents.Contains("using UnityEngine")) + { + errors.Add("WARNING: SerializeField requires 'using UnityEngine;'"); + } + + // Check for proper coroutine usage + if (contents.Contains("StartCoroutine") && !contents.Contains("IEnumerator")) + { + errors.Add("WARNING: StartCoroutine typically requires IEnumerator methods"); + } + + // Check for Update without FixedUpdate for physics + if (contents.Contains("Rigidbody") && contents.Contains("Update()") && !contents.Contains("FixedUpdate()")) + { + errors.Add("WARNING: Consider using FixedUpdate() for Rigidbody operations"); + } + + // Check for missing null checks on Unity objects + if (contents.Contains("GetComponent<") && !contents.Contains("!= null")) + { + errors.Add("WARNING: Consider null checking GetComponent results"); + } + + // Check for proper event function signatures + if (contents.Contains("void Start(") && !contents.Contains("void Start()")) + { + errors.Add("WARNING: Start() should not have parameters"); + } + + if (contents.Contains("void Update(") && !contents.Contains("void Update()")) + { + errors.Add("WARNING: Update() should not have parameters"); + } + + // Check for inefficient string operations + if (contents.Contains("Update()") && contents.Contains("\"") && contents.Contains("+")) + { + errors.Add("WARNING: String concatenation in Update() can cause garbage collection issues"); + } + } + + /// + /// Validates semantic rules and common coding issues + /// + private static void ValidateSemanticRules(string contents, System.Collections.Generic.List errors) + { + // Check for potential memory leaks + if (contents.Contains("new ") && contents.Contains("Update()")) + { + errors.Add("WARNING: Creating objects in Update() may cause memory issues"); + } + + // Check for magic numbers + var magicNumberPattern = new Regex(@"\b\d+\.?\d*f?\b(?!\s*[;})\]])"); + var matches = magicNumberPattern.Matches(contents); + if (matches.Count > 5) + { + errors.Add("WARNING: Consider using named constants instead of magic numbers"); + } + + // Check for long methods (simple line count check) + var methodPattern = new Regex(@"(public|private|protected|internal)?\s*(static)?\s*\w+\s+\w+\s*\([^)]*\)\s*{"); + var methodMatches = methodPattern.Matches(contents); + foreach (Match match in methodMatches) + { + int startIndex = match.Index; + int braceCount = 0; + int lineCount = 0; + bool inMethod = false; + + for (int i = startIndex; i < contents.Length; i++) + { + if (contents[i] == '{') + { + braceCount++; + inMethod = true; + } + else if (contents[i] == '}') + { + braceCount--; + if (braceCount == 0 && inMethod) + break; + } + else if (contents[i] == '\n' && inMethod) + { + lineCount++; + } + } + + if (lineCount > 50) + { + errors.Add("WARNING: Method is very long, consider breaking it into smaller methods"); + break; // Only report once + } + } + + // Check for proper exception handling + if (contents.Contains("catch") && contents.Contains("catch()")) + { + errors.Add("WARNING: Empty catch blocks should be avoided"); + } + + // Check for proper async/await usage + if (contents.Contains("async ") && !contents.Contains("await")) + { + errors.Add("WARNING: Async method should contain await or return Task"); + } + + // Check for hardcoded tags and layers + if (contents.Contains("\"Player\"") || contents.Contains("\"Enemy\"")) + { + errors.Add("WARNING: Consider using constants for tags instead of hardcoded strings"); + } + } + + //TODO: A easier way for users to update incorrect scripts (now duplicated with the updateScript method and need to also update server side, put aside for now) + /// + /// Public method to validate script syntax with configurable validation level + /// Returns detailed validation results including errors and warnings + /// + // public static object ValidateScript(JObject @params) + // { + // string contents = @params["contents"]?.ToString(); + // string validationLevel = @params["validationLevel"]?.ToString() ?? "standard"; + + // if (string.IsNullOrEmpty(contents)) + // { + // return Response.Error("Contents parameter is required for validation."); + // } + + // // Parse validation level + // ValidationLevel level = ValidationLevel.Standard; + // switch (validationLevel.ToLower()) + // { + // case "basic": level = ValidationLevel.Basic; break; + // case "standard": level = ValidationLevel.Standard; break; + // case "comprehensive": level = ValidationLevel.Comprehensive; break; + // case "strict": level = ValidationLevel.Strict; break; + // default: + // return Response.Error($"Invalid validation level: '{validationLevel}'. Valid levels are: basic, standard, comprehensive, strict."); + // } + + // // Perform validation + // bool isValid = ValidateScriptSyntax(contents, level, out string[] validationErrors); + + // var errors = validationErrors?.Where(e => e.StartsWith("ERROR:")).ToArray() ?? new string[0]; + // var warnings = validationErrors?.Where(e => e.StartsWith("WARNING:")).ToArray() ?? new string[0]; + + // var result = new + // { + // isValid = isValid, + // validationLevel = validationLevel, + // errorCount = errors.Length, + // warningCount = warnings.Length, + // errors = errors, + // warnings = warnings, + // summary = isValid + // ? (warnings.Length > 0 ? $"Validation passed with {warnings.Length} warnings" : "Validation passed with no issues") + // : $"Validation failed with {errors.Length} errors and {warnings.Length} warnings" + // }; + + // if (isValid) + // { + // return Response.Success("Script validation completed successfully.", result); + // } + // else + // { + // return Response.Error("Script validation failed.", result); + // } + // } } } diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index bf11f1f..3b1e69d 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using Newtonsoft.Json; using UnityEditor; @@ -19,6 +20,19 @@ namespace UnityMcpBridge.Editor.Windows private const int unityPort = 6400; // Hardcoded Unity port private const int mcpPort = 6500; // Hardcoded MCP port private readonly McpClients mcpClients = new(); + + // Script validation settings + private int validationLevelIndex = 1; // Default to Standard + private readonly string[] validationLevelOptions = new string[] + { + "Basic - Only syntax checks", + "Standard - Syntax + Unity practices", + "Comprehensive - All checks + semantic analysis", + "Strict - Full semantic validation (requires Roslyn)" + }; + + // UI state + private int selectedClientIndex = 0; [MenuItem("Window/Unity MCP")] public static void ShowWindow() @@ -35,6 +49,9 @@ namespace UnityMcpBridge.Editor.Windows { CheckMcpConfiguration(mcpClient); } + + // Load validation level setting + LoadValidationLevelSetting(); } private Color GetStatusColor(McpStatus status) @@ -79,164 +96,18 @@ namespace UnityMcpBridge.Editor.Windows } } - private void ConfigurationSection(McpClient mcpClient) + + private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12) { - // Calculate if we should use half-width layout - // Minimum width for half-width layout is 400 pixels - bool useHalfWidth = position.width >= 800; - float sectionWidth = useHalfWidth ? (position.width / 2) - 15 : position.width - 20; - - // Begin horizontal layout if using half-width - if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 0) - { - EditorGUILayout.BeginHorizontal(); - } - - // Begin section with fixed width - EditorGUILayout.BeginVertical(EditorStyles.helpBox, GUILayout.Width(sectionWidth)); - - // Header with improved styling - EditorGUILayout.Space(5); - Rect headerRect = EditorGUILayout.GetControlRect(false, 24); - GUI.Label( - new Rect( - headerRect.x + 8, - headerRect.y + 4, - headerRect.width - 16, - headerRect.height - ), - mcpClient.name + " Configuration", - EditorStyles.boldLabel - ); - EditorGUILayout.Space(5); - - // Status indicator with colored dot - Rect statusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20)); - Color statusColor = GetStatusColor(mcpClient.status); - - // Draw status dot - DrawStatusDot(statusRect, statusColor); - - // Status text with some padding - EditorGUILayout.LabelField( - new GUIContent(" " + mcpClient.configStatus), - GUILayout.Height(20), - GUILayout.MinWidth(100) - ); - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.Space(8); - - // Configure button with improved styling - GUIStyle buttonStyle = new(GUI.skin.button) - { - padding = new RectOffset(15, 15, 5, 5), - margin = new RectOffset(10, 10, 5, 5), - }; - - // Create muted button style for Manual Setup - GUIStyle mutedButtonStyle = new(buttonStyle); - - if (mcpClient.mcpType == McpTypes.VSCode) - { - // Special handling for VSCode GitHub Copilot - if ( - GUILayout.Button( - "Auto Configure VSCode with GitHub Copilot", - buttonStyle, - GUILayout.Height(28) - ) - ) - { - ConfigureMcpClient(mcpClient); - } - - if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28))) - { - // Show VSCode specific manual setup window - string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? mcpClient.windowsConfigPath - : mcpClient.linuxConfigPath; - - // Get the Python directory path - string pythonDir = FindPackagePythonDirectory(); - - // Create VSCode-specific configuration - var vscodeConfig = new - { - mcp = new - { - servers = new - { - unityMCP = new - { - command = "uv", - args = new[] { "--directory", pythonDir, "run", "server.py" } - } - } - } - }; - - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); - - // Use the VSCodeManualSetupWindow directly since we're in the same namespace - VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); - } - } - else - { - // Standard client buttons - if ( - GUILayout.Button( - $"Auto Configure {mcpClient.name}", - buttonStyle, - GUILayout.Height(28) - ) - ) - { - ConfigureMcpClient(mcpClient); - } - - if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28))) - { - // Get the appropriate config path based on OS - string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? mcpClient.windowsConfigPath - : mcpClient.linuxConfigPath; - ShowManualInstructionsWindow(configPath, mcpClient); - } - } - EditorGUILayout.Space(5); - - EditorGUILayout.EndVertical(); - - // End horizontal layout if using half-width and at the end of a row - if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 1) - { - EditorGUILayout.EndHorizontal(); - EditorGUILayout.Space(5); - } - // Add space and end the horizontal layout if last item is odd - else if ( - useHalfWidth - && mcpClients.clients.IndexOf(mcpClient) == mcpClients.clients.Count - 1 - ) - { - EditorGUILayout.EndHorizontal(); - EditorGUILayout.Space(5); - } - } - - private void DrawStatusDot(Rect statusRect, Color statusColor) - { - Rect dotRect = new(statusRect.x + 6, statusRect.y + 4, 12, 12); + float offsetX = (statusRect.width - size) / 2; + float offsetY = (statusRect.height - size) / 2; + Rect dotRect = new(statusRect.x + offsetX, statusRect.y + offsetY, size, size); Vector3 center = new( dotRect.x + (dotRect.width / 2), dotRect.y + (dotRect.height / 2), 0 ); - float radius = dotRect.width / 2; + float radius = size / 2; // Draw the main dot Handles.color = statusColor; @@ -256,59 +127,255 @@ namespace UnityMcpBridge.Editor.Windows { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + // Header + DrawHeader(); + + // Main sections in a more compact layout + EditorGUILayout.BeginHorizontal(); + + // Left column - Status and Bridge + EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.5f)); + DrawServerStatusSection(); + EditorGUILayout.Space(5); + DrawBridgeSection(); + EditorGUILayout.EndVertical(); + + // Right column - Validation Settings + EditorGUILayout.BeginVertical(); + DrawValidationSection(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(10); - // Title with improved styling - Rect titleRect = EditorGUILayout.GetControlRect(false, 30); - EditorGUI.DrawRect( - new Rect(titleRect.x, titleRect.y, titleRect.width, titleRect.height), - new Color(0.2f, 0.2f, 0.2f, 0.1f) - ); + + // Unified MCP Client Configuration + DrawUnifiedClientConfiguration(); + + EditorGUILayout.EndScrollView(); + } + + private void DrawHeader() + { + EditorGUILayout.Space(15); + Rect titleRect = EditorGUILayout.GetControlRect(false, 40); + EditorGUI.DrawRect(titleRect, new Color(0.2f, 0.2f, 0.2f, 0.1f)); + + GUIStyle titleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 16, + alignment = TextAnchor.MiddleLeft + }; + GUI.Label( - new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height), - "MCP Editor", - EditorStyles.boldLabel + new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height), + "Unity MCP Editor", + titleStyle ); - EditorGUILayout.Space(10); + EditorGUILayout.Space(15); + } - // Python Server Installation Status Section + private void DrawServerStatusSection() + { EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("Python Server Status", EditorStyles.boldLabel); + + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 14 + }; + EditorGUILayout.LabelField("Server Status", sectionTitleStyle); + EditorGUILayout.Space(8); - // Status indicator with colored dot - Rect installStatusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20)); - DrawStatusDot(installStatusRect, pythonServerInstallationStatusColor); - EditorGUILayout.LabelField(" " + pythonServerInstallationStatus); + EditorGUILayout.BeginHorizontal(); + Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); + DrawStatusDot(statusRect, pythonServerInstallationStatusColor, 16); + + GUIStyle statusStyle = new GUIStyle(EditorStyles.label) + { + fontSize = 12, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(pythonServerInstallationStatus, statusStyle, GUILayout.Height(28)); EditorGUILayout.EndHorizontal(); - EditorGUILayout.LabelField($"Unity Port: {unityPort}"); - EditorGUILayout.LabelField($"MCP Port: {mcpPort}"); - EditorGUILayout.HelpBox( - "Your MCP client (e.g. Cursor or Claude Desktop) will start the server automatically when you start it.", - MessageType.Info - ); + EditorGUILayout.Space(5); + GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel) + { + fontSize = 11 + }; + EditorGUILayout.LabelField($"Ports: Unity {unityPort}, MCP {mcpPort}", portStyle); + EditorGUILayout.Space(5); EditorGUILayout.EndVertical(); + } - EditorGUILayout.Space(10); - - // Unity Bridge Section + private void DrawBridgeSection() + { EditorGUILayout.BeginVertical(EditorStyles.helpBox); - EditorGUILayout.LabelField("Unity MCP Bridge", EditorStyles.boldLabel); - EditorGUILayout.LabelField($"Status: {(isUnityBridgeRunning ? "Running" : "Stopped")}"); - EditorGUILayout.LabelField($"Port: {unityPort}"); + + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 14 + }; + EditorGUILayout.LabelField("Unity Bridge", sectionTitleStyle); + EditorGUILayout.Space(8); + + EditorGUILayout.BeginHorizontal(); + Color bridgeColor = isUnityBridgeRunning ? Color.green : Color.red; + Rect bridgeStatusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); + DrawStatusDot(bridgeStatusRect, bridgeColor, 16); + + GUIStyle bridgeStatusStyle = new GUIStyle(EditorStyles.label) + { + fontSize = 12, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(isUnityBridgeRunning ? "Running" : "Stopped", bridgeStatusStyle, GUILayout.Height(28)); + EditorGUILayout.EndHorizontal(); - if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge")) + EditorGUILayout.Space(8); + if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge", GUILayout.Height(32))) { ToggleUnityBridge(); } + EditorGUILayout.Space(5); EditorGUILayout.EndVertical(); + } - foreach (McpClient mcpClient in mcpClients.clients) + private void DrawValidationSection() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) { - EditorGUILayout.Space(10); - ConfigurationSection(mcpClient); + fontSize = 14 + }; + EditorGUILayout.LabelField("Script Validation", sectionTitleStyle); + EditorGUILayout.Space(8); + + EditorGUI.BeginChangeCheck(); + validationLevelIndex = EditorGUILayout.Popup("Validation Level", validationLevelIndex, validationLevelOptions, GUILayout.Height(20)); + if (EditorGUI.EndChangeCheck()) + { + SaveValidationLevelSetting(); } + + EditorGUILayout.Space(8); + string description = GetValidationLevelDescription(validationLevelIndex); + EditorGUILayout.HelpBox(description, MessageType.Info); + EditorGUILayout.Space(5); + EditorGUILayout.EndVertical(); + } - EditorGUILayout.EndScrollView(); + private void DrawUnifiedClientConfiguration() + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 14 + }; + EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle); + EditorGUILayout.Space(10); + + // Client selector + string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray(); + EditorGUI.BeginChangeCheck(); + selectedClientIndex = EditorGUILayout.Popup("Select Client", selectedClientIndex, clientNames, GUILayout.Height(20)); + if (EditorGUI.EndChangeCheck()) + { + selectedClientIndex = Mathf.Clamp(selectedClientIndex, 0, mcpClients.clients.Count - 1); + } + + EditorGUILayout.Space(10); + + if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count) + { + McpClient selectedClient = mcpClients.clients[selectedClientIndex]; + DrawClientConfigurationCompact(selectedClient); + } + + EditorGUILayout.Space(5); + EditorGUILayout.EndVertical(); + } + + private void DrawClientConfigurationCompact(McpClient mcpClient) + { + // Status display + EditorGUILayout.BeginHorizontal(); + Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); + Color statusColor = GetStatusColor(mcpClient.status); + DrawStatusDot(statusRect, statusColor, 16); + + GUIStyle clientStatusStyle = new GUIStyle(EditorStyles.label) + { + fontSize = 12, + fontStyle = FontStyle.Bold + }; + EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28)); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(10); + + // Action buttons in horizontal layout + EditorGUILayout.BeginHorizontal(); + + if (mcpClient.mcpType == McpTypes.VSCode) + { + if (GUILayout.Button("Auto Configure", GUILayout.Height(32))) + { + ConfigureMcpClient(mcpClient); + } + } + else + { + if (GUILayout.Button($"Auto Configure", GUILayout.Height(32))) + { + ConfigureMcpClient(mcpClient); + } + } + + if (GUILayout.Button("Manual Setup", GUILayout.Height(32))) + { + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? mcpClient.windowsConfigPath + : mcpClient.linuxConfigPath; + + if (mcpClient.mcpType == McpTypes.VSCode) + { + string pythonDir = FindPackagePythonDirectory(); + var vscodeConfig = new + { + mcp = new + { + servers = new + { + unityMCP = new + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" } + } + } + } + }; + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); + } + else + { + ShowManualInstructionsWindow(configPath, mcpClient); + } + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + // Quick info + GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel) + { + fontSize = 10 + }; + EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle); } private void ToggleUnityBridge() @@ -355,6 +422,7 @@ namespace UnityMcpBridge.Editor.Windows existingConfig ??= new Newtonsoft.Json.Linq.JObject(); // Handle different client types with a switch statement + //Comments: Interestingly, VSCode has mcp.servers.unityMCP while others have mcpServers.unityMCP, which is why we need to prevent this switch (mcpClient?.mcpType) { case McpTypes.VSCode: @@ -370,6 +438,12 @@ namespace UnityMcpBridge.Editor.Windows { existingConfig.mcp.servers = new Newtonsoft.Json.Linq.JObject(); } + + // Add/update UnityMCP server in VSCode settings + existingConfig.mcp.servers.unityMCP = + JsonConvert.DeserializeObject( + JsonConvert.SerializeObject(unityMCPConfig) + ); break; default: @@ -379,15 +453,15 @@ namespace UnityMcpBridge.Editor.Windows { existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject(); } + + // Add/update UnityMCP server in standard MCP settings + existingConfig.mcpServers.unityMCP = + JsonConvert.DeserializeObject( + JsonConvert.SerializeObject(unityMCPConfig) + ); break; } - // Add/update UnityMCP server in VSCode settings - existingConfig.mcp.servers.unityMCP = - JsonConvert.DeserializeObject( - JsonConvert.SerializeObject(unityMCPConfig) - ); - // Write the merged configuration back to file string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings); File.WriteAllText(configPath, mergedJson); @@ -613,6 +687,50 @@ namespace UnityMcpBridge.Editor.Windows ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); } + private void LoadValidationLevelSetting() + { + string savedLevel = EditorPrefs.GetString("UnityMCP_ScriptValidationLevel", "standard"); + validationLevelIndex = savedLevel.ToLower() switch + { + "basic" => 0, + "standard" => 1, + "comprehensive" => 2, + "strict" => 3, + _ => 1 // Default to Standard + }; + } + + private void SaveValidationLevelSetting() + { + string levelString = validationLevelIndex switch + { + 0 => "basic", + 1 => "standard", + 2 => "comprehensive", + 3 => "strict", + _ => "standard" + }; + EditorPrefs.SetString("UnityMCP_ScriptValidationLevel", levelString); + } + + private string GetValidationLevelDescription(int index) + { + return index switch + { + 0 => "Only basic syntax checks (braces, quotes, comments)", + 1 => "Syntax checks + Unity best practices and warnings", + 2 => "All checks + semantic analysis and performance warnings", + 3 => "Full semantic validation with namespace/type resolution (requires Roslyn)", + _ => "Standard validation" + }; + } + + public static string GetCurrentValidationLevel() + { + string savedLevel = EditorPrefs.GetString("UnityMCP_ScriptValidationLevel", "standard"); + return savedLevel; + } + private void CheckMcpConfiguration(McpClient mcpClient) { try From 68efdf1bfecd679596f4237569c7cc375ab5e515 Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Fri, 25 Jul 2025 02:26:24 -0400 Subject: [PATCH 23/32] Clean up comment formatting in GameObjectSerializer --- .../Editor/Helpers/GameObjectSerializer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs index 51f6d97..5fc3fce 100644 --- a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs +++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs @@ -13,7 +13,7 @@ namespace UnityMcpBridge.Editor.Helpers /// /// Handles serialization of GameObjects and Components for MCP responses. /// Includes reflection helpers and caching for performance. - /// tew + ///
public static class GameObjectSerializer { // --- Data Serialization --- @@ -422,7 +422,7 @@ namespace UnityMcpBridge.Editor.Helpers catch (Exception e) { // Catch potential errors during JToken conversion or addition to dictionary - // Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping."); + Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping."); } } @@ -505,7 +505,7 @@ namespace UnityMcpBridge.Editor.Helpers // Helper to create JToken using the output serializer private static JToken CreateTokenFromValue(object value, Type type) { - if (value == null) return JValue.CreateNull(); + if (value == null) return JValue.CreateNull(); try { @@ -514,12 +514,12 @@ namespace UnityMcpBridge.Editor.Helpers } catch (JsonSerializationException e) { - // Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field."); - return null; // Indicate serialization failure + Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field."); + return null; // Indicate serialization failure } catch (Exception e) // Catch other unexpected errors { - // Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field."); + Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field."); return null; // Indicate serialization failure } } From 37db670427e27f76109d1e41c29009effab6a28a Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Fri, 25 Jul 2025 02:27:20 -0400 Subject: [PATCH 24/32] Fix missing closing brace in SetComponentPropertiesInternal method --- .../Editor/Tools/ManageGameObject.cs | 121 +++++++++++++----- 1 file changed, 86 insertions(+), 35 deletions(-) diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index 66c64cb..cb7b714 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -10,7 +10,6 @@ using UnityEditorInternal; using UnityEngine; using UnityEngine.SceneManagement; using UnityMcpBridge.Editor.Helpers; // For Response class AND GameObjectSerializer -using UnityMcpBridge.Runtime.Serialization; // <<< Keep for Converters access? Might not be needed here directly namespace UnityMcpBridge.Editor.Tools { @@ -23,10 +22,6 @@ namespace UnityMcpBridge.Editor.Tools public static object HandleCommand(JObject @params) { - // --- DEBUG --- Log the raw parameter value --- - // JToken rawIncludeFlag = @params["includeNonPublicSerialized"]; - // Debug.Log($"[HandleCommand Debug] Raw includeNonPublicSerialized parameter: Type={rawIncludeFlag?.Type.ToString() ?? "Null"}, Value={rawIncludeFlag?.ToString() ?? "N/A"}"); - // --- END DEBUG --- string action = @params["action"]?.ToString().ToLower(); if (string.IsNullOrEmpty(action)) @@ -219,17 +214,22 @@ namespace UnityMcpBridge.Editor.Tools $"[ManageGameObject.Create] Provided prefabPath '{prefabPath}' does not end with .prefab. Assuming it's missing and appending." ); prefabPath += ".prefab"; + // Note: This path might still not exist, AssetDatabase.LoadAssetAtPath will handle that. } + // The logic above now handles finding or assuming the .prefab extension. GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); if (prefabAsset != null) { try { + // Instantiate the prefab, initially place it at the root + // Parent will be set later if specified newGo = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject; if (newGo == null) { + // This might happen if the asset exists but isn't a valid GameObject prefab somehow Debug.LogError( $"[ManageGameObject.Create] Failed to instantiate prefab at '{prefabPath}', asset might be corrupted or not a GameObject." ); @@ -237,12 +237,12 @@ namespace UnityMcpBridge.Editor.Tools $"Failed to instantiate prefab at '{prefabPath}'." ); } - + // Name the instance based on the 'name' parameter, not the prefab's default name if (!string.IsNullOrEmpty(name)) { newGo.name = name; } - + // Register Undo for prefab instantiation Undo.RegisterCreatedObjectUndo( newGo, $"Instantiate Prefab '{prefabAsset.name}' as '{newGo.name}'" @@ -260,9 +260,12 @@ namespace UnityMcpBridge.Editor.Tools } else { + // Only return error if prefabPath was specified but not found. + // If prefabPath was empty/null, we proceed to create primitive/empty. Debug.LogWarning( $"[ManageGameObject.Create] Prefab asset not found at path: '{prefabPath}'. Will proceed to create new object if specified." ); + // Do not return error here, allow fallback to primitive/empty creation } } @@ -277,6 +280,7 @@ namespace UnityMcpBridge.Editor.Tools PrimitiveType type = (PrimitiveType) Enum.Parse(typeof(PrimitiveType), primitiveType, true); newGo = GameObject.CreatePrimitive(type); + // Set name *after* creation for primitives if (!string.IsNullOrEmpty(name)) newGo.name = name; else @@ -309,18 +313,21 @@ namespace UnityMcpBridge.Editor.Tools newGo = new GameObject(name); createdNewObject = true; } - + // Record creation for Undo *only* if we created a new object if (createdNewObject) { Undo.RegisterCreatedObjectUndo(newGo, $"Create GameObject '{newGo.name}'"); } } - + // --- Common Setup (Parent, Transform, Tag, Components) - Applied AFTER object exists --- if (newGo == null) { + // Should theoretically not happen if logic above is correct, but safety check. return Response.Error("Failed to create or instantiate the GameObject."); } + // Record potential changes to the existing prefab instance or the new GO + // Record transform separately in case parent changes affect it Undo.RecordObject(newGo.transform, "Set GameObject Transform"); Undo.RecordObject(newGo, "Set GameObject Properties"); @@ -352,6 +359,7 @@ namespace UnityMcpBridge.Editor.Tools // Set Tag (added for create action) if (!string.IsNullOrEmpty(tag)) { + // Similar logic as in ModifyGameObject for setting/creating tags string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; try { @@ -448,13 +456,16 @@ namespace UnityMcpBridge.Editor.Tools if (createdNewObject && saveAsPrefab) { string finalPrefabPath = prefabPath; // Use a separate variable for saving path + // This check should now happen *before* attempting to save if (string.IsNullOrEmpty(finalPrefabPath)) { + // Clean up the created object before returning error UnityEngine.Object.DestroyImmediate(newGo); return Response.Error( "'prefabPath' is required when 'saveAsPrefab' is true and creating a new object." ); } + // Ensure the *saving* path ends with .prefab if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) { Debug.Log( @@ -465,6 +476,7 @@ namespace UnityMcpBridge.Editor.Tools try { + // Ensure directory exists using the final saving path string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath); if ( !string.IsNullOrEmpty(directoryPath) @@ -477,7 +489,7 @@ namespace UnityMcpBridge.Editor.Tools $"[ManageGameObject.Create] Created directory for prefab: {directoryPath}" ); } - + // Use SaveAsPrefabAssetAndConnect with the final saving path finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect( newGo, finalPrefabPath, @@ -486,6 +498,7 @@ namespace UnityMcpBridge.Editor.Tools if (finalInstance == null) { + // Destroy the original if saving failed somehow (shouldn't usually happen if path is valid) UnityEngine.Object.DestroyImmediate(newGo); return Response.Error( $"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions." @@ -494,16 +507,21 @@ namespace UnityMcpBridge.Editor.Tools Debug.Log( $"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected." ); + // Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it. + // EditorUtility.SetDirty(finalInstance); // Instance is handled by SaveAsPrefabAssetAndConnect } catch (Exception e) { + // Clean up the instance if prefab saving fails UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt return Response.Error($"Error saving prefab '{finalPrefabPath}': {e.Message}"); } } + // Select the instance in the scene (either prefab instance or newly created/saved one) Selection.activeGameObject = finalInstance; + // Determine appropriate success message using the potentially updated or original path string messagePrefabPath = finalInstance == null ? originalPrefabPath @@ -529,6 +547,7 @@ namespace UnityMcpBridge.Editor.Tools } // Use the new serializer helper + //return Response.Success(successMessage, GetGameObjectData(finalInstance)); return Response.Success(successMessage, Helpers.GameObjectSerializer.GetGameObjectData(finalInstance)); } @@ -546,6 +565,7 @@ namespace UnityMcpBridge.Editor.Tools ); } + // Record state for Undo *before* modifications Undo.RecordObject(targetGo.transform, "Modify GameObject Transform"); Undo.RecordObject(targetGo, "Modify GameObject Properties"); @@ -564,6 +584,7 @@ namespace UnityMcpBridge.Editor.Tools if (parentToken != null) { GameObject newParentGo = FindObjectInternal(parentToken, "by_id_or_name_or_path"); + // Check for hierarchy loops if ( newParentGo == null && !( @@ -600,8 +621,11 @@ namespace UnityMcpBridge.Editor.Tools // Change Tag (using consolidated 'tag' parameter) string tag = @params["tag"]?.ToString(); + // Only attempt to change tag if a non-null tag is provided and it's different from the current one. + // Allow setting an empty string to remove the tag (Unity uses "Untagged"). if (tag != null && targetGo.tag != tag) { + // Ensure the tag is not empty, if empty, it means "Untagged" implicitly string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag; try { @@ -610,6 +634,7 @@ namespace UnityMcpBridge.Editor.Tools } catch (UnityException ex) { + // Check if the error is specifically because the tag doesn't exist if (ex.Message.Contains("is not defined")) { Debug.LogWarning( @@ -617,7 +642,12 @@ namespace UnityMcpBridge.Editor.Tools ); try { + // Attempt to create the tag using internal utility InternalEditorUtility.AddTag(tagToSet); + // Wait a frame maybe? Not strictly necessary but sometimes helps editor updates. + // yield return null; // Cannot yield here, editor script limitation + + // Retry setting the tag immediately after creation targetGo.tag = tagToSet; modified = true; Debug.Log( @@ -626,6 +656,7 @@ namespace UnityMcpBridge.Editor.Tools } catch (Exception innerEx) { + // Handle failure during tag creation or the second assignment attempt Debug.LogError( $"[ManageGameObject] Failed to create or assign tag '{tagToSet}' after attempting creation: {innerEx.Message}" ); @@ -636,6 +667,7 @@ namespace UnityMcpBridge.Editor.Tools } else { + // If the exception was for a different reason, return the original error return Response.Error($"Failed to set tag to '{tagToSet}': {ex.Message}."); } } @@ -681,12 +713,14 @@ namespace UnityMcpBridge.Editor.Tools } // --- Component Modifications --- + // Note: These might need more specific Undo recording per component // Remove Components if (@params["componentsToRemove"] is JArray componentsToRemoveArray) { foreach (var compToken in componentsToRemoveArray) { + // ... (parsing logic as in CreateGameObject) ... string typeName = compToken.ToString(); if (!string.IsNullOrEmpty(typeName)) { @@ -746,7 +780,11 @@ namespace UnityMcpBridge.Editor.Tools if (!modified) { - // Use the new serializer helper + // Use the new serializer helper + // return Response.Success( + // $"No modifications applied to GameObject '{targetGo.name}'.", + // GetGameObjectData(targetGo)); + return Response.Success( $"No modifications applied to GameObject '{targetGo.name}'.", Helpers.GameObjectSerializer.GetGameObjectData(targetGo) @@ -754,11 +792,15 @@ namespace UnityMcpBridge.Editor.Tools } EditorUtility.SetDirty(targetGo); // Mark scene as dirty - // Use the new serializer helper + // Use the new serializer helper return Response.Success( $"GameObject '{targetGo.name}' modified successfully.", Helpers.GameObjectSerializer.GetGameObjectData(targetGo) ); + // return Response.Success( + // $"GameObject '{targetGo.name}' modified successfully.", + // GetGameObjectData(targetGo)); + } private static object DeleteGameObject(JToken targetToken, string searchMethod) @@ -821,11 +863,12 @@ namespace UnityMcpBridge.Editor.Tools } // Use the new serializer helper + //var results = foundObjects.Select(go => GetGameObjectData(go)).ToList(); var results = foundObjects.Select(go => Helpers.GameObjectSerializer.GetGameObjectData(go)).ToList(); return Response.Success($"Found {results.Count} GameObject(s).", results); } - private static object GetComponentsFromTarget(string target, string searchMethod, bool includeNonPublicSerialized) + private static object GetComponentsFromTarget(string target, string searchMethod, bool includeNonPublicSerialized = true) { GameObject targetGo = FindObjectInternal(target, searchMethod); if (targetGo == null) @@ -1443,6 +1486,8 @@ namespace UnityMcpBridge.Editor.Tools Debug.LogWarning( $"[ManageGameObject] Could not set property '{propName}' on component '{compName}' ('{targetComponent.GetType().Name}'). Property might not exist, be read-only, or type mismatch." ); + // Optionally return an error here instead of just logging + // return Response.Error($"Could not set property '{propName}' on component '{compName}'."); } } catch (Exception e) @@ -1450,6 +1495,8 @@ namespace UnityMcpBridge.Editor.Tools Debug.LogError( $"[ManageGameObject] Error setting property '{propName}' on '{compName}': {e.Message}" ); + // Optionally return an error here + // return Response.Error($"Error setting property '{propName}' on '{compName}': {e.Message}"); } } EditorUtility.SetDirty(targetComponent); @@ -1488,6 +1535,7 @@ namespace UnityMcpBridge.Editor.Tools try { // Handle special case for materials with dot notation (material.property) + // Examples: material.color, sharedMaterial.color, materials[0].color if (memberName.Contains('.') || memberName.Contains('[')) { // Pass the inputSerializer down for nested conversions @@ -1538,7 +1586,8 @@ namespace UnityMcpBridge.Editor.Tools /// /// Sets a nested property using dot notation (e.g., "material.color") or array access (e.g., "materials[0]") /// - // Pass the input serializer for conversions + // Pass the input serializer for conversions + //Using the serializer helper private static bool SetNestedProperty(object target, string path, JToken value, JsonSerializer inputSerializer) { try @@ -1560,6 +1609,7 @@ namespace UnityMcpBridge.Editor.Tools bool isArray = false; int arrayIndex = -1; + // Check if this part contains array indexing if (part.Contains("[")) { int startBracket = part.IndexOf('['); @@ -1577,7 +1627,7 @@ namespace UnityMcpBridge.Editor.Tools } } } - + // Get the property/field PropertyInfo propInfo = currentType.GetProperty(part, flags); FieldInfo fieldInfo = null; if (propInfo == null) @@ -1592,11 +1642,12 @@ namespace UnityMcpBridge.Editor.Tools } } + // Get the value currentObject = propInfo != null ? propInfo.GetValue(currentObject) : fieldInfo.GetValue(currentObject); - + //Need to stop if current property is null if (currentObject == null) { Debug.LogWarning( @@ -1604,7 +1655,7 @@ namespace UnityMcpBridge.Editor.Tools ); return false; } - + // If this part was an array or list, access the specific index if (isArray) { if (currentObject is Material[]) @@ -1653,32 +1704,32 @@ namespace UnityMcpBridge.Editor.Tools { // Try converting to known types that SetColor/SetVector accept if (jArray.Count == 4) { - try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch {} - try { Vector4 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch {} + try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } + try { Vector4 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { } } else if (jArray.Count == 3) { - try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch {} // ToObject handles conversion to Color + try { Color color = value.ToObject(inputSerializer); material.SetColor(finalPart, color); return true; } catch { } // ToObject handles conversion to Color } else if (jArray.Count == 2) { - try { Vector2 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch {} + try { Vector2 vec = value.ToObject(inputSerializer); material.SetVector(finalPart, vec); return true; } catch { } } } else if (value.Type == JTokenType.Float || value.Type == JTokenType.Integer) { - try { material.SetFloat(finalPart, value.ToObject(inputSerializer)); return true; } catch {} + try { material.SetFloat(finalPart, value.ToObject(inputSerializer)); return true; } catch { } } else if (value.Type == JTokenType.Boolean) { - try { material.SetFloat(finalPart, value.ToObject(inputSerializer) ? 1f : 0f); return true; } catch {} + try { material.SetFloat(finalPart, value.ToObject(inputSerializer) ? 1f : 0f); return true; } catch { } } else if (value.Type == JTokenType.String) { // Try converting to Texture using the serializer/converter - try { - Texture texture = value.ToObject(inputSerializer); - if (texture != null) { - material.SetTexture(finalPart, texture); - return true; - } - } catch {} + try { + Texture texture = value.ToObject(inputSerializer); + if (texture != null) { + material.SetTexture(finalPart, texture); + return true; + } + } catch { } } Debug.LogWarning( @@ -1698,7 +1749,7 @@ namespace UnityMcpBridge.Editor.Tools finalPropInfo.SetValue(currentObject, convertedValue); return true; } - else { + else { Debug.LogWarning($"[SetNestedProperty] Final conversion failed for property '{finalPart}' (Type: {finalPropInfo.PropertyType.Name}) from token: {value.ToString(Formatting.None)}"); } } @@ -1707,16 +1758,16 @@ namespace UnityMcpBridge.Editor.Tools FieldInfo finalFieldInfo = currentType.GetField(finalPart, flags); if (finalFieldInfo != null) { - // Use the inputSerializer for conversion + // Use the inputSerializer for conversion object convertedValue = ConvertJTokenToType(value, finalFieldInfo.FieldType, inputSerializer); if (convertedValue != null || value.Type == JTokenType.Null) { finalFieldInfo.SetValue(currentObject, convertedValue); return true; } - else { - Debug.LogWarning($"[SetNestedProperty] Final conversion failed for field '{finalPart}' (Type: {finalFieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); - } + else { + Debug.LogWarning($"[SetNestedProperty] Final conversion failed for field '{finalPart}' (Type: {finalFieldInfo.FieldType.Name}) from token: {value.ToString(Formatting.None)}"); + } } else { From 46d7271e3db9e16b6cbe36463a3b334467b372fb Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Fri, 25 Jul 2025 02:32:19 -0400 Subject: [PATCH 25/32] Update ManageGameObject.cs Update with added comments --- UnityMcpBridge/Editor/Tools/ManageGameObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index cb7b714..d6aa9f8 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -1793,6 +1793,7 @@ namespace UnityMcpBridge.Editor.Tools ///
private static string[] SplitPropertyPath(string path) { + // Handle complex paths with both dots and array indexers List parts = new List(); int startIndex = 0; bool inBrackets = false; @@ -1811,6 +1812,7 @@ namespace UnityMcpBridge.Editor.Tools } else if (c == '.' && !inBrackets) { + // Found a dot separator outside of brackets parts.Add(path.Substring(startIndex, i - startIndex)); startIndex = i + 1; } From a2ae3d51ef5ce04806ac6d624c8ad11874dbb574 Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Fri, 25 Jul 2025 02:44:00 -0400 Subject: [PATCH 26/32] Update ManageGameObject.cs Minor fix --- UnityMcpBridge/Editor/Tools/ManageGameObject.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index 414603d..bf19161 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -9,6 +9,7 @@ using UnityEditorInternal; using UnityEngine; using UnityEngine.SceneManagement; using UnityMcpBridge.Editor.Helpers; // For Response class +using UnityMcpBridge.Runtime.Serialization; namespace UnityMcpBridge.Editor.Tools { From 3ca4746a784efe327adb6fcaeb847c356985a7b3 Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Sun, 27 Jul 2025 23:57:18 -0400 Subject: [PATCH 27/32] Update README.md --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 2d8f82e..19e81d4 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Unity MCP connects your tools using two components: ``` * **An MCP Client:** * [Claude Desktop](https://claude.ai/download) + * [Claude Code](https://github.com/anthropics/claude-code) * [Cursor](https://www.cursor.com/en/downloads) * [Visual Studio Code Copilot](https://code.visualstudio.com/docs/copilot/overview) * *(Others may work with manual config)* @@ -195,6 +196,20 @@ If Auto-Configure fails or you use a different client: +**Option C: Claude Code Registration** + +If you're using Claude Code, you can register the MCP server using these commands: + +**macOS:** +```bash +claude mcp add UnityMCP -- uv --directory /[PATH_TO]/UnityMCP/UnityMcpServer/src run server.py +``` + +**Windows:** +```bash +claude mcp add UnityMCP -- "C:/Users/USERNAME/AppData/Roaming/Python/Python313/Scripts/uv.exe" --directory "C:/Users/USERNAME/AppData/Local/Programs/UnityMCP/UnityMcpServer/src" run server.py +``` + --- ## Usage ▶️ From cb59b08b5166778af5a67887c6dee80c14924196 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Mon, 28 Jul 2025 10:45:07 -0700 Subject: [PATCH 28/32] Add Claude Code support with register/unregister toggle - Added Claude Code as new MCP client type - One-click registration via 'claude mcp add' command - Toggle button to unregister when already configured - Cross-platform support (Windows/macOS/Linux) - Auto-detects configuration in ~/.claude.json --- UnityMcpBridge/Editor/Data/McpClients.cs | 14 + UnityMcpBridge/Editor/Models/McpTypes.cs | 1 + .../Windows/ManualConfigEditorWindow.cs | 35 +- .../Editor/Windows/UnityMcpEditorWindow.cs | 368 ++++++++++++++++-- 4 files changed, 371 insertions(+), 47 deletions(-) diff --git a/UnityMcpBridge/Editor/Data/McpClients.cs b/UnityMcpBridge/Editor/Data/McpClients.cs index 2360ab1..9b2d7c1 100644 --- a/UnityMcpBridge/Editor/Data/McpClients.cs +++ b/UnityMcpBridge/Editor/Data/McpClients.cs @@ -63,6 +63,20 @@ namespace UnityMcpBridge.Editor.Data mcpType = McpTypes.VSCode, configStatus = "Not Configured", }, + new() + { + name = "Claude Code", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".claude.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".claude.json" + ), + mcpType = McpTypes.ClaudeCode, + configStatus = "Not Configured", + }, }; // Initialize status enums after construction diff --git a/UnityMcpBridge/Editor/Models/McpTypes.cs b/UnityMcpBridge/Editor/Models/McpTypes.cs index 0259b9c..cb691a2 100644 --- a/UnityMcpBridge/Editor/Models/McpTypes.cs +++ b/UnityMcpBridge/Editor/Models/McpTypes.cs @@ -5,6 +5,7 @@ namespace UnityMcpBridge.Editor.Models ClaudeDesktop, Cursor, VSCode, + ClaudeCode, } } diff --git a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs index 07b7da3..89f6099 100644 --- a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs @@ -39,7 +39,7 @@ namespace UnityMcpBridge.Editor.Windows ); GUI.Label( new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height), - mcpClient.name + " Manual Configuration", + (mcpClient?.name ?? "Unknown") + " Manual Configuration", EditorStyles.boldLabel ); EditorGUILayout.Space(10); @@ -70,17 +70,17 @@ namespace UnityMcpBridge.Editor.Windows }; EditorGUILayout.LabelField( - "1. Open " + mcpClient.name + " config file by either:", + "1. Open " + (mcpClient?.name ?? "Unknown") + " config file by either:", instructionStyle ); - if (mcpClient.mcpType == McpTypes.ClaudeDesktop) + if (mcpClient?.mcpType == McpTypes.ClaudeDesktop) { EditorGUILayout.LabelField( " a) Going to Settings > Developer > Edit Config", instructionStyle ); } - else if (mcpClient.mcpType == McpTypes.Cursor) + else if (mcpClient?.mcpType == McpTypes.Cursor) { EditorGUILayout.LabelField( " a) Going to File > Preferences > Cursor Settings > MCP > Add new global MCP server", @@ -96,16 +96,23 @@ namespace UnityMcpBridge.Editor.Windows // Path section with improved styling EditorGUILayout.BeginVertical(EditorStyles.helpBox); string displayPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (mcpClient != null) { - displayPath = mcpClient.windowsConfigPath; - } - else if ( - RuntimeInformation.IsOSPlatform(OSPlatform.OSX) - || RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - ) - { - displayPath = mcpClient.linuxConfigPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + displayPath = mcpClient.windowsConfigPath; + } + else if ( + RuntimeInformation.IsOSPlatform(OSPlatform.OSX) + || RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + ) + { + displayPath = mcpClient.linuxConfigPath; + } + else + { + displayPath = configPath; + } } else { @@ -224,7 +231,7 @@ namespace UnityMcpBridge.Editor.Windows EditorGUILayout.Space(10); EditorGUILayout.LabelField( - "3. Save the file and restart " + mcpClient.name, + "3. Save the file and restart " + (mcpClient?.name ?? "Unknown"), instructionStyle ); diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 3b1e69d..80cd65b 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -53,6 +54,16 @@ namespace UnityMcpBridge.Editor.Windows // Load validation level setting LoadValidationLevelSetting(); } + + private void OnFocus() + { + // Refresh configuration status when window gains focus + foreach (McpClient mcpClient in mcpClients.clients) + { + CheckMcpConfiguration(mcpClient); + } + Repaint(); + } private Color GetStatusColor(McpStatus status) { @@ -326,6 +337,23 @@ namespace UnityMcpBridge.Editor.Windows ConfigureMcpClient(mcpClient); } } + else if (mcpClient.mcpType == McpTypes.ClaudeCode) + { + bool isConfigured = mcpClient.status == McpStatus.Configured; + string buttonText = isConfigured ? "Unregister UnityMCP with Claude Code" : "Register with Claude Code"; + if (GUILayout.Button(buttonText, GUILayout.Height(32))) + { + if (isConfigured) + { + UnregisterWithClaudeCode(); + } + else + { + string pythonDir = FindPackagePythonDirectory(); + RegisterWithClaudeCode(pythonDir); + } + } + } else { if (GUILayout.Button($"Auto Configure", GUILayout.Height(32))) @@ -334,36 +362,39 @@ namespace UnityMcpBridge.Editor.Windows } } - if (GUILayout.Button("Manual Setup", GUILayout.Height(32))) + if (mcpClient.mcpType != McpTypes.ClaudeCode) { - string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? mcpClient.windowsConfigPath - : mcpClient.linuxConfigPath; - - if (mcpClient.mcpType == McpTypes.VSCode) + if (GUILayout.Button("Manual Setup", GUILayout.Height(32))) { - string pythonDir = FindPackagePythonDirectory(); - var vscodeConfig = new + string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? mcpClient.windowsConfigPath + : mcpClient.linuxConfigPath; + + if (mcpClient.mcpType == McpTypes.VSCode) { - mcp = new + string pythonDir = FindPackagePythonDirectory(); + var vscodeConfig = new { - servers = new + mcp = new { - unityMCP = new + servers = new { - command = "uv", - args = new[] { "--directory", pythonDir, "run", "server.py" } + unityMCP = new + { + command = "uv", + args = new[] { "--directory", pythonDir, "run", "server.py" } + } } } - } - }; - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); - VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); - } - else - { - ShowManualInstructionsWindow(configPath, mcpClient); + }; + JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; + string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); + VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); + } + else + { + ShowManualInstructionsWindow(configPath, mcpClient); + } } } @@ -413,7 +444,7 @@ namespace UnityMcpBridge.Editor.Windows } catch (Exception e) { - Debug.LogWarning($"Error reading existing config: {e.Message}."); + UnityEngine.Debug.LogWarning($"Error reading existing config: {e.Message}."); } } @@ -546,12 +577,17 @@ namespace UnityMcpBridge.Editor.Windows if (package.name == "com.justinpbarnett.unity-mcp") { string packagePath = package.resolvedPath; + + // Check for local package structure (UnityMcpServer/src) + string localPythonDir = Path.Combine(Path.GetDirectoryName(packagePath), "UnityMcpServer", "src"); + if (Directory.Exists(localPythonDir) && File.Exists(Path.Combine(localPythonDir, "server.py"))) + { + return localPythonDir; + } + + // Check for old structure (Python subdirectory) string potentialPythonDir = Path.Combine(packagePath, "Python"); - - if ( - Directory.Exists(potentialPythonDir) - && File.Exists(Path.Combine(potentialPythonDir, "server.py")) - ) + if (Directory.Exists(potentialPythonDir) && File.Exists(Path.Combine(potentialPythonDir, "server.py"))) { return potentialPythonDir; } @@ -560,13 +596,22 @@ namespace UnityMcpBridge.Editor.Windows } else if (request.Error != null) { - Debug.LogError("Failed to list packages: " + request.Error.message); + UnityEngine.Debug.LogError("Failed to list packages: " + request.Error.message); } // If not found via Package Manager, try manual approaches - // First check for local installation + // Check for local development structure string[] possibleDirs = { + // Check in the Unity project's Packages folder (for local package development) + Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Packages", "unity-mcp", "UnityMcpServer", "src")), + // Check relative to the Unity project (for development) + Path.GetFullPath(Path.Combine(Application.dataPath, "..", "unity-mcp", "UnityMcpServer", "src")), + // Check in user's home directory (common installation location) + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "unity-mcp", "UnityMcpServer", "src"), + // Check in Applications folder (macOS/Linux common location) + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications", "UnityMCP", "UnityMcpServer", "src"), + // Legacy Python folder structure Path.GetFullPath(Path.Combine(Application.dataPath, "unity-mcp", "Python")), }; @@ -579,11 +624,11 @@ namespace UnityMcpBridge.Editor.Windows } // If still not found, return the placeholder path - Debug.LogWarning("Could not find Python directory, using placeholder path"); + UnityEngine.Debug.LogWarning("Could not find Python directory, using placeholder path"); } catch (Exception e) { - Debug.LogError($"Error finding package path: {e.Message}"); + UnityEngine.Debug.LogError($"Error finding package path: {e.Message}"); } return pythonDir; @@ -651,7 +696,7 @@ namespace UnityMcpBridge.Editor.Windows } ShowManualInstructionsWindow(configPath, mcpClient); - Debug.LogError( + UnityEngine.Debug.LogError( $"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}" ); return $"Failed to configure {mcpClient.name}"; @@ -735,6 +780,13 @@ namespace UnityMcpBridge.Editor.Windows { try { + // Special handling for Claude Code + if (mcpClient.mcpType == McpTypes.ClaudeCode) + { + CheckClaudeCodeConfiguration(mcpClient); + return; + } + string configPath; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -814,5 +866,255 @@ namespace UnityMcpBridge.Editor.Windows mcpClient.SetStatus(McpStatus.Error, e.Message); } } + + private void RegisterWithClaudeCode(string pythonDir) + { + string command; + string args; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + command = "claude"; + + // Try to find uv.exe in common locations + string uvPath = FindWindowsUvPath(); + + if (string.IsNullOrEmpty(uvPath)) + { + // Fallback to expecting uv in PATH + args = $"mcp add UnityMCP -- uv --directory \"{pythonDir}\" run server.py"; + } + else + { + args = $"mcp add UnityMCP -- \"{uvPath}\" --directory \"{pythonDir}\" run server.py"; + } + } + else + { + // Use full path to claude command + command = "/usr/local/bin/claude"; + args = $"mcp add UnityMCP -- uv --directory \"{pythonDir}\" run server.py"; + } + + try + { + // Get the Unity project directory (where the Assets folder is) + string unityProjectDir = Application.dataPath; + string projectDir = Path.GetDirectoryName(unityProjectDir); + + var psi = new ProcessStartInfo + { + FileName = command, + Arguments = args, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = projectDir // Set working directory to Unity project directory + }; + + // Set PATH to include common binary locations + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + string additionalPaths = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"; + psi.EnvironmentVariables["PATH"] = $"{additionalPaths}:{currentPath}"; + + using var process = Process.Start(psi); + string output = process.StandardOutput.ReadToEnd(); + string errors = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + + + // Check for success or already exists + if (output.Contains("Added stdio MCP server") || errors.Contains("already exists")) + { + // Force refresh the configuration status + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) + { + CheckMcpConfiguration(claudeClient); + } + Repaint(); + + + } + else if (!string.IsNullOrEmpty(errors)) + { + UnityEngine.Debug.LogWarning($"Claude MCP errors: {errors}"); + } + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"Claude CLI registration failed: {e.Message}"); + } + } + + private void UnregisterWithClaudeCode() + { + string command; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + command = "claude"; + } + else + { + // Use full path to claude command + command = "/usr/local/bin/claude"; + } + + try + { + // Get the Unity project directory (where the Assets folder is) + string unityProjectDir = Application.dataPath; + string projectDir = Path.GetDirectoryName(unityProjectDir); + + var psi = new ProcessStartInfo + { + FileName = command, + Arguments = "mcp remove UnityMCP", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = projectDir // Set working directory to Unity project directory + }; + + // Set PATH to include common binary locations + string currentPath = Environment.GetEnvironmentVariable("PATH") ?? ""; + string additionalPaths = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"; + psi.EnvironmentVariables["PATH"] = $"{additionalPaths}:{currentPath}"; + + using var process = Process.Start(psi); + string output = process.StandardOutput.ReadToEnd(); + string errors = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + // Check for success + if (output.Contains("Removed MCP server") || process.ExitCode == 0) + { + // Force refresh the configuration status + var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); + if (claudeClient != null) + { + CheckMcpConfiguration(claudeClient); + } + Repaint(); + + UnityEngine.Debug.Log("UnityMCP server successfully unregistered from Claude Code."); + } + else if (!string.IsNullOrEmpty(errors)) + { + UnityEngine.Debug.LogWarning($"Claude MCP removal errors: {errors}"); + } + } + catch (Exception e) + { + UnityEngine.Debug.LogError($"Claude CLI unregistration failed: {e.Message}"); + } + } + + private string FindWindowsUvPath() + { + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + + // Check for different Python versions + string[] pythonVersions = { "Python313", "Python312", "Python311", "Python310", "Python39", "Python38" }; + + foreach (string version in pythonVersions) + { + string uvPath = Path.Combine(appData, version, "Scripts", "uv.exe"); + if (File.Exists(uvPath)) + { + return uvPath; + } + } + + // Check Program Files locations + string[] programFilesPaths = { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Python"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Python"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Python") + }; + + foreach (string basePath in programFilesPaths) + { + if (Directory.Exists(basePath)) + { + foreach (string dir in Directory.GetDirectories(basePath, "Python*")) + { + string uvPath = Path.Combine(dir, "Scripts", "uv.exe"); + if (File.Exists(uvPath)) + { + return uvPath; + } + } + } + } + + return null; // Will fallback to using 'uv' from PATH + } + + private void CheckClaudeCodeConfiguration(McpClient mcpClient) + { + try + { + // Get the Unity project directory to check project-specific config + string unityProjectDir = Application.dataPath; + string projectDir = Path.GetDirectoryName(unityProjectDir); + + // Read the global Claude config file + string configPath = mcpClient.linuxConfigPath; // ~/.claude.json + if (!File.Exists(configPath)) + { + mcpClient.SetStatus(McpStatus.NotConfigured); + return; + } + + string configJson = File.ReadAllText(configPath); + dynamic claudeConfig = JsonConvert.DeserializeObject(configJson); + + // Check for UnityMCP server in the mcpServers section (current format) + if (claudeConfig?.mcpServers != null) + { + var servers = claudeConfig.mcpServers; + if (servers.UnityMCP != null || servers.unityMCP != null) + { + // Found UnityMCP configured + mcpClient.SetStatus(McpStatus.Configured); + return; + } + } + + // Also check if there's a project-specific configuration for this Unity project (legacy format) + if (claudeConfig?.projects != null) + { + // Look for the project path in the config + foreach (var project in claudeConfig.projects) + { + string projectPath = project.Name; + if (projectPath == projectDir && project.Value?.mcpServers != null) + { + // Check for UnityMCP (case variations) + var servers = project.Value.mcpServers; + if (servers.UnityMCP != null || servers.unityMCP != null) + { + // Found UnityMCP configured for this project + mcpClient.SetStatus(McpStatus.Configured); + return; + } + } + } + } + + // No configuration found for this project + mcpClient.SetStatus(McpStatus.NotConfigured); + } + catch (Exception e) + { + UnityEngine.Debug.LogWarning($"Error checking Claude Code config: {e.Message}"); + mcpClient.SetStatus(McpStatus.Error, e.Message); + } + } } } From 2749b4e0c0ff61e3ae1061b8dab457b19a545fc5 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Mon, 28 Jul 2025 11:55:08 -0700 Subject: [PATCH 29/32] feat: Add comprehensive UV error handling and installation guidance - Enhanced FindUvPath() to return null when UV is not found - Added detailed installation instructions for all platforms - Implemented null checks in all UV usage points - Added cross-platform path resolution for Windows, macOS, and Linux - Improved user experience with clear error messages instead of silent failures - Prevents 'spawn uv ENOENT' errors by using full paths and proper validation --- .../Editor/Windows/UnityMcpEditorWindow.cs | 104 +++++++++++++++++- 1 file changed, 100 insertions(+), 4 deletions(-) diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 80cd65b..9d63585 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -373,6 +373,13 @@ namespace UnityMcpBridge.Editor.Windows if (mcpClient.mcpType == McpTypes.VSCode) { string pythonDir = FindPackagePythonDirectory(); + string uvPath = FindUvPath(); + if (uvPath == null) + { + UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode."); + return; + } + var vscodeConfig = new { mcp = new @@ -381,7 +388,7 @@ namespace UnityMcpBridge.Editor.Windows { unityMCP = new { - command = "uv", + command = uvPath, args = new[] { "--directory", pythonDir, "run", "server.py" } } } @@ -425,10 +432,16 @@ namespace UnityMcpBridge.Editor.Windows private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null) { + string uvPath = FindUvPath(); + if (uvPath == null) + { + return "UV package manager not found. Please install UV first."; + } + // Create configuration object for unityMCP McpConfigServer unityMCPConfig = new() { - command = "uv", + command = uvPath, args = new[] { "--directory", pythonDir, "run", "server.py" }, }; @@ -541,13 +554,20 @@ namespace UnityMcpBridge.Editor.Windows default: // Create standard MCP configuration for other clients + string uvPath = FindUvPath(); + if (uvPath == null) + { + UnityEngine.Debug.LogError("UV package manager not found. Cannot configure manual setup."); + return; + } + McpConfig jsonConfig = new() { mcpServers = new McpConfigServers { unityMCP = new McpConfigServer { - command = "uv", + command = uvPath, args = new[] { "--directory", pythonDir, "run", "server.py" }, }, }, @@ -714,13 +734,20 @@ namespace UnityMcpBridge.Editor.Windows string pythonDir = FindPackagePythonDirectory(); // Create the manual configuration message + string uvPath = FindUvPath(); + if (uvPath == null) + { + UnityEngine.Debug.LogError("UV package manager not found. Cannot configure manual setup."); + return; + } + McpConfig jsonConfig = new() { mcpServers = new McpConfigServers { unityMCP = new McpConfigServer { - command = "uv", + command = uvPath, args = new[] { "--directory", pythonDir, "run", "server.py" }, }, }, @@ -1014,6 +1041,75 @@ namespace UnityMcpBridge.Editor.Windows } } + private string FindUvPath() + { + string uvPath = null; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + uvPath = FindWindowsUvPath(); + } + else + { + // macOS/Linux paths + string[] possiblePaths = { + "/Library/Frameworks/Python.framework/Versions/3.13/bin/uv", + "/usr/local/bin/uv", + "/opt/homebrew/bin/uv", + "/usr/bin/uv" + }; + + foreach (string path in possiblePaths) + { + if (File.Exists(path)) + { + uvPath = path; + break; + } + } + + // If not found in common locations, try to find via which command + if (uvPath == null) + { + try + { + var psi = new ProcessStartInfo + { + FileName = "which", + Arguments = "uv", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + if (!string.IsNullOrEmpty(output) && File.Exists(output)) + { + uvPath = output; + } + } + catch + { + // Ignore errors + } + } + } + + if (uvPath == null) + { + UnityEngine.Debug.LogError("UV package manager not found! Please install UV first:\n" + + "• macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh\n" + + "• Windows: pip install uv\n" + + "• Or visit: https://docs.astral.sh/uv/getting-started/installation"); + return null; + } + + return uvPath; + } + private string FindWindowsUvPath() { string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); From 7dbb03b84ec493632e4e4f7825874567f7a5cbac Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Mon, 28 Jul 2025 18:11:23 -0400 Subject: [PATCH 30/32] Update UnityMcpEditorWindow.cs Prevention for CLI to be found on Windows. --- .../Editor/Windows/UnityMcpEditorWindow.cs | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 9d63585..46da78b 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -901,7 +901,13 @@ namespace UnityMcpBridge.Editor.Windows if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - command = "claude"; + command = FindClaudeCommand(); + + if (string.IsNullOrEmpty(command)) + { + UnityEngine.Debug.LogError("Claude CLI not found. Please ensure Claude Code is installed and accessible."); + return; + } // Try to find uv.exe in common locations string uvPath = FindWindowsUvPath(); @@ -982,7 +988,13 @@ namespace UnityMcpBridge.Editor.Windows if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - command = "claude"; + command = FindClaudeCommand(); + + if (string.IsNullOrEmpty(command)) + { + UnityEngine.Debug.LogError("Claude CLI not found. Please ensure Claude Code is installed and accessible."); + return; + } } else { @@ -1151,6 +1163,68 @@ namespace UnityMcpBridge.Editor.Windows return null; // Will fallback to using 'uv' from PATH } + private string FindClaudeCommand() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Common locations for Claude CLI on Windows + string[] possiblePaths = { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "npm", "claude.cmd"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "npm", "claude.cmd"), + "claude.cmd", // Fallback to PATH + "claude" // Final fallback + }; + + foreach (string path in possiblePaths) + { + if (path.Contains("\\") && File.Exists(path)) + { + return path; + } + } + + // Try to find via where command + try + { + var psi = new ProcessStartInfo + { + FileName = "where", + Arguments = "claude", + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + using var process = Process.Start(psi); + string output = process.StandardOutput.ReadToEnd().Trim(); + process.WaitForExit(); + + if (!string.IsNullOrEmpty(output)) + { + string[] lines = output.Split('\n'); + foreach (string line in lines) + { + string cleanPath = line.Trim(); + if (File.Exists(cleanPath)) + { + return cleanPath; + } + } + } + } + catch + { + // Ignore errors and fall back + } + + return "claude"; // Final fallback to PATH + } + else + { + return "/usr/local/bin/claude"; + } + } + private void CheckClaudeCodeConfiguration(McpClient mcpClient) { try From 92ad2c50314fdbe9feda8c1e1ede6144f8d83d80 Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Mon, 28 Jul 2025 18:30:33 -0400 Subject: [PATCH 31/32] Minor Changes Add successfully registration info and reorder the seleciton --- UnityMcpBridge/Editor/Data/McpClients.cs | 28 +++++++++---------- .../Editor/Windows/UnityMcpEditorWindow.cs | 3 +- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/UnityMcpBridge/Editor/Data/McpClients.cs b/UnityMcpBridge/Editor/Data/McpClients.cs index 9b2d7c1..dec53c8 100644 --- a/UnityMcpBridge/Editor/Data/McpClients.cs +++ b/UnityMcpBridge/Editor/Data/McpClients.cs @@ -28,6 +28,20 @@ namespace UnityMcpBridge.Editor.Data configStatus = "Not Configured", }, new() + { + name = "Claude Code", + windowsConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".claude.json" + ), + linuxConfigPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".claude.json" + ), + mcpType = McpTypes.ClaudeCode, + configStatus = "Not Configured", + }, + new() { name = "Cursor", windowsConfigPath = Path.Combine( @@ -63,20 +77,6 @@ namespace UnityMcpBridge.Editor.Data mcpType = McpTypes.VSCode, configStatus = "Not Configured", }, - new() - { - name = "Claude Code", - windowsConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".claude.json" - ), - linuxConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".claude.json" - ), - mcpType = McpTypes.ClaudeCode, - configStatus = "Not Configured", - }, }; // Initialize status enums after construction diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 46da78b..561cd39 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -957,7 +957,7 @@ namespace UnityMcpBridge.Editor.Windows process.WaitForExit(); - + // Check for success or already exists if (output.Contains("Added stdio MCP server") || errors.Contains("already exists")) { @@ -968,6 +968,7 @@ namespace UnityMcpBridge.Editor.Windows CheckMcpConfiguration(claudeClient); } Repaint(); + UnityEngine.Debug.Log("UnityMCP server successfully registered from Claude Code."); } From 32e4b2642f41339cb38913401bfc1ea4a40ac262 Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Tue, 29 Jul 2025 00:17:36 -0400 Subject: [PATCH 32/32] Unity-MCP AutoConnect Autoconnect feature to prevent the port being taken by other applications. --- UnityMcpBridge/Editor/Helpers/PortManager.cs | 195 ++++++++++++++++++ .../Editor/Helpers/PortManager.cs.meta | 11 + UnityMcpBridge/Editor/UnityMcpBridge.cs | 43 +++- .../Editor/Windows/UnityMcpEditorWindow.cs | 37 +++- UnityMcpServer/src/config.py | 2 +- UnityMcpServer/src/port_discovery.py | 69 +++++++ UnityMcpServer/src/unity_connection.py | 8 +- 7 files changed, 356 insertions(+), 9 deletions(-) create mode 100644 UnityMcpBridge/Editor/Helpers/PortManager.cs create mode 100644 UnityMcpBridge/Editor/Helpers/PortManager.cs.meta create mode 100644 UnityMcpServer/src/port_discovery.py diff --git a/UnityMcpBridge/Editor/Helpers/PortManager.cs b/UnityMcpBridge/Editor/Helpers/PortManager.cs new file mode 100644 index 0000000..8e368a6 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/PortManager.cs @@ -0,0 +1,195 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using Newtonsoft.Json; +using UnityEngine; + +namespace UnityMcpBridge.Editor.Helpers +{ + /// + /// Manages dynamic port allocation and persistent storage for Unity MCP Bridge + /// + public static class PortManager + { + private const int DefaultPort = 6400; + private const int MaxPortAttempts = 100; + private const string RegistryFileName = "unity-mcp-port.json"; + + [Serializable] + public class PortConfig + { + public int unity_port; + public string created_date; + public string project_path; + } + + /// + /// Get the port to use - either from storage or discover a new one + /// Will try stored port first, then fallback to discovering new port + /// + /// Port number to use + public static int GetPortWithFallback() + { + // Try to load stored port first + int storedPort = LoadStoredPort(); + if (storedPort > 0 && IsPortAvailable(storedPort)) + { + Debug.Log($"Using stored port {storedPort}"); + return storedPort; + } + + // If no stored port or stored port is unavailable, find a new one + int newPort = FindAvailablePort(); + SavePort(newPort); + return newPort; + } + + /// + /// Discover and save a new available port (used by Auto-Connect button) + /// + /// New available port + public static int DiscoverNewPort() + { + int newPort = FindAvailablePort(); + SavePort(newPort); + Debug.Log($"Discovered and saved new port: {newPort}"); + return newPort; + } + + /// + /// Find an available port starting from the default port + /// + /// Available port number + private static int FindAvailablePort() + { + // Always try default port first + if (IsPortAvailable(DefaultPort)) + { + Debug.Log($"Using default port {DefaultPort}"); + return DefaultPort; + } + + Debug.Log($"Default port {DefaultPort} is in use, searching for alternative..."); + + // Search for alternatives + for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++) + { + if (IsPortAvailable(port)) + { + Debug.Log($"Found available port {port}"); + return port; + } + } + + throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}"); + } + + /// + /// Check if a specific port is available + /// + /// Port to check + /// True if port is available + public static bool IsPortAvailable(int port) + { + try + { + var testListener = new TcpListener(IPAddress.Loopback, port); + testListener.Start(); + testListener.Stop(); + return true; + } + catch (SocketException) + { + return false; + } + } + + /// + /// Save port to persistent storage + /// + /// Port to save + private static void SavePort(int port) + { + try + { + var portConfig = new PortConfig + { + unity_port = port, + created_date = DateTime.UtcNow.ToString("O"), + project_path = Application.dataPath + }; + + string registryDir = GetRegistryDirectory(); + Directory.CreateDirectory(registryDir); + + string registryFile = Path.Combine(registryDir, RegistryFileName); + string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented); + File.WriteAllText(registryFile, json); + + Debug.Log($"Saved port {port} to storage"); + } + catch (Exception ex) + { + Debug.LogWarning($"Could not save port to storage: {ex.Message}"); + } + } + + /// + /// Load port from persistent storage + /// + /// Stored port number, or 0 if not found + private static int LoadStoredPort() + { + try + { + string registryFile = Path.Combine(GetRegistryDirectory(), RegistryFileName); + + if (!File.Exists(registryFile)) + { + return 0; + } + + string json = File.ReadAllText(registryFile); + var portConfig = JsonConvert.DeserializeObject(json); + + return portConfig?.unity_port ?? 0; + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load port from storage: {ex.Message}"); + return 0; + } + } + + /// + /// Get the current stored port configuration + /// + /// Port configuration if exists, null otherwise + public static PortConfig GetStoredPortConfig() + { + try + { + string registryFile = Path.Combine(GetRegistryDirectory(), RegistryFileName); + + if (!File.Exists(registryFile)) + { + return null; + } + + string json = File.ReadAllText(registryFile); + return JsonConvert.DeserializeObject(json); + } + catch (Exception ex) + { + Debug.LogWarning($"Could not load port config: {ex.Message}"); + return null; + } + } + + private static string GetRegistryDirectory() + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp"); + } + } +} \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Helpers/PortManager.cs.meta b/UnityMcpBridge/Editor/Helpers/PortManager.cs.meta new file mode 100644 index 0000000..ee3f667 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/PortManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1b2c3d4e5f6789012345678901234ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.cs b/UnityMcpBridge/Editor/UnityMcpBridge.cs index 2242cd6..4f3a608 100644 --- a/UnityMcpBridge/Editor/UnityMcpBridge.cs +++ b/UnityMcpBridge/Editor/UnityMcpBridge.cs @@ -25,9 +25,40 @@ namespace UnityMcpBridge.Editor string, (string commandJson, TaskCompletionSource tcs) > commandQueue = new(); - private static readonly int unityPort = 6400; // Hardcoded port + private static int currentUnityPort = 6400; // Dynamic port, starts with default + private static bool isAutoConnectMode = false; public static bool IsRunning => isRunning; + public static int GetCurrentPort() => currentUnityPort; + public static bool IsAutoConnectMode() => isAutoConnectMode; + + /// + /// Start with Auto-Connect mode - discovers new port and saves it + /// + public static void StartAutoConnect() + { + Stop(); // Stop current connection + + try + { + // Discover new port and save it + currentUnityPort = PortManager.DiscoverNewPort(); + + listener = new TcpListener(IPAddress.Loopback, currentUnityPort); + listener.Start(); + isRunning = true; + isAutoConnectMode = true; + + Debug.Log($"UnityMcpBridge auto-connected on port {currentUnityPort}"); + Task.Run(ListenerLoop); + EditorApplication.update += ProcessCommands; + } + catch (Exception ex) + { + Debug.LogError($"Auto-connect failed: {ex.Message}"); + throw; + } + } public static bool FolderExists(string path) { @@ -74,10 +105,14 @@ namespace UnityMcpBridge.Editor try { - listener = new TcpListener(IPAddress.Loopback, unityPort); + // Use PortManager to get available port with automatic fallback + currentUnityPort = PortManager.GetPortWithFallback(); + + listener = new TcpListener(IPAddress.Loopback, currentUnityPort); listener.Start(); isRunning = true; - Debug.Log($"UnityMcpBridge started on port {unityPort}."); + isAutoConnectMode = false; // Normal startup mode + Debug.Log($"UnityMcpBridge started on port {currentUnityPort}."); // Assuming ListenerLoop and ProcessCommands are defined elsewhere Task.Run(ListenerLoop); EditorApplication.update += ProcessCommands; @@ -87,7 +122,7 @@ namespace UnityMcpBridge.Editor if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) { Debug.LogError( - $"Port {unityPort} is already in use. Ensure no other instances are running or change the port." + $"Port {currentUnityPort} is already in use. This should not happen with dynamic port allocation." ); } else diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 561cd39..62c919d 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -18,8 +18,7 @@ namespace UnityMcpBridge.Editor.Windows private Vector2 scrollPosition; private string pythonServerInstallationStatus = "Not Installed"; private Color pythonServerInstallationStatusColor = Color.red; - private const int unityPort = 6400; // Hardcoded Unity port - private const int mcpPort = 6500; // Hardcoded MCP port + private const int mcpPort = 6500; // MCP port (still hardcoded for MCP server) private readonly McpClients mcpClients = new(); // Script validation settings @@ -45,6 +44,7 @@ namespace UnityMcpBridge.Editor.Windows { UpdatePythonServerInstallationStatus(); + // Refresh bridge status isUnityBridgeRunning = UnityMcpBridge.IsRunning; foreach (McpClient mcpClient in mcpClients.clients) { @@ -210,11 +210,42 @@ namespace UnityMcpBridge.Editor.Windows EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); + + // Connection mode and Auto-Connect button + EditorGUILayout.BeginHorizontal(); + + bool isAutoMode = UnityMcpBridge.IsAutoConnectMode(); + GUIStyle modeStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 }; + EditorGUILayout.LabelField($"Mode: {(isAutoMode ? "Auto" : "Standard")}", modeStyle); + + // Auto-Connect button + if (GUILayout.Button(isAutoMode ? "Connected ✓" : "Auto-Connect", GUILayout.Width(100), GUILayout.Height(24))) + { + if (!isAutoMode) + { + try + { + UnityMcpBridge.StartAutoConnect(); + // Update UI state + isUnityBridgeRunning = UnityMcpBridge.IsRunning; + Repaint(); + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Auto-Connect Failed", ex.Message, "OK"); + } + } + } + + EditorGUILayout.EndHorizontal(); + + // Current ports display + int currentUnityPort = UnityMcpBridge.GetCurrentPort(); GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 }; - EditorGUILayout.LabelField($"Ports: Unity {unityPort}, MCP {mcpPort}", portStyle); + EditorGUILayout.LabelField($"Ports: Unity {currentUnityPort}, MCP {mcpPort}", portStyle); EditorGUILayout.Space(5); EditorGUILayout.EndVertical(); } diff --git a/UnityMcpServer/src/config.py b/UnityMcpServer/src/config.py index 58f6f84..c42437a 100644 --- a/UnityMcpServer/src/config.py +++ b/UnityMcpServer/src/config.py @@ -15,7 +15,7 @@ class ServerConfig: mcp_port: int = 6500 # Connection settings - connection_timeout: float = 86400.0 # 24 hours timeout + connection_timeout: float = 600.0 # 10 minutes timeout buffer_size: int = 16 * 1024 * 1024 # 16MB buffer # Logging settings diff --git a/UnityMcpServer/src/port_discovery.py b/UnityMcpServer/src/port_discovery.py new file mode 100644 index 0000000..a0dfe96 --- /dev/null +++ b/UnityMcpServer/src/port_discovery.py @@ -0,0 +1,69 @@ +""" +Port discovery utility for Unity MCP Server. +Reads port configuration saved by Unity Bridge. +""" + +import json +import os +import logging +from pathlib import Path +from typing import Optional + +logger = logging.getLogger("unity-mcp-server") + +class PortDiscovery: + """Handles port discovery from Unity Bridge registry""" + + REGISTRY_FILE = "unity-mcp-port.json" + + @staticmethod + def get_registry_path() -> Path: + """Get the path to the port registry file""" + return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE + + @staticmethod + def discover_unity_port() -> int: + """ + Discover Unity port from registry file with fallback to default + + Returns: + Port number to connect to + """ + registry_file = PortDiscovery.get_registry_path() + + if registry_file.exists(): + try: + with open(registry_file, 'r') as f: + port_config = json.load(f) + + unity_port = port_config.get('unity_port') + if unity_port and isinstance(unity_port, int): + logger.info(f"Discovered Unity port from registry: {unity_port}") + return unity_port + + except Exception as e: + logger.warning(f"Could not read port registry: {e}") + + # Fallback to default port + logger.info("No port registry found, using default port 6400") + return 6400 + + @staticmethod + def get_port_config() -> Optional[dict]: + """ + Get the full port configuration from registry + + Returns: + Port configuration dict or None if not found + """ + registry_file = PortDiscovery.get_registry_path() + + if not registry_file.exists(): + return None + + try: + with open(registry_file, 'r') as f: + return json.load(f) + except Exception as e: + logger.warning(f"Could not read port configuration: {e}") + return None \ No newline at end of file diff --git a/UnityMcpServer/src/unity_connection.py b/UnityMcpServer/src/unity_connection.py index 252b504..da88d9b 100644 --- a/UnityMcpServer/src/unity_connection.py +++ b/UnityMcpServer/src/unity_connection.py @@ -4,6 +4,7 @@ import logging from dataclasses import dataclass from typing import Dict, Any from config import config +from port_discovery import PortDiscovery # Configure logging using settings from config logging.basicConfig( @@ -16,8 +17,13 @@ logger = logging.getLogger("unity-mcp-server") class UnityConnection: """Manages the socket connection to the Unity Editor.""" host: str = config.unity_host - port: int = config.unity_port + port: int = None # Will be set dynamically sock: socket.socket = None # Socket for Unity communication + + def __post_init__(self): + """Set port from discovery if not explicitly provided""" + if self.port is None: + self.port = PortDiscovery.discover_unity_port() def connect(self) -> bool: """Establish a connection to the Unity Editor."""