@@ -23,6 +23,7 @@ public static void EnsureServerInstalled()
2323 try
2424 {
2525 string saveLocation = GetSaveLocation ( ) ;
26+ TryCreateMacSymlinkForAppSupport ( ) ;
2627 string destRoot = Path . Combine ( saveLocation , ServerFolder ) ;
2728 string destSrc = Path . Combine ( destRoot , "src" ) ;
2829
@@ -117,57 +118,79 @@ public static string GetServerPath()
117118 /// </summary>
118119 private static string GetSaveLocation ( )
119120 {
120- // Prefer Unity's platform first (more reliable under Mono/macOS), then fallback
121- try
122- {
123- if ( Application . platform == RuntimePlatform . OSXEditor )
124- {
125- string home = Environment . GetFolderPath ( Environment . SpecialFolder . Personal ) ?? string . Empty ;
126- string appSupport = Path . Combine ( home , "Library" , "Application Support" ) ;
127- return Path . Combine ( appSupport , RootFolder ) ;
128- }
129- if ( Application . platform == RuntimePlatform . WindowsEditor )
130- {
131- var localAppData = Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData )
132- ?? Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ?? string . Empty , "AppData" , "Local" ) ;
133- return Path . Combine ( localAppData , RootFolder ) ;
134- }
135- if ( Application . platform == RuntimePlatform . LinuxEditor )
136- {
137- var xdg = Environment . GetEnvironmentVariable ( "XDG_DATA_HOME" ) ;
138- if ( string . IsNullOrEmpty ( xdg ) )
139- {
140- xdg = Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ?? string . Empty , ".local" , "share" ) ;
141- }
142- return Path . Combine ( xdg , RootFolder ) ;
143- }
144- }
145- catch { }
146-
147- // Fallback to RuntimeInformation
148121 if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
149122 {
123+ // Use per-user LocalApplicationData for canonical install location
150124 var localAppData = Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData )
151125 ?? Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ?? string . Empty , "AppData" , "Local" ) ;
152126 return Path . Combine ( localAppData , RootFolder ) ;
153127 }
154- if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Linux ) )
128+ else if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Linux ) )
155129 {
156130 var xdg = Environment . GetEnvironmentVariable ( "XDG_DATA_HOME" ) ;
157131 if ( string . IsNullOrEmpty ( xdg ) )
158132 {
159- xdg = Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ?? string . Empty , ".local" , "share" ) ;
133+ xdg = Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ?? string . Empty ,
134+ ".local" , "share" ) ;
160135 }
161136 return Path . Combine ( xdg , RootFolder ) ;
162137 }
163- if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
138+ else if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
164139 {
165- string home = Environment . GetFolderPath ( Environment . SpecialFolder . Personal ) ?? string . Empty ;
166- return Path . Combine ( home , "Library" , "Application Support" , RootFolder ) ;
140+ // On macOS, use LocalApplicationData (~/Library/Application Support)
141+ var localAppSupport = Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ) ;
142+ // Unity/Mono may map LocalApplicationData to ~/.local/share on macOS; normalize to Application Support
143+ bool looksLikeXdg = ! string . IsNullOrEmpty ( localAppSupport ) && localAppSupport . Replace ( '\\ ' , '/' ) . Contains ( "/.local/share" ) ;
144+ if ( string . IsNullOrEmpty ( localAppSupport ) || looksLikeXdg )
145+ {
146+ // Fallback: construct from $HOME
147+ var home = Environment . GetFolderPath ( Environment . SpecialFolder . Personal ) ?? string . Empty ;
148+ localAppSupport = Path . Combine ( home , "Library" , "Application Support" ) ;
149+ }
150+ TryCreateMacSymlinkForAppSupport ( ) ;
151+ return Path . Combine ( localAppSupport , RootFolder ) ;
167152 }
168153 throw new Exception ( "Unsupported operating system." ) ;
169154 }
170155
156+ /// <summary>
157+ /// On macOS, create a no-spaces symlink ~/Library/AppSupport -> ~/Library/Application Support
158+ /// to mitigate arg parsing and quoting issues in some MCP clients.
159+ /// Safe to call repeatedly.
160+ /// </summary>
161+ private static void TryCreateMacSymlinkForAppSupport ( )
162+ {
163+ try
164+ {
165+ if ( ! RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) ) return ;
166+ string home = Environment . GetFolderPath ( Environment . SpecialFolder . Personal ) ?? string . Empty ;
167+ if ( string . IsNullOrEmpty ( home ) ) return ;
168+
169+ string canonical = Path . Combine ( home , "Library" , "Application Support" ) ;
170+ string symlink = Path . Combine ( home , "Library" , "AppSupport" ) ;
171+
172+ // If symlink exists already, nothing to do
173+ if ( Directory . Exists ( symlink ) || File . Exists ( symlink ) ) return ;
174+
175+ // Create symlink only if canonical exists
176+ if ( ! Directory . Exists ( canonical ) ) return ;
177+
178+ // Use 'ln -s' to create a directory symlink (macOS)
179+ var psi = new System . Diagnostics . ProcessStartInfo
180+ {
181+ FileName = "/bin/ln" ,
182+ Arguments = $ "-s \" { canonical } \" \" { symlink } \" ",
183+ UseShellExecute = false ,
184+ RedirectStandardOutput = true ,
185+ RedirectStandardError = true ,
186+ CreateNoWindow = true
187+ } ;
188+ using var p = System . Diagnostics . Process . Start ( psi ) ;
189+ p ? . WaitForExit ( 2000 ) ;
190+ }
191+ catch { /* best-effort */ }
192+ }
193+
171194 private static bool IsDirectoryWritable ( string path )
172195 {
173196 try
@@ -302,11 +325,10 @@ private static void TryKillUvForPath(string serverSrcPath)
302325 if ( string . IsNullOrEmpty ( serverSrcPath ) ) return ;
303326 if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ) return ;
304327
305- string safePath = EscapeForPgrep ( serverSrcPath ) ;
306328 var psi = new System . Diagnostics . ProcessStartInfo
307329 {
308330 FileName = "/usr/bin/pgrep" ,
309- Arguments = $ "-f \" uv .*--directory { safePath } \" ",
331+ Arguments = $ "-f \" uv .*--directory { serverSrcPath } \" ",
310332 UseShellExecute = false ,
311333 RedirectStandardOutput = true ,
312334 RedirectStandardError = true ,
@@ -330,21 +352,6 @@ private static void TryKillUvForPath(string serverSrcPath)
330352 catch { }
331353 }
332354
333- // Escape regex metacharacters so the path is treated literally by pgrep -f
334- private static string EscapeForPgrep ( string path )
335- {
336- if ( string . IsNullOrEmpty ( path ) ) return path ;
337- string s = path . Replace ( "\\ " , "\\ \\ " ) ;
338- char [ ] meta = new [ ] { '.' , '+' , '*' , '?' , '^' , '$' , '(' , ')' , '[' , ']' , '{' , '}' , '|' } ;
339- var sb = new StringBuilder ( s . Length * 2 ) ;
340- foreach ( char c in s )
341- {
342- if ( Array . IndexOf ( meta , c ) >= 0 ) sb . Append ( '\\ ' ) ;
343- sb . Append ( c ) ;
344- }
345- return sb . ToString ( ) . Replace ( "\" " , "\\ \" " ) ;
346- }
347-
348355 private static string ReadVersionFile ( string path )
349356 {
350357 try
0 commit comments