Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MCPForUnity/Editor/Dependencies/Models.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion MCPForUnity/Editor/Dependencies/PlatformDetectors.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 75 additions & 1 deletion MCPForUnity/Editor/Helpers/PackageDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ static PackageDetector()
bool legacyPresent = LegacyRootsExist();
bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py"));

if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing)
// Check if any MCPForUnityTools have updated versions
bool toolsNeedUpdate = ToolsVersionsChanged();

if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing || toolsNeedUpdate)
{
// Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs.
EditorApplication.delayCall += () =>
Expand Down Expand Up @@ -103,5 +106,76 @@ private static bool LegacyRootsExist()
catch { }
return false;
}

/// <summary>
/// Checks if any MCPForUnityTools folders have version.txt files that differ from installed versions.
/// Returns true if any tool needs updating.
/// </summary>
private static bool ToolsVersionsChanged()
{
try
{
// Get Unity project root
string projectRoot = System.IO.Directory.GetParent(UnityEngine.Application.dataPath)?.FullName;
if (string.IsNullOrEmpty(projectRoot))
{
return false;
}

// Get server tools directory
string serverPath = ServerInstaller.GetServerPath();
string toolsDir = System.IO.Path.Combine(serverPath, "tools");

if (!System.IO.Directory.Exists(toolsDir))
{
// Tools directory doesn't exist yet, needs initial setup
return true;
}

// Find all MCPForUnityTools folders in project
var toolsFolders = System.IO.Directory.GetDirectories(projectRoot, "MCPForUnityTools", System.IO.SearchOption.AllDirectories);

foreach (var folder in toolsFolders)
{
// Check if version.txt exists in this folder
string versionFile = System.IO.Path.Combine(folder, "version.txt");
if (!System.IO.File.Exists(versionFile))
{
continue; // No version tracking for this folder
}

// Read source version
string sourceVersion = System.IO.File.ReadAllText(versionFile)?.Trim();
if (string.IsNullOrEmpty(sourceVersion))
{
continue;
}

// Get folder identifier (same logic as ServerInstaller.GetToolsFolderIdentifier)
string folderIdentifier = ServerInstaller.GetToolsFolderIdentifier(folder);
string trackingFile = System.IO.Path.Combine(toolsDir, $"{folderIdentifier}_version.txt");

// Read installed version
string installedVersion = null;
if (System.IO.File.Exists(trackingFile))
{
installedVersion = System.IO.File.ReadAllText(trackingFile)?.Trim();
}

// Check if versions differ
if (string.IsNullOrEmpty(installedVersion) || sourceVersion != installedVersion)
{
return true; // Version changed, needs update
}
}

return false; // All versions match
}
catch
{
// On error, assume update needed to be safe
return true;
}
}
}
}
235 changes: 235 additions & 0 deletions MCPForUnity/Editor/Helpers/ServerInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,16 @@ public static void EnsureServerInstalled()
// Copy the entire UnityMcpServer folder (parent of src)
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
CopyDirectoryRecursive(embeddedRoot, destRoot);

// Write/refresh version file
try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { }
McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer}).");
}

// Copy Unity project tools (runs independently of server version updates)
string destToolsDir = Path.Combine(destSrc, "tools");
CopyUnityProjectTools(destToolsDir);

// Cleanup legacy installs that are missing version or older than embedded
foreach (var legacyRoot in GetLegacyRootsForDetection())
{
Expand Down Expand Up @@ -397,6 +402,232 @@ private static bool TryGetEmbeddedServerSource(out string srcPath)
}

private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" };

/// <summary>
/// Searches Unity project for MCPForUnityTools folders and copies .py files to server tools directory.
/// Only copies if the tool's version.txt has changed (or doesn't exist).
/// Files are copied into per-folder subdirectories to avoid conflicts.
/// </summary>
private static void CopyUnityProjectTools(string destToolsDir)
{
try
{
// Get Unity project root
string projectRoot = Directory.GetParent(Application.dataPath)?.FullName;
if (string.IsNullOrEmpty(projectRoot))
{
return;
}

// Ensure destToolsDir exists
Directory.CreateDirectory(destToolsDir);

// Limit scan to specific directories to avoid deep recursion
var searchRoots = new List<string>();
var assetsPath = Path.Combine(projectRoot, "Assets");
var packagesPath = Path.Combine(projectRoot, "Packages");
var packageCachePath = Path.Combine(projectRoot, "Library", "PackageCache");

if (Directory.Exists(assetsPath)) searchRoots.Add(assetsPath);
if (Directory.Exists(packagesPath)) searchRoots.Add(packagesPath);
if (Directory.Exists(packageCachePath)) searchRoots.Add(packageCachePath);

// Find all MCPForUnityTools folders in limited search roots
var toolsFolders = new List<string>();
foreach (var searchRoot in searchRoots)
{
try
{
toolsFolders.AddRange(Directory.GetDirectories(searchRoot, "MCPForUnityTools", SearchOption.AllDirectories));
}
catch (Exception ex)
{
McpLog.Warn($"Failed to search {searchRoot}: {ex.Message}");
}
}

int copiedCount = 0;
int skippedCount = 0;

// Track all active folder identifiers (for cleanup)
var activeFolderIdentifiers = new HashSet<string>();

foreach (var folder in toolsFolders)
{
// Generate unique identifier for this tools folder based on its parent directory structure
// e.g., "MooseRunner_MCPForUnityTools" or "MyPackage_MCPForUnityTools"
string folderIdentifier = GetToolsFolderIdentifier(folder);
activeFolderIdentifiers.Add(folderIdentifier);

// Create per-folder subdirectory in destToolsDir
string destFolderSubdir = Path.Combine(destToolsDir, folderIdentifier);
Directory.CreateDirectory(destFolderSubdir);

string versionTrackingFile = Path.Combine(destFolderSubdir, "version.txt");

// Read source version
string sourceVersionFile = Path.Combine(folder, "version.txt");
string sourceVersion = ReadVersionFile(sourceVersionFile) ?? "0.0.0";

// Read installed version (tracked separately per tools folder)
string installedVersion = ReadVersionFile(versionTrackingFile);

// Check if update is needed (version different or no tracking file)
bool needsUpdate = string.IsNullOrEmpty(installedVersion) || sourceVersion != installedVersion;

if (needsUpdate)
{
// Get all .py files (excluding __init__.py)
var pyFiles = Directory.GetFiles(folder, "*.py")
.Where(f => !Path.GetFileName(f).Equals("__init__.py", StringComparison.OrdinalIgnoreCase));

// Skip folders with no .py files
if (!pyFiles.Any())
{
skippedCount++;
continue;
}

bool copyFailed = false;
foreach (var pyFile in pyFiles)
{
string fileName = Path.GetFileName(pyFile);
string destFile = Path.Combine(destFolderSubdir, fileName);

try
{
File.Copy(pyFile, destFile, overwrite: true);
copiedCount++;
McpLog.Info($"Copied Unity project tool: {fileName} from {folderIdentifier} (v{sourceVersion})");
}
catch (Exception ex)
{
McpLog.Warn($"Failed to copy {fileName}: {ex.Message}");
copyFailed = true;
}
}
Comment on lines +478 to +508
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Copy one-level subdirectories too (aligns with server’s module discovery).

Only copying top-level .py files breaks directory-style tools (packages with init.py). Copy subdirs (excluding _skipDirs) one level deep.

                         bool copyFailed = false;
                         foreach (var pyFile in pyFiles)
                         {
                             string fileName = Path.GetFileName(pyFile);
                             string destFile = Path.Combine(destFolderSubdir, fileName);
 
                             try
                             {
                                 File.Copy(pyFile, destFile, overwrite: true);
                                 copiedCount++;
                                 McpLog.Info($"Copied Unity project tool: {fileName} from {folderIdentifier} (v{sourceVersion})");
                             }
                             catch (Exception ex)
                             {
                                 McpLog.Warn($"Failed to copy {fileName}: {ex.Message}");
                                 copyFailed = true;
                             }
                         }
+
+                        // Also copy one-level subdirectories (package-style tools), skipping caches/venvs
+                        try
+                        {
+                            foreach (var sub in Directory.GetDirectories(folder, "*", SearchOption.TopDirectoryOnly))
+                            {
+                                string name = Path.GetFileName(sub);
+                                if (_skipDirs.Any(s => name.Equals(s, StringComparison.OrdinalIgnoreCase)))
+                                    continue;
+                                string destSub = Path.Combine(destFolderSubdir, name);
+                                try
+                                {
+                                    CopyDirectoryRecursive(sub, destSub);
+                                }
+                                catch (Exception ex)
+                                {
+                                    McpLog.Warn($"Failed to copy subdirectory {name}: {ex.Message}");
+                                    copyFailed = true;
+                                }
+                            }
+                        }
+                        catch (Exception ex)
+                        {
+                            McpLog.Warn($"Failed enumerating subdirectories for {folderIdentifier}: {ex.Message}");
+                            copyFailed = true;
+                        }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In MCPForUnity/Editor/Helpers/ServerInstaller.cs around lines 478 to 508, the
current loop only copies top-level .py files which breaks package-style tools;
update the logic to also enumerate and copy one-level subdirectories under the
source folder (use Directory.GetDirectories(folder) and ignore any directory
names in the _skipDirs set), and for each allowed subdirectory create the
corresponding destination subdirectory (Path.Combine(destFolderSubdir,
subdirName)) then copy all files inside that subdirectory (preserving file names
and overwriting) while handling exceptions per file the same way as top-level
files (set copyFailed if any copy fails, increment copiedCount on success, log
successes and warnings). Ensure you still skip the whole source folder if there
are no top-level .py files and no allowed subdirectories to copy, and keep
existing skippedCount behavior.


// Update version tracking file only on full success
if (!copyFailed)
{
try
{
File.WriteAllText(versionTrackingFile, sourceVersion);
}
catch (Exception ex)
{
McpLog.Warn($"Failed to write version tracking file for {folderIdentifier}: {ex.Message}");
}
}
}
else
{
skippedCount++;
}
}

// Clean up stale subdirectories (folders removed from upstream)
CleanupStaleToolFolders(destToolsDir, activeFolderIdentifiers);

if (copiedCount > 0)
{
McpLog.Info($"Copied {copiedCount} Unity project tool(s) to server");
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to scan Unity project for tools: {ex.Message}");
}
}

/// <summary>
/// Removes stale tool subdirectories that are no longer present in the Unity project.
/// </summary>
private static void CleanupStaleToolFolders(string destToolsDir, HashSet<string> activeFolderIdentifiers)
{
try
{
if (!Directory.Exists(destToolsDir)) return;

// Get all subdirectories in destToolsDir
var existingSubdirs = Directory.GetDirectories(destToolsDir);

foreach (var subdir in existingSubdirs)
{
string subdirName = Path.GetFileName(subdir);

// Skip Python cache and virtual environment directories
foreach (var skip in _skipDirs)
{
if (subdirName.Equals(skip, StringComparison.OrdinalIgnoreCase))
goto NextSubdir;
}

// Only manage per-folder tool installs created by this feature
if (!subdirName.EndsWith("_MCPForUnityTools", StringComparison.OrdinalIgnoreCase))
goto NextSubdir;

// Check if this subdirectory corresponds to an active tools folder
if (!activeFolderIdentifiers.Contains(subdirName))
{
try
{
Directory.Delete(subdir, recursive: true);
McpLog.Info($"Cleaned up stale tools folder: {subdirName}");
}
catch (Exception ex)
{
McpLog.Warn($"Failed to delete stale folder {subdirName}: {ex.Message}");
}
}
NextSubdir:;
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to cleanup stale tool folders: {ex.Message}");
}
}

/// <summary>
/// Generates a unique identifier for a MCPForUnityTools folder based on its parent directory.
/// Example: "Assets/MooseRunner/Editor/MCPForUnityTools" → "MooseRunner_MCPForUnityTools"
/// </summary>
internal static string GetToolsFolderIdentifier(string toolsFolderPath)
{
try
{
// Get parent directory name (e.g., "Editor" or package name)
DirectoryInfo parent = Directory.GetParent(toolsFolderPath);
if (parent == null) return "MCPForUnityTools";

// Walk up to find a distinctive parent (Assets/PackageName or Packages/PackageName)
DirectoryInfo current = parent;
while (current != null)
{
string name = current.Name;
DirectoryInfo grandparent = current.Parent;

// Stop at Assets, Packages, or if we find a package-like structure
if (grandparent != null &&
(grandparent.Name.Equals("Assets", StringComparison.OrdinalIgnoreCase) ||
grandparent.Name.Equals("Packages", StringComparison.OrdinalIgnoreCase)))
{
return $"{grandparent.Name}_{name}_MCPForUnityTools";
}

current = grandparent;
}

// Fallback: use immediate parent
return $"{parent.Name}_MCPForUnityTools";
}
catch
{
return "MCPForUnityTools";
}
}

private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
{
Directory.CreateDirectory(destinationDir);
Expand Down Expand Up @@ -461,6 +692,10 @@ public static bool RebuildMcpServer()
Directory.CreateDirectory(destRoot);
CopyDirectoryRecursive(embeddedRoot, destRoot);

// Copy Unity project tools
string destToolsDir = Path.Combine(destSrc, "tools");
CopyUnityProjectTools(destToolsDir);

// Write version file
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
try
Expand Down
Loading