Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions MCPForUnity/Editor/Services/IPackageUpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
namespace MCPForUnity.Editor.Services
{
/// <summary>
/// Service for checking package updates and version information
/// </summary>
public interface IPackageUpdateService
{
/// <summary>
/// Checks if a newer version of the package is available
/// </summary>
/// <param name="currentVersion">The current package version</param>
/// <returns>Update check result containing availability and latest version info</returns>
UpdateCheckResult CheckForUpdate(string currentVersion);

/// <summary>
/// Compares two version strings to determine if the first is newer than the second
/// </summary>
/// <param name="version1">First version string</param>
/// <param name="version2">Second version string</param>
/// <returns>True if version1 is newer than version2</returns>
bool IsNewerVersion(string version1, string version2);

/// <summary>
/// Determines if the package was installed via Git or Asset Store
/// </summary>
/// <returns>True if installed via Git, false if Asset Store or unknown</returns>
bool IsGitInstallation();

/// <summary>
/// Clears the cached update check data, forcing a fresh check on next request
/// </summary>
void ClearCache();
}

/// <summary>
/// Result of an update check operation
/// </summary>
public class UpdateCheckResult
{
/// <summary>
/// Whether an update is available
/// </summary>
public bool UpdateAvailable { get; set; }

/// <summary>
/// The latest version available (null if check failed or no update)
/// </summary>
public string LatestVersion { get; set; }

/// <summary>
/// Whether the check was successful (false if network error, etc.)
/// </summary>
public bool CheckSucceeded { get; set; }

/// <summary>
/// Optional message about the check result
/// </summary>
public string Message { get; set; }
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Services/IPackageUpdateService.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions MCPForUnity/Editor/Services/MCPServiceLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ public static class MCPServiceLocator
private static IPythonToolRegistryService _pythonToolRegistryService;
private static ITestRunnerService _testRunnerService;
private static IToolSyncService _toolSyncService;
private static IPackageUpdateService _packageUpdateService;

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

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

/// <summary>
Expand All @@ -53,13 +57,15 @@ public static void Reset()
(_pythonToolRegistryService as IDisposable)?.Dispose();
(_testRunnerService as IDisposable)?.Dispose();
(_toolSyncService as IDisposable)?.Dispose();
(_packageUpdateService as IDisposable)?.Dispose();

_bridgeService = null;
_clientService = null;
_pathService = null;
_pythonToolRegistryService = null;
_testRunnerService = null;
_toolSyncService = null;
_packageUpdateService = null;
}
}
}
161 changes: 161 additions & 0 deletions MCPForUnity/Editor/Services/PackageUpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System;
using System.Net;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;

namespace MCPForUnity.Editor.Services
{
/// <summary>
/// Service for checking package updates from GitHub
/// </summary>
public class PackageUpdateService : IPackageUpdateService
{
private const string LastCheckDateKey = "MCPForUnity.LastUpdateCheck";
private const string CachedVersionKey = "MCPForUnity.LatestKnownVersion";
private const string PackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/main/MCPForUnity/package.json";

/// <inheritdoc/>
public UpdateCheckResult CheckForUpdate(string currentVersion)
{
// Check cache first - only check once per day
string lastCheckDate = EditorPrefs.GetString(LastCheckDateKey, "");
string cachedLatestVersion = EditorPrefs.GetString(CachedVersionKey, "");

if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion))
{
return new UpdateCheckResult
{
CheckSucceeded = true,
LatestVersion = cachedLatestVersion,
UpdateAvailable = IsNewerVersion(cachedLatestVersion, currentVersion),
Message = "Using cached version check"
};
}

// Don't check for Asset Store installations
if (!IsGitInstallation())
{
return new UpdateCheckResult
{
CheckSucceeded = false,
UpdateAvailable = false,
Message = "Asset Store installations are updated via Unity Asset Store"
};
}

// Fetch latest version from GitHub
string latestVersion = FetchLatestVersionFromGitHub();

if (!string.IsNullOrEmpty(latestVersion))
{
// Cache the result
EditorPrefs.SetString(LastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd"));
EditorPrefs.SetString(CachedVersionKey, latestVersion);

return new UpdateCheckResult
{
CheckSucceeded = true,
LatestVersion = latestVersion,
UpdateAvailable = IsNewerVersion(latestVersion, currentVersion),
Message = "Successfully checked for updates"
};
}

return new UpdateCheckResult
{
CheckSucceeded = false,
UpdateAvailable = false,
Message = "Failed to check for updates (network issue or offline)"
};
}

/// <inheritdoc/>
public bool IsNewerVersion(string version1, string version2)
{
try
{
// Remove any "v" prefix
version1 = version1.TrimStart('v', 'V');
version2 = version2.TrimStart('v', 'V');

var version1Parts = version1.Split('.');
var version2Parts = version2.Split('.');

for (int i = 0; i < Math.Min(version1Parts.Length, version2Parts.Length); i++)
{
if (int.TryParse(version1Parts[i], out int v1Num) &&
int.TryParse(version2Parts[i], out int v2Num))
{
if (v1Num > v2Num) return true;
if (v1Num < v2Num) return false;
}
}
return false;
}
catch
{
return false;
}
}
Comment on lines +74 to +100
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix version comparison logic for differing part counts.

IsNewerVersion has a bug when comparing versions with different numbers of parts. Currently, IsNewerVersion("1.0.1", "1.0") returns false, but it should return true since 1.0.1 is newer than 1.0.

Apply this diff to handle versions with different part counts:

 public bool IsNewerVersion(string version1, string version2)
 {
     try
     {
         // Remove any "v" prefix
         version1 = version1.TrimStart('v', 'V');
         version2 = version2.TrimStart('v', 'V');

         var version1Parts = version1.Split('.');
         var version2Parts = version2.Split('.');

-        for (int i = 0; i < Math.Min(version1Parts.Length, version2Parts.Length); i++)
+        int maxLength = Math.Max(version1Parts.Length, version2Parts.Length);
+        
+        for (int i = 0; i < maxLength; i++)
         {
-            if (int.TryParse(version1Parts[i], out int v1Num) &&
-                int.TryParse(version2Parts[i], out int v2Num))
+            int v1Num = 0;
+            int v2Num = 0;
+            
+            if (i < version1Parts.Length)
+                int.TryParse(version1Parts[i], out v1Num);
+            if (i < version2Parts.Length)
+                int.TryParse(version2Parts[i], out v2Num);
+            
+            if (v1Num > v2Num) return true;
+            if (v1Num < v2Num) return false;
-            {
-                if (v1Num > v2Num) return true;
-                if (v1Num < v2Num) return false;
-            }
         }
         return false;
     }
     catch
     {
         return false;
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public bool IsNewerVersion(string version1, string version2)
{
try
{
// Remove any "v" prefix
version1 = version1.TrimStart('v', 'V');
version2 = version2.TrimStart('v', 'V');
var version1Parts = version1.Split('.');
var version2Parts = version2.Split('.');
for (int i = 0; i < Math.Min(version1Parts.Length, version2Parts.Length); i++)
{
if (int.TryParse(version1Parts[i], out int v1Num) &&
int.TryParse(version2Parts[i], out int v2Num))
{
if (v1Num > v2Num) return true;
if (v1Num < v2Num) return false;
}
}
return false;
}
catch
{
return false;
}
}
public bool IsNewerVersion(string version1, string version2)
{
try
{
// Remove any "v" prefix
version1 = version1.TrimStart('v', 'V');
version2 = version2.TrimStart('v', 'V');
var version1Parts = version1.Split('.');
var version2Parts = version2.Split('.');
int maxLength = Math.Max(version1Parts.Length, version2Parts.Length);
for (int i = 0; i < maxLength; i++)
{
int v1Num = 0;
int v2Num = 0;
if (i < version1Parts.Length)
int.TryParse(version1Parts[i], out v1Num);
if (i < version2Parts.Length)
int.TryParse(version2Parts[i], out v2Num);
if (v1Num > v2Num) return true;
if (v1Num < v2Num) return false;
}
return false;
}
catch
{
return false;
}
}
🤖 Prompt for AI Agents
In MCPForUnity/Editor/Services/PackageUpdateService.cs around lines 74 to 100,
the version comparison only compares shared parts and returns false for "1.0.1"
vs "1.0"; modify the logic so after the loop you inspect the remaining parts of
the longer version: parse each remaining segment as int (treat non-numeric or
missing as 0) and if any remaining segment in version1 > 0 return true, if any
remaining segment in version2 > 0 return false, otherwise fall back to false;
preserve the existing try/catch and trimming of 'v' prefixes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package only uses semantic versions, bump-version.yml (not in PR) updates versions in a standard way

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine the bump-version.yml file
fd -t f "bump-version.yml" --exec cat {}

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the package.json to see the version format
fd -t f "package.json" --exec cat {}

Length of output: 2261


🏁 Script executed:

#!/bin/bash
# Search for where GetPackageVersion is implemented to understand version retrieval
rg -nP -C5 "GetPackageVersion"

Length of output: 4114


🏁 Script executed:

#!/bin/bash
# Check what version comparison test cases exist
rg -nP -A2 "IsNewerVersion" TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs | head -80

Length of output: 2635


🏁 Script executed:

#!/bin/bash
# Look for .github/workflows directory and bump-version file
fd -t f "bump" -e yml -e yaml

Length of output: 0


@msanatan, thanks for the clarification! You're absolutely right—if the package strictly uses semantic versioning (major.minor.patch) and bump-version.yml ensures versions are always in that format, then the edge case CodeRabbit flagged (comparing versions with different part counts like "1.0.1" vs "1.0") won't occur in practice.

Looking at your test suite, all the test cases confirm this: they all use complete three-part versions ("2.0.0", "1.2.0", "1.0.2", etc.), and the package.json shows versions like "4.1.1" and "6.1.0".

Since IsNewerVersion is primarily used internally to compare package versions that are always semantic versioned, the current implementation should work fine for your use case. The bug would only surface if someone called the method with inconsistent version formats, which doesn't happen in your workflow.

Feel free to disregard CodeRabbit's suggestion if you're confident in your versioning process! 👍


/// <inheritdoc/>
public bool IsGitInstallation()
{
// Git packages are installed via Package Manager and have a package.json in Packages/
// Asset Store packages are in Assets/
string packageRoot = AssetPathUtility.GetMcpPackageRootPath();

if (string.IsNullOrEmpty(packageRoot))
{
return false;
}

// If the package is in Packages/ it's a PM install (likely Git)
// If it's in Assets/ it's an Asset Store install
return packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase);
}

/// <inheritdoc/>
public void ClearCache()
{
EditorPrefs.DeleteKey(LastCheckDateKey);
EditorPrefs.DeleteKey(CachedVersionKey);
}

/// <summary>
/// Fetches the latest version from GitHub's main branch package.json
/// </summary>
private string FetchLatestVersionFromGitHub()
{
try
{
// GitHub API endpoint (Option 1 - has rate limits):
// https://api.github.com/repos/CoplayDev/unity-mcp/releases/latest
//
// We use Option 2 (package.json directly) because:
// - No API rate limits (GitHub serves raw files freely)
// - Simpler - just parse JSON for version field
// - More reliable - doesn't require releases to be published
// - Direct source of truth from the main branch

using (var client = new WebClient())
{
client.Headers.Add("User-Agent", "Unity-MCPForUnity-UpdateChecker");
string jsonContent = client.DownloadString(PackageJsonUrl);

var packageJson = JObject.Parse(jsonContent);
string version = packageJson["version"]?.ToString();

return string.IsNullOrEmpty(version) ? null : version;
}
}
catch (Exception ex)
{
// Silent fail - don't interrupt the user if network is unavailable
McpLog.Info($"Update check failed (this is normal if offline): {ex.Message}");
return null;
}
}
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Services/PackageUpdateService.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 24 additions & 1 deletion MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ private void CacheUIElements()
private void InitializeUI()
{
// Settings Section
versionLabel.text = AssetPathUtility.GetPackageVersion();
UpdateVersionLabel();
debugLogsToggle.value = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);

validationLevelField.Init(ValidationLevel.Standard);
Expand Down Expand Up @@ -833,5 +833,28 @@ private void OnCopyJsonClicked()
EditorGUIUtility.systemCopyBuffer = configJsonField.value;
McpLog.Info("Configuration copied to clipboard");
}

private void UpdateVersionLabel()
{
string currentVersion = AssetPathUtility.GetPackageVersion();
versionLabel.text = $"v{currentVersion}";

// Check for updates using the service
var updateCheck = MCPServiceLocator.Updates.CheckForUpdate(currentVersion);

if (updateCheck.UpdateAvailable && !string.IsNullOrEmpty(updateCheck.LatestVersion))
{
// Update available - enhance the label
versionLabel.text = $"\u2191 v{currentVersion} (Update available: v{updateCheck.LatestVersion})";
versionLabel.style.color = new Color(1f, 0.7f, 0f); // Orange
versionLabel.tooltip = $"Version {updateCheck.LatestVersion} is available. Update via Package Manager.\n\nGit URL: https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity";
}
else
{
versionLabel.style.color = StyleKeyword.Null; // Default color
versionLabel.tooltip = $"Current version: {currentVersion}";
}
}

}
}
Loading