Skip to content

Commit 79b3255

Browse files
authored
[FEATURE] Deployment of local source code to Unity (#450)
* [FEATURE] Local MCPForUnity Deployment Similar to deploy.bat, but sideload it to MCP For Unity for easier deployment inside Unity menu. * Update PackageDeploymentService.cs * Update with meta file * Updated Readme
1 parent 97b8574 commit 79b3255

File tree

12 files changed

+544
-5
lines changed

12 files changed

+544
-5
lines changed

MCPForUnity/Editor/Constants/EditorPrefKeys.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ internal static class EditorPrefKeys
2121
internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl";
2222
internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride";
2323

24+
internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath";
25+
internal const string PackageDeployLastBackupPath = "MCPForUnity.PackageDeploy.LastBackupPath";
26+
internal const string PackageDeployLastTargetPath = "MCPForUnity.PackageDeploy.LastTargetPath";
27+
internal const string PackageDeployLastSourcePath = "MCPForUnity.PackageDeploy.LastSourcePath";
28+
2429
internal const string ServerSrc = "MCPForUnity.ServerSrc";
2530
internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer";
2631
internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig";
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
3+
namespace MCPForUnity.Editor.Services
4+
{
5+
public interface IPackageDeploymentService
6+
{
7+
string GetStoredSourcePath();
8+
void SetStoredSourcePath(string path);
9+
void ClearStoredSourcePath();
10+
11+
string GetTargetPath();
12+
string GetTargetDisplayPath();
13+
14+
string GetLastBackupPath();
15+
bool HasBackup();
16+
17+
PackageDeploymentResult DeployFromStoredSource();
18+
PackageDeploymentResult RestoreLastBackup();
19+
}
20+
21+
public class PackageDeploymentResult
22+
{
23+
public bool Success { get; set; }
24+
public string Message { get; set; }
25+
public string SourcePath { get; set; }
26+
public string TargetPath { get; set; }
27+
public string BackupPath { get; set; }
28+
}
29+
}

MCPForUnity/Editor/Services/IPackageDeploymentService.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MCPForUnity/Editor/Services/MCPServiceLocator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public static class MCPServiceLocator
1919
private static IToolDiscoveryService _toolDiscoveryService;
2020
private static IServerManagementService _serverManagementService;
2121
private static TransportManager _transportManager;
22+
private static IPackageDeploymentService _packageDeploymentService;
2223

2324
public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService();
2425
public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService();
@@ -29,6 +30,7 @@ public static class MCPServiceLocator
2930
public static IToolDiscoveryService ToolDiscovery => _toolDiscoveryService ??= new ToolDiscoveryService();
3031
public static IServerManagementService Server => _serverManagementService ??= new ServerManagementService();
3132
public static TransportManager TransportManager => _transportManager ??= new TransportManager();
33+
public static IPackageDeploymentService Deployment => _packageDeploymentService ??= new PackageDeploymentService();
3234

3335
/// <summary>
3436
/// Registers a custom implementation for a service (useful for testing)
@@ -53,6 +55,8 @@ public static void Register<T>(T implementation) where T : class
5355
_toolDiscoveryService = td;
5456
else if (implementation is IServerManagementService sm)
5557
_serverManagementService = sm;
58+
else if (implementation is IPackageDeploymentService pd)
59+
_packageDeploymentService = pd;
5660
else if (implementation is TransportManager tm)
5761
_transportManager = tm;
5862
}
@@ -71,6 +75,7 @@ public static void Reset()
7175
(_toolDiscoveryService as IDisposable)?.Dispose();
7276
(_serverManagementService as IDisposable)?.Dispose();
7377
(_transportManager as IDisposable)?.Dispose();
78+
(_packageDeploymentService as IDisposable)?.Dispose();
7479

7580
_bridgeService = null;
7681
_clientService = null;
@@ -81,6 +86,7 @@ public static void Reset()
8186
_toolDiscoveryService = null;
8287
_serverManagementService = null;
8388
_transportManager = null;
89+
_packageDeploymentService = null;
8490
}
8591
}
8692
}
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
using System;
2+
using System.IO;
3+
using MCPForUnity.Editor.Constants;
4+
using MCPForUnity.Editor.Helpers;
5+
using UnityEditor;
6+
using UnityEngine;
7+
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
8+
9+
namespace MCPForUnity.Editor.Services
10+
{
11+
/// <summary>
12+
/// Handles copying a local MCPForUnity folder into the current project's package location with backup/restore support.
13+
/// </summary>
14+
public class PackageDeploymentService : IPackageDeploymentService
15+
{
16+
private const string BackupRootFolderName = "MCPForUnityDeployBackups";
17+
18+
public string GetStoredSourcePath()
19+
{
20+
return EditorPrefs.GetString(EditorPrefKeys.PackageDeploySourcePath, string.Empty);
21+
}
22+
23+
public void SetStoredSourcePath(string path)
24+
{
25+
ValidateSource(path);
26+
EditorPrefs.SetString(EditorPrefKeys.PackageDeploySourcePath, Path.GetFullPath(path));
27+
}
28+
29+
public void ClearStoredSourcePath()
30+
{
31+
EditorPrefs.DeleteKey(EditorPrefKeys.PackageDeploySourcePath);
32+
}
33+
34+
public string GetTargetPath()
35+
{
36+
// Prefer Package Manager resolved path for the installed package
37+
var packageInfo = PackageInfo.FindForAssembly(typeof(PackageDeploymentService).Assembly);
38+
if (packageInfo != null)
39+
{
40+
if (!string.IsNullOrEmpty(packageInfo.resolvedPath) && Directory.Exists(packageInfo.resolvedPath))
41+
{
42+
return packageInfo.resolvedPath;
43+
}
44+
45+
if (!string.IsNullOrEmpty(packageInfo.assetPath))
46+
{
47+
string absoluteFromAsset = MakeAbsolute(packageInfo.assetPath);
48+
if (Directory.Exists(absoluteFromAsset))
49+
{
50+
return absoluteFromAsset;
51+
}
52+
}
53+
}
54+
55+
// Fallback to computed package root
56+
string packageRoot = AssetPathUtility.GetMcpPackageRootPath();
57+
if (!string.IsNullOrEmpty(packageRoot))
58+
{
59+
string absolutePath = MakeAbsolute(packageRoot);
60+
if (Directory.Exists(absolutePath))
61+
{
62+
return absolutePath;
63+
}
64+
}
65+
66+
return null;
67+
}
68+
69+
public string GetTargetDisplayPath()
70+
{
71+
string target = GetTargetPath();
72+
return string.IsNullOrEmpty(target)
73+
? "Not found (check Packages/manifest.json)"
74+
: target;
75+
}
76+
77+
public string GetLastBackupPath()
78+
{
79+
return EditorPrefs.GetString(EditorPrefKeys.PackageDeployLastBackupPath, string.Empty);
80+
}
81+
82+
public bool HasBackup()
83+
{
84+
string path = GetLastBackupPath();
85+
return !string.IsNullOrEmpty(path) && Directory.Exists(path);
86+
}
87+
88+
public PackageDeploymentResult DeployFromStoredSource()
89+
{
90+
string sourcePath = GetStoredSourcePath();
91+
if (string.IsNullOrEmpty(sourcePath))
92+
{
93+
return Fail("Select a MCPForUnity folder first.");
94+
}
95+
96+
string validationError = ValidateSource(sourcePath, throwOnError: false);
97+
if (!string.IsNullOrEmpty(validationError))
98+
{
99+
return Fail(validationError);
100+
}
101+
102+
string targetPath = GetTargetPath();
103+
if (string.IsNullOrEmpty(targetPath))
104+
{
105+
return Fail("Could not locate the installed MCP package. Check Packages/manifest.json.");
106+
}
107+
108+
if (PathsEqual(sourcePath, targetPath))
109+
{
110+
return Fail("Source and target are the same. Choose a different MCPForUnity folder.");
111+
}
112+
113+
try
114+
{
115+
EditorUtility.DisplayProgressBar("Deploy MCP for Unity", "Creating backup...", 0.25f);
116+
string backupPath = CreateBackup(targetPath);
117+
118+
EditorUtility.DisplayProgressBar("Deploy MCP for Unity", "Replacing package contents...", 0.7f);
119+
CopyCoreFolders(sourcePath, targetPath);
120+
121+
EditorPrefs.SetString(EditorPrefKeys.PackageDeployLastBackupPath, backupPath);
122+
EditorPrefs.SetString(EditorPrefKeys.PackageDeployLastTargetPath, targetPath);
123+
EditorPrefs.SetString(EditorPrefKeys.PackageDeployLastSourcePath, sourcePath);
124+
125+
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
126+
return Success("Deployment completed.", sourcePath, targetPath, backupPath);
127+
}
128+
catch (Exception ex)
129+
{
130+
McpLog.Error($"Deployment failed: {ex.Message}");
131+
return Fail($"Deployment failed: {ex.Message}");
132+
}
133+
finally
134+
{
135+
EditorUtility.ClearProgressBar();
136+
}
137+
}
138+
139+
public PackageDeploymentResult RestoreLastBackup()
140+
{
141+
string backupPath = GetLastBackupPath();
142+
string targetPath = EditorPrefs.GetString(EditorPrefKeys.PackageDeployLastTargetPath, string.Empty);
143+
144+
if (string.IsNullOrEmpty(backupPath) || !Directory.Exists(backupPath))
145+
{
146+
return Fail("No backup available to restore.");
147+
}
148+
149+
if (string.IsNullOrEmpty(targetPath) || !Directory.Exists(targetPath))
150+
{
151+
targetPath = GetTargetPath();
152+
}
153+
154+
if (string.IsNullOrEmpty(targetPath) || !Directory.Exists(targetPath))
155+
{
156+
return Fail("Could not locate target package path.");
157+
}
158+
159+
try
160+
{
161+
EditorUtility.DisplayProgressBar("Restore MCP for Unity", "Restoring backup...", 0.5f);
162+
ReplaceDirectory(backupPath, targetPath);
163+
164+
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
165+
return Success("Restore completed.", null, targetPath, backupPath);
166+
}
167+
catch (Exception ex)
168+
{
169+
McpLog.Error($"Restore failed: {ex.Message}");
170+
return Fail($"Restore failed: {ex.Message}");
171+
}
172+
finally
173+
{
174+
EditorUtility.ClearProgressBar();
175+
}
176+
}
177+
178+
private void CopyCoreFolders(string sourceRoot, string targetRoot)
179+
{
180+
string sourceEditor = Path.Combine(sourceRoot, "Editor");
181+
string sourceRuntime = Path.Combine(sourceRoot, "Runtime");
182+
183+
ReplaceDirectory(sourceEditor, Path.Combine(targetRoot, "Editor"));
184+
ReplaceDirectory(sourceRuntime, Path.Combine(targetRoot, "Runtime"));
185+
}
186+
187+
private static void ReplaceDirectory(string source, string destination)
188+
{
189+
if (Directory.Exists(destination))
190+
{
191+
FileUtil.DeleteFileOrDirectory(destination);
192+
}
193+
194+
FileUtil.CopyFileOrDirectory(source, destination);
195+
}
196+
197+
private string CreateBackup(string targetPath)
198+
{
199+
string backupRoot = Path.Combine(GetProjectRoot(), "Library", BackupRootFolderName);
200+
Directory.CreateDirectory(backupRoot);
201+
202+
string stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
203+
string backupPath = Path.Combine(backupRoot, $"backup_{stamp}");
204+
205+
if (Directory.Exists(backupPath))
206+
{
207+
FileUtil.DeleteFileOrDirectory(backupPath);
208+
}
209+
210+
FileUtil.CopyFileOrDirectory(targetPath, backupPath);
211+
return backupPath;
212+
}
213+
214+
private static string ValidateSource(string sourcePath, bool throwOnError = true)
215+
{
216+
if (string.IsNullOrEmpty(sourcePath))
217+
{
218+
if (throwOnError)
219+
{
220+
throw new ArgumentException("Source path cannot be empty.");
221+
}
222+
223+
return "Source path is empty.";
224+
}
225+
226+
if (!Directory.Exists(sourcePath))
227+
{
228+
if (throwOnError)
229+
{
230+
throw new ArgumentException("Selected folder does not exist.");
231+
}
232+
233+
return "Selected folder does not exist.";
234+
}
235+
236+
bool hasEditor = Directory.Exists(Path.Combine(sourcePath, "Editor"));
237+
bool hasRuntime = Directory.Exists(Path.Combine(sourcePath, "Runtime"));
238+
239+
if (!hasEditor || !hasRuntime)
240+
{
241+
string message = "Folder must contain Editor and Runtime subfolders.";
242+
if (throwOnError)
243+
{
244+
throw new ArgumentException(message);
245+
}
246+
247+
return message;
248+
}
249+
250+
return null;
251+
}
252+
253+
private static string MakeAbsolute(string assetPath)
254+
{
255+
assetPath = assetPath.Replace('\\', '/');
256+
257+
if (assetPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
258+
{
259+
return Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath));
260+
}
261+
262+
if (assetPath.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase))
263+
{
264+
return Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath));
265+
}
266+
267+
return Path.GetFullPath(assetPath);
268+
}
269+
270+
private static string GetProjectRoot()
271+
{
272+
return Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
273+
}
274+
275+
private static bool PathsEqual(string a, string b)
276+
{
277+
string normA = Path.GetFullPath(a).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
278+
string normB = Path.GetFullPath(b).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
279+
return string.Equals(normA, normB, StringComparison.OrdinalIgnoreCase);
280+
}
281+
282+
private static PackageDeploymentResult Success(string message, string source, string target, string backup)
283+
{
284+
return new PackageDeploymentResult
285+
{
286+
Success = true,
287+
Message = message,
288+
SourcePath = source,
289+
TargetPath = target,
290+
BackupPath = backup
291+
};
292+
}
293+
294+
private static PackageDeploymentResult Fail(string message)
295+
{
296+
return new PackageDeploymentResult
297+
{
298+
Success = false,
299+
Message = message
300+
};
301+
}
302+
}
303+
}

0 commit comments

Comments
 (0)