feat(asset-store): implement post-installation prompt system for Asset Store compliance

Add comprehensive dependency detection system with cross-platform support:
- DependencyManager: Main orchestrator for dependency validation
- SetupWizard: Auto-trigger logic with InitializeOnLoad
- SetupWizardWindow: Complete EditorWindow implementation
- Platform detectors: Windows, macOS, Linux specific detection
- InstallationOrchestrator: Guided installation workflow

Asset Store compliance features:
- No bundled Python interpreter or UV package manager
- User-guided installation process with platform-specific instructions
- Clean package structure without large binary dependencies
- Fallback modes for incomplete installations
- Clear error messages with actionable guidance

Integration:
- Maintains backward compatibility with existing functionality
- Integrates with existing ServerInstaller and MCP infrastructure
- Adds menu items for manual setup wizard access and dependency checking
- Comprehensive error handling and user guidance
main
Justin Barnett 2025-09-23 06:51:48 -04:00
parent 6e72b33309
commit ab25a71bc5
152 changed files with 24523 additions and 0 deletions

View File

@ -0,0 +1,43 @@
# Asset Store Compliance Feature Development
## Project: Unity MCP Bridge
### Compliance Objectives
- Separate Python server dependencies
- Create clean package structure
- Implement dependency management wizard
- Ensure Asset Store submission readiness
### Key Development Areas
1. UnityMcpBridge/Editor/
- Refactor dependency management
- Create setup wizard
- Implement optional dependency prompting
2. Package Structure
- Modularize server dependencies
- Create clear installation paths
- Support optional component installation
3. Dependency Management System
- Detect existing Python environments
- Provide guided installation steps
- Support multiple Python version compatibility
4. Setup Wizard Requirements
- Detect Unity project Python configuration
- Offer manual and automatic setup modes
- Provide clear user guidance
- Validate Python environment
### Technical Constraints
- Maintain existing Unity MCP Bridge functionality
- Minimize additional package size
- Support cross-platform compatibility
- Provide clear user documentation
### Development Workflow
- Isolated worktree for focused development
- Incremental feature implementation
- Comprehensive testing
- Asset Store submission preparation

View File

@ -0,0 +1,221 @@
# Unity MCP Bridge - Asset Store Compliance Implementation
## Overview
This implementation provides a comprehensive post-installation prompt system for Unity MCP Bridge that ensures Asset Store compliance while maintaining full functionality. The system guides users through dependency installation and setup without bundling external dependencies in the package.
## Key Features
### 1. Dependency Detection System
- **Cross-platform detection** for Windows, macOS, and Linux
- **Intelligent path resolution** for Python and UV installations
- **Version validation** to ensure compatibility
- **Comprehensive diagnostics** for troubleshooting
### 2. Setup Wizard System
- **Automatic triggering** on first use or when dependencies are missing
- **Progressive disclosure** with step-by-step guidance
- **Persistent state management** to avoid repeated prompts
- **Manual invocation** via Window menu
### 3. Installation Orchestrator
- **Guided installation workflow** with progress tracking
- **Asset Store compliant** - no automatic downloads of external tools
- **Clear instructions** for manual installation
- **Fallback modes** for incomplete installations
### 4. Asset Store Compliance
- **No bundled Python dependencies** in package structure
- **External server distribution** strategy
- **Clean package structure** without embedded executables
- **User-guided installation** process
## Architecture
### Core Components
```
UnityMcpBridge/Editor/
├── Dependencies/
│ ├── DependencyManager.cs # Main orchestrator
│ ├── Models/
│ │ ├── DependencyStatus.cs # Status representation
│ │ ├── DependencyCheckResult.cs # Check results
│ │ └── SetupState.cs # Persistent state
│ └── PlatformDetectors/
│ ├── IPlatformDetector.cs # Platform interface
│ ├── WindowsPlatformDetector.cs
│ ├── MacOSPlatformDetector.cs
│ └── LinuxPlatformDetector.cs
├── Setup/
│ ├── SetupWizard.cs # Auto-trigger logic
│ └── SetupWizardWindow.cs # UI implementation
└── Installation/
└── InstallationOrchestrator.cs # Installation workflow
```
### Integration Points
The system integrates seamlessly with existing Unity MCP Bridge components:
- **ServerInstaller**: Enhanced with dependency validation
- **MCPForUnityBridge**: Maintains existing functionality
- **Menu System**: New setup options in Window menu
- **Logging**: Uses existing McpLog infrastructure
## User Experience Flow
### First-Time Setup
1. **Automatic Detection**: System checks for dependencies on first load
2. **Setup Wizard**: Shows if dependencies are missing
3. **Guided Installation**: Step-by-step instructions for each platform
4. **Validation**: Confirms successful installation
5. **Completion**: Marks setup as complete to avoid repeated prompts
### Ongoing Usage
- **Background Checks**: Periodic validation of dependency availability
- **Error Recovery**: Helpful messages when dependencies become unavailable
- **Manual Access**: Setup wizard available via Window menu
- **Diagnostics**: Comprehensive dependency information for troubleshooting
## Asset Store Compliance Features
### No Bundled Dependencies
- Python interpreter not included in package
- UV package manager not included in package
- MCP server distributed separately (embedded in package as source only)
### User-Guided Installation
- Platform-specific installation instructions
- Direct links to official installation sources
- Clear error messages with actionable guidance
- Fallback modes for partial installations
### Clean Package Structure
- No executable files in package
- No large binary dependencies
- Minimal package size impact
- Clear separation of concerns
## Platform Support
### Windows
- **Python Detection**: Microsoft Store, python.org, and PATH resolution
- **UV Detection**: WinGet, direct installation, and PATH resolution
- **Installation Guidance**: PowerShell commands and direct download links
### macOS
- **Python Detection**: Homebrew, Framework, system, and PATH resolution
- **UV Detection**: Homebrew, curl installation, and PATH resolution
- **Installation Guidance**: Homebrew commands and curl scripts
### Linux
- **Python Detection**: Package managers, snap, and PATH resolution
- **UV Detection**: curl installation and PATH resolution
- **Installation Guidance**: Distribution-specific package manager commands
## Error Handling
### Graceful Degradation
- System continues to function with missing optional dependencies
- Clear error messages for missing required dependencies
- Fallback modes for partial installations
- Recovery suggestions for common issues
### Comprehensive Diagnostics
- Detailed dependency status information
- Platform-specific troubleshooting guidance
- Version compatibility checking
- Path resolution diagnostics
## Testing Strategy
### Unit Testing
- Platform detector validation
- Dependency status modeling
- Setup state persistence
- Error condition handling
### Integration Testing
- End-to-end setup workflow
- Cross-platform compatibility
- Existing functionality preservation
- Performance impact assessment
### User Acceptance Testing
- First-time user experience
- Setup wizard usability
- Error recovery scenarios
- Documentation clarity
## Performance Considerations
### Minimal Impact
- Lazy loading of dependency checks
- Cached results where appropriate
- Background processing for non-critical operations
- Efficient platform detection
### Resource Usage
- Minimal memory footprint
- No persistent background processes
- Efficient file system operations
- Optimized UI rendering
## Future Enhancements
### Planned Features
- **Automatic Updates**: Notification system for dependency updates
- **Advanced Diagnostics**: More detailed system information
- **Custom Installation Paths**: Support for non-standard installations
- **Offline Mode**: Enhanced functionality without internet access
### Extensibility
- **Plugin Architecture**: Support for additional dependency types
- **Custom Detectors**: User-defined detection logic
- **Integration APIs**: Programmatic access to dependency system
- **Event System**: Hooks for custom setup workflows
## Migration Strategy
### Existing Users
- Automatic detection of existing installations
- Seamless upgrade path from previous versions
- Preservation of existing configuration
- Optional re-setup for enhanced features
### New Users
- Guided onboarding experience
- Clear setup requirements
- Comprehensive documentation
- Community support resources
## Documentation
### User Documentation
- Setup guide for each platform
- Troubleshooting common issues
- FAQ for dependency management
- Video tutorials for complex setups
### Developer Documentation
- API reference for dependency system
- Extension guide for custom detectors
- Integration examples
- Best practices guide
## Support and Maintenance
### Issue Resolution
- Comprehensive logging for debugging
- Diagnostic information collection
- Platform-specific troubleshooting
- Community support channels
### Updates and Patches
- Backward compatibility maintenance
- Security update procedures
- Performance optimization
- Feature enhancement process
This implementation ensures Unity MCP Bridge meets Asset Store requirements while providing an excellent user experience for dependency management and setup.

View File

@ -0,0 +1,206 @@
# Unity MCP Bridge - Asset Store Compliance Implementation Summary
## Implementation Completed ✅
### 1. Dependency Detection System
**Location**: `UnityMcpBridge/Editor/Dependencies/`
#### Core Components:
- **DependencyManager.cs**: Main orchestrator for dependency validation
- **Models/DependencyStatus.cs**: Represents individual dependency status
- **Models/DependencyCheckResult.cs**: Comprehensive check results
- **Models/SetupState.cs**: Persistent state management
#### Platform Detectors:
- **IPlatformDetector.cs**: Interface for platform-specific detection
- **WindowsPlatformDetector.cs**: Windows-specific dependency detection
- **MacOSPlatformDetector.cs**: macOS-specific dependency detection
- **LinuxPlatformDetector.cs**: Linux-specific dependency detection
#### Features:
✅ Cross-platform Python detection (3.10+ validation)
✅ UV package manager detection
✅ MCP server installation validation
✅ Platform-specific installation recommendations
✅ Comprehensive error handling and diagnostics
### 2. Setup Wizard System
**Location**: `UnityMcpBridge/Editor/Setup/`
#### Components:
- **SetupWizard.cs**: Auto-trigger logic with `[InitializeOnLoad]`
- **SetupWizardWindow.cs**: Complete EditorWindow implementation
#### Features:
✅ Automatic triggering on missing dependencies
✅ 5-step progressive wizard (Welcome → Check → Options → Progress → Complete)
✅ Persistent state to avoid repeated prompts
✅ Manual access via Window menu
✅ Version-aware setup completion tracking
### 3. Installation Orchestrator
**Location**: `UnityMcpBridge/Editor/Installation/`
#### Components:
- **InstallationOrchestrator.cs**: Guided installation workflow
#### Features:
✅ Asset Store compliant (no automatic downloads)
✅ Progress tracking and user feedback
✅ Platform-specific installation guidance
✅ Error handling and recovery suggestions
### 4. Asset Store Compliance
#### Package Structure Changes:
✅ Updated package.json to remove Python references
✅ Added dependency requirements to description
✅ Clean separation of embedded vs external dependencies
✅ No bundled executables or large binaries
#### User Experience:
✅ Clear setup requirements communication
✅ Guided installation process
✅ Fallback modes for incomplete installations
✅ Comprehensive error messages with actionable guidance
### 5. Integration with Existing System
#### Maintained Compatibility:
✅ Integrates with existing ServerInstaller
✅ Uses existing McpLog infrastructure
✅ Preserves all existing MCP functionality
✅ No breaking changes to public APIs
#### Enhanced Features:
✅ Menu items for dependency checking
✅ Diagnostic information collection
✅ Setup state persistence
✅ Platform-aware installation guidance
## File Structure Created
```
UnityMcpBridge/Editor/
├── Dependencies/
│ ├── DependencyManager.cs
│ ├── DependencyManagerTests.cs
│ ├── Models/
│ │ ├── DependencyStatus.cs
│ │ ├── DependencyCheckResult.cs
│ │ └── SetupState.cs
│ └── PlatformDetectors/
│ ├── IPlatformDetector.cs
│ ├── WindowsPlatformDetector.cs
│ ├── MacOSPlatformDetector.cs
│ └── LinuxPlatformDetector.cs
├── Setup/
│ ├── SetupWizard.cs
│ └── SetupWizardWindow.cs
└── Installation/
└── InstallationOrchestrator.cs
```
## Key Features Implemented
### 1. Automatic Dependency Detection
- **Multi-platform support**: Windows, macOS, Linux
- **Intelligent path resolution**: Common installation locations + PATH
- **Version validation**: Ensures Python 3.10+ compatibility
- **Comprehensive diagnostics**: Detailed status information
### 2. User-Friendly Setup Wizard
- **Progressive disclosure**: 5-step guided process
- **Visual feedback**: Progress bars and status indicators
- **Persistent state**: Avoids repeated prompts
- **Manual access**: Available via Window menu
### 3. Asset Store Compliance
- **No bundled dependencies**: Python/UV not included in package
- **External distribution**: MCP server as source code only
- **User-guided installation**: Clear instructions for each platform
- **Clean package structure**: Minimal size impact
### 4. Error Handling & Recovery
- **Graceful degradation**: System works with partial dependencies
- **Clear error messages**: Actionable guidance for users
- **Diagnostic tools**: Comprehensive system information
- **Recovery suggestions**: Platform-specific troubleshooting
## Testing & Validation
### Test Infrastructure:
✅ DependencyManagerTests.cs with menu-driven test execution
✅ Basic functionality validation
✅ Setup wizard testing
✅ State management testing
### Manual Testing Points:
- [ ] First-time user experience
- [ ] Cross-platform compatibility
- [ ] Error condition handling
- [ ] Setup wizard flow
- [ ] Dependency detection accuracy
## Integration Points
### With Existing Codebase:
**ServerInstaller**: Enhanced with dependency validation
**MCPForUnityBridge**: Maintains existing functionality
**Menu System**: New setup options added
**Logging**: Uses existing McpLog infrastructure
### New Menu Items Added:
- Window/MCP for Unity/Setup Wizard
- Window/MCP for Unity/Reset Setup
- Window/MCP for Unity/Check Dependencies
- Window/MCP for Unity/Run Dependency Tests (debug)
## Asset Store Readiness
### Compliance Checklist:
✅ No bundled Python interpreter
✅ No bundled UV package manager
✅ No large binary dependencies
✅ Clear dependency requirements in description
✅ User-guided installation process
✅ Fallback modes for missing dependencies
✅ Clean package structure
✅ Comprehensive documentation
### User Experience:
✅ Clear setup requirements
✅ Guided installation process
✅ Platform-specific instructions
✅ Error recovery guidance
✅ Minimal friction for users with dependencies
## Next Steps
### Before Asset Store Submission:
1. **Comprehensive Testing**: Test on all target platforms
2. **Documentation Update**: Update README with new setup process
3. **Performance Validation**: Ensure minimal impact on Unity startup
4. **User Acceptance Testing**: Validate setup wizard usability
### Post-Implementation:
1. **Monitor User Feedback**: Track setup success rates
2. **Iterate on UX**: Improve based on user experience
3. **Add Advanced Features**: Enhanced diagnostics, auto-updates
4. **Expand Platform Support**: Additional installation methods
## Technical Highlights
### Architecture Strengths:
- **SOLID Principles**: Clear separation of concerns
- **Platform Abstraction**: Extensible detector pattern
- **State Management**: Persistent setup state
- **Error Handling**: Comprehensive exception management
- **Performance**: Lazy loading and efficient detection
### Code Quality:
- **Documentation**: Comprehensive XML comments
- **Naming**: Clear, descriptive naming conventions
- **Error Handling**: Defensive programming practices
- **Maintainability**: Modular, testable design
- **Extensibility**: Easy to add new platforms/dependencies
This implementation successfully addresses Asset Store compliance requirements while maintaining excellent user experience and full MCP functionality.

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 31e7fac5858840340a75cc6df0ad3d9e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be61633e00d934610ac1ff8192ffbe3d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e59036660cc33d24596fbbf6d4657a83
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Data
{
public class DefaultServerConfig : ServerConfig
{
public new string unityHost = "localhost";
public new int unityPort = 6400;
public new int mcpPort = 6500;
public new float connectionTimeout = 15.0f;
public new int bufferSize = 32768;
public new string logLevel = "INFO";
public new string logFormat = "%(asctime)s - %(name)s - %(levelname)s - %(message)s";
public new int maxRetries = 3;
public new float retryDelay = 1.0f;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: de8f5721c34f7194392e9d8c7d0226c0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Data
{
public class McpClients
{
public List<McpClient> clients = new()
{
// 1) Cursor
new()
{
name = "Cursor",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".cursor",
"mcp.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".cursor",
"mcp.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".cursor",
"mcp.json"
),
mcpType = McpTypes.Cursor,
configStatus = "Not Configured",
},
// 2) Claude Code
new()
{
name = "Claude Code",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".claude.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".claude.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".claude.json"
),
mcpType = McpTypes.ClaudeCode,
configStatus = "Not Configured",
},
// 3) Windsurf
new()
{
name = "Windsurf",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".codeium",
"windsurf",
"mcp_config.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".codeium",
"windsurf",
"mcp_config.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".codeium",
"windsurf",
"mcp_config.json"
),
mcpType = McpTypes.Windsurf,
configStatus = "Not Configured",
},
// 4) Claude Desktop
new()
{
name = "Claude Desktop",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Claude",
"claude_desktop_config.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library",
"Application Support",
"Claude",
"claude_desktop_config.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config",
"Claude",
"claude_desktop_config.json"
),
mcpType = McpTypes.ClaudeDesktop,
configStatus = "Not Configured",
},
// 5) VSCode GitHub Copilot
new()
{
name = "VSCode GitHub Copilot",
// Windows path is canonical under %AppData%\Code\User
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Code",
"User",
"mcp.json"
),
// macOS: ~/Library/Application Support/Code/User/mcp.json
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Library",
"Application Support",
"Code",
"User",
"mcp.json"
),
// Linux: ~/.config/Code/User/mcp.json
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config",
"Code",
"User",
"mcp.json"
),
mcpType = McpTypes.VSCode,
configStatus = "Not Configured",
},
// 3) Kiro
new()
{
name = "Kiro",
windowsConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".kiro",
"settings",
"mcp.json"
),
macConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".kiro",
"settings",
"mcp.json"
),
linuxConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".kiro",
"settings",
"mcp.json"
),
mcpType = McpTypes.Kiro,
configStatus = "Not Configured",
},
};
// Initialize status enums after construction
public McpClients()
{
foreach (var client in clients)
{
if (client.configStatus == "Not Configured")
{
client.status = McpStatus.NotConfigured;
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 711b86bbc1f661e4fb2c822e14970e16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a1b2c3d4e5f6789012345678901234ab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,308 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Dependencies.PlatformDetectors;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Dependencies
{
/// <summary>
/// Main orchestrator for dependency validation and management
/// </summary>
public static class DependencyManager
{
private static readonly List<IPlatformDetector> _detectors = new List<IPlatformDetector>
{
new WindowsPlatformDetector(),
new MacOSPlatformDetector(),
new LinuxPlatformDetector()
};
private static IPlatformDetector _currentDetector;
/// <summary>
/// Get the platform detector for the current operating system
/// </summary>
public static IPlatformDetector GetCurrentPlatformDetector()
{
if (_currentDetector == null)
{
_currentDetector = _detectors.FirstOrDefault(d => d.CanDetect);
if (_currentDetector == null)
{
throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}");
}
}
return _currentDetector;
}
/// <summary>
/// Perform a comprehensive dependency check
/// </summary>
public static DependencyCheckResult CheckAllDependencies()
{
var result = new DependencyCheckResult();
try
{
var detector = GetCurrentPlatformDetector();
McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false);
// Check Python
var pythonStatus = detector.DetectPython();
result.Dependencies.Add(pythonStatus);
// Check UV
var uvStatus = detector.DetectUV();
result.Dependencies.Add(uvStatus);
// Check MCP Server
var serverStatus = detector.DetectMCPServer();
result.Dependencies.Add(serverStatus);
// Generate summary and recommendations
result.GenerateSummary();
GenerateRecommendations(result, detector);
McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false);
}
catch (Exception ex)
{
McpLog.Error($"Error during dependency check: {ex.Message}");
result.Summary = $"Dependency check failed: {ex.Message}";
result.IsSystemReady = false;
}
return result;
}
/// <summary>
/// Quick check if system is ready for MCP operations
/// </summary>
public static bool IsSystemReady()
{
try
{
var result = CheckAllDependencies();
return result.IsSystemReady;
}
catch
{
return false;
}
}
/// <summary>
/// Get a summary of missing dependencies
/// </summary>
public static string GetMissingDependenciesSummary()
{
try
{
var result = CheckAllDependencies();
var missing = result.GetMissingRequired();
if (missing.Count == 0)
{
return "All required dependencies are available.";
}
var names = missing.Select(d => d.Name).ToArray();
return $"Missing required dependencies: {string.Join(", ", names)}";
}
catch (Exception ex)
{
return $"Error checking dependencies: {ex.Message}";
}
}
/// <summary>
/// Check if a specific dependency is available
/// </summary>
public static bool IsDependencyAvailable(string dependencyName)
{
try
{
var detector = GetCurrentPlatformDetector();
return dependencyName.ToLowerInvariant() switch
{
"python" => detector.DetectPython().IsAvailable,
"uv" => detector.DetectUV().IsAvailable,
"mcpserver" or "mcp-server" => detector.DetectMCPServer().IsAvailable,
_ => false
};
}
catch
{
return false;
}
}
/// <summary>
/// Get installation recommendations for the current platform
/// </summary>
public static string GetInstallationRecommendations()
{
try
{
var detector = GetCurrentPlatformDetector();
return detector.GetInstallationRecommendations();
}
catch (Exception ex)
{
return $"Error getting installation recommendations: {ex.Message}";
}
}
/// <summary>
/// Get platform-specific installation URLs
/// </summary>
public static (string pythonUrl, string uvUrl) GetInstallationUrls()
{
try
{
var detector = GetCurrentPlatformDetector();
return (detector.GetPythonInstallUrl(), detector.GetUVInstallUrl());
}
catch
{
return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/");
}
}
/// <summary>
/// Validate that the MCP server can be started
/// </summary>
public static bool ValidateMCPServerStartup()
{
try
{
// Check if Python and UV are available
if (!IsDependencyAvailable("python") || !IsDependencyAvailable("uv"))
{
return false;
}
// Try to ensure server is installed
ServerInstaller.EnsureServerInstalled();
// Check if server files exist
var serverStatus = GetCurrentPlatformDetector().DetectMCPServer();
return serverStatus.IsAvailable;
}
catch (Exception ex)
{
McpLog.Error($"Error validating MCP server startup: {ex.Message}");
return false;
}
}
/// <summary>
/// Attempt to repair the Python environment
/// </summary>
public static bool RepairPythonEnvironment()
{
try
{
McpLog.Info("Attempting to repair Python environment...");
return ServerInstaller.RepairPythonEnvironment();
}
catch (Exception ex)
{
McpLog.Error($"Error repairing Python environment: {ex.Message}");
return false;
}
}
/// <summary>
/// Get detailed dependency information for diagnostics
/// </summary>
public static string GetDependencyDiagnostics()
{
try
{
var result = CheckAllDependencies();
var detector = GetCurrentPlatformDetector();
var diagnostics = new System.Text.StringBuilder();
diagnostics.AppendLine($"Platform: {detector.PlatformName}");
diagnostics.AppendLine($"Check Time: {result.CheckedAt:yyyy-MM-dd HH:mm:ss} UTC");
diagnostics.AppendLine($"System Ready: {result.IsSystemReady}");
diagnostics.AppendLine();
foreach (var dep in result.Dependencies)
{
diagnostics.AppendLine($"=== {dep.Name} ===");
diagnostics.AppendLine($"Available: {dep.IsAvailable}");
diagnostics.AppendLine($"Required: {dep.IsRequired}");
if (!string.IsNullOrEmpty(dep.Version))
diagnostics.AppendLine($"Version: {dep.Version}");
if (!string.IsNullOrEmpty(dep.Path))
diagnostics.AppendLine($"Path: {dep.Path}");
if (!string.IsNullOrEmpty(dep.Details))
diagnostics.AppendLine($"Details: {dep.Details}");
if (!string.IsNullOrEmpty(dep.ErrorMessage))
diagnostics.AppendLine($"Error: {dep.ErrorMessage}");
diagnostics.AppendLine();
}
if (result.RecommendedActions.Count > 0)
{
diagnostics.AppendLine("=== Recommended Actions ===");
foreach (var action in result.RecommendedActions)
{
diagnostics.AppendLine($"- {action}");
}
}
return diagnostics.ToString();
}
catch (Exception ex)
{
return $"Error generating diagnostics: {ex.Message}";
}
}
private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector)
{
var missing = result.GetMissingDependencies();
if (missing.Count == 0)
{
result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity.");
return;
}
foreach (var dep in missing)
{
if (dep.Name == "Python")
{
result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}");
}
else if (dep.Name == "UV Package Manager")
{
result.RecommendedActions.Add($"Install UV package manager from: {detector.GetUVInstallUrl()}");
}
else if (dep.Name == "MCP Server")
{
result.RecommendedActions.Add("MCP Server will be installed automatically when needed.");
}
}
if (result.GetMissingRequired().Count > 0)
{
result.RecommendedActions.Add("Use the Setup Wizard (Window > MCP for Unity > Setup Wizard) for guided installation.");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6789012345678901234abcdef012345
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,102 @@
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Dependencies.Models;
using UnityEngine;
namespace MCPForUnity.Editor.Dependencies
{
/// <summary>
/// Simple test class for dependency management functionality
/// This can be expanded into proper unit tests later
/// </summary>
public static class DependencyManagerTests
{
/// <summary>
/// Test basic dependency detection functionality
/// </summary>
[UnityEditor.MenuItem("Window/MCP for Unity/Run Dependency Tests", priority = 100)]
public static void RunBasicTests()
{
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Running Dependency Manager Tests...");
try
{
// Test 1: Platform detector availability
var detector = DependencyManager.GetCurrentPlatformDetector();
Debug.Log($"✓ Platform detector found: {detector.PlatformName}");
// Test 2: Dependency check
var result = DependencyManager.CheckAllDependencies();
Debug.Log($"✓ Dependency check completed. System ready: {result.IsSystemReady}");
// Test 3: Individual dependency checks
bool pythonAvailable = DependencyManager.IsDependencyAvailable("python");
bool uvAvailable = DependencyManager.IsDependencyAvailable("uv");
bool serverAvailable = DependencyManager.IsDependencyAvailable("mcpserver");
Debug.Log($"✓ Python available: {pythonAvailable}");
Debug.Log($"✓ UV available: {uvAvailable}");
Debug.Log($"✓ MCP Server available: {serverAvailable}");
// Test 4: Installation recommendations
var recommendations = DependencyManager.GetInstallationRecommendations();
Debug.Log($"✓ Installation recommendations generated ({recommendations.Length} characters)");
// Test 5: Setup state management
var setupState = Setup.SetupWizard.GetSetupState();
Debug.Log($"✓ Setup state loaded. Completed: {setupState.HasCompletedSetup}");
// Test 6: Diagnostics
var diagnostics = DependencyManager.GetDependencyDiagnostics();
Debug.Log($"✓ Diagnostics generated ({diagnostics.Length} characters)");
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: All tests completed successfully!");
// Show detailed results
Debug.Log($"<b>Detailed Dependency Status:</b>\n{diagnostics}");
}
catch (System.Exception ex)
{
Debug.LogError($"<b><color=#FF6B6B>MCP-FOR-UNITY</color></b>: Test failed: {ex.Message}\n{ex.StackTrace}");
}
}
/// <summary>
/// Test setup wizard functionality
/// </summary>
[UnityEditor.MenuItem("Window/MCP for Unity/Test Setup Wizard", priority = 101)]
public static void TestSetupWizard()
{
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Testing Setup Wizard...");
try
{
// Force show setup wizard for testing
Setup.SetupWizard.ShowSetupWizard();
Debug.Log("✓ Setup wizard opened successfully");
}
catch (System.Exception ex)
{
Debug.LogError($"<b><color=#FF6B6B>MCP-FOR-UNITY</color></b>: Setup wizard test failed: {ex.Message}");
}
}
/// <summary>
/// Reset setup state for testing
/// </summary>
[UnityEditor.MenuItem("Window/MCP for Unity/Reset Setup State (Test)", priority = 102)]
public static void ResetSetupStateForTesting()
{
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Resetting setup state for testing...");
try
{
Setup.SetupWizard.ResetSetupState();
Debug.Log("✓ Setup state reset successfully");
}
catch (System.Exception ex)
{
Debug.LogError($"<b><color=#FF6B6B>MCP-FOR-UNITY</color></b>: Setup state reset failed: {ex.Message}");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 678901234abcdef0123456789abcdef0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b2c3d4e5f6789012345678901234abcd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MCPForUnity.Editor.Dependencies.Models
{
/// <summary>
/// Result of a comprehensive dependency check
/// </summary>
[Serializable]
public class DependencyCheckResult
{
/// <summary>
/// List of all dependency statuses checked
/// </summary>
public List<DependencyStatus> Dependencies { get; set; }
/// <summary>
/// Overall system readiness for MCP operations
/// </summary>
public bool IsSystemReady { get; set; }
/// <summary>
/// Whether all required dependencies are available
/// </summary>
public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false;
/// <summary>
/// Whether any optional dependencies are missing
/// </summary>
public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false;
/// <summary>
/// Summary message about the dependency state
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Recommended next steps for the user
/// </summary>
public List<string> RecommendedActions { get; set; }
/// <summary>
/// Timestamp when this check was performed
/// </summary>
public DateTime CheckedAt { get; set; }
public DependencyCheckResult()
{
Dependencies = new List<DependencyStatus>();
RecommendedActions = new List<string>();
CheckedAt = DateTime.UtcNow;
}
/// <summary>
/// Get dependencies by availability status
/// </summary>
public List<DependencyStatus> GetMissingDependencies()
{
return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
}
/// <summary>
/// Get missing required dependencies
/// </summary>
public List<DependencyStatus> GetMissingRequired()
{
return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
}
/// <summary>
/// Generate a user-friendly summary of the dependency state
/// </summary>
public void GenerateSummary()
{
var missing = GetMissingDependencies();
var missingRequired = GetMissingRequired();
if (missing.Count == 0)
{
Summary = "All dependencies are available and ready.";
IsSystemReady = true;
}
else if (missingRequired.Count == 0)
{
Summary = $"System is ready. {missing.Count} optional dependencies are missing.";
IsSystemReady = true;
}
else
{
Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing.";
IsSystemReady = false;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 789012345678901234abcdef01234567
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,65 @@
using System;
namespace MCPForUnity.Editor.Dependencies.Models
{
/// <summary>
/// Represents the status of a dependency check
/// </summary>
[Serializable]
public class DependencyStatus
{
/// <summary>
/// Name of the dependency being checked
/// </summary>
public string Name { get; set; }
/// <summary>
/// Whether the dependency is available and functional
/// </summary>
public bool IsAvailable { get; set; }
/// <summary>
/// Version information if available
/// </summary>
public string Version { get; set; }
/// <summary>
/// Path to the dependency executable/installation
/// </summary>
public string Path { get; set; }
/// <summary>
/// Additional details about the dependency status
/// </summary>
public string Details { get; set; }
/// <summary>
/// Error message if dependency check failed
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// Whether this dependency is required for basic functionality
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Suggested installation method or URL
/// </summary>
public string InstallationHint { get; set; }
public DependencyStatus(string name, bool isRequired = true)
{
Name = name;
IsRequired = isRequired;
IsAvailable = false;
}
public override string ToString()
{
var status = IsAvailable ? "✓" : "✗";
var version = !string.IsNullOrEmpty(Version) ? $" ({Version})" : "";
return $"{status} {Name}{version}";
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6789012345678901234abcdef0123456
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,127 @@
using System;
using UnityEngine;
namespace MCPForUnity.Editor.Dependencies.Models
{
/// <summary>
/// Persistent state for the setup wizard to avoid repeated prompts
/// </summary>
[Serializable]
public class SetupState
{
/// <summary>
/// Whether the user has completed the initial setup wizard
/// </summary>
public bool HasCompletedSetup { get; set; }
/// <summary>
/// Whether the user has dismissed the setup wizard permanently
/// </summary>
public bool HasDismissedSetup { get; set; }
/// <summary>
/// Last time dependencies were checked
/// </summary>
public string LastDependencyCheck { get; set; }
/// <summary>
/// Version of the package when setup was last completed
/// </summary>
public string SetupVersion { get; set; }
/// <summary>
/// Whether to show the setup wizard on next domain reload
/// </summary>
public bool ShowSetupOnReload { get; set; }
/// <summary>
/// User's preferred installation mode (automatic/manual)
/// </summary>
public string PreferredInstallMode { get; set; }
/// <summary>
/// Number of times setup has been attempted
/// </summary>
public int SetupAttempts { get; set; }
/// <summary>
/// Last error encountered during setup
/// </summary>
public string LastSetupError { get; set; }
public SetupState()
{
HasCompletedSetup = false;
HasDismissedSetup = false;
ShowSetupOnReload = false;
PreferredInstallMode = "automatic";
SetupAttempts = 0;
}
/// <summary>
/// Check if setup should be shown based on current state
/// </summary>
public bool ShouldShowSetup(string currentVersion)
{
// Don't show if user has permanently dismissed
if (HasDismissedSetup)
return false;
// Show if never completed setup
if (!HasCompletedSetup)
return true;
// Show if package version has changed significantly
if (!string.IsNullOrEmpty(currentVersion) && SetupVersion != currentVersion)
return true;
// Show if explicitly requested
if (ShowSetupOnReload)
return true;
return false;
}
/// <summary>
/// Mark setup as completed for the current version
/// </summary>
public void MarkSetupCompleted(string version)
{
HasCompletedSetup = true;
SetupVersion = version;
ShowSetupOnReload = false;
LastSetupError = null;
}
/// <summary>
/// Mark setup as dismissed permanently
/// </summary>
public void MarkSetupDismissed()
{
HasDismissedSetup = true;
ShowSetupOnReload = false;
}
/// <summary>
/// Record a setup attempt with optional error
/// </summary>
public void RecordSetupAttempt(string error = null)
{
SetupAttempts++;
LastSetupError = error;
}
/// <summary>
/// Reset setup state (for debugging or re-setup)
/// </summary>
public void Reset()
{
HasCompletedSetup = false;
HasDismissedSetup = false;
ShowSetupOnReload = false;
SetupAttempts = 0;
LastSetupError = null;
LastDependencyCheck = null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 89012345678901234abcdef012345678
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c3d4e5f6789012345678901234abcdef
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,50 @@
using MCPForUnity.Editor.Dependencies.Models;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Interface for platform-specific dependency detection
/// </summary>
public interface IPlatformDetector
{
/// <summary>
/// Platform name this detector handles
/// </summary>
string PlatformName { get; }
/// <summary>
/// Whether this detector can run on the current platform
/// </summary>
bool CanDetect { get; }
/// <summary>
/// Detect Python installation on this platform
/// </summary>
DependencyStatus DetectPython();
/// <summary>
/// Detect UV package manager on this platform
/// </summary>
DependencyStatus DetectUV();
/// <summary>
/// Detect MCP server installation on this platform
/// </summary>
DependencyStatus DetectMCPServer();
/// <summary>
/// Get platform-specific installation recommendations
/// </summary>
string GetInstallationRecommendations();
/// <summary>
/// Get platform-specific Python installation URL
/// </summary>
string GetPythonInstallUrl();
/// <summary>
/// Get platform-specific UV installation URL
/// </summary>
string GetUVInstallUrl();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9012345678901234abcdef0123456789
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,351 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Linux-specific dependency detection
/// </summary>
public class LinuxPlatformDetector : IPlatformDetector
{
public string PlatformName => "Linux";
public bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
public DependencyStatus DetectPython()
{
var status = new DependencyStatus("Python", isRequired: true)
{
InstallationHint = GetPythonInstallUrl()
};
try
{
// Check common Python installation paths on Linux
var candidates = new[]
{
"python3",
"python",
"/usr/bin/python3",
"/usr/local/bin/python3",
"/opt/python/bin/python3",
"/snap/bin/python3"
};
foreach (var candidate in candidates)
{
if (TryValidatePython(candidate, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} at {fullPath}";
return status;
}
}
// Try PATH resolution using 'which' command
if (TryFindInPath("python3", out string pathResult) ||
TryFindInPath("python", out pathResult))
{
if (TryValidatePython(pathResult, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH at {fullPath}";
return status;
}
}
status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
status.Details = "Checked common installation paths including system, snap, and user-local locations.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
}
return status;
}
public DependencyStatus DetectUV()
{
var status = new DependencyStatus("UV Package Manager", isRequired: true)
{
InstallationHint = GetUVInstallUrl()
};
try
{
// Use existing UV detection from ServerInstaller
string uvPath = ServerInstaller.FindUvPath();
if (!string.IsNullOrEmpty(uvPath))
{
if (TryValidateUV(uvPath, out string version))
{
status.IsAvailable = true;
status.Version = version;
status.Path = uvPath;
status.Details = $"Found UV {version} at {uvPath}";
return status;
}
}
status.ErrorMessage = "UV package manager not found. Please install UV.";
status.Details = "UV is required for managing Python dependencies.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting UV: {ex.Message}";
}
return status;
}
public DependencyStatus DetectMCPServer()
{
var status = new DependencyStatus("MCP Server", isRequired: false);
try
{
// Check if server is installed
string serverPath = ServerInstaller.GetServerPath();
string serverPy = Path.Combine(serverPath, "server.py");
if (File.Exists(serverPy))
{
status.IsAvailable = true;
status.Path = serverPath;
// Try to get version
string versionFile = Path.Combine(serverPath, "server_version.txt");
if (File.Exists(versionFile))
{
status.Version = File.ReadAllText(versionFile).Trim();
}
status.Details = $"MCP Server found at {serverPath}";
}
else
{
// Check for embedded server
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath))
{
status.IsAvailable = true;
status.Path = embeddedPath;
status.Details = "MCP Server available (embedded in package)";
}
else
{
status.ErrorMessage = "MCP Server not found";
status.Details = "Server will be installed automatically when needed";
}
}
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}";
}
return status;
}
public string GetInstallationRecommendations()
{
return @"Linux Installation Recommendations:
1. Python: Install via package manager or pyenv
- Ubuntu/Debian: sudo apt install python3 python3-pip
- Fedora/RHEL: sudo dnf install python3 python3-pip
- Arch: sudo pacman -S python python-pip
- Or use pyenv: https://github.com/pyenv/pyenv
2. UV Package Manager: Install via curl
- Run: curl -LsSf https://astral.sh/uv/install.sh | sh
- Or download from: https://github.com/astral-sh/uv/releases
3. MCP Server: Will be installed automatically by Unity MCP Bridge
Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
}
public string GetPythonInstallUrl()
{
return "https://www.python.org/downloads/source/";
}
public string GetUVInstallUrl()
{
return "https://docs.astral.sh/uv/getting-started/installation/#linux";
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = pythonPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Set PATH to include common locations
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pathAdditions = new[]
{
"/usr/local/bin",
"/usr/bin",
"/bin",
"/snap/bin",
Path.Combine(homeDir, ".local", "bin")
};
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("Python "))
{
version = output.Substring(7); // Remove "Python " prefix
fullPath = pythonPath;
// Validate minimum version (3.10+)
if (TryParseVersion(version, out var major, out var minor))
{
return major >= 3 && minor >= 10;
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
private bool TryValidateUV(string uvPath, out string version)
{
version = null;
try
{
var psi = new ProcessStartInfo
{
FileName = uvPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("uv "))
{
version = output.Substring(3); // Remove "uv " prefix
return true;
}
}
catch
{
// Ignore validation errors
}
return false;
}
private bool TryFindInPath(string executable, out string fullPath)
{
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = "/usr/bin/which",
Arguments = executable,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Enhance PATH for Unity's GUI environment
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pathAdditions = new[]
{
"/usr/local/bin",
"/usr/bin",
"/bin",
"/snap/bin",
Path.Combine(homeDir, ".local", "bin")
};
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(3000);
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
{
fullPath = output;
return true;
}
}
catch
{
// Ignore errors
}
return false;
}
private bool TryParseVersion(string version, out int major, out int minor)
{
major = 0;
minor = 0;
try
{
var parts = version.Split('.');
if (parts.Length >= 2)
{
return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
}
}
catch
{
// Ignore parsing errors
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2345678901234abcdef0123456789abc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,351 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// macOS-specific dependency detection
/// </summary>
public class MacOSPlatformDetector : IPlatformDetector
{
public string PlatformName => "macOS";
public bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public DependencyStatus DetectPython()
{
var status = new DependencyStatus("Python", isRequired: true)
{
InstallationHint = GetPythonInstallUrl()
};
try
{
// Check common Python installation paths on macOS
var candidates = new[]
{
"python3",
"python",
"/usr/bin/python3",
"/usr/local/bin/python3",
"/opt/homebrew/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.11/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.10/bin/python3"
};
foreach (var candidate in candidates)
{
if (TryValidatePython(candidate, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} at {fullPath}";
return status;
}
}
// Try PATH resolution using 'which' command
if (TryFindInPath("python3", out string pathResult) ||
TryFindInPath("python", out pathResult))
{
if (TryValidatePython(pathResult, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH at {fullPath}";
return status;
}
}
status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
status.Details = "Checked common installation paths including Homebrew, Framework, and system locations.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
}
return status;
}
public DependencyStatus DetectUV()
{
var status = new DependencyStatus("UV Package Manager", isRequired: true)
{
InstallationHint = GetUVInstallUrl()
};
try
{
// Use existing UV detection from ServerInstaller
string uvPath = ServerInstaller.FindUvPath();
if (!string.IsNullOrEmpty(uvPath))
{
if (TryValidateUV(uvPath, out string version))
{
status.IsAvailable = true;
status.Version = version;
status.Path = uvPath;
status.Details = $"Found UV {version} at {uvPath}";
return status;
}
}
status.ErrorMessage = "UV package manager not found. Please install UV.";
status.Details = "UV is required for managing Python dependencies.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting UV: {ex.Message}";
}
return status;
}
public DependencyStatus DetectMCPServer()
{
var status = new DependencyStatus("MCP Server", isRequired: false);
try
{
// Check if server is installed
string serverPath = ServerInstaller.GetServerPath();
string serverPy = Path.Combine(serverPath, "server.py");
if (File.Exists(serverPy))
{
status.IsAvailable = true;
status.Path = serverPath;
// Try to get version
string versionFile = Path.Combine(serverPath, "server_version.txt");
if (File.Exists(versionFile))
{
status.Version = File.ReadAllText(versionFile).Trim();
}
status.Details = $"MCP Server found at {serverPath}";
}
else
{
// Check for embedded server
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath))
{
status.IsAvailable = true;
status.Path = embeddedPath;
status.Details = "MCP Server available (embedded in package)";
}
else
{
status.ErrorMessage = "MCP Server not found";
status.Details = "Server will be installed automatically when needed";
}
}
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}";
}
return status;
}
public string GetInstallationRecommendations()
{
return @"macOS Installation Recommendations:
1. Python: Install via Homebrew (recommended) or python.org
- Homebrew: brew install python3
- Direct download: https://python.org/downloads/macos/
2. UV Package Manager: Install via curl or Homebrew
- Curl: curl -LsSf https://astral.sh/uv/install.sh | sh
- Homebrew: brew install uv
3. MCP Server: Will be installed automatically by Unity MCP Bridge
Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
}
public string GetPythonInstallUrl()
{
return "https://www.python.org/downloads/macos/";
}
public string GetUVInstallUrl()
{
return "https://docs.astral.sh/uv/getting-started/installation/#macos";
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = pythonPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Set PATH to include common locations
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pathAdditions = new[]
{
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
Path.Combine(homeDir, ".local", "bin")
};
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("Python "))
{
version = output.Substring(7); // Remove "Python " prefix
fullPath = pythonPath;
// Validate minimum version (3.10+)
if (TryParseVersion(version, out var major, out var minor))
{
return major >= 3 && minor >= 10;
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
private bool TryValidateUV(string uvPath, out string version)
{
version = null;
try
{
var psi = new ProcessStartInfo
{
FileName = uvPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("uv "))
{
version = output.Substring(3); // Remove "uv " prefix
return true;
}
}
catch
{
// Ignore validation errors
}
return false;
}
private bool TryFindInPath(string executable, out string fullPath)
{
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = "/usr/bin/which",
Arguments = executable,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Enhance PATH for Unity's GUI environment
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pathAdditions = new[]
{
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin",
Path.Combine(homeDir, ".local", "bin")
};
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(3000);
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
{
fullPath = output;
return true;
}
}
catch
{
// Ignore errors
}
return false;
}
private bool TryParseVersion(string version, out int major, out int minor)
{
major = 0;
minor = 0;
try
{
var parts = version.Split('.');
if (parts.Length >= 2)
{
return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
}
}
catch
{
// Ignore parsing errors
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 12345678901234abcdef0123456789ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,330 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
{
/// <summary>
/// Windows-specific dependency detection
/// </summary>
public class WindowsPlatformDetector : IPlatformDetector
{
public string PlatformName => "Windows";
public bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public DependencyStatus DetectPython()
{
var status = new DependencyStatus("Python", isRequired: true)
{
InstallationHint = GetPythonInstallUrl()
};
try
{
// Check common Python installation paths
var candidates = new[]
{
"python.exe",
"python3.exe",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Programs", "Python", "Python313", "python.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Programs", "Python", "Python312", "python.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Programs", "Python", "Python311", "python.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"Python313", "python.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"Python312", "python.exe")
};
foreach (var candidate in candidates)
{
if (TryValidatePython(candidate, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} at {fullPath}";
return status;
}
}
// Try PATH resolution using 'where' command
if (TryFindInPath("python.exe", out string pathResult) ||
TryFindInPath("python3.exe", out pathResult))
{
if (TryValidatePython(pathResult, out string version, out string fullPath))
{
status.IsAvailable = true;
status.Version = version;
status.Path = fullPath;
status.Details = $"Found Python {version} in PATH at {fullPath}";
return status;
}
}
status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
status.Details = "Checked common installation paths and PATH environment variable.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting Python: {ex.Message}";
}
return status;
}
public DependencyStatus DetectUV()
{
var status = new DependencyStatus("UV Package Manager", isRequired: true)
{
InstallationHint = GetUVInstallUrl()
};
try
{
// Use existing UV detection from ServerInstaller
string uvPath = ServerInstaller.FindUvPath();
if (!string.IsNullOrEmpty(uvPath))
{
if (TryValidateUV(uvPath, out string version))
{
status.IsAvailable = true;
status.Version = version;
status.Path = uvPath;
status.Details = $"Found UV {version} at {uvPath}";
return status;
}
}
status.ErrorMessage = "UV package manager not found. Please install UV.";
status.Details = "UV is required for managing Python dependencies.";
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting UV: {ex.Message}";
}
return status;
}
public DependencyStatus DetectMCPServer()
{
var status = new DependencyStatus("MCP Server", isRequired: false);
try
{
// Check if server is installed
string serverPath = ServerInstaller.GetServerPath();
string serverPy = Path.Combine(serverPath, "server.py");
if (File.Exists(serverPy))
{
status.IsAvailable = true;
status.Path = serverPath;
// Try to get version
string versionFile = Path.Combine(serverPath, "server_version.txt");
if (File.Exists(versionFile))
{
status.Version = File.ReadAllText(versionFile).Trim();
}
status.Details = $"MCP Server found at {serverPath}";
}
else
{
// Check for embedded server
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embeddedPath))
{
status.IsAvailable = true;
status.Path = embeddedPath;
status.Details = "MCP Server available (embedded in package)";
}
else
{
status.ErrorMessage = "MCP Server not found";
status.Details = "Server will be installed automatically when needed";
}
}
}
catch (Exception ex)
{
status.ErrorMessage = $"Error detecting MCP Server: {ex.Message}";
}
return status;
}
public string GetInstallationRecommendations()
{
return @"Windows Installation Recommendations:
1. Python: Install from Microsoft Store or python.org
- Microsoft Store: Search for 'Python 3.12' or 'Python 3.13'
- Direct download: https://python.org/downloads/windows/
2. UV Package Manager: Install via PowerShell
- Run: powershell -ExecutionPolicy ByPass -c ""irm https://astral.sh/uv/install.ps1 | iex""
- Or download from: https://github.com/astral-sh/uv/releases
3. MCP Server: Will be installed automatically by Unity MCP Bridge";
}
public string GetPythonInstallUrl()
{
return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP";
}
public string GetUVInstallUrl()
{
return "https://docs.astral.sh/uv/getting-started/installation/#windows";
}
private bool TryValidatePython(string pythonPath, out string version, out string fullPath)
{
version = null;
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = pythonPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("Python "))
{
version = output.Substring(7); // Remove "Python " prefix
fullPath = pythonPath;
// Validate minimum version (3.10+)
if (TryParseVersion(version, out var major, out var minor))
{
return major >= 3 && minor >= 10;
}
}
}
catch
{
// Ignore validation errors
}
return false;
}
private bool TryValidateUV(string uvPath, out string version)
{
version = null;
try
{
var psi = new ProcessStartInfo
{
FileName = uvPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(5000);
if (process.ExitCode == 0 && output.StartsWith("uv "))
{
version = output.Substring(3); // Remove "uv " prefix
return true;
}
}
catch
{
// Ignore validation errors
}
return false;
}
private bool TryFindInPath(string executable, out string fullPath)
{
fullPath = null;
try
{
var psi = new ProcessStartInfo
{
FileName = "where",
Arguments = executable,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return false;
string output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(3000);
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
{
// Take the first result
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length > 0)
{
fullPath = lines[0].Trim();
return File.Exists(fullPath);
}
}
}
catch
{
// Ignore errors
}
return false;
}
private bool TryParseVersion(string version, out int major, out int minor)
{
major = 0;
minor = 0;
try
{
var parts = version.Split('.');
if (parts.Length >= 2)
{
return int.TryParse(parts[0], out major) && int.TryParse(parts[1], out minor);
}
}
catch
{
// Ignore parsing errors
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 012345678901234abcdef0123456789a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 94cb070dc5e15024da86150b27699ca0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,129 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Helpers
{
public static class ConfigJsonBuilder
{
public static string BuildManualConfigJson(string uvPath, string pythonDir, McpClient client)
{
var root = new JObject();
bool isVSCode = client?.mcpType == McpTypes.VSCode;
JObject container;
if (isVSCode)
{
container = EnsureObject(root, "servers");
}
else
{
container = EnsureObject(root, "mcpServers");
}
var unity = new JObject();
PopulateUnityNode(unity, uvPath, pythonDir, client, isVSCode);
container["unityMCP"] = unity;
return root.ToString(Formatting.Indented);
}
public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, string serverSrc, McpClient client)
{
if (root == null) root = new JObject();
bool isVSCode = client?.mcpType == McpTypes.VSCode;
JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers");
JObject unity = container["unityMCP"] as JObject ?? new JObject();
PopulateUnityNode(unity, uvPath, serverSrc, client, isVSCode);
container["unityMCP"] = unity;
return root;
}
/// <summary>
/// Centralized builder that applies all caveats consistently.
/// - Sets command/args with provided directory
/// - Ensures env exists
/// - Adds type:"stdio" for VSCode
/// - Adds disabled:false for Windsurf/Kiro only when missing
/// </summary>
private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode)
{
unity["command"] = uvPath;
// For Cursor (non-VSCode) on macOS, prefer a no-spaces symlink path to avoid arg parsing issues in some runners
string effectiveDir = directory;
#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
bool isCursor = !isVSCode && (client == null || client.mcpType != McpTypes.VSCode);
if (isCursor && !string.IsNullOrEmpty(directory))
{
// Replace canonical path segment with the symlink path if present
const string canonical = "/Library/Application Support/";
const string symlinkSeg = "/Library/AppSupport/";
try
{
// Normalize to full path style
if (directory.Contains(canonical))
{
var candidate = directory.Replace(canonical, symlinkSeg).Replace('\\', '/');
if (System.IO.Directory.Exists(candidate))
{
effectiveDir = candidate;
}
}
else
{
// If installer returned XDG-style on macOS, map to canonical symlink
string norm = directory.Replace('\\', '/');
int idx = norm.IndexOf("/.local/share/UnityMCP/", System.StringComparison.Ordinal);
if (idx >= 0)
{
string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal) ?? string.Empty;
string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
string candidate = System.IO.Path.Combine(home, "Library", "AppSupport", suffix).Replace('\\', '/');
if (System.IO.Directory.Exists(candidate))
{
effectiveDir = candidate;
}
}
}
}
catch { /* fallback to original directory on any error */ }
}
#endif
unity["args"] = JArray.FromObject(new[] { "run", "--directory", effectiveDir, "server.py" });
if (isVSCode)
{
unity["type"] = "stdio";
}
else
{
// Remove type if it somehow exists from previous clients
if (unity["type"] != null) unity.Remove("type");
}
if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro))
{
if (unity["env"] == null)
{
unity["env"] = new JObject();
}
if (unity["disabled"] == null)
{
unity["disabled"] = false;
}
}
}
private static JObject EnsureObject(JObject parent, string name)
{
if (parent[name] is JObject o) return o;
var created = new JObject();
parent[name] = created;
return created;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5c07c3369f73943919d9e086a81d1dcc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,280 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using UnityEditor;
namespace MCPForUnity.Editor.Helpers
{
internal static class ExecPath
{
private const string PrefClaude = "MCPForUnity.ClaudeCliPath";
// Resolve Claude CLI absolute path. Pref → env → common locations → PATH.
internal static string ResolveClaude()
{
try
{
string pref = EditorPrefs.GetString(PrefClaude, string.Empty);
if (!string.IsNullOrEmpty(pref) && File.Exists(pref)) return pref;
}
catch { }
string env = Environment.GetEnvironmentVariable("CLAUDE_CLI");
if (!string.IsNullOrEmpty(env) && File.Exists(env)) return env;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] candidates =
{
"/opt/homebrew/bin/claude",
"/usr/local/bin/claude",
Path.Combine(home, ".local", "bin", "claude"),
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
string nvmClaude = ResolveClaudeFromNvm(home);
if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
return Which("claude", "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin");
#else
return null;
#endif
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
#if UNITY_EDITOR_WIN
// Common npm global locations
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
string[] candidates =
{
// Prefer .cmd (most reliable from non-interactive processes)
Path.Combine(appData, "npm", "claude.cmd"),
Path.Combine(localAppData, "npm", "claude.cmd"),
// Fall back to PowerShell shim if only .ps1 is present
Path.Combine(appData, "npm", "claude.ps1"),
Path.Combine(localAppData, "npm", "claude.ps1"),
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
#endif
return null;
}
// Linux
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] candidates =
{
"/usr/local/bin/claude",
"/usr/bin/claude",
Path.Combine(home, ".local", "bin", "claude"),
};
foreach (string c in candidates) { if (File.Exists(c)) return c; }
// Try NVM-installed claude under ~/.nvm/versions/node/*/bin/claude
string nvmClaude = ResolveClaudeFromNvm(home);
if (!string.IsNullOrEmpty(nvmClaude)) return nvmClaude;
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
return Which("claude", "/usr/local/bin:/usr/bin:/bin");
#else
return null;
#endif
}
}
// Attempt to resolve claude from NVM-managed Node installations, choosing the newest version
private static string ResolveClaudeFromNvm(string home)
{
try
{
if (string.IsNullOrEmpty(home)) return null;
string nvmNodeDir = Path.Combine(home, ".nvm", "versions", "node");
if (!Directory.Exists(nvmNodeDir)) return null;
string bestPath = null;
Version bestVersion = null;
foreach (string versionDir in Directory.EnumerateDirectories(nvmNodeDir))
{
string name = Path.GetFileName(versionDir);
if (string.IsNullOrEmpty(name)) continue;
if (name.StartsWith("v", StringComparison.OrdinalIgnoreCase))
{
// Extract numeric portion: e.g., v18.19.0-nightly -> 18.19.0
string versionStr = name.Substring(1);
int dashIndex = versionStr.IndexOf('-');
if (dashIndex > 0)
{
versionStr = versionStr.Substring(0, dashIndex);
}
if (Version.TryParse(versionStr, out Version parsed))
{
string candidate = Path.Combine(versionDir, "bin", "claude");
if (File.Exists(candidate))
{
if (bestVersion == null || parsed > bestVersion)
{
bestVersion = parsed;
bestPath = candidate;
}
}
}
}
}
return bestPath;
}
catch { return null; }
}
// Explicitly set the Claude CLI absolute path override in EditorPrefs
internal static void SetClaudeCliPath(string absolutePath)
{
try
{
if (!string.IsNullOrEmpty(absolutePath) && File.Exists(absolutePath))
{
EditorPrefs.SetString(PrefClaude, absolutePath);
}
}
catch { }
}
// Clear any previously set Claude CLI override path
internal static void ClearClaudeCliPath()
{
try
{
if (EditorPrefs.HasKey(PrefClaude))
{
EditorPrefs.DeleteKey(PrefClaude);
}
}
catch { }
}
// Use existing UV resolver; returns absolute path or null.
internal static string ResolveUv()
{
return ServerInstaller.FindUvPath();
}
internal static bool TryRun(
string file,
string args,
string workingDir,
out string stdout,
out string stderr,
int timeoutMs = 15000,
string extraPathPrepend = null)
{
stdout = string.Empty;
stderr = string.Empty;
try
{
// Handle PowerShell scripts on Windows by invoking through powershell.exe
bool isPs1 = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
file.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase);
var psi = new ProcessStartInfo
{
FileName = isPs1 ? "powershell.exe" : file,
Arguments = isPs1
? $"-NoProfile -ExecutionPolicy Bypass -File \"{file}\" {args}".Trim()
: args,
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Environment.CurrentDirectory : workingDir,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
if (!string.IsNullOrEmpty(extraPathPrepend))
{
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath)
? extraPathPrepend
: (extraPathPrepend + System.IO.Path.PathSeparator + currentPath);
}
using var process = new Process { StartInfo = psi, EnableRaisingEvents = false };
var so = new StringBuilder();
var se = new StringBuilder();
process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); };
if (!process.Start()) return false;
process.BeginOutputReadLine();
process.BeginErrorReadLine();
if (!process.WaitForExit(timeoutMs))
{
try { process.Kill(); } catch { }
return false;
}
// Ensure async buffers are flushed
process.WaitForExit();
stdout = so.ToString();
stderr = se.ToString();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
private static string Which(string exe, string prependPath)
{
try
{
var psi = new ProcessStartInfo("/usr/bin/which", exe)
{
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
using var p = Process.Start(psi);
string output = p?.StandardOutput.ReadToEnd().Trim();
p?.WaitForExit(1500);
return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
}
catch { return null; }
}
#endif
#if UNITY_EDITOR_WIN
private static string Where(string exe)
{
try
{
var psi = new ProcessStartInfo("where", exe)
{
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
using var p = Process.Start(psi);
string first = p?.StandardOutput.ReadToEnd()
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault();
p?.WaitForExit(1500);
return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
}
catch { return null; }
}
#endif
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,527 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Runtime.Serialization; // For Converters
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Handles serialization of GameObjects and Components for MCP responses.
/// Includes reflection helpers and caching for performance.
/// </summary>
public static class GameObjectSerializer
{
// --- Data Serialization ---
/// <summary>
/// Creates a serializable representation of a GameObject.
/// </summary>
public static object GetGameObjectData(GameObject go)
{
if (go == null)
return null;
return new
{
name = go.name,
instanceID = go.GetInstanceID(),
tag = go.tag,
layer = go.layer,
activeSelf = go.activeSelf,
activeInHierarchy = go.activeInHierarchy,
isStatic = go.isStatic,
scenePath = go.scene.path, // Identify which scene it belongs to
transform = new // Serialize transform components carefully to avoid JSON issues
{
// Serialize Vector3 components individually to prevent self-referencing loops.
// The default serializer can struggle with properties like Vector3.normalized.
position = new
{
x = go.transform.position.x,
y = go.transform.position.y,
z = go.transform.position.z,
},
localPosition = new
{
x = go.transform.localPosition.x,
y = go.transform.localPosition.y,
z = go.transform.localPosition.z,
},
rotation = new
{
x = go.transform.rotation.eulerAngles.x,
y = go.transform.rotation.eulerAngles.y,
z = go.transform.rotation.eulerAngles.z,
},
localRotation = new
{
x = go.transform.localRotation.eulerAngles.x,
y = go.transform.localRotation.eulerAngles.y,
z = go.transform.localRotation.eulerAngles.z,
},
scale = new
{
x = go.transform.localScale.x,
y = go.transform.localScale.y,
z = go.transform.localScale.z,
},
forward = new
{
x = go.transform.forward.x,
y = go.transform.forward.y,
z = go.transform.forward.z,
},
up = new
{
x = go.transform.up.x,
y = go.transform.up.y,
z = go.transform.up.z,
},
right = new
{
x = go.transform.right.x,
y = go.transform.right.y,
z = go.transform.right.z,
},
},
parentInstanceID = go.transform.parent?.gameObject.GetInstanceID() ?? 0, // 0 if no parent
// Optionally include components, but can be large
// components = go.GetComponents<Component>().Select(c => GetComponentData(c)).ToList()
// Or just component names:
componentNames = go.GetComponents<Component>()
.Select(c => c.GetType().FullName)
.ToList(),
};
}
// --- Metadata Caching for Reflection ---
private class CachedMetadata
{
public readonly List<PropertyInfo> SerializableProperties;
public readonly List<FieldInfo> SerializableFields;
public CachedMetadata(List<PropertyInfo> properties, List<FieldInfo> fields)
{
SerializableProperties = properties;
SerializableFields = fields;
}
}
// Key becomes Tuple<Type, bool>
private static readonly Dictionary<Tuple<Type, bool>, CachedMetadata> _metadataCache = new Dictionary<Tuple<Type, bool>, CachedMetadata>();
// --- End Metadata Caching ---
/// <summary>
/// Creates a serializable representation of a Component, attempting to serialize
/// public properties and fields using reflection, with caching and control over non-public fields.
/// </summary>
// Add the flag parameter here
public static object GetComponentData(Component c, bool includeNonPublicSerializedFields = true)
{
// --- Add Early Logging ---
// Debug.Log($"[GetComponentData] Starting for component: {c?.GetType()?.FullName ?? "null"} (ID: {c?.GetInstanceID() ?? 0})");
// --- End Early Logging ---
if (c == null) return null;
Type componentType = c.GetType();
// --- Special handling for Transform to avoid reflection crashes and problematic properties ---
if (componentType == typeof(Transform))
{
Transform tr = c as Transform;
// Debug.Log($"[GetComponentData] Manually serializing Transform (ID: {tr.GetInstanceID()})");
return new Dictionary<string, object>
{
{ "typeName", componentType.FullName },
{ "instanceID", tr.GetInstanceID() },
// Manually extract known-safe properties. Avoid Quaternion 'rotation' and 'lossyScale'.
{ "position", CreateTokenFromValue(tr.position, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "localPosition", CreateTokenFromValue(tr.localPosition, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "eulerAngles", CreateTokenFromValue(tr.eulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() }, // Use Euler angles
{ "localEulerAngles", CreateTokenFromValue(tr.localEulerAngles, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "localScale", CreateTokenFromValue(tr.localScale, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "right", CreateTokenFromValue(tr.right, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "up", CreateTokenFromValue(tr.up, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "forward", CreateTokenFromValue(tr.forward, typeof(Vector3))?.ToObject<object>() ?? new JObject() },
{ "parentInstanceID", tr.parent?.gameObject.GetInstanceID() ?? 0 },
{ "rootInstanceID", tr.root?.gameObject.GetInstanceID() ?? 0 },
{ "childCount", tr.childCount },
// Include standard Object/Component properties
{ "name", tr.name },
{ "tag", tr.tag },
{ "gameObjectInstanceID", tr.gameObject?.GetInstanceID() ?? 0 }
};
}
// --- End Special handling for Transform ---
// --- Special handling for Camera to avoid matrix-related crashes ---
if (componentType == typeof(Camera))
{
Camera cam = c as Camera;
var cameraProperties = new Dictionary<string, object>();
// List of safe properties to serialize
var safeProperties = new Dictionary<string, Func<object>>
{
{ "nearClipPlane", () => cam.nearClipPlane },
{ "farClipPlane", () => cam.farClipPlane },
{ "fieldOfView", () => cam.fieldOfView },
{ "renderingPath", () => (int)cam.renderingPath },
{ "actualRenderingPath", () => (int)cam.actualRenderingPath },
{ "allowHDR", () => cam.allowHDR },
{ "allowMSAA", () => cam.allowMSAA },
{ "allowDynamicResolution", () => cam.allowDynamicResolution },
{ "forceIntoRenderTexture", () => cam.forceIntoRenderTexture },
{ "orthographicSize", () => cam.orthographicSize },
{ "orthographic", () => cam.orthographic },
{ "opaqueSortMode", () => (int)cam.opaqueSortMode },
{ "transparencySortMode", () => (int)cam.transparencySortMode },
{ "depth", () => cam.depth },
{ "aspect", () => cam.aspect },
{ "cullingMask", () => cam.cullingMask },
{ "eventMask", () => cam.eventMask },
{ "backgroundColor", () => cam.backgroundColor },
{ "clearFlags", () => (int)cam.clearFlags },
{ "stereoEnabled", () => cam.stereoEnabled },
{ "stereoSeparation", () => cam.stereoSeparation },
{ "stereoConvergence", () => cam.stereoConvergence },
{ "enabled", () => cam.enabled },
{ "name", () => cam.name },
{ "tag", () => cam.tag },
{ "gameObject", () => new { name = cam.gameObject.name, instanceID = cam.gameObject.GetInstanceID() } }
};
foreach (var prop in safeProperties)
{
try
{
var value = prop.Value();
if (value != null)
{
AddSerializableValue(cameraProperties, prop.Key, value.GetType(), value);
}
}
catch (Exception)
{
// Silently skip any property that fails
continue;
}
}
return new Dictionary<string, object>
{
{ "typeName", componentType.FullName },
{ "instanceID", cam.GetInstanceID() },
{ "properties", cameraProperties }
};
}
// --- End Special handling for Camera ---
var data = new Dictionary<string, object>
{
{ "typeName", componentType.FullName },
{ "instanceID", c.GetInstanceID() }
};
// --- Get Cached or Generate Metadata (using new cache key) ---
Tuple<Type, bool> cacheKey = new Tuple<Type, bool>(componentType, includeNonPublicSerializedFields);
if (!_metadataCache.TryGetValue(cacheKey, out CachedMetadata cachedData))
{
var propertiesToCache = new List<PropertyInfo>();
var fieldsToCache = new List<FieldInfo>();
// Traverse the hierarchy from the component type up to MonoBehaviour
Type currentType = componentType;
while (currentType != null && currentType != typeof(MonoBehaviour) && currentType != typeof(object))
{
// Get properties declared only at the current type level
BindingFlags propFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
foreach (var propInfo in currentType.GetProperties(propFlags))
{
// Basic filtering (readable, not indexer, not transform which is handled elsewhere)
if (!propInfo.CanRead || propInfo.GetIndexParameters().Length > 0 || propInfo.Name == "transform") continue;
// Add if not already added (handles overrides - keep the most derived version)
if (!propertiesToCache.Any(p => p.Name == propInfo.Name)) {
propertiesToCache.Add(propInfo);
}
}
// Get fields declared only at the current type level (both public and non-public)
BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
var declaredFields = currentType.GetFields(fieldFlags);
// Process the declared Fields for caching
foreach (var fieldInfo in declaredFields)
{
if (fieldInfo.Name.EndsWith("k__BackingField")) continue; // Skip backing fields
// Add if not already added (handles hiding - keep the most derived version)
if (fieldsToCache.Any(f => f.Name == fieldInfo.Name)) continue;
bool shouldInclude = false;
if (includeNonPublicSerializedFields)
{
// If TRUE, include Public OR NonPublic with [SerializeField]
shouldInclude = fieldInfo.IsPublic || (fieldInfo.IsPrivate && fieldInfo.IsDefined(typeof(SerializeField), inherit: false));
}
else // includeNonPublicSerializedFields is FALSE
{
// If FALSE, include ONLY if it is explicitly Public.
shouldInclude = fieldInfo.IsPublic;
}
if (shouldInclude)
{
fieldsToCache.Add(fieldInfo);
}
}
// Move to the base type
currentType = currentType.BaseType;
}
// --- End Hierarchy Traversal ---
cachedData = new CachedMetadata(propertiesToCache, fieldsToCache);
_metadataCache[cacheKey] = cachedData; // Add to cache with combined key
}
// --- End Get Cached or Generate Metadata ---
// --- Use cached metadata ---
var serializablePropertiesOutput = new Dictionary<string, object>();
// --- Add Logging Before Property Loop ---
// Debug.Log($"[GetComponentData] Starting property loop for {componentType.Name}...");
// --- End Logging Before Property Loop ---
// Use cached properties
foreach (var propInfo in cachedData.SerializableProperties)
{
string propName = propInfo.Name;
// --- Skip known obsolete/problematic Component shortcut properties ---
bool skipProperty = false;
if (propName == "rigidbody" || propName == "rigidbody2D" || propName == "camera" ||
propName == "light" || propName == "animation" || propName == "constantForce" ||
propName == "renderer" || propName == "audio" || propName == "networkView" ||
propName == "collider" || propName == "collider2D" || propName == "hingeJoint" ||
propName == "particleSystem" ||
// Also skip potentially problematic Matrix properties prone to cycles/errors
propName == "worldToLocalMatrix" || propName == "localToWorldMatrix")
{
// Debug.Log($"[GetComponentData] Explicitly skipping generic property: {propName}"); // Optional log
skipProperty = true;
}
// --- End Skip Generic Properties ---
// --- Skip specific potentially problematic Camera properties ---
if (componentType == typeof(Camera) &&
(propName == "pixelRect" ||
propName == "rect" ||
propName == "cullingMatrix" ||
propName == "useOcclusionCulling" ||
propName == "worldToCameraMatrix" ||
propName == "projectionMatrix" ||
propName == "nonJitteredProjectionMatrix" ||
propName == "previousViewProjectionMatrix" ||
propName == "cameraToWorldMatrix"))
{
// Debug.Log($"[GetComponentData] Explicitly skipping Camera property: {propName}");
skipProperty = true;
}
// --- End Skip Camera Properties ---
// --- Skip specific potentially problematic Transform properties ---
if (componentType == typeof(Transform) &&
(propName == "lossyScale" ||
propName == "rotation" ||
propName == "worldToLocalMatrix" ||
propName == "localToWorldMatrix"))
{
// Debug.Log($"[GetComponentData] Explicitly skipping Transform property: {propName}");
skipProperty = true;
}
// --- End Skip Transform Properties ---
// Skip if flagged
if (skipProperty)
{
continue;
}
try
{
// --- Add detailed logging ---
// Debug.Log($"[GetComponentData] Accessing: {componentType.Name}.{propName}");
// --- End detailed logging ---
object value = propInfo.GetValue(c);
Type propType = propInfo.PropertyType;
AddSerializableValue(serializablePropertiesOutput, propName, propType, value);
}
catch (Exception)
{
// Debug.LogWarning($"Could not read property {propName} on {componentType.Name}");
}
}
// --- Add Logging Before Field Loop ---
// Debug.Log($"[GetComponentData] Starting field loop for {componentType.Name}...");
// --- End Logging Before Field Loop ---
// Use cached fields
foreach (var fieldInfo in cachedData.SerializableFields)
{
try
{
// --- Add detailed logging for fields ---
// Debug.Log($"[GetComponentData] Accessing Field: {componentType.Name}.{fieldInfo.Name}");
// --- End detailed logging for fields ---
object value = fieldInfo.GetValue(c);
string fieldName = fieldInfo.Name;
Type fieldType = fieldInfo.FieldType;
AddSerializableValue(serializablePropertiesOutput, fieldName, fieldType, value);
}
catch (Exception)
{
// Debug.LogWarning($"Could not read field {fieldInfo.Name} on {componentType.Name}");
}
}
// --- End Use cached metadata ---
if (serializablePropertiesOutput.Count > 0)
{
data["properties"] = serializablePropertiesOutput;
}
return data;
}
// Helper function to decide how to serialize different types
private static void AddSerializableValue(Dictionary<string, object> dict, string name, Type type, object value)
{
// Simplified: Directly use CreateTokenFromValue which uses the serializer
if (value == null)
{
dict[name] = null;
return;
}
try
{
// Use the helper that employs our custom serializer settings
JToken token = CreateTokenFromValue(value, type);
if (token != null) // Check if serialization succeeded in the helper
{
// Convert JToken back to a basic object structure for the dictionary
dict[name] = ConvertJTokenToPlainObject(token);
}
// If token is null, it means serialization failed and a warning was logged.
}
catch (Exception e)
{
// Catch potential errors during JToken conversion or addition to dictionary
Debug.LogWarning($"[AddSerializableValue] Error processing value for '{name}' (Type: {type.FullName}): {e.Message}. Skipping.");
}
}
// Helper to convert JToken back to basic object structure
private static object ConvertJTokenToPlainObject(JToken token)
{
if (token == null) return null;
switch (token.Type)
{
case JTokenType.Object:
var objDict = new Dictionary<string, object>();
foreach (var prop in ((JObject)token).Properties())
{
objDict[prop.Name] = ConvertJTokenToPlainObject(prop.Value);
}
return objDict;
case JTokenType.Array:
var list = new List<object>();
foreach (var item in (JArray)token)
{
list.Add(ConvertJTokenToPlainObject(item));
}
return list;
case JTokenType.Integer:
return token.ToObject<long>(); // Use long for safety
case JTokenType.Float:
return token.ToObject<double>(); // Use double for safety
case JTokenType.String:
return token.ToObject<string>();
case JTokenType.Boolean:
return token.ToObject<bool>();
case JTokenType.Date:
return token.ToObject<DateTime>();
case JTokenType.Guid:
return token.ToObject<Guid>();
case JTokenType.Uri:
return token.ToObject<Uri>();
case JTokenType.TimeSpan:
return token.ToObject<TimeSpan>();
case JTokenType.Bytes:
return token.ToObject<byte[]>();
case JTokenType.Null:
return null;
case JTokenType.Undefined:
return null; // Treat undefined as null
default:
// Fallback for simple value types not explicitly listed
if (token is JValue jValue && jValue.Value != null)
{
return jValue.Value;
}
// Debug.LogWarning($"Unsupported JTokenType encountered: {token.Type}. Returning null.");
return null;
}
}
// --- Define custom JsonSerializerSettings for OUTPUT ---
private static readonly JsonSerializerSettings _outputSerializerSettings = new JsonSerializerSettings
{
Converters = new List<JsonConverter>
{
new Vector3Converter(),
new Vector2Converter(),
new QuaternionConverter(),
new ColorConverter(),
new RectConverter(),
new BoundsConverter(),
new UnityEngineObjectConverter() // Handles serialization of references
},
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
// ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() } // Example if needed
};
private static readonly JsonSerializer _outputSerializer = JsonSerializer.Create(_outputSerializerSettings);
// --- End Define custom JsonSerializerSettings ---
// Helper to create JToken using the output serializer
private static JToken CreateTokenFromValue(object value, Type type)
{
if (value == null) return JValue.CreateNull();
try
{
// Use the pre-configured OUTPUT serializer instance
return JToken.FromObject(value, _outputSerializer);
}
catch (JsonSerializationException e)
{
Debug.LogWarning($"[GameObjectSerializer] Newtonsoft.Json Error serializing value of type {type.FullName}: {e.Message}. Skipping property/field.");
return null; // Indicate serialization failure
}
catch (Exception e) // Catch other unexpected errors
{
Debug.LogWarning($"[GameObjectSerializer] Unexpected error serializing value of type {type.FullName}: {e}. Skipping property/field.");
return null; // Indicate serialization failure
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 64b8ff807bc9a401c82015cbafccffac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,33 @@
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
internal static class McpLog
{
private const string Prefix = "<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>:";
private static bool IsDebugEnabled()
{
try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); } catch { return false; }
}
public static void Info(string message, bool always = true)
{
if (!always && !IsDebugEnabled()) return;
Debug.Log($"{Prefix} {message}");
}
public static void Warn(string message)
{
Debug.LogWarning($"<color=#cc7a00>{Prefix} {message}</color>");
}
public static void Error(string message)
{
Debug.LogError($"<color=#cc3333>{Prefix} {message}</color>");
}
}
}

View File

@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 9e2c3f8a4f4f48d8a4c1b7b8e3f5a1c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,109 @@
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Auto-runs legacy/older install detection on package load/update (log-only).
/// Runs once per embedded server version using an EditorPrefs version-scoped key.
/// </summary>
[InitializeOnLoad]
public static class PackageDetector
{
private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:";
static PackageDetector()
{
try
{
string pkgVer = ReadPackageVersionOrFallback();
string key = DetectOnceFlagKeyPrefix + pkgVer;
// Always force-run if legacy roots exist or canonical install is missing
bool legacyPresent = LegacyRootsExist();
bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py"));
if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing)
{
// Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs.
EditorApplication.delayCall += () =>
{
string error = null;
System.Exception capturedEx = null;
try
{
// Ensure any UnityEditor API usage inside runs on the main thread
ServerInstaller.EnsureServerInstalled();
}
catch (System.Exception ex)
{
error = ex.Message;
capturedEx = ex;
}
// Unity APIs must stay on main thread
try { EditorPrefs.SetBool(key, true); } catch { }
// Ensure prefs cleanup happens on main thread
try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); } catch { }
try { EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { }
if (!string.IsNullOrEmpty(error))
{
Debug.LogWarning($"MCP for Unity: Auto-detect on load failed: {capturedEx}");
// Alternatively: Debug.LogException(capturedEx);
}
};
}
}
catch { /* ignore */ }
}
private static string ReadEmbeddedVersionOrFallback()
{
try
{
if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
{
var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt");
if (System.IO.File.Exists(p))
return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown");
}
}
catch { }
return "unknown";
}
private static string ReadPackageVersionOrFallback()
{
try
{
var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly);
if (info != null && !string.IsNullOrEmpty(info.version)) return info.version;
}
catch { }
// Fallback to embedded server version if package info unavailable
return ReadEmbeddedVersionOrFallback();
}
private static bool LegacyRootsExist()
{
try
{
string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] roots =
{
System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
};
foreach (var r in roots)
{
try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { }
}
}
catch { }
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b82eaef548d164ca095f17db64d15af8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,43 @@
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Handles automatic installation of the Python server when the package is first installed.
/// </summary>
[InitializeOnLoad]
public static class PackageInstaller
{
private const string InstallationFlagKey = "MCPForUnity.ServerInstalled";
static PackageInstaller()
{
// Check if this is the first time the package is loaded
if (!EditorPrefs.GetBool(InstallationFlagKey, false))
{
// Schedule the installation for after Unity is fully loaded
EditorApplication.delayCall += InstallServerOnFirstLoad;
}
}
private static void InstallServerOnFirstLoad()
{
try
{
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Installing Python server...");
ServerInstaller.EnsureServerInstalled();
// Mark as installed
EditorPrefs.SetBool(InstallationFlagKey, true);
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Python server installation completed successfully.");
}
catch (System.Exception ex)
{
Debug.LogError($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Failed to install Python server: {ex.Message}");
Debug.LogWarning("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: You may need to manually install the Python server. Check the MCP for Unity Editor Window for instructions.");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19e6eaa637484e9fa19f9a0459809de2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,319 @@
using System;
using System.IO;
using UnityEditor;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Manages dynamic port allocation and persistent storage for MCP for Unity
/// </summary>
public static class PortManager
{
private static bool IsDebugEnabled()
{
try { return EditorPrefs.GetBool("MCPForUnity.DebugLogs", false); }
catch { return false; }
}
private const int DefaultPort = 6400;
private const int MaxPortAttempts = 100;
private const string RegistryFileName = "unity-mcp-port.json";
[Serializable]
public class PortConfig
{
public int unity_port;
public string created_date;
public string project_path;
}
/// <summary>
/// Get the port to use - either from storage or discover a new one
/// Will try stored port first, then fallback to discovering new port
/// </summary>
/// <returns>Port number to use</returns>
public static int GetPortWithFallback()
{
// Try to load stored port first, but only if it's from the current project
var storedConfig = GetStoredPortConfig();
if (storedConfig != null &&
storedConfig.unity_port > 0 &&
string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase) &&
IsPortAvailable(storedConfig.unity_port))
{
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using stored port {storedConfig.unity_port} for current project");
return storedConfig.unity_port;
}
// If stored port exists but is currently busy, wait briefly for release
if (storedConfig != null && storedConfig.unity_port > 0)
{
if (WaitForPortRelease(storedConfig.unity_port, 1500))
{
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} became available after short wait");
return storedConfig.unity_port;
}
// Prefer sticking to the same port; let the caller handle bind retries/fallbacks
return storedConfig.unity_port;
}
// If no valid stored port, find a new one and save it
int newPort = FindAvailablePort();
SavePort(newPort);
return newPort;
}
/// <summary>
/// Discover and save a new available port (used by Auto-Connect button)
/// </summary>
/// <returns>New available port</returns>
public static int DiscoverNewPort()
{
int newPort = FindAvailablePort();
SavePort(newPort);
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Discovered and saved new port: {newPort}");
return newPort;
}
/// <summary>
/// Find an available port starting from the default port
/// </summary>
/// <returns>Available port number</returns>
private static int FindAvailablePort()
{
// Always try default port first
if (IsPortAvailable(DefaultPort))
{
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using default port {DefaultPort}");
return DefaultPort;
}
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Default port {DefaultPort} is in use, searching for alternative...");
// Search for alternatives
for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++)
{
if (IsPortAvailable(port))
{
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Found available port {port}");
return port;
}
}
throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}");
}
/// <summary>
/// Check if a specific port is available for binding
/// </summary>
/// <param name="port">Port to check</param>
/// <returns>True if port is available</returns>
public static bool IsPortAvailable(int port)
{
try
{
var testListener = new TcpListener(IPAddress.Loopback, port);
testListener.Start();
testListener.Stop();
return true;
}
catch (SocketException)
{
return false;
}
}
/// <summary>
/// Check if a port is currently being used by MCP for Unity
/// This helps avoid unnecessary port changes when Unity itself is using the port
/// </summary>
/// <param name="port">Port to check</param>
/// <returns>True if port appears to be used by MCP for Unity</returns>
public static bool IsPortUsedByMCPForUnity(int port)
{
try
{
// Try to make a quick connection to see if it's an MCP for Unity server
using var client = new TcpClient();
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
if (connectTask.Wait(100)) // 100ms timeout
{
// If connection succeeded, it's likely the MCP for Unity server
return client.Connected;
}
return false;
}
catch
{
return false;
}
}
/// <summary>
/// Wait for a port to become available for a limited amount of time.
/// Used to bridge the gap during domain reload when the old listener
/// hasn't released the socket yet.
/// </summary>
private static bool WaitForPortRelease(int port, int timeoutMs)
{
int waited = 0;
const int step = 100;
while (waited < timeoutMs)
{
if (IsPortAvailable(port))
{
return true;
}
// If the port is in use by an MCP instance, continue waiting briefly
if (!IsPortUsedByMCPForUnity(port))
{
// In use by something else; don't keep waiting
return false;
}
Thread.Sleep(step);
waited += step;
}
return IsPortAvailable(port);
}
/// <summary>
/// Save port to persistent storage
/// </summary>
/// <param name="port">Port to save</param>
private static void SavePort(int port)
{
try
{
var portConfig = new PortConfig
{
unity_port = port,
created_date = DateTime.UtcNow.ToString("O"),
project_path = Application.dataPath
};
string registryDir = GetRegistryDirectory();
Directory.CreateDirectory(registryDir);
string registryFile = GetRegistryFilePath();
string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
// Write to hashed, project-scoped file
File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false));
// Also write to legacy stable filename to avoid hash/case drift across reloads
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
File.WriteAllText(legacy, json, new System.Text.UTF8Encoding(false));
if (IsDebugEnabled()) Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Saved port {port} to storage");
}
catch (Exception ex)
{
Debug.LogWarning($"Could not save port to storage: {ex.Message}");
}
}
/// <summary>
/// Load port from persistent storage
/// </summary>
/// <returns>Stored port number, or 0 if not found</returns>
private static int LoadStoredPort()
{
try
{
string registryFile = GetRegistryFilePath();
if (!File.Exists(registryFile))
{
// Backwards compatibility: try the legacy file name
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
if (!File.Exists(legacy))
{
return 0;
}
registryFile = legacy;
}
string json = File.ReadAllText(registryFile);
var portConfig = JsonConvert.DeserializeObject<PortConfig>(json);
return portConfig?.unity_port ?? 0;
}
catch (Exception ex)
{
Debug.LogWarning($"Could not load port from storage: {ex.Message}");
return 0;
}
}
/// <summary>
/// Get the current stored port configuration
/// </summary>
/// <returns>Port configuration if exists, null otherwise</returns>
public static PortConfig GetStoredPortConfig()
{
try
{
string registryFile = GetRegistryFilePath();
if (!File.Exists(registryFile))
{
// Backwards compatibility: try the legacy file
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
if (!File.Exists(legacy))
{
return null;
}
registryFile = legacy;
}
string json = File.ReadAllText(registryFile);
return JsonConvert.DeserializeObject<PortConfig>(json);
}
catch (Exception ex)
{
Debug.LogWarning($"Could not load port config: {ex.Message}");
return null;
}
}
private static string GetRegistryDirectory()
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
}
private static string GetRegistryFilePath()
{
string dir = GetRegistryDirectory();
string hash = ComputeProjectHash(Application.dataPath);
string fileName = $"unity-mcp-port-{hash}.json";
return Path.Combine(dir, fileName);
}
private static string ComputeProjectHash(string input)
{
try
{
using SHA1 sha1 = SHA1.Create();
byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
byte[] hashBytes = sha1.ComputeHash(bytes);
var sb = new StringBuilder();
foreach (byte b in hashBytes)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString()[..8]; // short, sufficient for filenames
}
catch
{
return "default";
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a1b2c3d4e5f6789012345678901234ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Provides static methods for creating standardized success and error response objects.
/// Ensures consistent JSON structure for communication back to the Python server.
/// </summary>
public static class Response
{
/// <summary>
/// Creates a standardized success response object.
/// </summary>
/// <param name="message">A message describing the successful operation.</param>
/// <param name="data">Optional additional data to include in the response.</param>
/// <returns>An object representing the success response.</returns>
public static object Success(string message, object data = null)
{
if (data != null)
{
return new
{
success = true,
message = message,
data = data,
};
}
else
{
return new { success = true, message = message };
}
}
/// <summary>
/// Creates a standardized error response object.
/// </summary>
/// <param name="errorCodeOrMessage">A message describing the error.</param>
/// <param name="data">Optional additional data (e.g., error details) to include.</param>
/// <returns>An object representing the error response.</returns>
public static object Error(string errorCodeOrMessage, object data = null)
{
if (data != null)
{
// Note: The key is "error" for error messages, not "message"
return new
{
success = false,
// Preserve original behavior while adding a machine-parsable code field.
// If callers pass a code string, it will be echoed in both code and error.
code = errorCodeOrMessage,
error = errorCodeOrMessage,
data = data,
};
}
else
{
return new { success = false, code = errorCodeOrMessage, error = errorCodeOrMessage };
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 80c09a76b944f8c4691e06c4d76c4be8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,744 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
public static class ServerInstaller
{
private const string RootFolder = "UnityMCP";
private const string ServerFolder = "UnityMcpServer";
private const string VersionFileName = "server_version.txt";
/// <summary>
/// Ensures the mcp-for-unity-server is installed locally by copying from the embedded package source.
/// No network calls or Git operations are performed.
/// </summary>
public static void EnsureServerInstalled()
{
try
{
string saveLocation = GetSaveLocation();
TryCreateMacSymlinkForAppSupport();
string destRoot = Path.Combine(saveLocation, ServerFolder);
string destSrc = Path.Combine(destRoot, "src");
// Detect legacy installs and version state (logs)
DetectAndLogLegacyInstallStates(destRoot);
// Resolve embedded source and versions
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
{
throw new Exception("Could not find embedded UnityMcpServer/src in the package.");
}
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName));
bool destHasServer = File.Exists(Path.Combine(destSrc, "server.py"));
bool needOverwrite = !destHasServer
|| string.IsNullOrEmpty(installedVer)
|| (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0);
// Ensure destination exists
Directory.CreateDirectory(destRoot);
if (needOverwrite)
{
// Copy the entire UnityMcpServer folder (parent of src)
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
CopyDirectoryRecursive(embeddedRoot, destRoot);
// Write/refresh version file
try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { }
McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer}).");
}
// Cleanup legacy installs that are missing version or older than embedded
foreach (var legacyRoot in GetLegacyRootsForDetection())
{
try
{
string legacySrc = Path.Combine(legacyRoot, "src");
if (!File.Exists(Path.Combine(legacySrc, "server.py"))) continue;
string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
bool legacyOlder = string.IsNullOrEmpty(legacyVer)
|| (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0);
if (legacyOlder)
{
TryKillUvForPath(legacySrc);
try
{
Directory.Delete(legacyRoot, recursive: true);
McpLog.Info($"Removed legacy server at '{legacyRoot}'.");
}
catch (Exception ex)
{
McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}': {ex.Message}");
}
}
}
catch { }
}
// Clear overrides that might point at legacy locations
try
{
EditorPrefs.DeleteKey("MCPForUnity.ServerSrc");
EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride");
}
catch { }
return;
}
catch (Exception ex)
{
// If a usable server is already present (installed or embedded), don't fail hard—just warn.
bool hasInstalled = false;
try { hasInstalled = File.Exists(Path.Combine(GetServerPath(), "server.py")); } catch { }
if (hasInstalled || TryGetEmbeddedServerSource(out _))
{
McpLog.Warn($"Using existing server; skipped install. Details: {ex.Message}");
return;
}
McpLog.Error($"Failed to ensure server installation: {ex.Message}");
}
}
public static string GetServerPath()
{
return Path.Combine(GetSaveLocation(), ServerFolder, "src");
}
/// <summary>
/// Gets the platform-specific save location for the server.
/// </summary>
private static string GetSaveLocation()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Use per-user LocalApplicationData for canonical install location
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty, "AppData", "Local");
return Path.Combine(localAppData, RootFolder);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var xdg = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
if (string.IsNullOrEmpty(xdg))
{
xdg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty,
".local", "share");
}
return Path.Combine(xdg, RootFolder);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
// On macOS, use LocalApplicationData (~/Library/Application Support)
var localAppSupport = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
// Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support
bool looksLikeXdg = !string.IsNullOrEmpty(localAppSupport) && localAppSupport.Replace('\\', '/').Contains("/.local/share");
if (string.IsNullOrEmpty(localAppSupport) || looksLikeXdg)
{
// Fallback: construct from $HOME
var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
localAppSupport = Path.Combine(home, "Library", "Application Support");
}
TryCreateMacSymlinkForAppSupport();
return Path.Combine(localAppSupport, RootFolder);
}
throw new Exception("Unsupported operating system.");
}
/// <summary>
/// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support
/// to mitigate arg parsing and quoting issues in some MCP clients.
/// Safe to call repeatedly.
/// </summary>
private static void TryCreateMacSymlinkForAppSupport()
{
try
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return;
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
if (string.IsNullOrEmpty(home)) return;
string canonical = Path.Combine(home, "Library", "Application Support");
string symlink = Path.Combine(home, "Library", "AppSupport");
// If symlink exists already, nothing to do
if (Directory.Exists(symlink) || File.Exists(symlink)) return;
// Create symlink only if canonical exists
if (!Directory.Exists(canonical)) return;
// Use 'ln -s' to create a directory symlink (macOS)
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "/bin/ln",
Arguments = $"-s \"{canonical}\" \"{symlink}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
p?.WaitForExit(2000);
}
catch { /* best-effort */ }
}
private static bool IsDirectoryWritable(string path)
{
try
{
File.Create(Path.Combine(path, "test.txt")).Dispose();
File.Delete(Path.Combine(path, "test.txt"));
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Checks if the server is installed at the specified location.
/// </summary>
private static bool IsServerInstalled(string location)
{
return Directory.Exists(location)
&& File.Exists(Path.Combine(location, ServerFolder, "src", "server.py"));
}
/// <summary>
/// Detects legacy installs or older versions and logs findings (no deletion yet).
/// </summary>
private static void DetectAndLogLegacyInstallStates(string canonicalRoot)
{
try
{
string canonicalSrc = Path.Combine(canonicalRoot, "src");
// Normalize canonical root for comparisons
string normCanonicalRoot = NormalizePathSafe(canonicalRoot);
string embeddedSrc = null;
TryGetEmbeddedServerSource(out embeddedSrc);
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc ?? string.Empty, VersionFileName));
string installedVer = ReadVersionFile(Path.Combine(canonicalSrc, VersionFileName));
// Legacy paths (macOS/Linux .config; Windows roaming as example)
foreach (var legacyRoot in GetLegacyRootsForDetection())
{
// Skip logging for the canonical root itself
if (PathsEqualSafe(legacyRoot, normCanonicalRoot))
continue;
string legacySrc = Path.Combine(legacyRoot, "src");
bool hasServer = File.Exists(Path.Combine(legacySrc, "server.py"));
string legacyVer = ReadVersionFile(Path.Combine(legacySrc, VersionFileName));
if (hasServer)
{
// Case 1: No version file
if (string.IsNullOrEmpty(legacyVer))
{
McpLog.Info("Detected legacy install without version file at: " + legacyRoot, always: false);
}
// Case 2: Lives in legacy path
McpLog.Info("Detected legacy install path: " + legacyRoot, always: false);
// Case 3: Has version but appears older than embedded
if (!string.IsNullOrEmpty(embeddedVer) && !string.IsNullOrEmpty(legacyVer) && CompareSemverSafe(legacyVer, embeddedVer) < 0)
{
McpLog.Info($"Legacy install version {legacyVer} is older than embedded {embeddedVer}", always: false);
}
}
}
// Also log if canonical is missing version (treated as older)
if (Directory.Exists(canonicalRoot))
{
if (string.IsNullOrEmpty(installedVer))
{
McpLog.Info("Canonical install missing version file (treat as older). Path: " + canonicalRoot, always: false);
}
else if (!string.IsNullOrEmpty(embeddedVer) && CompareSemverSafe(installedVer, embeddedVer) < 0)
{
McpLog.Info($"Canonical install version {installedVer} is older than embedded {embeddedVer}", always: false);
}
}
}
catch (Exception ex)
{
McpLog.Warn("Detect legacy/version state failed: " + ex.Message);
}
}
private static string NormalizePathSafe(string path)
{
try { return string.IsNullOrEmpty(path) ? path : Path.GetFullPath(path.Trim()); }
catch { return path; }
}
private static bool PathsEqualSafe(string a, string b)
{
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
string na = NormalizePathSafe(a);
string nb = NormalizePathSafe(b);
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
}
return string.Equals(na, nb, StringComparison.Ordinal);
}
catch { return false; }
}
private static IEnumerable<string> GetLegacyRootsForDetection()
{
var roots = new System.Collections.Generic.List<string>();
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
// macOS/Linux legacy
roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer"));
roots.Add(Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer"));
// Windows roaming example
try
{
string roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
if (!string.IsNullOrEmpty(roaming))
roots.Add(Path.Combine(roaming, "UnityMCP", "UnityMcpServer"));
// Windows legacy: early installers/dev scripts used %LOCALAPPDATA%\Programs\UnityMCP\UnityMcpServer
// Detect this location so we can clean up older copies during install/update.
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
if (!string.IsNullOrEmpty(localAppData))
roots.Add(Path.Combine(localAppData, "Programs", "UnityMCP", "UnityMcpServer"));
}
catch { }
return roots;
}
private static void TryKillUvForPath(string serverSrcPath)
{
try
{
if (string.IsNullOrEmpty(serverSrcPath)) return;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "/usr/bin/pgrep",
Arguments = $"-f \"uv .*--directory {serverSrcPath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
if (p == null) return;
string outp = p.StandardOutput.ReadToEnd();
p.WaitForExit(1500);
if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
{
foreach (var line in outp.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
{
if (int.TryParse(line.Trim(), out int pid))
{
try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { }
}
}
}
}
catch { }
}
private static string ReadVersionFile(string path)
{
try
{
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
string v = File.ReadAllText(path).Trim();
return string.IsNullOrEmpty(v) ? null : v;
}
catch { return null; }
}
private static int CompareSemverSafe(string a, string b)
{
try
{
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return 0;
var ap = a.Split('.');
var bp = b.Split('.');
for (int i = 0; i < Math.Max(ap.Length, bp.Length); i++)
{
int ai = (i < ap.Length && int.TryParse(ap[i], out var t1)) ? t1 : 0;
int bi = (i < bp.Length && int.TryParse(bp[i], out var t2)) ? t2 : 0;
if (ai != bi) return ai.CompareTo(bi);
}
return 0;
}
catch { return 0; }
}
/// <summary>
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
/// or common development locations.
/// </summary>
private static bool TryGetEmbeddedServerSource(out string srcPath)
{
return ServerPathResolver.TryFindEmbeddedServerSource(out srcPath);
}
private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" };
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
{
Directory.CreateDirectory(destinationDir);
foreach (string filePath in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(filePath);
string destFile = Path.Combine(destinationDir, fileName);
File.Copy(filePath, destFile, overwrite: true);
}
foreach (string dirPath in Directory.GetDirectories(sourceDir))
{
string dirName = Path.GetFileName(dirPath);
foreach (var skip in _skipDirs)
{
if (dirName.Equals(skip, StringComparison.OrdinalIgnoreCase))
goto NextDir;
}
try { if ((File.GetAttributes(dirPath) & FileAttributes.ReparsePoint) != 0) continue; } catch { }
string destSubDir = Path.Combine(destinationDir, dirName);
CopyDirectoryRecursive(dirPath, destSubDir);
NextDir: ;
}
}
public static bool RepairPythonEnvironment()
{
try
{
string serverSrc = GetServerPath();
bool hasServer = File.Exists(Path.Combine(serverSrc, "server.py"));
if (!hasServer)
{
// In dev mode or if not installed yet, try the embedded/dev source
if (TryGetEmbeddedServerSource(out string embeddedSrc) && File.Exists(Path.Combine(embeddedSrc, "server.py")))
{
serverSrc = embeddedSrc;
hasServer = true;
}
else
{
// Attempt to install then retry
EnsureServerInstalled();
serverSrc = GetServerPath();
hasServer = File.Exists(Path.Combine(serverSrc, "server.py"));
}
}
if (!hasServer)
{
Debug.LogWarning("RepairPythonEnvironment: server.py not found; ensure server is installed first.");
return false;
}
// Remove stale venv and pinned version file if present
string venvPath = Path.Combine(serverSrc, ".venv");
if (Directory.Exists(venvPath))
{
try { Directory.Delete(venvPath, recursive: true); } catch (Exception ex) { Debug.LogWarning($"Failed to delete .venv: {ex.Message}"); }
}
string pyPin = Path.Combine(serverSrc, ".python-version");
if (File.Exists(pyPin))
{
try { File.Delete(pyPin); } catch (Exception ex) { Debug.LogWarning($"Failed to delete .python-version: {ex.Message}"); }
}
string uvPath = FindUvPath();
if (uvPath == null)
{
Debug.LogError("UV not found. Please install uv (https://docs.astral.sh/uv/)." );
return false;
}
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = uvPath,
Arguments = "sync",
WorkingDirectory = serverSrc,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var proc = new System.Diagnostics.Process { StartInfo = psi };
var sbOut = new StringBuilder();
var sbErr = new StringBuilder();
proc.OutputDataReceived += (_, e) => { if (e.Data != null) sbOut.AppendLine(e.Data); };
proc.ErrorDataReceived += (_, e) => { if (e.Data != null) sbErr.AppendLine(e.Data); };
if (!proc.Start())
{
Debug.LogError("Failed to start uv process.");
return false;
}
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
if (!proc.WaitForExit(60000))
{
try { proc.Kill(); } catch { }
Debug.LogError("uv sync timed out.");
return false;
}
// Ensure async buffers flushed
proc.WaitForExit();
string stdout = sbOut.ToString();
string stderr = sbErr.ToString();
if (proc.ExitCode != 0)
{
Debug.LogError($"uv sync failed: {stderr}\n{stdout}");
return false;
}
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Python environment repaired successfully.");
return true;
}
catch (Exception ex)
{
Debug.LogError($"RepairPythonEnvironment failed: {ex.Message}");
return false;
}
}
internal static string FindUvPath()
{
// Allow user override via EditorPrefs
try
{
string overridePath = EditorPrefs.GetString("MCPForUnity.UvPath", string.Empty);
if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
{
if (ValidateUvBinary(overridePath)) return overridePath;
}
}
catch { }
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
// Platform-specific candidate lists
string[] candidates;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty;
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
// Fast path: resolve from PATH first
try
{
var wherePsi = new System.Diagnostics.ProcessStartInfo
{
FileName = "where",
Arguments = "uv.exe",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var wp = System.Diagnostics.Process.Start(wherePsi);
string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(1500);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
{
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
string path = line.Trim();
if (File.Exists(path) && ValidateUvBinary(path)) return path;
}
}
}
catch { }
// Windows Store (PythonSoftwareFoundation) install location probe
// Example: %LOCALAPPDATA%\Packages\PythonSoftwareFoundation.Python.3.13_*\LocalCache\local-packages\Python313\Scripts\uv.exe
try
{
string pkgsRoot = Path.Combine(localAppData, "Packages");
if (Directory.Exists(pkgsRoot))
{
var pythonPkgs = Directory.GetDirectories(pkgsRoot, "PythonSoftwareFoundation.Python.*", SearchOption.TopDirectoryOnly)
.OrderByDescending(p => p, StringComparer.OrdinalIgnoreCase);
foreach (var pkg in pythonPkgs)
{
string localCache = Path.Combine(pkg, "LocalCache", "local-packages");
if (!Directory.Exists(localCache)) continue;
var pyRoots = Directory.GetDirectories(localCache, "Python*", SearchOption.TopDirectoryOnly)
.OrderByDescending(d => d, StringComparer.OrdinalIgnoreCase);
foreach (var pyRoot in pyRoots)
{
string uvExe = Path.Combine(pyRoot, "Scripts", "uv.exe");
if (File.Exists(uvExe) && ValidateUvBinary(uvExe)) return uvExe;
}
}
}
}
catch { }
candidates = new[]
{
// Preferred: WinGet Links shims (stable entrypoints)
// Per-user shim (LOCALAPPDATA) → machine-wide shim (Program Files\WinGet\Links)
Path.Combine(localAppData, "Microsoft", "WinGet", "Links", "uv.exe"),
Path.Combine(programFiles, "WinGet", "Links", "uv.exe"),
// Common per-user installs
Path.Combine(localAppData, @"Programs\Python\Python313\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python312\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python311\Scripts\uv.exe"),
Path.Combine(localAppData, @"Programs\Python\Python310\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python313\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python312\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python311\Scripts\uv.exe"),
Path.Combine(appData, @"Python\Python310\Scripts\uv.exe"),
// Program Files style installs (if a native installer was used)
Path.Combine(programFiles, @"uv\uv.exe"),
// Try simple name resolution later via PATH
"uv.exe",
"uv"
};
}
else
{
candidates = new[]
{
"/opt/homebrew/bin/uv",
"/usr/local/bin/uv",
"/usr/bin/uv",
"/opt/local/bin/uv",
Path.Combine(home, ".local", "bin", "uv"),
"/opt/homebrew/opt/uv/bin/uv",
// Framework Python installs
"/Library/Frameworks/Python.framework/Versions/3.13/bin/uv",
"/Library/Frameworks/Python.framework/Versions/3.12/bin/uv",
// Fallback to PATH resolution by name
"uv"
};
}
foreach (string c in candidates)
{
try
{
if (File.Exists(c) && ValidateUvBinary(c)) return c;
}
catch { /* ignore */ }
}
// Use platform-appropriate which/where to resolve from PATH (non-Windows handled here; Windows tried earlier)
try
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var whichPsi = new System.Diagnostics.ProcessStartInfo
{
FileName = "/usr/bin/which",
Arguments = "uv",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
try
{
// Prepend common user-local and package manager locations so 'which' can see them in Unity's GUI env
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string prepend = string.Join(":", new[]
{
System.IO.Path.Combine(homeDir, ".local", "bin"),
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin"
});
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath);
}
catch { }
using var wp = System.Diagnostics.Process.Start(whichPsi);
string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(3000);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
{
if (ValidateUvBinary(output)) return output;
}
}
}
catch { }
// Manual PATH scan
try
{
string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
string[] parts = pathEnv.Split(Path.PathSeparator);
foreach (string part in parts)
{
try
{
// Check both uv and uv.exe
string candidateUv = Path.Combine(part, "uv");
string candidateUvExe = Path.Combine(part, "uv.exe");
if (File.Exists(candidateUv) && ValidateUvBinary(candidateUv)) return candidateUv;
if (File.Exists(candidateUvExe) && ValidateUvBinary(candidateUvExe)) return candidateUvExe;
}
catch { }
}
}
catch { }
return null;
}
private static bool ValidateUvBinary(string uvPath)
{
try
{
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = uvPath,
Arguments = "--version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var p = System.Diagnostics.Process.Start(psi);
if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; }
if (p.ExitCode == 0)
{
string output = p.StandardOutput.ReadToEnd().Trim();
return output.StartsWith("uv ");
}
}
catch { }
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5862c6a6d0a914f4d83224f8d039cf7b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,151 @@
using System;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
public static class ServerPathResolver
{
/// <summary>
/// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
/// or common development locations. Returns true if found and sets srcPath to the folder
/// containing server.py.
/// </summary>
public static bool TryFindEmbeddedServerSource(out string srcPath, bool warnOnLegacyPackageId = true)
{
// 1) Repo development layouts commonly used alongside this package
try
{
string projectRoot = Path.GetDirectoryName(Application.dataPath);
string[] devCandidates =
{
Path.Combine(projectRoot ?? string.Empty, "unity-mcp", "UnityMcpServer", "src"),
Path.Combine(projectRoot ?? string.Empty, "..", "unity-mcp", "UnityMcpServer", "src"),
};
foreach (string candidate in devCandidates)
{
string full = Path.GetFullPath(candidate);
if (Directory.Exists(full) && File.Exists(Path.Combine(full, "server.py")))
{
srcPath = full;
return true;
}
}
}
catch { /* ignore */ }
// 2) Resolve via local package info (no network). Fall back to Client.List on older editors.
try
{
#if UNITY_2021_2_OR_NEWER
// Primary: the package that owns this assembly
var owner = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(ServerPathResolver).Assembly);
if (owner != null)
{
if (TryResolveWithinPackage(owner, out srcPath, warnOnLegacyPackageId))
{
return true;
}
}
// Secondary: scan all registered packages locally
foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
{
if (TryResolveWithinPackage(p, out srcPath, warnOnLegacyPackageId))
{
return true;
}
}
#else
// Older Unity versions: use Package Manager Client.List as a fallback
var list = UnityEditor.PackageManager.Client.List();
while (!list.IsCompleted) { }
if (list.Status == UnityEditor.PackageManager.StatusCode.Success)
{
foreach (var pkg in list.Result)
{
if (TryResolveWithinPackage(pkg, out srcPath, warnOnLegacyPackageId))
{
return true;
}
}
}
#endif
}
catch { /* ignore */ }
// 3) Fallback to previous common install locations
try
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] candidates =
{
Path.Combine(home, "unity-mcp", "UnityMcpServer", "src"),
Path.Combine(home, "Applications", "UnityMCP", "UnityMcpServer", "src"),
};
foreach (string candidate in candidates)
{
if (Directory.Exists(candidate) && File.Exists(Path.Combine(candidate, "server.py")))
{
srcPath = candidate;
return true;
}
}
}
catch { /* ignore */ }
srcPath = null;
return false;
}
private static bool TryResolveWithinPackage(UnityEditor.PackageManager.PackageInfo p, out string srcPath, bool warnOnLegacyPackageId)
{
const string CurrentId = "com.coplaydev.unity-mcp";
const string LegacyId = "com.justinpbarnett.unity-mcp";
srcPath = null;
if (p == null || (p.name != CurrentId && p.name != LegacyId))
{
return false;
}
if (warnOnLegacyPackageId && p.name == LegacyId)
{
Debug.LogWarning(
"MCP for Unity: Detected legacy package id 'com.justinpbarnett.unity-mcp'. " +
"Please update Packages/manifest.json to 'com.coplaydev.unity-mcp' to avoid future breakage.");
}
string packagePath = p.resolvedPath;
// Preferred tilde folder (embedded but excluded from import)
string embeddedTilde = Path.Combine(packagePath, "UnityMcpServer~", "src");
if (Directory.Exists(embeddedTilde) && File.Exists(Path.Combine(embeddedTilde, "server.py")))
{
srcPath = embeddedTilde;
return true;
}
// Legacy non-tilde folder
string embedded = Path.Combine(packagePath, "UnityMcpServer", "src");
if (Directory.Exists(embedded) && File.Exists(Path.Combine(embedded, "server.py")))
{
srcPath = embedded;
return true;
}
// Dev-linked sibling of the package folder
string sibling = Path.Combine(Path.GetDirectoryName(packagePath) ?? string.Empty, "UnityMcpServer", "src");
if (Directory.Exists(sibling) && File.Exists(Path.Combine(sibling, "server.py")))
{
srcPath = sibling;
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Unity Bridge telemetry helper for collecting usage analytics
/// Following privacy-first approach with easy opt-out mechanisms
/// </summary>
public static class TelemetryHelper
{
private const string TELEMETRY_DISABLED_KEY = "MCPForUnity.TelemetryDisabled";
private const string CUSTOMER_UUID_KEY = "MCPForUnity.CustomerUUID";
private static Action<Dictionary<string, object>> s_sender;
/// <summary>
/// Check if telemetry is enabled (can be disabled via Environment Variable or EditorPrefs)
/// </summary>
public static bool IsEnabled
{
get
{
// Check environment variables first
var envDisable = Environment.GetEnvironmentVariable("DISABLE_TELEMETRY");
if (!string.IsNullOrEmpty(envDisable) &&
(envDisable.ToLower() == "true" || envDisable == "1"))
{
return false;
}
var unityMcpDisable = Environment.GetEnvironmentVariable("UNITY_MCP_DISABLE_TELEMETRY");
if (!string.IsNullOrEmpty(unityMcpDisable) &&
(unityMcpDisable.ToLower() == "true" || unityMcpDisable == "1"))
{
return false;
}
// Honor protocol-wide opt-out as well
var mcpDisable = Environment.GetEnvironmentVariable("MCP_DISABLE_TELEMETRY");
if (!string.IsNullOrEmpty(mcpDisable) &&
(mcpDisable.Equals("true", StringComparison.OrdinalIgnoreCase) || mcpDisable == "1"))
{
return false;
}
// Check EditorPrefs
return !UnityEditor.EditorPrefs.GetBool(TELEMETRY_DISABLED_KEY, false);
}
}
/// <summary>
/// Get or generate customer UUID for anonymous tracking
/// </summary>
public static string GetCustomerUUID()
{
var uuid = UnityEditor.EditorPrefs.GetString(CUSTOMER_UUID_KEY, "");
if (string.IsNullOrEmpty(uuid))
{
uuid = System.Guid.NewGuid().ToString();
UnityEditor.EditorPrefs.SetString(CUSTOMER_UUID_KEY, uuid);
}
return uuid;
}
/// <summary>
/// Disable telemetry (stored in EditorPrefs)
/// </summary>
public static void DisableTelemetry()
{
UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, true);
}
/// <summary>
/// Enable telemetry (stored in EditorPrefs)
/// </summary>
public static void EnableTelemetry()
{
UnityEditor.EditorPrefs.SetBool(TELEMETRY_DISABLED_KEY, false);
}
/// <summary>
/// Send telemetry data to Python server for processing
/// This is a lightweight bridge - the actual telemetry logic is in Python
/// </summary>
public static void RecordEvent(string eventType, Dictionary<string, object> data = null)
{
if (!IsEnabled)
return;
try
{
var telemetryData = new Dictionary<string, object>
{
["event_type"] = eventType,
["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
["customer_uuid"] = GetCustomerUUID(),
["unity_version"] = Application.unityVersion,
["platform"] = Application.platform.ToString(),
["source"] = "unity_bridge"
};
if (data != null)
{
telemetryData["data"] = data;
}
// Send to Python server via existing bridge communication
// The Python server will handle actual telemetry transmission
SendTelemetryToPythonServer(telemetryData);
}
catch (Exception e)
{
// Never let telemetry errors interfere with functionality
if (IsDebugEnabled())
{
Debug.LogWarning($"Telemetry error (non-blocking): {e.Message}");
}
}
}
/// <summary>
/// Allows the bridge to register a concrete sender for telemetry payloads.
/// </summary>
public static void RegisterTelemetrySender(Action<Dictionary<string, object>> sender)
{
Interlocked.Exchange(ref s_sender, sender);
}
public static void UnregisterTelemetrySender()
{
Interlocked.Exchange(ref s_sender, null);
}
/// <summary>
/// Record bridge startup event
/// </summary>
public static void RecordBridgeStartup()
{
RecordEvent("bridge_startup", new Dictionary<string, object>
{
["bridge_version"] = "3.0.2",
["auto_connect"] = MCPForUnityBridge.IsAutoConnectMode()
});
}
/// <summary>
/// Record bridge connection event
/// </summary>
public static void RecordBridgeConnection(bool success, string error = null)
{
var data = new Dictionary<string, object>
{
["success"] = success
};
if (!string.IsNullOrEmpty(error))
{
data["error"] = error.Substring(0, Math.Min(200, error.Length));
}
RecordEvent("bridge_connection", data);
}
/// <summary>
/// Record tool execution from Unity side
/// </summary>
public static void RecordToolExecution(string toolName, bool success, float durationMs, string error = null)
{
var data = new Dictionary<string, object>
{
["tool_name"] = toolName,
["success"] = success,
["duration_ms"] = Math.Round(durationMs, 2)
};
if (!string.IsNullOrEmpty(error))
{
data["error"] = error.Substring(0, Math.Min(200, error.Length));
}
RecordEvent("tool_execution_unity", data);
}
private static void SendTelemetryToPythonServer(Dictionary<string, object> telemetryData)
{
var sender = Volatile.Read(ref s_sender);
if (sender != null)
{
try
{
sender(telemetryData);
return;
}
catch (Exception e)
{
if (IsDebugEnabled())
{
Debug.LogWarning($"Telemetry sender error (non-blocking): {e.Message}");
}
}
}
// Fallback: log when debug is enabled
if (IsDebugEnabled())
{
Debug.Log($"<b><color=#2EA3FF>MCP-TELEMETRY</color></b>: {telemetryData["event_type"]}");
}
}
private static bool IsDebugEnabled()
{
try
{
return UnityEditor.EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
}
catch
{
return false;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b8f3c2d1e7a94f6c8a9b5e3d2c1a0f9e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,25 @@
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Helper class for Vector3 operations
/// </summary>
public static class Vector3Helper
{
/// <summary>
/// Parses a JArray into a Vector3
/// </summary>
/// <param name="array">The array containing x, y, z coordinates</param>
/// <returns>A Vector3 with the parsed coordinates</returns>
/// <exception cref="System.Exception">Thrown when array is invalid</exception>
public static Vector3 ParseVector3(JArray array)
{
if (array == null || array.Count != 3)
throw new System.Exception("Vector3 must be an array of 3 floats [x, y, z].");
return new Vector3((float)array[0], (float)array[1], (float)array[2]);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f8514fd42f23cb641a36e52550825b35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e5f6789012345678901234abcdef0123
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using UnityEngine;
namespace MCPForUnity.Editor.Installation
{
/// <summary>
/// Orchestrates the installation of missing dependencies
/// </summary>
public class InstallationOrchestrator
{
public event Action<string> OnProgressUpdate;
public event Action<bool, string> OnInstallationComplete;
private bool _isInstalling = false;
/// <summary>
/// Start installation of missing dependencies
/// </summary>
public async void StartInstallation(List<DependencyStatus> missingDependencies)
{
if (_isInstalling)
{
McpLog.Warn("Installation already in progress");
return;
}
_isInstalling = true;
try
{
OnProgressUpdate?.Invoke("Starting installation process...");
bool allSuccessful = true;
string finalMessage = "";
foreach (var dependency in missingDependencies)
{
OnProgressUpdate?.Invoke($"Installing {dependency.Name}...");
bool success = await InstallDependency(dependency);
if (!success)
{
allSuccessful = false;
finalMessage += $"Failed to install {dependency.Name}. ";
}
else
{
finalMessage += $"Successfully installed {dependency.Name}. ";
}
}
if (allSuccessful)
{
OnProgressUpdate?.Invoke("Installation completed successfully!");
OnInstallationComplete?.Invoke(true, "All dependencies installed successfully.");
}
else
{
OnProgressUpdate?.Invoke("Installation completed with errors.");
OnInstallationComplete?.Invoke(false, finalMessage);
}
}
catch (Exception ex)
{
McpLog.Error($"Installation failed: {ex.Message}");
OnInstallationComplete?.Invoke(false, $"Installation failed: {ex.Message}");
}
finally
{
_isInstalling = false;
}
}
/// <summary>
/// Install a specific dependency
/// </summary>
private async Task<bool> InstallDependency(DependencyStatus dependency)
{
try
{
switch (dependency.Name)
{
case "Python":
return await InstallPython();
case "UV Package Manager":
return await InstallUV();
case "MCP Server":
return await InstallMCPServer();
default:
McpLog.Warn($"Unknown dependency: {dependency.Name}");
return false;
}
}
catch (Exception ex)
{
McpLog.Error($"Error installing {dependency.Name}: {ex.Message}");
return false;
}
}
/// <summary>
/// Attempt to install Python (limited automatic options)
/// </summary>
private async Task<bool> InstallPython()
{
OnProgressUpdate?.Invoke("Python installation requires manual intervention...");
// For Asset Store compliance, we cannot automatically install Python
// We can only guide the user to install it manually
await Task.Delay(1000); // Simulate some work
OnProgressUpdate?.Invoke("Python must be installed manually. Please visit the installation URL provided.");
return false; // Always return false since we can't auto-install
}
/// <summary>
/// Attempt to install UV package manager
/// </summary>
private async Task<bool> InstallUV()
{
OnProgressUpdate?.Invoke("UV installation requires manual intervention...");
// For Asset Store compliance, we cannot automatically install UV
// We can only guide the user to install it manually
await Task.Delay(1000); // Simulate some work
OnProgressUpdate?.Invoke("UV must be installed manually. Please visit the installation URL provided.");
return false; // Always return false since we can't auto-install
}
/// <summary>
/// Install MCP Server (this we can do automatically)
/// </summary>
private async Task<bool> InstallMCPServer()
{
try
{
OnProgressUpdate?.Invoke("Installing MCP Server...");
// Run server installation on a background thread
bool success = await Task.Run(() =>
{
try
{
ServerInstaller.EnsureServerInstalled();
return true;
}
catch (Exception ex)
{
McpLog.Error($"Server installation failed: {ex.Message}");
return false;
}
});
if (success)
{
OnProgressUpdate?.Invoke("MCP Server installed successfully.");
return true;
}
else
{
OnProgressUpdate?.Invoke("MCP Server installation failed.");
return false;
}
}
catch (Exception ex)
{
McpLog.Error($"Error during MCP Server installation: {ex.Message}");
OnProgressUpdate?.Invoke($"MCP Server installation error: {ex.Message}");
return false;
}
}
/// <summary>
/// Check if installation is currently in progress
/// </summary>
public bool IsInstalling => _isInstalling;
/// <summary>
/// Cancel ongoing installation (if possible)
/// </summary>
public void CancelInstallation()
{
if (_isInstalling)
{
OnProgressUpdate?.Invoke("Cancelling installation...");
_isInstalling = false;
OnInstallationComplete?.Invoke(false, "Installation cancelled by user.");
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5678901234abcdef0123456789abcdef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,19 @@
{
"name": "MCPForUnity.Editor",
"rootNamespace": "MCPForUnity.Editor",
"references": [
"MCPForUnity.Runtime",
"GUID:560b04d1a97f54a46a2660c3cc343a6f"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 98f702da6ca044be59a864a9419c4eab
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96dc847eb7f7a45e0b91241db934a4be
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 16d3ab36890b6c14f9afeabee30e03e3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,21 @@
using Newtonsoft.Json.Linq;
namespace MCPForUnity.Editor.Models
{
/// <summary>
/// Represents a command received from the MCP client
/// </summary>
public class Command
{
/// <summary>
/// The type of command to execute
/// </summary>
public string type { get; set; }
/// <summary>
/// The parameters for the command
/// </summary>
public JObject @params { get; set; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6754c84e5deb74749bc3a19e0c9aa280
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,19 @@
using System;
using Newtonsoft.Json;
namespace MCPForUnity.Editor.Models
{
[Serializable]
public class McpConfigServer
{
[JsonProperty("command")]
public string command;
[JsonProperty("args")]
public string[] args;
// VSCode expects a transport type; include only when explicitly set
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public string type;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5fae9d995f514e9498e9613e2cdbeca9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,12 @@
using System;
using Newtonsoft.Json;
namespace MCPForUnity.Editor.Models
{
[Serializable]
public class McpConfigServers
{
[JsonProperty("unityMCP")]
public McpConfigServer unityMCP;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcb583553e8173b49be71a5c43bd9502
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,47 @@
namespace MCPForUnity.Editor.Models
{
public class McpClient
{
public string name;
public string windowsConfigPath;
public string macConfigPath;
public string linuxConfigPath;
public McpTypes mcpType;
public string configStatus;
public McpStatus status = McpStatus.NotConfigured;
// Helper method to convert the enum to a display string
public string GetStatusDisplayString()
{
return status switch
{
McpStatus.NotConfigured => "Not Configured",
McpStatus.Configured => "Configured",
McpStatus.Running => "Running",
McpStatus.Connected => "Connected",
McpStatus.IncorrectPath => "Incorrect Path",
McpStatus.CommunicationError => "Communication Error",
McpStatus.NoResponse => "No Response",
McpStatus.UnsupportedOS => "Unsupported OS",
McpStatus.MissingConfig => "Missing MCPForUnity Config",
McpStatus.Error => configStatus.StartsWith("Error:") ? configStatus : "Error",
_ => "Unknown",
};
}
// Helper method to set both status enum and string for backward compatibility
public void SetStatus(McpStatus newStatus, string errorDetails = null)
{
status = newStatus;
if (newStatus == McpStatus.Error && !string.IsNullOrEmpty(errorDetails))
{
configStatus = $"Error: {errorDetails}";
}
else
{
configStatus = GetStatusDisplayString();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1afa56984aec0d41808edcebf805e6a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,12 @@
using System;
using Newtonsoft.Json;
namespace MCPForUnity.Editor.Models
{
[Serializable]
public class McpConfig
{
[JsonProperty("mcpServers")]
public McpConfigServers mcpServers;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c17c09908f0c1524daa8b6957ce1f7f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
namespace MCPForUnity.Editor.Models
{
// Enum representing the various status states for MCP clients
public enum McpStatus
{
NotConfigured, // Not set up yet
Configured, // Successfully configured
Running, // Service is running
Connected, // Successfully connected
IncorrectPath, // Configuration has incorrect paths
CommunicationError, // Connected but communication issues
NoResponse, // Connected but not responding
MissingConfig, // Config file exists but missing required elements
UnsupportedOS, // OS is not supported
Error, // General error state
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa63057c9e5282d4887352578bf49971
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,13 @@
namespace MCPForUnity.Editor.Models
{
public enum McpTypes
{
ClaudeCode,
ClaudeDesktop,
Cursor,
VSCode,
Windsurf,
Kiro,
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,36 @@
using System;
using Newtonsoft.Json;
namespace MCPForUnity.Editor.Models
{
[Serializable]
public class ServerConfig
{
[JsonProperty("unity_host")]
public string unityHost = "localhost";
[JsonProperty("unity_port")]
public int unityPort;
[JsonProperty("mcp_port")]
public int mcpPort;
[JsonProperty("connection_timeout")]
public float connectionTimeout;
[JsonProperty("buffer_size")]
public int bufferSize;
[JsonProperty("log_level")]
public string logLevel;
[JsonProperty("log_format")]
public string logFormat;
[JsonProperty("max_retries")]
public int maxRetries;
[JsonProperty("retry_delay")]
public float retryDelay;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e4e45386fcc282249907c2e3c7e5d9c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d4e5f6789012345678901234abcdef01
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,278 @@
using System;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Setup
{
/// <summary>
/// Handles automatic triggering of the setup wizard based on dependency state
/// </summary>
[InitializeOnLoad]
public static class SetupWizard
{
private const string SETUP_STATE_KEY = "MCPForUnity.SetupState";
private const string PACKAGE_VERSION = "3.4.0"; // Should match package.json version
private static SetupState _setupState;
private static bool _hasCheckedThisSession = false;
static SetupWizard()
{
// Skip in batch mode unless explicitly allowed
if (Application.isBatchMode && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITY_MCP_ALLOW_BATCH")))
{
return;
}
// Defer setup check until editor is ready
EditorApplication.delayCall += CheckSetupNeeded;
}
/// <summary>
/// Get the current setup state
/// </summary>
public static SetupState GetSetupState()
{
if (_setupState == null)
{
LoadSetupState();
}
return _setupState;
}
/// <summary>
/// Save the current setup state
/// </summary>
public static void SaveSetupState()
{
if (_setupState != null)
{
try
{
string json = JsonUtility.ToJson(_setupState, true);
EditorPrefs.SetString(SETUP_STATE_KEY, json);
McpLog.Info("Setup state saved", always: false);
}
catch (Exception ex)
{
McpLog.Error($"Failed to save setup state: {ex.Message}");
}
}
}
/// <summary>
/// Load setup state from EditorPrefs
/// </summary>
private static void LoadSetupState()
{
try
{
string json = EditorPrefs.GetString(SETUP_STATE_KEY, "");
if (!string.IsNullOrEmpty(json))
{
_setupState = JsonUtility.FromJson<SetupState>(json);
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to load setup state: {ex.Message}");
}
// Create default state if loading failed
if (_setupState == null)
{
_setupState = new SetupState();
}
}
/// <summary>
/// Check if setup wizard should be shown
/// </summary>
private static void CheckSetupNeeded()
{
// Only check once per session
if (_hasCheckedThisSession)
return;
_hasCheckedThisSession = true;
try
{
var setupState = GetSetupState();
// Don't show setup if user has dismissed it or if already completed for this version
if (!setupState.ShouldShowSetup(PACKAGE_VERSION))
{
McpLog.Info("Setup wizard not needed - already completed or dismissed", always: false);
return;
}
// Check if dependencies are missing
var dependencyResult = DependencyManager.CheckAllDependencies();
if (dependencyResult.IsSystemReady)
{
McpLog.Info("All dependencies available - marking setup as completed", always: false);
setupState.MarkSetupCompleted(PACKAGE_VERSION);
SaveSetupState();
return;
}
// Show setup wizard if dependencies are missing
var missingRequired = dependencyResult.GetMissingRequired();
if (missingRequired.Count > 0)
{
McpLog.Info($"Missing required dependencies: {string.Join(", ", missingRequired.ConvertAll(d => d.Name))}");
// Delay showing the wizard slightly to ensure Unity is fully loaded
EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult);
}
}
catch (Exception ex)
{
McpLog.Error($"Error checking setup requirements: {ex.Message}");
}
}
/// <summary>
/// Show the setup wizard window
/// </summary>
public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null)
{
try
{
// If no dependency result provided, check now
if (dependencyResult == null)
{
dependencyResult = DependencyManager.CheckAllDependencies();
}
// Show the setup wizard window
SetupWizardWindow.ShowWindow(dependencyResult);
// Record that we've attempted setup
var setupState = GetSetupState();
setupState.RecordSetupAttempt();
SaveSetupState();
}
catch (Exception ex)
{
McpLog.Error($"Error showing setup wizard: {ex.Message}");
}
}
/// <summary>
/// Mark setup as completed
/// </summary>
public static void MarkSetupCompleted()
{
try
{
var setupState = GetSetupState();
setupState.MarkSetupCompleted(PACKAGE_VERSION);
SaveSetupState();
McpLog.Info("Setup marked as completed");
}
catch (Exception ex)
{
McpLog.Error($"Error marking setup as completed: {ex.Message}");
}
}
/// <summary>
/// Mark setup as dismissed
/// </summary>
public static void MarkSetupDismissed()
{
try
{
var setupState = GetSetupState();
setupState.MarkSetupDismissed();
SaveSetupState();
McpLog.Info("Setup marked as dismissed");
}
catch (Exception ex)
{
McpLog.Error($"Error marking setup as dismissed: {ex.Message}");
}
}
/// <summary>
/// Reset setup state (for debugging or re-setup)
/// </summary>
public static void ResetSetupState()
{
try
{
var setupState = GetSetupState();
setupState.Reset();
SaveSetupState();
McpLog.Info("Setup state reset");
}
catch (Exception ex)
{
McpLog.Error($"Error resetting setup state: {ex.Message}");
}
}
/// <summary>
/// Force show setup wizard (for manual invocation)
/// </summary>
[MenuItem("Window/MCP for Unity/Setup Wizard", priority = 1)]
public static void ShowSetupWizardManual()
{
ShowSetupWizard();
}
/// <summary>
/// Reset setup and show wizard again
/// </summary>
[MenuItem("Window/MCP for Unity/Reset Setup", priority = 2)]
public static void ResetAndShowSetup()
{
ResetSetupState();
_hasCheckedThisSession = false;
ShowSetupWizard();
}
/// <summary>
/// Check dependencies and show status
/// </summary>
[MenuItem("Window/MCP for Unity/Check Dependencies", priority = 3)]
public static void CheckDependencies()
{
var result = DependencyManager.CheckAllDependencies();
var diagnostics = DependencyManager.GetDependencyDiagnostics();
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Dependency Check Results\n{diagnostics}");
if (!result.IsSystemReady)
{
bool showWizard = EditorUtility.DisplayDialog(
"MCP for Unity - Dependencies",
$"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?",
"Open Setup Wizard",
"Close"
);
if (showWizard)
{
ShowSetupWizard(result);
}
}
else
{
EditorUtility.DisplayDialog(
"MCP for Unity - Dependencies",
"✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.",
"OK"
);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 345678901234abcdef0123456789abcd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,465 @@
using System;
using System.Collections.Generic;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Installation;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Setup
{
/// <summary>
/// Setup wizard window for guiding users through dependency installation
/// </summary>
public class SetupWizardWindow : EditorWindow
{
private DependencyCheckResult _dependencyResult;
private Vector2 _scrollPosition;
private int _currentStep = 0;
private bool _isInstalling = false;
private string _installationStatus = "";
private InstallationOrchestrator _orchestrator;
private readonly string[] _stepTitles = {
"Welcome",
"Dependency Check",
"Installation Options",
"Installation Progress",
"Complete"
};
public static void ShowWindow(DependencyCheckResult dependencyResult = null)
{
var window = GetWindow<SetupWizardWindow>("MCP for Unity Setup");
window.minSize = new Vector2(500, 400);
window.maxSize = new Vector2(800, 600);
window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies();
window.Show();
}
private void OnEnable()
{
if (_dependencyResult == null)
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
_orchestrator = new InstallationOrchestrator();
_orchestrator.OnProgressUpdate += OnInstallationProgress;
_orchestrator.OnInstallationComplete += OnInstallationComplete;
}
private void OnDisable()
{
if (_orchestrator != null)
{
_orchestrator.OnProgressUpdate -= OnInstallationProgress;
_orchestrator.OnInstallationComplete -= OnInstallationComplete;
}
}
private void OnGUI()
{
DrawHeader();
DrawProgressBar();
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
switch (_currentStep)
{
case 0: DrawWelcomeStep(); break;
case 1: DrawDependencyCheckStep(); break;
case 2: DrawInstallationOptionsStep(); break;
case 3: DrawInstallationProgressStep(); break;
case 4: DrawCompleteStep(); break;
}
EditorGUILayout.EndScrollView();
DrawFooter();
}
private void DrawHeader()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}");
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Step title
var titleStyle = new GUIStyle(EditorStyles.largeLabel)
{
fontSize = 16,
fontStyle = FontStyle.Bold
};
EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle);
EditorGUILayout.Space();
}
private void DrawProgressBar()
{
var rect = EditorGUILayout.GetControlRect(false, 4);
var progress = (_currentStep + 1) / (float)_stepTitles.Length;
EditorGUI.ProgressBar(rect, progress, "");
EditorGUILayout.Space();
}
private void DrawWelcomeStep()
{
EditorGUILayout.LabelField("Welcome to MCP for Unity!", EditorStyles.boldLabel);
EditorGUILayout.Space();
EditorGUILayout.LabelField(
"This wizard will help you set up the required dependencies for MCP for Unity to work properly.",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
EditorGUILayout.LabelField("What is MCP for Unity?", EditorStyles.boldLabel);
EditorGUILayout.LabelField(
"MCP for Unity is a bridge that connects AI assistants like Claude Desktop to your Unity Editor, " +
"allowing them to help you with Unity development tasks directly.",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Required Dependencies:", EditorStyles.boldLabel);
EditorGUILayout.LabelField("• Python 3.10 or later", EditorStyles.label);
EditorGUILayout.LabelField("• UV package manager", EditorStyles.label);
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"This wizard will check for these dependencies and guide you through installation if needed.",
MessageType.Info
);
}
private void DrawDependencyCheckStep()
{
EditorGUILayout.LabelField("Checking Dependencies", EditorStyles.boldLabel);
EditorGUILayout.Space();
if (GUILayout.Button("Refresh Dependency Check"))
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
EditorGUILayout.Space();
// Show dependency status
foreach (var dep in _dependencyResult.Dependencies)
{
DrawDependencyStatus(dep);
}
EditorGUILayout.Space();
// Overall status
var statusColor = _dependencyResult.IsSystemReady ? Color.green : Color.red;
var statusText = _dependencyResult.IsSystemReady ? "✓ System Ready" : "✗ Dependencies Missing";
var originalColor = GUI.color;
GUI.color = statusColor;
EditorGUILayout.LabelField(statusText, EditorStyles.boldLabel);
GUI.color = originalColor;
EditorGUILayout.Space();
EditorGUILayout.LabelField(_dependencyResult.Summary, EditorStyles.wordWrappedLabel);
if (!_dependencyResult.IsSystemReady)
{
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"Some dependencies are missing. The next step will help you install them.",
MessageType.Warning
);
}
}
private void DrawDependencyStatus(DependencyStatus dep)
{
EditorGUILayout.BeginHorizontal();
// Status icon
var statusIcon = dep.IsAvailable ? "✓" : "✗";
var statusColor = dep.IsAvailable ? Color.green : (dep.IsRequired ? Color.red : Color.yellow);
var originalColor = GUI.color;
GUI.color = statusColor;
GUILayout.Label(statusIcon, GUILayout.Width(20));
GUI.color = originalColor;
// Dependency name and details
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel);
if (!string.IsNullOrEmpty(dep.Version))
{
EditorGUILayout.LabelField($"Version: {dep.Version}", EditorStyles.miniLabel);
}
if (!string.IsNullOrEmpty(dep.Details))
{
EditorGUILayout.LabelField(dep.Details, EditorStyles.miniLabel);
}
if (!string.IsNullOrEmpty(dep.ErrorMessage))
{
EditorGUILayout.LabelField($"Error: {dep.ErrorMessage}", EditorStyles.miniLabel);
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
}
private void DrawInstallationOptionsStep()
{
EditorGUILayout.LabelField("Installation Options", EditorStyles.boldLabel);
EditorGUILayout.Space();
var missingDeps = _dependencyResult.GetMissingRequired();
if (missingDeps.Count == 0)
{
EditorGUILayout.HelpBox("All required dependencies are already available!", MessageType.Info);
return;
}
EditorGUILayout.LabelField("Missing Dependencies:", EditorStyles.boldLabel);
foreach (var dep in missingDeps)
{
EditorGUILayout.LabelField($"• {dep.Name}", EditorStyles.label);
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("Installation Methods:", EditorStyles.boldLabel);
// Automatic installation option
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Automatic Installation (Recommended)", EditorStyles.boldLabel);
EditorGUILayout.LabelField(
"The wizard will attempt to install missing dependencies automatically.",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
if (GUILayout.Button("Start Automatic Installation", GUILayout.Height(30)))
{
StartAutomaticInstallation();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
// Manual installation option
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Manual Installation", EditorStyles.boldLabel);
EditorGUILayout.LabelField(
"Install dependencies manually using the platform-specific instructions below.",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
var recommendations = DependencyManager.GetInstallationRecommendations();
EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel);
EditorGUILayout.Space();
if (GUILayout.Button("Open Installation URLs"))
{
OpenInstallationUrls();
}
EditorGUILayout.EndVertical();
}
private void DrawInstallationProgressStep()
{
EditorGUILayout.LabelField("Installation Progress", EditorStyles.boldLabel);
EditorGUILayout.Space();
if (_isInstalling)
{
EditorGUILayout.LabelField("Installing dependencies...", EditorStyles.boldLabel);
EditorGUILayout.Space();
// Show progress
var rect = EditorGUILayout.GetControlRect(false, 20);
EditorGUI.ProgressBar(rect, 0.5f, "Installing...");
EditorGUILayout.Space();
EditorGUILayout.LabelField(_installationStatus, EditorStyles.wordWrappedLabel);
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"Please wait while dependencies are being installed. This may take a few minutes.",
MessageType.Info
);
}
else
{
EditorGUILayout.LabelField("Installation completed!", EditorStyles.boldLabel);
EditorGUILayout.Space();
if (GUILayout.Button("Check Dependencies Again"))
{
_dependencyResult = DependencyManager.CheckAllDependencies();
if (_dependencyResult.IsSystemReady)
{
_currentStep = 4; // Go to complete step
}
}
}
}
private void DrawCompleteStep()
{
EditorGUILayout.LabelField("Setup Complete!", EditorStyles.boldLabel);
EditorGUILayout.Space();
if (_dependencyResult.IsSystemReady)
{
EditorGUILayout.HelpBox(
"✓ All dependencies are now available! MCP for Unity is ready to use.",
MessageType.Info
);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Next Steps:", EditorStyles.boldLabel);
EditorGUILayout.LabelField("1. Configure your AI assistant (Claude Desktop, Cursor, etc.)", EditorStyles.label);
EditorGUILayout.LabelField("2. Add MCP for Unity to your AI assistant's configuration", EditorStyles.label);
EditorGUILayout.LabelField("3. Start using AI assistance in Unity!", EditorStyles.label);
EditorGUILayout.Space();
if (GUILayout.Button("Open Documentation"))
{
Application.OpenURL("https://github.com/CoplayDev/unity-mcp");
}
}
else
{
EditorGUILayout.HelpBox(
"Some dependencies are still missing. Please install them manually or try the automatic installation again.",
MessageType.Warning
);
}
}
private void DrawFooter()
{
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
// Back button
GUI.enabled = _currentStep > 0 && !_isInstalling;
if (GUILayout.Button("Back"))
{
_currentStep--;
}
GUILayout.FlexibleSpace();
// Skip/Dismiss button
GUI.enabled = !_isInstalling;
if (GUILayout.Button("Skip Setup"))
{
bool dismiss = EditorUtility.DisplayDialog(
"Skip Setup",
"Are you sure you want to skip the setup? You can run it again later from the Window menu.",
"Skip",
"Cancel"
);
if (dismiss)
{
SetupWizard.MarkSetupDismissed();
Close();
}
}
// Next/Finish button
GUI.enabled = !_isInstalling;
string nextButtonText = _currentStep == _stepTitles.Length - 1 ? "Finish" : "Next";
if (GUILayout.Button(nextButtonText))
{
if (_currentStep == _stepTitles.Length - 1)
{
// Finish setup
SetupWizard.MarkSetupCompleted();
Close();
}
else
{
_currentStep++;
}
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
}
private void StartAutomaticInstallation()
{
_currentStep = 3; // Go to progress step
_isInstalling = true;
_installationStatus = "Starting installation...";
var missingDeps = _dependencyResult.GetMissingRequired();
_orchestrator.StartInstallation(missingDeps);
}
private void OpenInstallationUrls()
{
var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls();
bool openPython = EditorUtility.DisplayDialog(
"Open Installation URLs",
"Open Python installation page?",
"Yes",
"No"
);
if (openPython)
{
Application.OpenURL(pythonUrl);
}
bool openUV = EditorUtility.DisplayDialog(
"Open Installation URLs",
"Open UV installation page?",
"Yes",
"No"
);
if (openUV)
{
Application.OpenURL(uvUrl);
}
}
private void OnInstallationProgress(string status)
{
_installationStatus = status;
Repaint();
}
private void OnInstallationComplete(bool success, string message)
{
_isInstalling = false;
_installationStatus = message;
if (success)
{
_dependencyResult = DependencyManager.CheckAllDependencies();
if (_dependencyResult.IsSystemReady)
{
_currentStep = 4; // Go to complete step
}
}
Repaint();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 45678901234abcdef0123456789abcde
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c97b83a6ac92a704b864eef27c3d285b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools.MenuItems;
namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Registry for all MCP command handlers (Refactored Version)
/// </summary>
public static class CommandRegistry
{
// Maps command names (matching those called from Python via ctx.bridge.unity_editor.HandlerName)
// to the corresponding static HandleCommand method in the appropriate tool class.
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new()
{
{ "HandleManageScript", ManageScript.HandleCommand },
{ "HandleManageScene", ManageScene.HandleCommand },
{ "HandleManageEditor", ManageEditor.HandleCommand },
{ "HandleManageGameObject", ManageGameObject.HandleCommand },
{ "HandleManageAsset", ManageAsset.HandleCommand },
{ "HandleReadConsole", ReadConsole.HandleCommand },
{ "HandleManageMenuItem", ManageMenuItem.HandleCommand },
{ "HandleManageShader", ManageShader.HandleCommand},
};
/// <summary>
/// Gets a command handler by name.
/// </summary>
/// <param name="commandName">Name of the command handler (e.g., "HandleManageAsset").</param>
/// <returns>The command handler function if found, null otherwise.</returns>
public static Func<JObject, object> GetHandler(string commandName)
{
// Use case-insensitive comparison for flexibility, although Python side should be consistent
return _handlers.TryGetValue(commandName, out var handler) ? handler : null;
// Consider adding logging here if a handler is not found
/*
if (_handlers.TryGetValue(commandName, out var handler)) {
return handler;
} else {
UnityEngine.Debug.LogError($\"[CommandRegistry] No handler found for command: {commandName}\");
return null;
}
*/
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5b61b5a84813b5749a5c64422694a0fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: de90a1d9743a2874cb235cf0b83444b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,613 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditorInternal; // Required for tag management
using UnityEngine;
using MCPForUnity.Editor.Helpers; // For Response class
namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Handles operations related to controlling and querying the Unity Editor state,
/// including managing Tags and Layers.
/// </summary>
public static class ManageEditor
{
// Constant for starting user layer index
private const int FirstUserLayerIndex = 8;
// Constant for total layer count
private const int TotalLayerCount = 32;
/// <summary>
/// Main handler for editor management actions.
/// </summary>
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString().ToLower();
// Parameters for specific actions
string tagName = @params["tagName"]?.ToString();
string layerName = @params["layerName"]?.ToString();
bool waitForCompletion = @params["waitForCompletion"]?.ToObject<bool>() ?? false; // Example - not used everywhere
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
// Route action
switch (action)
{
// Play Mode Control
case "play":
try
{
if (!EditorApplication.isPlaying)
{
EditorApplication.isPlaying = true;
return Response.Success("Entered play mode.");
}
return Response.Success("Already in play mode.");
}
catch (Exception e)
{
return Response.Error($"Error entering play mode: {e.Message}");
}
case "pause":
try
{
if (EditorApplication.isPlaying)
{
EditorApplication.isPaused = !EditorApplication.isPaused;
return Response.Success(
EditorApplication.isPaused ? "Game paused." : "Game resumed."
);
}
return Response.Error("Cannot pause/resume: Not in play mode.");
}
catch (Exception e)
{
return Response.Error($"Error pausing/resuming game: {e.Message}");
}
case "stop":
try
{
if (EditorApplication.isPlaying)
{
EditorApplication.isPlaying = false;
return Response.Success("Exited play mode.");
}
return Response.Success("Already stopped (not in play mode).");
}
catch (Exception e)
{
return Response.Error($"Error stopping play mode: {e.Message}");
}
// Editor State/Info
case "get_state":
return GetEditorState();
case "get_project_root":
return GetProjectRoot();
case "get_windows":
return GetEditorWindows();
case "get_active_tool":
return GetActiveTool();
case "get_selection":
return GetSelection();
case "set_active_tool":
string toolName = @params["toolName"]?.ToString();
if (string.IsNullOrEmpty(toolName))
return Response.Error("'toolName' parameter required for set_active_tool.");
return SetActiveTool(toolName);
// Tag Management
case "add_tag":
if (string.IsNullOrEmpty(tagName))
return Response.Error("'tagName' parameter required for add_tag.");
return AddTag(tagName);
case "remove_tag":
if (string.IsNullOrEmpty(tagName))
return Response.Error("'tagName' parameter required for remove_tag.");
return RemoveTag(tagName);
case "get_tags":
return GetTags(); // Helper to list current tags
// Layer Management
case "add_layer":
if (string.IsNullOrEmpty(layerName))
return Response.Error("'layerName' parameter required for add_layer.");
return AddLayer(layerName);
case "remove_layer":
if (string.IsNullOrEmpty(layerName))
return Response.Error("'layerName' parameter required for remove_layer.");
return RemoveLayer(layerName);
case "get_layers":
return GetLayers(); // Helper to list current layers
// --- Settings (Example) ---
// case "set_resolution":
// int? width = @params["width"]?.ToObject<int?>();
// int? height = @params["height"]?.ToObject<int?>();
// if (!width.HasValue || !height.HasValue) return Response.Error("'width' and 'height' parameters required.");
// return SetGameViewResolution(width.Value, height.Value);
// case "set_quality":
// // Handle string name or int index
// return SetQualityLevel(@params["qualityLevel"]);
default:
return Response.Error(
$"Unknown action: '{action}'. Supported actions include play, pause, stop, get_state, get_project_root, get_windows, get_active_tool, get_selection, set_active_tool, add_tag, remove_tag, get_tags, add_layer, remove_layer, get_layers."
);
}
}
// --- Editor State/Info Methods ---
private static object GetEditorState()
{
try
{
var state = new
{
isPlaying = EditorApplication.isPlaying,
isPaused = EditorApplication.isPaused,
isCompiling = EditorApplication.isCompiling,
isUpdating = EditorApplication.isUpdating,
applicationPath = EditorApplication.applicationPath,
applicationContentsPath = EditorApplication.applicationContentsPath,
timeSinceStartup = EditorApplication.timeSinceStartup,
};
return Response.Success("Retrieved editor state.", state);
}
catch (Exception e)
{
return Response.Error($"Error getting editor state: {e.Message}");
}
}
private static object GetProjectRoot()
{
try
{
// Application.dataPath points to <Project>/Assets
string assetsPath = Application.dataPath.Replace('\\', '/');
string projectRoot = Directory.GetParent(assetsPath)?.FullName.Replace('\\', '/');
if (string.IsNullOrEmpty(projectRoot))
{
return Response.Error("Could not determine project root from Application.dataPath");
}
return Response.Success("Project root resolved.", new { projectRoot });
}
catch (Exception e)
{
return Response.Error($"Error getting project root: {e.Message}");
}
}
private static object GetEditorWindows()
{
try
{
// Get all types deriving from EditorWindow
var windowTypes = AppDomain
.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => type.IsSubclassOf(typeof(EditorWindow)))
.ToList();
var openWindows = new List<object>();
// Find currently open instances
// Resources.FindObjectsOfTypeAll seems more reliable than GetWindow for finding *all* open windows
EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll<EditorWindow>();
foreach (EditorWindow window in allWindows)
{
if (window == null)
continue; // Skip potentially destroyed windows
try
{
openWindows.Add(
new
{
title = window.titleContent.text,
typeName = window.GetType().FullName,
isFocused = EditorWindow.focusedWindow == window,
position = new
{
x = window.position.x,
y = window.position.y,
width = window.position.width,
height = window.position.height,
},
instanceID = window.GetInstanceID(),
}
);
}
catch (Exception ex)
{
Debug.LogWarning(
$"Could not get info for window {window.GetType().Name}: {ex.Message}"
);
}
}
return Response.Success("Retrieved list of open editor windows.", openWindows);
}
catch (Exception e)
{
return Response.Error($"Error getting editor windows: {e.Message}");
}
}
private static object GetActiveTool()
{
try
{
Tool currentTool = UnityEditor.Tools.current;
string toolName = currentTool.ToString(); // Enum to string
bool customToolActive = UnityEditor.Tools.current == Tool.Custom; // Check if a custom tool is active
string activeToolName = customToolActive
? EditorTools.GetActiveToolName()
: toolName; // Get custom name if needed
var toolInfo = new
{
activeTool = activeToolName,
isCustom = customToolActive,
pivotMode = UnityEditor.Tools.pivotMode.ToString(),
pivotRotation = UnityEditor.Tools.pivotRotation.ToString(),
handleRotation = UnityEditor.Tools.handleRotation.eulerAngles, // Euler for simplicity
handlePosition = UnityEditor.Tools.handlePosition,
};
return Response.Success("Retrieved active tool information.", toolInfo);
}
catch (Exception e)
{
return Response.Error($"Error getting active tool: {e.Message}");
}
}
private static object SetActiveTool(string toolName)
{
try
{
Tool targetTool;
if (Enum.TryParse<Tool>(toolName, true, out targetTool)) // Case-insensitive parse
{
// Check if it's a valid built-in tool
if (targetTool != Tool.None && targetTool <= Tool.Custom) // Tool.Custom is the last standard tool
{
UnityEditor.Tools.current = targetTool;
return Response.Success($"Set active tool to '{targetTool}'.");
}
else
{
return Response.Error(
$"Cannot directly set tool to '{toolName}'. It might be None, Custom, or invalid."
);
}
}
else
{
// Potentially try activating a custom tool by name here if needed
// This often requires specific editor scripting knowledge for that tool.
return Response.Error(
$"Could not parse '{toolName}' as a standard Unity Tool (View, Move, Rotate, Scale, Rect, Transform, Custom)."
);
}
}
catch (Exception e)
{
return Response.Error($"Error setting active tool: {e.Message}");
}
}
private static object GetSelection()
{
try
{
var selectionInfo = new
{
activeObject = Selection.activeObject?.name,
activeGameObject = Selection.activeGameObject?.name,
activeTransform = Selection.activeTransform?.name,
activeInstanceID = Selection.activeInstanceID,
count = Selection.count,
objects = Selection
.objects.Select(obj => new
{
name = obj?.name,
type = obj?.GetType().FullName,
instanceID = obj?.GetInstanceID(),
})
.ToList(),
gameObjects = Selection
.gameObjects.Select(go => new
{
name = go?.name,
instanceID = go?.GetInstanceID(),
})
.ToList(),
assetGUIDs = Selection.assetGUIDs, // GUIDs for selected assets in Project view
};
return Response.Success("Retrieved current selection details.", selectionInfo);
}
catch (Exception e)
{
return Response.Error($"Error getting selection: {e.Message}");
}
}
// --- Tag Management Methods ---
private static object AddTag(string tagName)
{
if (string.IsNullOrWhiteSpace(tagName))
return Response.Error("Tag name cannot be empty or whitespace.");
// Check if tag already exists
if (InternalEditorUtility.tags.Contains(tagName))
{
return Response.Error($"Tag '{tagName}' already exists.");
}
try
{
// Add the tag using the internal utility
InternalEditorUtility.AddTag(tagName);
// Force save assets to ensure the change persists in the TagManager asset
AssetDatabase.SaveAssets();
return Response.Success($"Tag '{tagName}' added successfully.");
}
catch (Exception e)
{
return Response.Error($"Failed to add tag '{tagName}': {e.Message}");
}
}
private static object RemoveTag(string tagName)
{
if (string.IsNullOrWhiteSpace(tagName))
return Response.Error("Tag name cannot be empty or whitespace.");
if (tagName.Equals("Untagged", StringComparison.OrdinalIgnoreCase))
return Response.Error("Cannot remove the built-in 'Untagged' tag.");
// Check if tag exists before attempting removal
if (!InternalEditorUtility.tags.Contains(tagName))
{
return Response.Error($"Tag '{tagName}' does not exist.");
}
try
{
// Remove the tag using the internal utility
InternalEditorUtility.RemoveTag(tagName);
// Force save assets
AssetDatabase.SaveAssets();
return Response.Success($"Tag '{tagName}' removed successfully.");
}
catch (Exception e)
{
// Catch potential issues if the tag is somehow in use or removal fails
return Response.Error($"Failed to remove tag '{tagName}': {e.Message}");
}
}
private static object GetTags()
{
try
{
string[] tags = InternalEditorUtility.tags;
return Response.Success("Retrieved current tags.", tags);
}
catch (Exception e)
{
return Response.Error($"Failed to retrieve tags: {e.Message}");
}
}
// --- Layer Management Methods ---
private static object AddLayer(string layerName)
{
if (string.IsNullOrWhiteSpace(layerName))
return Response.Error("Layer name cannot be empty or whitespace.");
// Access the TagManager asset
SerializedObject tagManager = GetTagManager();
if (tagManager == null)
return Response.Error("Could not access TagManager asset.");
SerializedProperty layersProp = tagManager.FindProperty("layers");
if (layersProp == null || !layersProp.isArray)
return Response.Error("Could not find 'layers' property in TagManager.");
// Check if layer name already exists (case-insensitive check recommended)
for (int i = 0; i < TotalLayerCount; i++)
{
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
if (
layerSP != null
&& layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase)
)
{
return Response.Error($"Layer '{layerName}' already exists at index {i}.");
}
}
// Find the first empty user layer slot (indices 8 to 31)
int firstEmptyUserLayer = -1;
for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++)
{
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
if (layerSP != null && string.IsNullOrEmpty(layerSP.stringValue))
{
firstEmptyUserLayer = i;
break;
}
}
if (firstEmptyUserLayer == -1)
{
return Response.Error("No empty User Layer slots available (8-31 are full).");
}
// Assign the name to the found slot
try
{
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(
firstEmptyUserLayer
);
targetLayerSP.stringValue = layerName;
// Apply the changes to the TagManager asset
tagManager.ApplyModifiedProperties();
// Save assets to make sure it's written to disk
AssetDatabase.SaveAssets();
return Response.Success(
$"Layer '{layerName}' added successfully to slot {firstEmptyUserLayer}."
);
}
catch (Exception e)
{
return Response.Error($"Failed to add layer '{layerName}': {e.Message}");
}
}
private static object RemoveLayer(string layerName)
{
if (string.IsNullOrWhiteSpace(layerName))
return Response.Error("Layer name cannot be empty or whitespace.");
// Access the TagManager asset
SerializedObject tagManager = GetTagManager();
if (tagManager == null)
return Response.Error("Could not access TagManager asset.");
SerializedProperty layersProp = tagManager.FindProperty("layers");
if (layersProp == null || !layersProp.isArray)
return Response.Error("Could not find 'layers' property in TagManager.");
// Find the layer by name (must be user layer)
int layerIndexToRemove = -1;
for (int i = FirstUserLayerIndex; i < TotalLayerCount; i++) // Start from user layers
{
SerializedProperty layerSP = layersProp.GetArrayElementAtIndex(i);
// Case-insensitive comparison is safer
if (
layerSP != null
&& layerName.Equals(layerSP.stringValue, StringComparison.OrdinalIgnoreCase)
)
{
layerIndexToRemove = i;
break;
}
}
if (layerIndexToRemove == -1)
{
return Response.Error($"User layer '{layerName}' not found.");
}
// Clear the name for that index
try
{
SerializedProperty targetLayerSP = layersProp.GetArrayElementAtIndex(
layerIndexToRemove
);
targetLayerSP.stringValue = string.Empty; // Set to empty string to remove
// Apply the changes
tagManager.ApplyModifiedProperties();
// Save assets
AssetDatabase.SaveAssets();
return Response.Success(
$"Layer '{layerName}' (slot {layerIndexToRemove}) removed successfully."
);
}
catch (Exception e)
{
return Response.Error($"Failed to remove layer '{layerName}': {e.Message}");
}
}
private static object GetLayers()
{
try
{
var layers = new Dictionary<int, string>();
for (int i = 0; i < TotalLayerCount; i++)
{
string layerName = LayerMask.LayerToName(i);
if (!string.IsNullOrEmpty(layerName)) // Only include layers that have names
{
layers.Add(i, layerName);
}
}
return Response.Success("Retrieved current named layers.", layers);
}
catch (Exception e)
{
return Response.Error($"Failed to retrieve layers: {e.Message}");
}
}
// --- Helper Methods ---
/// <summary>
/// Gets the SerializedObject for the TagManager asset.
/// </summary>
private static SerializedObject GetTagManager()
{
try
{
// Load the TagManager asset from the ProjectSettings folder
UnityEngine.Object[] tagManagerAssets = AssetDatabase.LoadAllAssetsAtPath(
"ProjectSettings/TagManager.asset"
);
if (tagManagerAssets == null || tagManagerAssets.Length == 0)
{
Debug.LogError("[ManageEditor] TagManager.asset not found in ProjectSettings.");
return null;
}
// The first object in the asset file should be the TagManager
return new SerializedObject(tagManagerAssets[0]);
}
catch (Exception e)
{
Debug.LogError($"[ManageEditor] Error accessing TagManager.asset: {e.Message}");
return null;
}
}
// --- Example Implementations for Settings ---
/*
private static object SetGameViewResolution(int width, int height) { ... }
private static object SetQualityLevel(JToken qualityLevelToken) { ... }
*/
}
// Helper class to get custom tool names (remains the same)
internal static class EditorTools
{
public static string GetActiveToolName()
{
// This is a placeholder. Real implementation depends on how custom tools
// are registered and tracked in the specific Unity project setup.
// It might involve checking static variables, calling methods on specific tool managers, etc.
if (UnityEditor.Tools.current == Tool.Custom)
{
// Example: Check a known custom tool manager
// if (MyCustomToolManager.IsActive) return MyCustomToolManager.ActiveToolName;
return "Unknown Custom Tool";
}
return UnityEditor.Tools.current.ToString();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 43ac60aa36b361b4dbe4a038ae9f35c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7641d7388f0f6634b9d83d34de87b2ee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,475 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using MCPForUnity.Editor.Helpers; // For Response class
namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Handles scene management operations like loading, saving, creating, and querying hierarchy.
/// </summary>
public static class ManageScene
{
private sealed class SceneCommand
{
public string action { get; set; } = string.Empty;
public string name { get; set; } = string.Empty;
public string path { get; set; } = string.Empty;
public int? buildIndex { get; set; }
}
private static SceneCommand ToSceneCommand(JObject p)
{
if (p == null) return new SceneCommand();
int? BI(JToken t)
{
if (t == null || t.Type == JTokenType.Null) return null;
var s = t.ToString().Trim();
if (s.Length == 0) return null;
if (int.TryParse(s, out var i)) return i;
if (double.TryParse(s, out var d)) return (int)d;
return t.Type == JTokenType.Integer ? t.Value<int>() : (int?)null;
}
return new SceneCommand
{
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
name = p["name"]?.ToString() ?? string.Empty,
path = p["path"]?.ToString() ?? string.Empty,
buildIndex = BI(p["buildIndex"] ?? p["build_index"])
};
}
/// <summary>
/// Main handler for scene management actions.
/// </summary>
public static object HandleCommand(JObject @params)
{
try { McpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { }
var cmd = ToSceneCommand(@params);
string action = cmd.action;
string name = string.IsNullOrEmpty(cmd.name) ? null : cmd.name;
string path = string.IsNullOrEmpty(cmd.path) ? null : cmd.path; // Relative to Assets/
int? buildIndex = cmd.buildIndex;
// bool loadAdditive = @params["loadAdditive"]?.ToObject<bool>() ?? false; // Example for future extension
// Ensure path is relative to Assets/, removing any leading "Assets/"
string relativeDir = path ?? string.Empty;
if (!string.IsNullOrEmpty(relativeDir))
{
relativeDir = relativeDir.Replace('\\', '/').Trim('/');
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
}
}
// Apply default *after* sanitizing, using the original path variable for the check
if (string.IsNullOrEmpty(path) && action == "create") // Check original path for emptiness
{
relativeDir = "Scenes"; // Default relative directory
}
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
string sceneFileName = string.IsNullOrEmpty(name) ? null : $"{name}.unity";
// Construct full system path correctly: ProjectRoot/Assets/relativeDir/sceneFileName
string fullPathDir = Path.Combine(Application.dataPath, relativeDir); // Combine with Assets path (Application.dataPath ends in Assets)
string fullPath = string.IsNullOrEmpty(sceneFileName)
? null
: Path.Combine(fullPathDir, sceneFileName);
// Ensure relativePath always starts with "Assets/" and uses forward slashes
string relativePath = string.IsNullOrEmpty(sceneFileName)
? null
: Path.Combine("Assets", relativeDir, sceneFileName).Replace('\\', '/');
// Ensure directory exists for 'create'
if (action == "create" && !string.IsNullOrEmpty(fullPathDir))
{
try
{
Directory.CreateDirectory(fullPathDir);
}
catch (Exception e)
{
return Response.Error(
$"Could not create directory '{fullPathDir}': {e.Message}"
);
}
}
// Route action
try { McpLog.Info($"[ManageScene] Route action='{action}' name='{name}' path='{path}' buildIndex={(buildIndex.HasValue ? buildIndex.Value.ToString() : "null")}", always: false); } catch { }
switch (action)
{
case "create":
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(relativePath))
return Response.Error(
"'name' and 'path' parameters are required for 'create' action."
);
return CreateScene(fullPath, relativePath);
case "load":
// Loading can be done by path/name or build index
if (!string.IsNullOrEmpty(relativePath))
return LoadScene(relativePath);
else if (buildIndex.HasValue)
return LoadScene(buildIndex.Value);
else
return Response.Error(
"Either 'name'/'path' or 'buildIndex' must be provided for 'load' action."
);
case "save":
// Save current scene, optionally to a new path
return SaveScene(fullPath, relativePath);
case "get_hierarchy":
try { McpLog.Info("[ManageScene] get_hierarchy: entering", always: false); } catch { }
var gh = GetSceneHierarchy();
try { McpLog.Info("[ManageScene] get_hierarchy: exiting", always: false); } catch { }
return gh;
case "get_active":
try { McpLog.Info("[ManageScene] get_active: entering", always: false); } catch { }
var ga = GetActiveSceneInfo();
try { McpLog.Info("[ManageScene] get_active: exiting", always: false); } catch { }
return ga;
case "get_build_settings":
return GetBuildSettingsScenes();
// Add cases for modifying build settings, additive loading, unloading etc.
default:
return Response.Error(
$"Unknown action: '{action}'. Valid actions: create, load, save, get_hierarchy, get_active, get_build_settings."
);
}
}
private static object CreateScene(string fullPath, string relativePath)
{
if (File.Exists(fullPath))
{
return Response.Error($"Scene already exists at '{relativePath}'.");
}
try
{
// Create a new empty scene
Scene newScene = EditorSceneManager.NewScene(
NewSceneSetup.EmptyScene,
NewSceneMode.Single
);
// Save it to the specified path
bool saved = EditorSceneManager.SaveScene(newScene, relativePath);
if (saved)
{
AssetDatabase.Refresh(); // Ensure Unity sees the new scene file
return Response.Success(
$"Scene '{Path.GetFileName(relativePath)}' created successfully at '{relativePath}'.",
new { path = relativePath }
);
}
else
{
// If SaveScene fails, it might leave an untitled scene open.
// Optionally try to close it, but be cautious.
return Response.Error($"Failed to save new scene to '{relativePath}'.");
}
}
catch (Exception e)
{
return Response.Error($"Error creating scene '{relativePath}': {e.Message}");
}
}
private static object LoadScene(string relativePath)
{
if (
!File.Exists(
Path.Combine(
Application.dataPath.Substring(
0,
Application.dataPath.Length - "Assets".Length
),
relativePath
)
)
)
{
return Response.Error($"Scene file not found at '{relativePath}'.");
}
// Check for unsaved changes in the current scene
if (EditorSceneManager.GetActiveScene().isDirty)
{
// Optionally prompt the user or save automatically before loading
return Response.Error(
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
);
// Example: bool saveOK = EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo();
// if (!saveOK) return Response.Error("Load cancelled by user.");
}
try
{
EditorSceneManager.OpenScene(relativePath, OpenSceneMode.Single);
return Response.Success(
$"Scene '{relativePath}' loaded successfully.",
new
{
path = relativePath,
name = Path.GetFileNameWithoutExtension(relativePath),
}
);
}
catch (Exception e)
{
return Response.Error($"Error loading scene '{relativePath}': {e.Message}");
}
}
private static object LoadScene(int buildIndex)
{
if (buildIndex < 0 || buildIndex >= SceneManager.sceneCountInBuildSettings)
{
return Response.Error(
$"Invalid build index: {buildIndex}. Must be between 0 and {SceneManager.sceneCountInBuildSettings - 1}."
);
}
// Check for unsaved changes
if (EditorSceneManager.GetActiveScene().isDirty)
{
return Response.Error(
"Current scene has unsaved changes. Please save or discard changes before loading a new scene."
);
}
try
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex);
EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
return Response.Success(
$"Scene at build index {buildIndex} ('{scenePath}') loaded successfully.",
new
{
path = scenePath,
name = Path.GetFileNameWithoutExtension(scenePath),
buildIndex = buildIndex,
}
);
}
catch (Exception e)
{
return Response.Error(
$"Error loading scene with build index {buildIndex}: {e.Message}"
);
}
}
private static object SaveScene(string fullPath, string relativePath)
{
try
{
Scene currentScene = EditorSceneManager.GetActiveScene();
if (!currentScene.IsValid())
{
return Response.Error("No valid scene is currently active to save.");
}
bool saved;
string finalPath = currentScene.path; // Path where it was last saved or will be saved
if (!string.IsNullOrEmpty(relativePath) && currentScene.path != relativePath)
{
// Save As...
// Ensure directory exists
string dir = Path.GetDirectoryName(fullPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
saved = EditorSceneManager.SaveScene(currentScene, relativePath);
finalPath = relativePath;
}
else
{
// Save (overwrite existing or save untitled)
if (string.IsNullOrEmpty(currentScene.path))
{
// Scene is untitled, needs a path
return Response.Error(
"Cannot save an untitled scene without providing a 'name' and 'path'. Use Save As functionality."
);
}
saved = EditorSceneManager.SaveScene(currentScene);
}
if (saved)
{
AssetDatabase.Refresh();
return Response.Success(
$"Scene '{currentScene.name}' saved successfully to '{finalPath}'.",
new { path = finalPath, name = currentScene.name }
);
}
else
{
return Response.Error($"Failed to save scene '{currentScene.name}'.");
}
}
catch (Exception e)
{
return Response.Error($"Error saving scene: {e.Message}");
}
}
private static object GetActiveSceneInfo()
{
try
{
try { McpLog.Info("[ManageScene] get_active: querying EditorSceneManager.GetActiveScene", always: false); } catch { }
Scene activeScene = EditorSceneManager.GetActiveScene();
try { McpLog.Info($"[ManageScene] get_active: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { }
if (!activeScene.IsValid())
{
return Response.Error("No active scene found.");
}
var sceneInfo = new
{
name = activeScene.name,
path = activeScene.path,
buildIndex = activeScene.buildIndex, // -1 if not in build settings
isDirty = activeScene.isDirty,
isLoaded = activeScene.isLoaded,
rootCount = activeScene.rootCount,
};
return Response.Success("Retrieved active scene information.", sceneInfo);
}
catch (Exception e)
{
try { McpLog.Error($"[ManageScene] get_active: exception {e.Message}"); } catch { }
return Response.Error($"Error getting active scene info: {e.Message}");
}
}
private static object GetBuildSettingsScenes()
{
try
{
var scenes = new List<object>();
for (int i = 0; i < EditorBuildSettings.scenes.Length; i++)
{
var scene = EditorBuildSettings.scenes[i];
scenes.Add(
new
{
path = scene.path,
guid = scene.guid.ToString(),
enabled = scene.enabled,
buildIndex = i, // Actual build index considering only enabled scenes might differ
}
);
}
return Response.Success("Retrieved scenes from Build Settings.", scenes);
}
catch (Exception e)
{
return Response.Error($"Error getting scenes from Build Settings: {e.Message}");
}
}
private static object GetSceneHierarchy()
{
try
{
try { McpLog.Info("[ManageScene] get_hierarchy: querying EditorSceneManager.GetActiveScene", always: false); } catch { }
Scene activeScene = EditorSceneManager.GetActiveScene();
try { McpLog.Info($"[ManageScene] get_hierarchy: got scene valid={activeScene.IsValid()} loaded={activeScene.isLoaded} name='{activeScene.name}'", always: false); } catch { }
if (!activeScene.IsValid() || !activeScene.isLoaded)
{
return Response.Error(
"No valid and loaded scene is active to get hierarchy from."
);
}
try { McpLog.Info("[ManageScene] get_hierarchy: fetching root objects", always: false); } catch { }
GameObject[] rootObjects = activeScene.GetRootGameObjects();
try { McpLog.Info($"[ManageScene] get_hierarchy: rootCount={rootObjects?.Length ?? 0}", always: false); } catch { }
var hierarchy = rootObjects.Select(go => GetGameObjectDataRecursive(go)).ToList();
var resp = Response.Success(
$"Retrieved hierarchy for scene '{activeScene.name}'.",
hierarchy
);
try { McpLog.Info("[ManageScene] get_hierarchy: success", always: false); } catch { }
return resp;
}
catch (Exception e)
{
try { McpLog.Error($"[ManageScene] get_hierarchy: exception {e.Message}"); } catch { }
return Response.Error($"Error getting scene hierarchy: {e.Message}");
}
}
/// <summary>
/// Recursively builds a data representation of a GameObject and its children.
/// </summary>
private static object GetGameObjectDataRecursive(GameObject go)
{
if (go == null)
return null;
var childrenData = new List<object>();
foreach (Transform child in go.transform)
{
childrenData.Add(GetGameObjectDataRecursive(child.gameObject));
}
var gameObjectData = new Dictionary<string, object>
{
{ "name", go.name },
{ "activeSelf", go.activeSelf },
{ "activeInHierarchy", go.activeInHierarchy },
{ "tag", go.tag },
{ "layer", go.layer },
{ "isStatic", go.isStatic },
{ "instanceID", go.GetInstanceID() }, // Useful unique identifier
{
"transform",
new
{
position = new
{
x = go.transform.localPosition.x,
y = go.transform.localPosition.y,
z = go.transform.localPosition.z,
},
rotation = new
{
x = go.transform.localRotation.eulerAngles.x,
y = go.transform.localRotation.eulerAngles.y,
z = go.transform.localRotation.eulerAngles.z,
}, // Euler for simplicity
scale = new
{
x = go.transform.localScale.x,
y = go.transform.localScale.y,
z = go.transform.localScale.z,
},
}
},
{ "children", childrenData },
};
return gameObjectData;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6ddda47f4077e74fbb5092388cefcc2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 626d2d44668019a45ae52e9ee066b7ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,342 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Handles CRUD operations for shader files within the Unity project.
/// </summary>
public static class ManageShader
{
/// <summary>
/// Main handler for shader management actions.
/// </summary>
public static object HandleCommand(JObject @params)
{
// Extract parameters
string action = @params["action"]?.ToString().ToLower();
string name = @params["name"]?.ToString();
string path = @params["path"]?.ToString(); // Relative to Assets/
string contents = null;
// Check if we have base64 encoded contents
bool contentsEncoded = @params["contentsEncoded"]?.ToObject<bool>() ?? false;
if (contentsEncoded && @params["encodedContents"] != null)
{
try
{
contents = DecodeBase64(@params["encodedContents"].ToString());
}
catch (Exception e)
{
return Response.Error($"Failed to decode shader contents: {e.Message}");
}
}
else
{
contents = @params["contents"]?.ToString();
}
// Validate required parameters
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
if (string.IsNullOrEmpty(name))
{
return Response.Error("Name parameter is required.");
}
// Basic name validation (alphanumeric, underscores, cannot start with number)
if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$"))
{
return Response.Error(
$"Invalid shader name: '{name}'. Use only letters, numbers, underscores, and don't start with a number."
);
}
// Ensure path is relative to Assets/, removing any leading "Assets/"
// Set default directory to "Shaders" if path is not provided
string relativeDir = path ?? "Shaders"; // Default to "Shaders" if path is null
if (!string.IsNullOrEmpty(relativeDir))
{
relativeDir = relativeDir.Replace('\\', '/').Trim('/');
if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/');
}
}
// Handle empty string case explicitly after processing
if (string.IsNullOrEmpty(relativeDir))
{
relativeDir = "Shaders"; // Ensure default if path was provided as "" or only "/" or "Assets/"
}
// Construct paths
string shaderFileName = $"{name}.shader";
string fullPathDir = Path.Combine(Application.dataPath, relativeDir);
string fullPath = Path.Combine(fullPathDir, shaderFileName);
string relativePath = Path.Combine("Assets", relativeDir, shaderFileName)
.Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes
// Ensure the target directory exists for create/update
if (action == "create" || action == "update")
{
try
{
if (!Directory.Exists(fullPathDir))
{
Directory.CreateDirectory(fullPathDir);
// Refresh AssetDatabase to recognize new folders
AssetDatabase.Refresh();
}
}
catch (Exception e)
{
return Response.Error(
$"Could not create directory '{fullPathDir}': {e.Message}"
);
}
}
// Route to specific action handlers
switch (action)
{
case "create":
return CreateShader(fullPath, relativePath, name, contents);
case "read":
return ReadShader(fullPath, relativePath);
case "update":
return UpdateShader(fullPath, relativePath, name, contents);
case "delete":
return DeleteShader(fullPath, relativePath);
default:
return Response.Error(
$"Unknown action: '{action}'. Valid actions are: create, read, update, delete."
);
}
}
/// <summary>
/// Decode base64 string to normal text
/// </summary>
private static string DecodeBase64(string encoded)
{
byte[] data = Convert.FromBase64String(encoded);
return System.Text.Encoding.UTF8.GetString(data);
}
/// <summary>
/// Encode text to base64 string
/// </summary>
private static string EncodeBase64(string text)
{
byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
return Convert.ToBase64String(data);
}
private static object CreateShader(
string fullPath,
string relativePath,
string name,
string contents
)
{
// Check if shader already exists
if (File.Exists(fullPath))
{
return Response.Error(
$"Shader already exists at '{relativePath}'. Use 'update' action to modify."
);
}
// Add validation for shader name conflicts in Unity
if (Shader.Find(name) != null)
{
return Response.Error(
$"A shader with name '{name}' already exists in the project. Choose a different name."
);
}
// Generate default content if none provided
if (string.IsNullOrEmpty(contents))
{
contents = GenerateDefaultShaderContent(name);
}
try
{
File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false));
AssetDatabase.ImportAsset(relativePath);
AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader
return Response.Success(
$"Shader '{name}.shader' created successfully at '{relativePath}'.",
new { path = relativePath }
);
}
catch (Exception e)
{
return Response.Error($"Failed to create shader '{relativePath}': {e.Message}");
}
}
private static object ReadShader(string fullPath, string relativePath)
{
if (!File.Exists(fullPath))
{
return Response.Error($"Shader not found at '{relativePath}'.");
}
try
{
string contents = File.ReadAllText(fullPath);
// Return both normal and encoded contents for larger files
//TODO: Consider a threshold for large files
bool isLarge = contents.Length > 10000; // If content is large, include encoded version
var responseData = new
{
path = relativePath,
contents = contents,
// For large files, also include base64-encoded version
encodedContents = isLarge ? EncodeBase64(contents) : null,
contentsEncoded = isLarge,
};
return Response.Success(
$"Shader '{Path.GetFileName(relativePath)}' read successfully.",
responseData
);
}
catch (Exception e)
{
return Response.Error($"Failed to read shader '{relativePath}': {e.Message}");
}
}
private static object UpdateShader(
string fullPath,
string relativePath,
string name,
string contents
)
{
if (!File.Exists(fullPath))
{
return Response.Error(
$"Shader not found at '{relativePath}'. Use 'create' action to add a new shader."
);
}
if (string.IsNullOrEmpty(contents))
{
return Response.Error("Content is required for the 'update' action.");
}
try
{
File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false));
AssetDatabase.ImportAsset(relativePath);
AssetDatabase.Refresh();
return Response.Success(
$"Shader '{Path.GetFileName(relativePath)}' updated successfully.",
new { path = relativePath }
);
}
catch (Exception e)
{
return Response.Error($"Failed to update shader '{relativePath}': {e.Message}");
}
}
private static object DeleteShader(string fullPath, string relativePath)
{
if (!File.Exists(fullPath))
{
return Response.Error($"Shader not found at '{relativePath}'.");
}
try
{
// Delete the asset through Unity's AssetDatabase first
bool success = AssetDatabase.DeleteAsset(relativePath);
if (!success)
{
return Response.Error($"Failed to delete shader through Unity's AssetDatabase: '{relativePath}'");
}
// If the file still exists (rare case), try direct deletion
if (File.Exists(fullPath))
{
File.Delete(fullPath);
}
return Response.Success($"Shader '{Path.GetFileName(relativePath)}' deleted successfully.");
}
catch (Exception e)
{
return Response.Error($"Failed to delete shader '{relativePath}': {e.Message}");
}
}
//This is a CGProgram template
//TODO: making a HLSL template as well?
private static string GenerateDefaultShaderContent(string name)
{
return @"Shader """ + name + @"""
{
Properties
{
_MainTex (""Texture"", 2D) = ""white"" {}
}
SubShader
{
Tags { ""RenderType""=""Opaque"" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include ""UnityCG.cginc""
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}";
}
}
}

Some files were not shown because too many files have changed in this diff Show More