feat: Automatic Background Compile and Domain Reload for MCP Script Edits and New Script Creation (#248)
* Unity MCP: reliable auto-reload via Unity-side sentinel flip; remove Python writes - Add MCP/Flip Reload Sentinel editor menu and flip package sentinel synchronously - Trigger sentinel flip after Create/Update/ApplyTextEdits (sync) in ManageScript - Instrument flip path with Debug.Log for traceability - Remove Python reload_sentinel writes; tools now execute Unity menu instead - Harden reload_sentinel path resolution to project/package - ExecuteMenuItem runs synchronously for deterministic results - Verified MCP edits trigger compile/reload without focus; no Python permission errors * Getting double flips * Fix double reload and ensure accurate batch edits; immediate structured reloads * Remove MCP/Flip Reload Sentinel menu; rely on synchronous import/compile for reloads * Route bridge/editor logs through McpLog and gate behind debug; create path now reloads synchronously * chore: ignore backup artifacts; remove stray ManageScript.cs.backup files * fix span logic * fix: honor UNITY_MCP_STATUS_DIR for sentinel status file lookup (fallback to ~/.unity-mcp) * test: add sentinel test honoring UNITY_MCP_STATUS_DIR; chore: ManageScript overlap check simplification and log consistency * Harden environment path, remove extraneous flip menu test * refactor: centralize import/compile via ManageScriptRefreshHelpers.ImportAndRequestCompile; replace duplicated sequences * feat: add scheduledRefresh flag; standardize logs; gate info and DRY immediate import/compile * chore: remove execute_menu_item sentinel flip from manage_script_edits; rely on import/compile * chore: remove unused MCP reload sentinel mechanism * fix: honor ignore_case for anchor_insert in text conversion pathmain
parent
ad848f06df
commit
0c52c1c92e
|
|
@ -36,3 +36,7 @@ CONTRIBUTING.md.meta
|
|||
.DS_Store*
|
||||
# Unity test project lock files
|
||||
TestProjects/UnityMCPTests/Packages/packages-lock.json
|
||||
|
||||
# Backup artifacts
|
||||
*.backup
|
||||
*.backup.meta
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 46421b2ea84fe4b1a903e2483cff3958
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
public class Hello : MonoBehaviour
|
||||
{
|
||||
|
||||
// Use this for initialization
|
||||
void Start()
|
||||
{
|
||||
Debug.Log("Hello World");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bebdf68a6876b425494ee770d20f70ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ namespace MCPForUnity.Editor
|
|||
{
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: [{stage}]");
|
||||
McpLog.Info($"[{stage}]", always: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -229,8 +229,11 @@ namespace MCPForUnity.Editor
|
|||
{
|
||||
// Don't restart if already running on a working port
|
||||
if (isRunning && listener != null)
|
||||
{
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCPForUnityBridge already running on port {currentUnityPort}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -348,7 +351,7 @@ namespace MCPForUnity.Editor
|
|||
listener?.Stop();
|
||||
listener = null;
|
||||
EditorApplication.update -= ProcessCommands;
|
||||
Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCPForUnityBridge stopped.");
|
||||
if (IsDebugEnabled()) Debug.Log("<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCPForUnityBridge stopped.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -389,7 +392,7 @@ namespace MCPForUnity.Editor
|
|||
{
|
||||
if (isRunning)
|
||||
{
|
||||
Debug.LogError($"Listener error: {ex.Message}");
|
||||
if (IsDebugEnabled()) Debug.LogError($"Listener error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -402,10 +405,13 @@ namespace MCPForUnity.Editor
|
|||
{
|
||||
// Framed I/O only; legacy mode removed
|
||||
try
|
||||
{
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
var ep = client.Client?.RemoteEndPoint?.ToString() ?? "unknown";
|
||||
Debug.Log($"<b><color=#2EA3FF>UNITY-MCP</color></b>: Client connected {ep}");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
// Strict framing: always require FRAMING=1 and frame all I/O
|
||||
try
|
||||
|
|
@ -423,11 +429,11 @@ namespace MCPForUnity.Editor
|
|||
#else
|
||||
await stream.WriteAsync(handshakeBytes, 0, handshakeBytes.Length, cts.Token).ConfigureAwait(false);
|
||||
#endif
|
||||
Debug.Log("<b><color=#2EA3FF>UNITY-MCP</color></b>: Sent handshake FRAMING=1 (strict)");
|
||||
if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info("Sent handshake FRAMING=1 (strict)", always: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"<b><color=#2EA3FF>UNITY-MCP</color></b>: Handshake failed: {ex.Message}");
|
||||
if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Warn($"Handshake failed: {ex.Message}");
|
||||
return; // abort this client
|
||||
}
|
||||
|
||||
|
|
@ -439,9 +445,12 @@ namespace MCPForUnity.Editor
|
|||
string commandText = await ReadFrameAsUtf8Async(stream, FrameIOTimeoutMs);
|
||||
|
||||
try
|
||||
{
|
||||
if (IsDebugEnabled())
|
||||
{
|
||||
var preview = commandText.Length > 120 ? commandText.Substring(0, 120) + "…" : commandText;
|
||||
Debug.Log($"<b><color=#2EA3FF>UNITY-MCP</color></b>: recv framed: {preview}");
|
||||
MCPForUnity.Editor.Helpers.McpLog.Info($"recv framed: {preview}", always: false);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
string commandId = Guid.NewGuid().ToString();
|
||||
|
|
@ -470,7 +479,20 @@ namespace MCPForUnity.Editor
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Client handler error: {ex.Message}");
|
||||
// Treat common disconnects/timeouts as benign; only surface hard errors
|
||||
string msg = ex.Message ?? string.Empty;
|
||||
bool isBenign =
|
||||
msg.IndexOf("Connection closed before reading expected bytes", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| msg.IndexOf("Read timed out", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| ex is System.IO.IOException;
|
||||
if (isBenign)
|
||||
{
|
||||
if (IsDebugEnabled()) MCPForUnity.Editor.Helpers.McpLog.Info($"Client handler: {msg}", always: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
MCPForUnity.Editor.Helpers.McpLog.Error($"Client handler error: {msg}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ namespace MCPForUnity.Editor.Tools
|
|||
{
|
||||
// Try both naming conventions: snake_case and camelCase
|
||||
string menuPath = @params["menu_path"]?.ToString() ?? @params["menuPath"]?.ToString();
|
||||
// Optional future param retained for API compatibility; not used in synchronous mode
|
||||
// int timeoutMs = Math.Max(0, (@params["timeout_ms"]?.ToObject<int>() ?? 2000));
|
||||
|
||||
// string alias = @params["alias"]?.ToString(); // TODO: Implement alias mapping based on refactor plan requirements.
|
||||
// JObject parameters = @params["parameters"] as JObject; // TODO: Investigate parameter passing (often not directly supported by ExecuteMenuItem).
|
||||
|
|
@ -94,42 +96,29 @@ namespace MCPForUnity.Editor.Tools
|
|||
|
||||
try
|
||||
{
|
||||
// Attempt to execute the menu item on the main thread using delayCall for safety.
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
bool executed = EditorApplication.ExecuteMenuItem(menuPath);
|
||||
// Log potential failure inside the delayed call.
|
||||
if (!executed)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[ExecuteMenuItem] Failed to find or execute menu item via delayCall: '{menuPath}'. It might be invalid, disabled, or context-dependent."
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception delayEx)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[ExecuteMenuItem] Exception during delayed execution of '{menuPath}': {delayEx}"
|
||||
);
|
||||
}
|
||||
};
|
||||
// Trace incoming execute requests
|
||||
Debug.Log($"[ExecuteMenuItem] Request to execute menu: '{menuPath}'");
|
||||
|
||||
// Report attempt immediately, as execution is delayed.
|
||||
// Execute synchronously. This code runs on the Editor main thread in our bridge path.
|
||||
bool executed = EditorApplication.ExecuteMenuItem(menuPath);
|
||||
if (executed)
|
||||
{
|
||||
Debug.Log($"[ExecuteMenuItem] Executed successfully: '{menuPath}'");
|
||||
return Response.Success(
|
||||
$"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors."
|
||||
$"Executed menu item: '{menuPath}'",
|
||||
new { executed = true, menuPath }
|
||||
);
|
||||
}
|
||||
Debug.LogWarning($"[ExecuteMenuItem] Failed (not found/disabled): '{menuPath}'");
|
||||
return Response.Error(
|
||||
$"Failed to execute menu item (not found or disabled): '{menuPath}'",
|
||||
new { executed = false, menuPath }
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Catch errors during setup phase.
|
||||
Debug.LogError(
|
||||
$"[ExecuteMenuItem] Failed to setup execution for '{menuPath}': {e}"
|
||||
);
|
||||
return Response.Error(
|
||||
$"Error setting up execution for menu item '{menuPath}': {e.Message}"
|
||||
);
|
||||
Debug.LogError($"[ExecuteMenuItem] Error executing '{menuPath}': {e}");
|
||||
return Response.Error($"Error executing menu item '{menuPath}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -193,10 +193,10 @@ namespace MCPForUnity.Editor.Tools
|
|||
namespaceName
|
||||
);
|
||||
case "read":
|
||||
Debug.LogWarning("manage_script.read is deprecated; prefer resources/read. Serving read for backward compatibility.");
|
||||
McpLog.Warn("manage_script.read is deprecated; prefer resources/read. Serving read for backward compatibility.");
|
||||
return ReadScript(fullPath, relativePath);
|
||||
case "update":
|
||||
Debug.LogWarning("manage_script.update is deprecated; prefer apply_text_edits. Serving update for backward compatibility.");
|
||||
McpLog.Warn("manage_script.update is deprecated; prefer apply_text_edits. Serving update for backward compatibility.");
|
||||
return UpdateScript(fullPath, relativePath, name, contents);
|
||||
case "delete":
|
||||
return DeleteScript(fullPath, relativePath);
|
||||
|
|
@ -356,11 +356,11 @@ namespace MCPForUnity.Editor.Tools
|
|||
var uri = $"unity://path/{relativePath}";
|
||||
var ok = Response.Success(
|
||||
$"Script '{name}.cs' created successfully at '{relativePath}'.",
|
||||
new { uri, scheduledRefresh = true }
|
||||
new { uri, scheduledRefresh = false }
|
||||
);
|
||||
|
||||
// Schedule heavy work AFTER replying
|
||||
ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath);
|
||||
ManageScriptRefreshHelpers.ImportAndRequestCompile(relativePath);
|
||||
|
||||
return ok;
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -763,8 +763,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
string.Equals(refreshModeFromCaller, "sync", StringComparison.OrdinalIgnoreCase);
|
||||
if (immediate)
|
||||
{
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
McpLog.Info($"[ManageScript] ApplyTextEdits: immediate refresh for '{relativePath}'");
|
||||
AssetDatabase.ImportAsset(
|
||||
relativePath,
|
||||
ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate
|
||||
|
|
@ -772,10 +771,10 @@ namespace MCPForUnity.Editor.Tools
|
|||
#if UNITY_EDITOR
|
||||
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
|
||||
#endif
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Info($"[ManageScript] ApplyTextEdits: debounced refresh scheduled for '{relativePath}'");
|
||||
ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath);
|
||||
}
|
||||
|
||||
|
|
@ -786,7 +785,8 @@ namespace MCPForUnity.Editor.Tools
|
|||
uri = $"unity://path/{relativePath}",
|
||||
path = relativePath,
|
||||
editsApplied = spans.Count,
|
||||
sha256 = newSha
|
||||
sha256 = newSha,
|
||||
scheduledRefresh = !immediate
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -1326,7 +1326,7 @@ namespace MCPForUnity.Editor.Tools
|
|||
if (ordered[i].start + ordered[i].length > ordered[i - 1].start)
|
||||
{
|
||||
var conflict = new[] { new { startA = ordered[i].start, endA = ordered[i].start + ordered[i].length, startB = ordered[i - 1].start, endB = ordered[i - 1].start + ordered[i - 1].length } };
|
||||
return Response.Error("overlap", new { status = "overlap", conflicts = conflict, hint = "Apply in descending order against the same precondition snapshot." });
|
||||
return Response.Error("overlap", new { status = "overlap", conflicts = conflict, hint = "Sort ranges descending by start and compute from the same snapshot." });
|
||||
}
|
||||
}
|
||||
return Response.Error("overlap", new { status = "overlap" });
|
||||
|
|
@ -1421,17 +1421,8 @@ namespace MCPForUnity.Editor.Tools
|
|||
|
||||
if (immediate)
|
||||
{
|
||||
// Force on main thread
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
AssetDatabase.ImportAsset(
|
||||
relativePath,
|
||||
ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate
|
||||
);
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
|
||||
#endif
|
||||
};
|
||||
McpLog.Info($"[ManageScript] EditScript: immediate refresh for '{relativePath}'", always: false);
|
||||
ManageScriptRefreshHelpers.ImportAndRequestCompile(relativePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -2620,5 +2611,15 @@ static class ManageScriptRefreshHelpers
|
|||
{
|
||||
RefreshDebounce.Schedule(relPath, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
public static void ImportAndRequestCompile(string relPath, bool synchronous = true)
|
||||
{
|
||||
var opts = ImportAssetOptions.ForceUpdate;
|
||||
if (synchronous) opts |= ImportAssetOptions.ForceSynchronousImport;
|
||||
AssetDatabase.ImportAsset(relPath, opts);
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1771,7 +1771,7 @@ namespace MCPForUnity.Editor.Windows
|
|||
{
|
||||
if (debugLogsEnabled)
|
||||
{
|
||||
UnityEngine.Debug.Log($"MCP for Unity: Auto-updated MCP config for '{mcpClient.name}' to new path: {pythonDir}");
|
||||
MCPForUnity.Editor.Helpers.McpLog.Info($"Auto-updated MCP config for '{mcpClient.name}' to new path: {pythonDir}", always: false);
|
||||
}
|
||||
mcpClient.SetStatus(McpStatus.Configured);
|
||||
}
|
||||
|
|
@ -1971,7 +1971,7 @@ namespace MCPForUnity.Editor.Windows
|
|||
|
||||
if (debugLogsEnabled)
|
||||
{
|
||||
UnityEngine.Debug.Log($"Checking Claude config at: {configPath}");
|
||||
MCPForUnity.Editor.Helpers.McpLog.Info($"Checking Claude config at: {configPath}", always: false);
|
||||
}
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
"""
|
||||
Deprecated: Sentinel flipping is handled inside Unity via the MCP menu
|
||||
'MCP/Flip Reload Sentinel'. This module remains only as a compatibility shim.
|
||||
All functions are no-ops to prevent accidental external writes.
|
||||
"""
|
||||
|
||||
def flip_reload_sentinel(*args, **kwargs) -> str:
|
||||
return "reload_sentinel.py is deprecated; use execute_menu_item → 'MCP/Flip Reload Sentinel'"
|
||||
|
|
@ -306,6 +306,38 @@ def register_manage_script_tools(mcp: FastMCP):
|
|||
data.setdefault("normalizedEdits", normalized_edits)
|
||||
if warnings:
|
||||
data.setdefault("warnings", warnings)
|
||||
if resp.get("success") and (options or {}).get("force_sentinel_reload"):
|
||||
# Optional: flip sentinel via menu if explicitly requested
|
||||
try:
|
||||
import threading, time, json, glob, os
|
||||
def _latest_status() -> dict | None:
|
||||
try:
|
||||
files = sorted(glob.glob(os.path.expanduser("~/.unity-mcp/unity-mcp-status-*.json")), key=os.path.getmtime, reverse=True)
|
||||
if not files:
|
||||
return None
|
||||
with open(files[0], "r") as f:
|
||||
return json.loads(f.read())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _flip_async():
|
||||
try:
|
||||
time.sleep(0.1)
|
||||
st = _latest_status()
|
||||
if st and st.get("reloading"):
|
||||
return
|
||||
send_command_with_retry(
|
||||
"execute_menu_item",
|
||||
{"menuPath": "MCP/Flip Reload Sentinel"},
|
||||
max_retries=0,
|
||||
retry_ms=0,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
threading.Thread(target=_flip_async, daemon=True).start()
|
||||
except Exception:
|
||||
pass
|
||||
return resp
|
||||
return resp
|
||||
return {"success": False, "message": str(resp)}
|
||||
|
||||
|
|
@ -457,7 +489,7 @@ def register_manage_script_tools(mcp: FastMCP):
|
|||
"path": path,
|
||||
"edits": edits,
|
||||
"precondition_sha256": sha,
|
||||
"options": {"refresh": "immediate", "validate": "standard"},
|
||||
"options": {"refresh": "debounced", "validate": "standard"},
|
||||
}
|
||||
# Preflight size vs. default cap (256 KiB) to avoid opaque server errors
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from mcp.server.fastmcp import FastMCP, Context
|
|||
from typing import Dict, Any, List, Tuple
|
||||
import base64
|
||||
import re
|
||||
import os
|
||||
from unity_connection import send_command_with_retry
|
||||
|
||||
|
||||
|
|
@ -80,6 +81,37 @@ def _apply_edits_locally(original_text: str, edits: List[Dict[str, Any]]) -> str
|
|||
return text
|
||||
|
||||
|
||||
def _trigger_sentinel_async() -> None:
|
||||
"""Fire the Unity menu flip on a short-lived background thread.
|
||||
|
||||
This avoids blocking the current request or getting stuck during domain reloads
|
||||
(socket reconnects) when the Editor recompiles.
|
||||
"""
|
||||
try:
|
||||
import threading, time
|
||||
|
||||
def _flip():
|
||||
try:
|
||||
import json, glob, os
|
||||
# Small delay so write flushes; prefer early flip to avoid editor-focus second reload
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
files = sorted(glob.glob(os.path.expanduser("~/.unity-mcp/unity-mcp-status-*.json")), key=os.path.getmtime, reverse=True)
|
||||
if files:
|
||||
with open(files[0], "r") as f:
|
||||
st = json.loads(f.read())
|
||||
if st.get("reloading"):
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
threading.Thread(target=_flip, daemon=True).start()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _infer_class_name(script_name: str) -> str:
|
||||
# Default to script name as class name (common Unity pattern)
|
||||
return (script_name or "").strip()
|
||||
|
|
@ -470,7 +502,7 @@ def register_manage_script_edits_tools(mcp: FastMCP):
|
|||
# If everything is structured (method/class/anchor ops), forward directly to Unity's structured editor.
|
||||
if all_struct:
|
||||
opts2 = dict(options or {})
|
||||
# Do not force sequential; allow server default (atomic) unless caller requests otherwise
|
||||
# For structured edits, prefer immediate refresh to avoid missed reloads when Editor is unfocused
|
||||
opts2.setdefault("refresh", "immediate")
|
||||
params_struct: Dict[str, Any] = {
|
||||
"action": "edit",
|
||||
|
|
@ -482,6 +514,13 @@ def register_manage_script_edits_tools(mcp: FastMCP):
|
|||
"options": opts2,
|
||||
}
|
||||
resp_struct = send_command_with_retry("manage_script", params_struct)
|
||||
if isinstance(resp_struct, dict) and resp_struct.get("success"):
|
||||
# Optional: flip sentinel only if explicitly requested
|
||||
if (options or {}).get("force_sentinel_reload"):
|
||||
try:
|
||||
_trigger_sentinel_async()
|
||||
except Exception:
|
||||
pass
|
||||
return _with_norm(resp_struct if isinstance(resp_struct, dict) else {"success": False, "message": str(resp_struct)}, normalized_for_echo, routing="structured")
|
||||
|
||||
# 1) read from Unity
|
||||
|
|
@ -597,18 +636,24 @@ def register_manage_script_edits_tools(mcp: FastMCP):
|
|||
"scriptType": script_type,
|
||||
"edits": at_edits,
|
||||
"precondition_sha256": sha,
|
||||
"options": {"refresh": "immediate", "validate": (options or {}).get("validate", "standard"), "applyMode": ("atomic" if len(at_edits) > 1 else (options or {}).get("applyMode", "sequential"))}
|
||||
"options": {"refresh": (options or {}).get("refresh", "debounced"), "validate": (options or {}).get("validate", "standard"), "applyMode": ("atomic" if len(at_edits) > 1 else (options or {}).get("applyMode", "sequential"))}
|
||||
}
|
||||
resp_text = send_command_with_retry("manage_script", params_text)
|
||||
if not (isinstance(resp_text, dict) and resp_text.get("success")):
|
||||
return _with_norm(resp_text if isinstance(resp_text, dict) else {"success": False, "message": str(resp_text)}, normalized_for_echo, routing="mixed/text-first")
|
||||
# Successful text write; flip sentinel only if explicitly requested
|
||||
if (options or {}).get("force_sentinel_reload"):
|
||||
try:
|
||||
_trigger_sentinel_async()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
return _with_norm({"success": False, "message": f"Text edit conversion failed: {e}"}, normalized_for_echo, routing="mixed/text-first")
|
||||
|
||||
if struct_edits:
|
||||
opts2 = dict(options or {})
|
||||
# Let server decide; do not force sequential
|
||||
opts2.setdefault("refresh", "immediate")
|
||||
# Prefer debounced background refresh unless explicitly overridden
|
||||
opts2.setdefault("refresh", "debounced")
|
||||
params_struct: Dict[str, Any] = {
|
||||
"action": "edit",
|
||||
"name": name,
|
||||
|
|
@ -619,6 +664,12 @@ def register_manage_script_edits_tools(mcp: FastMCP):
|
|||
"options": opts2
|
||||
}
|
||||
resp_struct = send_command_with_retry("manage_script", params_struct)
|
||||
if isinstance(resp_struct, dict) and resp_struct.get("success"):
|
||||
if (options or {}).get("force_sentinel_reload"):
|
||||
try:
|
||||
_trigger_sentinel_async()
|
||||
except Exception:
|
||||
pass
|
||||
return _with_norm(resp_struct if isinstance(resp_struct, dict) else {"success": False, "message": str(resp_struct)}, normalized_for_echo, routing="mixed/text-first")
|
||||
|
||||
return _with_norm({"success": True, "message": "Applied text edits (no structured ops)"}, normalized_for_echo, routing="mixed/text-first")
|
||||
|
|
@ -648,9 +699,10 @@ def register_manage_script_edits_tools(mcp: FastMCP):
|
|||
if op == "anchor_insert":
|
||||
anchor = e.get("anchor") or ""
|
||||
position = (e.get("position") or "after").lower()
|
||||
# Early regex compile with helpful errors
|
||||
# Early regex compile with helpful errors, honoring ignore_case
|
||||
try:
|
||||
regex_obj = _re.compile(anchor, _re.MULTILINE)
|
||||
flags = _re.MULTILINE | (_re.IGNORECASE if e.get("ignore_case") else 0)
|
||||
regex_obj = _re.compile(anchor, flags)
|
||||
except Exception as ex:
|
||||
return _with_norm(_err("bad_regex", f"Invalid anchor regex: {ex}", normalized=normalized_for_echo, routing="text", extra={"hint": "Escape parentheses/braces or use a simpler anchor."}), normalized_for_echo, routing="text")
|
||||
m = regex_obj.search(base_text)
|
||||
|
|
@ -734,12 +786,18 @@ def register_manage_script_edits_tools(mcp: FastMCP):
|
|||
"edits": at_edits,
|
||||
"precondition_sha256": sha,
|
||||
"options": {
|
||||
"refresh": "immediate",
|
||||
"refresh": (options or {}).get("refresh", "debounced"),
|
||||
"validate": (options or {}).get("validate", "standard"),
|
||||
"applyMode": ("atomic" if len(at_edits) > 1 else (options or {}).get("applyMode", "sequential"))
|
||||
}
|
||||
}
|
||||
resp = send_command_with_retry("manage_script", params)
|
||||
if isinstance(resp, dict) and resp.get("success"):
|
||||
if (options or {}).get("force_sentinel_reload"):
|
||||
try:
|
||||
_trigger_sentinel_async()
|
||||
except Exception:
|
||||
pass
|
||||
return _with_norm(
|
||||
resp if isinstance(resp, dict) else {"success": False, "message": str(resp)},
|
||||
normalized_for_echo,
|
||||
|
|
@ -791,7 +849,7 @@ def register_manage_script_edits_tools(mcp: FastMCP):
|
|||
# Default refresh/validate for natural usage on text path as well
|
||||
options = dict(options or {})
|
||||
options.setdefault("validate", "standard")
|
||||
options.setdefault("refresh", "immediate")
|
||||
options.setdefault("refresh", "debounced")
|
||||
|
||||
import hashlib
|
||||
# Compute the SHA of the current file contents for the precondition
|
||||
|
|
@ -816,10 +874,16 @@ def register_manage_script_edits_tools(mcp: FastMCP):
|
|||
}
|
||||
],
|
||||
"precondition_sha256": sha,
|
||||
"options": options or {"validate": "standard", "refresh": "immediate"},
|
||||
"options": options or {"validate": "standard", "refresh": "debounced"},
|
||||
}
|
||||
|
||||
write_resp = send_command_with_retry("manage_script", params)
|
||||
if isinstance(write_resp, dict) and write_resp.get("success"):
|
||||
if (options or {}).get("force_sentinel_reload"):
|
||||
try:
|
||||
_trigger_sentinel_async()
|
||||
except Exception:
|
||||
pass
|
||||
return _with_norm(
|
||||
write_resp if isinstance(write_resp, dict)
|
||||
else {"success": False, "message": str(write_resp)},
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ cli = [
|
|||
|
||||
[[package]]
|
||||
name = "mcpforunityserver"
|
||||
version = "3.0.2"
|
||||
version = "3.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
|
|
|
|||
Loading…
Reference in New Issue