Skip to content

Conversation

@Jordonh18
Copy link
Contributor

@Jordonh18 Jordonh18 commented Dec 8, 2025

Issue

Unity logged [WebSocket] Unexpected receive error: HasKey can only be called from the main thread during WebSocket registration. The background receive loop invoked ToolDiscoveryService.GetEnabledTools(), which touches EditorPrefs.HasKey, a Unity Editor API that must run on the main thread.

Root Cause

Tool discovery was executed on a background thread in the WebSocket registration flow, violating Unity’s main-thread requirement for Editor APIs.

Fix

  • Added GetEnabledToolsOnMainThreadAsync() to marshal tool discovery onto the Unity main thread via EditorApplication.delayCall.
  • Updated SendRegisterToolsAsync to await this main-thread helper with cancellation checks.

Files Touched

  • MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs

Notes

  • Prevents the HasKey main-thread violation for anyone hitting WebSocket registration. Run/enter play mode to confirm the log is gone.

Summary by CodeRabbit

  • Bug Fixes
    • Improved tool registration reliability by ensuring operations safely execute on Unity's main thread, preventing potential crashes or hangs during tool discovery and registration processes.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 8, 2025

Walkthrough

The change introduces thread-safe tool access in WebSocketTransportClient by adding a new async helper method that marshals tool discovery operations to Unity's main thread via EditorApplication.delayCall, replacing the previous synchronous call in SendRegisterToolsAsync.

Changes

Cohort / File(s) Change Summary
Main-thread marshaling for tool discovery
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
Added GetEnabledToolsOnMainThreadAsync() helper method to defer tool retrieval to Unity's main thread. Updated SendRegisterToolsAsync() to use the new async method instead of synchronous GetEnabledTools() call, with cancellation checks before and after. Added UnityEditor using directive.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

  • Areas requiring attention:
    • Verify that EditorApplication.delayCall properly handles the task completion and exception propagation
    • Confirm cancellation token is checked and honored at appropriate points in the async flow
    • Ensure the List<ToolMetadata> returned from the main thread is safely accessible from the calling context

Possibly related issues

  • Unity pings to http server infinitely.. #441: This PR directly addresses the requirement to marshal Unity main-thread API access in WebSocketTransportClient by implementing async main-thread deference for tool discovery operations, preventing background-thread violations of Unity API constraints.

Poem

🐰 Threads were tangled, chaos in the queue,
Till async marshaling came into view!
Main thread awaits our tools with care,
EditorApplication.delayCall answers our prayer! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately summarizes the main change: fixing WebSocket tool registration by running it on the main thread, which directly addresses the root cause described in the PR objectives.
✨ 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: 1

📜 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 208cbf4.

📒 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
🔇 Additional comments (2)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (2)

15-15: LGTM!

The UnityEditor namespace is correctly added for EditorApplication.delayCall used in the new helper method.


446-448: Good addition of cancellation checks and async marshaling.

The changes correctly fix the main-thread violation by awaiting the new helper method, and the cancellation checks before and after the async call improve responsiveness during connection teardown.

Comment on lines +70 to +88
private Task<List<ToolMetadata>> GetEnabledToolsOnMainThreadAsync()
{
var tcs = new TaskCompletionSource<List<ToolMetadata>>(TaskCreationOptions.RunContinuationsAsynchronously);

EditorApplication.delayCall += () =>
{
try
{
var tools = _toolDiscoveryService?.GetEnabledTools() ?? new List<ToolMetadata>();
tcs.TrySetResult(tools);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
};

return tcs.Task;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Consider adding cancellation/timeout safeguards to prevent hanging.

The implementation correctly marshals tool discovery to Unity's main thread. However, if EditorApplication.delayCall callback never executes (e.g., during Unity shutdown or domain reload), the TaskCompletionSource would hang indefinitely, potentially blocking Dispose() which awaits the receive loop.

Consider registering a cancellation callback to handle scenarios where Unity stops executing delayCall callbacks:

-private Task<List<ToolMetadata>> GetEnabledToolsOnMainThreadAsync()
+private Task<List<ToolMetadata>> GetEnabledToolsOnMainThreadAsync(CancellationToken cancellationToken = default)
 {
     var tcs = new TaskCompletionSource<List<ToolMetadata>>(TaskCreationOptions.RunContinuationsAsynchronously);
+    
+    // Register cancellation to prevent indefinite hanging
+    if (cancellationToken.CanBeCanceled)
+    {
+        cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken));
+    }
 
     EditorApplication.delayCall += () =>
     {
         try
         {
             var tools = _toolDiscoveryService?.GetEnabledTools() ?? new List<ToolMetadata>();
             tcs.TrySetResult(tools);
         }
         catch (Exception ex)
         {
             tcs.TrySetException(ex);
         }
     };
 
     return tcs.Task;
 }

Then update the call site:

 token.ThrowIfCancellationRequested();
-var tools = await GetEnabledToolsOnMainThreadAsync().ConfigureAwait(false);
+var tools = await GetEnabledToolsOnMainThreadAsync(token).ConfigureAwait(false);
 token.ThrowIfCancellationRequested();

Committable suggestion skipped: line range outside the PR's diff.

@dsarno
Copy link
Collaborator

dsarno commented Dec 8, 2025

Thanks @Jordonh18 ! I added a guardrail and merged this as part of #443.

@dsarno dsarno closed this Dec 8, 2025
@Jordonh18
Copy link
Contributor Author

ahh perfect 😊

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.

3 participants