@@ -38,6 +38,7 @@ private static Dictionary<
3838 string ,
3939 ( string commandJson , TaskCompletionSource < string > tcs )
4040 > commandQueue = new ( ) ;
41+ private static int mainThreadId ;
4142 private static int currentUnityPort = 6400 ; // Dynamic port, starts with default
4243 private static bool isAutoConnectMode = false ;
4344 private const ulong MaxFrameBytes = 64UL * 1024 * 1024 ; // 64 MiB hard cap for framed payloads
@@ -109,6 +110,8 @@ public static bool FolderExists(string path)
109110
110111 static MCPForUnityBridge ( )
111112 {
113+ // Record the main thread ID for safe thread checks
114+ try { mainThreadId = Thread . CurrentThread . ManagedThreadId ; } catch { mainThreadId = 0 ; }
112115 // Skip bridge in headless/batch environments (CI/builds) unless explicitly allowed via env
113116 // CI override: set UNITY_MCP_ALLOW_BATCH=1 to allow the bridge in batch mode
114117 if ( Application . isBatchMode && string . IsNullOrWhiteSpace ( Environment . GetEnvironmentVariable ( "UNITY_MCP_ALLOW_BATCH" ) ) )
@@ -539,7 +542,39 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken
539542 commandQueue [ commandId ] = ( commandText , tcs ) ;
540543 }
541544
542- string response = await tcs . Task . ConfigureAwait ( false ) ;
545+ // Wait for the handler to produce a response, but do not block indefinitely
546+ string response ;
547+ try
548+ {
549+ using var respCts = new CancellationTokenSource ( FrameIOTimeoutMs ) ;
550+ var completed = await Task . WhenAny ( tcs . Task , Task . Delay ( FrameIOTimeoutMs , respCts . Token ) ) . ConfigureAwait ( false ) ;
551+ if ( completed == tcs . Task )
552+ {
553+ // Got a result from the handler
554+ respCts . Cancel ( ) ;
555+ response = tcs . Task . Result ;
556+ }
557+ else
558+ {
559+ // Timeout: return a structured error so the client can recover
560+ var timeoutResponse = new
561+ {
562+ status = "error" ,
563+ error = $ "Command processing timed out after { FrameIOTimeoutMs } ms",
564+ } ;
565+ response = JsonConvert . SerializeObject ( timeoutResponse ) ;
566+ }
567+ }
568+ catch ( Exception ex )
569+ {
570+ var errorResponse = new
571+ {
572+ status = "error" ,
573+ error = ex . Message ,
574+ } ;
575+ response = JsonConvert . SerializeObject ( errorResponse ) ;
576+ }
577+
543578 byte [ ] responseBytes = System . Text . Encoding . UTF8 . GetBytes ( response ) ;
544579 await WriteFrameAsync ( stream , responseBytes ) ;
545580 }
@@ -816,6 +851,60 @@ private static void ProcessCommands()
816851 }
817852 }
818853
854+ // Invoke the given function on the Unity main thread and wait up to timeoutMs for the result.
855+ // Returns null on timeout or error; caller should provide a fallback error response.
856+ private static object InvokeOnMainThreadWithTimeout ( Func < object > func , int timeoutMs )
857+ {
858+ if ( func == null ) return null ;
859+ try
860+ {
861+ // If we are already on the main thread, execute directly to avoid deadlocks
862+ try
863+ {
864+ if ( Thread . CurrentThread . ManagedThreadId == mainThreadId )
865+ {
866+ return func ( ) ;
867+ }
868+ }
869+ catch { }
870+
871+ object result = null ;
872+ Exception captured = null ;
873+ var tcs = new TaskCompletionSource < bool > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
874+ EditorApplication . delayCall += ( ) =>
875+ {
876+ try
877+ {
878+ result = func ( ) ;
879+ }
880+ catch ( Exception ex )
881+ {
882+ captured = ex ;
883+ }
884+ finally
885+ {
886+ try { tcs . TrySetResult ( true ) ; } catch { }
887+ }
888+ } ;
889+
890+ // Wait for completion with timeout (Editor thread will pump delayCall)
891+ bool completed = tcs . Task . Wait ( timeoutMs ) ;
892+ if ( ! completed )
893+ {
894+ return null ; // timeout
895+ }
896+ if ( captured != null )
897+ {
898+ return Response . Error ( $ "Main thread handler error: { captured . Message } ") ;
899+ }
900+ return result ;
901+ }
902+ catch ( Exception ex )
903+ {
904+ return Response . Error ( $ "Failed to invoke on main thread: { ex . Message } ") ;
905+ }
906+ }
907+
819908 // Helper method to check if a string is valid JSON
820909 private static bool IsValidJson ( string text )
821910 {
@@ -880,7 +969,8 @@ private static string ExecuteCommand(Command command)
880969 // Maps the command type (tool name) to the corresponding handler's static HandleCommand method
881970 // Assumes each handler class has a static method named 'HandleCommand' that takes JObject parameters
882971 "manage_script" => ManageScript . HandleCommand ( paramsObject ) ,
883- "manage_scene" => ManageScene . HandleCommand ( paramsObject ) ,
972+ // Run scene operations on the main thread to avoid deadlocks/hangs
973+ "manage_scene" => InvokeOnMainThreadWithTimeout ( ( ) => ManageScene . HandleCommand ( paramsObject ) , FrameIOTimeoutMs ) ?? Response . Error ( "manage_scene timed out on main thread" ) ,
884974 "manage_editor" => ManageEditor . HandleCommand ( paramsObject ) ,
885975 "manage_gameobject" => ManageGameObject . HandleCommand ( paramsObject ) ,
886976 "manage_asset" => ManageAsset . HandleCommand ( paramsObject ) ,
0 commit comments