unity-mcp/MCPForUnity/UnityMcpServer~/src/module_discovery.py

56 lines
2.0 KiB
Python
Raw Normal View History

Allow users to easily add tools in the Asset folder (#324) * Fix issue #308: Find py files in MCPForUnityTools and version.txt This allows for auto finding new tools. A good dir on a custom tool would look like this: CustomTool/ ├── CustomTool.MCPEnabler.asmdef ├── CustomTool.MCPEnabler.asmdef.meta ├── ExternalAssetToolFunction.cs ├── ExternalAssetToolFunction.cs.meta ├── external_asset_tool_function.py ├── external_asset_tool_function.py.meta ├── version.txt └── version.txt.meta CS files are left in the tools folder. The asmdef is recommended to allow for dependency on MCPForUnity when MCP For Unity is installed: asmdef example { "name": "CustomTool.MCPEnabler", "rootNamespace": "MCPForUnity.Editor.Tools", "references": [ "CustomTool", "MCPForUnity.Editor" ], "includePlatforms": [ "Editor" ], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], "autoReferenced": true, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false } * Follow-up: address CodeRabbit feedback for #308 (<GetToolsFolderIdentifier was duplicated>) * Follow-up: address CodeRabbit feedback for #308 – centralize GetToolsFolderIdentifier, fix tools copy dir, and limit scan scope * Fixing so the MCP don't removes _skipDirs e.g. __pycache__ * skip empty folders with no py files * Rabbit: "Fix identifier collision between different package roots." * Update MCPForUnity/Editor/Helpers/ServerInstaller.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Rabbbit: Cleanup may delete server’s built-in tool subfolders — restrict to managed names. * Fixed minor + missed onadding rabit change * Revert "Fixed minor + missed onadding rabit change" This reverts commit 571ca8c5de3d07da3791dad558677909a07e886d. * refactor: remove Unity project tools copying and version tracking functionality * refactor: consolidate module discovery logic into shared utility function * Remove unused imports * feat: add Python tool registry and sync system for MCP server integration * feat: add auto-sync processor for Python tools with Unity editor integration * feat: add menu item to reimport all Python files in project Good to give users a manual option * Fix infinite loop error Don't react to PythonToolAsset changes - it only needs to react to Python file changes. And we also batch asset edits to minimise the DB refreshes * refactor: move Python tool sync menu items under Window/MCP For Unity/Tool Sync * Update docs * Remove duplicate header * feat: add OnValidate handler to sync Python tools when asset is modified This fixes the issue with deletions in the asset, now file removals are synced * test: add unit tests for Python tools asset and sync services * Update MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * style: remove trailing whitespace from Python tool sync files * test: remove incomplete unit tests from ToolSyncServiceTests * perf: optimize Python file reimport by using AssetDatabase.FindAssets instead of GetAllAssetPaths --------- Co-authored-by: Johan Holtby <72528418+JohanHoltby@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-18 12:18:25 +08:00
"""
Shared module discovery utilities for auto-registering tools and resources.
"""
import importlib
import logging
from pathlib import Path
import pkgutil
from typing import Generator
logger = logging.getLogger("mcp-for-unity-server")
def discover_modules(base_dir: Path, package_name: str) -> Generator[str, None, None]:
"""
Discover and import all Python modules in a directory and its subdirectories.
Args:
base_dir: The base directory to search for modules
package_name: The package name to use for relative imports (e.g., 'tools' or 'resources')
Yields:
Full module names that were successfully imported
"""
# Discover modules in the top level
for _, module_name, _ in pkgutil.iter_modules([str(base_dir)]):
# Skip private modules and __init__
if module_name.startswith('_'):
continue
try:
full_module_name = f'.{module_name}'
importlib.import_module(full_module_name, package_name)
yield full_module_name
except Exception as e:
logger.warning(f"Failed to import module {module_name}: {e}")
# Discover modules in subdirectories (one level deep)
for subdir in base_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 package.subdirname.modulename
full_module_name = f'.{subdir.name}.{module_name}'
importlib.import_module(full_module_name, package_name)
yield full_module_name
except Exception as e:
logger.warning(
f"Failed to import module {subdir.name}.{module_name}: {e}")