diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
index 021c14ce4..5708626ab 100644
--- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
+++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
@@ -741,7 +741,7 @@ public override void Configure()
public void ConfigureWithCapturedValues(
string projectDir, string claudePath, string pathPrepend,
bool useHttpTransport, string httpUrl,
- string uvxPath, string fromArgs, string packageName, bool shouldForceRefresh,
+ string uvxPath, string fromArgs, string packageName, string uvxDevFlags,
string apiKey,
Models.ConfiguredTransport serverTransport)
{
@@ -752,7 +752,7 @@ public void ConfigureWithCapturedValues(
else
{
RegisterWithCapturedValues(projectDir, claudePath, pathPrepend,
- useHttpTransport, httpUrl, uvxPath, fromArgs, packageName, shouldForceRefresh,
+ useHttpTransport, httpUrl, uvxPath, fromArgs, packageName, uvxDevFlags,
apiKey, serverTransport);
}
}
@@ -763,7 +763,7 @@ public void ConfigureWithCapturedValues(
private void RegisterWithCapturedValues(
string projectDir, string claudePath, string pathPrepend,
bool useHttpTransport, string httpUrl,
- string uvxPath, string fromArgs, string packageName, bool shouldForceRefresh,
+ string uvxPath, string fromArgs, string packageName, string uvxDevFlags,
string apiKey,
Models.ConfiguredTransport serverTransport)
{
@@ -789,10 +789,8 @@ private void RegisterWithCapturedValues(
}
else
{
- // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
- string devFlags = shouldForceRefresh ? "--no-cache --refresh " : string.Empty;
// Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
- args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} {packageName}";
+ args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {uvxDevFlags}{fromArgs} {packageName}";
}
// Remove any existing registrations from ALL scopes to prevent stale config conflicts (#664)
@@ -867,9 +865,7 @@ private void Register()
else
{
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
- // Use central helper that checks both DevModeForceServerRefresh AND local path detection.
- // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
- string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
+ string devFlags = AssetPathUtility.GetUvxDevFlags();
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
// Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} {packageName}";
@@ -977,9 +973,7 @@ public override string GetManualSnippet()
return "# Error: Configuration not available - check paths in Advanced Settings";
}
- // Use central helper that checks both DevModeForceServerRefresh AND local path detection.
- // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
- string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
+ string devFlags = AssetPathUtility.GetUvxDevFlags();
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
return "# Register the MCP server with Claude Code:\n" +
diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs
index 1af9ea236..c648c19e4 100644
--- a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs
+++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Services;
@@ -438,6 +439,95 @@ public static bool ShouldForceUvxRefresh()
return IsLocalServerPath();
}
+ private static bool _offlineCacheResult;
+ private static double _offlineCacheTimestamp = -999;
+ private const double OfflineCacheTtlSeconds = 30.0;
+
+ ///
+ /// Determines whether uvx should use --offline mode for faster startup.
+ /// Runs a lightweight probe (uvx --offline ... mcp-for-unity --help) with a 3-second timeout
+ /// to check if the package is already cached. If cached, --offline skips the network
+ /// dependency check that can hang for 30+ seconds on poor connections.
+ /// Returns false if force refresh is enabled (new download needed).
+ /// The result is cached for 30 seconds to avoid redundant subprocess spawns.
+ /// Must be called on the main thread (reads EditorPrefs).
+ ///
+ public static bool ShouldUseUvxOffline()
+ {
+ if (ShouldForceUvxRefresh())
+ return false;
+ return GetCachedOfflineProbeResult();
+ }
+
+ private static bool GetCachedOfflineProbeResult()
+ {
+ double now = EditorApplication.timeSinceStartup;
+ if (now - _offlineCacheTimestamp < OfflineCacheTtlSeconds)
+ return _offlineCacheResult;
+
+ bool result = RunOfflineProbe();
+ _offlineCacheResult = result;
+ _offlineCacheTimestamp = now;
+ return result;
+ }
+
+ private static bool RunOfflineProbe()
+ {
+ try
+ {
+ string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
+ if (string.IsNullOrEmpty(uvxPath))
+ return false;
+
+ string fromArgs = GetBetaServerFromArgs(quoteFromPath: false);
+ string probeArgs = string.IsNullOrEmpty(fromArgs)
+ ? "--offline mcp-for-unity --help"
+ : $"--offline {fromArgs} mcp-for-unity --help";
+
+ return ExecPath.TryRun(uvxPath, probeArgs, null, out _, out _, timeoutMs: 3000);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Returns the uvx dev-mode flags as a single string for command-line builders.
+ /// Returns "--no-cache --refresh " if force refresh is enabled,
+ /// "--offline " if the cache is warm, or string.Empty otherwise.
+ /// Must be called on the main thread (reads EditorPrefs).
+ ///
+ public static string GetUvxDevFlags()
+ {
+ bool forceRefresh = ShouldForceUvxRefresh();
+ return GetUvxDevFlags(forceRefresh, !forceRefresh && GetCachedOfflineProbeResult());
+ }
+
+ ///
+ /// Returns the uvx dev-mode flags from pre-captured bool values.
+ /// Use this overload when values were captured on the main thread for background use.
+ ///
+ public static string GetUvxDevFlags(bool forceRefresh, bool useOffline)
+ {
+ if (forceRefresh) return "--no-cache --refresh ";
+ if (useOffline) return "--offline ";
+ return string.Empty;
+ }
+
+ ///
+ /// Returns the uvx dev-mode flags as a list of individual arguments.
+ /// Suitable for callers that build argument lists (ConfigJsonBuilder, CodexConfigHelper).
+ /// Must be called on the main thread (reads EditorPrefs).
+ ///
+ public static IReadOnlyList GetUvxDevFlagsList()
+ {
+ bool forceRefresh = ShouldForceUvxRefresh();
+ if (forceRefresh) return new[] { "--no-cache", "--refresh" };
+ if (GetCachedOfflineProbeResult()) return new[] { "--offline" };
+ return Array.Empty();
+ }
+
///
/// Returns true if the server URL is a local path (file:// or absolute path).
///
diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
index a68d47ed7..45ae5a39e 100644
--- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
+++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
@@ -17,14 +17,11 @@ namespace MCPForUnity.Editor.Helpers
///
public static class CodexConfigHelper
{
- private static void AddDevModeArgs(TomlArray args)
+ private static void AddUvxModeFlags(TomlArray args)
{
if (args == null) return;
- // Use central helper that checks both DevModeForceServerRefresh AND local path detection.
- // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
- if (!AssetPathUtility.ShouldForceUvxRefresh()) return;
- args.Add(new TomlString { Value = "--no-cache" });
- args.Add(new TomlString { Value = "--refresh" });
+ foreach (var flag in AssetPathUtility.GetUvxDevFlagsList())
+ args.Add(new TomlString { Value = flag });
}
public static string BuildCodexServerBlock(string uvPath)
@@ -53,7 +50,7 @@ public static string BuildCodexServerBlock(string uvPath)
unityMCP["command"] = uvxPath;
var args = new TomlArray();
- AddDevModeArgs(args);
+ AddUvxModeFlags(args);
// Use centralized helper for beta server / prerelease args
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
{
@@ -205,7 +202,7 @@ private static TomlTable CreateUnityMcpTable(string uvPath)
unityMCP["command"] = new TomlString { Value = uvxPath };
var argsArray = new TomlArray();
- AddDevModeArgs(argsArray);
+ AddUvxModeFlags(argsArray);
// Use centralized helper for beta server / prerelease args
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
{
diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
index 815477232..8405068dd 100644
--- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
+++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
@@ -180,12 +180,8 @@ private static IList BuildUvxArgs(string fromUrl, string packageName)
// Keep ordering consistent with other uvx builders: dev flags first, then --from , then package name.
var args = new List();
- // Use central helper that checks both DevModeForceServerRefresh AND local path detection.
- if (AssetPathUtility.ShouldForceUvxRefresh())
- {
- args.Add("--no-cache");
- args.Add("--refresh");
- }
+ foreach (var flag in AssetPathUtility.GetUvxDevFlagsList())
+ args.Add(flag);
// Use centralized helper for beta server / prerelease args
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
diff --git a/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs b/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs
index 850e27337..ee1050958 100644
--- a/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs
+++ b/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs
@@ -74,6 +74,12 @@ private static void RunMigrationIfNeeded()
if (!ConfigUsesStdIo(configurator.Client))
continue;
+ // Skip clients that don't support the current transport setting —
+ // Configure() would throw (e.g., Claude Desktop when HTTP is enabled).
+ bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
+ if (useHttp && !configurator.Client.SupportsHttpTransport)
+ continue;
+
MCPServiceLocator.Client.ConfigureClient(configurator);
touchedAny = true;
}
diff --git a/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs b/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs
index 47ba7186d..3e505634a 100644
--- a/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs
+++ b/MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs
@@ -46,9 +46,7 @@ public bool TryBuildCommand(out string fileName, out string arguments, out strin
return false;
}
- // Use central helper that checks both DevModeForceServerRefresh AND local path detection.
- // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
- string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
+ string devFlags = AssetPathUtility.GetUvxDevFlags();
bool projectScopedTools = EditorPrefs.GetBool(
EditorPrefKeys.ProjectScopedToolsLocalHttp,
true
diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
index 33ffc8a79..31069022e 100644
--- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
+++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
@@ -304,7 +304,7 @@ private void ConfigureClaudeCliAsync(IMcpClientConfigurator client)
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
- bool shouldForceRefresh = AssetPathUtility.ShouldForceUvxRefresh();
+ string uvxDevFlags = AssetPathUtility.GetUvxDevFlags();
string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
// Compute pathPrepend on main thread
@@ -331,7 +331,7 @@ private void ConfigureClaudeCliAsync(IMcpClientConfigurator client)
cliConfigurator.ConfigureWithCapturedValues(
projectDir, claudePath, pathPrepend,
useHttpTransport, httpUrl,
- uvxPath, fromArgs, packageName, shouldForceRefresh,
+ uvxPath, fromArgs, packageName, uvxDevFlags,
apiKey, serverTransport);
}
return (success: true, error: (string)null);
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/AssetPathUtilityOfflineTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/AssetPathUtilityOfflineTests.cs
new file mode 100644
index 000000000..2d7679513
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/AssetPathUtilityOfflineTests.cs
@@ -0,0 +1,38 @@
+using NUnit.Framework;
+using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Constants;
+using UnityEditor;
+
+namespace MCPForUnityTests.Editor.Helpers
+{
+ public class AssetPathUtilityOfflineTests
+ {
+ private bool _originalForceRefresh;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _originalForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, _originalForceRefresh);
+ }
+
+ [Test]
+ public void ShouldUseUvxOffline_WhenForceRefreshEnabled_ReturnsFalse()
+ {
+ EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, true);
+ Assert.IsFalse(AssetPathUtility.ShouldUseUvxOffline());
+ }
+
+ [Test]
+ public void ShouldUseUvxOffline_DoesNotThrow()
+ {
+ EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
+ Assert.DoesNotThrow(() => AssetPathUtility.ShouldUseUvxOffline());
+ }
+ }
+}
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/AssetPathUtilityOfflineTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/AssetPathUtilityOfflineTests.cs.meta
new file mode 100644
index 000000000..328664e45
--- /dev/null
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/AssetPathUtilityOfflineTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 39ea6f0fc573340d689bf01ef5510153
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs
index ec89eda32..8a9039ec2 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs
@@ -323,10 +323,11 @@ public void SetComponentProperties_ContinuesAfterException()
};
// Expect the error logs from the invalid property
- // Note: PropertyConversion logs "Error converting token to..." when conversion fails
+ // Note: PropertyConversion logs "Error converting token to..." when conversion fails,
+ // then ComponentOps catches the exception and returns an error string (no second Error log).
+ // GameObjectComponentHelpers logs the failure as a warning.
LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("Error converting token to UnityEngine.Vector3"));
- LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex(@"\[SetProperty\].*Failed to set 'velocity'"));
- LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'velocity' not found"));
+ LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex(@"\[ManageGameObject\].*Failed to set property 'velocity'"));
// Act
var result = ManageGameObject.HandleCommand(setPropertiesParams);