Skip to content

Commit 673456b

Browse files
authored
Notify users when there's a new version (#329)
* feat: add package update service with version check and GitHub integration * feat: add migration warning banner and dialog for legacy package users * test: remove redundant cache expiration and clearing tests from PackageUpdateService * test: add package update service tests for expired cache and asset store installations
1 parent 3503378 commit 673456b

File tree

9 files changed

+644
-1
lines changed

9 files changed

+644
-1
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
namespace MCPForUnity.Editor.Services
2+
{
3+
/// <summary>
4+
/// Service for checking package updates and version information
5+
/// </summary>
6+
public interface IPackageUpdateService
7+
{
8+
/// <summary>
9+
/// Checks if a newer version of the package is available
10+
/// </summary>
11+
/// <param name="currentVersion">The current package version</param>
12+
/// <returns>Update check result containing availability and latest version info</returns>
13+
UpdateCheckResult CheckForUpdate(string currentVersion);
14+
15+
/// <summary>
16+
/// Compares two version strings to determine if the first is newer than the second
17+
/// </summary>
18+
/// <param name="version1">First version string</param>
19+
/// <param name="version2">Second version string</param>
20+
/// <returns>True if version1 is newer than version2</returns>
21+
bool IsNewerVersion(string version1, string version2);
22+
23+
/// <summary>
24+
/// Determines if the package was installed via Git or Asset Store
25+
/// </summary>
26+
/// <returns>True if installed via Git, false if Asset Store or unknown</returns>
27+
bool IsGitInstallation();
28+
29+
/// <summary>
30+
/// Clears the cached update check data, forcing a fresh check on next request
31+
/// </summary>
32+
void ClearCache();
33+
}
34+
35+
/// <summary>
36+
/// Result of an update check operation
37+
/// </summary>
38+
public class UpdateCheckResult
39+
{
40+
/// <summary>
41+
/// Whether an update is available
42+
/// </summary>
43+
public bool UpdateAvailable { get; set; }
44+
45+
/// <summary>
46+
/// The latest version available (null if check failed or no update)
47+
/// </summary>
48+
public string LatestVersion { get; set; }
49+
50+
/// <summary>
51+
/// Whether the check was successful (false if network error, etc.)
52+
/// </summary>
53+
public bool CheckSucceeded { get; set; }
54+
55+
/// <summary>
56+
/// Optional message about the check result
57+
/// </summary>
58+
public string Message { get; set; }
59+
}
60+
}

MCPForUnity/Editor/Services/IPackageUpdateService.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
@@ -13,13 +13,15 @@ public static class MCPServiceLocator
1313
private static IPythonToolRegistryService _pythonToolRegistryService;
1414
private static ITestRunnerService _testRunnerService;
1515
private static IToolSyncService _toolSyncService;
16+
private static IPackageUpdateService _packageUpdateService;
1617

1718
public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService();
1819
public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService();
1920
public static IPathResolverService Paths => _pathService ??= new PathResolverService();
2021
public static IPythonToolRegistryService PythonToolRegistry => _pythonToolRegistryService ??= new PythonToolRegistryService();
2122
public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService();
2223
public static IToolSyncService ToolSync => _toolSyncService ??= new ToolSyncService();
24+
public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService();
2325

2426
/// <summary>
2527
/// Registers a custom implementation for a service (useful for testing)
@@ -40,6 +42,8 @@ public static void Register<T>(T implementation) where T : class
4042
_testRunnerService = t;
4143
else if (implementation is IToolSyncService ts)
4244
_toolSyncService = ts;
45+
else if (implementation is IPackageUpdateService pu)
46+
_packageUpdateService = pu;
4347
}
4448

4549
/// <summary>
@@ -53,13 +57,15 @@ public static void Reset()
5357
(_pythonToolRegistryService as IDisposable)?.Dispose();
5458
(_testRunnerService as IDisposable)?.Dispose();
5559
(_toolSyncService as IDisposable)?.Dispose();
60+
(_packageUpdateService as IDisposable)?.Dispose();
5661

5762
_bridgeService = null;
5863
_clientService = null;
5964
_pathService = null;
6065
_pythonToolRegistryService = null;
6166
_testRunnerService = null;
6267
_toolSyncService = null;
68+
_packageUpdateService = null;
6369
}
6470
}
6571
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using System;
2+
using System.Net;
3+
using MCPForUnity.Editor.Helpers;
4+
using Newtonsoft.Json.Linq;
5+
using UnityEditor;
6+
7+
namespace MCPForUnity.Editor.Services
8+
{
9+
/// <summary>
10+
/// Service for checking package updates from GitHub
11+
/// </summary>
12+
public class PackageUpdateService : IPackageUpdateService
13+
{
14+
private const string LastCheckDateKey = "MCPForUnity.LastUpdateCheck";
15+
private const string CachedVersionKey = "MCPForUnity.LatestKnownVersion";
16+
private const string PackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/main/MCPForUnity/package.json";
17+
18+
/// <inheritdoc/>
19+
public UpdateCheckResult CheckForUpdate(string currentVersion)
20+
{
21+
// Check cache first - only check once per day
22+
string lastCheckDate = EditorPrefs.GetString(LastCheckDateKey, "");
23+
string cachedLatestVersion = EditorPrefs.GetString(CachedVersionKey, "");
24+
25+
if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion))
26+
{
27+
return new UpdateCheckResult
28+
{
29+
CheckSucceeded = true,
30+
LatestVersion = cachedLatestVersion,
31+
UpdateAvailable = IsNewerVersion(cachedLatestVersion, currentVersion),
32+
Message = "Using cached version check"
33+
};
34+
}
35+
36+
// Don't check for Asset Store installations
37+
if (!IsGitInstallation())
38+
{
39+
return new UpdateCheckResult
40+
{
41+
CheckSucceeded = false,
42+
UpdateAvailable = false,
43+
Message = "Asset Store installations are updated via Unity Asset Store"
44+
};
45+
}
46+
47+
// Fetch latest version from GitHub
48+
string latestVersion = FetchLatestVersionFromGitHub();
49+
50+
if (!string.IsNullOrEmpty(latestVersion))
51+
{
52+
// Cache the result
53+
EditorPrefs.SetString(LastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd"));
54+
EditorPrefs.SetString(CachedVersionKey, latestVersion);
55+
56+
return new UpdateCheckResult
57+
{
58+
CheckSucceeded = true,
59+
LatestVersion = latestVersion,
60+
UpdateAvailable = IsNewerVersion(latestVersion, currentVersion),
61+
Message = "Successfully checked for updates"
62+
};
63+
}
64+
65+
return new UpdateCheckResult
66+
{
67+
CheckSucceeded = false,
68+
UpdateAvailable = false,
69+
Message = "Failed to check for updates (network issue or offline)"
70+
};
71+
}
72+
73+
/// <inheritdoc/>
74+
public bool IsNewerVersion(string version1, string version2)
75+
{
76+
try
77+
{
78+
// Remove any "v" prefix
79+
version1 = version1.TrimStart('v', 'V');
80+
version2 = version2.TrimStart('v', 'V');
81+
82+
var version1Parts = version1.Split('.');
83+
var version2Parts = version2.Split('.');
84+
85+
for (int i = 0; i < Math.Min(version1Parts.Length, version2Parts.Length); i++)
86+
{
87+
if (int.TryParse(version1Parts[i], out int v1Num) &&
88+
int.TryParse(version2Parts[i], out int v2Num))
89+
{
90+
if (v1Num > v2Num) return true;
91+
if (v1Num < v2Num) return false;
92+
}
93+
}
94+
return false;
95+
}
96+
catch
97+
{
98+
return false;
99+
}
100+
}
101+
102+
/// <inheritdoc/>
103+
public bool IsGitInstallation()
104+
{
105+
// Git packages are installed via Package Manager and have a package.json in Packages/
106+
// Asset Store packages are in Assets/
107+
string packageRoot = AssetPathUtility.GetMcpPackageRootPath();
108+
109+
if (string.IsNullOrEmpty(packageRoot))
110+
{
111+
return false;
112+
}
113+
114+
// If the package is in Packages/ it's a PM install (likely Git)
115+
// If it's in Assets/ it's an Asset Store install
116+
return packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase);
117+
}
118+
119+
/// <inheritdoc/>
120+
public void ClearCache()
121+
{
122+
EditorPrefs.DeleteKey(LastCheckDateKey);
123+
EditorPrefs.DeleteKey(CachedVersionKey);
124+
}
125+
126+
/// <summary>
127+
/// Fetches the latest version from GitHub's main branch package.json
128+
/// </summary>
129+
private string FetchLatestVersionFromGitHub()
130+
{
131+
try
132+
{
133+
// GitHub API endpoint (Option 1 - has rate limits):
134+
// https://api.github.com/repos/CoplayDev/unity-mcp/releases/latest
135+
//
136+
// We use Option 2 (package.json directly) because:
137+
// - No API rate limits (GitHub serves raw files freely)
138+
// - Simpler - just parse JSON for version field
139+
// - More reliable - doesn't require releases to be published
140+
// - Direct source of truth from the main branch
141+
142+
using (var client = new WebClient())
143+
{
144+
client.Headers.Add("User-Agent", "Unity-MCPForUnity-UpdateChecker");
145+
string jsonContent = client.DownloadString(PackageJsonUrl);
146+
147+
var packageJson = JObject.Parse(jsonContent);
148+
string version = packageJson["version"]?.ToString();
149+
150+
return string.IsNullOrEmpty(version) ? null : version;
151+
}
152+
}
153+
catch (Exception ex)
154+
{
155+
// Silent fail - don't interrupt the user if network is unavailable
156+
McpLog.Info($"Update check failed (this is normal if offline): {ex.Message}");
157+
return null;
158+
}
159+
}
160+
}
161+
}

MCPForUnity/Editor/Services/PackageUpdateService.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/Windows/MCPForUnityEditorWindowNew.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ private void CacheUIElements()
242242
private void InitializeUI()
243243
{
244244
// Settings Section
245-
versionLabel.text = AssetPathUtility.GetPackageVersion();
245+
UpdateVersionLabel();
246246
debugLogsToggle.value = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
247247

248248
validationLevelField.Init(ValidationLevel.Standard);
@@ -833,5 +833,28 @@ private void OnCopyJsonClicked()
833833
EditorGUIUtility.systemCopyBuffer = configJsonField.value;
834834
McpLog.Info("Configuration copied to clipboard");
835835
}
836+
837+
private void UpdateVersionLabel()
838+
{
839+
string currentVersion = AssetPathUtility.GetPackageVersion();
840+
versionLabel.text = $"v{currentVersion}";
841+
842+
// Check for updates using the service
843+
var updateCheck = MCPServiceLocator.Updates.CheckForUpdate(currentVersion);
844+
845+
if (updateCheck.UpdateAvailable && !string.IsNullOrEmpty(updateCheck.LatestVersion))
846+
{
847+
// Update available - enhance the label
848+
versionLabel.text = $"\u2191 v{currentVersion} (Update available: v{updateCheck.LatestVersion})";
849+
versionLabel.style.color = new Color(1f, 0.7f, 0f); // Orange
850+
versionLabel.tooltip = $"Version {updateCheck.LatestVersion} is available. Update via Package Manager.\n\nGit URL: https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity";
851+
}
852+
else
853+
{
854+
versionLabel.style.color = StyleKeyword.Null; // Default color
855+
versionLabel.tooltip = $"Current version: {currentVersion}";
856+
}
857+
}
858+
836859
}
837860
}

0 commit comments

Comments
 (0)