Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
09661a8
refactor: Split ParseColorOrDefault into two overloads and change def…
msanatan Jan 7, 2026
e48879b
Auto-format Python code
msanatan Jan 7, 2026
69e62bb
Remove unused Python module
msanatan Jan 7, 2026
7ee51f6
Refactored VFX functionality into multiple files
msanatan Jan 7, 2026
e68fbb6
Rename ManageVfx folder to just Vfx
msanatan Jan 7, 2026
47ba35f
Clean up whitespace on plugin tools and resources
msanatan Jan 7, 2026
8321db2
Make ManageGameObject less of a monolith by splitting it out into dif…
msanatan Jan 7, 2026
c038996
Remove obsolete FindObjectByInstruction method
msanatan Jan 7, 2026
c0d5182
Add local test harness for fast developer iteration
dsarno Jan 7, 2026
369c97f
Merge branch 'main' into pre-release-tidying
msanatan Jan 7, 2026
98f791b
Fix issue #525: Save dirty scenes for all test modes
Jan 7, 2026
d83c439
refactor: Consolidate editor state resources into single canonical im…
msanatan Jan 7, 2026
0a95563
Validate editor state with Pydantic models in both C# and Python
msanatan Jan 7, 2026
bcbba05
Consolidate run_tests and run_tests_async into single async implement…
msanatan Jan 7, 2026
93f1084
Validate test job responses with Pydantic models in Python
msanatan Jan 7, 2026
cfd81e2
Merge remote-tracking branch 'msanatan/pre-release-tidying' into feat…
dsarno Jan 7, 2026
88ed90a
Change resources URI from unity:// to mcpforunity://
msanatan Jan 7, 2026
07d1d9d
Update README with all tools + better listing for resources
msanatan Jan 7, 2026
c23412c
Update other references to resources
msanatan Jan 7, 2026
9569de6
Updated translated doc - unfortunately I cannot verify
msanatan Jan 7, 2026
21db63a
Update the Chinese translation of the dev docks
msanatan Jan 7, 2026
7e38e31
Change menu item from Setup Window to Local Setup Window
msanatan Jan 7, 2026
e155470
Fix URIs for menu items and tests
msanatan Jan 7, 2026
e03df44
Shouldn't have removed it
msanatan Jan 7, 2026
594e2a2
fix: add missing FAST_FAIL_TIMEOUT constant in PluginHub
dsarno Jan 7, 2026
c2f49a0
feat(ScriptableObject): enhance dry-run validation for AnimationCurve…
dsarno Jan 7, 2026
642f467
Merge msanatan/pre-release-tidying into pre-launch-enhancements
dsarno Jan 7, 2026
73030d2
test: fix integration tests after merge
dsarno Jan 7, 2026
2bd1712
Update warning message to apply to all test modes
dsarno Jan 7, 2026
df678cd
Merge pr-527: fix issue #525 (save dirty scenes for all test modes)
dsarno Jan 7, 2026
837fe20
feat(run_tests): add wait_timeout to get_test_job to avoid client loo…
dsarno Jan 7, 2026
9890f1b
fix: use Pydantic attribute access in test_run_tests_async for merge …
dsarno Jan 7, 2026
098a7ef
revert: remove local test harness - will be submitted in separate PR
dsarno Jan 7, 2026
4ff544c
fix: stdio transport survives test runs without UI flicker
dsarno Jan 8, 2026
3730ba7
Merge upstream/main into pre-launch-enhancements
dsarno Jan 8, 2026
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
14 changes: 4 additions & 10 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,8 +467,8 @@ private void Register()
else
{
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
bool devForceRefresh = GetDevModeForceRefresh();
string devFlags = devForceRefresh ? "--no-cache --refresh " : string.Empty;
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}";
}

Expand Down Expand Up @@ -586,8 +586,8 @@ public override string GetManualSnippet()
}

string gitUrl = AssetPathUtility.GetMcpServerGitUrl();
bool devForceRefresh = GetDevModeForceRefresh();
string devFlags = devForceRefresh ? "--no-cache --refresh " : string.Empty;
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;

return "# Register the MCP server with Claude Code:\n" +
$"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" mcp-for-unity\n\n" +
Expand All @@ -597,12 +597,6 @@ public override string GetManualSnippet()
"claude mcp list # Only works when claude is run in the project's directory";
}

private static bool GetDevModeForceRefresh()
{
try { return EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); }
catch { return false; }
}

public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Claude CLI is installed",
Expand Down
82 changes: 82 additions & 0 deletions MCPForUnity/Editor/Helpers/AssetPathUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,88 @@ public static (string uvxPath, string fromUrl, string packageName) GetUvxCommand
return (uvxPath, fromUrl, packageName);
}

/// <summary>
/// Determines whether uvx should use --no-cache --refresh flags.
/// Returns true if DevModeForceServerRefresh is enabled OR if the server URL is a local path.
/// Local paths (file:// or absolute) always need fresh builds to avoid stale uvx cache.
/// </summary>
public static bool ShouldForceUvxRefresh()
{
bool devForceRefresh = false;
try { devForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { }

if (devForceRefresh)
return true;

// Auto-enable force refresh when using a local path override.
return IsLocalServerPath();
}

/// <summary>
/// Returns true if the server URL is a local path (file:// or absolute path).
/// </summary>
public static bool IsLocalServerPath()
{
string fromUrl = GetMcpServerGitUrl();
if (string.IsNullOrEmpty(fromUrl))
return false;

// Check for file:// protocol or absolute local path
return fromUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase) ||
System.IO.Path.IsPathRooted(fromUrl);
}

/// <summary>
/// Gets the local server path if GitUrlOverride points to a local directory.
/// Returns null if not using a local path.
/// </summary>
public static string GetLocalServerPath()
{
if (!IsLocalServerPath())
return null;

string fromUrl = GetMcpServerGitUrl();
if (fromUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
// Strip file:// prefix
fromUrl = fromUrl.Substring(7);
}

return fromUrl;
}

/// <summary>
/// Cleans stale Python build artifacts from the local server path.
/// This is necessary because Python's build system doesn't remove deleted files from build/,
/// and the auto-discovery mechanism will pick up old .py files causing ghost resources/tools.
/// </summary>
/// <returns>True if cleaning was performed, false if not applicable or failed.</returns>
public static bool CleanLocalServerBuildArtifacts()
{
string localPath = GetLocalServerPath();
if (string.IsNullOrEmpty(localPath))
return false;

// Clean the build/ directory which can contain stale .py files
string buildPath = System.IO.Path.Combine(localPath, "build");
if (System.IO.Directory.Exists(buildPath))
{
try
{
System.IO.Directory.Delete(buildPath, recursive: true);
McpLog.Info($"Cleaned stale build artifacts from: {buildPath}");
return true;
}
catch (Exception ex)
{
McpLog.Warn($"Failed to clean build artifacts: {ex.Message}");
return false;
}
}

return false;
}

/// <summary>
/// Gets the package version from package.json
/// </summary>
Expand Down
23 changes: 5 additions & 18 deletions MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,11 @@ namespace MCPForUnity.Editor.Helpers
/// </summary>
public static class CodexConfigHelper
{
private static bool GetDevModeForceRefresh()
{
try
{
return EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
}
catch
{
return false;
}
}

private static void AddDevModeArgs(TomlArray args, bool devForceRefresh)
private static void AddDevModeArgs(TomlArray args)
{
if (args == null) return;
if (!devForceRefresh) return;
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
if (!AssetPathUtility.ShouldForceUvxRefresh()) return;
args.Add(new TomlString { Value = "--no-cache" });
args.Add(new TomlString { Value = "--refresh" });
}
Expand All @@ -59,12 +48,11 @@ public static string BuildCodexServerBlock(string uvPath)
{
// Stdio mode: Use command and args
var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
bool devForceRefresh = GetDevModeForceRefresh();

unityMCP["command"] = uvxPath;

var args = new TomlArray();
AddDevModeArgs(args, devForceRefresh);
AddDevModeArgs(args);
if (!string.IsNullOrEmpty(fromUrl))
{
args.Add(new TomlString { Value = "--from" });
Expand Down Expand Up @@ -209,12 +197,11 @@ private static TomlTable CreateUnityMcpTable(string uvPath)
{
// Stdio mode: Use command and args
var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
bool devForceRefresh = GetDevModeForceRefresh();

unityMCP["command"] = new TomlString { Value = uvxPath };

var argsArray = new TomlArray();
AddDevModeArgs(argsArray, devForceRefresh);
AddDevModeArgs(argsArray);
if (!string.IsNullOrEmpty(fromUrl))
{
argsArray.Add(new TomlString { Value = "--from" });
Expand Down
15 changes: 8 additions & 7 deletions MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa
private static void PopulateUnityNode(JObject unity, string uvPath, McpClient client, bool isVSCode)
{
// Get transport preference (default to HTTP)
bool useHttpTransport = client?.SupportsHttpTransport != false && EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
bool prefValue = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
bool clientSupportsHttp = client?.SupportsHttpTransport != false;
bool useHttpTransport = clientSupportsHttp && prefValue;
string httpProperty = string.IsNullOrEmpty(client?.HttpUrlProperty) ? "url" : client.HttpUrlProperty;
var urlPropsToRemove = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "url", "serverUrl" };
urlPropsToRemove.Remove(httpProperty);
Expand Down Expand Up @@ -81,10 +83,7 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl
// Stdio mode: Use uvx command
var (uvxPath, fromUrl, packageName) = AssetPathUtility.GetUvxCommandParts();

bool devForceRefresh = false;
try { devForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { }

var toolArgs = BuildUvxArgs(fromUrl, packageName, devForceRefresh);
var toolArgs = BuildUvxArgs(fromUrl, packageName);

if (ShouldUseWindowsCmdShim(client))
{
Expand Down Expand Up @@ -152,13 +151,15 @@ private static JObject EnsureObject(JObject parent, string name)
return created;
}

private static IList<string> BuildUvxArgs(string fromUrl, string packageName, bool devForceRefresh)
private static IList<string> BuildUvxArgs(string fromUrl, string packageName)
{
// Dev mode: force a fresh install/resolution (avoids stale cached builds while iterating).
// `--no-cache` is the key flag; `--refresh` ensures metadata is revalidated.
// Keep ordering consistent with other uvx builders: dev flags first, then --from <url>, then package name.
var args = new List<string>();
if (devForceRefresh)

// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
if (AssetPathUtility.ShouldForceUvxRefresh())
{
args.Add("--no-cache");
args.Add("--refresh");
Expand Down
14 changes: 14 additions & 0 deletions MCPForUnity/Editor/Services/ClientConfigurationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Clients;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;

namespace MCPForUnity.Editor.Services
Expand All @@ -22,11 +23,24 @@ public ClientConfigurationService()

public void ConfigureClient(IMcpClientConfigurator configurator)
{
// When using a local server path, clean stale build artifacts first.
// This prevents old deleted .py files from being picked up by Python's auto-discovery.
if (AssetPathUtility.IsLocalServerPath())
{
AssetPathUtility.CleanLocalServerBuildArtifacts();
}

configurator.Configure();
}

public ClientConfigurationSummary ConfigureAllDetectedClients()
{
// When using a local server path, clean stale build artifacts once before configuring all clients.
if (AssetPathUtility.IsLocalServerPath())
{
AssetPathUtility.CleanLocalServerBuildArtifacts();
}

var summary = new ClientConfigurationSummary();
foreach (var configurator in configurators)
{
Expand Down
9 changes: 5 additions & 4 deletions MCPForUnity/Editor/Services/ServerManagementService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,9 @@ private string GetPlatformSpecificPathPrepend()
/// </summary>
public bool StartLocalHttpServer()
{
/// Clean stale Python build artifacts when using a local dev server path
AssetPathUtility.CleanLocalServerBuildArtifacts();

if (!TryGetLocalHttpServerCommandParts(out _, out _, out var displayCommand, out var error))
{
EditorUtility.DisplayDialog(
Expand Down Expand Up @@ -1236,10 +1239,8 @@ private bool TryGetLocalHttpServerCommandParts(out string fileName, out string a
return false;
}

bool devForceRefresh = false;
try { devForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); } catch { }

string devFlags = devForceRefresh ? "--no-cache --refresh " : string.Empty;
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
string args = string.IsNullOrEmpty(fromUrl)
? $"{devFlags}{packageName} --transport http --http-url {httpUrl}"
: $"{devFlags}--from {fromUrl} {packageName} --transport http --http-url {httpUrl}";
Expand Down
20 changes: 15 additions & 5 deletions MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ private static void OnBeforeAssemblyReload()
bool useHttp = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
// Check both TransportManager AND StdioBridgeHost directly, because CI starts via StdioBridgeHost
// bypassing TransportManager state.
bool isRunning = MCPServiceLocator.TransportManager.IsRunning(TransportMode.Stdio)
|| StdioBridgeHost.IsRunning;
bool tmRunning = MCPServiceLocator.TransportManager.IsRunning(TransportMode.Stdio);
bool hostRunning = StdioBridgeHost.IsRunning;
bool isRunning = tmRunning || hostRunning;
bool shouldResume = !useHttp && isRunning;

if (shouldResume)
Expand Down Expand Up @@ -60,10 +61,12 @@ private static void OnAfterAssemblyReload()
bool resume = false;
try
{
resume = EditorPrefs.GetBool(EditorPrefKeys.ResumeStdioAfterReload, false);
bool resumeFlag = EditorPrefs.GetBool(EditorPrefKeys.ResumeStdioAfterReload, false);
bool useHttp = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
resume = resume && !useHttp;
if (resume)
resume = resumeFlag && !useHttp;

// If we're not going to resume, clear the flag immediately to avoid stuck "Resuming..." state
if (!resume)
{
EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload);
}
Expand All @@ -87,6 +90,13 @@ private static void TryStartBridgeImmediate()
var startTask = MCPServiceLocator.TransportManager.StartAsync(TransportMode.Stdio);
startTask.ContinueWith(t =>
{
// Clear the flag after attempting to start (success or failure).
// This prevents getting stuck in "Resuming..." state.
// We do this synchronously on the continuation thread - it's safe because
// EditorPrefs operations are thread-safe and any new reload will set the flag
// fresh in OnBeforeAssemblyReload before we get here.
try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); } catch { }

if (t.IsFaulted)
{
var baseEx = t.Exception?.GetBaseException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ private void InitializeUI()
}

claudeCliPathRow.style.display = DisplayStyle.None;

// Initialize the configuration display for the first selected client
UpdateClientStatus();
UpdateManualConfiguration();
UpdateClaudeCliPathVisibility();
}

private void RegisterCallbacks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ private void RegisterCallbacks()
var selected = (TransportProtocol)evt.newValue;
bool useHttp = selected != TransportProtocol.Stdio;
EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, useHttp);

// Clear any stale resume flags when user manually changes transport
try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); } catch { }
try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeHttpAfterReload); } catch { }

if (useHttp)
{
Expand Down Expand Up @@ -274,7 +278,9 @@ public void UpdateConnectionStatus()
bool isRunning = bridgeService.IsRunning;
bool showLocalServerControls = IsHttpLocalSelected();
bool debugMode = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
bool stdioSelected = transportDropdown != null && (TransportProtocol)transportDropdown.value == TransportProtocol.Stdio;
// Use EditorPrefs as source of truth for stdio selection - more reliable after domain reload
// than checking the dropdown which may not be initialized yet
bool stdioSelected = !EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);

// Keep the Start/Stop Server button label in sync even when the session is not running
// (e.g., orphaned server after a domain reload).
Expand Down Expand Up @@ -327,19 +333,38 @@ public void UpdateConnectionStatus()
statusIndicator.RemoveFromClassList("disconnected");
statusIndicator.AddToClassList("connected");
connectionToggleButton.text = "End Session";
connectionToggleButton.SetEnabled(true); // Re-enable in case it was disabled during resumption

// Force the UI to reflect the actual port being used
unityPortField.value = bridgeService.CurrentPort.ToString();
unityPortField.SetEnabled(false);
}
else
{
connectionStatusLabel.text = "No Session";
statusIndicator.RemoveFromClassList("connected");
statusIndicator.AddToClassList("disconnected");
connectionToggleButton.text = "Start Session";
// Check if we're resuming the stdio bridge after a domain reload.
// During this brief window, show "Resuming..." instead of "No Session" to avoid UI flicker.
bool isStdioResuming = stdioSelected
&& EditorPrefs.GetBool(EditorPrefKeys.ResumeStdioAfterReload, false);

if (isStdioResuming)
{
connectionStatusLabel.text = "Resuming...";
// Keep the indicator in a neutral/transitional state
statusIndicator.RemoveFromClassList("connected");
statusIndicator.RemoveFromClassList("disconnected");
connectionToggleButton.text = "Start Session";
connectionToggleButton.SetEnabled(false);
}
else
{
connectionStatusLabel.text = "No Session";
statusIndicator.RemoveFromClassList("connected");
statusIndicator.AddToClassList("disconnected");
connectionToggleButton.text = "Start Session";
connectionToggleButton.SetEnabled(true);
}

unityPortField.SetEnabled(true);
unityPortField.SetEnabled(!isStdioResuming);

healthStatusLabel.text = HealthStatusUnknown;
healthIndicator.RemoveFromClassList("healthy");
Expand Down
Loading