Skip to content

Commit 267755e

Browse files
committed
feat: add configuration helpers for MCP client setup with sophisticated path resolution
1 parent cea06de commit 267755e

File tree

6 files changed

+487
-417
lines changed

6 files changed

+487
-417
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Runtime.InteropServices;
5+
using Newtonsoft.Json;
6+
using Newtonsoft.Json.Linq;
7+
using UnityEditor;
8+
using UnityEngine;
9+
using MCPForUnity.Editor.Dependencies;
10+
using MCPForUnity.Editor.Helpers;
11+
using MCPForUnity.Editor.Models;
12+
13+
namespace MCPForUnity.Editor.Helpers
14+
{
15+
/// <summary>
16+
/// Shared helper for MCP client configuration management with sophisticated
17+
/// logic for preserving existing configs and handling different client types
18+
/// </summary>
19+
public static class McpConfigurationHelper
20+
{
21+
private const string LOCK_CONFIG_KEY = "MCPForUnity.LockCursorConfig";
22+
23+
/// <summary>
24+
/// Writes MCP configuration to the specified path using sophisticated logic
25+
/// that preserves existing configuration and only writes when necessary
26+
/// </summary>
27+
public static string WriteMcpConfiguration(string pythonDir, string configPath, McpClient mcpClient = null)
28+
{
29+
// 0) Respect explicit lock (hidden pref or UI toggle)
30+
try
31+
{
32+
if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
33+
return "Skipped (locked)";
34+
}
35+
catch { }
36+
37+
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
38+
39+
// Read existing config if it exists
40+
string existingJson = "{}";
41+
if (File.Exists(configPath))
42+
{
43+
try
44+
{
45+
existingJson = File.ReadAllText(configPath);
46+
}
47+
catch (Exception e)
48+
{
49+
Debug.LogWarning($"Error reading existing config: {e.Message}.");
50+
}
51+
}
52+
53+
// Parse the existing JSON while preserving all properties
54+
dynamic existingConfig;
55+
try
56+
{
57+
if (string.IsNullOrWhiteSpace(existingJson))
58+
{
59+
existingConfig = new JObject();
60+
}
61+
else
62+
{
63+
existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new JObject();
64+
}
65+
}
66+
catch
67+
{
68+
// If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object
69+
if (!string.IsNullOrWhiteSpace(existingJson))
70+
{
71+
Debug.LogWarning("UnityMCP: Configuration file could not be parsed; rewriting server block.");
72+
}
73+
existingConfig = new JObject();
74+
}
75+
76+
// Determine existing entry references (command/args)
77+
string existingCommand = null;
78+
string[] existingArgs = null;
79+
bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
80+
try
81+
{
82+
if (isVSCode)
83+
{
84+
existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
85+
existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject<string[]>();
86+
}
87+
else
88+
{
89+
existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
90+
existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject<string[]>();
91+
}
92+
}
93+
catch { }
94+
95+
// 1) Start from existing, only fill gaps (prefer trusted resolver)
96+
string uvPath = ServerInstaller.FindUvPath();
97+
// Optionally trust existingCommand if it looks like uv/uv.exe
98+
try
99+
{
100+
var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
101+
if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
102+
{
103+
uvPath = existingCommand;
104+
}
105+
}
106+
catch { }
107+
if (uvPath == null) return "UV package manager not found. Please install UV first.";
108+
string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
109+
110+
// 2) Canonical args order
111+
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
112+
113+
// 3) Only write if changed
114+
bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
115+
|| !ArgsEqual(existingArgs, newArgs);
116+
if (!changed)
117+
{
118+
return "Configured successfully"; // nothing to do
119+
}
120+
121+
// 4) Ensure containers exist and write back minimal changes
122+
JObject existingRoot;
123+
if (existingConfig is JObject eo)
124+
existingRoot = eo;
125+
else
126+
existingRoot = JObject.FromObject(existingConfig);
127+
128+
existingRoot = ConfigJsonBuilder.ApplyUnityServerToExistingConfig(existingRoot, uvPath, serverSrc, mcpClient);
129+
130+
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
131+
132+
McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson);
133+
134+
try
135+
{
136+
if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
137+
EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
138+
}
139+
catch { }
140+
141+
return "Configured successfully";
142+
}
143+
144+
/// <summary>
145+
/// Configures a Codex client with sophisticated TOML handling
146+
/// </summary>
147+
public static string ConfigureCodexClient(string pythonDir, string configPath, McpClient mcpClient)
148+
{
149+
try
150+
{
151+
if (EditorPrefs.GetBool(LOCK_CONFIG_KEY, false))
152+
return "Skipped (locked)";
153+
}
154+
catch { }
155+
156+
string existingToml = string.Empty;
157+
if (File.Exists(configPath))
158+
{
159+
try
160+
{
161+
existingToml = File.ReadAllText(configPath);
162+
}
163+
catch (Exception e)
164+
{
165+
Debug.LogWarning($"UnityMCP: Failed to read Codex config '{configPath}': {e.Message}");
166+
existingToml = string.Empty;
167+
}
168+
}
169+
170+
string existingCommand = null;
171+
string[] existingArgs = null;
172+
if (!string.IsNullOrWhiteSpace(existingToml))
173+
{
174+
CodexConfigHelper.TryParseCodexServer(existingToml, out existingCommand, out existingArgs);
175+
}
176+
177+
string uvPath = ServerInstaller.FindUvPath();
178+
try
179+
{
180+
var name = Path.GetFileName((existingCommand ?? string.Empty).Trim()).ToLowerInvariant();
181+
if ((name == "uv" || name == "uv.exe") && IsValidUvBinary(existingCommand))
182+
{
183+
uvPath = existingCommand;
184+
}
185+
}
186+
catch { }
187+
188+
if (uvPath == null)
189+
{
190+
return "UV package manager not found. Please install UV first.";
191+
}
192+
193+
string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
194+
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
195+
196+
bool changed = true;
197+
if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null)
198+
{
199+
changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
200+
|| !ArgsEqual(existingArgs, newArgs);
201+
}
202+
203+
if (!changed)
204+
{
205+
return "Configured successfully";
206+
}
207+
208+
string codexBlock = CodexConfigHelper.BuildCodexServerBlock(uvPath, serverSrc);
209+
string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, codexBlock);
210+
211+
McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);
212+
213+
try
214+
{
215+
if (File.Exists(uvPath)) EditorPrefs.SetString("MCPForUnity.UvPath", uvPath);
216+
EditorPrefs.SetString("MCPForUnity.ServerSrc", serverSrc);
217+
}
218+
catch { }
219+
220+
return "Configured successfully";
221+
}
222+
223+
/// <summary>
224+
/// Validates UV binary by running --version command
225+
/// </summary>
226+
private static bool IsValidUvBinary(string path)
227+
{
228+
try
229+
{
230+
if (!File.Exists(path)) return false;
231+
var psi = new System.Diagnostics.ProcessStartInfo
232+
{
233+
FileName = path,
234+
Arguments = "--version",
235+
UseShellExecute = false,
236+
RedirectStandardOutput = true,
237+
RedirectStandardError = true,
238+
CreateNoWindow = true
239+
};
240+
using var p = System.Diagnostics.Process.Start(psi);
241+
if (p == null) return false;
242+
if (!p.WaitForExit(3000)) { try { p.Kill(); } catch { } return false; }
243+
if (p.ExitCode != 0) return false;
244+
string output = p.StandardOutput.ReadToEnd().Trim();
245+
return output.StartsWith("uv ");
246+
}
247+
catch { return false; }
248+
}
249+
250+
/// <summary>
251+
/// Compares two string arrays for equality
252+
/// </summary>
253+
private static bool ArgsEqual(string[] a, string[] b)
254+
{
255+
if (a == null || b == null) return a == b;
256+
if (a.Length != b.Length) return false;
257+
for (int i = 0; i < a.Length; i++)
258+
{
259+
if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false;
260+
}
261+
return true;
262+
}
263+
264+
/// <summary>
265+
/// Gets the appropriate config file path for the given MCP client based on OS
266+
/// </summary>
267+
public static string GetClientConfigPath(McpClient mcpClient)
268+
{
269+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
270+
{
271+
return mcpClient.windowsConfigPath;
272+
}
273+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
274+
{
275+
return string.IsNullOrEmpty(mcpClient.macConfigPath)
276+
? mcpClient.linuxConfigPath
277+
: mcpClient.macConfigPath;
278+
}
279+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
280+
{
281+
return mcpClient.linuxConfigPath;
282+
}
283+
else
284+
{
285+
return mcpClient.linuxConfigPath; // fallback
286+
}
287+
}
288+
289+
/// <summary>
290+
/// Creates the directory for the config file if it doesn't exist
291+
/// </summary>
292+
public static void EnsureConfigDirectoryExists(string configPath)
293+
{
294+
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
295+
}
296+
}
297+
}

UnityMcpBridge/Editor/Helpers/McpConfigurationHelper.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)