Skip to content

Commit 2fd74f5

Browse files
committed
manage_scene: tolerant params + optional buildIndex; add writer IO logs; keep direct write path
1 parent 397ba32 commit 2fd74f5

File tree

3 files changed

+113
-17
lines changed

3 files changed

+113
-17
lines changed

UnityMcpBridge/Editor/MCPForUnityBridge.cs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Concurrent;
34
using System.IO;
45
using System.Linq;
56
using System.Net;
@@ -25,6 +26,14 @@ public static partial class MCPForUnityBridge
2526
private static readonly object startStopLock = new();
2627
private static readonly object clientsLock = new();
2728
private static readonly System.Collections.Generic.HashSet<TcpClient> activeClients = new();
29+
// Single-writer outbox for framed responses
30+
private class Outbound
31+
{
32+
public byte[] Payload;
33+
public string Tag;
34+
public int? ReqId;
35+
}
36+
private static readonly BlockingCollection<Outbound> _outbox = new(new ConcurrentQueue<Outbound>());
2837
private static CancellationTokenSource cts;
2938
private static Task listenerTask;
3039
private static int processingCommands = 0;
@@ -44,6 +53,10 @@ private static Dictionary<
4453
private const ulong MaxFrameBytes = 64UL * 1024 * 1024; // 64 MiB hard cap for framed payloads
4554
private const int FrameIOTimeoutMs = 30000; // Per-read timeout to avoid stalled clients
4655

56+
// IO diagnostics
57+
private static long _ioSeq = 0;
58+
private static void IoInfo(string s) { McpLog.Info(s, always: false); }
59+
4760
// Debug helpers
4861
private static bool IsDebugEnabled()
4962
{
@@ -112,6 +125,35 @@ static MCPForUnityBridge()
112125
{
113126
// Record the main thread ID for safe thread checks
114127
try { mainThreadId = Thread.CurrentThread.ManagedThreadId; } catch { mainThreadId = 0; }
128+
// Start single writer thread for framed responses
129+
try
130+
{
131+
var writerThread = new Thread(() =>
132+
{
133+
foreach (var item in _outbox.GetConsumingEnumerable())
134+
{
135+
try
136+
{
137+
long seq = Interlocked.Increment(ref _ioSeq);
138+
IoInfo($"[IO] ➜ write start seq={seq} tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")}");
139+
var sw = System.Diagnostics.Stopwatch.StartNew();
140+
// Note: We currently have a per-connection 'stream' in the client handler. For simplicity,
141+
// writes are performed inline there. This outbox provides single-writer semantics; if a shared
142+
// stream is introduced, redirect here accordingly.
143+
// No-op: actual write happens in client loop using WriteFrameAsync
144+
sw.Stop();
145+
IoInfo($"[IO] ✓ write end tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")} durMs={sw.Elapsed.TotalMilliseconds:F1}");
146+
}
147+
catch (Exception ex)
148+
{
149+
IoInfo($"[IO] ✗ write FAIL tag={item.Tag} reqId={(item.ReqId?.ToString() ?? "?")} {ex.GetType().Name}: {ex.Message}");
150+
}
151+
}
152+
}) { IsBackground = true, Name = "MCP-Writer" };
153+
writerThread.Start();
154+
}
155+
catch { }
156+
115157
// Skip bridge in headless/batch environments (CI/builds) unless explicitly allowed via env
116158
// CI override: set UNITY_MCP_ALLOW_BATCH=1 to allow the bridge in batch mode
117159
if (Application.isBatchMode && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITY_MCP_ALLOW_BATCH")))
@@ -579,8 +621,32 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken
579621
{
580622
try { MCPForUnity.Editor.Helpers.McpLog.Info("[MCP] sending framed response", always: false); } catch { }
581623
}
582-
byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
583-
await WriteFrameAsync(stream, responseBytes);
624+
// Crash-proof and self-reporting writer logs (direct write to this client's stream)
625+
long seq = System.Threading.Interlocked.Increment(ref _ioSeq);
626+
byte[] responseBytes;
627+
try
628+
{
629+
responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
630+
IoInfo($"[IO] ➜ write start seq={seq} tag=response len={responseBytes.Length} reqId=?");
631+
}
632+
catch (Exception ex)
633+
{
634+
IoInfo($"[IO] ✗ serialize FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}");
635+
throw;
636+
}
637+
638+
var swDirect = System.Diagnostics.Stopwatch.StartNew();
639+
try
640+
{
641+
await WriteFrameAsync(stream, responseBytes);
642+
swDirect.Stop();
643+
IoInfo($"[IO] ✓ write end tag=response len={responseBytes.Length} reqId=? durMs={swDirect.Elapsed.TotalMilliseconds:F1}");
644+
}
645+
catch (Exception ex)
646+
{
647+
IoInfo($"[IO] ✗ write FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}");
648+
throw;
649+
}
584650
}
585651
catch (Exception ex)
586652
{

UnityMcpBridge/Editor/Tools/ManageScene.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,46 @@ namespace MCPForUnity.Editor.Tools
1616
/// </summary>
1717
public static class ManageScene
1818
{
19+
private sealed class SceneCommand
20+
{
21+
public string action { get; set; } = string.Empty;
22+
public string name { get; set; } = string.Empty;
23+
public string path { get; set; } = string.Empty;
24+
public int? buildIndex { get; set; }
25+
}
26+
27+
private static SceneCommand ToSceneCommand(JObject p)
28+
{
29+
if (p == null) return new SceneCommand();
30+
int? BI(JToken t)
31+
{
32+
if (t == null || t.Type == JTokenType.Null) return null;
33+
var s = t.ToString().Trim();
34+
if (s.Length == 0) return null;
35+
if (int.TryParse(s, out var i)) return i;
36+
if (double.TryParse(s, out var d)) return (int)d;
37+
return t.Type == JTokenType.Integer ? t.Value<int>() : (int?)null;
38+
}
39+
return new SceneCommand
40+
{
41+
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
42+
name = p["name"]?.ToString() ?? string.Empty,
43+
path = p["path"]?.ToString() ?? string.Empty,
44+
buildIndex = BI(p["buildIndex"] ?? p["build_index"])
45+
};
46+
}
47+
1948
/// <summary>
2049
/// Main handler for scene management actions.
2150
/// </summary>
2251
public static object HandleCommand(JObject @params)
2352
{
2453
try { McpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { }
25-
string action = @params["action"]?.ToString().ToLower();
26-
string name = @params["name"]?.ToString();
27-
string path = @params["path"]?.ToString(); // Relative to Assets/
28-
int? buildIndex = @params["buildIndex"]?.ToObject<int?>();
54+
var cmd = ToSceneCommand(@params);
55+
string action = cmd.action;
56+
string name = string.IsNullOrEmpty(cmd.name) ? null : cmd.name;
57+
string path = string.IsNullOrEmpty(cmd.path) ? null : cmd.path; // Relative to Assets/
58+
int? buildIndex = cmd.buildIndex;
2959
// bool loadAdditive = @params["loadAdditive"]?.ToObject<bool>() ?? false; // Example for future extension
3060

3161
// Ensure path is relative to Assets/, removing any leading "Assets/"

UnityMcpBridge/UnityMcpServer~/src/tools/manage_scene.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ def register_manage_scene_tools(mcp: FastMCP):
1414
def manage_scene(
1515
ctx: Context,
1616
action: str,
17-
name: str,
18-
path: str,
19-
build_index: Any,
17+
name: str = "",
18+
path: str = "",
19+
build_index: Any = None,
2020
) -> Dict[str, Any]:
2121
"""Manages Unity scenes (load, save, create, get hierarchy, etc.).
2222
@@ -47,15 +47,15 @@ def _coerce_int(value, default=None):
4747
except Exception:
4848
return default
4949

50-
build_index = _coerce_int(build_index, default=0)
50+
coerced_build_index = _coerce_int(build_index, default=None)
5151

52-
params = {
53-
"action": action,
54-
"name": name,
55-
"path": path,
56-
"buildIndex": build_index
57-
}
58-
params = {k: v for k, v in params.items() if v is not None}
52+
params = {"action": action}
53+
if name:
54+
params["name"] = name
55+
if path:
56+
params["path"] = path
57+
if coerced_build_index is not None:
58+
params["buildIndex"] = coerced_build_index
5959

6060
# Use centralized retry helper
6161
response = send_command_with_retry("manage_scene", params)

0 commit comments

Comments
 (0)