Merge pull request #169 from Scriptwonder/master

Code Validation Update
main
Shutong Wu 2025-07-24 11:32:53 +08:00 committed by GitHub
commit c0bcfcaab1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 998 additions and 215 deletions

View File

@ -73,6 +73,27 @@ Unity MCP connects your tools using two components:
* [Cursor](https://www.cursor.com/en/downloads) * [Cursor](https://www.cursor.com/en/downloads)
* [Visual Studio Code Copilot](https://code.visualstudio.com/docs/copilot/overview) * [Visual Studio Code Copilot](https://code.visualstudio.com/docs/copilot/overview)
* *(Others may work with manual config)* * *(Others may work with manual config)*
* <details> <summary><strong>[Optional] Roslyn for Advanced Script Validation</strong></summary>
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.</details>
### Step 1: Install the Unity Package (Bridge) ### Step 1: Install the Unity Package (Bridge)
@ -192,7 +213,7 @@ If Auto-Configure fails or you use a different client:
### 🔴 High Priority ### 🔴 High Priority
- [ ] **Asset Generation Improvements** - Enhanced server request handling and asset pipeline optimization - [ ] **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 - [ ] **Robust Error Handling** - Comprehensive error messages, recovery mechanisms, and graceful degradation
- [ ] **Remote Connection Support** - Enable seamless remote connection between Unity host and MCP server - [ ] **Remote Connection Support** - Enable seamless remote connection between Unity host and MCP server
- [ ] **Documentation Expansion** - Complete tutorials for custom tool creation and API reference - [ ] **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:
<summary><strong>✅ Completed Features<strong></summary> <summary><strong>✅ Completed Features<strong></summary>
- [x] **Shader Generation** - Generate shaders using CGProgram template - [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)).
</details> </details>
### 🔬 Research & Exploration ### 🔬 Research & Exploration
@ -295,4 +317,4 @@ Thanks to the contributors and the Unity team.
## Star History ## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=unity-mcp/unity-mcp,justinpbarnett/unity-mcp&type=Date)](https://www.star-history.com/#unity-mcp/unity-mcp&justinpbarnett/unity-mcp&Date) [![Star History Chart](https://api.star-history.com/svg?repos=justinpbarnett/unity-mcp&type=Date)](https://www.star-history.com/#justinpbarnett/unity-mcp&Date)

View File

@ -7,10 +7,43 @@ using UnityEditor;
using UnityEngine; using UnityEngine;
using UnityMcpBridge.Editor.Helpers; 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 namespace UnityMcpBridge.Editor.Tools
{ {
/// <summary> /// <summary>
/// Handles CRUD operations for C# scripts within the Unity project. /// 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.
/// </summary> /// </summary>
public static class ManageScript public static class ManageScript
{ {
@ -168,12 +201,18 @@ namespace UnityMcpBridge.Editor.Tools
contents = GenerateDefaultScriptContent(name, scriptType, namespaceName); contents = GenerateDefaultScriptContent(name, scriptType, namespaceName);
} }
// Validate syntax (basic check) // Validate syntax with detailed error reporting using GUI setting
if (!ValidateScriptSyntax(contents)) ValidationLevel validationLevel = GetValidationLevelFromGUI();
bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors);
if (!isValid)
{ {
// Optionally return a specific error or warning about syntax string errorMessage = "Script validation failed:\n" + string.Join("\n", validationErrors);
// return Response.Error("Provided script content has potential syntax errors."); return Response.Error(errorMessage);
Debug.LogWarning($"Potential syntax error in script being created: {name}"); }
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 try
@ -243,11 +282,18 @@ namespace UnityMcpBridge.Editor.Tools
return Response.Error("Content is required for the 'update' action."); return Response.Error("Content is required for the 'update' action.");
} }
// Validate syntax (basic check) // Validate syntax with detailed error reporting using GUI setting
if (!ValidateScriptSyntax(contents)) ValidationLevel validationLevel = GetValidationLevelFromGUI();
bool isValid = ValidateScriptSyntax(contents, validationLevel, out string[] validationErrors);
if (!isValid)
{ {
Debug.LogWarning($"Potential syntax error in script being updated: {name}"); string errorMessage = "Script validation failed:\n" + string.Join("\n", validationErrors);
// Consider if this should be a hard error or just a warning 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 try
@ -361,27 +407,624 @@ namespace UnityMcpBridge.Editor.Tools
} }
/// <summary> /// <summary>
/// Performs a very basic syntax validation (checks for balanced braces). /// Gets the validation level from the GUI settings
/// TODO: Implement more robust syntax checking if possible. /// </summary>
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
};
}
/// <summary>
/// Validates C# script syntax using multiple validation layers.
/// </summary> /// </summary>
private static bool ValidateScriptSyntax(string contents) private static bool ValidateScriptSyntax(string contents)
{ {
if (string.IsNullOrEmpty(contents)) return ValidateScriptSyntax(contents, ValidationLevel.Standard, out _);
return true; // Empty is technically valid? }
int braceBalance = 0; /// <summary>
foreach (char c in contents) /// Advanced syntax validation with detailed diagnostics and configurable strictness.
/// </summary>
private static bool ValidateScriptSyntax(string contents, ValidationLevel level, out string[] errors)
{
var errorList = new System.Collections.Generic.List<string>();
errors = null;
if (string.IsNullOrEmpty(contents))
{ {
if (c == '{') return true; // Empty content is valid
braceBalance++;
else if (c == '}')
braceBalance--;
} }
return braceBalance == 0; // Basic structural validation
// This is extremely basic. A real C# parser/compiler check would be ideal if (!ValidateBasicStructure(contents, errorList))
// but is complex to implement directly here. {
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:")));
} }
/// <summary>
/// Validation strictness levels
/// </summary>
private enum ValidationLevel
{
Basic, // Only syntax errors
Standard, // Syntax + Unity best practices
Comprehensive, // All checks + semantic analysis
Strict // Treat all issues as errors
}
/// <summary>
/// Validates basic code structure (braces, quotes, comments)
/// </summary>
private static bool ValidateBasicStructure(string contents, System.Collections.Generic.List<string> 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
/// <summary>
/// Cached compilation references for performance
/// </summary>
private static System.Collections.Generic.List<MetadataReference> _cachedReferences = null;
private static DateTime _cacheTime = DateTime.MinValue;
private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(5);
/// <summary>
/// Validates syntax using Roslyn compiler services
/// </summary>
private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List<string> 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;
}
}
/// <summary>
/// Validates script semantics using full compilation context to catch namespace, type, and method resolution errors
/// </summary>
private static bool ValidateScriptSemantics(string contents, System.Collections.Generic.List<string> 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;
}
}
/// <summary>
/// Gets compilation references with caching for performance
/// </summary>
private static System.Collections.Generic.List<MetadataReference> GetCompilationReferences()
{
// Check cache validity
if (_cachedReferences != null && DateTime.Now - _cacheTime < CacheExpiry)
{
return _cachedReferences;
}
try
{
var references = new System.Collections.Generic.List<MetadataReference>();
// 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<MetadataReference>();
}
}
#else
private static bool ValidateScriptSyntaxRoslyn(string contents, ValidationLevel level, System.Collections.Generic.List<string> errors)
{
// Fallback when Roslyn is not available
return true;
}
#endif
/// <summary>
/// Validates Unity-specific coding rules and best practices
/// //TODO: Naive Unity Checks and not really yield any results, need to be improved
/// </summary>
private static void ValidateScriptSyntaxUnity(string contents, System.Collections.Generic.List<string> 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");
}
}
/// <summary>
/// Validates semantic rules and common coding issues
/// </summary>
private static void ValidateSemanticRules(string contents, System.Collections.Generic.List<string> 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)
/// <summary>
/// Public method to validate script syntax with configurable validation level
/// Returns detailed validation results including errors and warnings
/// </summary>
// 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);
// }
// }
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Newtonsoft.Json; using Newtonsoft.Json;
using UnityEditor; using UnityEditor;
@ -20,6 +21,19 @@ namespace UnityMcpBridge.Editor.Windows
private const int mcpPort = 6500; // Hardcoded MCP port private const int mcpPort = 6500; // Hardcoded MCP port
private readonly McpClients mcpClients = new(); 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")] [MenuItem("Window/Unity MCP")]
public static void ShowWindow() public static void ShowWindow()
{ {
@ -35,6 +49,9 @@ namespace UnityMcpBridge.Editor.Windows
{ {
CheckMcpConfiguration(mcpClient); CheckMcpConfiguration(mcpClient);
} }
// Load validation level setting
LoadValidationLevelSetting();
} }
private Color GetStatusColor(McpStatus status) private Color GetStatusColor(McpStatus status)
@ -79,164 +96,18 @@ namespace UnityMcpBridge.Editor.Windows
} }
} }
private void ConfigurationSection(McpClient mcpClient)
private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12)
{ {
// Calculate if we should use half-width layout float offsetX = (statusRect.width - size) / 2;
// Minimum width for half-width layout is 400 pixels float offsetY = (statusRect.height - size) / 2;
bool useHalfWidth = position.width >= 800; Rect dotRect = new(statusRect.x + offsetX, statusRect.y + offsetY, size, size);
float sectionWidth = useHalfWidth ? (position.width / 2) - 15 : position.width - 20;
// Begin horizontal layout if using half-width
if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 0)
{
EditorGUILayout.BeginHorizontal();
}
// Begin section with fixed width
EditorGUILayout.BeginVertical(EditorStyles.helpBox, GUILayout.Width(sectionWidth));
// Header with improved styling
EditorGUILayout.Space(5);
Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
GUI.Label(
new Rect(
headerRect.x + 8,
headerRect.y + 4,
headerRect.width - 16,
headerRect.height
),
mcpClient.name + " Configuration",
EditorStyles.boldLabel
);
EditorGUILayout.Space(5);
// Status indicator with colored dot
Rect statusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20));
Color statusColor = GetStatusColor(mcpClient.status);
// Draw status dot
DrawStatusDot(statusRect, statusColor);
// Status text with some padding
EditorGUILayout.LabelField(
new GUIContent(" " + mcpClient.configStatus),
GUILayout.Height(20),
GUILayout.MinWidth(100)
);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
// Configure button with improved styling
GUIStyle buttonStyle = new(GUI.skin.button)
{
padding = new RectOffset(15, 15, 5, 5),
margin = new RectOffset(10, 10, 5, 5),
};
// Create muted button style for Manual Setup
GUIStyle mutedButtonStyle = new(buttonStyle);
if (mcpClient.mcpType == McpTypes.VSCode)
{
// Special handling for VSCode GitHub Copilot
if (
GUILayout.Button(
"Auto Configure VSCode with GitHub Copilot",
buttonStyle,
GUILayout.Height(28)
)
)
{
ConfigureMcpClient(mcpClient);
}
if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28)))
{
// Show VSCode specific manual setup window
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? mcpClient.windowsConfigPath
: mcpClient.linuxConfigPath;
// Get the Python directory path
string pythonDir = FindPackagePythonDirectory();
// Create VSCode-specific configuration
var vscodeConfig = new
{
mcp = new
{
servers = new
{
unityMCP = new
{
command = "uv",
args = new[] { "--directory", pythonDir, "run", "server.py" }
}
}
}
};
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
// Use the VSCodeManualSetupWindow directly since we're in the same namespace
VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson);
}
}
else
{
// Standard client buttons
if (
GUILayout.Button(
$"Auto Configure {mcpClient.name}",
buttonStyle,
GUILayout.Height(28)
)
)
{
ConfigureMcpClient(mcpClient);
}
if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28)))
{
// Get the appropriate config path based on OS
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? mcpClient.windowsConfigPath
: mcpClient.linuxConfigPath;
ShowManualInstructionsWindow(configPath, mcpClient);
}
}
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
// End horizontal layout if using half-width and at the end of a row
if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 1)
{
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
}
// Add space and end the horizontal layout if last item is odd
else if (
useHalfWidth
&& mcpClients.clients.IndexOf(mcpClient) == mcpClients.clients.Count - 1
)
{
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
}
}
private void DrawStatusDot(Rect statusRect, Color statusColor)
{
Rect dotRect = new(statusRect.x + 6, statusRect.y + 4, 12, 12);
Vector3 center = new( Vector3 center = new(
dotRect.x + (dotRect.width / 2), dotRect.x + (dotRect.width / 2),
dotRect.y + (dotRect.height / 2), dotRect.y + (dotRect.height / 2),
0 0
); );
float radius = dotRect.width / 2; float radius = size / 2;
// Draw the main dot // Draw the main dot
Handles.color = statusColor; Handles.color = statusColor;
@ -256,59 +127,255 @@ namespace UnityMcpBridge.Editor.Windows
{ {
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
EditorGUILayout.Space(10); // Header
// Title with improved styling DrawHeader();
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 // Main sections in a more compact layout
EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Python Server Status", EditorStyles.boldLabel);
// Status indicator with colored dot // Left column - Status and Bridge
Rect installStatusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20)); EditorGUILayout.BeginVertical(GUILayout.Width(position.width * 0.5f));
DrawStatusDot(installStatusRect, pythonServerInstallationStatusColor); DrawServerStatusSection();
EditorGUILayout.LabelField(" " + pythonServerInstallationStatus); EditorGUILayout.Space(5);
EditorGUILayout.EndHorizontal(); DrawBridgeSection();
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.EndVertical();
// Right column - Validation Settings
EditorGUILayout.BeginVertical();
DrawValidationSection();
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10); EditorGUILayout.Space(10);
// Unity Bridge Section // Unified MCP Client Configuration
EditorGUILayout.BeginVertical(EditorStyles.helpBox); DrawUnifiedClientConfiguration();
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")) 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 + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height),
"Unity MCP Editor",
titleStyle
);
EditorGUILayout.Space(15);
}
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();
EditorGUILayout.Space(5);
GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel)
{
fontSize = 11
};
EditorGUILayout.LabelField($"Ports: Unity {unityPort}, MCP {mcpPort}", portStyle);
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
}
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(); ToggleUnityBridge();
} }
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical();
}
foreach (McpClient mcpClient in mcpClients.clients) private void DrawValidationSection()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
{ {
EditorGUILayout.Space(10); fontSize = 14
ConfigurationSection(mcpClient); };
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.EndScrollView(); EditorGUILayout.Space(8);
string description = GetValidationLevelDescription(validationLevelIndex);
EditorGUILayout.HelpBox(description, MessageType.Info);
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
}
private void DrawUnifiedClientConfiguration()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 14
};
EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle);
EditorGUILayout.Space(10);
// Client selector
string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray();
EditorGUI.BeginChangeCheck();
selectedClientIndex = EditorGUILayout.Popup("Select Client", selectedClientIndex, clientNames, GUILayout.Height(20));
if (EditorGUI.EndChangeCheck())
{
selectedClientIndex = Mathf.Clamp(selectedClientIndex, 0, mcpClients.clients.Count - 1);
}
EditorGUILayout.Space(10);
if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count)
{
McpClient selectedClient = mcpClients.clients[selectedClientIndex];
DrawClientConfigurationCompact(selectedClient);
}
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
}
private void DrawClientConfigurationCompact(McpClient mcpClient)
{
// Status display
EditorGUILayout.BeginHorizontal();
Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
Color statusColor = GetStatusColor(mcpClient.status);
DrawStatusDot(statusRect, statusColor, 16);
GUIStyle clientStatusStyle = new GUIStyle(EditorStyles.label)
{
fontSize = 12,
fontStyle = FontStyle.Bold
};
EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28));
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10);
// Action buttons in horizontal layout
EditorGUILayout.BeginHorizontal();
if (mcpClient.mcpType == McpTypes.VSCode)
{
if (GUILayout.Button("Auto Configure", GUILayout.Height(32)))
{
ConfigureMcpClient(mcpClient);
}
}
else
{
if (GUILayout.Button($"Auto Configure", GUILayout.Height(32)))
{
ConfigureMcpClient(mcpClient);
}
}
if (GUILayout.Button("Manual Setup", GUILayout.Height(32)))
{
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? mcpClient.windowsConfigPath
: mcpClient.linuxConfigPath;
if (mcpClient.mcpType == McpTypes.VSCode)
{
string pythonDir = FindPackagePythonDirectory();
var vscodeConfig = new
{
mcp = new
{
servers = new
{
unityMCP = new
{
command = "uv",
args = new[] { "--directory", pythonDir, "run", "server.py" }
}
}
}
};
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson);
}
else
{
ShowManualInstructionsWindow(configPath, mcpClient);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
// Quick info
GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel)
{
fontSize = 10
};
EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle);
} }
private void ToggleUnityBridge() private void ToggleUnityBridge()
@ -355,6 +422,7 @@ namespace UnityMcpBridge.Editor.Windows
existingConfig ??= new Newtonsoft.Json.Linq.JObject(); existingConfig ??= new Newtonsoft.Json.Linq.JObject();
// Handle different client types with a switch statement // 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) switch (mcpClient?.mcpType)
{ {
case McpTypes.VSCode: case McpTypes.VSCode:
@ -370,6 +438,12 @@ namespace UnityMcpBridge.Editor.Windows
{ {
existingConfig.mcp.servers = new Newtonsoft.Json.Linq.JObject(); existingConfig.mcp.servers = new Newtonsoft.Json.Linq.JObject();
} }
// Add/update UnityMCP server in VSCode settings
existingConfig.mcp.servers.unityMCP =
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
JsonConvert.SerializeObject(unityMCPConfig)
);
break; break;
default: default:
@ -379,15 +453,15 @@ namespace UnityMcpBridge.Editor.Windows
{ {
existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject(); existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
} }
// Add/update UnityMCP server in standard MCP settings
existingConfig.mcpServers.unityMCP =
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
JsonConvert.SerializeObject(unityMCPConfig)
);
break; break;
} }
// Add/update UnityMCP server in VSCode settings
existingConfig.mcp.servers.unityMCP =
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
JsonConvert.SerializeObject(unityMCPConfig)
);
// Write the merged configuration back to file // Write the merged configuration back to file
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings); string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
File.WriteAllText(configPath, mergedJson); File.WriteAllText(configPath, mergedJson);
@ -613,6 +687,50 @@ namespace UnityMcpBridge.Editor.Windows
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, 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) private void CheckMcpConfiguration(McpClient mcpClient)
{ {
try try