Code Validation Update

1. Update the code validation feature. With Roslyn installed(see guide), MCP clients will receive detailed error messages and making the code generation process more error-proof
2. Minor update to the EditorWindow, making more space for upcoming features
3. Readme update to include Code validation guides
4. Minor bug fixes including installation bugs from previous VSC PR
main
Scriptwonder 2025-07-23 23:31:47 -04:00
parent 6906c24290
commit 9101105212
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)
* [Visual Studio Code Copilot](https://code.visualstudio.com/docs/copilot/overview)
* *(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)
@ -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:
<summary><strong>✅ Completed Features<strong></summary>
- [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>
### 🔬 Research & Exploration
@ -295,4 +317,4 @@ Thanks to the contributors and the Unity team.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=unity-mcp/unity-mcp,justinpbarnett/unity-mcp&type=Date)](https://www.star-history.com/#unity-mcp/unity-mcp&justinpbarnett/unity-mcp&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=justinpbarnett/unity-mcp&type=Date)](https://www.star-history.com/#justinpbarnett/unity-mcp&Date)

View File

@ -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
{
/// <summary>
/// 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>
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
}
/// <summary>
/// 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
/// </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>
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)
/// <summary>
/// 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 == '{')
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:")));
}
/// <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.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using UnityEditor;
@ -20,6 +21,19 @@ namespace UnityMcpBridge.Editor.Windows
private const int mcpPort = 6500; // Hardcoded MCP port
private readonly McpClients mcpClients = new();
// Script validation settings
private int validationLevelIndex = 1; // Default to Standard
private readonly string[] validationLevelOptions = new string[]
{
"Basic - Only syntax checks",
"Standard - Syntax + Unity practices",
"Comprehensive - All checks + semantic analysis",
"Strict - Full semantic validation (requires Roslyn)"
};
// UI state
private int selectedClientIndex = 0;
[MenuItem("Window/Unity MCP")]
public static void ShowWindow()
{
@ -35,6 +49,9 @@ namespace UnityMcpBridge.Editor.Windows
{
CheckMcpConfiguration(mcpClient);
}
// Load validation level setting
LoadValidationLevelSetting();
}
private Color GetStatusColor(McpStatus status)
@ -79,164 +96,18 @@ namespace UnityMcpBridge.Editor.Windows
}
}
private void ConfigurationSection(McpClient mcpClient)
private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12)
{
// Calculate if we should use half-width layout
// Minimum width for half-width layout is 400 pixels
bool useHalfWidth = position.width >= 800;
float sectionWidth = useHalfWidth ? (position.width / 2) - 15 : position.width - 20;
// Begin horizontal layout if using half-width
if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 0)
{
EditorGUILayout.BeginHorizontal();
}
// Begin section with fixed width
EditorGUILayout.BeginVertical(EditorStyles.helpBox, GUILayout.Width(sectionWidth));
// Header with improved styling
EditorGUILayout.Space(5);
Rect headerRect = EditorGUILayout.GetControlRect(false, 24);
GUI.Label(
new Rect(
headerRect.x + 8,
headerRect.y + 4,
headerRect.width - 16,
headerRect.height
),
mcpClient.name + " Configuration",
EditorStyles.boldLabel
);
EditorGUILayout.Space(5);
// Status indicator with colored dot
Rect statusRect = EditorGUILayout.BeginHorizontal(GUILayout.Height(20));
Color statusColor = GetStatusColor(mcpClient.status);
// Draw status dot
DrawStatusDot(statusRect, statusColor);
// Status text with some padding
EditorGUILayout.LabelField(
new GUIContent(" " + mcpClient.configStatus),
GUILayout.Height(20),
GUILayout.MinWidth(100)
);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
// Configure button with improved styling
GUIStyle buttonStyle = new(GUI.skin.button)
{
padding = new RectOffset(15, 15, 5, 5),
margin = new RectOffset(10, 10, 5, 5),
};
// Create muted button style for Manual Setup
GUIStyle mutedButtonStyle = new(buttonStyle);
if (mcpClient.mcpType == McpTypes.VSCode)
{
// Special handling for VSCode GitHub Copilot
if (
GUILayout.Button(
"Auto Configure VSCode with GitHub Copilot",
buttonStyle,
GUILayout.Height(28)
)
)
{
ConfigureMcpClient(mcpClient);
}
if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28)))
{
// Show VSCode specific manual setup window
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? mcpClient.windowsConfigPath
: mcpClient.linuxConfigPath;
// Get the Python directory path
string pythonDir = FindPackagePythonDirectory();
// Create VSCode-specific configuration
var vscodeConfig = new
{
mcp = new
{
servers = new
{
unityMCP = new
{
command = "uv",
args = new[] { "--directory", pythonDir, "run", "server.py" }
}
}
}
};
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
// Use the VSCodeManualSetupWindow directly since we're in the same namespace
VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson);
}
}
else
{
// Standard client buttons
if (
GUILayout.Button(
$"Auto Configure {mcpClient.name}",
buttonStyle,
GUILayout.Height(28)
)
)
{
ConfigureMcpClient(mcpClient);
}
if (GUILayout.Button("Manual Setup", mutedButtonStyle, GUILayout.Height(28)))
{
// Get the appropriate config path based on OS
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? mcpClient.windowsConfigPath
: mcpClient.linuxConfigPath;
ShowManualInstructionsWindow(configPath, mcpClient);
}
}
EditorGUILayout.Space(5);
EditorGUILayout.EndVertical();
// End horizontal layout if using half-width and at the end of a row
if (useHalfWidth && mcpClients.clients.IndexOf(mcpClient) % 2 == 1)
{
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
}
// Add space and end the horizontal layout if last item is odd
else if (
useHalfWidth
&& mcpClients.clients.IndexOf(mcpClient) == mcpClients.clients.Count - 1
)
{
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
}
}
private void DrawStatusDot(Rect statusRect, Color statusColor)
{
Rect dotRect = new(statusRect.x + 6, statusRect.y + 4, 12, 12);
float offsetX = (statusRect.width - size) / 2;
float offsetY = (statusRect.height - size) / 2;
Rect dotRect = new(statusRect.x + offsetX, statusRect.y + offsetY, size, size);
Vector3 center = new(
dotRect.x + (dotRect.width / 2),
dotRect.y + (dotRect.height / 2),
0
);
float radius = dotRect.width / 2;
float radius = size / 2;
// Draw the main dot
Handles.color = statusColor;
@ -256,59 +127,255 @@ namespace UnityMcpBridge.Editor.Windows
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
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);
// Header
DrawHeader();
// Python Server Installation Status Section
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Python Server Status", EditorStyles.boldLabel);
// Main sections in a more compact layout
EditorGUILayout.BeginHorizontal();
// 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
);
// 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);
// Unity Bridge Section
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Unity MCP Bridge", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"Status: {(isUnityBridgeRunning ? "Running" : "Stopped")}");
EditorGUILayout.LabelField($"Port: {unityPort}");
// Unified MCP Client Configuration
DrawUnifiedClientConfiguration();
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();
}
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.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()
@ -355,6 +422,7 @@ namespace UnityMcpBridge.Editor.Windows
existingConfig ??= new Newtonsoft.Json.Linq.JObject();
// Handle different client types with a switch statement
//Comments: Interestingly, VSCode has mcp.servers.unityMCP while others have mcpServers.unityMCP, which is why we need to prevent this
switch (mcpClient?.mcpType)
{
case McpTypes.VSCode:
@ -370,6 +438,12 @@ namespace UnityMcpBridge.Editor.Windows
{
existingConfig.mcp.servers = new Newtonsoft.Json.Linq.JObject();
}
// Add/update UnityMCP server in VSCode settings
existingConfig.mcp.servers.unityMCP =
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
JsonConvert.SerializeObject(unityMCPConfig)
);
break;
default:
@ -379,15 +453,15 @@ namespace UnityMcpBridge.Editor.Windows
{
existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
}
// Add/update UnityMCP server in standard MCP settings
existingConfig.mcpServers.unityMCP =
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
JsonConvert.SerializeObject(unityMCPConfig)
);
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
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
File.WriteAllText(configPath, mergedJson);
@ -613,6 +687,50 @@ namespace UnityMcpBridge.Editor.Windows
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
}
private void LoadValidationLevelSetting()
{
string savedLevel = EditorPrefs.GetString("UnityMCP_ScriptValidationLevel", "standard");
validationLevelIndex = savedLevel.ToLower() switch
{
"basic" => 0,
"standard" => 1,
"comprehensive" => 2,
"strict" => 3,
_ => 1 // Default to Standard
};
}
private void SaveValidationLevelSetting()
{
string levelString = validationLevelIndex switch
{
0 => "basic",
1 => "standard",
2 => "comprehensive",
3 => "strict",
_ => "standard"
};
EditorPrefs.SetString("UnityMCP_ScriptValidationLevel", levelString);
}
private string GetValidationLevelDescription(int index)
{
return index switch
{
0 => "Only basic syntax checks (braces, quotes, comments)",
1 => "Syntax checks + Unity best practices and warnings",
2 => "All checks + semantic analysis and performance warnings",
3 => "Full semantic validation with namespace/type resolution (requires Roslyn)",
_ => "Standard validation"
};
}
public static string GetCurrentValidationLevel()
{
string savedLevel = EditorPrefs.GetString("UnityMCP_ScriptValidationLevel", "standard");
return savedLevel;
}
private void CheckMcpConfiguration(McpClient mcpClient)
{
try