Skip to content

Commit 3613fee

Browse files
authored
Merge pull request CoplayDev#768 from dsarno/feature/uvx-offline-760
Add --offline to uvx launches for faster startup (CoplayDev#760)
2 parents 4ecaee1 + 173322f commit 3613fee

10 files changed

Lines changed: 165 additions & 34 deletions

File tree

MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ public override void Configure()
741741
public void ConfigureWithCapturedValues(
742742
string projectDir, string claudePath, string pathPrepend,
743743
bool useHttpTransport, string httpUrl,
744-
string uvxPath, string fromArgs, string packageName, bool shouldForceRefresh,
744+
string uvxPath, string fromArgs, string packageName, string uvxDevFlags,
745745
string apiKey,
746746
Models.ConfiguredTransport serverTransport)
747747
{
@@ -752,7 +752,7 @@ public void ConfigureWithCapturedValues(
752752
else
753753
{
754754
RegisterWithCapturedValues(projectDir, claudePath, pathPrepend,
755-
useHttpTransport, httpUrl, uvxPath, fromArgs, packageName, shouldForceRefresh,
755+
useHttpTransport, httpUrl, uvxPath, fromArgs, packageName, uvxDevFlags,
756756
apiKey, serverTransport);
757757
}
758758
}
@@ -763,7 +763,7 @@ public void ConfigureWithCapturedValues(
763763
private void RegisterWithCapturedValues(
764764
string projectDir, string claudePath, string pathPrepend,
765765
bool useHttpTransport, string httpUrl,
766-
string uvxPath, string fromArgs, string packageName, bool shouldForceRefresh,
766+
string uvxPath, string fromArgs, string packageName, string uvxDevFlags,
767767
string apiKey,
768768
Models.ConfiguredTransport serverTransport)
769769
{
@@ -789,10 +789,8 @@ private void RegisterWithCapturedValues(
789789
}
790790
else
791791
{
792-
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
793-
string devFlags = shouldForceRefresh ? "--no-cache --refresh " : string.Empty;
794792
// Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
795-
args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} {packageName}";
793+
args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {uvxDevFlags}{fromArgs} {packageName}";
796794
}
797795

798796
// Remove any existing registrations from ALL scopes to prevent stale config conflicts (#664)
@@ -867,9 +865,7 @@ private void Register()
867865
else
868866
{
869867
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
870-
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
871-
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
872-
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
868+
string devFlags = AssetPathUtility.GetUvxDevFlags();
873869
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
874870
// Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
875871
args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} {packageName}";
@@ -977,9 +973,7 @@ public override string GetManualSnippet()
977973
return "# Error: Configuration not available - check paths in Advanced Settings";
978974
}
979975

980-
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
981-
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
982-
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
976+
string devFlags = AssetPathUtility.GetUvxDevFlags();
983977
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
984978

985979
return "# Register the MCP server with Claude Code:\n" +

MCPForUnity/Editor/Helpers/AssetPathUtility.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using MCPForUnity.Editor.Constants;
45
using MCPForUnity.Editor.Services;
@@ -438,6 +439,95 @@ public static bool ShouldForceUvxRefresh()
438439
return IsLocalServerPath();
439440
}
440441

442+
private static bool _offlineCacheResult;
443+
private static double _offlineCacheTimestamp = -999;
444+
private const double OfflineCacheTtlSeconds = 30.0;
445+
446+
/// <summary>
447+
/// Determines whether uvx should use --offline mode for faster startup.
448+
/// Runs a lightweight probe (uvx --offline ... mcp-for-unity --help) with a 3-second timeout
449+
/// to check if the package is already cached. If cached, --offline skips the network
450+
/// dependency check that can hang for 30+ seconds on poor connections.
451+
/// Returns false if force refresh is enabled (new download needed).
452+
/// The result is cached for 30 seconds to avoid redundant subprocess spawns.
453+
/// Must be called on the main thread (reads EditorPrefs).
454+
/// </summary>
455+
public static bool ShouldUseUvxOffline()
456+
{
457+
if (ShouldForceUvxRefresh())
458+
return false;
459+
return GetCachedOfflineProbeResult();
460+
}
461+
462+
private static bool GetCachedOfflineProbeResult()
463+
{
464+
double now = EditorApplication.timeSinceStartup;
465+
if (now - _offlineCacheTimestamp < OfflineCacheTtlSeconds)
466+
return _offlineCacheResult;
467+
468+
bool result = RunOfflineProbe();
469+
_offlineCacheResult = result;
470+
_offlineCacheTimestamp = now;
471+
return result;
472+
}
473+
474+
private static bool RunOfflineProbe()
475+
{
476+
try
477+
{
478+
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
479+
if (string.IsNullOrEmpty(uvxPath))
480+
return false;
481+
482+
string fromArgs = GetBetaServerFromArgs(quoteFromPath: false);
483+
string probeArgs = string.IsNullOrEmpty(fromArgs)
484+
? "--offline mcp-for-unity --help"
485+
: $"--offline {fromArgs} mcp-for-unity --help";
486+
487+
return ExecPath.TryRun(uvxPath, probeArgs, null, out _, out _, timeoutMs: 3000);
488+
}
489+
catch
490+
{
491+
return false;
492+
}
493+
}
494+
495+
/// <summary>
496+
/// Returns the uvx dev-mode flags as a single string for command-line builders.
497+
/// Returns "--no-cache --refresh " if force refresh is enabled,
498+
/// "--offline " if the cache is warm, or string.Empty otherwise.
499+
/// Must be called on the main thread (reads EditorPrefs).
500+
/// </summary>
501+
public static string GetUvxDevFlags()
502+
{
503+
bool forceRefresh = ShouldForceUvxRefresh();
504+
return GetUvxDevFlags(forceRefresh, !forceRefresh && GetCachedOfflineProbeResult());
505+
}
506+
507+
/// <summary>
508+
/// Returns the uvx dev-mode flags from pre-captured bool values.
509+
/// Use this overload when values were captured on the main thread for background use.
510+
/// </summary>
511+
public static string GetUvxDevFlags(bool forceRefresh, bool useOffline)
512+
{
513+
if (forceRefresh) return "--no-cache --refresh ";
514+
if (useOffline) return "--offline ";
515+
return string.Empty;
516+
}
517+
518+
/// <summary>
519+
/// Returns the uvx dev-mode flags as a list of individual arguments.
520+
/// Suitable for callers that build argument lists (ConfigJsonBuilder, CodexConfigHelper).
521+
/// Must be called on the main thread (reads EditorPrefs).
522+
/// </summary>
523+
public static IReadOnlyList<string> GetUvxDevFlagsList()
524+
{
525+
bool forceRefresh = ShouldForceUvxRefresh();
526+
if (forceRefresh) return new[] { "--no-cache", "--refresh" };
527+
if (GetCachedOfflineProbeResult()) return new[] { "--offline" };
528+
return Array.Empty<string>();
529+
}
530+
441531
/// <summary>
442532
/// Returns true if the server URL is a local path (file:// or absolute path).
443533
/// </summary>

MCPForUnity/Editor/Helpers/CodexConfigHelper.cs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,11 @@ namespace MCPForUnity.Editor.Helpers
1717
/// </summary>
1818
public static class CodexConfigHelper
1919
{
20-
private static void AddDevModeArgs(TomlArray args)
20+
private static void AddUvxModeFlags(TomlArray args)
2121
{
2222
if (args == null) return;
23-
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
24-
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
25-
if (!AssetPathUtility.ShouldForceUvxRefresh()) return;
26-
args.Add(new TomlString { Value = "--no-cache" });
27-
args.Add(new TomlString { Value = "--refresh" });
23+
foreach (var flag in AssetPathUtility.GetUvxDevFlagsList())
24+
args.Add(new TomlString { Value = flag });
2825
}
2926

3027
public static string BuildCodexServerBlock(string uvPath)
@@ -53,7 +50,7 @@ public static string BuildCodexServerBlock(string uvPath)
5350
unityMCP["command"] = uvxPath;
5451

5552
var args = new TomlArray();
56-
AddDevModeArgs(args);
53+
AddUvxModeFlags(args);
5754
// Use centralized helper for beta server / prerelease args
5855
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
5956
{
@@ -205,7 +202,7 @@ private static TomlTable CreateUnityMcpTable(string uvPath)
205202
unityMCP["command"] = new TomlString { Value = uvxPath };
206203

207204
var argsArray = new TomlArray();
208-
AddDevModeArgs(argsArray);
205+
AddUvxModeFlags(argsArray);
209206
// Use centralized helper for beta server / prerelease args
210207
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())
211208
{

MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,8 @@ private static IList<string> BuildUvxArgs(string fromUrl, string packageName)
180180
// Keep ordering consistent with other uvx builders: dev flags first, then --from <url>, then package name.
181181
var args = new List<string>();
182182

183-
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
184-
if (AssetPathUtility.ShouldForceUvxRefresh())
185-
{
186-
args.Add("--no-cache");
187-
args.Add("--refresh");
188-
}
183+
foreach (var flag in AssetPathUtility.GetUvxDevFlagsList())
184+
args.Add(flag);
189185

190186
// Use centralized helper for beta server / prerelease args
191187
foreach (var arg in AssetPathUtility.GetBetaServerFromArgsList())

MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ private static void RunMigrationIfNeeded()
7474
if (!ConfigUsesStdIo(configurator.Client))
7575
continue;
7676

77+
// Skip clients that don't support the current transport setting —
78+
// Configure() would throw (e.g., Claude Desktop when HTTP is enabled).
79+
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
80+
if (useHttp && !configurator.Client.SupportsHttpTransport)
81+
continue;
82+
7783
MCPServiceLocator.Client.ConfigureClient(configurator);
7884
touchedAny = true;
7985
}

MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ public bool TryBuildCommand(out string fileName, out string arguments, out strin
4646
return false;
4747
}
4848

49-
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
50-
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
51-
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
49+
string devFlags = AssetPathUtility.GetUvxDevFlags();
5250
bool projectScopedTools = EditorPrefs.GetBool(
5351
EditorPrefKeys.ProjectScopedToolsLocalHttp,
5452
true

MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ private void ConfigureClaudeCliAsync(IMcpClientConfigurator client)
304304
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
305305
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
306306
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
307-
bool shouldForceRefresh = AssetPathUtility.ShouldForceUvxRefresh();
307+
string uvxDevFlags = AssetPathUtility.GetUvxDevFlags();
308308
string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
309309

310310
// Compute pathPrepend on main thread
@@ -331,7 +331,7 @@ private void ConfigureClaudeCliAsync(IMcpClientConfigurator client)
331331
cliConfigurator.ConfigureWithCapturedValues(
332332
projectDir, claudePath, pathPrepend,
333333
useHttpTransport, httpUrl,
334-
uvxPath, fromArgs, packageName, shouldForceRefresh,
334+
uvxPath, fromArgs, packageName, uvxDevFlags,
335335
apiKey, serverTransport);
336336
}
337337
return (success: true, error: (string)null);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using NUnit.Framework;
2+
using MCPForUnity.Editor.Helpers;
3+
using MCPForUnity.Editor.Constants;
4+
using UnityEditor;
5+
6+
namespace MCPForUnityTests.Editor.Helpers
7+
{
8+
public class AssetPathUtilityOfflineTests
9+
{
10+
private bool _originalForceRefresh;
11+
12+
[SetUp]
13+
public void SetUp()
14+
{
15+
_originalForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
16+
}
17+
18+
[TearDown]
19+
public void TearDown()
20+
{
21+
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, _originalForceRefresh);
22+
}
23+
24+
[Test]
25+
public void ShouldUseUvxOffline_WhenForceRefreshEnabled_ReturnsFalse()
26+
{
27+
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, true);
28+
Assert.IsFalse(AssetPathUtility.ShouldUseUvxOffline());
29+
}
30+
31+
[Test]
32+
public void ShouldUseUvxOffline_DoesNotThrow()
33+
{
34+
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
35+
Assert.DoesNotThrow(() => AssetPathUtility.ShouldUseUvxOffline());
36+
}
37+
}
38+
}

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/AssetPathUtilityOfflineTests.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageGameObjectTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,11 @@ public void SetComponentProperties_ContinuesAfterException()
323323
};
324324

325325
// Expect the error logs from the invalid property
326-
// Note: PropertyConversion logs "Error converting token to..." when conversion fails
326+
// Note: PropertyConversion logs "Error converting token to..." when conversion fails,
327+
// then ComponentOps catches the exception and returns an error string (no second Error log).
328+
// GameObjectComponentHelpers logs the failure as a warning.
327329
LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("Error converting token to UnityEngine.Vector3"));
328-
LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex(@"\[SetProperty\].*Failed to set 'velocity'"));
329-
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'velocity' not found"));
330+
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex(@"\[ManageGameObject\].*Failed to set property 'velocity'"));
330331

331332
// Act
332333
var result = ManageGameObject.HandleCommand(setPropertiesParams);

0 commit comments

Comments
 (0)