Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions TestProjects/UnityMCPTests/Assets/Editor.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions TestProjects/UnityMCPTests/Assets/Scripts/Hello.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}



}
11 changes: 1 addition & 10 deletions TestProjects/UnityMCPTests/Assets/Scripts/Hello.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 33 additions & 11 deletions UnityMcpBridge/Editor/MCPForUnityBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private static void LogBreadcrumb(string stage)
{
if (IsDebugEnabled())
{
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: [{stage}]");
McpLog.Info($"[{stage}]", always: false);
}
}

Expand Down Expand Up @@ -230,7 +230,10 @@ public static void Start()
// Don't restart if already running on a working port
if (isRunning && listener != null)
{
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCPForUnityBridge already running on port {currentUnityPort}");
if (IsDebugEnabled())
{
Debug.Log($"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: MCPForUnityBridge already running on port {currentUnityPort}");
}
return;
}

Expand Down Expand Up @@ -348,7 +351,7 @@ public static void Stop()
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)
{
Expand Down Expand Up @@ -389,7 +392,7 @@ private static async Task ListenerLoop()
{
if (isRunning)
{
Debug.LogError($"Listener error: {ex.Message}");
if (IsDebugEnabled()) Debug.LogError($"Listener error: {ex.Message}");
}
}
}
Expand All @@ -403,8 +406,11 @@ private static async Task HandleClientAsync(TcpClient client)
// Framed I/O only; legacy mode removed
try
{
var ep = client.Client?.RemoteEndPoint?.ToString() ?? "unknown";
Debug.Log($"<b><color=#2EA3FF>UNITY-MCP</color></b>: Client connected {ep}");
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
Expand All @@ -423,11 +429,11 @@ private static async Task HandleClientAsync(TcpClient client)
#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
}

Expand All @@ -440,8 +446,11 @@ private static async Task HandleClientAsync(TcpClient client)

try
{
var preview = commandText.Length > 120 ? commandText.Substring(0, 120) + "…" : commandText;
Debug.Log($"<b><color=#2EA3FF>UNITY-MCP</color></b>: recv framed: {preview}");
if (IsDebugEnabled())
{
var preview = commandText.Length > 120 ? commandText.Substring(0, 120) + "…" : commandText;
MCPForUnity.Editor.Helpers.McpLog.Info($"recv framed: {preview}", always: false);
}
}
catch { }
string commandId = Guid.NewGuid().ToString();
Expand Down Expand Up @@ -470,7 +479,20 @@ private static async Task HandleClientAsync(TcpClient client)
}
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;
}
}
Expand Down
51 changes: 20 additions & 31 deletions UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ private static object ExecuteItem(JObject @params)
{
// 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).
Expand All @@ -94,42 +96,29 @@ private static object ExecuteItem(JObject @params)

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.
return Response.Success(
$"Attempted to execute menu item: '{menuPath}'. Check Unity logs for confirmation or errors."
// 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(
$"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}");
}
}

Expand Down
55 changes: 28 additions & 27 deletions UnityMcpBridge/Editor/Tools/ManageScript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,10 @@ public static object HandleCommand(JObject @params)
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);
Expand Down Expand Up @@ -356,11 +356,11 @@ string namespaceName
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)
Expand Down Expand Up @@ -650,7 +650,7 @@ private static object ApplyTextEdits(
spans = spans.OrderByDescending(t => t.start).ToList();
for (int i = 1; i < spans.Count; i++)
{
if (spans[i].end > spans[i - 1].start)
if (spans[i].end > spans[i - 1].start)
{
var conflict = new[] { new { startA = spans[i].start, endA = spans[i].end, startB = spans[i - 1].start, endB = spans[i - 1].end } };
return Response.Error("overlap", new { status = "overlap", conflicts = conflict, hint = "Sort ranges descending by start and compute from the same snapshot." });
Expand Down Expand Up @@ -763,19 +763,18 @@ private static object ApplyTextEdits(
string.Equals(refreshModeFromCaller, "sync", StringComparison.OrdinalIgnoreCase);
if (immediate)
{
EditorApplication.delayCall += () =>
{
AssetDatabase.ImportAsset(
relativePath,
ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate
);
McpLog.Info($"[ManageScript] ApplyTextEdits: immediate refresh for '{relativePath}'");
AssetDatabase.ImportAsset(
relativePath,
ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate
);
#if UNITY_EDITOR
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
#endif
};
}
else
{
McpLog.Info($"[ManageScript] ApplyTextEdits: debounced refresh scheduled for '{relativePath}'");
ManageScriptRefreshHelpers.ScheduleScriptRefresh(relativePath);
}

Expand All @@ -786,7 +785,8 @@ private static object ApplyTextEdits(
uri = $"unity://path/{relativePath}",
path = relativePath,
editsApplied = spans.Count,
sha256 = newSha
sha256 = newSha,
scheduledRefresh = !immediate
}
);
}
Expand Down Expand Up @@ -1326,7 +1326,7 @@ private static object EditScript(
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" });
Expand Down Expand Up @@ -1421,17 +1421,8 @@ private static object EditScript(

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
{
Expand Down Expand Up @@ -2620,5 +2611,15 @@ public static void ScheduleScriptRefresh(string relPath)
{
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
}
}

4 changes: 2 additions & 2 deletions UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1771,7 +1771,7 @@ private void CheckMcpConfiguration(McpClient mcpClient)
{
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);
}
Expand Down Expand Up @@ -1971,7 +1971,7 @@ private void CheckClaudeCodeConfiguration(McpClient mcpClient)

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))
Expand Down
8 changes: 8 additions & 0 deletions UnityMcpBridge/UnityMcpServer~/src/reload_sentinel.py
Original file line number Diff line number Diff line change
@@ -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'"
Loading