@@ -43,15 +43,84 @@ public static bool IsCodexConfigured(string pythonDir)
4343 public static string BuildCodexServerBlock ( string uvPath , string serverSrc )
4444 {
4545 string argsArray = FormatTomlStringArray ( new [ ] { "run" , "--directory" , serverSrc , "server.py" } ) ;
46- return $ "[mcp_servers.unityMCP]{ Environment . NewLine } " +
47- $ "command = \" { EscapeTomlString ( uvPath ) } \" { Environment . NewLine } " +
48- $ "args = { argsArray } ";
46+
47+ var sb = new StringBuilder ( ) ;
48+ sb . AppendLine ( "[mcp_servers.unityMCP]" ) ;
49+ sb . AppendLine ( $ "command = \" { EscapeTomlString ( uvPath ) } \" ") ;
50+ sb . AppendLine ( $ "args = { argsArray } ") ;
51+ sb . AppendLine ( $ "startup_timeout_sec = 30") ;
52+
53+ // Windows-specific environment block to help Codex locate needed paths
54+ try
55+ {
56+ if ( Environment . OSVersion . Platform == PlatformID . Win32NT )
57+ {
58+ string userProfile = Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ?? string . Empty ;
59+ string appData = Environment . GetFolderPath ( Environment . SpecialFolder . ApplicationData ) ?? string . Empty ; // Roaming
60+ string localAppData = Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ) ?? string . Empty ;
61+ string programData = Environment . GetFolderPath ( Environment . SpecialFolder . CommonApplicationData ) ?? string . Empty ;
62+ string programFiles = Environment . GetFolderPath ( Environment . SpecialFolder . ProgramFiles ) ?? string . Empty ;
63+ string systemDrive = Environment . GetEnvironmentVariable ( "SystemDrive" ) ?? ( Path . GetPathRoot ( userProfile ) ? . TrimEnd ( '\\ ' , '/' ) ?? "C:" ) ;
64+ string systemRoot = Environment . GetEnvironmentVariable ( "SystemRoot" ) ?? Path . Combine ( systemDrive + "\\ " , "Windows" ) ;
65+ string comspec = Environment . GetEnvironmentVariable ( "COMSPEC" ) ?? Path . Combine ( Environment . SystemDirectory ?? ( systemRoot + "\\ System32" ) , "cmd.exe" ) ;
66+ string homeDrive = Environment . GetEnvironmentVariable ( "HOMEDRIVE" ) ;
67+ string homePath = Environment . GetEnvironmentVariable ( "HOMEPATH" ) ;
68+ if ( string . IsNullOrEmpty ( homeDrive ) )
69+ {
70+ homeDrive = systemDrive ;
71+ }
72+ if ( string . IsNullOrEmpty ( homePath ) && ! string . IsNullOrEmpty ( userProfile ) )
73+ {
74+ // Derive HOMEPATH from USERPROFILE (e.g., C:\\Users\\name -> \\Users\\name)
75+ if ( userProfile . StartsWith ( homeDrive + "\\ " , StringComparison . OrdinalIgnoreCase ) )
76+ {
77+ homePath = userProfile . Substring ( homeDrive . Length ) ;
78+ }
79+ else
80+ {
81+ try
82+ {
83+ var root = Path . GetPathRoot ( userProfile ) ?? string . Empty ; // e.g., C:\\
84+ homePath = userProfile . Substring ( root . Length - 1 ) ; // keep leading backslash
85+ }
86+ catch { homePath = "\\ " ; }
87+ }
88+ }
89+
90+ string powershell = Path . Combine ( Environment . SystemDirectory ?? ( systemRoot + "\\ System32" ) , "WindowsPowerShell\\ v1.0\\ powershell.exe" ) ;
91+ string pwsh = Path . Combine ( programFiles , "PowerShell\\ 7\\ pwsh.exe" ) ;
92+
93+ string tempDir = Path . Combine ( localAppData , "Temp" ) ;
94+
95+ sb . AppendLine ( ) ;
96+ sb . AppendLine ( "[mcp_servers.unityMCP.env]" ) ;
97+ sb . AppendLine ( $ "SystemRoot = \" { EscapeTomlString ( systemRoot ) } \" ") ;
98+ sb . AppendLine ( $ "APPDATA = \" { EscapeTomlString ( appData ) } \" ") ;
99+ sb . AppendLine ( $ "COMSPEC = \" { EscapeTomlString ( comspec ) } \" ") ;
100+ sb . AppendLine ( $ "HOMEDRIVE = \" { EscapeTomlString ( homeDrive ? . TrimEnd ( '\\ ' ) ?? string . Empty ) } \" ") ;
101+ sb . AppendLine ( $ "HOMEPATH = \" { EscapeTomlString ( homePath ?? string . Empty ) } \" ") ;
102+ sb . AppendLine ( $ "LOCALAPPDATA = \" { EscapeTomlString ( localAppData ) } \" ") ;
103+ sb . AppendLine ( $ "POWERSHELL = \" { EscapeTomlString ( powershell ) } \" ") ;
104+ sb . AppendLine ( $ "PROGRAMDATA = \" { EscapeTomlString ( programData ) } \" ") ;
105+ sb . AppendLine ( $ "PROGRAMFILES = \" { EscapeTomlString ( programFiles ) } \" ") ;
106+ sb . AppendLine ( $ "PWSH = \" { EscapeTomlString ( pwsh ) } \" ") ;
107+ sb . AppendLine ( $ "SYSTEMDRIVE = \" { EscapeTomlString ( systemDrive ) } \" ") ;
108+ sb . AppendLine ( $ "SYSTEMROOT = \" { EscapeTomlString ( systemRoot ) } \" ") ;
109+ sb . AppendLine ( $ "TEMP = \" { EscapeTomlString ( tempDir ) } \" ") ;
110+ sb . AppendLine ( $ "TMP = \" { EscapeTomlString ( tempDir ) } \" ") ;
111+ sb . AppendLine ( $ "USERPROFILE = \" { EscapeTomlString ( userProfile ) } \" ") ;
112+ }
113+ }
114+ catch { /* best effort */ }
115+
116+ return sb . ToString ( ) ;
49117 }
50118
51119 public static string UpsertCodexServerBlock ( string existingToml , string newBlock )
52120 {
53121 if ( string . IsNullOrWhiteSpace ( existingToml ) )
54122 {
123+ // Default to snake_case section when creating new files
55124 return newBlock . TrimEnd ( ) + Environment . NewLine ;
56125 }
57126
@@ -60,25 +129,62 @@ public static string UpsertCodexServerBlock(string existingToml, string newBlock
60129 string line ;
61130 bool inTarget = false ;
62131 bool replaced = false ;
132+
133+ // Support both TOML section casings and nested subtables (e.g., env)
134+ // Prefer the casing already present in the user's file; fall back to snake_case
135+ bool hasCamelSection = existingToml . IndexOf ( "[mcpServers.unityMCP]" , StringComparison . OrdinalIgnoreCase ) >= 0
136+ || existingToml . IndexOf ( "[mcpServers.unityMCP." , StringComparison . OrdinalIgnoreCase ) >= 0 ;
137+ bool hasSnakeSection = existingToml . IndexOf ( "[mcp_servers.unityMCP]" , StringComparison . OrdinalIgnoreCase ) >= 0
138+ || existingToml . IndexOf ( "[mcp_servers.unityMCP." , StringComparison . OrdinalIgnoreCase ) >= 0 ;
139+ bool preferCamel = hasCamelSection || ( ! hasSnakeSection && existingToml . IndexOf ( "[mcpServers]" , StringComparison . OrdinalIgnoreCase ) >= 0 ) ;
140+
141+ // Prepare block variants matching the chosen casing, including nested tables
142+ string newBlockCamel = newBlock
143+ . Replace ( "[mcp_servers.unityMCP.env]" , "[mcpServers.unityMCP.env]" )
144+ . Replace ( "[mcp_servers.unityMCP]" , "[mcpServers.unityMCP]" ) ;
145+ string newBlockEffective = preferCamel ? newBlockCamel : newBlock ;
146+
147+ static bool IsSection ( string s )
148+ {
149+ string t = s . Trim ( ) ;
150+ return t . StartsWith ( "[" ) && t . EndsWith ( "]" ) && ! t . StartsWith ( "[[" ) ;
151+ }
152+
153+ static string SectionName ( string header )
154+ {
155+ string t = header . Trim ( ) ;
156+ if ( t . StartsWith ( "[" ) && t . EndsWith ( "]" ) ) t = t . Substring ( 1 , t . Length - 2 ) ;
157+ return t ;
158+ }
159+
160+ bool TargetOrChild ( string section )
161+ {
162+ // Compare case-insensitively; accept both snake and camel as the same logical table
163+ string name = SectionName ( section ) ;
164+ return name . StartsWith ( "mcp_servers.unityMCP" , StringComparison . OrdinalIgnoreCase )
165+ || name . StartsWith ( "mcpServers.unityMCP" , StringComparison . OrdinalIgnoreCase ) ;
166+ }
167+
63168 while ( ( line = reader . ReadLine ( ) ) != null )
64169 {
65170 string trimmed = line . Trim ( ) ;
66- bool isSection = trimmed . StartsWith ( "[" ) && trimmed . EndsWith ( "]" ) && ! trimmed . StartsWith ( "[[" ) ;
171+ bool isSection = IsSection ( trimmed ) ;
67172 if ( isSection )
68173 {
69- bool isTarget = string . Equals ( trimmed , "[mcp_servers.unityMCP]" , StringComparison . OrdinalIgnoreCase ) ;
70- if ( isTarget )
174+ // If we encounter the target section or any of its nested tables, mark/keep in-target
175+ if ( TargetOrChild ( trimmed ) )
71176 {
72177 if ( ! replaced )
73178 {
74179 if ( sb . Length > 0 && sb [ ^ 1 ] != '\n ' ) sb . AppendLine ( ) ;
75- sb . AppendLine ( newBlock . TrimEnd ( ) ) ;
180+ sb . AppendLine ( newBlockEffective . TrimEnd ( ) ) ;
76181 replaced = true ;
77182 }
78183 inTarget = true ;
79184 continue ;
80185 }
81186
187+ // A new unrelated section ends the target region
82188 if ( inTarget )
83189 {
84190 inTarget = false ;
@@ -96,7 +202,7 @@ public static string UpsertCodexServerBlock(string existingToml, string newBlock
96202 if ( ! replaced )
97203 {
98204 if ( sb . Length > 0 && sb [ ^ 1 ] != '\n ' ) sb . AppendLine ( ) ;
99- sb . AppendLine ( newBlock . TrimEnd ( ) ) ;
205+ sb . AppendLine ( newBlockEffective . TrimEnd ( ) ) ;
100206 }
101207
102208 return sb . ToString ( ) . TrimEnd ( ) + Environment . NewLine ;
0 commit comments