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: 14 additions & 4 deletions .github/workflows/unity-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,11 @@ jobs:
unityVersion:
- 2021.3.45f2
steps:
# Checkout
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true

# Cache
- uses: actions/cache@v4
with:
path: ${{ matrix.projectPath }}/Library
Expand All @@ -40,7 +38,20 @@ jobs:
Library-${{ matrix.projectPath }}-
Library-

# Test
# Run domain reload tests first (they're [Explicit] so need explicit category)
- name: Run domain reload tests
uses: game-ci/unity-test-runner@v4
id: domain-tests
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: ${{ matrix.testMode }}
customParameters: -testCategory domain_reload

- name: Run tests
uses: game-ci/unity-test-runner@v4
id: tests
Expand All @@ -53,7 +64,6 @@ jobs:
unityVersion: ${{ matrix.unityVersion }}
testMode: ${{ matrix.testMode }}

# Upload test results
- uses: actions/upload-artifact@v4
if: always()
with:
Expand Down
29 changes: 29 additions & 0 deletions MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json.Linq;

namespace MCPForUnity.Editor.Resources.Editor
{
/// <summary>
/// Provides a cached, v2 readiness snapshot. This is designed to remain responsive even when Unity is busy.
/// </summary>
[McpForUnityResource("get_editor_state_v2")]
public static class EditorStateV2
{
public static object HandleCommand(JObject @params)
{
try
{
var snapshot = EditorStateCache.GetSnapshot();
return new SuccessResponse("Retrieved editor state (v2).", snapshot);
}
catch (Exception e)
{
return new ErrorResponse($"Error getting editor state (v2): {e.Message}");
}
}
}
}


11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Resources/Editor/EditorStateV2.cs.meta

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

234 changes: 234 additions & 0 deletions MCPForUnity/Editor/Services/EditorStateCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
using System;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditorInternal;
using UnityEditor.SceneManagement;
using UnityEngine;

namespace MCPForUnity.Editor.Services
{
/// <summary>
/// Maintains a cached readiness snapshot (v2) so status reads remain fast even when Unity is busy.
/// Updated on the main thread via Editor callbacks and periodic update ticks.
/// </summary>
[InitializeOnLoad]
internal static class EditorStateCache
{
private static readonly object LockObj = new();
private static long _sequence;
private static long _observedUnixMs;

private static bool _lastIsCompiling;
private static long? _lastCompileStartedUnixMs;
private static long? _lastCompileFinishedUnixMs;

private static bool _domainReloadPending;
private static long? _domainReloadBeforeUnixMs;
private static long? _domainReloadAfterUnixMs;

private static double _lastUpdateTimeSinceStartup;
private const double MinUpdateIntervalSeconds = 0.25;

private static JObject _cached;

static EditorStateCache()
{
try
{
_sequence = 0;
_observedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
_cached = BuildSnapshot("init");

EditorApplication.update += OnUpdate;
EditorApplication.playModeStateChanged += _ => ForceUpdate("playmode");

AssemblyReloadEvents.beforeAssemblyReload += () =>
{
_domainReloadPending = true;
_domainReloadBeforeUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
ForceUpdate("before_domain_reload");
};
AssemblyReloadEvents.afterAssemblyReload += () =>
{
_domainReloadPending = false;
_domainReloadAfterUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
ForceUpdate("after_domain_reload");
};
}
catch (Exception ex)
{
McpLog.Error($"[EditorStateCache] Failed to initialise: {ex.Message}\n{ex.StackTrace}");
}
}

private static void OnUpdate()
{
// Throttle to reduce overhead while keeping the snapshot fresh enough for polling clients.
double now = EditorApplication.timeSinceStartup;
if (now - _lastUpdateTimeSinceStartup < MinUpdateIntervalSeconds)
{
// Still update on compilation edge transitions to keep timestamps meaningful.
bool isCompiling = EditorApplication.isCompiling;
if (isCompiling == _lastIsCompiling)
{
return;
}
}

_lastUpdateTimeSinceStartup = now;
ForceUpdate("tick");
}

private static void ForceUpdate(string reason)
{
lock (LockObj)
{
_cached = BuildSnapshot(reason);
}
}

private static JObject BuildSnapshot(string reason)
{
_sequence++;
_observedUnixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

bool isCompiling = EditorApplication.isCompiling;
if (isCompiling && !_lastIsCompiling)
{
_lastCompileStartedUnixMs = _observedUnixMs;
}
else if (!isCompiling && _lastIsCompiling)
{
_lastCompileFinishedUnixMs = _observedUnixMs;
}
_lastIsCompiling = isCompiling;

var scene = EditorSceneManager.GetActiveScene();
string scenePath = string.IsNullOrEmpty(scene.path) ? null : scene.path;
string sceneGuid = !string.IsNullOrEmpty(scenePath) ? AssetDatabase.AssetPathToGUID(scenePath) : null;

bool testsRunning = TestRunStatus.IsRunning;
var testsMode = TestRunStatus.Mode?.ToString();
string currentJobId = TestJobManager.CurrentJobId;
bool isFocused = InternalEditorUtility.isApplicationActive;

var activityPhase = "idle";
if (testsRunning)
{
activityPhase = "running_tests";
}
else if (isCompiling)
{
activityPhase = "compiling";
}
else if (_domainReloadPending)
{
activityPhase = "domain_reload";
}
else if (EditorApplication.isUpdating)
{
activityPhase = "asset_import";
}
else if (EditorApplication.isPlayingOrWillChangePlaymode)
{
activityPhase = "playmode_transition";
}

// Keep this as a plain JSON object for minimal friction with transports.
return JObject.FromObject(new
{
schema_version = "unity-mcp/editor_state@2",
observed_at_unix_ms = _observedUnixMs,
sequence = _sequence,
unity = new
{
instance_id = (string)null,
unity_version = Application.unityVersion,
project_id = (string)null,
platform = Application.platform.ToString(),
is_batch_mode = Application.isBatchMode
},
editor = new
{
is_focused = isFocused,
play_mode = new
{
is_playing = EditorApplication.isPlaying,
is_paused = EditorApplication.isPaused,
is_changing = EditorApplication.isPlayingOrWillChangePlaymode
},
active_scene = new
{
path = scenePath,
guid = sceneGuid,
name = scene.name ?? string.Empty
}
},
activity = new
{
phase = activityPhase,
since_unix_ms = _observedUnixMs,
reasons = new[] { reason }
},
compilation = new
{
is_compiling = isCompiling,
is_domain_reload_pending = _domainReloadPending,
last_compile_started_unix_ms = _lastCompileStartedUnixMs,
last_compile_finished_unix_ms = _lastCompileFinishedUnixMs,
last_domain_reload_before_unix_ms = _domainReloadBeforeUnixMs,
last_domain_reload_after_unix_ms = _domainReloadAfterUnixMs
},
assets = new
{
is_updating = EditorApplication.isUpdating,
external_changes_dirty = false,
external_changes_last_seen_unix_ms = (long?)null,
refresh = new
{
is_refresh_in_progress = false,
last_refresh_requested_unix_ms = (long?)null,
last_refresh_finished_unix_ms = (long?)null
}
},
tests = new
{
is_running = testsRunning,
mode = testsMode,
current_job_id = string.IsNullOrEmpty(currentJobId) ? null : currentJobId,
started_unix_ms = TestRunStatus.StartedUnixMs,
started_by = "unknown",
last_run = TestRunStatus.FinishedUnixMs.HasValue
? new
{
finished_unix_ms = TestRunStatus.FinishedUnixMs,
result = "unknown",
counts = (object)null
}
: null
},
transport = new
{
unity_bridge_connected = (bool?)null,
last_message_unix_ms = (long?)null
}
});
}

public static JObject GetSnapshot()
{
lock (LockObj)
{
// Defensive: if something went wrong early, rebuild once.
if (_cached == null)
{
_cached = BuildSnapshot("rebuild");
}
return (JObject)_cached.DeepClone();
}
}
}
}


11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Services/EditorStateCache.cs.meta

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

Loading