diff --git a/MCPForUnity/Editor/Dependencies/Models.meta b/MCPForUnity/Editor/Dependencies/Models.meta
index 2174dd52..3ba640c9 100644
--- a/MCPForUnity/Editor/Dependencies/Models.meta
+++ b/MCPForUnity/Editor/Dependencies/Models.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: b2c3d4e5f6789012345678901234abcd
+guid: 2b4fca8c8f964494e82a2c1d1d8d2041
folderAsset: yes
DefaultImporter:
externalObjects: {}
diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta b/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta
index 22a6b1db..be8b8dce 100644
--- a/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta
+++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: c3d4e5f6789012345678901234abcdef
+guid: c6d16631d05433740a7193d3384364a8
folderAsset: yes
DefaultImporter:
externalObjects: {}
diff --git a/MCPForUnity/Editor/Helpers/PackageDetector.cs b/MCPForUnity/Editor/Helpers/PackageDetector.cs
index bb8861fe..cb044093 100644
--- a/MCPForUnity/Editor/Helpers/PackageDetector.cs
+++ b/MCPForUnity/Editor/Helpers/PackageDetector.cs
@@ -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 += () =>
@@ -103,5 +106,76 @@ private static bool LegacyRootsExist()
catch { }
return false;
}
+
+ ///
+ /// Checks if any MCPForUnityTools folders have version.txt files that differ from installed versions.
+ /// Returns true if any tool needs updating.
+ ///
+ 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;
+ }
+ }
}
}
diff --git a/MCPForUnity/Editor/Helpers/ServerInstaller.cs b/MCPForUnity/Editor/Helpers/ServerInstaller.cs
index f41e03c3..0368b596 100644
--- a/MCPForUnity/Editor/Helpers/ServerInstaller.cs
+++ b/MCPForUnity/Editor/Helpers/ServerInstaller.cs
@@ -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())
{
@@ -397,6 +402,232 @@ private static bool TryGetEmbeddedServerSource(out string srcPath)
}
private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" };
+
+ ///
+ /// 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.
+ ///
+ 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();
+ 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();
+ 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();
+
+ 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;
+ }
+ }
+
+ // 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}");
+ }
+ }
+
+ ///
+ /// Removes stale tool subdirectories that are no longer present in the Unity project.
+ ///
+ private static void CleanupStaleToolFolders(string destToolsDir, HashSet 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}");
+ }
+ }
+
+ ///
+ /// Generates a unique identifier for a MCPForUnityTools folder based on its parent directory.
+ /// Example: "Assets/MooseRunner/Editor/MCPForUnityTools" → "MooseRunner_MCPForUnityTools"
+ ///
+ 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);
@@ -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
diff --git a/MCPForUnity/UnityMcpServer~/src/tools/__init__.py b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py
index 6ede53d3..a6fcb17c 100644
--- a/MCPForUnity/UnityMcpServer~/src/tools/__init__.py
+++ b/MCPForUnity/UnityMcpServer~/src/tools/__init__.py
@@ -21,13 +21,14 @@ def register_all_tools(mcp: FastMCP):
"""
Auto-discover and register all tools in the tools/ directory.
- Any .py file in this directory with @mcp_for_unity_tool decorated
+ Any .py file in this directory or subdirectories with @mcp_for_unity_tool decorated
functions will be automatically registered.
"""
logger.info("Auto-discovering MCP for Unity Server tools...")
# Dynamic import of all modules in this directory
tools_dir = Path(__file__).parent
+ # Discover modules in the top level
for _, module_name, _ in pkgutil.iter_modules([str(tools_dir)]):
# Skip private modules and __init__
if module_name.startswith('_'):
@@ -38,6 +39,24 @@ def register_all_tools(mcp: FastMCP):
except Exception as e:
logger.warning(f"Failed to import tool module {module_name}: {e}")
+ # Discover modules in subdirectories (one level deep)
+ for subdir in tools_dir.iterdir():
+ if not subdir.is_dir() or subdir.name.startswith('_') or subdir.name.startswith('.'):
+ continue
+
+ # Check if subdirectory contains Python modules
+ for _, module_name, _ in pkgutil.iter_modules([str(subdir)]):
+ # Skip private modules and __init__
+ if module_name.startswith('_'):
+ continue
+
+ try:
+ # Import as tools.subdirname.modulename
+ full_module_name = f'.{subdir.name}.{module_name}'
+ importlib.import_module(full_module_name, __package__)
+ except Exception as e:
+ logger.warning(f"Failed to import tool module {subdir.name}.{module_name}: {e}")
+
tools = get_registered_tools()
if not tools: