From 00d7706364c34862b66f5f0c4786c1d5987fac98 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Wed, 20 Aug 2025 18:36:40 -0400 Subject: [PATCH 1/8] Move the current test to a Tools folder --- .../UnityMCPTests/Assets/Tests/EditMode/Tools.meta | 8 ++++++++ .../Tests/EditMode/{ => Tools}/CommandRegistryTests.cs | 2 +- .../EditMode/{ => Tools}/CommandRegistryTests.cs.meta | 0 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools.meta rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/{ => Tools}/CommandRegistryTests.cs (96%) rename TestProjects/UnityMCPTests/Assets/Tests/EditMode/{ => Tools}/CommandRegistryTests.cs.meta (100%) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools.meta new file mode 100644 index 00000000..97c801b2 --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d2b20bbd70a544baf891f3caecd384cb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/CommandRegistryTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs similarity index 96% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/CommandRegistryTests.cs rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs index 86d3c01a..c12d1fd1 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/CommandRegistryTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using MCPForUnity.Editor.Tools; -namespace MCPForUnityTests.Editor +namespace MCPForUnityTests.Editor.Tools { public class CommandRegistryTests { diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/CommandRegistryTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs.meta similarity index 100% rename from TestProjects/UnityMCPTests/Assets/Tests/EditMode/CommandRegistryTests.cs.meta rename to TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/CommandRegistryTests.cs.meta From 6ee2890f215edccfa74ea336a9058a5fa119c07a Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Wed, 20 Aug 2025 19:37:04 -0400 Subject: [PATCH 2/8] feat: add env object and disabled flag handling for MCP client configuration --- .../Assets/Tests/EditMode/Windows.meta | 8 + .../EditMode/Windows/WriteToConfigTests.cs | 240 ++++++++++++++++++ .../Windows/WriteToConfigTests.cs.meta | 11 + .../Editor/Windows/MCPForUnityEditorWindow.cs | 18 ++ 4 files changed, 277 insertions(+) create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows.meta create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs.meta diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows.meta new file mode 100644 index 00000000..d4ad8e5d --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 575ef6172fca24e4bbe5ecc1160691bb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs new file mode 100644 index 00000000..41088d8c --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs @@ -0,0 +1,240 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; +using MCPForUnity.Editor.Data; +using MCPForUnity.Editor.Models; +using MCPForUnity.Editor.Windows; + +namespace MCPForUnityTests.Editor.Windows +{ + public class WriteToConfigTests + { + private string _tempRoot; + private string _fakeUvPath; + private string _serverSrcDir; + + [SetUp] + public void SetUp() + { + // Tests are designed for Linux/macOS runners. Skip on Windows due to ProcessStartInfo + // restrictions when UseShellExecute=false for .cmd/.bat scripts. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Ignore("WriteToConfig tests are skipped on Windows (CI runs linux).\n" + + "ValidateUvBinarySafe requires launching an actual exe on Windows."); + } + _tempRoot = Path.Combine(Path.GetTempPath(), "UnityMCPTests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(_tempRoot); + + // Create a fake uv executable that prints a valid version string + _fakeUvPath = Path.Combine(_tempRoot, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "uv.cmd" : "uv"); + File.WriteAllText(_fakeUvPath, "#!/bin/sh\n\necho 'uv 9.9.9'\n"); + TryChmodX(_fakeUvPath); + + // Create a fake server directory with server.py + _serverSrcDir = Path.Combine(_tempRoot, "server-src"); + Directory.CreateDirectory(_serverSrcDir); + File.WriteAllText(Path.Combine(_serverSrcDir, "server.py"), "# dummy server\n"); + + // Point the editor to our server dir (so ResolveServerSrc() uses this) + EditorPrefs.SetString("MCPForUnity.ServerSrc", _serverSrcDir); + // Ensure no lock is enabled + EditorPrefs.SetBool("MCPForUnity.LockCursorConfig", false); + } + + [TearDown] + public void TearDown() + { + // Clean up editor preferences set during SetUp + EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); + EditorPrefs.DeleteKey("MCPForUnity.LockCursorConfig"); + + // Remove temp files + try { if (Directory.Exists(_tempRoot)) Directory.Delete(_tempRoot, true); } catch { } + } + + // --- Tests --- + + [Test] + public void AddsEnvAndDisabledFalse_ForWindsurf() + { + var configPath = Path.Combine(_tempRoot, "windsurf.json"); + WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path"); + + var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf }; + InvokeWriteToConfig(configPath, client); + + var root = JObject.Parse(File.ReadAllText(configPath)); + var unity = (JObject)root.SelectToken("mcpServers.unityMCP"); + Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); + Assert.NotNull(unity["env"], "env should be present for all clients"); + Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object"); + Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Windsurf when missing"); + } + + [Test] + public void AddsEnvAndDisabledFalse_ForKiro() + { + var configPath = Path.Combine(_tempRoot, "kiro.json"); + WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path"); + + var client = new McpClient { name = "Kiro", mcpType = McpTypes.Kiro }; + InvokeWriteToConfig(configPath, client); + + var root = JObject.Parse(File.ReadAllText(configPath)); + var unity = (JObject)root.SelectToken("mcpServers.unityMCP"); + Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); + Assert.NotNull(unity["env"], "env should be present for all clients"); + Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object"); + Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Kiro when missing"); + } + + [Test] + public void AddsOnlyEnv_ForCursor() + { + var configPath = Path.Combine(_tempRoot, "cursor.json"); + WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path"); + + var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor }; + InvokeWriteToConfig(configPath, client); + + var root = JObject.Parse(File.ReadAllText(configPath)); + var unity = (JObject)root.SelectToken("mcpServers.unityMCP"); + Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); + Assert.NotNull(unity["env"], "env should be present for all clients"); + Assert.IsNull(unity["disabled"], "disabled should not be added for non-Windsurf/Kiro clients"); + } + + [Test] + public void VSCode_AddsEnv_NoDisabled() + { + var configPath = Path.Combine(_tempRoot, "vscode.json"); + WriteInitialConfig(configPath, isVSCode:true, command:_fakeUvPath, directory:"/old/path"); + + var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode }; + InvokeWriteToConfig(configPath, client); + + var root = JObject.Parse(File.ReadAllText(configPath)); + var unity = (JObject)root.SelectToken("servers.unityMCP"); + Assert.NotNull(unity, "Expected servers.unityMCP node"); + Assert.NotNull(unity["env"], "env should be present for VSCode as well"); + Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode client"); + Assert.AreEqual("stdio", (string)unity["type"], "VSCode entry should include type=stdio"); + } + + [Test] + public void PreservesExistingEnvAndDisabled() + { + var configPath = Path.Combine(_tempRoot, "preserve.json"); + + // Existing config with env and disabled=true should be preserved + var json = new JObject + { + ["mcpServers"] = new JObject + { + ["unityMCP"] = new JObject + { + ["command"] = _fakeUvPath, + ["args"] = new JArray("run", "--directory", "/old/path", "server.py"), + ["env"] = new JObject { ["FOO"] = "bar" }, + ["disabled"] = true + } + } + }; + File.WriteAllText(configPath, json.ToString()); + + var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf }; + InvokeWriteToConfig(configPath, client); + + var root = JObject.Parse(File.ReadAllText(configPath)); + var unity = (JObject)root.SelectToken("mcpServers.unityMCP"); + Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); + Assert.AreEqual("bar", (string)unity["env"]!["FOO"], "Existing env should be preserved"); + Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved"); + } + + // --- Helpers --- + + private static void TryChmodX(string path) + { + try + { + var psi = new ProcessStartInfo + { + FileName = "/bin/chmod", + Arguments = "+x \"" + path + "\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + using var p = Process.Start(psi); + p?.WaitForExit(2000); + } + catch { /* best-effort on non-Unix */ } + } + + private static void WriteInitialConfig(string configPath, bool isVSCode, string command, string directory) + { + Directory.CreateDirectory(Path.GetDirectoryName(configPath)!); + JObject root; + if (isVSCode) + { + root = new JObject + { + ["servers"] = new JObject + { + ["unityMCP"] = new JObject + { + ["command"] = command, + ["args"] = new JArray("run", "--directory", directory, "server.py"), + ["type"] = "stdio" + } + } + }; + } + else + { + root = new JObject + { + ["mcpServers"] = new JObject + { + ["unityMCP"] = new JObject + { + ["command"] = command, + ["args"] = new JArray("run", "--directory", directory, "server.py") + } + } + }; + } + File.WriteAllText(configPath, root.ToString()); + } + + private static MCPForUnityEditorWindow CreateWindow() + { + return ScriptableObject.CreateInstance(); + } + + private static void InvokeWriteToConfig(string configPath, McpClient client) + { + var window = CreateWindow(); + var mi = typeof(MCPForUnityEditorWindow).GetMethod("WriteToConfig", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(mi, "Could not find WriteToConfig via reflection"); + + // pythonDir is unused by WriteToConfig, but pass server src to keep it consistent + var result = (string)mi!.Invoke(window, new object[] { + /* pythonDir */ string.Empty, + /* configPath */ configPath, + /* mcpClient */ client + }); + + Assert.AreEqual("Configured successfully", result, "WriteToConfig should return success"); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs.meta new file mode 100644 index 00000000..4d3029ad --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3fc4210e7cbef4479b2cb9498b1580a6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index 3091f371..40bc0773 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -1102,6 +1102,15 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC existingConfig.servers.unityMCP.command = uvPath; existingConfig.servers.unityMCP.args = Newtonsoft.Json.Linq.JArray.FromObject(newArgs); existingConfig.servers.unityMCP.type = "stdio"; + // Ensure env is present for all clients + if (existingConfig.servers.unityMCP.env == null) + existingConfig.servers.unityMCP.env = new Newtonsoft.Json.Linq.JObject(); + // Add disabled=false for Windsurf and Kiro (do not overwrite if already set) + if (mcpClient != null && (mcpClient.mcpType == McpTypes.Windsurf || mcpClient.mcpType == McpTypes.Kiro)) + { + if (existingConfig.servers.unityMCP.disabled == null) + existingConfig.servers.unityMCP.disabled = false; + } } else { @@ -1109,6 +1118,15 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC if (existingConfig.mcpServers.unityMCP == null) existingConfig.mcpServers.unityMCP = new Newtonsoft.Json.Linq.JObject(); existingConfig.mcpServers.unityMCP.command = uvPath; existingConfig.mcpServers.unityMCP.args = Newtonsoft.Json.Linq.JArray.FromObject(newArgs); + // Ensure env is present for all clients + if (existingConfig.mcpServers.unityMCP.env == null) + existingConfig.mcpServers.unityMCP.env = new Newtonsoft.Json.Linq.JObject(); + // Add disabled=false for Windsurf and Kiro (do not overwrite if already set) + if (mcpClient != null && (mcpClient.mcpType == McpTypes.Windsurf || mcpClient.mcpType == McpTypes.Kiro)) + { + if (existingConfig.mcpServers.unityMCP.disabled == null) + existingConfig.mcpServers.unityMCP.disabled = false; + } } string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings); From 6220750423c922de3e129d5c5caf38d89662d417 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Wed, 20 Aug 2025 20:11:39 -0400 Subject: [PATCH 3/8] Format manual config specially for Windsurf and Kiro --- .../Editor/Windows/MCPForUnityEditorWindow.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index 40bc0773..8ee00ae2 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Runtime.InteropServices; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using MCPForUnity.Editor.Data; @@ -1217,9 +1218,49 @@ private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient break; } + // Ensure env is present for all and disabled:false for Windsurf/Kiro + manualConfigJson = ProcessManualConfigJson(manualConfigJson, mcpClient); + ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); } + // Inject env and disabled into the manual JSON shape for display/copy + private static string ProcessManualConfigJson(string json, McpClient client) + { + try + { + var token = JToken.Parse(json); + + JObject unityNode = null; + if (token["servers"] is JObject servers && servers["unityMCP"] is JObject unityVs) + { + unityNode = unityVs; + } + else if (token["mcpServers"] is JObject mservers && mservers["unityMCP"] is JObject unity) + { + unityNode = unity; + } + + if (unityNode != null) + { + if (unityNode["env"] == null) + unityNode["env"] = new JObject(); + + if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro)) + { + if (unityNode["disabled"] == null) + unityNode["disabled"] = false; + } + } + + return token.ToString(Formatting.Indented); + } + catch + { + return json; // fallback + } + } + private static string ResolveServerSrc() { try From 05011c42024a358ee171e15e3e8a68d76ed02496 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Wed, 20 Aug 2025 20:23:24 -0400 Subject: [PATCH 4/8] refactor: extract config JSON building logic into dedicated ConfigJsonBuilder class --- .../Windows/ManualConfigJsonBuilderTests.cs | 54 +++++++ .../ManualConfigJsonBuilderTests.cs.meta | 11 ++ .../Editor/Helpers/ConfigJsonBuilder.cs | 92 ++++++++++++ .../Editor/Helpers/ConfigJsonBuilder.cs.meta | 11 ++ .../Editor/Windows/MCPForUnityEditorWindow.cs | 141 ++---------------- 5 files changed, 182 insertions(+), 127 deletions(-) create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs.meta create mode 100644 UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs create mode 100644 UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs.meta diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs new file mode 100644 index 00000000..74015177 --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; + +namespace MCPForUnityTests.Editor.Windows +{ + public class ManualConfigJsonBuilderTests + { + [Test] + public void VSCode_ManualJson_HasServersEnvType_NoDisabled() + { + var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode }; + string json = ConfigJsonBuilder.BuildManualConfigJson("/usr/bin/uv", "/path/to/server", client); + + var root = JObject.Parse(json); + var unity = (JObject)root.SelectToken("servers.unityMCP"); + Assert.NotNull(unity, "Expected servers.unityMCP node"); + Assert.AreEqual("/usr/bin/uv", (string)unity["command"]); + CollectionAssert.AreEqual(new[] { "run", "--directory", "/path/to/server", "server.py" }, unity["args"].ToObject()); + Assert.AreEqual("stdio", (string)unity["type"], "VSCode should include type=stdio"); + Assert.NotNull(unity["env"], "env should be included"); + Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode"); + } + + [Test] + public void Windsurf_ManualJson_HasMcpServersEnv_DisabledFalse() + { + var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf }; + string json = ConfigJsonBuilder.BuildManualConfigJson("/usr/bin/uv", "/path/to/server", client); + + var root = JObject.Parse(json); + var unity = (JObject)root.SelectToken("mcpServers.unityMCP"); + Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); + Assert.NotNull(unity["env"], "env should be included"); + Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be added for Windsurf"); + Assert.IsNull(unity["type"], "type should not be added for non-VSCode clients"); + } + + [Test] + public void Cursor_ManualJson_HasMcpServersEnv_NoDisabled() + { + var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor }; + string json = ConfigJsonBuilder.BuildManualConfigJson("/usr/bin/uv", "/path/to/server", client); + + var root = JObject.Parse(json); + var unity = (JObject)root.SelectToken("mcpServers.unityMCP"); + Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); + Assert.NotNull(unity["env"], "env should be included"); + Assert.IsNull(unity["disabled"], "disabled should not be added for Cursor"); + Assert.IsNull(unity["type"], "type should not be added for non-VSCode clients"); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs.meta new file mode 100644 index 00000000..6e68950c --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2dbc50071a45a4adc8e7a91a25bd4fd8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs new file mode 100644 index 00000000..2c5c66f6 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs @@ -0,0 +1,92 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Helpers +{ + public static class ConfigJsonBuilder + { + public static string BuildManualConfigJson(string uvPath, string pythonDir, McpClient client) + { + var root = new JObject(); + bool isVSCode = client?.mcpType == McpTypes.VSCode; + JObject container; + if (isVSCode) + { + container = EnsureObject(root, "servers"); + } + else + { + container = EnsureObject(root, "mcpServers"); + } + + var unity = new JObject + { + ["command"] = uvPath, + ["args"] = JArray.FromObject(new[] { "run", "--directory", pythonDir, "server.py" }) + }; + + // VSCode requires transport type + if (isVSCode) + { + unity["type"] = "stdio"; + } + + // Always include env {} + unity["env"] = new JObject(); + + // Only for Windsurf/Kiro (not for VSCode client) + if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro)) + { + unity["disabled"] = false; + } + + container["unityMCP"] = unity; + + return root.ToString(Formatting.Indented); + } + + public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, string serverSrc, McpClient client) + { + if (root == null) root = new JObject(); + bool isVSCode = client?.mcpType == McpTypes.VSCode; + JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers"); + JObject unity = container["unityMCP"] as JObject ?? new JObject(); + + unity["command"] = uvPath; + unity["args"] = JArray.FromObject(new[] { "run", "--directory", serverSrc, "server.py" }); + + // VSCode transport type + if (isVSCode) + { + unity["type"] = "stdio"; + } + + // Ensure env exists + if (unity["env"] == null) + { + unity["env"] = new JObject(); + } + + // Only add disabled:false for Windsurf/Kiro + if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro)) + { + if (unity["disabled"] == null) + { + unity["disabled"] = false; + } + } + + container["unityMCP"] = unity; + return root; + } + + private static JObject EnsureObject(JObject parent, string name) + { + if (parent[name] is JObject o) return o; + var created = new JObject(); + parent[name] = created; + return created; + } + } +} diff --git a/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs.meta b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs.meta new file mode 100644 index 00000000..f574fde7 --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c07c3369f73943919d9e086a81d1dcc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index 8ee00ae2..faeb9f99 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -1096,41 +1096,15 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC } // 4) Ensure containers exist and write back minimal changes - if (isVSCode) - { - if (existingConfig.servers == null) existingConfig.servers = new Newtonsoft.Json.Linq.JObject(); - if (existingConfig.servers.unityMCP == null) existingConfig.servers.unityMCP = new Newtonsoft.Json.Linq.JObject(); - existingConfig.servers.unityMCP.command = uvPath; - existingConfig.servers.unityMCP.args = Newtonsoft.Json.Linq.JArray.FromObject(newArgs); - existingConfig.servers.unityMCP.type = "stdio"; - // Ensure env is present for all clients - if (existingConfig.servers.unityMCP.env == null) - existingConfig.servers.unityMCP.env = new Newtonsoft.Json.Linq.JObject(); - // Add disabled=false for Windsurf and Kiro (do not overwrite if already set) - if (mcpClient != null && (mcpClient.mcpType == McpTypes.Windsurf || mcpClient.mcpType == McpTypes.Kiro)) - { - if (existingConfig.servers.unityMCP.disabled == null) - existingConfig.servers.unityMCP.disabled = false; - } - } - else - { - if (existingConfig.mcpServers == null) existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject(); - if (existingConfig.mcpServers.unityMCP == null) existingConfig.mcpServers.unityMCP = new Newtonsoft.Json.Linq.JObject(); - existingConfig.mcpServers.unityMCP.command = uvPath; - existingConfig.mcpServers.unityMCP.args = Newtonsoft.Json.Linq.JArray.FromObject(newArgs); - // Ensure env is present for all clients - if (existingConfig.mcpServers.unityMCP.env == null) - existingConfig.mcpServers.unityMCP.env = new Newtonsoft.Json.Linq.JObject(); - // Add disabled=false for Windsurf and Kiro (do not overwrite if already set) - if (mcpClient != null && (mcpClient.mcpType == McpTypes.Windsurf || mcpClient.mcpType == McpTypes.Kiro)) - { - if (existingConfig.mcpServers.unityMCP.disabled == null) - existingConfig.mcpServers.unityMCP.disabled = false; - } - } + JObject existingRoot; + if (existingConfig is JObject eo) + existingRoot = eo; + else + existingRoot = JObject.FromObject(existingConfig); - string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings); + existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient); + + string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); string tmp = configPath + ".tmp"; System.IO.File.WriteAllText(tmp, mergedJson, System.Text.Encoding.UTF8); if (System.IO.File.Exists(configPath)) @@ -1162,105 +1136,18 @@ private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient { // Get the Python directory path using Package Manager API string pythonDir = FindPackagePythonDirectory(); - string manualConfigJson; - - // Create common JsonSerializerSettings - JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; - - // Use switch statement to handle different client types - switch (mcpClient.mcpType) + // Build manual JSON centrally using the shared builder + string uvPathForManual = FindUvPath(); + if (uvPathForManual == null) { - case McpTypes.VSCode: - // Resolve uv so VSCode launches the correct executable even if not on PATH - string uvPathManual = FindUvPath(); - if (uvPathManual == null) - { - UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration."); - return; - } - // Create VSCode-specific configuration with proper format - var vscodeConfig = new - { - servers = new - { - unityMCP = new - { - command = uvPathManual, - args = new[] { "run", "--directory", pythonDir, "server.py" }, - type = "stdio" - } - } - }; - manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); - break; - - default: - // Create standard MCP configuration for other clients - string uvPath = FindUvPath(); - if (uvPath == null) - { - UnityEngine.Debug.LogError("UV package manager not found. Cannot configure manual setup."); - return; - } - - McpConfig jsonConfig = new() - { - mcpServers = new McpConfigServers - { - unityMCP = new McpConfigServer - { - command = uvPath, - args = new[] { "run", "--directory", pythonDir, "server.py" }, - }, - }, - }; - manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings); - break; + UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration."); + return; } - // Ensure env is present for all and disabled:false for Windsurf/Kiro - manualConfigJson = ProcessManualConfigJson(manualConfigJson, mcpClient); - + string manualConfigJson = ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient); ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); } - // Inject env and disabled into the manual JSON shape for display/copy - private static string ProcessManualConfigJson(string json, McpClient client) - { - try - { - var token = JToken.Parse(json); - - JObject unityNode = null; - if (token["servers"] is JObject servers && servers["unityMCP"] is JObject unityVs) - { - unityNode = unityVs; - } - else if (token["mcpServers"] is JObject mservers && mservers["unityMCP"] is JObject unity) - { - unityNode = unity; - } - - if (unityNode != null) - { - if (unityNode["env"] == null) - unityNode["env"] = new JObject(); - - if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro)) - { - if (unityNode["disabled"] == null) - unityNode["disabled"] = false; - } - } - - return token.ToString(Formatting.Indented); - } - catch - { - return json; // fallback - } - } - private static string ResolveServerSrc() { try From d28a4685dd56021476321fcf43ffd46d5f1d7b1c Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Wed, 20 Aug 2025 20:32:33 -0400 Subject: [PATCH 5/8] refactor: extract unity node population logic into centralized helper method --- .../Editor/Helpers/ConfigJsonBuilder.cs | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs index 2c5c66f6..601e82d4 100644 --- a/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs +++ b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs @@ -20,26 +20,8 @@ public static string BuildManualConfigJson(string uvPath, string pythonDir, McpC container = EnsureObject(root, "mcpServers"); } - var unity = new JObject - { - ["command"] = uvPath, - ["args"] = JArray.FromObject(new[] { "run", "--directory", pythonDir, "server.py" }) - }; - - // VSCode requires transport type - if (isVSCode) - { - unity["type"] = "stdio"; - } - - // Always include env {} - unity["env"] = new JObject(); - - // Only for Windsurf/Kiro (not for VSCode client) - if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro)) - { - unity["disabled"] = false; - } + var unity = new JObject(); + PopulateUnityNode(unity, uvPath, pythonDir, client, isVSCode); container["unityMCP"] = unity; @@ -52,23 +34,39 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa bool isVSCode = client?.mcpType == McpTypes.VSCode; JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers"); JObject unity = container["unityMCP"] as JObject ?? new JObject(); + PopulateUnityNode(unity, uvPath, serverSrc, client, isVSCode); + container["unityMCP"] = unity; + return root; + } + + /// + /// Centralized builder that applies all caveats consistently. + /// - Sets command/args with provided directory + /// - Ensures env exists + /// - Adds type:"stdio" for VSCode + /// - Adds disabled:false for Windsurf/Kiro only when missing + /// + private static void PopulateUnityNode(JObject unity, string uvPath, string directory, McpClient client, bool isVSCode) + { unity["command"] = uvPath; - unity["args"] = JArray.FromObject(new[] { "run", "--directory", serverSrc, "server.py" }); + unity["args"] = JArray.FromObject(new[] { "run", "--directory", directory, "server.py" }); - // VSCode transport type if (isVSCode) { unity["type"] = "stdio"; } + else + { + // Remove type if it somehow exists from previous clients + if (unity["type"] != null) unity.Remove("type"); + } - // Ensure env exists if (unity["env"] == null) { unity["env"] = new JObject(); } - // Only add disabled:false for Windsurf/Kiro if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro)) { if (unity["disabled"] == null) @@ -76,9 +74,6 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa unity["disabled"] = false; } } - - container["unityMCP"] = unity; - return root; } private static JObject EnsureObject(JObject parent, string name) From 2c1630e44a887cdf15078a9add2b5b06b72c08a9 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Wed, 20 Aug 2025 20:37:35 -0400 Subject: [PATCH 6/8] refactor: only add env property to config for Windsurf and Kiro clients If it ain't broke with the other clients, don't fix... --- .../EditMode/Windows/ManualConfigJsonBuilderTests.cs | 8 ++++---- .../Tests/EditMode/Windows/WriteToConfigTests.cs | 8 ++++---- UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs index 74015177..6ff7ff9d 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/ManualConfigJsonBuilderTests.cs @@ -8,7 +8,7 @@ namespace MCPForUnityTests.Editor.Windows public class ManualConfigJsonBuilderTests { [Test] - public void VSCode_ManualJson_HasServersEnvType_NoDisabled() + public void VSCode_ManualJson_HasServers_NoEnv_NoDisabled() { var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode }; string json = ConfigJsonBuilder.BuildManualConfigJson("/usr/bin/uv", "/path/to/server", client); @@ -19,7 +19,7 @@ public void VSCode_ManualJson_HasServersEnvType_NoDisabled() Assert.AreEqual("/usr/bin/uv", (string)unity["command"]); CollectionAssert.AreEqual(new[] { "run", "--directory", "/path/to/server", "server.py" }, unity["args"].ToObject()); Assert.AreEqual("stdio", (string)unity["type"], "VSCode should include type=stdio"); - Assert.NotNull(unity["env"], "env should be included"); + Assert.IsNull(unity["env"], "env should not be added for VSCode"); Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode"); } @@ -38,7 +38,7 @@ public void Windsurf_ManualJson_HasMcpServersEnv_DisabledFalse() } [Test] - public void Cursor_ManualJson_HasMcpServersEnv_NoDisabled() + public void Cursor_ManualJson_HasMcpServers_NoEnv_NoDisabled() { var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor }; string json = ConfigJsonBuilder.BuildManualConfigJson("/usr/bin/uv", "/path/to/server", client); @@ -46,7 +46,7 @@ public void Cursor_ManualJson_HasMcpServersEnv_NoDisabled() var root = JObject.Parse(json); var unity = (JObject)root.SelectToken("mcpServers.unityMCP"); Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); - Assert.NotNull(unity["env"], "env should be included"); + Assert.IsNull(unity["env"], "env should not be added for Cursor"); Assert.IsNull(unity["disabled"], "disabled should not be added for Cursor"); Assert.IsNull(unity["type"], "type should not be added for non-VSCode clients"); } diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs index 41088d8c..c8f13b0c 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Windows/WriteToConfigTests.cs @@ -96,7 +96,7 @@ public void AddsEnvAndDisabledFalse_ForKiro() } [Test] - public void AddsOnlyEnv_ForCursor() + public void DoesNotAddEnvOrDisabled_ForCursor() { var configPath = Path.Combine(_tempRoot, "cursor.json"); WriteInitialConfig(configPath, isVSCode:false, command:_fakeUvPath, directory:"/old/path"); @@ -107,12 +107,12 @@ public void AddsOnlyEnv_ForCursor() var root = JObject.Parse(File.ReadAllText(configPath)); var unity = (JObject)root.SelectToken("mcpServers.unityMCP"); Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); - Assert.NotNull(unity["env"], "env should be present for all clients"); + Assert.IsNull(unity["env"], "env should not be added for non-Windsurf/Kiro clients"); Assert.IsNull(unity["disabled"], "disabled should not be added for non-Windsurf/Kiro clients"); } [Test] - public void VSCode_AddsEnv_NoDisabled() + public void DoesNotAddEnvOrDisabled_ForVSCode() { var configPath = Path.Combine(_tempRoot, "vscode.json"); WriteInitialConfig(configPath, isVSCode:true, command:_fakeUvPath, directory:"/old/path"); @@ -123,7 +123,7 @@ public void VSCode_AddsEnv_NoDisabled() var root = JObject.Parse(File.ReadAllText(configPath)); var unity = (JObject)root.SelectToken("servers.unityMCP"); Assert.NotNull(unity, "Expected servers.unityMCP node"); - Assert.NotNull(unity["env"], "env should be present for VSCode as well"); + Assert.IsNull(unity["env"], "env should not be added for VSCode client"); Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode client"); Assert.AreEqual("stdio", (string)unity["type"], "VSCode entry should include type=stdio"); } diff --git a/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs index 601e82d4..94ba5d97 100644 --- a/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs +++ b/UnityMcpBridge/Editor/Helpers/ConfigJsonBuilder.cs @@ -62,13 +62,13 @@ private static void PopulateUnityNode(JObject unity, string uvPath, string direc if (unity["type"] != null) unity.Remove("type"); } - if (unity["env"] == null) - { - unity["env"] = new JObject(); - } - if (client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro)) { + if (unity["env"] == null) + { + unity["env"] = new JObject(); + } + if (unity["disabled"] == null) { unity["disabled"] = false; From 317b099b3674befa9e237e354d05f783e75e78eb Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 21 Aug 2025 22:31:00 -0400 Subject: [PATCH 7/8] fix: write UTF-8 without BOM encoding for config files to avoid Windows compatibility issues --- UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs index faeb9f99..e1aa073c 100644 --- a/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/MCPForUnityEditorWindow.cs @@ -1106,7 +1106,8 @@ private string WriteToConfig(string pythonDir, string configPath, McpClient mcpC string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings); string tmp = configPath + ".tmp"; - System.IO.File.WriteAllText(tmp, mergedJson, System.Text.Encoding.UTF8); + // Write UTF-8 without BOM to avoid issues on Windows editors/tools + System.IO.File.WriteAllText(tmp, mergedJson, new System.Text.UTF8Encoding(false)); if (System.IO.File.Exists(configPath)) System.IO.File.Replace(tmp, configPath, null); else From c53f5f1e8ed06227f17a68ad4f1c546bb5291c35 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 21 Aug 2025 23:20:45 -0400 Subject: [PATCH 8/8] fix: enforce UTF-8 encoding without BOM when writing files to disk --- UnityMcpBridge/Editor/Helpers/PortManager.cs | 4 ++-- UnityMcpBridge/Editor/MCPForUnityBridge.cs | 2 +- UnityMcpBridge/Editor/Tools/ManageScript.cs | 4 ++-- UnityMcpBridge/Editor/Tools/ManageShader.cs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/UnityMcpBridge/Editor/Helpers/PortManager.cs b/UnityMcpBridge/Editor/Helpers/PortManager.cs index 20b63192..f041ac23 100644 --- a/UnityMcpBridge/Editor/Helpers/PortManager.cs +++ b/UnityMcpBridge/Editor/Helpers/PortManager.cs @@ -206,10 +206,10 @@ private static void SavePort(int port) string registryFile = GetRegistryFilePath(); string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented); // Write to hashed, project-scoped file - File.WriteAllText(registryFile, json); + File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false)); // Also write to legacy stable filename to avoid hash/case drift across reloads string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName); - File.WriteAllText(legacy, json); + File.WriteAllText(legacy, json, new System.Text.UTF8Encoding(false)); if (IsDebugEnabled()) Debug.Log($"MCP-FOR-UNITY: Saved port {port} to storage"); } diff --git a/UnityMcpBridge/Editor/MCPForUnityBridge.cs b/UnityMcpBridge/Editor/MCPForUnityBridge.cs index aa0c0889..82905cfe 100644 --- a/UnityMcpBridge/Editor/MCPForUnityBridge.cs +++ b/UnityMcpBridge/Editor/MCPForUnityBridge.cs @@ -719,7 +719,7 @@ private static void WriteHeartbeat(bool reloading, string reason = null) project_path = Application.dataPath, last_heartbeat = DateTime.UtcNow.ToString("O") }; - File.WriteAllText(filePath, JsonConvert.SerializeObject(payload)); + File.WriteAllText(filePath, JsonConvert.SerializeObject(payload), new System.Text.UTF8Encoding(false)); } catch (Exception) { diff --git a/UnityMcpBridge/Editor/Tools/ManageScript.cs b/UnityMcpBridge/Editor/Tools/ManageScript.cs index c36097ef..274f84d1 100644 --- a/UnityMcpBridge/Editor/Tools/ManageScript.cs +++ b/UnityMcpBridge/Editor/Tools/ManageScript.cs @@ -217,7 +217,7 @@ string namespaceName try { - File.WriteAllText(fullPath, contents); + File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); AssetDatabase.ImportAsset(relativePath); AssetDatabase.Refresh(); // Ensure Unity recognizes the new script return Response.Success( @@ -298,7 +298,7 @@ string contents try { - File.WriteAllText(fullPath, contents); + File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); AssetDatabase.ImportAsset(relativePath); // Re-import to reflect changes AssetDatabase.Refresh(); return Response.Success( diff --git a/UnityMcpBridge/Editor/Tools/ManageShader.cs b/UnityMcpBridge/Editor/Tools/ManageShader.cs index 17dffc32..c2dfbc2f 100644 --- a/UnityMcpBridge/Editor/Tools/ManageShader.cs +++ b/UnityMcpBridge/Editor/Tools/ManageShader.cs @@ -171,7 +171,7 @@ string contents try { - File.WriteAllText(fullPath, contents); + File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); AssetDatabase.ImportAsset(relativePath); AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader return Response.Success( @@ -239,7 +239,7 @@ string contents try { - File.WriteAllText(fullPath, contents); + File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); AssetDatabase.ImportAsset(relativePath); AssetDatabase.Refresh(); return Response.Success(