using System.IO;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Manages package lifecycle events including first-time installation,
/// version updates, and legacy installation detection.
/// Consolidates the functionality of PackageInstaller and PackageDetector.
/// </summary>
[InitializeOnLoad]
public static class PackageLifecycleManager
{
private const string VersionKeyPrefix = "MCPForUnity.InstalledVersion:";
private const string LegacyInstallFlagKey = "MCPForUnity.ServerInstalled"; // For migration
private const string InstallErrorKeyPrefix = "MCPForUnity.InstallError:"; // Stores last installation error
static PackageLifecycleManager()
{
// Schedule the check for after Unity is fully loaded
EditorApplication.delayCall += CheckAndInstallServer;
}
private static void CheckAndInstallServer()
{
try
{
string currentVersion = GetPackageVersion();
string versionKey = VersionKeyPrefix + currentVersion;
bool hasRunForThisVersion = EditorPrefs.GetBool(versionKey, false);
// Check for conditions that require installation/verification
bool isFirstTimeInstall = !EditorPrefs.HasKey(LegacyInstallFlagKey) && !hasRunForThisVersion;
bool legacyPresent = LegacyRootsExist();
bool canonicalMissing = !File.Exists(
Path.Combine(ServerInstaller.GetServerPath(), "server.py")
);
// Run if: first install, version update, legacy detected, or canonical missing
if (isFirstTimeInstall || !hasRunForThisVersion || legacyPresent || canonicalMissing)
{
PerformInstallation(currentVersion, versionKey, isFirstTimeInstall);
}
}
catch (System.Exception ex)
{
McpLog.Info($"Package lifecycle check failed: {ex.Message}. Open Window > MCP For Unity if needed.", always: false);
}
}
private static void PerformInstallation(string version, string versionKey, bool isFirstTimeInstall)
{
string error = null;
try
{
ServerInstaller.EnsureServerInstalled();
// Mark as installed for this version
EditorPrefs.SetBool(versionKey, true);
// Migrate legacy flag if this is first time
if (isFirstTimeInstall)
{
EditorPrefs.SetBool(LegacyInstallFlagKey, true);
}
// Clean up old version keys (keep only current version)
CleanupOldVersionKeys(version);
// Clean up legacy preference keys
CleanupLegacyPrefs();
// Only log success if server was actually embedded and copied
if (ServerInstaller.HasEmbeddedServer() && isFirstTimeInstall)
{
McpLog.Info("MCP server installation completed successfully.");
}
}
catch (System.Exception ex)
{
error = ex.Message;
// Store the error for display in the UI, but don't mark as handled
// This allows the user to manually rebuild via the "Rebuild Server" button
string errorKey = InstallErrorKeyPrefix + version;
EditorPrefs.SetString(errorKey, ex.Message ?? "Unknown error");
// Don't mark as installed - user needs to manually rebuild
}
if (!string.IsNullOrEmpty(error))
{
McpLog.Info($"Server installation failed: {error}. Use Window > MCP For Unity > Rebuild Server to retry.", always: false);
}
}
private static string GetPackageVersion()
{
try
{
var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(
typeof(PackageLifecycleManager).Assembly
);
if (info != null && !string.IsNullOrEmpty(info.version))
{
return info.version;
}
}
catch { }
// Fallback to embedded server version
return GetEmbeddedServerVersion();
}
private static string GetEmbeddedServerVersion()
{
try
{
if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
{
var versionPath = Path.Combine(embeddedSrc, "server_version.txt");
if (File.Exists(versionPath))
{
return File.ReadAllText(versionPath)?.Trim() ?? "unknown";
}
}
}
catch { }
return "unknown";
}
private static bool LegacyRootsExist()
{
try
{
string home = System.Environment.GetFolderPath(
System.Environment.SpecialFolder.UserProfile
) ?? string.Empty;
string[] legacyRoots =
{
Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
};
foreach (var root in legacyRoots)
{
try
{
if (File.Exists(Path.Combine(root, "server.py")))
{
return true;
}
}
catch { }
}
}
catch { }
return false;
}
private static void CleanupOldVersionKeys(string currentVersion)
{
try
{
// Get all EditorPrefs keys that start with our version prefix
// Note: Unity doesn't provide a way to enumerate all keys, so we can only
// clean up known legacy keys. Future versions will be cleaned up when
// a newer version runs.
// This is a best-effort cleanup.
}
catch { }
}
private static void CleanupLegacyPrefs()
{
try
{
// Clean up old preference keys that are no longer used
string[] legacyKeys =
{
"MCPForUnity.ServerSrc",
"MCPForUnity.PythonDirOverride",
"MCPForUnity.LegacyDetectLogged" // Old prefix without version
};
foreach (var key in legacyKeys)
{
try
{
if (EditorPrefs.HasKey(key))
{
EditorPrefs.DeleteKey(key);
}
}
catch { }
}
}
catch { }
}
/// <summary>
/// Gets the last installation error for the current package version, if any.
/// Returns null if there was no error or the error has been cleared.
/// </summary>
public static string GetLastInstallError()
{
try
{
string currentVersion = GetPackageVersion();
string errorKey = InstallErrorKeyPrefix + currentVersion;
if (EditorPrefs.HasKey(errorKey))
{
return EditorPrefs.GetString(errorKey, null);
}
}
catch { }
return null;
}
/// <summary>
/// Clears the last installation error. Should be called after a successful manual rebuild.
/// </summary>
public static void ClearLastInstallError()
{
try
{
string currentVersion = GetPackageVersion();
string errorKey = InstallErrorKeyPrefix + currentVersion;
if (EditorPrefs.HasKey(errorKey))
{
EditorPrefs.DeleteKey(errorKey);
}
}
catch { }
}
}
}