From 140a7e5c5549556d5574df721f60648ff80b025b Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Fri, 30 Jan 2026 21:31:23 -0400 Subject: [PATCH] Asset store updates (#660) * Add resource discovery service and UI for managing MCP resources * Consolidate duplicate IsBuiltIn logic into StringCaseUtility.IsBuiltInMcpType * Add resource enable/disable enforcement and improve error response handling - Block execution of disabled resources in TransportCommandDispatcher with clear error message - Add parse_resource_response() utility to handle error responses without Pydantic validation failures - Replace inline response parsing with parse_resource_response() across all resource handlers - Export parse_resource_response from models/__init__.py for consistent usage * Block execution of disabled built-in tools in TransportCommandDispatcher with clear error message Add tool enable/disable enforcement before command execution. Check tool metadata and enabled state, returning error response if tool is disabled. Prevents execution of disabled tools with user-friendly error message. * Fire warning in the rare chance there are duplicate names * Add Asset Store version checking with separate cache from Git installations To make this work I've added a publicly available JSON that's updated after every release. We can get the info from the asset store page that's against Unity's terms of service, so we want to avoid trouble. The release approval is manual, so this method suffices * Change LastUpdateCheck from Int to String type and add Asset Store version check EditorPrefs * Add EditorPrefs keys for local HTTP server state tracking * Add remote URL configuration parameter for Asset Store release preparation Needed to update this to set the default scope to "remote" because now it's a separate transport mode --- .../Editor/Constants/EditorPrefKeys.cs | 2 + .../Editor/Services/PackageUpdateService.cs | 67 ++++++--- .../Windows/EditorPrefs/EditorPrefsWindow.cs | 10 +- .../Services/PackageUpdateServiceTests.cs | 129 +++++++++++++----- tools/prepare_unity_asset_store_release.py | 27 +++- 5 files changed, 175 insertions(+), 60 deletions(-) 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():