diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index 8caf430..488bf39 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -54,6 +54,8 @@ namespace MCPForUnity.Editor.Constants internal const string LastUpdateCheck = "MCPForUnity.LastUpdateCheck"; internal const string LatestKnownVersion = "MCPForUnity.LatestKnownVersion"; + internal const string LastAssetStoreUpdateCheck = "MCPForUnity.LastAssetStoreUpdateCheck"; + internal const string LatestKnownAssetStoreVersion = "MCPForUnity.LatestKnownAssetStoreVersion"; internal const string LastStdIoUpgradeVersion = "MCPForUnity.LastStdIoUpgradeVersion"; internal const string TelemetryDisabled = "MCPForUnity.TelemetryDisabled"; diff --git a/MCPForUnity/Editor/Services/PackageUpdateService.cs b/MCPForUnity/Editor/Services/PackageUpdateService.cs index 91248cf..81c0161 100644 --- a/MCPForUnity/Editor/Services/PackageUpdateService.cs +++ b/MCPForUnity/Editor/Services/PackageUpdateService.cs @@ -8,20 +8,23 @@ using UnityEditor; namespace MCPForUnity.Editor.Services { /// - /// Service for checking package updates from GitHub + /// Service for checking package updates from GitHub or Asset Store metadata /// public class PackageUpdateService : IPackageUpdateService { private const string LastCheckDateKey = EditorPrefKeys.LastUpdateCheck; private const string CachedVersionKey = EditorPrefKeys.LatestKnownVersion; + private const string LastAssetStoreCheckDateKey = EditorPrefKeys.LastAssetStoreUpdateCheck; + private const string CachedAssetStoreVersionKey = EditorPrefKeys.LatestKnownAssetStoreVersion; private const string PackageJsonUrl = "https://raw.githubusercontent.com/CoplayDev/unity-mcp/main/MCPForUnity/package.json"; + private const string AssetStoreVersionUrl = "https://gqoqjkkptwfbkwyssmnj.supabase.co/storage/v1/object/public/coplay-images/assetstoreversion.json"; /// public UpdateCheckResult CheckForUpdate(string currentVersion) { - // Check cache first - only check once per day - string lastCheckDate = EditorPrefs.GetString(LastCheckDateKey, ""); - string cachedLatestVersion = EditorPrefs.GetString(CachedVersionKey, ""); + bool isGitInstallation = IsGitInstallation(); + string lastCheckDate = EditorPrefs.GetString(isGitInstallation ? LastCheckDateKey : LastAssetStoreCheckDateKey, ""); + string cachedLatestVersion = EditorPrefs.GetString(isGitInstallation ? CachedVersionKey : CachedAssetStoreVersionKey, ""); if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion)) { @@ -34,25 +37,15 @@ namespace MCPForUnity.Editor.Services }; } - // 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(); + string latestVersion = isGitInstallation + ? FetchLatestVersionFromGitHub() + : FetchLatestVersionFromAssetStoreJson(); if (!string.IsNullOrEmpty(latestVersion)) { // Cache the result - EditorPrefs.SetString(LastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); - EditorPrefs.SetString(CachedVersionKey, latestVersion); + EditorPrefs.SetString(isGitInstallation ? LastCheckDateKey : LastAssetStoreCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); + EditorPrefs.SetString(isGitInstallation ? CachedVersionKey : CachedAssetStoreVersionKey, latestVersion); return new UpdateCheckResult { @@ -67,7 +60,9 @@ namespace MCPForUnity.Editor.Services { CheckSucceeded = false, UpdateAvailable = false, - Message = "Failed to check for updates (network issue or offline)" + Message = isGitInstallation + ? "Failed to check for updates (network issue or offline)" + : "Failed to check for Asset Store updates (network issue or offline)" }; } @@ -101,7 +96,7 @@ namespace MCPForUnity.Editor.Services } /// - public bool IsGitInstallation() + public virtual bool IsGitInstallation() { // Git packages are installed via Package Manager and have a package.json in Packages/ // Asset Store packages are in Assets/ @@ -122,12 +117,14 @@ namespace MCPForUnity.Editor.Services { EditorPrefs.DeleteKey(LastCheckDateKey); EditorPrefs.DeleteKey(CachedVersionKey); + EditorPrefs.DeleteKey(LastAssetStoreCheckDateKey); + EditorPrefs.DeleteKey(CachedAssetStoreVersionKey); } /// /// Fetches the latest version from GitHub's main branch package.json /// - private string FetchLatestVersionFromGitHub() + protected virtual string FetchLatestVersionFromGitHub() { try { @@ -158,5 +155,31 @@ namespace MCPForUnity.Editor.Services return null; } } + + /// + /// Fetches the latest Asset Store version from a hosted JSON file. + /// + protected virtual string FetchLatestVersionFromAssetStoreJson() + { + try + { + using (var client = new WebClient()) + { + client.Headers.Add("User-Agent", "Unity-MCPForUnity-AssetStoreUpdateChecker"); + string jsonContent = client.DownloadString(AssetStoreVersionUrl); + + var versionJson = JObject.Parse(jsonContent); + string version = versionJson["version"]?.ToString(); + + return string.IsNullOrEmpty(version) ? null : version; + } + } + catch (Exception ex) + { + // Silent fail - don't interrupt the user if network is unavailable + McpLog.Info($"Asset Store update check failed (this is normal if offline): {ex.Message}"); + return null; + } + } } } diff --git a/MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs b/MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs index 55bbbec..9a2b86a 100644 --- a/MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs +++ b/MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs @@ -48,8 +48,10 @@ namespace MCPForUnity.Editor.Windows // Integer prefs { EditorPrefKeys.UnitySocketPort, EditorPrefType.Int }, { EditorPrefKeys.ValidationLevel, EditorPrefType.Int }, - { EditorPrefKeys.LastUpdateCheck, EditorPrefType.Int }, + { EditorPrefKeys.LastUpdateCheck, EditorPrefType.String }, { EditorPrefKeys.LastStdIoUpgradeVersion, EditorPrefType.Int }, + { EditorPrefKeys.LastLocalHttpServerPid, EditorPrefType.Int }, + { EditorPrefKeys.LastLocalHttpServerPort, EditorPrefType.Int }, // String prefs { EditorPrefKeys.EditorWindowActivePanel, EditorPrefType.String }, @@ -67,6 +69,12 @@ namespace MCPForUnity.Editor.Windows { EditorPrefKeys.PackageDeployLastSourcePath, EditorPrefType.String }, { EditorPrefKeys.ServerSrc, EditorPrefType.String }, { EditorPrefKeys.LatestKnownVersion, EditorPrefType.String }, + { EditorPrefKeys.LastAssetStoreUpdateCheck, EditorPrefType.String }, + { EditorPrefKeys.LatestKnownAssetStoreVersion, EditorPrefType.String }, + { EditorPrefKeys.LastLocalHttpServerStartedUtc, EditorPrefType.String }, + { EditorPrefKeys.LastLocalHttpServerPidArgsHash, EditorPrefType.String }, + { EditorPrefKeys.LastLocalHttpServerPidFilePath, EditorPrefType.String }, + { EditorPrefKeys.LastLocalHttpServerInstanceToken, EditorPrefType.String }, }; // Templates diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs index 2d60063..1eced45 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/PackageUpdateServiceTests.cs @@ -11,6 +11,8 @@ namespace MCPForUnityTests.Editor.Services private PackageUpdateService _service; private const string TestLastCheckDateKey = EditorPrefKeys.LastUpdateCheck; private const string TestCachedVersionKey = EditorPrefKeys.LatestKnownVersion; + private const string TestAssetStoreLastCheckDateKey = EditorPrefKeys.LastAssetStoreUpdateCheck; + private const string TestAssetStoreCachedVersionKey = EditorPrefKeys.LatestKnownAssetStoreVersion; [SetUp] public void SetUp() @@ -38,6 +40,14 @@ namespace MCPForUnityTests.Editor.Services { EditorPrefs.DeleteKey(TestCachedVersionKey); } + if (EditorPrefs.HasKey(TestAssetStoreLastCheckDateKey)) + { + EditorPrefs.DeleteKey(TestAssetStoreLastCheckDateKey); + } + if (EditorPrefs.HasKey(TestAssetStoreCachedVersionKey)) + { + EditorPrefs.DeleteKey(TestAssetStoreCachedVersionKey); + } } [Test] @@ -211,23 +221,73 @@ namespace MCPForUnityTests.Editor.Services } [Test] - public void CheckForUpdate_ReturnsAssetStoreMessage_ForNonGitInstallations() + public void CheckForUpdate_UsesAssetStoreCache_WhenCacheIsValid() { - // Note: This test verifies the service behavior when IsGitInstallation() returns false. - // Since the actual result depends on package installation method, we create a mock - // implementation to test this specific code path. - - var mockService = new MockAssetStorePackageUpdateService(); - + // Arrange: Set up valid Asset Store cache + string today = DateTime.Now.ToString("yyyy-MM-dd"); + string cachedVersion = "9.0.1"; + EditorPrefs.SetString(TestAssetStoreLastCheckDateKey, today); + EditorPrefs.SetString(TestAssetStoreCachedVersionKey, cachedVersion); + + var mockService = new TestablePackageUpdateService + { + IsGitInstallationResult = false, + AssetStoreFetchResult = "9.9.9" + }; + // Act - var result = mockService.CheckForUpdate("5.0.0"); + var result = mockService.CheckForUpdate("9.0.0"); // Assert - Assert.IsFalse(result.CheckSucceeded, "Check should not succeed for Asset Store installs"); - Assert.IsFalse(result.UpdateAvailable, "No update should be reported for Asset Store installs"); - Assert.AreEqual("Asset Store installations are updated via Unity Asset Store", result.Message, - "Should return Asset Store update message"); - Assert.IsNull(result.LatestVersion, "Latest version should be null for Asset Store installs"); + Assert.IsTrue(result.CheckSucceeded, "Check should succeed with valid Asset Store cache"); + Assert.AreEqual(cachedVersion, result.LatestVersion, "Should return cached Asset Store version"); + Assert.IsTrue(result.UpdateAvailable, "Update should be available (9.0.1 > 9.0.0)"); + Assert.IsFalse(mockService.AssetStoreFetchCalled, "Should not fetch when Asset Store cache is valid"); + } + + [Test] + public void CheckForUpdate_FetchesAssetStoreJson_WhenCacheExpired() + { + // Arrange: Set expired Asset Store cache and a valid Git cache to ensure separation + string yesterday = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd"); + EditorPrefs.SetString(TestAssetStoreLastCheckDateKey, yesterday); + EditorPrefs.SetString(TestAssetStoreCachedVersionKey, "9.0.0"); + EditorPrefs.SetString(TestLastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); + EditorPrefs.SetString(TestCachedVersionKey, "99.0.0"); + + var mockService = new TestablePackageUpdateService + { + IsGitInstallationResult = false, + AssetStoreFetchResult = "9.1.0" + }; + + // Act + var result = mockService.CheckForUpdate("9.0.0"); + + // Assert + Assert.IsTrue(result.CheckSucceeded, "Check should succeed when fetch returns a version"); + Assert.AreEqual("9.1.0", result.LatestVersion, "Should use fetched Asset Store version"); + Assert.IsTrue(mockService.AssetStoreFetchCalled, "Should fetch when Asset Store cache is expired"); + } + + [Test] + public void CheckForUpdate_ReturnsAssetStoreFailureMessage_WhenFetchFails() + { + // Arrange + var mockService = new TestablePackageUpdateService + { + IsGitInstallationResult = false, + AssetStoreFetchResult = null + }; + + // Act + var result = mockService.CheckForUpdate("9.0.0"); + + // Assert + Assert.IsFalse(result.CheckSucceeded, "Check should fail when Asset Store fetch fails"); + Assert.IsFalse(result.UpdateAvailable, "No update should be reported when fetch fails"); + Assert.AreEqual("Failed to check for Asset Store updates (network issue or offline)", result.Message); + Assert.IsNull(result.LatestVersion, "Latest version should be null when fetch fails"); } [Test] @@ -236,10 +296,14 @@ namespace MCPForUnityTests.Editor.Services // Arrange: Set up cache EditorPrefs.SetString(TestLastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); EditorPrefs.SetString(TestCachedVersionKey, "5.0.0"); + EditorPrefs.SetString(TestAssetStoreLastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); + EditorPrefs.SetString(TestAssetStoreCachedVersionKey, "9.0.0"); // Verify cache exists Assert.IsTrue(EditorPrefs.HasKey(TestLastCheckDateKey), "Cache should exist before clearing"); Assert.IsTrue(EditorPrefs.HasKey(TestCachedVersionKey), "Cache should exist before clearing"); + Assert.IsTrue(EditorPrefs.HasKey(TestAssetStoreLastCheckDateKey), "Asset Store cache should exist before clearing"); + Assert.IsTrue(EditorPrefs.HasKey(TestAssetStoreCachedVersionKey), "Asset Store cache should exist before clearing"); // Act _service.ClearCache(); @@ -247,6 +311,8 @@ namespace MCPForUnityTests.Editor.Services // Assert Assert.IsFalse(EditorPrefs.HasKey(TestLastCheckDateKey), "Date cache should be cleared"); Assert.IsFalse(EditorPrefs.HasKey(TestCachedVersionKey), "Version cache should be cleared"); + Assert.IsFalse(EditorPrefs.HasKey(TestAssetStoreLastCheckDateKey), "Asset Store date cache should be cleared"); + Assert.IsFalse(EditorPrefs.HasKey(TestAssetStoreCachedVersionKey), "Asset Store version cache should be cleared"); } [Test] @@ -261,36 +327,31 @@ namespace MCPForUnityTests.Editor.Services } /// - /// Mock implementation of IPackageUpdateService that simulates Asset Store installation behavior + /// Testable implementation that allows forcing install type and fetch results. /// - internal class MockAssetStorePackageUpdateService : IPackageUpdateService + internal class TestablePackageUpdateService : PackageUpdateService { - public UpdateCheckResult CheckForUpdate(string currentVersion) + public bool IsGitInstallationResult { get; set; } = true; + public string GitFetchResult { get; set; } + public string AssetStoreFetchResult { get; set; } + public bool GitFetchCalled { get; private set; } + public bool AssetStoreFetchCalled { get; private set; } + + public override bool IsGitInstallation() { - // Simulate Asset Store installation (IsGitInstallation returns false) - return new UpdateCheckResult - { - CheckSucceeded = false, - UpdateAvailable = false, - Message = "Asset Store installations are updated via Unity Asset Store" - }; + return IsGitInstallationResult; } - public bool IsNewerVersion(string version1, string version2) + protected override string FetchLatestVersionFromGitHub() { - // Not used in the Asset Store test, but required by interface - return false; + GitFetchCalled = true; + return GitFetchResult; } - public bool IsGitInstallation() + protected override string FetchLatestVersionFromAssetStoreJson() { - // Simulate non-Git installation (Asset Store) - return false; - } - - public void ClearCache() - { - // Not used in the Asset Store test, but required by interface + AssetStoreFetchCalled = true; + return AssetStoreFetchResult; } } } diff --git a/tools/prepare_unity_asset_store_release.py b/tools/prepare_unity_asset_store_release.py index b8dda3d..2b80fe2 100644 --- a/tools/prepare_unity_asset_store_release.py +++ b/tools/prepare_unity_asset_store_release.py @@ -1,4 +1,12 @@ #!/usr/bin/env python3 +"""Prepare MCPForUnity for Asset Store upload. + +Usage: + python tools/prepare_unity_asset_store_release.py \ + --remote-url https://your.remote.endpoint/ \ + --asset-project /path/to/AssetStoreUploads \ + --backup +""" from __future__ import annotations import argparse @@ -73,6 +81,11 @@ def main() -> int: default=None, help="Path to the Unity project used for Asset Store uploads.", ) + parser.add_argument( + "--remote-url", + required=True, + help="Remote MCP HTTP base URL to set as default for Asset Store builds.", + ) parser.add_argument( "--backup", action="store_true", @@ -88,6 +101,9 @@ def main() -> int: repo_root = Path(args.repo_root).expanduser().resolve() asset_project = Path(args.asset_project).expanduser().resolve( ) if args.asset_project else (repo_root / "TestProjects" / "AssetStoreUploads") + remote_url = args.remote_url.strip() + if not remote_url: + raise RuntimeError("--remote-url must be a non-empty URL") source_mcp = repo_root / "MCPForUnity" if not source_mcp.is_dir(): @@ -125,11 +141,11 @@ def main() -> int: # Remove auto-popup setup window for Asset Store packaging remove_line_exact(setup_service, "[InitializeOnLoad]") - # Set default base URL to the hosted endpoint + # Set default remote base URL to the hosted endpoint replace_once( http_util, - r'private const string DefaultBaseUrl = "http://localhost:8080";', - 'private const string DefaultBaseUrl = "https://mc-0cb5e1039f6b4499b473670f70662d29.ecs.us-east-2.on.aws/";', + r'private const string DefaultRemoteBaseUrl = "";', + f'private const string DefaultRemoteBaseUrl = "{remote_url}";', ) # Default transport to HTTP Remote and persist inferred scope when missing @@ -138,6 +154,11 @@ def main() -> int: r'transportDropdown\.Init\(TransportProtocol\.HTTPLocal\);', 'transportDropdown.Init(TransportProtocol.HTTPRemote);', ) + replace_once( + connection_section, + r'scope = MCPServiceLocator\.Server\.IsLocalUrl\(\) \? "local" : "remote";', + 'scope = "remote";', + ) # 2) Replace Assets/MCPForUnity in the target project if dest_mcp.exists():