Skip to content

Commit 85cc93f

Browse files
msanatanJohanHoltbycoderabbitai[bot]
authored
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 571ca8c. * 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>
1 parent 4a0e633 commit 85cc93f

32 files changed

+1332
-96
lines changed

MCPForUnity/Editor/Data/McpClients.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4-
using System.Runtime.InteropServices;
54
using MCPForUnity.Editor.Models;
65

76
namespace MCPForUnity.Editor.Data
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using UnityEngine;
5+
6+
namespace MCPForUnity.Editor.Data
7+
{
8+
/// <summary>
9+
/// Registry of Python tool files to sync to the MCP server.
10+
/// Add your Python files here - they can be stored anywhere in your project.
11+
/// </summary>
12+
[CreateAssetMenu(fileName = "PythonTools", menuName = "MCP For Unity/Python Tools")]
13+
public class PythonToolsAsset : ScriptableObject
14+
{
15+
[Tooltip("Add Python files (.py) to sync to the MCP server. Files can be located anywhere in your project.")]
16+
public List<TextAsset> pythonFiles = new List<TextAsset>();
17+
18+
[Header("Sync Options")]
19+
[Tooltip("Use content hashing to detect changes (recommended). If false, always copies on startup.")]
20+
public bool useContentHashing = true;
21+
22+
[Header("Sync State (Read-only)")]
23+
[Tooltip("Internal tracking - do not modify")]
24+
public List<PythonFileState> fileStates = new List<PythonFileState>();
25+
26+
/// <summary>
27+
/// Gets all valid Python files (filters out null/missing references)
28+
/// </summary>
29+
public IEnumerable<TextAsset> GetValidFiles()
30+
{
31+
return pythonFiles.Where(f => f != null);
32+
}
33+
34+
/// <summary>
35+
/// Checks if a file needs syncing
36+
/// </summary>
37+
public bool NeedsSync(TextAsset file, string currentHash)
38+
{
39+
if (!useContentHashing) return true; // Always sync if hashing disabled
40+
41+
var state = fileStates.FirstOrDefault(s => s.assetGuid == GetAssetGuid(file));
42+
return state == null || state.contentHash != currentHash;
43+
}
44+
45+
/// <summary>
46+
/// Records that a file was synced
47+
/// </summary>
48+
public void RecordSync(TextAsset file, string hash)
49+
{
50+
string guid = GetAssetGuid(file);
51+
var state = fileStates.FirstOrDefault(s => s.assetGuid == guid);
52+
53+
if (state == null)
54+
{
55+
state = new PythonFileState { assetGuid = guid };
56+
fileStates.Add(state);
57+
}
58+
59+
state.contentHash = hash;
60+
state.lastSyncTime = DateTime.UtcNow;
61+
state.fileName = file.name;
62+
}
63+
64+
/// <summary>
65+
/// Removes state entries for files no longer in the list
66+
/// </summary>
67+
public void CleanupStaleStates()
68+
{
69+
var validGuids = new HashSet<string>(GetValidFiles().Select(GetAssetGuid));
70+
fileStates.RemoveAll(s => !validGuids.Contains(s.assetGuid));
71+
}
72+
73+
private string GetAssetGuid(TextAsset asset)
74+
{
75+
return UnityEditor.AssetDatabase.AssetPathToGUID(UnityEditor.AssetDatabase.GetAssetPath(asset));
76+
}
77+
78+
/// <summary>
79+
/// Called when the asset is modified in the Inspector
80+
/// Triggers sync to handle file additions/removals
81+
/// </summary>
82+
private void OnValidate()
83+
{
84+
// Cleanup stale states immediately
85+
CleanupStaleStates();
86+
87+
// Trigger sync after a delay to handle file removals
88+
// Delay ensures the asset is saved before sync runs
89+
UnityEditor.EditorApplication.delayCall += () =>
90+
{
91+
if (this != null) // Check if asset still exists
92+
{
93+
MCPForUnity.Editor.Helpers.PythonToolSyncProcessor.SyncAllTools();
94+
}
95+
};
96+
}
97+
}
98+
99+
[Serializable]
100+
public class PythonFileState
101+
{
102+
public string assetGuid;
103+
public string fileName;
104+
public string contentHash;
105+
public DateTime lastSyncTime;
106+
}
107+
}

MCPForUnity/Editor/Data/PythonToolsAsset.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
using System.IO;
2+
using System.Linq;
3+
using MCPForUnity.Editor.Data;
4+
using MCPForUnity.Editor.Services;
5+
using UnityEditor;
6+
using UnityEngine;
7+
8+
namespace MCPForUnity.Editor.Helpers
9+
{
10+
/// <summary>
11+
/// Automatically syncs Python tools to the MCP server when:
12+
/// - PythonToolsAsset is modified
13+
/// - Python files are imported/reimported
14+
/// - Unity starts up
15+
/// </summary>
16+
[InitializeOnLoad]
17+
public class PythonToolSyncProcessor : AssetPostprocessor
18+
{
19+
private const string SyncEnabledKey = "MCPForUnity.AutoSyncEnabled";
20+
private static bool _isSyncing = false;
21+
22+
static PythonToolSyncProcessor()
23+
{
24+
// Sync on Unity startup
25+
EditorApplication.delayCall += () =>
26+
{
27+
if (IsAutoSyncEnabled())
28+
{
29+
SyncAllTools();
30+
}
31+
};
32+
}
33+
34+
/// <summary>
35+
/// Called after any assets are imported, deleted, or moved
36+
/// </summary>
37+
private static void OnPostprocessAllAssets(
38+
string[] importedAssets,
39+
string[] deletedAssets,
40+
string[] movedAssets,
41+
string[] movedFromAssetPaths)
42+
{
43+
// Prevent infinite loop - don't process if we're currently syncing
44+
if (_isSyncing || !IsAutoSyncEnabled())
45+
return;
46+
47+
bool needsSync = false;
48+
49+
// Only check for .py file changes, not PythonToolsAsset changes
50+
// (PythonToolsAsset changes are internal state updates from syncing)
51+
foreach (string path in importedAssets.Concat(movedAssets))
52+
{
53+
// Check if any .py files were modified
54+
if (path.EndsWith(".py"))
55+
{
56+
needsSync = true;
57+
break;
58+
}
59+
}
60+
61+
// Check if any .py files were deleted
62+
if (!needsSync && deletedAssets.Any(path => path.EndsWith(".py")))
63+
{
64+
needsSync = true;
65+
}
66+
67+
if (needsSync)
68+
{
69+
SyncAllTools();
70+
}
71+
}
72+
73+
/// <summary>
74+
/// Syncs all Python tools from all PythonToolsAsset instances to the MCP server
75+
/// </summary>
76+
public static void SyncAllTools()
77+
{
78+
// Prevent re-entrant calls
79+
if (_isSyncing)
80+
{
81+
McpLog.Warn("Sync already in progress, skipping...");
82+
return;
83+
}
84+
85+
_isSyncing = true;
86+
try
87+
{
88+
if (!ServerPathResolver.TryFindEmbeddedServerSource(out string srcPath))
89+
{
90+
McpLog.Warn("Cannot sync Python tools: MCP server source not found");
91+
return;
92+
}
93+
94+
string toolsDir = Path.Combine(srcPath, "tools", "custom");
95+
96+
var result = MCPServiceLocator.ToolSync.SyncProjectTools(toolsDir);
97+
98+
if (result.Success)
99+
{
100+
if (result.CopiedCount > 0 || result.SkippedCount > 0)
101+
{
102+
McpLog.Info($"Python tools synced: {result.CopiedCount} copied, {result.SkippedCount} skipped");
103+
}
104+
}
105+
else
106+
{
107+
McpLog.Error($"Python tool sync failed with {result.ErrorCount} errors");
108+
foreach (var msg in result.Messages)
109+
{
110+
McpLog.Error($" - {msg}");
111+
}
112+
}
113+
}
114+
catch (System.Exception ex)
115+
{
116+
McpLog.Error($"Python tool sync exception: {ex.Message}");
117+
}
118+
finally
119+
{
120+
_isSyncing = false;
121+
}
122+
}
123+
124+
/// <summary>
125+
/// Checks if auto-sync is enabled (default: true)
126+
/// </summary>
127+
public static bool IsAutoSyncEnabled()
128+
{
129+
return EditorPrefs.GetBool(SyncEnabledKey, true);
130+
}
131+
132+
/// <summary>
133+
/// Enables or disables auto-sync
134+
/// </summary>
135+
public static void SetAutoSyncEnabled(bool enabled)
136+
{
137+
EditorPrefs.SetBool(SyncEnabledKey, enabled);
138+
McpLog.Info($"Python tool auto-sync {(enabled ? "enabled" : "disabled")}");
139+
}
140+
141+
/// <summary>
142+
/// Menu item to reimport all Python files in the project
143+
/// </summary>
144+
[MenuItem("Window/MCP For Unity/Tool Sync/Reimport Python Files", priority = 99)]
145+
public static void ReimportPythonFiles()
146+
{
147+
// Find all Python files (imported as TextAssets by PythonFileImporter)
148+
var pythonGuids = AssetDatabase.FindAssets("t:TextAsset", new[] { "Assets" })
149+
.Select(AssetDatabase.GUIDToAssetPath)
150+
.Where(path => path.EndsWith(".py", System.StringComparison.OrdinalIgnoreCase))
151+
.ToArray();
152+
153+
foreach (string path in pythonGuids)
154+
{
155+
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
156+
}
157+
158+
int count = pythonGuids.Length;
159+
McpLog.Info($"Reimported {count} Python files");
160+
AssetDatabase.Refresh();
161+
}
162+
163+
/// <summary>
164+
/// Menu item to manually trigger sync
165+
/// </summary>
166+
[MenuItem("Window/MCP For Unity/Tool Sync/Sync Python Tools", priority = 100)]
167+
public static void ManualSync()
168+
{
169+
McpLog.Info("Manually syncing Python tools...");
170+
SyncAllTools();
171+
}
172+
173+
/// <summary>
174+
/// Menu item to toggle auto-sync
175+
/// </summary>
176+
[MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", priority = 101)]
177+
public static void ToggleAutoSync()
178+
{
179+
SetAutoSyncEnabled(!IsAutoSyncEnabled());
180+
}
181+
182+
/// <summary>
183+
/// Validate menu item (shows checkmark when enabled)
184+
/// </summary>
185+
[MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", true, priority = 101)]
186+
public static bool ToggleAutoSyncValidate()
187+
{
188+
Menu.SetChecked("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", IsAutoSyncEnabled());
189+
return true;
190+
}
191+
}
192+
}

MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

MCPForUnity/Editor/Helpers/ServerInstaller.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public static void EnsureServerInstalled()
6565
// Copy the entire UnityMcpServer folder (parent of src)
6666
string embeddedRoot = Path.GetDirectoryName(embeddedSrc) ?? embeddedSrc; // go up from src to UnityMcpServer
6767
CopyDirectoryRecursive(embeddedRoot, destRoot);
68+
6869
// Write/refresh version file
6970
try { File.WriteAllText(Path.Combine(destSrc, VersionFileName), embeddedVer ?? "unknown"); } catch { }
7071
McpLog.Info($"Installed/updated server to {destRoot} (version {embeddedVer}).");
@@ -410,6 +411,7 @@ private static bool TryGetEmbeddedServerSource(out string srcPath)
410411
}
411412

412413
private static readonly string[] _skipDirs = { ".venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".git" };
414+
413415
private static void CopyDirectoryRecursive(string sourceDir, string destinationDir)
414416
{
415417
Directory.CreateDirectory(destinationDir);

MCPForUnity/Editor/Importers.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using UnityEngine;
2+
using UnityEditor.AssetImporters;
3+
using System.IO;
4+
5+
namespace MCPForUnity.Editor.Importers
6+
{
7+
/// <summary>
8+
/// Custom importer that allows Unity to recognize .py files as TextAssets.
9+
/// This enables Python files to be selected in the Inspector and used like any other text asset.
10+
/// </summary>
11+
[ScriptedImporter(1, "py")]
12+
public class PythonFileImporter : ScriptedImporter
13+
{
14+
public override void OnImportAsset(AssetImportContext ctx)
15+
{
16+
var textAsset = new TextAsset(File.ReadAllText(ctx.assetPath));
17+
ctx.AddObjectToAsset("main obj", textAsset);
18+
ctx.SetMainObject(textAsset);
19+
}
20+
}
21+
}

MCPForUnity/Editor/Importers/PythonFileImporter.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)