diff --git a/README.md b/README.md
index 60439a59..2d8f82e3 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,27 @@ Unity MCP connects your tools using two components:
* [Cursor](https://www.cursor.com/en/downloads)
* [Visual Studio Code Copilot](https://code.visualstudio.com/docs/copilot/overview)
* *(Others may work with manual config)*
+ * [Optional] Roslyn for Advanced Script Validation
+
+ For **Strict** validation level that catches undefined namespaces, types, and methods:
+
+ **Method 1: NuGet for Unity (Recommended)**
+ 1. Install [NuGetForUnity](https://github.com/GlitchEnzo/NuGetForUnity)
+ 2. Go to `Window > NuGet Package Manager`
+ 3. Search for `Microsoft.CodeAnalysis.CSharp` and install the package
+ 5. Go to `Player Settings > Scripting Define Symbols`
+ 6. Add `USE_ROSLYN`
+ 7. Restart Unity
+
+ **Method 2: Manual DLL Installation**
+ 1. Download Microsoft.CodeAnalysis.CSharp.dll and dependencies from [NuGet](https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp/)
+ 2. Place DLLs in `Assets/Plugins/` folder
+ 3. Ensure .NET compatibility settings are correct
+ 4. Add `USE_ROSLYN` to Scripting Define Symbols
+ 5. Restart Unity
+
+ **Note:** Without Roslyn, script validation falls back to basic structural checks. Roslyn enables full C# compiler diagnostics with precise error reporting.
+
### Step 1: Install the Unity Package (Bridge)
@@ -192,7 +213,7 @@ If Auto-Configure fails or you use a different client:
### 🔴 High Priority
- [ ] **Asset Generation Improvements** - Enhanced server request handling and asset pipeline optimization
-- [ ] **Code Generation Enhancements** - Improved generated code quality, validation, and error handling
+- [ ] **Code Generation Enhancements** - Improved generated code quality and error handling
- [ ] **Robust Error Handling** - Comprehensive error messages, recovery mechanisms, and graceful degradation
- [ ] **Remote Connection Support** - Enable seamless remote connection between Unity host and MCP server
- [ ] **Documentation Expansion** - Complete tutorials for custom tool creation and API reference
@@ -210,6 +231,7 @@ If Auto-Configure fails or you use a different client:
✅ Completed Features
- [x] **Shader Generation** - Generate shaders using CGProgram template
+ - [x] **Advanced Script Validation** - Multi-level validation with semantic analysis, namespace/type checking, and Unity best practices (Will need Roslyn Installed, see [Prerequisite](#prerequisites)).
### 🔬 Research & Exploration
@@ -295,4 +317,4 @@ Thanks to the contributors and the Unity team.
## Star History
-[](https://www.star-history.com/#unity-mcp/unity-mcp&justinpbarnett/unity-mcp&Date)
+[](https://www.star-history.com/#justinpbarnett/unity-mcp&Date)
diff --git a/UnityMcpBridge/Editor/Tools/ManageScript.cs b/UnityMcpBridge/Editor/Tools/ManageScript.cs
index 70c9f71b..d79e17a6 100644
--- a/UnityMcpBridge/Editor/Tools/ManageScript.cs
+++ b/UnityMcpBridge/Editor/Tools/ManageScript.cs
@@ -7,10 +7,43 @@
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 @@ string namespaceName
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)
+ {
+ string errorMessage = "Script validation failed:\n" + string.Join("\n", validationErrors);
+ return Response.Error(errorMessage);
+ }
+ else if (validationErrors != null && validationErrors.Length > 0)
{
- // 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}");
+ // Log warnings but don't block creation
+ Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors));
}
try
@@ -243,11 +282,18 @@ string contents
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)
+ {
+ string errorMessage = "Script validation failed:\n" + string.Join("\n", validationErrors);
+ return Response.Error(errorMessage);
+ }
+ else if (validationErrors != null && validationErrors.Length > 0)
{
- Debug.LogWarning($"Potential syntax error in script being updated: {name}");
- // Consider if this should be a hard error or just a warning
+ // Log warnings but don't block update
+ Debug.LogWarning($"Script validation warnings for {name}:\n" + string.Join("\n", validationErrors));
}
try
@@ -361,27 +407,624 @@ string namespaceName
}
///
- /// 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)
{
+ return ValidateScriptSyntax(contents, ValidationLevel.Standard, out _);
+ }
+
+ ///
+ /// 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))
- return true; // Empty is technically valid?
+ {
+ return true; // Empty content is valid
+ }
+
+ // 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;
- foreach (char c in contents)
+ 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)
{
- if (c == '{')
- braceBalance++;
- else if (c == '}')
- braceBalance--;
+ 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 braceBalance == 0;
- // This is extremely basic. A real C# parser/compiler check would be ideal
- // but is complex to implement directly here.
+ return isValid;
}
+
+#if USE_ROSLYN
+ ///
+ /// Cached compilation references for performance
+ ///
+ private static System.Collections.Generic.List _cachedReferences = null;
+ private static DateTime _cacheTime = DateTime.MinValue;
+ private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(5);
+
+ ///
+ /// Validates syntax using Roslyn compiler services
+ ///
+ private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List errors)
+ {
+ try
+ {
+ var syntaxTree = CSharpSyntaxTree.ParseText(contents);
+ var diagnostics = syntaxTree.GetDiagnostics();
+
+ bool hasErrors = false;
+ foreach (var diagnostic in diagnostics)
+ {
+ string severity = diagnostic.Severity.ToString().ToUpper();
+ string message = $"{severity}: {diagnostic.GetMessage()}";
+
+ if (diagnostic.Severity == DiagnosticSeverity.Error)
+ {
+ hasErrors = true;
+ }
+
+ // Include warnings in comprehensive mode
+ if (level >= ValidationLevel.Standard || diagnostic.Severity == DiagnosticSeverity.Error) //Also use Standard for now
+ {
+ var location = diagnostic.Location.GetLineSpan();
+ if (location.IsValid)
+ {
+ message += $" (Line {location.StartLinePosition.Line + 1})";
+ }
+ errors.Add(message);
+ }
+ }
+
+ return !hasErrors;
+ }
+ catch (Exception ex)
+ {
+ errors.Add($"ERROR: Roslyn validation failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Validates script semantics using full compilation context to catch namespace, type, and method resolution errors
+ ///
+ private static bool ValidateScriptSemantics(string contents, System.Collections.Generic.List errors)
+ {
+ try
+ {
+ // Get compilation references with caching
+ var references = GetCompilationReferences();
+ if (references == null || references.Count == 0)
+ {
+ errors.Add("WARNING: Could not load compilation references for semantic validation");
+ return true; // Don't fail if we can't get references
+ }
+
+ // Create syntax tree
+ var syntaxTree = CSharpSyntaxTree.ParseText(contents);
+
+ // Create compilation with full context
+ var compilation = CSharpCompilation.Create(
+ "TempValidation",
+ new[] { syntaxTree },
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
+ );
+
+ // Get semantic diagnostics - this catches all the issues you mentioned!
+ var diagnostics = compilation.GetDiagnostics();
+
+ bool hasErrors = false;
+ foreach (var diagnostic in diagnostics)
+ {
+ if (diagnostic.Severity == DiagnosticSeverity.Error)
+ {
+ hasErrors = true;
+ var location = diagnostic.Location.GetLineSpan();
+ string locationInfo = location.IsValid ?
+ $" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : "";
+
+ // Include diagnostic ID for better error identification
+ string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : "";
+ errors.Add($"ERROR: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}");
+ }
+ else if (diagnostic.Severity == DiagnosticSeverity.Warning)
+ {
+ var location = diagnostic.Location.GetLineSpan();
+ string locationInfo = location.IsValid ?
+ $" (Line {location.StartLinePosition.Line + 1}, Column {location.StartLinePosition.Character + 1})" : "";
+
+ string diagnosticId = !string.IsNullOrEmpty(diagnostic.Id) ? $" [{diagnostic.Id}]" : "";
+ errors.Add($"WARNING: {diagnostic.GetMessage()}{diagnosticId}{locationInfo}");
+ }
+ }
+
+ return !hasErrors;
+ }
+ catch (Exception ex)
+ {
+ errors.Add($"ERROR: Semantic validation failed: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Gets compilation references with caching for performance
+ ///
+ private static System.Collections.Generic.List GetCompilationReferences()
+ {
+ // Check cache validity
+ if (_cachedReferences != null && DateTime.Now - _cacheTime < CacheExpiry)
+ {
+ return _cachedReferences;
+ }
+
+ try
+ {
+ var references = new System.Collections.Generic.List();
+
+ // Core .NET assemblies
+ references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); // mscorlib/System.Private.CoreLib
+ references.Add(MetadataReference.CreateFromFile(typeof(System.Linq.Enumerable).Assembly.Location)); // System.Linq
+ references.Add(MetadataReference.CreateFromFile(typeof(System.Collections.Generic.List<>).Assembly.Location)); // System.Collections
+
+ // Unity assemblies
+ try
+ {
+ references.Add(MetadataReference.CreateFromFile(typeof(UnityEngine.Debug).Assembly.Location)); // UnityEngine
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"Could not load UnityEngine assembly: {ex.Message}");
+ }
+
+#if UNITY_EDITOR
+ try
+ {
+ references.Add(MetadataReference.CreateFromFile(typeof(UnityEditor.Editor).Assembly.Location)); // UnityEditor
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"Could not load UnityEditor assembly: {ex.Message}");
+ }
+
+ // Get Unity project assemblies
+ try
+ {
+ var assemblies = CompilationPipeline.GetAssemblies();
+ foreach (var assembly in assemblies)
+ {
+ if (File.Exists(assembly.outputPath))
+ {
+ references.Add(MetadataReference.CreateFromFile(assembly.outputPath));
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogWarning($"Could not load Unity project assemblies: {ex.Message}");
+ }
+#endif
+
+ // Cache the results
+ _cachedReferences = references;
+ _cacheTime = DateTime.Now;
+
+ return references;
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"Failed to get compilation references: {ex.Message}");
+ return new System.Collections.Generic.List();
+ }
+ }
+#else
+ private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List errors)
+ {
+ // Fallback when Roslyn is not available
+ return true;
+ }
+#endif
+
+ ///
+ /// Validates Unity-specific coding rules and best practices
+ /// //TODO: Naive Unity Checks and not really yield any results, need to be improved
+ ///
+ private static void ValidateScriptSyntaxUnity(string contents, System.Collections.Generic.List errors)
+ {
+ // Check for common Unity anti-patterns
+ if (contents.Contains("FindObjectOfType") && contents.Contains("Update()"))
+ {
+ errors.Add("WARNING: FindObjectOfType in Update() can cause performance issues");
+ }
+
+ if (contents.Contains("GameObject.Find") && contents.Contains("Update()"))
+ {
+ errors.Add("WARNING: GameObject.Find in Update() can cause performance issues");
+ }
+
+ // Check for proper MonoBehaviour usage
+ if (contents.Contains(": MonoBehaviour") && !contents.Contains("using UnityEngine"))
+ {
+ errors.Add("WARNING: MonoBehaviour requires 'using UnityEngine;'");
+ }
+
+ // Check for SerializeField usage
+ if (contents.Contains("[SerializeField]") && !contents.Contains("using UnityEngine"))
+ {
+ errors.Add("WARNING: SerializeField requires 'using UnityEngine;'");
+ }
+
+ // Check for proper coroutine usage
+ if (contents.Contains("StartCoroutine") && !contents.Contains("IEnumerator"))
+ {
+ errors.Add("WARNING: StartCoroutine typically requires IEnumerator methods");
+ }
+
+ // Check for Update without FixedUpdate for physics
+ if (contents.Contains("Rigidbody") && contents.Contains("Update()") && !contents.Contains("FixedUpdate()"))
+ {
+ errors.Add("WARNING: Consider using FixedUpdate() for Rigidbody operations");
+ }
+
+ // Check for missing null checks on Unity objects
+ if (contents.Contains("GetComponent<") && !contents.Contains("!= null"))
+ {
+ errors.Add("WARNING: Consider null checking GetComponent results");
+ }
+
+ // Check for proper event function signatures
+ if (contents.Contains("void Start(") && !contents.Contains("void Start()"))
+ {
+ errors.Add("WARNING: Start() should not have parameters");
+ }
+
+ if (contents.Contains("void Update(") && !contents.Contains("void Update()"))
+ {
+ errors.Add("WARNING: Update() should not have parameters");
+ }
+
+ // Check for inefficient string operations
+ if (contents.Contains("Update()") && contents.Contains("\"") && contents.Contains("+"))
+ {
+ errors.Add("WARNING: String concatenation in Update() can cause garbage collection issues");
+ }
+ }
+
+ ///
+ /// Validates semantic rules and common coding issues
+ ///
+ private static void ValidateSemanticRules(string contents, System.Collections.Generic.List errors)
+ {
+ // Check for potential memory leaks
+ if (contents.Contains("new ") && contents.Contains("Update()"))
+ {
+ errors.Add("WARNING: Creating objects in Update() may cause memory issues");
+ }
+
+ // Check for magic numbers
+ var magicNumberPattern = new Regex(@"\b\d+\.?\d*f?\b(?!\s*[;})\]])");
+ var matches = magicNumberPattern.Matches(contents);
+ if (matches.Count > 5)
+ {
+ errors.Add("WARNING: Consider using named constants instead of magic numbers");
+ }
+
+ // Check for long methods (simple line count check)
+ var methodPattern = new Regex(@"(public|private|protected|internal)?\s*(static)?\s*\w+\s+\w+\s*\([^)]*\)\s*{");
+ var methodMatches = methodPattern.Matches(contents);
+ foreach (Match match in methodMatches)
+ {
+ int startIndex = match.Index;
+ int braceCount = 0;
+ int lineCount = 0;
+ bool inMethod = false;
+
+ for (int i = startIndex; i < contents.Length; i++)
+ {
+ if (contents[i] == '{')
+ {
+ braceCount++;
+ inMethod = true;
+ }
+ else if (contents[i] == '}')
+ {
+ braceCount--;
+ if (braceCount == 0 && inMethod)
+ break;
+ }
+ else if (contents[i] == '\n' && inMethod)
+ {
+ lineCount++;
+ }
+ }
+
+ if (lineCount > 50)
+ {
+ errors.Add("WARNING: Method is very long, consider breaking it into smaller methods");
+ break; // Only report once
+ }
+ }
+
+ // Check for proper exception handling
+ if (contents.Contains("catch") && contents.Contains("catch()"))
+ {
+ errors.Add("WARNING: Empty catch blocks should be avoided");
+ }
+
+ // Check for proper async/await usage
+ if (contents.Contains("async ") && !contents.Contains("await"))
+ {
+ errors.Add("WARNING: Async method should contain await or return Task");
+ }
+
+ // Check for hardcoded tags and layers
+ if (contents.Contains("\"Player\"") || contents.Contains("\"Enemy\""))
+ {
+ errors.Add("WARNING: Consider using constants for tags instead of hardcoded strings");
+ }
+ }
+
+ //TODO: A easier way for users to update incorrect scripts (now duplicated with the updateScript method and need to also update server side, put aside for now)
+ ///
+ /// Public method to validate script syntax with configurable validation level
+ /// Returns detailed validation results including errors and warnings
+ ///
+ // public static object ValidateScript(JObject @params)
+ // {
+ // string contents = @params["contents"]?.ToString();
+ // string validationLevel = @params["validationLevel"]?.ToString() ?? "standard";
+
+ // if (string.IsNullOrEmpty(contents))
+ // {
+ // return Response.Error("Contents parameter is required for validation.");
+ // }
+
+ // // Parse validation level
+ // ValidationLevel level = ValidationLevel.Standard;
+ // switch (validationLevel.ToLower())
+ // {
+ // case "basic": level = ValidationLevel.Basic; break;
+ // case "standard": level = ValidationLevel.Standard; break;
+ // case "comprehensive": level = ValidationLevel.Comprehensive; break;
+ // case "strict": level = ValidationLevel.Strict; break;
+ // default:
+ // return Response.Error($"Invalid validation level: '{validationLevel}'. Valid levels are: basic, standard, comprehensive, strict.");
+ // }
+
+ // // Perform validation
+ // bool isValid = ValidateScriptSyntax(contents, level, out string[] validationErrors);
+
+ // var errors = validationErrors?.Where(e => e.StartsWith("ERROR:")).ToArray() ?? new string[0];
+ // var warnings = validationErrors?.Where(e => e.StartsWith("WARNING:")).ToArray() ?? new string[0];
+
+ // var result = new
+ // {
+ // isValid = isValid,
+ // validationLevel = validationLevel,
+ // errorCount = errors.Length,
+ // warningCount = warnings.Length,
+ // errors = errors,
+ // warnings = warnings,
+ // summary = isValid
+ // ? (warnings.Length > 0 ? $"Validation passed with {warnings.Length} warnings" : "Validation passed with no issues")
+ // : $"Validation failed with {errors.Length} errors and {warnings.Length} warnings"
+ // };
+
+ // if (isValid)
+ // {
+ // return Response.Success("Script validation completed successfully.", result);
+ // }
+ // else
+ // {
+ // return Response.Error("Script validation failed.", result);
+ // }
+ // }
}
}
diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs
index bf11f1fd..3b1e69d5 100644
--- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs
+++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Linq;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using UnityEditor;
@@ -19,6 +20,19 @@ public class UnityMcpEditorWindow : EditorWindow
private const int unityPort = 6400; // Hardcoded Unity port
private const int mcpPort = 6500; // Hardcoded MCP port
private readonly McpClients mcpClients = new();
+
+ // Script validation settings
+ private int validationLevelIndex = 1; // Default to Standard
+ private readonly string[] validationLevelOptions = new string[]
+ {
+ "Basic - Only syntax checks",
+ "Standard - Syntax + Unity practices",
+ "Comprehensive - All checks + semantic analysis",
+ "Strict - Full semantic validation (requires Roslyn)"
+ };
+
+ // UI state
+ private int selectedClientIndex = 0;
[MenuItem("Window/Unity MCP")]
public static void ShowWindow()
@@ -35,6 +49,9 @@ private void OnEnable()
{
CheckMcpConfiguration(mcpClient);
}
+
+ // Load validation level setting
+ LoadValidationLevelSetting();
}
private Color GetStatusColor(McpStatus status)
@@ -79,89 +96,253 @@ private void UpdatePythonServerInstallationStatus()
}
}
- 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;
+ 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 = size / 2;
- // Begin horizontal layout if using half-width
- if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 0)
- {
- EditorGUILayout.BeginHorizontal();
- }
+ // Draw the main dot
+ Handles.color = statusColor;
+ Handles.DrawSolidDisc(center, Vector3.forward, radius);
- // Begin section with fixed width
- EditorGUILayout.BeginVertical(EditorStyles.helpBox, GUILayout.Width(sectionWidth));
+ // Draw the border
+ Color borderColor = new(
+ statusColor.r * 0.7f,
+ statusColor.g * 0.7f,
+ statusColor.b * 0.7f
+ );
+ Handles.color = borderColor;
+ Handles.DrawWireDisc(center, Vector3.forward, radius);
+ }
- // Header with improved styling
+ private void OnGUI()
+ {
+ 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);
- Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
+ DrawBridgeSection();
+ EditorGUILayout.EndVertical();
+
+ // Right column - Validation Settings
+ EditorGUILayout.BeginVertical();
+ DrawValidationSection();
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.EndHorizontal();
+
+ EditorGUILayout.Space(10);
+
+ // 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(
- headerRect.x + 8,
- headerRect.y + 4,
- headerRect.width - 16,
- headerRect.height
- ),
- mcpClient.name + " Configuration",
- EditorStyles.boldLabel
+ new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height),
+ "Unity MCP Editor",
+ titleStyle
);
- EditorGUILayout.Space(5);
+ EditorGUILayout.Space(15);
+ }
- // Status indicator with colored dot
- Rect statusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20));
- Color statusColor = GetStatusColor(mcpClient.status);
+ private void DrawServerStatusSection()
+ {
+ EditorGUILayout.BeginVertical(EditorStyles.helpBox);
+
+ GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
+ {
+ fontSize = 14
+ };
+ EditorGUILayout.LabelField("Server Status", sectionTitleStyle);
+ EditorGUILayout.Space(8);
+
+ 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();
- // Draw status dot
- DrawStatusDot(statusRect, statusColor);
+ EditorGUILayout.Space(5);
+ GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel)
+ {
+ fontSize = 11
+ };
+ EditorGUILayout.LabelField($"Ports: Unity {unityPort}, MCP {mcpPort}", portStyle);
+ EditorGUILayout.Space(5);
+ EditorGUILayout.EndVertical();
+ }
- // Status text with some padding
- EditorGUILayout.LabelField(
- new GUIContent(" " + mcpClient.configStatus),
- GUILayout.Height(20),
- GUILayout.MinWidth(100)
- );
+ private void DrawBridgeSection()
+ {
+ EditorGUILayout.BeginVertical(EditorStyles.helpBox);
+
+ 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();
EditorGUILayout.Space(8);
+ if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge", GUILayout.Height(32)))
+ {
+ ToggleUnityBridge();
+ }
+ EditorGUILayout.Space(5);
+ EditorGUILayout.EndVertical();
+ }
- // Configure button with improved styling
- GUIStyle buttonStyle = new(GUI.skin.button)
+ private void DrawValidationSection()
+ {
+ EditorGUILayout.BeginVertical(EditorStyles.helpBox);
+
+ GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
{
- padding = new RectOffset(15, 15, 5, 5),
- margin = new RectOffset(10, 10, 5, 5),
+ 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();
+ }
- // Create muted button style for Manual Setup
- GUIStyle mutedButtonStyle = new(buttonStyle);
+ 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)
{
- // Special handling for VSCode GitHub Copilot
- if (
- GUILayout.Button(
- "Auto Configure VSCode with GitHub Copilot",
- buttonStyle,
- GUILayout.Height(28)
- )
- )
+ if (GUILayout.Button("Auto Configure", GUILayout.Height(32)))
{
ConfigureMcpClient(mcpClient);
}
-
- if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28)))
+ }
+ else
+ {
+ if (GUILayout.Button($"Auto Configure", GUILayout.Height(32)))
{
- // Show VSCode specific manual setup window
- string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- ? mcpClient.windowsConfigPath
- : mcpClient.linuxConfigPath;
-
- // Get the Python directory path
- string pythonDir = FindPackagePythonDirectory();
+ ConfigureMcpClient(mcpClient);
+ }
+ }
+
+ if (GUILayout.Button("Manual Setup", GUILayout.Height(32)))
+ {
+ string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? mcpClient.windowsConfigPath
+ : mcpClient.linuxConfigPath;
- // Create VSCode-specific configuration
+ if (mcpClient.mcpType == McpTypes.VSCode)
+ {
+ string pythonDir = FindPackagePythonDirectory();
var vscodeConfig = new
{
mcp = new
@@ -176,139 +357,25 @@ private void ConfigurationSection(McpClient mcpClient)
}
}
};
-
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
-
- // Use the VSCodeManualSetupWindow directly since we're in the same namespace
VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson);
}
- }
- else
- {
- // Standard client buttons
- if (
- GUILayout.Button(
- $"Auto Configure {mcpClient.name}",
- buttonStyle,
- GUILayout.Height(28)
- )
- )
- {
- ConfigureMcpClient(mcpClient);
- }
-
- if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28)))
+ else
{
- // 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);
- Vector3 center = new(
- dotRect.x + (dotRect.width / 2),
- dotRect.y + (dotRect.height / 2),
- 0
- );
- float radius = dotRect.width / 2;
-
- // Draw the main dot
- Handles.color = statusColor;
- Handles.DrawSolidDisc(center, Vector3.forward, radius);
-
- // Draw the border
- Color borderColor = new(
- statusColor.r * 0.7f,
- statusColor.g * 0.7f,
- statusColor.b * 0.7f
- );
- Handles.color = borderColor;
- Handles.DrawWireDisc(center, Vector3.forward, radius);
- }
-
- private void OnGUI()
- {
- scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
-
- 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)
- );
- GUI.Label(
- new Rect(titleRect.x + 10, titleRect.y + 6, titleRect.width - 20, titleRect.height),
- "MCP Editor",
- EditorStyles.boldLabel
- );
- EditorGUILayout.Space(10);
-
- // Python Server Installation Status Section
- EditorGUILayout.BeginVertical(EditorStyles.helpBox);
- EditorGUILayout.LabelField("Python Server Status", EditorStyles.boldLabel);
-
- // Status indicator with colored dot
- Rect installStatusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20));
- DrawStatusDot(installStatusRect, pythonServerInstallationStatusColor);
- EditorGUILayout.LabelField(" " + pythonServerInstallationStatus);
+
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.EndVertical();
-
- EditorGUILayout.Space(10);
-
- // Unity Bridge Section
- EditorGUILayout.BeginVertical(EditorStyles.helpBox);
- EditorGUILayout.LabelField("Unity MCP Bridge", EditorStyles.boldLabel);
- EditorGUILayout.LabelField($"Status: {(isUnityBridgeRunning ? "Running" : "Stopped")}");
- EditorGUILayout.LabelField($"Port: {unityPort}");
-
- if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge"))
- {
- ToggleUnityBridge();
- }
- EditorGUILayout.EndVertical();
-
- foreach (McpClient mcpClient in mcpClients.clients)
+
+ EditorGUILayout.Space(8);
+ // Quick info
+ GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel)
{
- EditorGUILayout.Space(10);
- ConfigurationSection(mcpClient);
- }
-
- EditorGUILayout.EndScrollView();
+ fontSize = 10
+ };
+ EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle);
}
private void ToggleUnityBridge()
@@ -355,6 +422,7 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC
existingConfig ??= new Newtonsoft.Json.Linq.JObject();
// Handle different client types with a switch statement
+ //Comments: Interestingly, VSCode has mcp.servers.unityMCP while others have mcpServers.unityMCP, which is why we need to prevent this
switch (mcpClient?.mcpType)
{
case McpTypes.VSCode:
@@ -370,6 +438,12 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC
{
existingConfig.mcp.servers = new Newtonsoft.Json.Linq.JObject();
}
+
+ // Add/update UnityMCP server in VSCode settings
+ existingConfig.mcp.servers.unityMCP =
+ JsonConvert.DeserializeObject(
+ JsonConvert.SerializeObject(unityMCPConfig)
+ );
break;
default:
@@ -379,15 +453,15 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC
{
existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
}
+
+ // Add/update UnityMCP server in standard MCP settings
+ existingConfig.mcpServers.unityMCP =
+ JsonConvert.DeserializeObject(
+ JsonConvert.SerializeObject(unityMCPConfig)
+ );
break;
}
- // Add/update UnityMCP server in VSCode settings
- existingConfig.mcp.servers.unityMCP =
- JsonConvert.DeserializeObject(
- JsonConvert.SerializeObject(unityMCPConfig)
- );
-
// Write the merged configuration back to file
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
File.WriteAllText(configPath, mergedJson);
@@ -613,6 +687,50 @@ McpClient mcpClient
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