Add Codex to autoconfig options (#288)
* feat: add Codex CLI client support with config.toml handling * feat: add config helpers for managing Codex and MCP server configurations * feat: add TOML array parsing support for multi-line and trailing comma formats * fix: handle TOML inline comments in section headers during parsing * fix: strip TOML comments before processing section headers * fix: improve JSON parsing to handle escaped single quotes in config strings * Use Tommy for TOML parsing It's a single file and OSS, easy to integrate into Unity * fix: patched Tommy’s literal-string handling so doubled single quotes inside literal strings are treated as embedded apostrophes instead of prematurely ending the value * Don't overwrite MCP configs while testing Seeing random JSON in my codex config was pretty annoying * PR Feedback * Keep Tommy compatible with Unity 2021 * Re-include Tommy's license Probably a good habit to keep all 3rd party licenses and copyrights, even if they're also MIT licensesmain
parent
da91f256a2
commit
549ac1eb0c
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d539787bf8f6a426e94bfffb32a36d4f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
using NUnit.Framework;
|
||||||
|
using MCPForUnity.Editor.Helpers;
|
||||||
|
|
||||||
|
namespace MCPForUnityTests.Editor.Helpers
|
||||||
|
{
|
||||||
|
public class CodexConfigHelperTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TryParseCodexServer_SingleLineArgs_ParsesSuccessfully()
|
||||||
|
{
|
||||||
|
string toml = string.Join("\n", new[]
|
||||||
|
{
|
||||||
|
"[mcp_servers.unityMCP]",
|
||||||
|
"command = \"uv\"",
|
||||||
|
"args = [\"run\", \"--directory\", \"/abs/path\", \"server.py\"]"
|
||||||
|
});
|
||||||
|
|
||||||
|
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||||
|
|
||||||
|
Assert.IsTrue(result, "Parser should detect server definition");
|
||||||
|
Assert.AreEqual("uv", command);
|
||||||
|
CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TryParseCodexServer_MultiLineArgsWithTrailingComma_ParsesSuccessfully()
|
||||||
|
{
|
||||||
|
string toml = string.Join("\n", new[]
|
||||||
|
{
|
||||||
|
"[mcp_servers.unityMCP]",
|
||||||
|
"command = \"uv\"",
|
||||||
|
"args = [",
|
||||||
|
" \"run\",",
|
||||||
|
" \"--directory\",",
|
||||||
|
" \"/abs/path\",",
|
||||||
|
" \"server.py\",",
|
||||||
|
"]"
|
||||||
|
});
|
||||||
|
|
||||||
|
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||||
|
|
||||||
|
Assert.IsTrue(result, "Parser should handle multi-line arrays with trailing comma");
|
||||||
|
Assert.AreEqual("uv", command);
|
||||||
|
CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TryParseCodexServer_MultiLineArgsWithComments_IgnoresComments()
|
||||||
|
{
|
||||||
|
string toml = string.Join("\n", new[]
|
||||||
|
{
|
||||||
|
"[mcp_servers.unityMCP]",
|
||||||
|
"command = \"uv\"",
|
||||||
|
"args = [",
|
||||||
|
" \"run\", # launch command",
|
||||||
|
" \"--directory\",",
|
||||||
|
" \"/abs/path\",",
|
||||||
|
" \"server.py\"",
|
||||||
|
"]"
|
||||||
|
});
|
||||||
|
|
||||||
|
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||||
|
|
||||||
|
Assert.IsTrue(result, "Parser should tolerate comments within the array block");
|
||||||
|
Assert.AreEqual("uv", command);
|
||||||
|
CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TryParseCodexServer_HeaderWithComment_StillDetected()
|
||||||
|
{
|
||||||
|
string toml = string.Join("\n", new[]
|
||||||
|
{
|
||||||
|
"[mcp_servers.unityMCP] # annotated header",
|
||||||
|
"command = \"uv\"",
|
||||||
|
"args = [\"run\", \"--directory\", \"/abs/path\", \"server.py\"]"
|
||||||
|
});
|
||||||
|
|
||||||
|
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||||
|
|
||||||
|
Assert.IsTrue(result, "Parser should recognize section headers even with inline comments");
|
||||||
|
Assert.AreEqual("uv", command);
|
||||||
|
CollectionAssert.AreEqual(new[] { "run", "--directory", "/abs/path", "server.py" }, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TryParseCodexServer_SingleQuotedArgsWithApostrophes_ParsesSuccessfully()
|
||||||
|
{
|
||||||
|
string toml = string.Join("\n", new[]
|
||||||
|
{
|
||||||
|
"[mcp_servers.unityMCP]",
|
||||||
|
"command = 'uv'",
|
||||||
|
"args = ['run', '--directory', '/Users/O''Connor/codex', 'server.py']"
|
||||||
|
});
|
||||||
|
|
||||||
|
bool result = CodexConfigHelper.TryParseCodexServer(toml, out string command, out string[] args);
|
||||||
|
|
||||||
|
Assert.IsTrue(result, "Parser should accept single-quoted arrays with escaped apostrophes");
|
||||||
|
Assert.AreEqual("uv", command);
|
||||||
|
CollectionAssert.AreEqual(new[] { "run", "--directory", "/Users/O'Connor/codex", "server.py" }, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 013424dea29744a98b3dc01618f4e95e
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -46,6 +46,8 @@ namespace MCPForUnityTests.Editor.Windows
|
||||||
EditorPrefs.SetString("MCPForUnity.ServerSrc", _serverSrcDir);
|
EditorPrefs.SetString("MCPForUnity.ServerSrc", _serverSrcDir);
|
||||||
// Ensure no lock is enabled
|
// Ensure no lock is enabled
|
||||||
EditorPrefs.SetBool("MCPForUnity.LockCursorConfig", false);
|
EditorPrefs.SetBool("MCPForUnity.LockCursorConfig", false);
|
||||||
|
// Disable auto-registration to avoid hitting user configs during tests
|
||||||
|
EditorPrefs.SetBool("MCPForUnity.AutoRegisterEnabled", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TearDown]
|
[TearDown]
|
||||||
|
|
@ -54,6 +56,7 @@ namespace MCPForUnityTests.Editor.Windows
|
||||||
// Clean up editor preferences set during SetUp
|
// Clean up editor preferences set during SetUp
|
||||||
EditorPrefs.DeleteKey("MCPForUnity.ServerSrc");
|
EditorPrefs.DeleteKey("MCPForUnity.ServerSrc");
|
||||||
EditorPrefs.DeleteKey("MCPForUnity.LockCursorConfig");
|
EditorPrefs.DeleteKey("MCPForUnity.LockCursorConfig");
|
||||||
|
EditorPrefs.DeleteKey("MCPForUnity.AutoRegisterEnabled");
|
||||||
|
|
||||||
// Remove temp files
|
// Remove temp files
|
||||||
try { if (Directory.Exists(_tempRoot)) Directory.Delete(_tempRoot, true); } catch { }
|
try { if (Directory.Exists(_tempRoot)) Directory.Delete(_tempRoot, true); } catch { }
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,28 @@ namespace MCPForUnity.Editor.Data
|
||||||
mcpType = McpTypes.Kiro,
|
mcpType = McpTypes.Kiro,
|
||||||
configStatus = "Not Configured",
|
configStatus = "Not Configured",
|
||||||
},
|
},
|
||||||
|
// 4) Codex CLI
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
name = "Codex CLI",
|
||||||
|
windowsConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
".codex",
|
||||||
|
"config.toml"
|
||||||
|
),
|
||||||
|
macConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
".codex",
|
||||||
|
"config.toml"
|
||||||
|
),
|
||||||
|
linuxConfigPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
".codex",
|
||||||
|
"config.toml"
|
||||||
|
),
|
||||||
|
mcpType = McpTypes.Codex,
|
||||||
|
configStatus = "Not Configured",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize status enums after construction
|
// Initialize status enums after construction
|
||||||
|
|
@ -174,4 +196,3 @@ namespace MCPForUnity.Editor.Data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c11944bcfb9ec4576bab52874b7df584
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ea652131dcdaa44ca8cb35cd1191be3f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MCPForUnity.External.Tommy;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Codex CLI specific configuration helpers. Handles TOML snippet
|
||||||
|
/// generation and lightweight parsing so Codex can join the auto-setup
|
||||||
|
/// flow alongside JSON-based clients.
|
||||||
|
/// </summary>
|
||||||
|
public static class CodexConfigHelper
|
||||||
|
{
|
||||||
|
public static bool IsCodexConfigured(string pythonDir)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string basePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
if (string.IsNullOrEmpty(basePath)) return false;
|
||||||
|
|
||||||
|
string configPath = Path.Combine(basePath, ".codex", "config.toml");
|
||||||
|
if (!File.Exists(configPath)) return false;
|
||||||
|
|
||||||
|
string toml = File.ReadAllText(configPath);
|
||||||
|
if (!TryParseCodexServer(toml, out _, out var args)) return false;
|
||||||
|
|
||||||
|
string dir = McpConfigFileHelper.ExtractDirectoryArg(args);
|
||||||
|
if (string.IsNullOrEmpty(dir)) return false;
|
||||||
|
|
||||||
|
return McpConfigFileHelper.PathsEqual(dir, pythonDir);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string BuildCodexServerBlock(string uvPath, string serverSrc)
|
||||||
|
{
|
||||||
|
string argsArray = FormatTomlStringArray(new[] { "run", "--directory", serverSrc, "server.py" });
|
||||||
|
return $"[mcp_servers.unityMCP]{Environment.NewLine}" +
|
||||||
|
$"command = \"{EscapeTomlString(uvPath)}\"{Environment.NewLine}" +
|
||||||
|
$"args = {argsArray}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string UpsertCodexServerBlock(string existingToml, string newBlock)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(existingToml))
|
||||||
|
{
|
||||||
|
return newBlock.TrimEnd() + Environment.NewLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
using StringReader reader = new StringReader(existingToml);
|
||||||
|
string line;
|
||||||
|
bool inTarget = false;
|
||||||
|
bool replaced = false;
|
||||||
|
while ((line = reader.ReadLine()) != null)
|
||||||
|
{
|
||||||
|
string trimmed = line.Trim();
|
||||||
|
bool isSection = trimmed.StartsWith("[") && trimmed.EndsWith("]") && !trimmed.StartsWith("[[");
|
||||||
|
if (isSection)
|
||||||
|
{
|
||||||
|
bool isTarget = string.Equals(trimmed, "[mcp_servers.unityMCP]", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (isTarget)
|
||||||
|
{
|
||||||
|
if (!replaced)
|
||||||
|
{
|
||||||
|
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
|
||||||
|
sb.AppendLine(newBlock.TrimEnd());
|
||||||
|
replaced = true;
|
||||||
|
}
|
||||||
|
inTarget = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inTarget)
|
||||||
|
{
|
||||||
|
inTarget = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inTarget)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.AppendLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!replaced)
|
||||||
|
{
|
||||||
|
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
|
||||||
|
sb.AppendLine(newBlock.TrimEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString().TrimEnd() + Environment.NewLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseCodexServer(string toml, out string command, out string[] args)
|
||||||
|
{
|
||||||
|
command = null;
|
||||||
|
args = null;
|
||||||
|
if (string.IsNullOrWhiteSpace(toml)) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var reader = new StringReader(toml);
|
||||||
|
TomlTable root = TOML.Parse(reader);
|
||||||
|
if (root == null) return false;
|
||||||
|
|
||||||
|
if (!TryGetTable(root, "mcp_servers", out var servers)
|
||||||
|
&& !TryGetTable(root, "mcpServers", out servers))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetTable(servers, "unityMCP", out var unity))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
command = GetTomlString(unity, "command");
|
||||||
|
args = GetTomlStringArray(unity, "args");
|
||||||
|
|
||||||
|
return !string.IsNullOrEmpty(command) && args != null;
|
||||||
|
}
|
||||||
|
catch (TomlParseException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (TomlSyntaxException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetTable(TomlTable parent, string key, out TomlTable table)
|
||||||
|
{
|
||||||
|
table = null;
|
||||||
|
if (parent == null) return false;
|
||||||
|
|
||||||
|
if (parent.TryGetNode(key, out var node))
|
||||||
|
{
|
||||||
|
if (node is TomlTable tbl)
|
||||||
|
{
|
||||||
|
table = tbl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node is TomlArray array)
|
||||||
|
{
|
||||||
|
var firstTable = array.Children.OfType<TomlTable>().FirstOrDefault();
|
||||||
|
if (firstTable != null)
|
||||||
|
{
|
||||||
|
table = firstTable;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetTomlString(TomlTable table, string key)
|
||||||
|
{
|
||||||
|
if (table != null && table.TryGetNode(key, out var node))
|
||||||
|
{
|
||||||
|
if (node is TomlString str) return str.Value;
|
||||||
|
if (node.HasValue) return node.ToString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] GetTomlStringArray(TomlTable table, string key)
|
||||||
|
{
|
||||||
|
if (table == null) return null;
|
||||||
|
if (!table.TryGetNode(key, out var node)) return null;
|
||||||
|
|
||||||
|
if (node is TomlArray array)
|
||||||
|
{
|
||||||
|
List<string> values = new List<string>();
|
||||||
|
foreach (TomlNode element in array.Children)
|
||||||
|
{
|
||||||
|
if (element is TomlString str)
|
||||||
|
{
|
||||||
|
values.Add(str.Value);
|
||||||
|
}
|
||||||
|
else if (element.HasValue)
|
||||||
|
{
|
||||||
|
values.Add(element.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.Count > 0 ? values.ToArray() : Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node is TomlString single)
|
||||||
|
{
|
||||||
|
return new[] { single.Value };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatTomlStringArray(IEnumerable<string> values)
|
||||||
|
{
|
||||||
|
if (values == null) return "[]";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.Append('[');
|
||||||
|
bool first = true;
|
||||||
|
foreach (string value in values)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
sb.Append(", ");
|
||||||
|
}
|
||||||
|
sb.Append('"').Append(EscapeTomlString(value ?? string.Empty)).Append('"');
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
sb.Append(']');
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EscapeTomlString(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||||||
|
return value
|
||||||
|
.Replace("\\", "\\\\")
|
||||||
|
.Replace("\"", "\\\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b3e68082ffc0b4cd39d3747673a4cc22
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using UnityEditor;
|
||||||
|
|
||||||
|
namespace MCPForUnity.Editor.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shared helpers for reading and writing MCP client configuration files.
|
||||||
|
/// Consolidates file atomics and server directory resolution so the editor
|
||||||
|
/// window can focus on UI concerns only.
|
||||||
|
/// </summary>
|
||||||
|
public static class McpConfigFileHelper
|
||||||
|
{
|
||||||
|
public static string ExtractDirectoryArg(string[] args)
|
||||||
|
{
|
||||||
|
if (args == null) return null;
|
||||||
|
for (int i = 0; i < args.Length - 1; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return args[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool PathsEqual(string a, string b)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string na = Path.GetFullPath(a.Trim());
|
||||||
|
string nb = Path.GetFullPath(b.Trim());
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
return string.Equals(na, nb, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the server directory to use for MCP tools, preferring
|
||||||
|
/// existing config values and falling back to installed/embedded copies.
|
||||||
|
/// </summary>
|
||||||
|
public static string ResolveServerDirectory(string pythonDir, string[] existingArgs)
|
||||||
|
{
|
||||||
|
string serverSrc = ExtractDirectoryArg(existingArgs);
|
||||||
|
bool serverValid = !string.IsNullOrEmpty(serverSrc)
|
||||||
|
&& File.Exists(Path.Combine(serverSrc, "server.py"));
|
||||||
|
if (!serverValid)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(pythonDir)
|
||||||
|
&& File.Exists(Path.Combine(pythonDir, "server.py")))
|
||||||
|
{
|
||||||
|
serverSrc = pythonDir;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
serverSrc = ResolveServerSource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc))
|
||||||
|
{
|
||||||
|
string norm = serverSrc.Replace('\\', '/');
|
||||||
|
int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
|
||||||
|
if (idx >= 0)
|
||||||
|
{
|
||||||
|
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
|
||||||
|
string suffix = norm.Substring(idx + "/.local/share/".Length);
|
||||||
|
serverSrc = Path.Combine(home, "Library", "Application Support", suffix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore failures and fall back to the original path.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
|
&& !string.IsNullOrEmpty(serverSrc)
|
||||||
|
&& serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
|
||||||
|
&& !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
|
||||||
|
{
|
||||||
|
serverSrc = ServerInstaller.GetServerPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteAtomicFile(string path, string contents)
|
||||||
|
{
|
||||||
|
string tmp = path + ".tmp";
|
||||||
|
string backup = path + ".backup";
|
||||||
|
bool writeDone = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(tmp, contents, new UTF8Encoding(false));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Replace(tmp, path, backup);
|
||||||
|
writeDone = true;
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
File.Move(tmp, path);
|
||||||
|
writeDone = true;
|
||||||
|
}
|
||||||
|
catch (PlatformNotSupportedException)
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(backup)) File.Delete(backup);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
File.Move(path, backup);
|
||||||
|
}
|
||||||
|
File.Move(tmp, path);
|
||||||
|
writeDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!writeDone && File.Exists(backup))
|
||||||
|
{
|
||||||
|
try { File.Copy(backup, path, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
|
||||||
|
try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveServerSource()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string remembered = EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty);
|
||||||
|
if (!string.IsNullOrEmpty(remembered)
|
||||||
|
&& File.Exists(Path.Combine(remembered, "server.py")))
|
||||||
|
{
|
||||||
|
return remembered;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerInstaller.EnsureServerInstalled();
|
||||||
|
string installed = ServerInstaller.GetServerPath();
|
||||||
|
if (File.Exists(Path.Combine(installed, "server.py")))
|
||||||
|
{
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool useEmbedded = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false);
|
||||||
|
if (useEmbedded
|
||||||
|
&& ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
|
||||||
|
&& File.Exists(Path.Combine(embedded, "server.py")))
|
||||||
|
{
|
||||||
|
return embedded;
|
||||||
|
}
|
||||||
|
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return ServerInstaller.GetServerPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f69ad468942b74c0ea24e3e8e5f21a4b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -4,10 +4,10 @@ namespace MCPForUnity.Editor.Models
|
||||||
{
|
{
|
||||||
ClaudeCode,
|
ClaudeCode,
|
||||||
ClaudeDesktop,
|
ClaudeDesktop,
|
||||||
|
Codex,
|
||||||
Cursor,
|
Cursor,
|
||||||
|
Kiro,
|
||||||
VSCode,
|
VSCode,
|
||||||
Windsurf,
|
Windsurf,
|
||||||
Kiro,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -568,8 +568,9 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// For Cursor/others, skip if already configured
|
CheckMcpConfiguration(client);
|
||||||
if (!IsCursorConfigured(pythonDir))
|
bool alreadyConfigured = client.status == McpStatus.Configured;
|
||||||
|
if (!alreadyConfigured)
|
||||||
{
|
{
|
||||||
ConfigureMcpClient(client);
|
ConfigureMcpClient(client);
|
||||||
anyRegistered = true;
|
anyRegistered = true;
|
||||||
|
|
@ -581,7 +582,10 @@ namespace MCPForUnity.Editor.Windows
|
||||||
MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup client '{client.name}' failed: {ex.Message}");
|
MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup client '{client.name}' failed: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastClientRegisteredOk = anyRegistered || IsCursorConfigured(pythonDir) || IsClaudeConfigured();
|
lastClientRegisteredOk = anyRegistered
|
||||||
|
|| IsCursorConfigured(pythonDir)
|
||||||
|
|| CodexConfigHelper.IsCodexConfigured(pythonDir)
|
||||||
|
|| IsClaudeConfigured();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the bridge is listening and has a fresh saved port
|
// Ensure the bridge is listening and has a fresh saved port
|
||||||
|
|
@ -658,7 +662,9 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!IsCursorConfigured(pythonDir))
|
CheckMcpConfiguration(client);
|
||||||
|
bool alreadyConfigured = client.status == McpStatus.Configured;
|
||||||
|
if (!alreadyConfigured)
|
||||||
{
|
{
|
||||||
ConfigureMcpClient(client);
|
ConfigureMcpClient(client);
|
||||||
anyRegistered = true;
|
anyRegistered = true;
|
||||||
|
|
@ -670,7 +676,10 @@ namespace MCPForUnity.Editor.Windows
|
||||||
UnityEngine.Debug.LogWarning($"Setup client '{client.name}' failed: {ex.Message}");
|
UnityEngine.Debug.LogWarning($"Setup client '{client.name}' failed: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastClientRegisteredOk = anyRegistered || IsCursorConfigured(pythonDir) || IsClaudeConfigured();
|
lastClientRegisteredOk = anyRegistered
|
||||||
|
|| IsCursorConfigured(pythonDir)
|
||||||
|
|| CodexConfigHelper.IsCodexConfigured(pythonDir)
|
||||||
|
|| IsClaudeConfigured();
|
||||||
|
|
||||||
// Restart/ensure bridge
|
// Restart/ensure bridge
|
||||||
MCPForUnityBridge.StartAutoConnect();
|
MCPForUnityBridge.StartAutoConnect();
|
||||||
|
|
@ -686,11 +695,11 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsCursorConfigured(string pythonDir)
|
private static bool IsCursorConfigured(string pythonDir)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||||
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
".cursor", "mcp.json")
|
".cursor", "mcp.json")
|
||||||
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
|
|
@ -708,24 +717,9 @@ namespace MCPForUnity.Editor.Windows
|
||||||
string[] strArgs = ((System.Collections.Generic.IEnumerable<object>)args)
|
string[] strArgs = ((System.Collections.Generic.IEnumerable<object>)args)
|
||||||
.Select(x => x?.ToString() ?? string.Empty)
|
.Select(x => x?.ToString() ?? string.Empty)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
string dir = ExtractDirectoryArg(strArgs);
|
string dir = McpConfigFileHelper.ExtractDirectoryArg(strArgs);
|
||||||
if (string.IsNullOrEmpty(dir)) return false;
|
if (string.IsNullOrEmpty(dir)) return false;
|
||||||
return PathsEqual(dir, pythonDir);
|
return McpConfigFileHelper.PathsEqual(dir, pythonDir);
|
||||||
}
|
|
||||||
catch { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool PathsEqual(string a, string b)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string na = System.IO.Path.GetFullPath(a.Trim());
|
|
||||||
string nb = System.IO.Path.GetFullPath(b.Trim());
|
|
||||||
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
|
|
||||||
return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
|
|
||||||
// Default to ordinal on Unix; optionally detect FS case-sensitivity at runtime if needed
|
|
||||||
return string.Equals(na, nb, StringComparison.Ordinal);
|
|
||||||
}
|
}
|
||||||
catch { return false; }
|
catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
@ -1136,19 +1130,6 @@ namespace MCPForUnity.Editor.Windows
|
||||||
catch { return false; }
|
catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ExtractDirectoryArg(string[] args)
|
|
||||||
{
|
|
||||||
if (args == null) return null;
|
|
||||||
for (int i = 0; i < args.Length - 1; i++)
|
|
||||||
{
|
|
||||||
if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return args[i + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ArgsEqual(string[] a, string[] b)
|
private static bool ArgsEqual(string[] a, string[] b)
|
||||||
{
|
{
|
||||||
if (a == null || b == null) return a == b;
|
if (a == null || b == null) return a == b;
|
||||||
|
|
@ -1236,48 +1217,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
if (uvPath == null) return "UV package manager not found. Please install UV first.";
|
if (uvPath == null) return "UV package manager not found. Please install UV first.";
|
||||||
string serverSrc = ExtractDirectoryArg(existingArgs);
|
string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
|
||||||
bool serverValid = !string.IsNullOrEmpty(serverSrc)
|
|
||||||
&& System.IO.File.Exists(System.IO.Path.Combine(serverSrc, "server.py"));
|
|
||||||
if (!serverValid)
|
|
||||||
{
|
|
||||||
// Prefer the provided pythonDir if valid; fall back to resolver
|
|
||||||
if (!string.IsNullOrEmpty(pythonDir) && System.IO.File.Exists(System.IO.Path.Combine(pythonDir, "server.py")))
|
|
||||||
{
|
|
||||||
serverSrc = pythonDir;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
serverSrc = ResolveServerSrc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// macOS normalization: map XDG-style ~/.local/share to canonical Application Support
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX)
|
|
||||||
&& !string.IsNullOrEmpty(serverSrc))
|
|
||||||
{
|
|
||||||
string norm = serverSrc.Replace('\\', '/');
|
|
||||||
int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
|
|
||||||
if (idx >= 0)
|
|
||||||
{
|
|
||||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
|
|
||||||
string suffix = norm.Substring(idx + "/.local/share/".Length); // UnityMCP/...
|
|
||||||
serverSrc = System.IO.Path.Combine(home, "Library", "Application Support", suffix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
// Hard-block PackageCache on Windows unless dev override is set
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
|
||||||
&& !string.IsNullOrEmpty(serverSrc)
|
|
||||||
&& serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
|
|
||||||
&& !UnityEditor.EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
|
|
||||||
{
|
|
||||||
serverSrc = ServerInstaller.GetServerPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Canonical args order
|
// 2) Canonical args order
|
||||||
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
|
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
|
||||||
|
|
@ -1301,60 +1241,7 @@ namespace MCPForUnity.Editor.Windows
|
||||||
|
|
||||||
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
|
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
|
||||||
|
|
||||||
// Robust atomic write without redundant backup or race on existence
|
McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson);
|
||||||
string tmp = configPath + ".tmp";
|
|
||||||
string backup = configPath + ".backup";
|
|
||||||
bool writeDone = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Write to temp file first (in same directory for atomicity)
|
|
||||||
System.IO.File.WriteAllText(tmp, mergedJson, new System.Text.UTF8Encoding(false));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Try atomic replace; creates 'backup' only on success (platform-dependent)
|
|
||||||
System.IO.File.Replace(tmp, configPath, backup);
|
|
||||||
writeDone = true;
|
|
||||||
}
|
|
||||||
catch (System.IO.FileNotFoundException)
|
|
||||||
{
|
|
||||||
// Destination didn't exist; fall back to move
|
|
||||||
System.IO.File.Move(tmp, configPath);
|
|
||||||
writeDone = true;
|
|
||||||
}
|
|
||||||
catch (System.PlatformNotSupportedException)
|
|
||||||
{
|
|
||||||
// Fallback: rename existing to backup, then move tmp into place
|
|
||||||
if (System.IO.File.Exists(configPath))
|
|
||||||
{
|
|
||||||
try { if (System.IO.File.Exists(backup)) System.IO.File.Delete(backup); } catch { }
|
|
||||||
System.IO.File.Move(configPath, backup);
|
|
||||||
}
|
|
||||||
System.IO.File.Move(tmp, configPath);
|
|
||||||
writeDone = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
|
|
||||||
// If write did not complete, attempt restore from backup without deleting current file first
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!writeDone && System.IO.File.Exists(backup))
|
|
||||||
{
|
|
||||||
try { System.IO.File.Copy(backup, configPath, true); } catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
throw new Exception($"Failed to write config file '{configPath}': {ex.Message}", ex);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
// Best-effort cleanup of temp
|
|
||||||
try { if (System.IO.File.Exists(tmp)) System.IO.File.Delete(tmp); } catch { }
|
|
||||||
// Only remove backup after a confirmed successful write
|
|
||||||
try { if (writeDone && System.IO.File.Exists(backup)) System.IO.File.Delete(backup); } catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -1377,54 +1264,27 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
|
|
||||||
// New method to show manual instructions without changing status
|
// New method to show manual instructions without changing status
|
||||||
private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient)
|
private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient)
|
||||||
{
|
|
||||||
// Get the Python directory path using Package Manager API
|
|
||||||
string pythonDir = FindPackagePythonDirectory();
|
|
||||||
// Build manual JSON centrally using the shared builder
|
|
||||||
string uvPathForManual = FindUvPath();
|
|
||||||
if (uvPathForManual == null)
|
|
||||||
{
|
|
||||||
UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string manualConfigJson = ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient);
|
|
||||||
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ResolveServerSrc()
|
|
||||||
{
|
{
|
||||||
try
|
// Get the Python directory path using Package Manager API
|
||||||
|
string pythonDir = FindPackagePythonDirectory();
|
||||||
|
// Build manual JSON centrally using the shared builder
|
||||||
|
string uvPathForManual = FindUvPath();
|
||||||
|
if (uvPathForManual == null)
|
||||||
{
|
{
|
||||||
string remembered = UnityEditor.EditorPrefs.GetString("MCPForUnity.ServerSrc", string.Empty);
|
UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration.");
|
||||||
if (!string.IsNullOrEmpty(remembered) && File.Exists(Path.Combine(remembered, "server.py")))
|
return;
|
||||||
{
|
|
||||||
return remembered;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerInstaller.EnsureServerInstalled();
|
|
||||||
string installed = ServerInstaller.GetServerPath();
|
|
||||||
if (File.Exists(Path.Combine(installed, "server.py")))
|
|
||||||
{
|
|
||||||
return installed;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool useEmbedded = UnityEditor.EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false);
|
|
||||||
if (useEmbedded && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
|
|
||||||
&& File.Exists(Path.Combine(embedded, "server.py")))
|
|
||||||
{
|
|
||||||
return embedded;
|
|
||||||
}
|
|
||||||
|
|
||||||
return installed;
|
|
||||||
}
|
}
|
||||||
catch { return ServerInstaller.GetServerPath(); }
|
|
||||||
|
string manualConfig = mcpClient?.mcpType == McpTypes.Codex
|
||||||
|
? CodexConfigHelper.BuildCodexServerBlock(uvPathForManual, McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)).TrimEnd() + Environment.NewLine
|
||||||
|
: ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient);
|
||||||
|
ManualConfigEditorWindow.ShowWindow(configPath, manualConfig, mcpClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FindPackagePythonDirectory()
|
private string FindPackagePythonDirectory()
|
||||||
{
|
{
|
||||||
string pythonDir = ResolveServerSrc();
|
string pythonDir = McpConfigFileHelper.ResolveServerSource();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -1508,12 +1368,12 @@ namespace MCPForUnity.Editor.Windows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ConfigureMcpClient(McpClient mcpClient)
|
private string ConfigureMcpClient(McpClient mcpClient)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Determine the config file path based on OS
|
// Determine the config file path based on OS
|
||||||
string configPath;
|
string configPath;
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
|
|
@ -1541,21 +1401,23 @@ namespace MCPForUnity.Editor.Windows
|
||||||
// Create directory if it doesn't exist
|
// Create directory if it doesn't exist
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
|
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
|
||||||
|
|
||||||
// Find the server.py file location using the same logic as FindPackagePythonDirectory
|
// Find the server.py file location using the same logic as FindPackagePythonDirectory
|
||||||
string pythonDir = FindPackagePythonDirectory();
|
string pythonDir = FindPackagePythonDirectory();
|
||||||
|
|
||||||
if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py")))
|
if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py")))
|
||||||
{
|
{
|
||||||
ShowManualInstructionsWindow(configPath, mcpClient);
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
||||||
return "Manual Configuration Required";
|
return "Manual Configuration Required";
|
||||||
}
|
}
|
||||||
|
|
||||||
string result = WriteToConfig(pythonDir, configPath, mcpClient);
|
string result = mcpClient.mcpType == McpTypes.Codex
|
||||||
|
? ConfigureCodexClient(pythonDir, configPath, mcpClient)
|
||||||
|
: WriteToConfig(pythonDir, configPath, mcpClient);
|
||||||
|
|
||||||
// Update the client status after successful configuration
|
// Update the client status after successful configuration
|
||||||
if (result == "Configured successfully")
|
if (result == "Configured successfully")
|
||||||
{
|
{
|
||||||
mcpClient.SetStatus(McpStatus.Configured);
|
mcpClient.SetStatus(McpStatus.Configured);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -1588,8 +1450,82 @@ namespace MCPForUnity.Editor.Windows
|
||||||
$"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}"
|
$"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}"
|
||||||
);
|
);
|
||||||
return $"Failed to configure {mcpClient.name}";
|
return $"Failed to configure {mcpClient.name}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient)
|
||||||
|
{
|
||||||
|
try { if (EditorPrefs.GetBool("MCPForUnity.LockCursorConfig", false)) return "Skipped (locked)"; } catch { }
|
||||||
|
|
||||||
|
string existingToml = string.Empty;
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
existingToml = File.ReadAllText(configPath);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (debugLogsEnabled)
|
||||||
|
{
|
||||||
|
UnityEngine.Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}");
|
||||||
|
}
|
||||||
|
existingToml = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string existingCommand = null;
|
||||||
|
string[] existingArgs = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(existingToml))
|
||||||
|
{
|
||||||
|
CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
string uvPath = ServerInstaller.FindUvPath();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
|
||||||
|
if ((name == "uv" || name == "uv.exe") && ValidateUvBinarySafe(existingCommand))
|
||||||
|
{
|
||||||
|
uvPath = existingCommand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
if (uvPath == null)
|
||||||
|
{
|
||||||
|
return "UV package manager not found. Please install UV first.";
|
||||||
|
}
|
||||||
|
|
||||||
|
string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
|
||||||
|
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
|
||||||
|
|
||||||
|
bool changed = true;
|
||||||
|
if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null)
|
||||||
|
{
|
||||||
|
changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
|
||||||
|
|| !ArgsEqual(existingArgs, newArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed)
|
||||||
|
{
|
||||||
|
return "Configured successfully";
|
||||||
|
}
|
||||||
|
|
||||||
|
string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc);
|
||||||
|
string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock);
|
||||||
|
|
||||||
|
McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsValidUv(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
|
||||||
|
EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
return "Configured successfully";
|
||||||
|
}
|
||||||
|
|
||||||
private void ShowCursorManualConfigurationInstructions(
|
private void ShowCursorManualConfigurationInstructions(
|
||||||
string configPath,
|
string configPath,
|
||||||
|
|
@ -1721,28 +1657,36 @@ namespace MCPForUnity.Editor.Windows
|
||||||
string[] args = null;
|
string[] args = null;
|
||||||
bool configExists = false;
|
bool configExists = false;
|
||||||
|
|
||||||
switch (mcpClient.mcpType)
|
switch (mcpClient.mcpType)
|
||||||
{
|
{
|
||||||
case McpTypes.VSCode:
|
case McpTypes.VSCode:
|
||||||
dynamic config = JsonConvert.DeserializeObject(configJson);
|
dynamic config = JsonConvert.DeserializeObject(configJson);
|
||||||
|
|
||||||
// New schema: top-level servers
|
// New schema: top-level servers
|
||||||
if (config?.servers?.unityMCP != null)
|
if (config?.servers?.unityMCP != null)
|
||||||
{
|
{
|
||||||
args = config.servers.unityMCP.args.ToObject<string[]>();
|
args = config.servers.unityMCP.args.ToObject<string[]>();
|
||||||
configExists = true;
|
configExists = true;
|
||||||
}
|
}
|
||||||
// Back-compat: legacy mcp.servers
|
// Back-compat: legacy mcp.servers
|
||||||
else if (config?.mcp?.servers?.unityMCP != null)
|
else if (config?.mcp?.servers?.unityMCP != null)
|
||||||
{
|
{
|
||||||
args = config.mcp.servers.unityMCP.args.ToObject<string[]>();
|
args = config.mcp.servers.unityMCP.args.ToObject<string[]>();
|
||||||
configExists = true;
|
configExists = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
case McpTypes.Codex:
|
||||||
// Standard MCP configuration check for Claude Desktop, Cursor, etc.
|
if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs))
|
||||||
McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
|
{
|
||||||
|
args = codexArgs;
|
||||||
|
configExists = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Standard MCP configuration check for Claude Desktop, Cursor, etc.
|
||||||
|
McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
|
||||||
|
|
||||||
if (standardConfig?.mcpServers?.unityMCP != null)
|
if (standardConfig?.mcpServers?.unityMCP != null)
|
||||||
{
|
{
|
||||||
|
|
@ -1755,8 +1699,8 @@ namespace MCPForUnity.Editor.Windows
|
||||||
// Common logic for checking configuration status
|
// Common logic for checking configuration status
|
||||||
if (configExists)
|
if (configExists)
|
||||||
{
|
{
|
||||||
string configuredDir = ExtractDirectoryArg(args);
|
string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args);
|
||||||
bool matches = !string.IsNullOrEmpty(configuredDir) && PathsEqual(configuredDir, pythonDir);
|
bool matches = !string.IsNullOrEmpty(configuredDir) && McpConfigFileHelper.PathsEqual(configuredDir, pythonDir);
|
||||||
if (matches)
|
if (matches)
|
||||||
{
|
{
|
||||||
mcpClient.SetStatus(McpStatus.Configured);
|
mcpClient.SetStatus(McpStatus.Configured);
|
||||||
|
|
@ -1766,7 +1710,9 @@ namespace MCPForUnity.Editor.Windows
|
||||||
// Attempt auto-rewrite once if the package path changed
|
// Attempt auto-rewrite once if the package path changed
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string rewriteResult = WriteToConfig(pythonDir, configPath, mcpClient);
|
string rewriteResult = mcpClient.mcpType == McpTypes.Codex
|
||||||
|
? ConfigureCodexClient(pythonDir, configPath, mcpClient)
|
||||||
|
: WriteToConfig(pythonDir, configPath, mcpClient);
|
||||||
if (rewriteResult == "Configured successfully")
|
if (rewriteResult == "Configured successfully")
|
||||||
{
|
{
|
||||||
if (debugLogsEnabled)
|
if (debugLogsEnabled)
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,13 @@ namespace MCPForUnity.Editor.Windows
|
||||||
instructionStyle
|
instructionStyle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
else if (mcpClient?.mcpType == McpTypes.Codex)
|
||||||
|
{
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
" a) Running `codex config edit` in a terminal",
|
||||||
|
instructionStyle
|
||||||
|
);
|
||||||
|
}
|
||||||
EditorGUILayout.LabelField(" OR", instructionStyle);
|
EditorGUILayout.LabelField(" OR", instructionStyle);
|
||||||
EditorGUILayout.LabelField(
|
EditorGUILayout.LabelField(
|
||||||
" b) Opening the configuration file at:",
|
" b) Opening the configuration file at:",
|
||||||
|
|
@ -201,10 +208,10 @@ namespace MCPForUnity.Editor.Windows
|
||||||
|
|
||||||
EditorGUILayout.Space(10);
|
EditorGUILayout.Space(10);
|
||||||
|
|
||||||
EditorGUILayout.LabelField(
|
string configLabel = mcpClient?.mcpType == McpTypes.Codex
|
||||||
"2. Paste the following JSON configuration:",
|
? "2. Paste the following TOML configuration:"
|
||||||
instructionStyle
|
: "2. Paste the following JSON configuration:";
|
||||||
);
|
EditorGUILayout.LabelField(configLabel, instructionStyle);
|
||||||
|
|
||||||
// JSON section with improved styling
|
// JSON section with improved styling
|
||||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue