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/README.md b/README.md
index e422540..19e81d4 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,22 @@
# Unity MCP ✨
-**Connect your Unity Editor to LLMs using the Model Context Protocol.**
+
+[](https://unity.com/releases/editor/archive)
+[](https://www.python.org)
+[](https://modelcontextprotocol.io/introduction)
+
+
+[](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.
---
-##
## Key Features 🚀
@@ -15,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:
@@ -25,6 +35,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").
@@ -48,8 +59,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/)
@@ -61,9 +70,31 @@ 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)*
-
+ * [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)
@@ -81,10 +112,12 @@ 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)**
+
+
+**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`.
+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**
@@ -163,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 ▶️
@@ -173,8 +220,41 @@ 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`.
+---
+
+## Future Dev Plans (Besides PR) 📝
+
+### 🔴 High Priority
+- [ ] **Asset Generation Improvements** - Enhanced server request handling and asset pipeline optimization
+- [ ] **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
+
+### 🟡 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
+
+### 🟢 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
+ - [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
+- [ ] **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
---
@@ -227,7 +307,15 @@ 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)!
+
+---
+
+## 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/)
+
---
@@ -235,15 +323,13 @@ Still stuck? [Open an Issue](https://www.google.com/url?sa=E&q=https%3A%2F%2Fgit
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 👋
-
-- **X/Twitter:** [@justinpbarnett](https://www.google.com/url?sa=E&q=https%3A%2F%2Fx.com%2Fjustinpbarnett)
-
-
---
## Acknowledgments 🙏
Thanks to the contributors and the Unity team.
+
+
+## Star History
+
+[](https://www.star-history.com/#justinpbarnett/unity-mcp&Date)
diff --git a/UnityMcpBridge/Editor/Data/McpClients.cs b/UnityMcpBridge/Editor/Data/McpClients.cs
index d370df8..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(
@@ -43,6 +57,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/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
}
}
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/Models/McpTypes.cs b/UnityMcpBridge/Editor/Models/McpTypes.cs
index 913ed47..cb691a2 100644
--- a/UnityMcpBridge/Editor/Models/McpTypes.cs
+++ b/UnityMcpBridge/Editor/Models/McpTypes.cs
@@ -4,6 +4,8 @@ namespace UnityMcpBridge.Editor.Models
{
ClaudeDesktop,
Cursor,
+ VSCode,
+ ClaudeCode,
}
}
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/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..432b234 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
{
///
@@ -177,6 +185,14 @@ namespace UnityMcpBridge.Editor.Tools
AssetDatabase.CreateAsset(mat, fullPath);
newAsset = mat;
}
+ else if (lowerAssetType == "physicsmaterial")
+ {
+ PhysicsMaterialType pmat = new PhysicsMaterialType();
+ if (properties != null)
+ ApplyPhysicsMaterialProperties(pmat, properties);
+ AssetDatabase.CreateAsset(pmat, fullPath);
+ newAsset = pmat;
+ }
else if (lowerAssetType == "scriptableobject")
{
string scriptClassName = properties?["scriptClass"]?.ToString();
@@ -891,6 +907,30 @@ namespace UnityMcpBridge.Editor.Tools
);
}
}
+ } else if (properties["color"] is JArray colorArr) //Use color now with examples set in manage_asset.py
+ {
+ string propName = "_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)
@@ -948,6 +988,77 @@ namespace UnityMcpBridge.Editor.Tools
return modified;
}
+ ///
+ /// Applies properties from JObject to a PhysicsMaterial.
+ ///
+ 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)
+ {
+ 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.
///
diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs
index 66c64cb..36897a9 100644
--- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs
+++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs
@@ -9,8 +9,8 @@ using UnityEditor.SceneManagement;
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
+using UnityMcpBridge.Editor.Helpers; // For Response class
+using UnityMcpBridge.Runtime.Serialization;
namespace UnityMcpBridge.Editor.Tools
{
@@ -23,10 +23,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 +215,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 +238,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 +261,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 +281,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 +314,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 +360,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 +457,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 +477,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 +490,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 +499,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 +508,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 +548,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 +566,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 +585,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 +622,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 +635,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 +643,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 +657,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 +668,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 +714,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 +781,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 +793,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 +864,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 +1487,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 +1496,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 +1536,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 +1587,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 +1610,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 +1628,7 @@ namespace UnityMcpBridge.Editor.Tools
}
}
}
-
+ // Get the property/field
PropertyInfo propInfo = currentType.GetProperty(part, flags);
FieldInfo fieldInfo = null;
if (propInfo == null)
@@ -1592,11 +1643,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 +1656,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 +1705,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 +1750,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 +1759,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
{
@@ -1742,6 +1794,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;
@@ -1760,6 +1813,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;
}
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/Tools/ManageShader.cs b/UnityMcpBridge/Editor/Tools/ManageShader.cs
new file mode 100644
index 0000000..03b28f1
--- /dev/null
+++ b/UnityMcpBridge/Editor/Tools/ManageShader.cs
@@ -0,0 +1,342 @@
+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
+ {
+ if (!Directory.Exists(fullPathDir))
+ {
+ Directory.CreateDirectory(fullPathDir);
+ // Refresh AssetDatabase to recognize new folders
+ AssetDatabase.Refresh();
+ }
+ }
+ 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."
+ );
+ }
+
+ // 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))
+ {
+ 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
+ //TODO: Consider a threshold for large 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);
+ AssetDatabase.Refresh();
+ 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}");
+ }
+ }
+
+ //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
+ {
+ 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
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 a0b112b..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
@@ -379,6 +414,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(
diff --git a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs
index 17c93e0..89f6099 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);
@@ -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
);
@@ -245,7 +252,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/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs
index c794fb7..62c919d 100644
--- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs
+++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs
@@ -1,5 +1,7 @@
using System;
+using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using UnityEditor;
@@ -16,9 +18,21 @@ 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
+ 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()
@@ -30,11 +44,25 @@ namespace UnityMcpBridge.Editor.Windows
{
UpdatePythonServerInstallationStatus();
+ // Refresh bridge status
isUnityBridgeRunning = UnityMcpBridge.IsRunning;
foreach (McpClient mcpClient in mcpClients.clients)
{
CheckMcpConfiguration(mcpClient);
}
+
+ // 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)
@@ -79,113 +107,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 (
- 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;
@@ -205,59 +138,313 @@ 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);
+
+ // 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 {currentUnityPort}, 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 (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)))
+ {
+ ConfigureMcpClient(mcpClient);
+ }
+ }
+
+ if (mcpClient.mcpType != McpTypes.ClaudeCode)
+ {
+ 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();
+ string uvPath = FindUvPath();
+ if (uvPath == null)
+ {
+ UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode.");
+ return;
+ }
+
+ var vscodeConfig = new
+ {
+ mcp = new
+ {
+ servers = new
+ {
+ unityMCP = new
+ {
+ command = uvPath,
+ 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()
@@ -274,12 +461,18 @@ namespace UnityMcpBridge.Editor.Windows
isUnityBridgeRunning = !isUnityBridgeRunning;
}
- private string WriteToConfig(string pythonDir, string configPath)
+ 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" },
};
@@ -295,7 +488,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}.");
}
}
@@ -303,17 +496,46 @@ 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
+ //Comments: Interestingly, VSCode has mcp.servers.unityMCP while others have mcpServers.unityMCP, which is why we need to prevent this
+ 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();
+ }
- // Add/update unityMCP while preserving other servers
- existingConfig.mcpServers.unityMCP =
- JsonConvert.DeserializeObject(
- JsonConvert.SerializeObject(unityMCPConfig)
- );
+ // Ensure mcp.servers object exists
+ if (existingConfig.mcp.servers == null)
+ {
+ 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:
+ // Standard MCP configuration (Claude Desktop, Cursor, etc.)
+ // Ensure mcpServers object exists
+ if (existingConfig.mcpServers == null)
+ {
+ existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
+ }
+
+ // Add/update UnityMCP server in standard MCP settings
+ existingConfig.mcpServers.unityMCP =
+ JsonConvert.DeserializeObject(
+ JsonConvert.SerializeObject(unityMCPConfig)
+ );
+ break;
+ }
// Write the merged configuration back to file
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
@@ -334,22 +556,56 @@ namespace UnityMcpBridge.Editor.Windows
{
// Get the Python directory path using Package Manager API
string pythonDir = FindPackagePythonDirectory();
-
- // Create the manual configuration message
- McpConfig jsonConfig = new()
- {
- mcpServers = new McpConfigServers
- {
- unityMCP = new McpConfigServer
- {
- command = "uv",
- args = new[] { "--directory", pythonDir, "run", "server.py" },
- },
- },
- };
-
+ string manualConfigJson;
+
+ // Create common JsonSerializerSettings
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
- string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
+
+ // Use switch statement to handle different client types
+ switch (mcpClient.mcpType)
+ {
+ case McpTypes.VSCode:
+ // Create VSCode-specific configuration with proper format
+ var vscodeConfig = new
+ {
+ mcp = new
+ {
+ servers = new
+ {
+ unityMCP = new
+ {
+ command = "uv",
+ args = new[] { "--directory", pythonDir, "run", "server.py" }
+ }
+ }
+ }
+ };
+ manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
+ break;
+
+ 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 = uvPath,
+ args = new[] { "--directory", pythonDir, "run", "server.py" },
+ },
+ },
+ };
+ manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
+ break;
+ }
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
}
@@ -372,12 +628,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;
}
@@ -386,13 +647,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")),
};
@@ -405,11 +675,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;
@@ -450,7 +720,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")
@@ -477,7 +747,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}";
@@ -495,13 +765,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" },
},
},
@@ -513,10 +790,61 @@ 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
{
+ // Special handling for Claude Code
+ if (mcpClient.mcpType == McpTypes.ClaudeCode)
+ {
+ CheckClaudeCodeConfiguration(mcpClient);
+ return;
+ }
+
string configPath;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@@ -542,18 +870,42 @@ namespace UnityMcpBridge.Editor.Windows
}
string configJson = File.ReadAllText(configPath);
- McpConfig config = JsonConvert.DeserializeObject(configJson);
-
- if (config?.mcpServers?.unityMCP != null)
+ string pythonDir = ServerInstaller.GetServerPath();
+
+ // Use switch statement to handle different client types, extracting common logic
+ string[] args = null;
+ bool configExists = false;
+
+ switch (mcpClient.mcpType)
{
- string pythonDir = ServerInstaller.GetServerPath();
- if (
- pythonDir != null
- && Array.Exists(
- config.mcpServers.unityMCP.args,
- arg => arg.Contains(pythonDir, StringComparison.Ordinal)
- )
- )
+ case McpTypes.VSCode:
+ dynamic config = JsonConvert.DeserializeObject(configJson);
+
+ if (config?.mcp?.servers?.unityMCP != null)
+ {
+ // Extract args from VSCode config format
+ 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)
+ {
+ 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);
}
@@ -572,5 +924,399 @@ 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 = 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();
+
+ 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();
+ UnityEngine.Debug.Log("UnityMCP server successfully registered from Claude Code.");
+
+
+ }
+ 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 = FindClaudeCommand();
+
+ if (string.IsNullOrEmpty(command))
+ {
+ UnityEngine.Debug.LogError("Claude CLI not found. Please ensure Claude Code is installed and accessible.");
+ return;
+ }
+ }
+ 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 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);
+
+ // 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 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
+ {
+ // 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);
+ }
+ }
}
}
diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs
new file mode 100644
index 0000000..a0b78e2
--- /dev/null
+++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs
@@ -0,0 +1,295 @@
+using System.Runtime.InteropServices;
+using UnityEditor;
+using UnityEngine;
+using UnityMcpBridge.Editor.Models;
+
+namespace UnityMcpBridge.Editor.Windows
+{
+ public class VSCodeManualSetupWindow : ManualConfigEditorWindow
+ {
+ public static new void ShowWindow(string configPath, string configJson)
+ {
+ 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();
+ }
+
+ protected override 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"
+ );
+ }
+
+ // 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 };
+
+ 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();
+ }
+
+ protected override void Update()
+ {
+ // Call the base implementation which handles the copy feedback timer
+ base.Update();
+ }
+ }
+}
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
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
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/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_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).
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 ---
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
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."""