commit
c0bcfcaab1
26
README.md
26
README.md
|
|
@ -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
|
||||||
|
|
||||||
[](https://www.star-history.com/#unity-mcp/unity-mcp&justinpbarnett/unity-mcp&Date)
|
[](https://www.star-history.com/#justinpbarnett/unity-mcp&Date)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue