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 guidancemain
parent
6e72b33309
commit
ab25a71bc5
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 31e7fac5858840340a75cc6df0ad3d9e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: be61633e00d934610ac1ff8192ffbe3d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e59036660cc33d24596fbbf6d4657a83
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: de8f5721c34f7194392e9d8c7d0226c0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 711b86bbc1f661e4fb2c822e14970e16
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f6789012345678901234ab
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f6789012345678901234abcdef012345
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 678901234abcdef0123456789abcdef0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b2c3d4e5f6789012345678901234abcd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 789012345678901234abcdef01234567
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6789012345678901234abcdef0123456
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 89012345678901234abcdef012345678
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c3d4e5f6789012345678901234abcdef
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9012345678901234abcdef0123456789
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2345678901234abcdef0123456789abc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 12345678901234abcdef0123456789ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 012345678901234abcdef0123456789a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 94cb070dc5e15024da86150b27699ca0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5c07c3369f73943919d9e086a81d1dcc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 64b8ff807bc9a401c82015cbafccffac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9e2c3f8a4f4f48d8a4c1b7b8e3f5a1c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b82eaef548d164ca095f17db64d15af8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 19e6eaa637484e9fa19f9a0459809de2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f6789012345678901234ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 80c09a76b944f8c4691e06c4d76c4be8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5862c6a6d0a914f4d83224f8d039cf7b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b8f3c2d1e7a94f6c8a9b5e3d2c1a0f9e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f8514fd42f23cb641a36e52550825b35
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e5f6789012345678901234abcdef0123
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5678901234abcdef0123456789abcdef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 98f702da6ca044be59a864a9419c4eab
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 96dc847eb7f7a45e0b91241db934a4be
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 16d3ab36890b6c14f9afeabee30e03e3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6754c84e5deb74749bc3a19e0c9aa280
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5fae9d995f514e9498e9613e2cdbeca9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MCPForUnity.Editor.Models
|
||||
{
|
||||
[Serializable]
|
||||
public class McpConfigServers
|
||||
{
|
||||
[JsonProperty("unityMCP")]
|
||||
public McpConfigServer unityMCP;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bcb583553e8173b49be71a5c43bd9502
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b1afa56984aec0d41808edcebf805e6a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MCPForUnity.Editor.Models
|
||||
{
|
||||
[Serializable]
|
||||
public class McpConfig
|
||||
{
|
||||
[JsonProperty("mcpServers")]
|
||||
public McpConfigServers mcpServers;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c17c09908f0c1524daa8b6957ce1f7f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: aa63057c9e5282d4887352578bf49971
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
namespace MCPForUnity.Editor.Models
|
||||
{
|
||||
public enum McpTypes
|
||||
{
|
||||
ClaudeCode,
|
||||
ClaudeDesktop,
|
||||
Cursor,
|
||||
VSCode,
|
||||
Windsurf,
|
||||
Kiro,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e4e45386fcc282249907c2e3c7e5d9c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d4e5f6789012345678901234abcdef01
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 345678901234abcdef0123456789abcd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 45678901234abcdef0123456789abcde
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c97b83a6ac92a704b864eef27c3d285b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5b61b5a84813b5749a5c64422694a0fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: de90a1d9743a2874cb235cf0b83444b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 43ac60aa36b361b4dbe4a038ae9f35c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7641d7388f0f6634b9d83d34de87b2ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b6ddda47f4077e74fbb5092388cefcc2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 626d2d44668019a45ae52e9ee066b7ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
Loading…
Reference in New Issue