Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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);
}
}
Expand All @@ -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)
{
Expand All @@ -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)
Expand Down Expand Up @@ -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}";
Expand Down Expand Up @@ -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" +
Expand Down
90 changes: 90 additions & 0 deletions MCPForUnity/Editor/Helpers/AssetPathUtility.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Services;
Expand Down Expand Up @@ -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;

/// <summary>
/// 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).
/// </summary>
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;
}
}

/// <summary>
/// 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).
/// </summary>
public static string GetUvxDevFlags()
{
bool forceRefresh = ShouldForceUvxRefresh();
return GetUvxDevFlags(forceRefresh, !forceRefresh && GetCachedOfflineProbeResult());
}

/// <summary>
/// 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.
/// </summary>
public static string GetUvxDevFlags(bool forceRefresh, bool useOffline)
{
if (forceRefresh) return "--no-cache --refresh ";
if (useOffline) return "--offline ";
return string.Empty;
}

/// <summary>
/// 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).
/// </summary>
public static IReadOnlyList<string> GetUvxDevFlagsList()
{
bool forceRefresh = ShouldForceUvxRefresh();
if (forceRefresh) return new[] { "--no-cache", "--refresh" };
if (GetCachedOfflineProbeResult()) return new[] { "--offline" };
return Array.Empty<string>();
}

/// <summary>
/// Returns true if the server URL is a local path (file:// or absolute path).
/// </summary>
Expand Down
13 changes: 5 additions & 8 deletions MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@ namespace MCPForUnity.Editor.Helpers
/// </summary>
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)
Expand Down Expand Up @@ -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())
{
Expand Down Expand Up @@ -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())
{
Expand Down
8 changes: 2 additions & 6 deletions MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,8 @@ private static IList<string> BuildUvxArgs(string fromUrl, string packageName)
// Keep ordering consistent with other uvx builders: dev flags first, then --from <url>, then package name.
var args = new List<string>();

// 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())
Expand Down
6 changes: 6 additions & 0 deletions MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 1 addition & 3 deletions MCPForUnity/Editor/Services/Server/ServerCommandBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down