Skip to content

Commit cdbf5be

Browse files
committed
feat: add config helpers for managing Codex and MCP server configurations
1 parent f14a22c commit cdbf5be

File tree

5 files changed

+521
-369
lines changed

5 files changed

+521
-369
lines changed
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text;
5+
using Newtonsoft.Json;
6+
7+
namespace MCPForUnity.Editor.Helpers
8+
{
9+
/// <summary>
10+
/// Codex CLI specific configuration helpers. Handles TOML snippet
11+
/// generation and lightweight parsing so Codex can join the auto-setup
12+
/// flow alongside JSON-based clients.
13+
/// </summary>
14+
public static class CodexConfigHelper
15+
{
16+
public static bool IsCodexConfigured(string pythonDir)
17+
{
18+
try
19+
{
20+
string basePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
21+
if (string.IsNullOrEmpty(basePath)) return false;
22+
23+
string configPath = Path.Combine(basePath, ".codex", "config.toml");
24+
if (!File.Exists(configPath)) return false;
25+
26+
string toml = File.ReadAllText(configPath);
27+
if (!TryParseCodexServer(toml, out _, out var args)) return false;
28+
29+
string dir = McpConfigFileHelper.ExtractDirectoryArg(args);
30+
if (string.IsNullOrEmpty(dir)) return false;
31+
32+
return McpConfigFileHelper.PathsEqual(dir, pythonDir);
33+
}
34+
catch
35+
{
36+
return false;
37+
}
38+
}
39+
40+
public static string BuildCodexServerBlock(string uvPath, string serverSrc)
41+
{
42+
string argsArray = FormatTomlStringArray(new[] { "run", "--directory", serverSrc, "server.py" });
43+
return $"[mcp_servers.unityMCP]{Environment.NewLine}" +
44+
$"command = \"{EscapeTomlString(uvPath)}\"{Environment.NewLine}" +
45+
$"args = {argsArray}";
46+
}
47+
48+
public static string UpsertCodexServerBlock(string existingToml, string newBlock)
49+
{
50+
if (string.IsNullOrWhiteSpace(existingToml))
51+
{
52+
return newBlock.TrimEnd() + Environment.NewLine;
53+
}
54+
55+
StringBuilder sb = new StringBuilder();
56+
using StringReader reader = new StringReader(existingToml);
57+
string line;
58+
bool inTarget = false;
59+
bool replaced = false;
60+
while ((line = reader.ReadLine()) != null)
61+
{
62+
string trimmed = line.Trim();
63+
bool isSection = trimmed.StartsWith("[") && trimmed.EndsWith("]") && !trimmed.StartsWith("[[");
64+
if (isSection)
65+
{
66+
bool isTarget = string.Equals(trimmed, "[mcp_servers.unityMCP]", StringComparison.OrdinalIgnoreCase);
67+
if (isTarget)
68+
{
69+
if (!replaced)
70+
{
71+
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
72+
sb.AppendLine(newBlock.TrimEnd());
73+
replaced = true;
74+
}
75+
inTarget = true;
76+
continue;
77+
}
78+
79+
if (inTarget)
80+
{
81+
inTarget = false;
82+
}
83+
}
84+
85+
if (inTarget)
86+
{
87+
continue;
88+
}
89+
90+
sb.AppendLine(line);
91+
}
92+
93+
if (!replaced)
94+
{
95+
if (sb.Length > 0 && sb[^1] != '\n') sb.AppendLine();
96+
sb.AppendLine(newBlock.TrimEnd());
97+
}
98+
99+
return sb.ToString().TrimEnd() + Environment.NewLine;
100+
}
101+
102+
public static bool TryParseCodexServer(string toml, out string command, out string[] args)
103+
{
104+
command = null;
105+
args = null;
106+
if (string.IsNullOrEmpty(toml)) return false;
107+
108+
using StringReader reader = new StringReader(toml);
109+
string line;
110+
bool inTarget = false;
111+
while ((line = reader.ReadLine()) != null)
112+
{
113+
string trimmed = line.Trim();
114+
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#")) continue;
115+
116+
bool isSection = trimmed.StartsWith("[") && trimmed.EndsWith("]") && !trimmed.StartsWith("[[");
117+
if (isSection)
118+
{
119+
inTarget = string.Equals(trimmed, "[mcp_servers.unityMCP]", StringComparison.OrdinalIgnoreCase);
120+
continue;
121+
}
122+
123+
if (!inTarget) continue;
124+
125+
if (trimmed.StartsWith("command", StringComparison.OrdinalIgnoreCase))
126+
{
127+
int eq = trimmed.IndexOf('=');
128+
if (eq >= 0)
129+
{
130+
string raw = trimmed[(eq + 1)..];
131+
command = ParseTomlStringValue(raw);
132+
}
133+
}
134+
else if (trimmed.StartsWith("args", StringComparison.OrdinalIgnoreCase))
135+
{
136+
int eq = trimmed.IndexOf('=');
137+
if (eq >= 0)
138+
{
139+
string raw = trimmed[(eq + 1)..];
140+
args = ParseTomlStringArray(raw);
141+
}
142+
}
143+
}
144+
145+
return !string.IsNullOrEmpty(command) && args != null;
146+
}
147+
148+
private static string FormatTomlStringArray(IEnumerable<string> values)
149+
{
150+
if (values == null) return "[]";
151+
StringBuilder sb = new StringBuilder();
152+
sb.Append('[');
153+
bool first = true;
154+
foreach (string value in values)
155+
{
156+
if (!first)
157+
{
158+
sb.Append(", ");
159+
}
160+
sb.Append('"').Append(EscapeTomlString(value ?? string.Empty)).Append('"');
161+
first = false;
162+
}
163+
sb.Append(']');
164+
return sb.ToString();
165+
}
166+
167+
private static string EscapeTomlString(string value)
168+
{
169+
if (string.IsNullOrEmpty(value)) return string.Empty;
170+
return value
171+
.Replace("\\", "\\\\")
172+
.Replace("\"", "\\\"");
173+
}
174+
175+
private static string ParseTomlStringValue(string value)
176+
{
177+
if (value == null) return null;
178+
string trimmed = StripTomlComment(value).Trim();
179+
if (trimmed.Length >= 2 && trimmed[0] == '"' && trimmed[^1] == '"')
180+
{
181+
return UnescapeTomlBasicString(trimmed.Substring(1, trimmed.Length - 2));
182+
}
183+
if (trimmed.Length >= 2 && trimmed[0] == '\'' && trimmed[^1] == '\'')
184+
{
185+
return trimmed.Substring(1, trimmed.Length - 2);
186+
}
187+
return trimmed.Trim();
188+
}
189+
190+
private static string[] ParseTomlStringArray(string value)
191+
{
192+
if (value == null) return null;
193+
string cleaned = StripTomlComment(value).Trim();
194+
if (!cleaned.StartsWith("[") || !cleaned.EndsWith("]")) return null;
195+
try
196+
{
197+
return JsonConvert.DeserializeObject<string[]>(cleaned);
198+
}
199+
catch
200+
{
201+
if (cleaned.IndexOf('"') < 0 && cleaned.IndexOf('\'') >= 0)
202+
{
203+
string alt = cleaned.Replace('\'', '\"');
204+
try { return JsonConvert.DeserializeObject<string[]>(alt); } catch { }
205+
}
206+
}
207+
return null;
208+
}
209+
210+
private static string StripTomlComment(string value)
211+
{
212+
if (string.IsNullOrEmpty(value)) return string.Empty;
213+
bool inDouble = false;
214+
bool inSingle = false;
215+
bool escape = false;
216+
for (int i = 0; i < value.Length; i++)
217+
{
218+
char c = value[i];
219+
if (escape)
220+
{
221+
escape = false;
222+
continue;
223+
}
224+
if (c == '\\' && inDouble)
225+
{
226+
escape = true;
227+
continue;
228+
}
229+
if (c == '"' && !inSingle)
230+
{
231+
inDouble = !inDouble;
232+
continue;
233+
}
234+
if (c == '\'' && !inDouble)
235+
{
236+
inSingle = !inSingle;
237+
continue;
238+
}
239+
if (c == '#' && !inSingle && !inDouble)
240+
{
241+
return value.Substring(0, i).TrimEnd();
242+
}
243+
}
244+
return value.Trim();
245+
}
246+
247+
private static string UnescapeTomlBasicString(string value)
248+
{
249+
if (string.IsNullOrEmpty(value)) return string.Empty;
250+
StringBuilder sb = new StringBuilder(value.Length);
251+
for (int i = 0; i < value.Length; i++)
252+
{
253+
char c = value[i];
254+
if (c == '\\' && i + 1 < value.Length)
255+
{
256+
char next = value[++i];
257+
sb.Append(next switch
258+
{
259+
'\\' => '\\',
260+
'"' => '"',
261+
'n' => '\n',
262+
'r' => '\r',
263+
't' => '\t',
264+
'b' => '\b',
265+
'f' => '\f',
266+
_ => next
267+
});
268+
continue;
269+
}
270+
sb.Append(c);
271+
}
272+
return sb.ToString();
273+
}
274+
}
275+
}

UnityMcpBridge/Editor/Helpers/CodexConfigHelper.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)