Skip to content

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Dec 8, 2025

Summary by CodeRabbit

  • Chores
    • Tool registration now gathers enabled tools on the main thread asynchronously, with improved cancellation handling and safer error handling.
  • Refactor
    • Cleaned and reorganized editor window imports and internal structure without changing visible behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 8, 2025

Walkthrough

A new async helper schedules enabled-tool retrieval on the Unity main thread via EditorApplication.delayCall and replaces the prior synchronous collection in SendRegisterToolsAsync; a separate editor window file had import/format cleanup only.

Changes

Cohort / File(s) Summary
Main-thread tool retrieval
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
Added GetEnabledToolsOnMainThreadAsync(CancellationToken) which schedules tool collection on the Unity main thread using EditorApplication.delayCall with cancellation and exception handling; SendRegisterToolsAsync now awaits this helper; added UnityEditor using.
Editor window tidy
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Reordered/cleaned using directives and minor structural cleanup; no functional or public API changes.
Project file
*.csproj
Manifest entry updated (unchanged summary from diff).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Inspect GetEnabledToolsOnMainThreadAsync for correct TaskCompletionSource usage and that exceptions/cancellations propagate to callers.
  • Verify cancellation token checks before and after the scheduled main-thread callback in SendRegisterToolsAsync.
  • Confirm no deadlocks or ordering issues when registration awaits main-thread work.

Possibly related PRs

  • [FEATURE] Batch Commands #418 — similar changes to tool collection during registration; this PR shifts retrieval to the main thread via a new async helper.

Suggested reviewers

  • Scriptwonder

Poem

🐰 Hopping threads where tools convene,
I schedule calls on Unity's green,
Async carrots, tokens held tight,
Main-thread blossoms in morning light,
A tiny rabbit cheers the sight! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix/websocket queue to main thread' directly aligns with the primary change: moving tool retrieval to the Unity main thread via EditorApplication.delayCall to ensure thread safety.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (1)

70-99: Well-implemented main thread marshalling.

The use of EditorApplication.delayCall with TaskCompletionSource properly handles Unity's main-thread requirement for tool discovery. The cancellation handling, race condition checks (IsCompleted), and exception handling are all correctly implemented.

Consider moving registration.Dispose() to a finally block to guarantee cleanup even if GetEnabledTools() throws:

 EditorApplication.delayCall += () =>
 {
     try
     {
-        // Clean up the registration once we're running
-        registration.Dispose();
-
         if (tcs.Task.IsCompleted)
         {
             return;
         }

         var tools = _toolDiscoveryService?.GetEnabledTools() ?? new List<ToolMetadata>();
         tcs.TrySetResult(tools);
     }
     catch (Exception ex)
     {
         tcs.TrySetException(ex);
+    }
+    finally
+    {
+        registration.Dispose();
     }
 };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd44ab3 and 1a900a7.

📒 Files selected for processing (1)
  • MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
🧬 Code graph analysis (1)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (2)
MCPForUnity/Editor/Services/IToolDiscoveryService.cs (4)
  • List (44-44)
  • List (54-54)
  • ToolMetadata (8-22)
  • ToolMetadata (49-49)
MCPForUnity/Editor/Services/ToolDiscoveryService.cs (5)
  • List (19-60)
  • List (72-77)
  • List (159-193)
  • ToolMetadata (62-70)
  • ToolMetadata (107-157)
🔇 Additional comments (2)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (2)

457-459: LGTM! Proper async tool retrieval with cancellation checks.

The replacement of synchronous tool retrieval with the new async method correctly ensures tool discovery runs on Unity's main thread. The cancellation checks before and after the operation provide appropriate safeguards.


15-15: LGTM!

Required for EditorApplication.delayCall used in the new marshalling method.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (1)

70-101: Main-thread marshalling via delayCall is solid; consider unsubscribing on cancellation

This TCS + EditorApplication.delayCall pattern cleanly moves GetEnabledTools onto the main thread, and the cancellation registration avoids hangs during shutdown. One optional refinement: also unsubscribe the delayCall callback when the token is cancelled before the callback runs, so you don’t keep a stale closure hanging off the editor event.

An example refactor keeping behavior but explicitly unsubscribing:

-        private Task<List<ToolMetadata>> GetEnabledToolsOnMainThreadAsync(CancellationToken token)
-        {
-            var tcs = new TaskCompletionSource<List<ToolMetadata>>(TaskCreationOptions.RunContinuationsAsynchronously);
-
-            // Register cancellation to break the deadlock if StopAsync is called while waiting for main thread
-            var registration = token.Register(() => tcs.TrySetCanceled());
-
-            EditorApplication.delayCall += () =>
-            {
-                try
-                {
-                    if (tcs.Task.IsCompleted)
-                    {
-                        return;
-                    }
-
-                    var tools = _toolDiscoveryService?.GetEnabledTools() ?? new List<ToolMetadata>();
-                    tcs.TrySetResult(tools);
-                }
-                catch (Exception ex)
-                {
-                    tcs.TrySetException(ex);
-                }
-                finally
-                {
-                    // Ensure registration is disposed even if discovery throws
-                    registration.Dispose();
-                }
-            };
-
-            return tcs.Task;
-        }
+        private Task<List<ToolMetadata>> GetEnabledToolsOnMainThreadAsync(CancellationToken token)
+        {
+            var tcs = new TaskCompletionSource<List<ToolMetadata>>(TaskCreationOptions.RunContinuationsAsynchronously);
+
+            EditorApplication.CallbackFunction callback = null;
+            CancellationTokenRegistration registration = default;
+
+            callback = () =>
+            {
+                try
+                {
+                    if (tcs.Task.IsCompleted)
+                    {
+                        return;
+                    }
+
+                    var tools = _toolDiscoveryService?.GetEnabledTools() ?? new List<ToolMetadata>();
+                    tcs.TrySetResult(tools);
+                }
+                catch (Exception ex)
+                {
+                    tcs.TrySetException(ex);
+                }
+                finally
+                {
+                    // Ensure registration is disposed and the callback is unsubscribed even if discovery throws
+                    registration.Dispose();
+                    EditorApplication.delayCall -= callback;
+                }
+            };
+
+            EditorApplication.delayCall += callback;
+
+            // Register cancellation to break the deadlock if StopAsync is called while waiting for main thread
+            registration = token.Register(() =>
+            {
+                EditorApplication.delayCall -= callback;
+                tcs.TrySetCanceled();
+            });
+
+            return tcs.Task;
+        }

This keeps the main-thread behavior and cancellation guarantees, while also cleaning up the event subscription eagerly on cancel.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a900a7 and 383c0c1.

📒 Files selected for processing (2)
  • MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (3 hunks)
  • MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
🔇 Additional comments (2)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (2)

11-16: Editor-only using directives look appropriate

Bringing in MCPForUnity.Editor.Services and UnityEditor here matches the use of IToolDiscoveryService and EditorApplication.delayCall in this editor-only transport class; no issues from my side.

Please just confirm this file is compiled into an editor-only assembly (path suggests it is) so that UnityEditor references never end up in player builds.


455-463: Cancellation-aware tool registration flow looks good

The added ThrowIfCancellationRequested calls before and after GetEnabledToolsOnMainThreadAsync ensure you don’t proceed with tool registration once the connection is being torn down, and the ConfigureAwait(false) keeps the heavy JSON work off the main thread. No further changes suggested.

@dsarno dsarno merged commit 0c8d2aa into CoplayDev:main Dec 8, 2025
1 check passed
@dsarno dsarno deleted the fix/websocket-queue-to-main-thread branch December 8, 2025 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants