11using System ;
22using System . IO ;
3- using System . Linq ;
4- using System . Net ;
53using System . Runtime . InteropServices ;
4+ using UnityEditor ;
65using UnityEngine ;
76
87namespace UnityMcpBridge . Editor . Helpers
@@ -11,37 +10,34 @@ public static class ServerInstaller
1110 {
1211 private const string RootFolder = "UnityMCP" ;
1312 private const string ServerFolder = "UnityMcpServer" ;
14- private const string BranchName = "master" ;
15- private const string GitUrl = "https://github.com/justinpbarnett/unity-mcp.git" ;
16- private const string PyprojectUrl =
17- "https://raw.githubusercontent.com/justinpbarnett/unity-mcp/refs/heads/"
18- + BranchName
19- + "/UnityMcpServer/src/pyproject.toml" ;
20-
2113 /// <summary>
22- /// Ensures the unity-mcp-server is installed and up to date.
14+ /// Ensures the unity-mcp-server is installed locally by copying from the embedded package source.
15+ /// No network calls or Git operations are performed.
2316 /// </summary>
2417 public static void EnsureServerInstalled ( )
2518 {
2619 try
2720 {
2821 string saveLocation = GetSaveLocation ( ) ;
22+ string destRoot = Path . Combine ( saveLocation , ServerFolder ) ;
23+ string destSrc = Path . Combine ( destRoot , "src" ) ;
2924
30- if ( ! IsServerInstalled ( saveLocation ) )
25+ if ( File . Exists ( Path . Combine ( destSrc , "server.py" ) ) )
3126 {
32- InstallServer ( saveLocation ) ;
27+ return ; // Already installed
3328 }
34- else
35- {
36- string installedVersion = GetInstalledVersion ( ) ;
37- string latestVersion = GetLatestVersion ( ) ;
3829
39- if ( IsNewerVersion ( latestVersion , installedVersion ) )
40- {
41- UpdateServer ( saveLocation ) ;
42- }
43- else { }
30+ if ( ! TryGetEmbeddedServerSource ( out string embeddedSrc ) )
31+ {
32+ throw new Exception ( "Could not find embedded UnityMcpServer/src in the package." ) ;
4433 }
34+
35+ // Ensure destination exists
36+ Directory . CreateDirectory ( destRoot ) ;
37+
38+ // Copy the entire UnityMcpServer folder (parent of src)
39+ string embeddedRoot = Path . GetDirectoryName ( embeddedSrc ) ?? embeddedSrc ; // go up from src to UnityMcpServer
40+ CopyDirectoryRecursive ( embeddedRoot , destRoot ) ;
4541 }
4642 catch ( Exception ex )
4743 {
@@ -111,139 +107,110 @@ private static bool IsDirectoryWritable(string path)
111107 private static bool IsServerInstalled ( string location )
112108 {
113109 return Directory . Exists ( location )
114- && File . Exists ( Path . Combine ( location , ServerFolder , "src" , "pyproject.toml" ) ) ;
115- }
116-
117- /// <summary>
118- /// Installs the server by cloning only the UnityMcpServer folder from the repository and setting up dependencies.
119- /// </summary>
120- private static void InstallServer ( string location )
121- {
122- // Create the src directory where the server code will reside
123- Directory . CreateDirectory ( location ) ;
124-
125- // Initialize git repo in the src directory
126- RunCommand ( "git" , $ "init", workingDirectory : location ) ;
127-
128- // Add remote
129- RunCommand ( "git" , $ "remote add origin { GitUrl } ", workingDirectory : location ) ;
130-
131- // Configure sparse checkout
132- RunCommand ( "git" , "config core.sparseCheckout true" , workingDirectory : location ) ;
133-
134- // Set sparse checkout path to only include UnityMcpServer folder
135- string sparseCheckoutPath = Path . Combine ( location , ".git" , "info" , "sparse-checkout" ) ;
136- File . WriteAllText ( sparseCheckoutPath , $ "{ ServerFolder } /") ;
137-
138- // Fetch and checkout the branch
139- RunCommand ( "git" , $ "fetch --depth=1 origin { BranchName } ", workingDirectory : location ) ;
140- RunCommand ( "git" , $ "checkout { BranchName } ", workingDirectory : location ) ;
141- }
142-
143- /// <summary>
144- /// Fetches the currently installed version from the local pyproject.toml file.
145- /// </summary>
146- public static string GetInstalledVersion ( )
147- {
148- string pyprojectPath = Path . Combine (
149- GetSaveLocation ( ) ,
150- ServerFolder ,
151- "src" ,
152- "pyproject.toml"
153- ) ;
154- return ParseVersionFromPyproject ( File . ReadAllText ( pyprojectPath ) ) ;
110+ && File . Exists ( Path . Combine ( location , ServerFolder , "src" , "server.py" ) ) ;
155111 }
156112
157113 /// <summary>
158- /// Fetches the latest version from the GitHub pyproject.toml file.
114+ /// Attempts to locate the embedded UnityMcpServer/src directory inside the installed package
115+ /// or common development locations.
159116 /// </summary>
160- public static string GetLatestVersion ( )
117+ private static bool TryGetEmbeddedServerSource ( out string srcPath )
161118 {
162- using WebClient webClient = new ( ) ;
163- string pyprojectContent = webClient . DownloadString ( PyprojectUrl ) ;
164- return ParseVersionFromPyproject ( pyprojectContent ) ;
165- }
166-
167- /// <summary>
168- /// Updates the server by pulling the latest changes for the UnityMcpServer folder only.
169- /// </summary>
170- private static void UpdateServer ( string location )
171- {
172- RunCommand ( "git" , $ "pull origin { BranchName } ", workingDirectory : location ) ;
173- }
174-
175- /// <summary>
176- /// Parses the version number from pyproject.toml content.
177- /// </summary>
178- private static string ParseVersionFromPyproject ( string content )
179- {
180- foreach ( string line in content . Split ( '\n ' ) )
119+ // 1) Development mode: common repo layouts
120+ try
181121 {
182- if ( line . Trim ( ) . StartsWith ( "version =" ) )
122+ string projectRoot = Path . GetDirectoryName ( Application . dataPath ) ;
123+ string [ ] devCandidates =
124+ {
125+ Path . Combine ( projectRoot ?? string . Empty , "unity-mcp" , "UnityMcpServer" , "src" ) ,
126+ Path . Combine ( projectRoot ?? string . Empty , ".." , "unity-mcp" , "UnityMcpServer" , "src" ) ,
127+ } ;
128+ foreach ( string candidate in devCandidates )
183129 {
184- string [ ] parts = line . Split ( '=' ) ;
185- if ( parts . Length == 2 )
130+ string full = Path . GetFullPath ( candidate ) ;
131+ if ( Directory . Exists ( full ) && File . Exists ( Path . Combine ( full , "server.py" ) ) )
186132 {
187- return parts [ 1 ] . Trim ( ) . Trim ( '"' ) ;
133+ srcPath = full ;
134+ return true ;
188135 }
189136 }
190137 }
191- throw new Exception ( "Version not found in pyproject.toml" ) ;
192- }
138+ catch { /* ignore */ }
193139
194- /// <summary>
195- /// Compares two version strings to determine if the latest is newer.
196- /// </summary>
197- public static bool IsNewerVersion ( string latest , string installed )
198- {
199- int [ ] latestParts = latest . Split ( '.' ) . Select ( int . Parse ) . ToArray ( ) ;
200- int [ ] installedParts = installed . Split ( '.' ) . Select ( int . Parse ) . ToArray ( ) ;
201- for ( int i = 0 ; i < Math . Min ( latestParts . Length , installedParts . Length ) ; i ++ )
140+ // 2) Installed package: resolve via Package Manager
141+ try
202142 {
203- if ( latestParts [ i ] > installedParts [ i ] )
143+ var list = UnityEditor . PackageManager . Client . List ( ) ;
144+ while ( ! list . IsCompleted ) { }
145+ if ( list . Status == UnityEditor . PackageManager . StatusCode . Success )
204146 {
205- return true ;
147+ foreach ( var pkg in list . Result )
148+ {
149+ if ( pkg . name == "com.justinpbarnett.unity-mcp" )
150+ {
151+ string packagePath = pkg . resolvedPath ; // e.g., Library/PackageCache/... or local path
152+
153+ // Preferred: UnityMcpServer embedded alongside Editor/Runtime within the package
154+ string embedded = Path . Combine ( packagePath , "UnityMcpServer" , "src" ) ;
155+ if ( Directory . Exists ( embedded ) && File . Exists ( Path . Combine ( embedded , "server.py" ) ) )
156+ {
157+ srcPath = embedded ;
158+ return true ;
159+ }
160+
161+ // Legacy: sibling of the package folder (dev-linked). Only valid when present on disk.
162+ string sibling = Path . Combine ( Path . GetDirectoryName ( packagePath ) ?? string . Empty , "UnityMcpServer" , "src" ) ;
163+ if ( Directory . Exists ( sibling ) && File . Exists ( Path . Combine ( sibling , "server.py" ) ) )
164+ {
165+ srcPath = sibling ;
166+ return true ;
167+ }
168+ }
169+ }
206170 }
171+ }
172+ catch { /* ignore */ }
207173
208- if ( latestParts [ i ] < installedParts [ i ] )
174+ // 3) Fallback to previous common install locations
175+ try
176+ {
177+ string home = Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) ;
178+ string [ ] candidates =
209179 {
210- return false ;
180+ Path . Combine ( home , "unity-mcp" , "UnityMcpServer" , "src" ) ,
181+ Path . Combine ( home , "Applications" , "UnityMCP" , "UnityMcpServer" , "src" ) ,
182+ } ;
183+ foreach ( string candidate in candidates )
184+ {
185+ if ( Directory . Exists ( candidate ) && File . Exists ( Path . Combine ( candidate , "server.py" ) ) )
186+ {
187+ srcPath = candidate ;
188+ return true ;
189+ }
211190 }
212191 }
213- return latestParts . Length > installedParts . Length ;
192+ catch { /* ignore */ }
193+
194+ srcPath = null ;
195+ return false ;
214196 }
215197
216- /// <summary>
217- /// Runs a command-line process and handles output/errors.
218- /// </summary>
219- private static void RunCommand (
220- string command ,
221- string arguments ,
222- string workingDirectory = null
223- )
198+ private static void CopyDirectoryRecursive ( string sourceDir , string destinationDir )
224199 {
225- System . Diagnostics . Process process = new ( )
200+ Directory . CreateDirectory ( destinationDir ) ;
201+
202+ foreach ( string filePath in Directory . GetFiles ( sourceDir ) )
226203 {
227- StartInfo = new System . Diagnostics . ProcessStartInfo
228- {
229- FileName = command ,
230- Arguments = arguments ,
231- RedirectStandardOutput = true ,
232- RedirectStandardError = true ,
233- UseShellExecute = false ,
234- CreateNoWindow = true ,
235- WorkingDirectory = workingDirectory ?? string . Empty ,
236- } ,
237- } ;
238- process . Start ( ) ;
239- string output = process . StandardOutput . ReadToEnd ( ) ;
240- string error = process . StandardError . ReadToEnd ( ) ;
241- process . WaitForExit ( ) ;
242- if ( process . ExitCode != 0 )
204+ string fileName = Path . GetFileName ( filePath ) ;
205+ string destFile = Path . Combine ( destinationDir , fileName ) ;
206+ File . Copy ( filePath , destFile , overwrite : true ) ;
207+ }
208+
209+ foreach ( string dirPath in Directory . GetDirectories ( sourceDir ) )
243210 {
244- throw new Exception (
245- $ "Command failed: { command } { arguments } \n Output: { output } \n Error: { error } "
246- ) ;
211+ string dirName = Path . GetFileName ( dirPath ) ;
212+ string destSubDir = Path . Combine ( destinationDir , dirName ) ;
213+ CopyDirectoryRecursive ( dirPath , destSubDir ) ;
247214 }
248215 }
249216 }
0 commit comments