manage_scene: tolerant params + optional buildIndex; add writer IO logs; keep direct write path
parent
397ba32a99
commit
2fd74f5dab
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
|
@ -25,6 +26,14 @@ namespace MCPForUnity.Editor
|
|||
private static readonly object startStopLock = new();
|
||||
private static readonly object clientsLock = new();
|
||||
private static readonly System.Collections.Generic.HashSet<TcpClient> activeClients = new();
|
||||
// Single-writer outbox for framed responses
|
||||
private class Outbound
|
||||
{
|
||||
public byte[] Payload;
|
||||
public string Tag;
|
||||
public int? ReqId;
|
||||
}
|
||||
private static readonly BlockingCollection<Outbound> _outbox = new(new ConcurrentQueue<Outbound>());
|
||||
private static CancellationTokenSource cts;
|
||||
private static Task listenerTask;
|
||||
private static int processingCommands = 0;
|
||||
|
|
@ -44,6 +53,10 @@ namespace MCPForUnity.Editor
|
|||
private const ulong MaxFrameBytes = 64UL * 1024 * 1024; // 64 MiB hard cap for framed payloads
|
||||
private const int FrameIOTimeoutMs = 30000; // Per-read timeout to avoid stalled clients
|
||||
|
||||
// IO diagnostics
|
||||
private static long _ioSeq = 0;
|
||||
private static void IoInfo(string s) { McpLog.Info(s, always: false); }
|
||||
|
||||
// Debug helpers
|
||||
private static bool IsDebugEnabled()
|
||||
{
|
||||
|
|
@ -112,6 +125,35 @@ namespace MCPForUnity.Editor
|
|||
{
|
||||
// Record the main thread ID for safe thread checks
|
||||
try { mainThreadId = Thread.CurrentThread.ManagedThreadId; } catch { mainThreadId = 0; }
|
||||
// Start single writer thread for framed responses
|
||||
try
|
||||
{
|
||||
var writerThread = new Thread(() =>
|
||||
{
|
||||
foreach (var item in _outbox.GetConsumingEnumerable())
|
||||
{
|
||||
try
|
||||
{
|
||||
long seq = Interlocked.Increment(ref _ioSeq);
|
||||
IoInfo($"[IO] ➜ write start seq={seq} tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")}");
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
// Note: We currently have a per-connection 'stream' in the client handler. For simplicity,
|
||||
// writes are performed inline there. This outbox provides single-writer semantics; if a shared
|
||||
// stream is introduced, redirect here accordingly.
|
||||
// No-op: actual write happens in client loop using WriteFrameAsync
|
||||
sw.Stop();
|
||||
IoInfo($"[IO] ✓ write end tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")} durMs={sw.Elapsed.TotalMilliseconds:F1}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IoInfo($"[IO] ✗ write FAIL tag={item.Tag} reqId={(item.ReqId?.ToString() ?? "?")} {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}) { IsBackground = true, Name = "MCP-Writer" };
|
||||
writerThread.Start();
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Skip bridge in headless/batch environments (CI/builds) unless explicitly allowed via env
|
||||
// CI override: set UNITY_MCP_ALLOW_BATCH=1 to allow the bridge in batch mode
|
||||
if (Application.isBatchMode && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITY_MCP_ALLOW_BATCH")))
|
||||
|
|
@ -579,8 +621,32 @@ namespace MCPForUnity.Editor
|
|||
{
|
||||
try { MCPForUnity.Editor.Helpers.McpLog.Info("[MCP] sending framed response", always: false); } catch { }
|
||||
}
|
||||
byte[] responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
|
||||
await WriteFrameAsync(stream, responseBytes);
|
||||
// Crash-proof and self-reporting writer logs (direct write to this client's stream)
|
||||
long seq = System.Threading.Interlocked.Increment(ref _ioSeq);
|
||||
byte[] responseBytes;
|
||||
try
|
||||
{
|
||||
responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
|
||||
IoInfo($"[IO] ➜ write start seq={seq} tag=response len={responseBytes.Length} reqId=?");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IoInfo($"[IO] ✗ serialize FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
|
||||
var swDirect = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
await WriteFrameAsync(stream, responseBytes);
|
||||
swDirect.Stop();
|
||||
IoInfo($"[IO] ✓ write end tag=response len={responseBytes.Length} reqId=? durMs={swDirect.Elapsed.TotalMilliseconds:F1}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IoInfo($"[IO] ✗ write FAIL tag=response reqId=? {ex.GetType().Name}: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,16 +16,46 @@ namespace MCPForUnity.Editor.Tools
|
|||
/// </summary>
|
||||
public static class ManageScene
|
||||
{
|
||||
private sealed class SceneCommand
|
||||
{
|
||||
public string action { get; set; } = string.Empty;
|
||||
public string name { get; set; } = string.Empty;
|
||||
public string path { get; set; } = string.Empty;
|
||||
public int? buildIndex { get; set; }
|
||||
}
|
||||
|
||||
private static SceneCommand ToSceneCommand(JObject p)
|
||||
{
|
||||
if (p == null) return new SceneCommand();
|
||||
int? BI(JToken t)
|
||||
{
|
||||
if (t == null || t.Type == JTokenType.Null) return null;
|
||||
var s = t.ToString().Trim();
|
||||
if (s.Length == 0) return null;
|
||||
if (int.TryParse(s, out var i)) return i;
|
||||
if (double.TryParse(s, out var d)) return (int)d;
|
||||
return t.Type == JTokenType.Integer ? t.Value<int>() : (int?)null;
|
||||
}
|
||||
return new SceneCommand
|
||||
{
|
||||
action = (p["action"]?.ToString() ?? string.Empty).Trim().ToLowerInvariant(),
|
||||
name = p["name"]?.ToString() ?? string.Empty,
|
||||
path = p["path"]?.ToString() ?? string.Empty,
|
||||
buildIndex = BI(p["buildIndex"] ?? p["build_index"])
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main handler for scene management actions.
|
||||
/// </summary>
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
try { McpLog.Info("[ManageScene] HandleCommand: start", always: false); } catch { }
|
||||
string action = @params["action"]?.ToString().ToLower();
|
||||
string name = @params["name"]?.ToString();
|
||||
string path = @params["path"]?.ToString(); // Relative to Assets/
|
||||
int? buildIndex = @params["buildIndex"]?.ToObject<int?>();
|
||||
var cmd = ToSceneCommand(@params);
|
||||
string action = cmd.action;
|
||||
string name = string.IsNullOrEmpty(cmd.name) ? null : cmd.name;
|
||||
string path = string.IsNullOrEmpty(cmd.path) ? null : cmd.path; // Relative to Assets/
|
||||
int? buildIndex = cmd.buildIndex;
|
||||
// bool loadAdditive = @params["loadAdditive"]?.ToObject<bool>() ?? false; // Example for future extension
|
||||
|
||||
// Ensure path is relative to Assets/, removing any leading "Assets/"
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ def register_manage_scene_tools(mcp: FastMCP):
|
|||
def manage_scene(
|
||||
ctx: Context,
|
||||
action: str,
|
||||
name: str,
|
||||
path: str,
|
||||
build_index: Any,
|
||||
name: str = "",
|
||||
path: str = "",
|
||||
build_index: Any = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Manages Unity scenes (load, save, create, get hierarchy, etc.).
|
||||
|
||||
|
|
@ -47,15 +47,15 @@ def register_manage_scene_tools(mcp: FastMCP):
|
|||
except Exception:
|
||||
return default
|
||||
|
||||
build_index = _coerce_int(build_index, default=0)
|
||||
coerced_build_index = _coerce_int(build_index, default=None)
|
||||
|
||||
params = {
|
||||
"action": action,
|
||||
"name": name,
|
||||
"path": path,
|
||||
"buildIndex": build_index
|
||||
}
|
||||
params = {k: v for k, v in params.items() if v is not None}
|
||||
params = {"action": action}
|
||||
if name:
|
||||
params["name"] = name
|
||||
if path:
|
||||
params["path"] = path
|
||||
if coerced_build_index is not None:
|
||||
params["buildIndex"] = coerced_build_index
|
||||
|
||||
# Use centralized retry helper
|
||||
response = send_command_with_retry("manage_scene", params)
|
||||
|
|
|
|||
Loading…
Reference in New Issue