It's time to let go, all dev for the plugin happens in MCPForUnity (#365)
parent
e4904ad0d7
commit
5e4b554e70
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 31e7fac5858840340a75cc6df0ad3d9e
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: be61633e00d934610ac1ff8192ffbe3d
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: e59036660cc33d24596fbbf6d4657a83
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: de8f5721c34f7194392e9d8c7d0226c0
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
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",
|
|
||||||
},
|
|
||||||
// Trae IDE
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
name = "Trae",
|
|
||||||
windowsConfigPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
||||||
"Trae",
|
|
||||||
"mcp.json"
|
|
||||||
),
|
|
||||||
macConfigPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
"Library",
|
|
||||||
"Application Support",
|
|
||||||
"Trae",
|
|
||||||
"mcp.json"
|
|
||||||
),
|
|
||||||
linuxConfigPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
".config",
|
|
||||||
"Trae",
|
|
||||||
"mcp.json"
|
|
||||||
),
|
|
||||||
mcpType = McpTypes.Trae,
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
// 4) Codex CLI
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
name = "Codex CLI",
|
|
||||||
windowsConfigPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
".codex",
|
|
||||||
"config.toml"
|
|
||||||
),
|
|
||||||
macConfigPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
".codex",
|
|
||||||
"config.toml"
|
|
||||||
),
|
|
||||||
linuxConfigPath = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
||||||
".codex",
|
|
||||||
"config.toml"
|
|
||||||
),
|
|
||||||
mcpType = McpTypes.Codex,
|
|
||||||
configStatus = "Not Configured",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize status enums after construction
|
|
||||||
public McpClients()
|
|
||||||
{
|
|
||||||
foreach (var client in clients)
|
|
||||||
{
|
|
||||||
if (client.configStatus == "Not Configured")
|
|
||||||
{
|
|
||||||
client.status = McpStatus.NotConfigured;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 711b86bbc1f661e4fb2c822e14970e16
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 221a4d6e595be6897a5b17b77aedd4d0
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
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>
|
|
||||||
/// 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/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: f6789012345678901234abcdef012345
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: b2c3d4e5f6789012345678901234abcd
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 789012345678901234abcdef01234567
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
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}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 6789012345678901234abcdef0123456
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: bdbaced669d14798a4ceeebfbff2b22c
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 9012345678901234abcdef0123456789
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
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 : PlatformDetectorBase
|
|
||||||
{
|
|
||||||
public override string PlatformName => "Linux";
|
|
||||||
|
|
||||||
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
|
||||||
|
|
||||||
public override 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 override string GetPythonInstallUrl()
|
|
||||||
{
|
|
||||||
return "https://www.python.org/downloads/source/";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string GetUVInstallUrl()
|
|
||||||
{
|
|
||||||
return "https://docs.astral.sh/uv/getting-started/installation/#linux";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override 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.";
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (Python 4+ or Python 3.10+)
|
|
||||||
if (TryParseVersion(version, out var major, out var minor))
|
|
||||||
{
|
|
||||||
return major > 3 || (major >= 3 && minor >= 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 2345678901234abcdef0123456789abc
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
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 : PlatformDetectorBase
|
|
||||||
{
|
|
||||||
public override string PlatformName => "macOS";
|
|
||||||
|
|
||||||
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
|
||||||
|
|
||||||
public override 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 override string GetPythonInstallUrl()
|
|
||||||
{
|
|
||||||
return "https://www.python.org/downloads/macos/";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string GetUVInstallUrl()
|
|
||||||
{
|
|
||||||
return "https://docs.astral.sh/uv/getting-started/installation/#macos";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override 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.";
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (Python 4+ or Python 3.10+)
|
|
||||||
if (TryParseVersion(version, out var major, out var minor))
|
|
||||||
{
|
|
||||||
return major > 3 || (major >= 3 && minor >= 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 12345678901234abcdef0123456789ab
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using MCPForUnity.Editor.Dependencies.Models;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for platform-specific dependency detection
|
|
||||||
/// </summary>
|
|
||||||
public abstract class PlatformDetectorBase : IPlatformDetector
|
|
||||||
{
|
|
||||||
public abstract string PlatformName { get; }
|
|
||||||
public abstract bool CanDetect { get; }
|
|
||||||
|
|
||||||
public abstract DependencyStatus DetectPython();
|
|
||||||
public abstract string GetPythonInstallUrl();
|
|
||||||
public abstract string GetUVInstallUrl();
|
|
||||||
public abstract string GetInstallationRecommendations();
|
|
||||||
|
|
||||||
public virtual 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 virtual 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 44d715aedea2b8b41bf914433bbb2c49
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,191 +0,0 @@
|
||||||
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 : PlatformDetectorBase
|
|
||||||
{
|
|
||||||
public override string PlatformName => "Windows";
|
|
||||||
|
|
||||||
public override bool CanDetect => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
||||||
|
|
||||||
public override 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 override string GetPythonInstallUrl()
|
|
||||||
{
|
|
||||||
return "https://apps.microsoft.com/store/detail/python-313/9NCVDN91XZQP";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string GetUVInstallUrl()
|
|
||||||
{
|
|
||||||
return "https://docs.astral.sh/uv/getting-started/installation/#windows";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override 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";
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (Python 4+ or Python 3.10+)
|
|
||||||
if (TryParseVersion(version, out var major, out var minor))
|
|
||||||
{
|
|
||||||
return major > 3 || (major >= 3 && minor >= 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 012345678901234abcdef0123456789a
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: c11944bcfb9ec4576bab52874b7df584
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: ea652131dcdaa44ca8cb35cd1191be3f
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 94cb070dc5e15024da86150b27699ca0
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Helpers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides common utility methods for working with Unity asset paths.
|
|
||||||
/// </summary>
|
|
||||||
public static class AssetPathUtility
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes a Unity asset path by ensuring forward slashes are used and that it is rooted under "Assets/".
|
|
||||||
/// </summary>
|
|
||||||
public static string SanitizeAssetPath(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
path = path.Replace('\\', '/');
|
|
||||||
if (!path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return "Assets/" + path.TrimStart('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 1d42f5b5ea5d4d43ad1a771e14bda2a0
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,349 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using MCPForUnity.External.Tommy;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Helpers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Codex CLI specific configuration helpers. Handles TOML snippet
|
|
||||||
/// generation and lightweight parsing so Codex can join the auto-setup
|
|
||||||
/// flow alongside JSON-based clients.
|
|
||||||
/// </summary>
|
|
||||||
public static class CodexConfigHelper
|
|
||||||
{
|
|
||||||
public static bool IsCodexConfigured(string pythonDir)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string basePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
||||||
if (string.IsNullOrEmpty(basePath)) return false;
|
|
||||||
|
|
||||||
string configPath = Path.Combine(basePath, ".codex", "config.toml");
|
|
||||||
if (!File.Exists(configPath)) return false;
|
|
||||||
|
|
||||||
string toml = File.ReadAllText(configPath);
|
|
||||||
if (!TryParseCodexServer(toml, out _, out var args)) return false;
|
|
||||||
|
|
||||||
string dir = McpConfigFileHelper.ExtractDirectoryArg(args);
|
|
||||||
if (string.IsNullOrEmpty(dir)) return false;
|
|
||||||
|
|
||||||
return McpConfigFileHelper.PathsEqual(dir, pythonDir);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string BuildCodexServerBlock(string uvPath, string serverSrc)
|
|
||||||
{
|
|
||||||
string argsArray = FormatTomlStringArray(new[] { "run", "--directory", serverSrc, "server.py" });
|
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
|
||||||
sb.AppendLine("[mcp_servers.unityMCP]");
|
|
||||||
sb.AppendLine($"command = \"{EscapeTomlString(uvPath)}\"");
|
|
||||||
sb.AppendLine($"args = {argsArray}");
|
|
||||||
sb.AppendLine($"startup_timeout_sec = 30");
|
|
||||||
|
|
||||||
// Windows-specific environment block to help Codex locate needed paths
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
|
||||||
{
|
|
||||||
string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
|
||||||
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; // Roaming
|
|
||||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
|
|
||||||
string programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) ?? string.Empty;
|
|
||||||
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) ?? string.Empty;
|
|
||||||
string systemDrive = Environment.GetEnvironmentVariable("SystemDrive") ?? (Path.GetPathRoot(userProfile)?.TrimEnd('\\', '/') ?? "C:");
|
|
||||||
string systemRoot = Environment.GetEnvironmentVariable("SystemRoot") ?? Path.Combine(systemDrive + "\\", "Windows");
|
|
||||||
string comspec = Environment.GetEnvironmentVariable("COMSPEC") ?? Path.Combine(Environment.SystemDirectory ?? (systemRoot + "\\System32"), "cmd.exe");
|
|
||||||
string homeDrive = Environment.GetEnvironmentVariable("HOMEDRIVE");
|
|
||||||
string homePath = Environment.GetEnvironmentVariable("HOMEPATH");
|
|
||||||
if (string.IsNullOrEmpty(homeDrive))
|
|
||||||
{
|
|
||||||
homeDrive = systemDrive;
|
|
||||||
}
|
|
||||||
if (string.IsNullOrEmpty(homePath) && !string.IsNullOrEmpty(userProfile))
|
|
||||||
{
|
|
||||||
// Derive HOMEPATH from USERPROFILE (e.g., C:\\Users\\name -> \\Users\\name)
|
|
||||||
if (userProfile.StartsWith(homeDrive + "\\", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
homePath = userProfile.Substring(homeDrive.Length);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var root = Path.GetPathRoot(userProfile) ?? string.Empty; // e.g., C:\\
|
|
||||||
homePath = userProfile.Substring(root.Length - 1); // keep leading backslash
|
|
||||||
}
|
|
||||||
catch { homePath = "\\"; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string powershell = Path.Combine(Environment.SystemDirectory ?? (systemRoot + "\\System32"), "WindowsPowerShell\\v1.0\\powershell.exe");
|
|
||||||
string pwsh = Path.Combine(programFiles, "PowerShell\\7\\pwsh.exe");
|
|
||||||
|
|
||||||
string tempDir = Path.Combine(localAppData, "Temp");
|
|
||||||
|
|
||||||
sb.AppendLine();
|
|
||||||
sb.AppendLine("[mcp_servers.unityMCP.env]");
|
|
||||||
sb.AppendLine($"SystemRoot = \"{EscapeTomlString(systemRoot)}\"");
|
|
||||||
sb.AppendLine($"APPDATA = \"{EscapeTomlString(appData)}\"");
|
|
||||||
sb.AppendLine($"COMSPEC = \"{EscapeTomlString(comspec)}\"");
|
|
||||||
sb.AppendLine($"HOMEDRIVE = \"{EscapeTomlString(homeDrive?.TrimEnd('\\') ?? string.Empty)}\"");
|
|
||||||
sb.AppendLine($"HOMEPATH = \"{EscapeTomlString(homePath ?? string.Empty)}\"");
|
|
||||||
sb.AppendLine($"LOCALAPPDATA = \"{EscapeTomlString(localAppData)}\"");
|
|
||||||
sb.AppendLine($"POWERSHELL = \"{EscapeTomlString(powershell)}\"");
|
|
||||||
sb.AppendLine($"PROGRAMDATA = \"{EscapeTomlString(programData)}\"");
|
|
||||||
sb.AppendLine($"PROGRAMFILES = \"{EscapeTomlString(programFiles)}\"");
|
|
||||||
sb.AppendLine($"PWSH = \"{EscapeTomlString(pwsh)}\"");
|
|
||||||
sb.AppendLine($"SYSTEMDRIVE = \"{EscapeTomlString(systemDrive)}\"");
|
|
||||||
sb.AppendLine($"SYSTEMROOT = \"{EscapeTomlString(systemRoot)}\"");
|
|
||||||
sb.AppendLine($"TEMP = \"{EscapeTomlString(tempDir)}\"");
|
|
||||||
sb.AppendLine($"TMP = \"{EscapeTomlString(tempDir)}\"");
|
|
||||||
sb.AppendLine($"USERPROFILE = \"{EscapeTomlString(userProfile)}\"");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { /* best effort */ }
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string UpsertCodexServerBlock(string existingToml, string newBlock)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(existingToml))
|
|
||||||
{
|
|
||||||
// Default to snake_case section when creating new files
|
|
||||||
return newBlock.TrimEnd() + Environment.NewLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
using StringReader reader = new StringReader(existingToml);
|
|
||||||
string line;
|
|
||||||
bool inTarget = false;
|
|
||||||
bool replaced = false;
|
|
||||||
|
|
||||||
// Support both TOML section casings and nested subtables (e.g., env)
|
|
||||||
// Prefer the casing already present in the user's file; fall back to snake_case
|
|
||||||
bool hasCamelSection = existingToml.IndexOf("[mcpServers.unityMCP]", StringComparison.OrdinalIgnoreCase) >= 0
|
|
||||||
|| existingToml.IndexOf("[mcpServers.unityMCP.", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
||||||
bool hasSnakeSection = existingToml.IndexOf("[mcp_servers.unityMCP]", StringComparison.OrdinalIgnoreCase) >= 0
|
|
||||||
|| existingToml.IndexOf("[mcp_servers.unityMCP.", StringComparison.OrdinalIgnoreCase) >= 0;
|
|
||||||
bool preferCamel = hasCamelSection || (!hasSnakeSection && existingToml.IndexOf("[mcpServers]", StringComparison.OrdinalIgnoreCase) >= 0);
|
|
||||||
|
|
||||||
// Prepare block variants matching the chosen casing, including nested tables
|
|
||||||
string newBlockCamel = newBlock
|
|
||||||
.Replace("[mcp_servers.unityMCP.env]", "[mcpServers.unityMCP.env]")
|
|
||||||
.Replace("[mcp_servers.unityMCP]", "[mcpServers.unityMCP]");
|
|
||||||
string newBlockEffective = preferCamel ? newBlockCamel : newBlock;
|
|
||||||
|
|
||||||
static bool IsSection(string s)
|
|
||||||
{
|
|
||||||
string t = s.Trim();
|
|
||||||
return t.StartsWith("[") && t.EndsWith("]") && !t.StartsWith("[[");
|
|
||||||
}
|
|
||||||
|
|
||||||
static string SectionName(string header)
|
|
||||||
{
|
|
||||||
string t = header.Trim();
|
|
||||||
if (t.StartsWith("[") && t.EndsWith("]")) t = t.Substring(1, t.Length - 2);
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TargetOrChild(string section)
|
|
||||||
{
|
|
||||||
// Compare case-insensitively; accept both snake and camel as the same logical table
|
|
||||||
string name = SectionName(section);
|
|
||||||
return name.StartsWith("mcp_servers.unityMCP", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| name.StartsWith("mcpServers.unityMCP", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
while ((line = reader.ReadLine()) != null)
|
|
||||||
{
|
|
||||||
string trimmed = line.Trim();
|
|
||||||
bool isSection = IsSection(trimmed);
|
|
||||||
if (isSection)
|
|
||||||
{
|
|
||||||
// If we encounter the target section or any of its nested tables, mark/keep in-target
|
|
||||||
if (TargetOrChild(trimmed))
|
|
||||||
{
|
|
||||||
if (!replaced)
|
|
||||||
{
|
|
||||||
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
|
|
||||||
sb.AppendLine(newBlockEffective.TrimEnd());
|
|
||||||
replaced = true;
|
|
||||||
}
|
|
||||||
inTarget = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A new unrelated section ends the target region
|
|
||||||
if (inTarget)
|
|
||||||
{
|
|
||||||
inTarget = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inTarget)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.AppendLine(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!replaced)
|
|
||||||
{
|
|
||||||
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
|
|
||||||
sb.AppendLine(newBlockEffective.TrimEnd());
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString().TrimEnd() + Environment.NewLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryParseCodexServer(string toml, out string command, out string[] args)
|
|
||||||
{
|
|
||||||
command = null;
|
|
||||||
args = null;
|
|
||||||
if (string.IsNullOrWhiteSpace(toml)) return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var reader = new StringReader(toml);
|
|
||||||
TomlTable root = TOML.Parse(reader);
|
|
||||||
if (root == null) return false;
|
|
||||||
|
|
||||||
if (!TryGetTable(root, "mcp_servers", out var servers)
|
|
||||||
&& !TryGetTable(root, "mcpServers", out servers))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryGetTable(servers, "unityMCP", out var unity))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
command = GetTomlString(unity, "command");
|
|
||||||
args = GetTomlStringArray(unity, "args");
|
|
||||||
|
|
||||||
return !string.IsNullOrEmpty(command) && args != null;
|
|
||||||
}
|
|
||||||
catch (TomlParseException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (TomlSyntaxException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (FormatException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetTable(TomlTable parent, string key, out TomlTable table)
|
|
||||||
{
|
|
||||||
table = null;
|
|
||||||
if (parent == null) return false;
|
|
||||||
|
|
||||||
if (parent.TryGetNode(key, out var node))
|
|
||||||
{
|
|
||||||
if (node is TomlTable tbl)
|
|
||||||
{
|
|
||||||
table = tbl;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node is TomlArray array)
|
|
||||||
{
|
|
||||||
var firstTable = array.Children.OfType<TomlTable>().FirstOrDefault();
|
|
||||||
if (firstTable != null)
|
|
||||||
{
|
|
||||||
table = firstTable;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetTomlString(TomlTable table, string key)
|
|
||||||
{
|
|
||||||
if (table != null && table.TryGetNode(key, out var node))
|
|
||||||
{
|
|
||||||
if (node is TomlString str) return str.Value;
|
|
||||||
if (node.HasValue) return node.ToString();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string[] GetTomlStringArray(TomlTable table, string key)
|
|
||||||
{
|
|
||||||
if (table == null) return null;
|
|
||||||
if (!table.TryGetNode(key, out var node)) return null;
|
|
||||||
|
|
||||||
if (node is TomlArray array)
|
|
||||||
{
|
|
||||||
List<string> values = new List<string>();
|
|
||||||
foreach (TomlNode element in array.Children)
|
|
||||||
{
|
|
||||||
if (element is TomlString str)
|
|
||||||
{
|
|
||||||
values.Add(str.Value);
|
|
||||||
}
|
|
||||||
else if (element.HasValue)
|
|
||||||
{
|
|
||||||
values.Add(element.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return values.Count > 0 ? values.ToArray() : Array.Empty<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node is TomlString single)
|
|
||||||
{
|
|
||||||
return new[] { single.Value };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatTomlStringArray(IEnumerable<string> values)
|
|
||||||
{
|
|
||||||
if (values == null) return "[]";
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.Append('[');
|
|
||||||
bool first = true;
|
|
||||||
foreach (string value in values)
|
|
||||||
{
|
|
||||||
if (!first)
|
|
||||||
{
|
|
||||||
sb.Append(", ");
|
|
||||||
}
|
|
||||||
sb.Append('"').Append(EscapeTomlString(value ?? string.Empty)).Append('"');
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
sb.Append(']');
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string EscapeTomlString(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
|
||||||
return value
|
|
||||||
.Replace("\\", "\\\\")
|
|
||||||
.Replace("\"", "\\\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: b3e68082ffc0b4cd39d3747673a4cc22
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 5c07c3369f73943919d9e086a81d1dcc
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,278 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 8f2b7b3e9c3e4a0f9b2a1d4c7e6f5a12
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,528 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 64b8ff807bc9a401c82015cbafccffac
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using UnityEditor;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Helpers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Shared helpers for reading and writing MCP client configuration files.
|
|
||||||
/// Consolidates file atomics and server directory resolution so the editor
|
|
||||||
/// window can focus on UI concerns only.
|
|
||||||
/// </summary>
|
|
||||||
public static class McpConfigFileHelper
|
|
||||||
{
|
|
||||||
public static string ExtractDirectoryArg(string[] args)
|
|
||||||
{
|
|
||||||
if (args == null) return null;
|
|
||||||
for (int i = 0; i < args.Length - 1; i++)
|
|
||||||
{
|
|
||||||
if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return args[i + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool PathsEqual(string a, string b)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string na = Path.GetFullPath(a.Trim());
|
|
||||||
string nb = Path.GetFullPath(b.Trim());
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
return string.Equals(na, nb, StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolves the server directory to use for MCP tools, preferring
|
|
||||||
/// existing config values and falling back to installed/embedded copies.
|
|
||||||
/// </summary>
|
|
||||||
public static string ResolveServerDirectory(string pythonDir, string[] existingArgs)
|
|
||||||
{
|
|
||||||
string serverSrc = ExtractDirectoryArg(existingArgs);
|
|
||||||
bool serverValid = !string.IsNullOrEmpty(serverSrc)
|
|
||||||
&& File.Exists(Path.Combine(serverSrc, "server.py"));
|
|
||||||
if (!serverValid)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(pythonDir)
|
|
||||||
&& File.Exists(Path.Combine(pythonDir, "server.py")))
|
|
||||||
{
|
|
||||||
serverSrc = pythonDir;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
serverSrc = ResolveServerSource();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc))
|
|
||||||
{
|
|
||||||
string norm = serverSrc.Replace('\\', '/');
|
|
||||||
int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
|
|
||||||
if (idx >= 0)
|
|
||||||
{
|
|
||||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
|
|
||||||
string suffix = norm.Substring(idx + "/.local/share/".Length);
|
|
||||||
serverSrc = Path.Combine(home, "Library", "Application Support", suffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Ignore failures and fall back to the original path.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
|
||||||
&& !string.IsNullOrEmpty(serverSrc)
|
|
||||||
&& serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
|
|
||||||
&& !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
|
|
||||||
{
|
|
||||||
serverSrc = ServerInstaller.GetServerPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
return serverSrc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void WriteAtomicFile(string path, string contents)
|
|
||||||
{
|
|
||||||
string tmp = path + ".tmp";
|
|
||||||
string backup = path + ".backup";
|
|
||||||
bool writeDone = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.WriteAllText(tmp, contents, new UTF8Encoding(false));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Replace(tmp, path, backup);
|
|
||||||
writeDone = true;
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
File.Move(tmp, path);
|
|
||||||
writeDone = true;
|
|
||||||
}
|
|
||||||
catch (PlatformNotSupportedException)
|
|
||||||
{
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(backup)) File.Delete(backup);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
File.Move(path, backup);
|
|
||||||
}
|
|
||||||
File.Move(tmp, path);
|
|
||||||
writeDone = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!writeDone && File.Exists(backup))
|
|
||||||
{
|
|
||||||
try { File.Copy(backup, path, true); } catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
|
|
||||||
try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ResolveServerSource()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string remembered = EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty);
|
|
||||||
if (!string.IsNullOrEmpty(remembered)
|
|
||||||
&& File.Exists(Path.Combine(remembered, "server.py")))
|
|
||||||
{
|
|
||||||
return remembered;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerInstaller.EnsureServerInstalled();
|
|
||||||
string installed = ServerInstaller.GetServerPath();
|
|
||||||
if (File.Exists(Path.Combine(installed, "server.py")))
|
|
||||||
{
|
|
||||||
return installed;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool useEmbedded = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false);
|
|
||||||
if (useEmbedded
|
|
||||||
&& ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
|
|
||||||
&& File.Exists(Path.Combine(embedded, "server.py")))
|
|
||||||
{
|
|
||||||
return embedded;
|
|
||||||
}
|
|
||||||
|
|
||||||
return installed;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return ServerInstaller.GetServerPath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: f69ad468942b74c0ea24e3e8e5f21a4b
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,297 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
using MCPForUnity.Editor.Dependencies;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
using MCPForUnity.Editor.Models;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Helpers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Shared helper for MCP client configuration management with sophisticated
|
|
||||||
/// logic for preserving existing configs and handling different client types
|
|
||||||
/// </summary>
|
|
||||||
public static class McpConfigurationHelper
|
|
||||||
{
|
|
||||||
private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes MCP configuration to the specified path using sophisticated logic
|
|
||||||
/// that preserves existing configuration and only writes when necessary
|
|
||||||
/// </summary>
|
|
||||||
public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null)
|
|
||||||
{
|
|
||||||
// 0) Respect explicit lock (hidden pref or UI toggle)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
|
|
||||||
return "Skipped (locked)";
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
|
|
||||||
|
|
||||||
// Read existing config if it exists
|
|
||||||
string existingJson = "{}";
|
|
||||||
if (File.Exists(configPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
existingJson = File.ReadAllText(configPath);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"Error reading existing config: {e.Message}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the existing JSON while preserving all properties
|
|
||||||
dynamic existingConfig;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(existingJson))
|
|
||||||
{
|
|
||||||
existingConfig = new JObject();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object
|
|
||||||
if (!string.IsNullOrWhiteSpace(existingJson))
|
|
||||||
{
|
|
||||||
Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block.");
|
|
||||||
}
|
|
||||||
existingConfig = new JObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine existing entry references (command/args)
|
|
||||||
string existingCommand = null;
|
|
||||||
string[] existingArgs = null;
|
|
||||||
bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (isVSCode)
|
|
||||||
{
|
|
||||||
existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
|
|
||||||
existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject<string[]>();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
|
|
||||||
existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject<string[]>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
// 1) Start from existing, only fill gaps (prefer trusted resolver)
|
|
||||||
string uvPath = ServerInstaller.FindUvPath();
|
|
||||||
// Optionally trust existingCommand if it looks like uv/uv.exe
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
|
|
||||||
if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
|
|
||||||
{
|
|
||||||
uvPath = existingCommand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
if (uvPath == null) return "UV package manager not found. Please install UV first.";
|
|
||||||
string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
|
|
||||||
|
|
||||||
// 2) Canonical args order
|
|
||||||
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
|
|
||||||
|
|
||||||
// 3) Only write if changed
|
|
||||||
bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
|
|
||||||
|| !ArgsEqual(existingArgs, newArgs);
|
|
||||||
if (!changed)
|
|
||||||
{
|
|
||||||
return "Configured successfully"; // nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Ensure containers exist and write back minimal changes
|
|
||||||
JObject existingRoot;
|
|
||||||
if (existingConfig is JObject eo)
|
|
||||||
existingRoot = eo;
|
|
||||||
else
|
|
||||||
existingRoot = JObject.FromObject(existingConfig);
|
|
||||||
|
|
||||||
existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);
|
|
||||||
|
|
||||||
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
|
|
||||||
|
|
||||||
McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
|
|
||||||
EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
return "Configured successfully";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configures a Codex client with sophisticated TOML handling
|
|
||||||
/// </summary>
|
|
||||||
public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
|
|
||||||
return "Skipped (locked)";
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
string existingToml = string.Empty;
|
|
||||||
if (File.Exists(configPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
existingToml = File.ReadAllText(configPath);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}");
|
|
||||||
existingToml = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string existingCommand = null;
|
|
||||||
string[] existingArgs = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(existingToml))
|
|
||||||
{
|
|
||||||
CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
string uvPath = ServerInstaller.FindUvPath();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
|
|
||||||
if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
|
|
||||||
{
|
|
||||||
uvPath = existingCommand;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
if (uvPath == null)
|
|
||||||
{
|
|
||||||
return "UV package manager not found. Please install UV first.";
|
|
||||||
}
|
|
||||||
|
|
||||||
string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
|
|
||||||
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
|
|
||||||
|
|
||||||
bool changed = true;
|
|
||||||
if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null)
|
|
||||||
{
|
|
||||||
changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
|
|
||||||
|| !ArgsEqual(existingArgs, newArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!changed)
|
|
||||||
{
|
|
||||||
return "Configured successfully";
|
|
||||||
}
|
|
||||||
|
|
||||||
string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc);
|
|
||||||
string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock);
|
|
||||||
|
|
||||||
McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
|
|
||||||
EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
return "Configured successfully";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates UV binary by running --version command
|
|
||||||
/// </summary>
|
|
||||||
private static bool IsValidUvBinary(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!File.Exists(path)) return false;
|
|
||||||
var psi = new System.Diagnostics.ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = path,
|
|
||||||
Arguments = "--version",
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
using var p = System.Diagnostics.Process.Start(psi);
|
|
||||||
if (p == null) return false;
|
|
||||||
if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
|
|
||||||
if (p.ExitCode != 0) return false;
|
|
||||||
string output = p.StandardOutput.ReadToEnd().Trim();
|
|
||||||
return output.StartsWith("uv ");
|
|
||||||
}
|
|
||||||
catch { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Compares two string arrays for equality
|
|
||||||
/// </summary>
|
|
||||||
private static bool ArgsEqual(string[] a, string[] b)
|
|
||||||
{
|
|
||||||
if (a == null || b == null) return a == b;
|
|
||||||
if (a.Length != b.Length) return false;
|
|
||||||
for (int i = 0; i < a.Length; i++)
|
|
||||||
{
|
|
||||||
if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the appropriate config file path for the given MCP client based on OS
|
|
||||||
/// </summary>
|
|
||||||
public static string GetClientConfigPath(McpClient mcpClient)
|
|
||||||
{
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
return mcpClient.windowsConfigPath;
|
|
||||||
}
|
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
||||||
{
|
|
||||||
return string.IsNullOrEmpty(mcpClient.macConfigPath)
|
|
||||||
? mcpClient.linuxConfigPath
|
|
||||||
: mcpClient.macConfigPath;
|
|
||||||
}
|
|
||||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
{
|
|
||||||
return mcpClient.linuxConfigPath;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return mcpClient.linuxConfigPath; // fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates the directory for the config file if it doesn't exist
|
|
||||||
/// </summary>
|
|
||||||
public static void EnsureConfigDirectoryExists(string configPath)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: e45ac2a13b4c1ba468b8e3aa67b292ca
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
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>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 9e2c3f8a4f4f48d8a4c1b7b8e3f5a1c2
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Helpers
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Shared helper for resolving Python server directory paths with support for
|
|
||||||
/// development mode, embedded servers, and installed packages
|
|
||||||
/// </summary>
|
|
||||||
public static class McpPathResolver
|
|
||||||
{
|
|
||||||
private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolves the Python server directory path with comprehensive logic
|
|
||||||
/// including development mode support and fallback mechanisms
|
|
||||||
/// </summary>
|
|
||||||
public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
|
|
||||||
{
|
|
||||||
string pythonDir = McpConfigFileHelper.ResolveServerSource();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Only check dev paths if we're using a file-based package (development mode)
|
|
||||||
bool isDevelopmentMode = IsDevelopmentMode();
|
|
||||||
if (isDevelopmentMode)
|
|
||||||
{
|
|
||||||
string currentPackagePath = Path.GetDirectoryName(Application.dataPath);
|
|
||||||
string[] devPaths = {
|
|
||||||
Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"),
|
|
||||||
Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"),
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (string devPath in devPaths)
|
|
||||||
{
|
|
||||||
if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py")))
|
|
||||||
{
|
|
||||||
if (debugLogsEnabled)
|
|
||||||
{
|
|
||||||
Debug.Log($"Currently in development mode. Package: {devPath}");
|
|
||||||
}
|
|
||||||
return devPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve via shared helper (handles local registry and older fallback) only if dev override on
|
|
||||||
if (EditorPrefs.GetBool(USE_EMBEDDED_SERVER_KEY, false))
|
|
||||||
{
|
|
||||||
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
|
|
||||||
{
|
|
||||||
return embedded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log only if the resolved path does not actually contain server.py
|
|
||||||
if (debugLogsEnabled)
|
|
||||||
{
|
|
||||||
bool hasServer = false;
|
|
||||||
try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { }
|
|
||||||
if (!hasServer)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.LogError($"Error finding package path: {e.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return pythonDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if the current Unity project is in development mode
|
|
||||||
/// (i.e., the package is referenced as a local file path in manifest.json)
|
|
||||||
/// </summary>
|
|
||||||
private static bool IsDevelopmentMode()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Only treat as development if manifest explicitly references a local file path for the package
|
|
||||||
string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");
|
|
||||||
if (!File.Exists(manifestPath)) return false;
|
|
||||||
|
|
||||||
string manifestContent = File.ReadAllText(manifestPath);
|
|
||||||
// Look specifically for our package dependency set to a file: URL
|
|
||||||
// This avoids auto-enabling dev mode just because a repo exists elsewhere on disk
|
|
||||||
if (manifestContent.IndexOf("\"com.coplaydev.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
||||||
{
|
|
||||||
int idx = manifestContent.IndexOf("com.coplaydev.unity-mcp", StringComparison.OrdinalIgnoreCase);
|
|
||||||
// Crude but effective: check for "file:" in the same line/value
|
|
||||||
if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0
|
|
||||||
&& manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the appropriate PATH prepend for the current platform when running external processes
|
|
||||||
/// </summary>
|
|
||||||
public static string GetPathPrepend()
|
|
||||||
{
|
|
||||||
if (Application.platform == RuntimePlatform.OSXEditor)
|
|
||||||
return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
|
||||||
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
|
||||||
return "/usr/local/bin:/usr/bin:/bin";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 2c76f0c7ff138ba4a952481e04bc3974
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: b82eaef548d164ca095f17db64d15af8
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
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 Window for instructions.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 19e6eaa637484e9fa19f9a0459809de2
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 28c39813a10b4331afc764a04089cbef
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 80c09a76b944f8c4691e06c4d76c4be8
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,700 +0,0 @@
|
||||||
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 RebuildMcpServer()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Find embedded source
|
|
||||||
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
|
|
||||||
{
|
|
||||||
Debug.LogError("RebuildMcpServer: Could not find embedded server source.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string saveLocation = GetSaveLocation();
|
|
||||||
string destRoot = Path.Combine(saveLocation, ServerFolder);
|
|
||||||
string destSrc = Path.Combine(destRoot, "src");
|
|
||||||
|
|
||||||
// Kill any running uv processes for this server
|
|
||||||
TryKillUvForPath(destSrc);
|
|
||||||
|
|
||||||
// Delete the entire installed server directory
|
|
||||||
if (Directory.Exists(destRoot))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.Delete(destRoot, recursive: true);
|
|
||||||
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Deleted existing server at {destRoot}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogError($"Failed to delete existing server: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-copy from embedded source
|
|
||||||
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc;
|
|
||||||
Directory.CreateDirectory(destRoot);
|
|
||||||
CopyDirectoryRecursive(embeddedRoot, destRoot);
|
|
||||||
|
|
||||||
// Write version file
|
|
||||||
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"Failed to write version file: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Server rebuilt successfully at {destRoot} (version {embeddedVer})");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogError($"RebuildMcpServer 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 5862c6a6d0a914f4d83224f8d039cf7b
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
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)
|
|
||||||
{
|
|
||||||
// 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))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Secondary: scan all registered packages locally
|
|
||||||
foreach (var p in UnityEditor.PackageManager.PackageInfo.GetAllRegisteredPackages())
|
|
||||||
{
|
|
||||||
if (TryResolveWithinPackage(p, out srcPath))
|
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
const string CurrentId = "com.coplaydev.unity-mcp";
|
|
||||||
|
|
||||||
srcPath = null;
|
|
||||||
if (p == null || p.name != CurrentId)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: a4d1d7c2b1e94b3f8a7d9c6e5f403a21
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: b8f3c2d1e7a94f6c8a9b5e3d2c1a0f9e
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: f8514fd42f23cb641a36e52550825b35
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 98f702da6ca044be59a864a9419c4eab
|
|
||||||
AssemblyDefinitionImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 96dc847eb7f7a45e0b91241db934a4be
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 16d3ab36890b6c14f9afeabee30e03e3
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 6754c84e5deb74749bc3a19e0c9aa280
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 5fae9d995f514e9498e9613e2cdbeca9
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Models
|
|
||||||
{
|
|
||||||
[Serializable]
|
|
||||||
public class McpConfigServers
|
|
||||||
{
|
|
||||||
[JsonProperty("unityMCP")]
|
|
||||||
public McpConfigServer unityMCP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: bcb583553e8173b49be71a5c43bd9502
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: b1afa56984aec0d41808edcebf805e6a
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Models
|
|
||||||
{
|
|
||||||
[Serializable]
|
|
||||||
public class McpConfig
|
|
||||||
{
|
|
||||||
[JsonProperty("mcpServers")]
|
|
||||||
public McpConfigServers mcpServers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: c17c09908f0c1524daa8b6957ce1f7f5
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: aa63057c9e5282d4887352578bf49971
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
namespace MCPForUnity.Editor.Models
|
|
||||||
{
|
|
||||||
public enum McpTypes
|
|
||||||
{
|
|
||||||
ClaudeCode,
|
|
||||||
ClaudeDesktop,
|
|
||||||
Codex,
|
|
||||||
Cursor,
|
|
||||||
Kiro,
|
|
||||||
VSCode,
|
|
||||||
Windsurf,
|
|
||||||
Trae,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: e4e45386fcc282249907c2e3c7e5d9c6
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 600c9cb20c329d761bfa799158a87bac
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
using System;
|
|
||||||
using MCPForUnity.Editor.Dependencies;
|
|
||||||
using MCPForUnity.Editor.Dependencies.Models;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
using MCPForUnity.Editor.Windows;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Setup
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Handles automatic triggering of the setup wizard
|
|
||||||
/// </summary>
|
|
||||||
[InitializeOnLoad]
|
|
||||||
public static class SetupWizard
|
|
||||||
{
|
|
||||||
private const string SETUP_COMPLETED_KEY = "MCPForUnity.SetupCompleted";
|
|
||||||
private const string SETUP_DISMISSED_KEY = "MCPForUnity.SetupDismissed";
|
|
||||||
private static bool _hasCheckedThisSession = false;
|
|
||||||
|
|
||||||
static SetupWizard()
|
|
||||||
{
|
|
||||||
// Skip in batch mode
|
|
||||||
if (Application.isBatchMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Show setup wizard on package import
|
|
||||||
EditorApplication.delayCall += CheckSetupNeeded;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check if setup wizard should be shown
|
|
||||||
/// </summary>
|
|
||||||
private static void CheckSetupNeeded()
|
|
||||||
{
|
|
||||||
if (_hasCheckedThisSession)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_hasCheckedThisSession = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Check if setup was already completed or dismissed in previous sessions
|
|
||||||
bool setupCompleted = EditorPrefs.GetBool(SETUP_COMPLETED_KEY, false);
|
|
||||||
bool setupDismissed = EditorPrefs.GetBool(SETUP_DISMISSED_KEY, false);
|
|
||||||
|
|
||||||
// Only show setup wizard if it hasn't been completed or dismissed before
|
|
||||||
if (!(setupCompleted || setupDismissed))
|
|
||||||
{
|
|
||||||
McpLog.Info("Package imported - showing setup wizard", always: false);
|
|
||||||
|
|
||||||
var dependencyResult = DependencyManager.CheckAllDependencies();
|
|
||||||
EditorApplication.delayCall += () => ShowSetupWizard(dependencyResult);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
McpLog.Info("Setup wizard skipped - previously completed or dismissed", always: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
McpLog.Error($"Error checking setup status: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Show the setup wizard window
|
|
||||||
/// </summary>
|
|
||||||
public static void ShowSetupWizard(DependencyCheckResult dependencyResult = null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
dependencyResult ??= DependencyManager.CheckAllDependencies();
|
|
||||||
SetupWizardWindow.ShowWindow(dependencyResult);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
McpLog.Error($"Error showing setup wizard: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Mark setup as completed
|
|
||||||
/// </summary>
|
|
||||||
public static void MarkSetupCompleted()
|
|
||||||
{
|
|
||||||
EditorPrefs.SetBool(SETUP_COMPLETED_KEY, true);
|
|
||||||
McpLog.Info("Setup marked as completed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Mark setup as dismissed
|
|
||||||
/// </summary>
|
|
||||||
public static void MarkSetupDismissed()
|
|
||||||
{
|
|
||||||
EditorPrefs.SetBool(SETUP_DISMISSED_KEY, true);
|
|
||||||
McpLog.Info("Setup marked as dismissed");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Force show setup wizard (for manual invocation)
|
|
||||||
/// </summary>
|
|
||||||
[MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)]
|
|
||||||
public static void ShowSetupWizardManual()
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
|
|
||||||
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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Open MCP Client Configuration window
|
|
||||||
/// </summary>
|
|
||||||
[MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)]
|
|
||||||
public static void OpenClientConfiguration()
|
|
||||||
{
|
|
||||||
Windows.MCPForUnityEditorWindow.ShowWindow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 345678901234abcdef0123456789abcd
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,736 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using MCPForUnity.Editor.Data;
|
|
||||||
using MCPForUnity.Editor.Dependencies;
|
|
||||||
using MCPForUnity.Editor.Dependencies.Models;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
using MCPForUnity.Editor.Models;
|
|
||||||
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 McpClients _mcpClients;
|
|
||||||
private int _selectedClientIndex = 0;
|
|
||||||
|
|
||||||
private readonly string[] _stepTitles = {
|
|
||||||
"Setup",
|
|
||||||
"Configure",
|
|
||||||
"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();
|
|
||||||
}
|
|
||||||
|
|
||||||
_mcpClients = new McpClients();
|
|
||||||
|
|
||||||
// Check client configurations on startup
|
|
||||||
foreach (var client in _mcpClients.clients)
|
|
||||||
{
|
|
||||||
CheckClientConfiguration(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnGUI()
|
|
||||||
{
|
|
||||||
DrawHeader();
|
|
||||||
DrawProgressBar();
|
|
||||||
|
|
||||||
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
|
|
||||||
|
|
||||||
switch (_currentStep)
|
|
||||||
{
|
|
||||||
case 0: DrawSetupStep(); break;
|
|
||||||
case 1: DrawConfigureStep(); break;
|
|
||||||
case 2: 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 DrawSetupStep()
|
|
||||||
{
|
|
||||||
// Welcome section
|
|
||||||
DrawSectionTitle("MCP for Unity Setup");
|
|
||||||
|
|
||||||
EditorGUILayout.LabelField(
|
|
||||||
"This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.",
|
|
||||||
EditorStyles.wordWrappedLabel
|
|
||||||
);
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
|
|
||||||
// Dependency check section
|
|
||||||
EditorGUILayout.BeginHorizontal();
|
|
||||||
DrawSectionTitle("System Check", 14);
|
|
||||||
GUILayout.FlexibleSpace();
|
|
||||||
if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20)))
|
|
||||||
{
|
|
||||||
_dependencyResult = DependencyManager.CheckAllDependencies();
|
|
||||||
}
|
|
||||||
EditorGUILayout.EndHorizontal();
|
|
||||||
|
|
||||||
// Show simplified dependency status
|
|
||||||
foreach (var dep in _dependencyResult.Dependencies)
|
|
||||||
{
|
|
||||||
DrawSimpleDependencyStatus(dep);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overall status and installation guidance
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
if (!_dependencyResult.IsSystemReady)
|
|
||||||
{
|
|
||||||
// Only show critical warnings when dependencies are actually missing
|
|
||||||
EditorGUILayout.HelpBox(
|
|
||||||
"⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.",
|
|
||||||
MessageType.Warning
|
|
||||||
);
|
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
||||||
DrawErrorStatus("Installation Required");
|
|
||||||
|
|
||||||
var recommendations = DependencyManager.GetInstallationRecommendations();
|
|
||||||
EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel);
|
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
if (GUILayout.Button("Open Installation Links", GUILayout.Height(25)))
|
|
||||||
{
|
|
||||||
OpenInstallationUrls();
|
|
||||||
}
|
|
||||||
EditorGUILayout.EndVertical();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DrawSuccessStatus("System Ready");
|
|
||||||
EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void DrawCompleteStep()
|
|
||||||
{
|
|
||||||
DrawSectionTitle("Setup Complete");
|
|
||||||
|
|
||||||
// Refresh dependency check with caching to avoid heavy operations on every repaint
|
|
||||||
if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
|
|
||||||
{
|
|
||||||
_dependencyResult = DependencyManager.CheckAllDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_dependencyResult.IsSystemReady)
|
|
||||||
{
|
|
||||||
DrawSuccessStatus("MCP for Unity Ready!");
|
|
||||||
|
|
||||||
EditorGUILayout.HelpBox(
|
|
||||||
"🎉 MCP for Unity is now set up and ready to use!\n\n" +
|
|
||||||
"• Dependencies verified\n" +
|
|
||||||
"• MCP server ready\n" +
|
|
||||||
"• Client configuration accessible",
|
|
||||||
MessageType.Info
|
|
||||||
);
|
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
EditorGUILayout.BeginHorizontal();
|
|
||||||
if (GUILayout.Button("Documentation", GUILayout.Height(30)))
|
|
||||||
{
|
|
||||||
Application.OpenURL("https://github.com/CoplayDev/unity-mcp");
|
|
||||||
}
|
|
||||||
if (GUILayout.Button("Client Settings", GUILayout.Height(30)))
|
|
||||||
{
|
|
||||||
Windows.MCPForUnityEditorWindow.ShowWindow();
|
|
||||||
}
|
|
||||||
EditorGUILayout.EndHorizontal();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DrawErrorStatus("Setup Incomplete - Package Non-Functional");
|
|
||||||
|
|
||||||
EditorGUILayout.HelpBox(
|
|
||||||
"🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" +
|
|
||||||
"Install ALL required dependencies before the package will function.",
|
|
||||||
MessageType.Error
|
|
||||||
);
|
|
||||||
|
|
||||||
var missingDeps = _dependencyResult.GetMissingRequired();
|
|
||||||
if (missingDeps.Count > 0)
|
|
||||||
{
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel);
|
|
||||||
foreach (var dep in missingDeps)
|
|
||||||
{
|
|
||||||
EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
|
|
||||||
{
|
|
||||||
_currentStep = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods for consistent UI components
|
|
||||||
private void DrawSectionTitle(string title, int fontSize = 16)
|
|
||||||
{
|
|
||||||
var titleStyle = new GUIStyle(EditorStyles.boldLabel)
|
|
||||||
{
|
|
||||||
fontSize = fontSize,
|
|
||||||
fontStyle = FontStyle.Bold
|
|
||||||
};
|
|
||||||
EditorGUILayout.LabelField(title, titleStyle);
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSuccessStatus(string message)
|
|
||||||
{
|
|
||||||
var originalColor = GUI.color;
|
|
||||||
GUI.color = Color.green;
|
|
||||||
EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel);
|
|
||||||
GUI.color = originalColor;
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawErrorStatus(string message)
|
|
||||||
{
|
|
||||||
var originalColor = GUI.color;
|
|
||||||
GUI.color = Color.red;
|
|
||||||
EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel);
|
|
||||||
GUI.color = originalColor;
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSimpleDependencyStatus(DependencyStatus dep)
|
|
||||||
{
|
|
||||||
EditorGUILayout.BeginHorizontal();
|
|
||||||
|
|
||||||
var statusIcon = dep.IsAvailable ? "✓" : "✗";
|
|
||||||
var statusColor = dep.IsAvailable ? Color.green : Color.red;
|
|
||||||
|
|
||||||
var originalColor = GUI.color;
|
|
||||||
GUI.color = statusColor;
|
|
||||||
GUILayout.Label(statusIcon, GUILayout.Width(20));
|
|
||||||
EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel);
|
|
||||||
GUI.color = originalColor;
|
|
||||||
|
|
||||||
if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage))
|
|
||||||
{
|
|
||||||
EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorGUILayout.EndHorizontal();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawConfigureStep()
|
|
||||||
{
|
|
||||||
DrawSectionTitle("AI Client Configuration");
|
|
||||||
|
|
||||||
// Check dependencies first (with caching to avoid heavy operations on every repaint)
|
|
||||||
if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
|
|
||||||
{
|
|
||||||
_dependencyResult = DependencyManager.CheckAllDependencies();
|
|
||||||
}
|
|
||||||
if (!_dependencyResult.IsSystemReady)
|
|
||||||
{
|
|
||||||
DrawErrorStatus("Cannot Configure - System Requirements Not Met");
|
|
||||||
|
|
||||||
EditorGUILayout.HelpBox(
|
|
||||||
"Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.",
|
|
||||||
MessageType.Warning
|
|
||||||
);
|
|
||||||
|
|
||||||
if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
|
|
||||||
{
|
|
||||||
_currentStep = 0;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorGUILayout.LabelField(
|
|
||||||
"Configure your AI assistants to work with Unity. Select a client below to set it up:",
|
|
||||||
EditorStyles.wordWrappedLabel
|
|
||||||
);
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
|
|
||||||
// Client selection and configuration
|
|
||||||
if (_mcpClients.clients.Count > 0)
|
|
||||||
{
|
|
||||||
// Client selector dropdown
|
|
||||||
string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray();
|
|
||||||
EditorGUI.BeginChangeCheck();
|
|
||||||
_selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames);
|
|
||||||
if (EditorGUI.EndChangeCheck())
|
|
||||||
{
|
|
||||||
_selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1);
|
|
||||||
// Refresh client status when selection changes
|
|
||||||
CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
|
|
||||||
var selectedClient = _mcpClients.clients[_selectedClientIndex];
|
|
||||||
DrawClientConfigurationInWizard(selectedClient);
|
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
|
|
||||||
// Batch configuration option
|
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
||||||
EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel);
|
|
||||||
EditorGUILayout.LabelField(
|
|
||||||
"Automatically configure all detected AI clients at once:",
|
|
||||||
EditorStyles.wordWrappedLabel
|
|
||||||
);
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
|
|
||||||
if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30)))
|
|
||||||
{
|
|
||||||
ConfigureAllClientsInWizard();
|
|
||||||
}
|
|
||||||
EditorGUILayout.EndVertical();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
EditorGUILayout.HelpBox(
|
|
||||||
"💡 You might need to restart your AI client after configuring.",
|
|
||||||
MessageType.Info
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawFooter()
|
|
||||||
{
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
EditorGUILayout.BeginHorizontal();
|
|
||||||
|
|
||||||
// Back button
|
|
||||||
GUI.enabled = _currentStep > 0;
|
|
||||||
if (GUILayout.Button("Back", GUILayout.Width(60)))
|
|
||||||
{
|
|
||||||
_currentStep--;
|
|
||||||
}
|
|
||||||
|
|
||||||
GUILayout.FlexibleSpace();
|
|
||||||
|
|
||||||
// Skip button
|
|
||||||
if (GUILayout.Button("Skip", GUILayout.Width(60)))
|
|
||||||
{
|
|
||||||
bool dismiss = EditorUtility.DisplayDialog(
|
|
||||||
"Skip Setup",
|
|
||||||
"⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" +
|
|
||||||
"You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)",
|
|
||||||
"Skip Anyway",
|
|
||||||
"Cancel"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dismiss)
|
|
||||||
{
|
|
||||||
SetupWizard.MarkSetupDismissed();
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next/Done button
|
|
||||||
GUI.enabled = true;
|
|
||||||
string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next";
|
|
||||||
|
|
||||||
if (GUILayout.Button(buttonText, GUILayout.Width(80)))
|
|
||||||
{
|
|
||||||
if (_currentStep == _stepTitles.Length - 1)
|
|
||||||
{
|
|
||||||
SetupWizard.MarkSetupCompleted();
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_currentStep++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GUI.enabled = true;
|
|
||||||
EditorGUILayout.EndHorizontal();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawClientConfigurationInWizard(McpClient client)
|
|
||||||
{
|
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
||||||
|
|
||||||
EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel);
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
|
|
||||||
// Show current status
|
|
||||||
var statusColor = GetClientStatusColor(client);
|
|
||||||
var originalColor = GUI.color;
|
|
||||||
GUI.color = statusColor;
|
|
||||||
EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label);
|
|
||||||
GUI.color = originalColor;
|
|
||||||
|
|
||||||
EditorGUILayout.Space();
|
|
||||||
|
|
||||||
// Configuration buttons
|
|
||||||
EditorGUILayout.BeginHorizontal();
|
|
||||||
|
|
||||||
if (client.mcpType == McpTypes.ClaudeCode)
|
|
||||||
{
|
|
||||||
// Special handling for Claude Code
|
|
||||||
bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude());
|
|
||||||
if (claudeAvailable)
|
|
||||||
{
|
|
||||||
bool isConfigured = client.status == McpStatus.Configured;
|
|
||||||
string buttonText = isConfigured ? "Unregister" : "Register";
|
|
||||||
if (GUILayout.Button($"{buttonText} with Claude Code"))
|
|
||||||
{
|
|
||||||
if (isConfigured)
|
|
||||||
{
|
|
||||||
UnregisterFromClaudeCode(client);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RegisterWithClaudeCode(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning);
|
|
||||||
if (GUILayout.Button("Open Claude Code Website"))
|
|
||||||
{
|
|
||||||
Application.OpenURL("https://claude.ai/download");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Standard client configuration
|
|
||||||
if (GUILayout.Button($"Configure {client.name}"))
|
|
||||||
{
|
|
||||||
ConfigureClientInWizard(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GUILayout.Button("Manual Setup"))
|
|
||||||
{
|
|
||||||
ShowManualSetupInWizard(client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorGUILayout.EndHorizontal();
|
|
||||||
EditorGUILayout.EndVertical();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Color GetClientStatusColor(McpClient client)
|
|
||||||
{
|
|
||||||
return client.status switch
|
|
||||||
{
|
|
||||||
McpStatus.Configured => Color.green,
|
|
||||||
McpStatus.Running => Color.green,
|
|
||||||
McpStatus.Connected => Color.green,
|
|
||||||
McpStatus.IncorrectPath => Color.yellow,
|
|
||||||
McpStatus.CommunicationError => Color.yellow,
|
|
||||||
McpStatus.NoResponse => Color.yellow,
|
|
||||||
_ => Color.red
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfigureClientInWizard(McpClient client)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string result = PerformClientConfiguration(client);
|
|
||||||
|
|
||||||
EditorUtility.DisplayDialog(
|
|
||||||
$"{client.name} Configuration",
|
|
||||||
result,
|
|
||||||
"OK"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refresh client status
|
|
||||||
CheckClientConfiguration(client);
|
|
||||||
Repaint();
|
|
||||||
}
|
|
||||||
catch (System.Exception ex)
|
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog(
|
|
||||||
"Configuration Error",
|
|
||||||
$"Failed to configure {client.name}: {ex.Message}",
|
|
||||||
"OK"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfigureAllClientsInWizard()
|
|
||||||
{
|
|
||||||
int successCount = 0;
|
|
||||||
int totalCount = _mcpClients.clients.Count;
|
|
||||||
|
|
||||||
foreach (var client in _mcpClients.clients)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (client.mcpType == McpTypes.ClaudeCode)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured)
|
|
||||||
{
|
|
||||||
RegisterWithClaudeCode(client);
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
else if (client.status == McpStatus.Configured)
|
|
||||||
{
|
|
||||||
successCount++; // Already configured
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
string result = PerformClientConfiguration(client);
|
|
||||||
if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
successCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckClientConfiguration(client);
|
|
||||||
}
|
|
||||||
catch (System.Exception ex)
|
|
||||||
{
|
|
||||||
McpLog.Error($"Failed to configure {client.name}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorUtility.DisplayDialog(
|
|
||||||
"Batch Configuration Complete",
|
|
||||||
$"Successfully configured {successCount} out of {totalCount} clients.\n\n" +
|
|
||||||
"Restart your AI clients for changes to take effect.",
|
|
||||||
"OK"
|
|
||||||
);
|
|
||||||
|
|
||||||
Repaint();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RegisterWithClaudeCode(McpClient client)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string pythonDir = McpPathResolver.FindPackagePythonDirectory();
|
|
||||||
string claudePath = ExecPath.ResolveClaude();
|
|
||||||
string uvPath = ExecPath.ResolveUv() ?? "uv";
|
|
||||||
|
|
||||||
string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
|
|
||||||
|
|
||||||
if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend()))
|
|
||||||
{
|
|
||||||
if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
CheckClientConfiguration(client);
|
|
||||||
EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new System.Exception($"Registration failed: {stderr}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckClientConfiguration(client);
|
|
||||||
EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (System.Exception ex)
|
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UnregisterFromClaudeCode(McpClient client)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string claudePath = ExecPath.ResolveClaude();
|
|
||||||
if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend()))
|
|
||||||
{
|
|
||||||
CheckClientConfiguration(client);
|
|
||||||
EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new System.Exception($"Unregistration failed: {stderr}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (System.Exception ex)
|
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string PerformClientConfiguration(McpClient client)
|
|
||||||
{
|
|
||||||
// This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient
|
|
||||||
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
|
|
||||||
string pythonDir = McpPathResolver.FindPackagePythonDirectory();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(pythonDir))
|
|
||||||
{
|
|
||||||
return "Manual configuration required - Python server directory not found.";
|
|
||||||
}
|
|
||||||
|
|
||||||
McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
|
|
||||||
// Use TOML writer for Codex; JSON writer for others
|
|
||||||
if (client != null && client.mcpType == McpTypes.Codex)
|
|
||||||
{
|
|
||||||
return McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowManualSetupInWizard(McpClient client)
|
|
||||||
{
|
|
||||||
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
|
|
||||||
string pythonDir = McpPathResolver.FindPackagePythonDirectory();
|
|
||||||
string uvPath = ServerInstaller.FindUvPath();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(uvPath))
|
|
||||||
{
|
|
||||||
EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build manual configuration using the sophisticated helper logic
|
|
||||||
string result = (client != null && client.mcpType == McpTypes.Codex)
|
|
||||||
? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client)
|
|
||||||
: McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
|
|
||||||
string manualConfig;
|
|
||||||
|
|
||||||
if (result == "Configured successfully")
|
|
||||||
{
|
|
||||||
// Read back the configuration that was written
|
|
||||||
try
|
|
||||||
{
|
|
||||||
manualConfig = System.IO.File.ReadAllText(configPath);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
manualConfig = "Configuration written successfully, but could not read back for display.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
manualConfig = $"Configuration failed: {result}";
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorUtility.DisplayDialog(
|
|
||||||
$"Manual Setup - {client.name}",
|
|
||||||
$"Configuration file location:\n{configPath}\n\n" +
|
|
||||||
$"Configuration result:\n{manualConfig}",
|
|
||||||
"OK"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckClientConfiguration(McpClient client)
|
|
||||||
{
|
|
||||||
// Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
|
|
||||||
if (System.IO.File.Exists(configPath))
|
|
||||||
{
|
|
||||||
client.configStatus = "Configured";
|
|
||||||
client.status = McpStatus.Configured;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
client.configStatus = "Not Configured";
|
|
||||||
client.status = McpStatus.NotConfigured;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
client.configStatus = "Error";
|
|
||||||
client.status = McpStatus.Error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 45678901234abcdef0123456789abcde
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: c97b83a6ac92a704b864eef27c3d285b
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Tools
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Registry for all MCP command handlers via reflection.
|
|
||||||
/// </summary>
|
|
||||||
public static class CommandRegistry
|
|
||||||
{
|
|
||||||
private static readonly Dictionary<string, Func<JObject, object>> _handlers = new();
|
|
||||||
private static bool _initialized = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize and auto-discover all tools marked with [McpForUnityTool]
|
|
||||||
/// </summary>
|
|
||||||
public static void Initialize()
|
|
||||||
{
|
|
||||||
if (_initialized) return;
|
|
||||||
|
|
||||||
AutoDiscoverTools();
|
|
||||||
_initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert PascalCase or camelCase to snake_case
|
|
||||||
/// </summary>
|
|
||||||
private static string ToSnakeCase(string name)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(name)) return name;
|
|
||||||
|
|
||||||
// Insert underscore before uppercase letters (except first)
|
|
||||||
var s1 = Regex.Replace(name, "(.)([A-Z][a-z]+)", "$1_$2");
|
|
||||||
var s2 = Regex.Replace(s1, "([a-z0-9])([A-Z])", "$1_$2");
|
|
||||||
return s2.ToLower();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Auto-discover all types with [McpForUnityTool] attribute
|
|
||||||
/// </summary>
|
|
||||||
private static void AutoDiscoverTools()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var toolTypes = AppDomain.CurrentDomain.GetAssemblies()
|
|
||||||
.Where(a => !a.IsDynamic)
|
|
||||||
.SelectMany(a =>
|
|
||||||
{
|
|
||||||
try { return a.GetTypes(); }
|
|
||||||
catch { return new Type[0]; }
|
|
||||||
})
|
|
||||||
.Where(t => t.GetCustomAttribute<McpForUnityToolAttribute>() != null);
|
|
||||||
|
|
||||||
foreach (var type in toolTypes)
|
|
||||||
{
|
|
||||||
RegisterToolType(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
McpLog.Info($"Auto-discovered {_handlers.Count} tools");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
McpLog.Error($"Failed to auto-discover MCP tools: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RegisterToolType(Type type)
|
|
||||||
{
|
|
||||||
var attr = type.GetCustomAttribute<McpForUnityToolAttribute>();
|
|
||||||
|
|
||||||
// Get command name (explicit or auto-generated)
|
|
||||||
string commandName = attr.CommandName;
|
|
||||||
if (string.IsNullOrEmpty(commandName))
|
|
||||||
{
|
|
||||||
commandName = ToSnakeCase(type.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate command names
|
|
||||||
if (_handlers.ContainsKey(commandName))
|
|
||||||
{
|
|
||||||
McpLog.Warn(
|
|
||||||
$"Duplicate command name '{commandName}' detected. " +
|
|
||||||
$"Tool {type.Name} will override previously registered handler."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find HandleCommand method
|
|
||||||
var method = type.GetMethod(
|
|
||||||
"HandleCommand",
|
|
||||||
BindingFlags.Public | BindingFlags.Static,
|
|
||||||
null,
|
|
||||||
new[] { typeof(JObject) },
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
if (method == null)
|
|
||||||
{
|
|
||||||
McpLog.Warn(
|
|
||||||
$"MCP tool {type.Name} is marked with [McpForUnityTool] " +
|
|
||||||
$"but has no public static HandleCommand(JObject) method"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var handler = (Func<JObject, object>)Delegate.CreateDelegate(
|
|
||||||
typeof(Func<JObject, object>),
|
|
||||||
method
|
|
||||||
);
|
|
||||||
_handlers[commandName] = handler;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
McpLog.Error($"Failed to register tool {type.Name}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a command handler by name
|
|
||||||
/// </summary>
|
|
||||||
public static Func<JObject, object> GetHandler(string commandName)
|
|
||||||
{
|
|
||||||
if (!_handlers.TryGetValue(commandName, out var handler))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Unknown or unsupported command type: {commandName}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
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
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: de90a1d9743a2874cb235cf0b83444b1
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,645 +0,0 @@
|
||||||
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 UnityEditor.SceneManagement;
|
|
||||||
using UnityEngine;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Tools
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Handles operations related to controlling and querying the Unity Editor state,
|
|
||||||
/// including managing Tags and Layers.
|
|
||||||
/// </summary>
|
|
||||||
[McpForUnityTool("manage_editor")]
|
|
||||||
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 "get_prefab_stage":
|
|
||||||
return GetPrefabStageInfo();
|
|
||||||
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, get_prefab_stage, 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 GetPrefabStageInfo()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
||||||
if (stage == null)
|
|
||||||
{
|
|
||||||
return Response.Success
|
|
||||||
("No prefab stage is currently open.", new { isOpen = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.Success(
|
|
||||||
"Prefab stage info retrieved.",
|
|
||||||
new
|
|
||||||
{
|
|
||||||
isOpen = true,
|
|
||||||
assetPath = stage.assetPath,
|
|
||||||
prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null,
|
|
||||||
mode = stage.mode.ToString(),
|
|
||||||
isDirty = stage.scene.isDirty
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return Response.Error($"Error getting prefab stage info: {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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
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
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 7641d7388f0f6634b9d83d34de87b2ee
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue