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
main
Marcus Sanatan 2026-01-30 21:31:23 -04:00 committed by GitHub
parent 4d969419d4
commit 140a7e5c55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 175 additions and 60 deletions

View File

@ -54,6 +54,8 @@ namespace MCPForUnity.Editor.Constants
internal const string LastUpdateCheck = "MCPForUnity.LastUpdateCheck"; internal const string LastUpdateCheck = "MCPForUnity.LastUpdateCheck";
internal const string LatestKnownVersion = "MCPForUnity.LatestKnownVersion"; 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 LastStdIoUpgradeVersion = "MCPForUnity.LastStdIoUpgradeVersion";
internal const string TelemetryDisabled = "MCPForUnity.TelemetryDisabled"; internal const string TelemetryDisabled = "MCPForUnity.TelemetryDisabled";

View File

@ -8,20 +8,23 @@ using UnityEditor;
namespace MCPForUnity.Editor.Services namespace MCPForUnity.Editor.Services
{ {
/// <summary> /// <summary>
/// Service for checking package updates from GitHub /// Service for checking package updates from GitHub or Asset Store metadata
/// </summary> /// </summary>
public class PackageUpdateService : IPackageUpdateService public class PackageUpdateService : IPackageUpdateService
{ {
private const string LastCheckDateKey = EditorPrefKeys.LastUpdateCheck; private const string LastCheckDateKey = EditorPrefKeys.LastUpdateCheck;
private const string CachedVersionKey = EditorPrefKeys.LatestKnownVersion; 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 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";
/// <inheritdoc/> /// <inheritdoc/>
public UpdateCheckResult CheckForUpdate(string currentVersion) public UpdateCheckResult CheckForUpdate(string currentVersion)
{ {
// Check cache first - only check once per day bool isGitInstallation = IsGitInstallation();
string lastCheckDate = EditorPrefs.GetString(LastCheckDateKey, ""); string lastCheckDate = EditorPrefs.GetString(isGitInstallation ? LastCheckDateKey : LastAssetStoreCheckDateKey, "");
string cachedLatestVersion = EditorPrefs.GetString(CachedVersionKey, ""); string cachedLatestVersion = EditorPrefs.GetString(isGitInstallation ? CachedVersionKey : CachedAssetStoreVersionKey, "");
if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion)) 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 string latestVersion = isGitInstallation
if (!IsGitInstallation()) ? FetchLatestVersionFromGitHub()
{ : FetchLatestVersionFromAssetStoreJson();
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)) if (!string.IsNullOrEmpty(latestVersion))
{ {
// Cache the result // Cache the result
EditorPrefs.SetString(LastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); EditorPrefs.SetString(isGitInstallation ? LastCheckDateKey : LastAssetStoreCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd"));
EditorPrefs.SetString(CachedVersionKey, latestVersion); EditorPrefs.SetString(isGitInstallation ? CachedVersionKey : CachedAssetStoreVersionKey, latestVersion);
return new UpdateCheckResult return new UpdateCheckResult
{ {
@ -67,7 +60,9 @@ namespace MCPForUnity.Editor.Services
{ {
CheckSucceeded = false, CheckSucceeded = false,
UpdateAvailable = 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
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool IsGitInstallation() public virtual bool IsGitInstallation()
{ {
// Git packages are installed via Package Manager and have a package.json in Packages/ // Git packages are installed via Package Manager and have a package.json in Packages/
// Asset Store packages are in Assets/ // Asset Store packages are in Assets/
@ -122,12 +117,14 @@ namespace MCPForUnity.Editor.Services
{ {
EditorPrefs.DeleteKey(LastCheckDateKey); EditorPrefs.DeleteKey(LastCheckDateKey);
EditorPrefs.DeleteKey(CachedVersionKey); EditorPrefs.DeleteKey(CachedVersionKey);
EditorPrefs.DeleteKey(LastAssetStoreCheckDateKey);
EditorPrefs.DeleteKey(CachedAssetStoreVersionKey);
} }
/// <summary> /// <summary>
/// Fetches the latest version from GitHub's main branch package.json /// Fetches the latest version from GitHub's main branch package.json
/// </summary> /// </summary>
private string FetchLatestVersionFromGitHub() protected virtual string FetchLatestVersionFromGitHub()
{ {
try try
{ {
@ -158,5 +155,31 @@ namespace MCPForUnity.Editor.Services
return null; return null;
} }
} }
/// <summary>
/// Fetches the latest Asset Store version from a hosted JSON file.
/// </summary>
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;
}
}
} }
} }

View File

@ -48,8 +48,10 @@ namespace MCPForUnity.Editor.Windows
// Integer prefs // Integer prefs
{ EditorPrefKeys.UnitySocketPort, EditorPrefType.Int }, { EditorPrefKeys.UnitySocketPort, EditorPrefType.Int },
{ EditorPrefKeys.ValidationLevel, EditorPrefType.Int }, { EditorPrefKeys.ValidationLevel, EditorPrefType.Int },
{ EditorPrefKeys.LastUpdateCheck, EditorPrefType.Int }, { EditorPrefKeys.LastUpdateCheck, EditorPrefType.String },
{ EditorPrefKeys.LastStdIoUpgradeVersion, EditorPrefType.Int }, { EditorPrefKeys.LastStdIoUpgradeVersion, EditorPrefType.Int },
{ EditorPrefKeys.LastLocalHttpServerPid, EditorPrefType.Int },
{ EditorPrefKeys.LastLocalHttpServerPort, EditorPrefType.Int },
// String prefs // String prefs
{ EditorPrefKeys.EditorWindowActivePanel, EditorPrefType.String }, { EditorPrefKeys.EditorWindowActivePanel, EditorPrefType.String },
@ -67,6 +69,12 @@ namespace MCPForUnity.Editor.Windows
{ EditorPrefKeys.PackageDeployLastSourcePath, EditorPrefType.String }, { EditorPrefKeys.PackageDeployLastSourcePath, EditorPrefType.String },
{ EditorPrefKeys.ServerSrc, EditorPrefType.String }, { EditorPrefKeys.ServerSrc, EditorPrefType.String },
{ EditorPrefKeys.LatestKnownVersion, 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 // Templates

View File

@ -11,6 +11,8 @@ namespace MCPForUnityTests.Editor.Services
private PackageUpdateService _service; private PackageUpdateService _service;
private const string TestLastCheckDateKey = EditorPrefKeys.LastUpdateCheck; private const string TestLastCheckDateKey = EditorPrefKeys.LastUpdateCheck;
private const string TestCachedVersionKey = EditorPrefKeys.LatestKnownVersion; private const string TestCachedVersionKey = EditorPrefKeys.LatestKnownVersion;
private const string TestAssetStoreLastCheckDateKey = EditorPrefKeys.LastAssetStoreUpdateCheck;
private const string TestAssetStoreCachedVersionKey = EditorPrefKeys.LatestKnownAssetStoreVersion;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
@ -38,6 +40,14 @@ namespace MCPForUnityTests.Editor.Services
{ {
EditorPrefs.DeleteKey(TestCachedVersionKey); EditorPrefs.DeleteKey(TestCachedVersionKey);
} }
if (EditorPrefs.HasKey(TestAssetStoreLastCheckDateKey))
{
EditorPrefs.DeleteKey(TestAssetStoreLastCheckDateKey);
}
if (EditorPrefs.HasKey(TestAssetStoreCachedVersionKey))
{
EditorPrefs.DeleteKey(TestAssetStoreCachedVersionKey);
}
} }
[Test] [Test]
@ -211,23 +221,73 @@ namespace MCPForUnityTests.Editor.Services
} }
[Test] [Test]
public void CheckForUpdate_ReturnsAssetStoreMessage_ForNonGitInstallations() public void CheckForUpdate_UsesAssetStoreCache_WhenCacheIsValid()
{ {
// Note: This test verifies the service behavior when IsGitInstallation() returns false. // Arrange: Set up valid Asset Store cache
// Since the actual result depends on package installation method, we create a mock string today = DateTime.Now.ToString("yyyy-MM-dd");
// implementation to test this specific code path. string cachedVersion = "9.0.1";
EditorPrefs.SetString(TestAssetStoreLastCheckDateKey, today);
EditorPrefs.SetString(TestAssetStoreCachedVersionKey, cachedVersion);
var mockService = new MockAssetStorePackageUpdateService(); var mockService = new TestablePackageUpdateService
{
IsGitInstallationResult = false,
AssetStoreFetchResult = "9.9.9"
};
// Act // Act
var result = mockService.CheckForUpdate("5.0.0"); var result = mockService.CheckForUpdate("9.0.0");
// Assert // Assert
Assert.IsFalse(result.CheckSucceeded, "Check should not succeed for Asset Store installs"); Assert.IsTrue(result.CheckSucceeded, "Check should succeed with valid Asset Store cache");
Assert.IsFalse(result.UpdateAvailable, "No update should be reported for Asset Store installs"); Assert.AreEqual(cachedVersion, result.LatestVersion, "Should return cached Asset Store version");
Assert.AreEqual("Asset Store installations are updated via Unity Asset Store", result.Message, Assert.IsTrue(result.UpdateAvailable, "Update should be available (9.0.1 > 9.0.0)");
"Should return Asset Store update message"); Assert.IsFalse(mockService.AssetStoreFetchCalled, "Should not fetch when Asset Store cache is valid");
Assert.IsNull(result.LatestVersion, "Latest version should be null for Asset Store installs"); }
[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] [Test]
@ -236,10 +296,14 @@ namespace MCPForUnityTests.Editor.Services
// Arrange: Set up cache // Arrange: Set up cache
EditorPrefs.SetString(TestLastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd")); EditorPrefs.SetString(TestLastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd"));
EditorPrefs.SetString(TestCachedVersionKey, "5.0.0"); EditorPrefs.SetString(TestCachedVersionKey, "5.0.0");
EditorPrefs.SetString(TestAssetStoreLastCheckDateKey, DateTime.Now.ToString("yyyy-MM-dd"));
EditorPrefs.SetString(TestAssetStoreCachedVersionKey, "9.0.0");
// Verify cache exists // Verify cache exists
Assert.IsTrue(EditorPrefs.HasKey(TestLastCheckDateKey), "Cache should exist before clearing"); Assert.IsTrue(EditorPrefs.HasKey(TestLastCheckDateKey), "Cache should exist before clearing");
Assert.IsTrue(EditorPrefs.HasKey(TestCachedVersionKey), "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 // Act
_service.ClearCache(); _service.ClearCache();
@ -247,6 +311,8 @@ namespace MCPForUnityTests.Editor.Services
// Assert // Assert
Assert.IsFalse(EditorPrefs.HasKey(TestLastCheckDateKey), "Date cache should be cleared"); Assert.IsFalse(EditorPrefs.HasKey(TestLastCheckDateKey), "Date cache should be cleared");
Assert.IsFalse(EditorPrefs.HasKey(TestCachedVersionKey), "Version 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] [Test]
@ -261,36 +327,31 @@ namespace MCPForUnityTests.Editor.Services
} }
/// <summary> /// <summary>
/// Mock implementation of IPackageUpdateService that simulates Asset Store installation behavior /// Testable implementation that allows forcing install type and fetch results.
/// </summary> /// </summary>
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 IsGitInstallationResult;
return new UpdateCheckResult
{
CheckSucceeded = false,
UpdateAvailable = false,
Message = "Asset Store installations are updated via Unity Asset Store"
};
} }
public bool IsNewerVersion(string version1, string version2) protected override string FetchLatestVersionFromGitHub()
{ {
// Not used in the Asset Store test, but required by interface GitFetchCalled = true;
return false; return GitFetchResult;
} }
public bool IsGitInstallation() protected override string FetchLatestVersionFromAssetStoreJson()
{ {
// Simulate non-Git installation (Asset Store) AssetStoreFetchCalled = true;
return false; return AssetStoreFetchResult;
}
public void ClearCache()
{
// Not used in the Asset Store test, but required by interface
} }
} }
} }

View File

@ -1,4 +1,12 @@
#!/usr/bin/env python3 #!/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 from __future__ import annotations
import argparse import argparse
@ -73,6 +81,11 @@ def main() -> int:
default=None, default=None,
help="Path to the Unity project used for Asset Store uploads.", 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( parser.add_argument(
"--backup", "--backup",
action="store_true", action="store_true",
@ -88,6 +101,9 @@ def main() -> int:
repo_root = Path(args.repo_root).expanduser().resolve() repo_root = Path(args.repo_root).expanduser().resolve()
asset_project = Path(args.asset_project).expanduser().resolve( asset_project = Path(args.asset_project).expanduser().resolve(
) if args.asset_project else (repo_root / "TestProjects" / "AssetStoreUploads") ) 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" source_mcp = repo_root / "MCPForUnity"
if not source_mcp.is_dir(): if not source_mcp.is_dir():
@ -125,11 +141,11 @@ def main() -> int:
# Remove auto-popup setup window for Asset Store packaging # Remove auto-popup setup window for Asset Store packaging
remove_line_exact(setup_service, "[InitializeOnLoad]") 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( replace_once(
http_util, http_util,
r'private const string DefaultBaseUrl = "http://localhost:8080";', r'private const string DefaultRemoteBaseUrl = "";',
'private const string DefaultBaseUrl = "https://mc-0cb5e1039f6b4499b473670f70662d29.ecs.us-east-2.on.aws/";', f'private const string DefaultRemoteBaseUrl = "{remote_url}";',
) )
# Default transport to HTTP Remote and persist inferred scope when missing # Default transport to HTTP Remote and persist inferred scope when missing
@ -138,6 +154,11 @@ def main() -> int:
r'transportDropdown\.Init\(TransportProtocol\.HTTPLocal\);', r'transportDropdown\.Init\(TransportProtocol\.HTTPLocal\);',
'transportDropdown.Init(TransportProtocol.HTTPRemote);', '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 # 2) Replace Assets/MCPForUnity in the target project
if dest_mcp.exists(): if dest_mcp.exists():