diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index 570e9f9dd..d6edcf1aa 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -21,6 +21,11 @@ internal static class EditorPrefKeys internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl"; internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride"; + internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath"; + internal const string PackageDeployLastBackupPath = "MCPForUnity.PackageDeploy.LastBackupPath"; + internal const string PackageDeployLastTargetPath = "MCPForUnity.PackageDeploy.LastTargetPath"; + internal const string PackageDeployLastSourcePath = "MCPForUnity.PackageDeploy.LastSourcePath"; + internal const string ServerSrc = "MCPForUnity.ServerSrc"; internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer"; internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig"; diff --git a/MCPForUnity/Editor/Services/IPackageDeploymentService.cs b/MCPForUnity/Editor/Services/IPackageDeploymentService.cs new file mode 100644 index 000000000..743834c01 --- /dev/null +++ b/MCPForUnity/Editor/Services/IPackageDeploymentService.cs @@ -0,0 +1,29 @@ +using System; + +namespace MCPForUnity.Editor.Services +{ + public interface IPackageDeploymentService + { + string GetStoredSourcePath(); + void SetStoredSourcePath(string path); + void ClearStoredSourcePath(); + + string GetTargetPath(); + string GetTargetDisplayPath(); + + string GetLastBackupPath(); + bool HasBackup(); + + PackageDeploymentResult DeployFromStoredSource(); + PackageDeploymentResult RestoreLastBackup(); + } + + public class PackageDeploymentResult + { + public bool Success { get; set; } + public string Message { get; set; } + public string SourcePath { get; set; } + public string TargetPath { get; set; } + public string BackupPath { get; set; } + } +} diff --git a/MCPForUnity/Editor/Services/IPackageDeploymentService.cs.meta b/MCPForUnity/Editor/Services/IPackageDeploymentService.cs.meta new file mode 100644 index 000000000..d1bf12d24 --- /dev/null +++ b/MCPForUnity/Editor/Services/IPackageDeploymentService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c7a6f1ce6cd4a8c8a3b5d58d4b760a2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/MCPForUnity/Editor/Services/MCPServiceLocator.cs index d537182f0..ae3d98fbd 100644 --- a/MCPForUnity/Editor/Services/MCPServiceLocator.cs +++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs @@ -19,6 +19,7 @@ public static class MCPServiceLocator private static IToolDiscoveryService _toolDiscoveryService; private static IServerManagementService _serverManagementService; private static TransportManager _transportManager; + private static IPackageDeploymentService _packageDeploymentService; public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService(); public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService(); @@ -29,6 +30,7 @@ public static class MCPServiceLocator public static IToolDiscoveryService ToolDiscovery => _toolDiscoveryService ??= new ToolDiscoveryService(); public static IServerManagementService Server => _serverManagementService ??= new ServerManagementService(); public static TransportManager TransportManager => _transportManager ??= new TransportManager(); + public static IPackageDeploymentService Deployment => _packageDeploymentService ??= new PackageDeploymentService(); /// /// Registers a custom implementation for a service (useful for testing) @@ -53,6 +55,8 @@ public static void Register(T implementation) where T : class _toolDiscoveryService = td; else if (implementation is IServerManagementService sm) _serverManagementService = sm; + else if (implementation is IPackageDeploymentService pd) + _packageDeploymentService = pd; else if (implementation is TransportManager tm) _transportManager = tm; } @@ -71,6 +75,7 @@ public static void Reset() (_toolDiscoveryService as IDisposable)?.Dispose(); (_serverManagementService as IDisposable)?.Dispose(); (_transportManager as IDisposable)?.Dispose(); + (_packageDeploymentService as IDisposable)?.Dispose(); _bridgeService = null; _clientService = null; @@ -81,6 +86,7 @@ public static void Reset() _toolDiscoveryService = null; _serverManagementService = null; _transportManager = null; + _packageDeploymentService = null; } } } diff --git a/MCPForUnity/Editor/Services/PackageDeploymentService.cs b/MCPForUnity/Editor/Services/PackageDeploymentService.cs new file mode 100644 index 000000000..3024ded38 --- /dev/null +++ b/MCPForUnity/Editor/Services/PackageDeploymentService.cs @@ -0,0 +1,303 @@ +using System; +using System.IO; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Helpers; +using UnityEditor; +using UnityEngine; +using PackageInfo = UnityEditor.PackageManager.PackageInfo; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Handles copying a local MCPForUnity folder into the current project's package location with backup/restore support. + /// + public class PackageDeploymentService : IPackageDeploymentService + { + private const string BackupRootFolderName = "MCPForUnityDeployBackups"; + + public string GetStoredSourcePath() + { + return EditorPrefs.GetString(EditorPrefKeys.PackageDeploySourcePath, string.Empty); + } + + public void SetStoredSourcePath(string path) + { + ValidateSource(path); + EditorPrefs.SetString(EditorPrefKeys.PackageDeploySourcePath, Path.GetFullPath(path)); + } + + public void ClearStoredSourcePath() + { + EditorPrefs.DeleteKey(EditorPrefKeys.PackageDeploySourcePath); + } + + public string GetTargetPath() + { + // Prefer Package Manager resolved path for the installed package + var packageInfo = PackageInfo.FindForAssembly(typeof(PackageDeploymentService).Assembly); + if (packageInfo != null) + { + if (!string.IsNullOrEmpty(packageInfo.resolvedPath) && Directory.Exists(packageInfo.resolvedPath)) + { + return packageInfo.resolvedPath; + } + + if (!string.IsNullOrEmpty(packageInfo.assetPath)) + { + string absoluteFromAsset = MakeAbsolute(packageInfo.assetPath); + if (Directory.Exists(absoluteFromAsset)) + { + return absoluteFromAsset; + } + } + } + + // Fallback to computed package root + string packageRoot = AssetPathUtility.GetMcpPackageRootPath(); + if (!string.IsNullOrEmpty(packageRoot)) + { + string absolutePath = MakeAbsolute(packageRoot); + if (Directory.Exists(absolutePath)) + { + return absolutePath; + } + } + + return null; + } + + public string GetTargetDisplayPath() + { + string target = GetTargetPath(); + return string.IsNullOrEmpty(target) + ? "Not found (check Packages/manifest.json)" + : target; + } + + public string GetLastBackupPath() + { + return EditorPrefs.GetString(EditorPrefKeys.PackageDeployLastBackupPath, string.Empty); + } + + public bool HasBackup() + { + string path = GetLastBackupPath(); + return !string.IsNullOrEmpty(path) && Directory.Exists(path); + } + + public PackageDeploymentResult DeployFromStoredSource() + { + string sourcePath = GetStoredSourcePath(); + if (string.IsNullOrEmpty(sourcePath)) + { + return Fail("Select a MCPForUnity folder first."); + } + + string validationError = ValidateSource(sourcePath, throwOnError: false); + if (!string.IsNullOrEmpty(validationError)) + { + return Fail(validationError); + } + + string targetPath = GetTargetPath(); + if (string.IsNullOrEmpty(targetPath)) + { + return Fail("Could not locate the installed MCP package. Check Packages/manifest.json."); + } + + if (PathsEqual(sourcePath, targetPath)) + { + return Fail("Source and target are the same. Choose a different MCPForUnity folder."); + } + + try + { + EditorUtility.DisplayProgressBar("Deploy MCP for Unity", "Creating backup...", 0.25f); + string backupPath = CreateBackup(targetPath); + + EditorUtility.DisplayProgressBar("Deploy MCP for Unity", "Replacing package contents...", 0.7f); + CopyCoreFolders(sourcePath, targetPath); + + EditorPrefs.SetString(EditorPrefKeys.PackageDeployLastBackupPath, backupPath); + EditorPrefs.SetString(EditorPrefKeys.PackageDeployLastTargetPath, targetPath); + EditorPrefs.SetString(EditorPrefKeys.PackageDeployLastSourcePath, sourcePath); + + AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); + return Success("Deployment completed.", sourcePath, targetPath, backupPath); + } + catch (Exception ex) + { + McpLog.Error($"Deployment failed: {ex.Message}"); + return Fail($"Deployment failed: {ex.Message}"); + } + finally + { + EditorUtility.ClearProgressBar(); + } + } + + public PackageDeploymentResult RestoreLastBackup() + { + string backupPath = GetLastBackupPath(); + string targetPath = EditorPrefs.GetString(EditorPrefKeys.PackageDeployLastTargetPath, string.Empty); + + if (string.IsNullOrEmpty(backupPath) || !Directory.Exists(backupPath)) + { + return Fail("No backup available to restore."); + } + + if (string.IsNullOrEmpty(targetPath) || !Directory.Exists(targetPath)) + { + targetPath = GetTargetPath(); + } + + if (string.IsNullOrEmpty(targetPath) || !Directory.Exists(targetPath)) + { + return Fail("Could not locate target package path."); + } + + try + { + EditorUtility.DisplayProgressBar("Restore MCP for Unity", "Restoring backup...", 0.5f); + ReplaceDirectory(backupPath, targetPath); + + AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate); + return Success("Restore completed.", null, targetPath, backupPath); + } + catch (Exception ex) + { + McpLog.Error($"Restore failed: {ex.Message}"); + return Fail($"Restore failed: {ex.Message}"); + } + finally + { + EditorUtility.ClearProgressBar(); + } + } + + private void CopyCoreFolders(string sourceRoot, string targetRoot) + { + string sourceEditor = Path.Combine(sourceRoot, "Editor"); + string sourceRuntime = Path.Combine(sourceRoot, "Runtime"); + + ReplaceDirectory(sourceEditor, Path.Combine(targetRoot, "Editor")); + ReplaceDirectory(sourceRuntime, Path.Combine(targetRoot, "Runtime")); + } + + private static void ReplaceDirectory(string source, string destination) + { + if (Directory.Exists(destination)) + { + FileUtil.DeleteFileOrDirectory(destination); + } + + FileUtil.CopyFileOrDirectory(source, destination); + } + + private string CreateBackup(string targetPath) + { + string backupRoot = Path.Combine(GetProjectRoot(), "Library", BackupRootFolderName); + Directory.CreateDirectory(backupRoot); + + string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + string backupPath = Path.Combine(backupRoot, $"backup_{stamp}"); + + if (Directory.Exists(backupPath)) + { + FileUtil.DeleteFileOrDirectory(backupPath); + } + + FileUtil.CopyFileOrDirectory(targetPath, backupPath); + return backupPath; + } + + private static string ValidateSource(string sourcePath, bool throwOnError = true) + { + if (string.IsNullOrEmpty(sourcePath)) + { + if (throwOnError) + { + throw new ArgumentException("Source path cannot be empty."); + } + + return "Source path is empty."; + } + + if (!Directory.Exists(sourcePath)) + { + if (throwOnError) + { + throw new ArgumentException("Selected folder does not exist."); + } + + return "Selected folder does not exist."; + } + + bool hasEditor = Directory.Exists(Path.Combine(sourcePath, "Editor")); + bool hasRuntime = Directory.Exists(Path.Combine(sourcePath, "Runtime")); + + if (!hasEditor || !hasRuntime) + { + string message = "Folder must contain Editor and Runtime subfolders."; + if (throwOnError) + { + throw new ArgumentException(message); + } + + return message; + } + + return null; + } + + private static string MakeAbsolute(string assetPath) + { + assetPath = assetPath.Replace('\\', '/'); + + if (assetPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + return Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath)); + } + + if (assetPath.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase)) + { + return Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath)); + } + + return Path.GetFullPath(assetPath); + } + + private static string GetProjectRoot() + { + return Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + } + + private static bool PathsEqual(string a, string b) + { + string normA = Path.GetFullPath(a).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + string normB = Path.GetFullPath(b).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return string.Equals(normA, normB, StringComparison.OrdinalIgnoreCase); + } + + private static PackageDeploymentResult Success(string message, string source, string target, string backup) + { + return new PackageDeploymentResult + { + Success = true, + Message = message, + SourcePath = source, + TargetPath = target, + BackupPath = backup + }; + } + + private static PackageDeploymentResult Fail(string message) + { + return new PackageDeploymentResult + { + Success = false, + Message = message + }; + } + } +} diff --git a/MCPForUnity/Editor/Services/PackageDeploymentService.cs.meta b/MCPForUnity/Editor/Services/PackageDeploymentService.cs.meta new file mode 100644 index 000000000..e43e0ccf8 --- /dev/null +++ b/MCPForUnity/Editor/Services/PackageDeploymentService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0b1f45e4e5d24413a6f1c8c0d8c5f2f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Windows/Components/Settings/McpSettingsSection.cs b/MCPForUnity/Editor/Windows/Components/Settings/McpSettingsSection.cs index 55a4c6657..1489304e9 100644 --- a/MCPForUnity/Editor/Windows/Components/Settings/McpSettingsSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Settings/McpSettingsSection.cs @@ -29,6 +29,14 @@ public class McpSettingsSection private VisualElement uvxPathStatus; private TextField gitUrlOverride; private Button clearGitUrlButton; + private TextField deploySourcePath; + private Button browseDeploySourceButton; + private Button clearDeploySourceButton; + private Button deployButton; + private Button deployRestoreButton; + private Label deployTargetLabel; + private Label deployBackupLabel; + private Label deployStatusLabel; // Data private ValidationLevel currentValidationLevel = ValidationLevel.Standard; @@ -69,6 +77,14 @@ private void CacheUIElements() uvxPathStatus = Root.Q("uv-path-status"); gitUrlOverride = Root.Q("git-url-override"); clearGitUrlButton = Root.Q