unity-mcp/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs

193 lines
6.2 KiB
C#
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
using System.IO;
using System.Linq;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Services;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Automatically syncs Python tools to the MCP server when:
/// - PythonToolsAsset is modified
/// - Python files are imported/reimported
/// - Unity starts up
/// </summary>
[InitializeOnLoad]
public class PythonToolSyncProcessor : AssetPostprocessor
{
private const string SyncEnabledKey = "MCPForUnity.AutoSyncEnabled";
private static bool _isSyncing = false;
static PythonToolSyncProcessor()
{
// Sync on Unity startup
EditorApplication.delayCall += () =>
{
if (IsAutoSyncEnabled())
{
SyncAllTools();
}
};
}
/// <summary>
/// Called after any assets are imported, deleted, or moved
/// </summary>
private static void OnPostprocessAllAssets(
string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths)
{
// Prevent infinite loop - don't process if we're currently syncing
if (_isSyncing || !IsAutoSyncEnabled())
return;
bool needsSync = false;
// Only check for .py file changes, not PythonToolsAsset changes
// (PythonToolsAsset changes are internal state updates from syncing)
foreach (string path in importedAssets.Concat(movedAssets))
{
// Check if any .py files were modified
if (path.EndsWith(".py"))
{
needsSync = true;
break;
}
}
// Check if any .py files were deleted
if (!needsSync && deletedAssets.Any(path => path.EndsWith(".py")))
{
needsSync = true;
}
if (needsSync)
{
SyncAllTools();
}
}
/// <summary>
/// Syncs all Python tools from all PythonToolsAsset instances to the MCP server
/// </summary>
public static void SyncAllTools()
{
// Prevent re-entrant calls
if (_isSyncing)
{
McpLog.Warn("Sync already in progress, skipping...");
return;
}
_isSyncing = true;
try
{
if (!ServerPathResolver.TryFindEmbeddedServerSource(out string srcPath))
{
McpLog.Warn("Cannot sync Python tools: MCP server source not found");
return;
}
string toolsDir = Path.Combine(srcPath, "tools", "custom");
var result = MCPServiceLocator.ToolSync.SyncProjectTools(toolsDir);
if (result.Success)
{
if (result.CopiedCount > 0 || result.SkippedCount > 0)
{
McpLog.Info($"Python tools synced: {result.CopiedCount} copied, {result.SkippedCount} skipped");
}
}
else
{
McpLog.Error($"Python tool sync failed with {result.ErrorCount} errors");
foreach (var msg in result.Messages)
{
McpLog.Error($" - {msg}");
}
}
}
catch (System.Exception ex)
{
McpLog.Error($"Python tool sync exception: {ex.Message}");
}
finally
{
_isSyncing = false;
}
}
/// <summary>
/// Checks if auto-sync is enabled (default: true)
/// </summary>
public static bool IsAutoSyncEnabled()
{
return EditorPrefs.GetBool(SyncEnabledKey, true);
}
/// <summary>
/// Enables or disables auto-sync
/// </summary>
public static void SetAutoSyncEnabled(bool enabled)
{
EditorPrefs.SetBool(SyncEnabledKey, enabled);
McpLog.Info($"Python tool auto-sync {(enabled ? "enabled" : "disabled")}");
}
/// <summary>
/// Menu item to reimport all Python files in the project
/// </summary>
[MenuItem("Window/MCP For Unity/Tool Sync/Reimport Python Files", priority = 99)]
public static void ReimportPythonFiles()
{
// Find all Python files (imported as TextAssets by PythonFileImporter)
var pythonGuids = AssetDatabase.FindAssets("t:TextAsset", new[] { "Assets" })
.Select(AssetDatabase.GUIDToAssetPath)
.Where(path => path.EndsWith(".py", System.StringComparison.OrdinalIgnoreCase))
.ToArray();
foreach (string path in pythonGuids)
{
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
}
int count = pythonGuids.Length;
McpLog.Info($"Reimported {count} Python files");
AssetDatabase.Refresh();
}
/// <summary>
/// Menu item to manually trigger sync
/// </summary>
[MenuItem("Window/MCP For Unity/Tool Sync/Sync Python Tools", priority = 100)]
public static void ManualSync()
{
McpLog.Info("Manually syncing Python tools...");
SyncAllTools();
}
/// <summary>
/// Menu item to toggle auto-sync
/// </summary>
[MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", priority = 101)]
public static void ToggleAutoSync()
{
SetAutoSyncEnabled(!IsAutoSyncEnabled());
}
/// <summary>
/// Validate menu item (shows checkmark when enabled)
/// </summary>
[MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", true, priority = 101)]
public static bool ToggleAutoSyncValidate()
{
Menu.SetChecked("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", IsAutoSyncEnabled());
return true;
}
}
}